From 6c4bc1a1cc57651e84656a495319e897c87e8af1 Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Mon, 11 May 2026 20:51:10 -0700 Subject: [PATCH 01/24] Import cosmos3 codebase from cosmos3-internal Bring the full Cosmos3 project tree into this repository: the cosmos3 Python package, vllm-cosmos3, docs, examples, inputs, schemas, CI config, Docker setup, and tooling (pyproject, uv.lock, pre-commit, ruff, pyrefly, justfile). Existing root files (README.md, RELEASE.md, cosmos-logo-thumbnail.png) are preserved unchanged; .gitattributes includes an LFS override for the preserved logo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .agents/skills/cosmos3-codebase-nav/SKILL.md | 99 + .../skills/cosmos3-env-troubleshoot/SKILL.md | 106 + .agents/skills/cosmos3-inference/SKILL.md | 59 + .agents/skills/cosmos3-post-training/SKILL.md | 82 + .agents/skills/cosmos3-setup/SKILL.md | 62 + .claude/skills/cosmos3-codebase-nav/SKILL.md | 99 + .../skills/cosmos3-env-troubleshoot/SKILL.md | 106 + .claude/skills/cosmos3-inference/SKILL.md | 59 + .claude/skills/cosmos3-post-training/SKILL.md | 82 + .claude/skills/cosmos3-setup/SKILL.md | 62 + .config/rumdl.toml | 43 + .coveragerc | 32 + .dockerignore | 8 + .gitattributes | 28 + .github/ISSUE_TEMPLATE/bug_report.md | 65 + .github/workflows/pre-commit.yml | 31 + .gitignore | 205 + .gitleaks.toml | 19 + .pre-commit-config.yaml | 67 + .pytest.toml | 47 + .python-version | 1 + .ruff.toml | 36 + AGENTS.md | 72 + ATTRIBUTIONS.md | 57745 ++++++++++++++++ CHANGELOG.md | 28 + CONTRIBUTING.md | 121 + Dockerfile | 71 + LICENSE | 222 + ci/.link-check.json | 10 + ci/.markdown-toc-creator.toml | 19 + ci/.pre-commit-config-base.yaml | 26 + ci/license.txt | 14 + ci/uv_lock.sh | 22 + ci/uv_lock_script.sh | 23 + conftest.py | 282 + cosmos3/__init__.py | 15 + cosmos3/_src/imaginaire/__init__.py | 15 + cosmos3/_src/imaginaire/attention/README.md | 47 + cosmos3/_src/imaginaire/attention/__init__.py | 37 + cosmos3/_src/imaginaire/attention/backends.py | 418 + cosmos3/_src/imaginaire/attention/checks.py | 634 + .../_src/imaginaire/attention/docs/README.md | 10 + .../_src/imaginaire/attention/docs/apis.md | 28 + .../imaginaire/attention/docs/backends.md | 61 + .../imaginaire/attention/docs/features.md | 151 + .../imaginaire/attention/docs/multi-dim.md | 113 + .../imaginaire/attention/flash2/__init__.py | 85 + .../imaginaire/attention/flash2/checks.py | 130 + .../imaginaire/attention/flash2/functions.py | 198 + .../_src/imaginaire/attention/flash2/meta.py | 64 + .../_src/imaginaire/attention/flash2/stubs.py | 47 + .../imaginaire/attention/flash3/__init__.py | 74 + .../imaginaire/attention/flash3/checks.py | 138 + .../imaginaire/attention/flash3/functions.py | 213 + .../_src/imaginaire/attention/flash3/meta.py | 64 + .../_src/imaginaire/attention/flash3/stubs.py | 47 + cosmos3/_src/imaginaire/attention/frontend.py | 769 + cosmos3/_src/imaginaire/attention/masks.py | 61 + .../imaginaire/attention/natten/__init__.py | 127 + .../imaginaire/attention/natten/checks.py | 521 + .../imaginaire/attention/natten/functions.py | 420 + .../_src/imaginaire/attention/natten/meta.py | 67 + .../_src/imaginaire/attention/natten/stubs.py | 82 + .../imaginaire/attention/utils/__init__.py | 85 + .../imaginaire/attention/utils/determinism.py | 38 + .../imaginaire/attention/utils/environment.py | 134 + .../attention/utils/safe_ops/__init__.py | 26 + .../attention/utils/safe_ops/functools.py | 68 + .../attention/utils/safe_ops/log.py | 64 + .../imaginaire/attention/utils/version.py | 50 + cosmos3/_src/imaginaire/attention/varlen.py | 225 + cosmos3/_src/imaginaire/auxiliary/__init__.py | 14 + .../auxiliary/guardrail/__init__.py | 14 + .../auxiliary/guardrail/blocklist/__init__.py | 14 + .../guardrail/blocklist/blocklist.py | 248 + .../guardrail/blocklist/profile_blocklist.py | 59 + .../auxiliary/guardrail/blocklist/utils.py | 45 + .../auxiliary/guardrail/common/__init__.py | 14 + .../auxiliary/guardrail/common/core.py | 79 + .../auxiliary/guardrail/common/io_utils.py | 78 + .../auxiliary/guardrail/common/presets.py | 78 + .../guardrail/face_blur_filter/__init__.py | 14 + .../guardrail/face_blur_filter/blur_utils.py | 35 + .../face_blur_filter/face_blur_filter.py | 242 + .../face_blur_filter/retinaface_utils.py | 117 + .../guardrail/llamaGuard3/__init__.py | 14 + .../guardrail/llamaGuard3/categories.py | 31 + .../guardrail/llamaGuard3/llamaGuard3.py | 130 + .../guardrail/qwen3guard/__init__.py | 15 + .../guardrail/qwen3guard/categories.py | 23 + .../guardrail/qwen3guard/qwen3guard.py | 102 + .../video_content_safety_filter/__init__.py | 14 + .../video_content_safety_filter/model.py | 60 + .../video_content_safety_filter.py | 208 + .../vision_encoder.py | 42 + cosmos3/_src/imaginaire/callbacks/__init__.py | 15 + cosmos3/_src/imaginaire/callbacks/every_n.py | 85 + .../imaginaire/callbacks/image_grad_clip.py | 79 + .../_src/imaginaire/callbacks/manual_gc.py | 50 + .../_src/imaginaire/checkpointer/__init__.py | 15 + cosmos3/_src/imaginaire/checkpointer/base.py | 185 + cosmos3/_src/imaginaire/checkpointer/ddp.py | 457 + cosmos3/_src/imaginaire/checkpointer/dummy.py | 47 + .../imaginaire/checkpointer/s3_filesystem.py | 330 + .../imaginaire/checkpointer/safe_broadcast.py | 96 + cosmos3/_src/imaginaire/checkpointer/tp.py | 42 + .../_src/imaginaire/checkpointer/tp_ema.py | 94 + cosmos3/_src/imaginaire/config.py | 600 + .../_src/imaginaire/configs/lr_scheduler.py | 26 + cosmos3/_src/imaginaire/datasets/__init__.py | 15 + .../datasets/augmentors/merge_datadict.py | 54 + .../datasets/augmentors/v3_text_transforms.py | 213 + .../imaginaire/datasets/decoders/__init__.py | 14 + .../datasets/decoders/json_loader.py | 33 + .../datasets/decoders/pkl_loader.py | 33 + .../datasets/decoders/video_decoder.py | 781 + .../imaginaire/datasets/joint_training.py | 105 + .../_src/imaginaire/datasets/mock_dataset.py | 186 + .../datasets/webdataset/__init__.py | 14 + .../webdataset/augmentors/augmentor.py | 64 + .../webdataset/augmentors/geometry/camera.py | 184 + .../webdataset/augmentors/geometry/depth.py | 184 + .../augmentors/geometry/pointcloud.py | 390 + .../webdataset/augmentors/image/__init__.py | 14 + .../webdataset/augmentors/image/cropping.py | 118 + .../webdataset/augmentors/image/flip.py | 44 + .../webdataset/augmentors/image/misc.py | 63 + .../webdataset/augmentors/image/normalize.py | 48 + .../webdataset/augmentors/image/padding.py | 72 + .../webdataset/augmentors/image/resize.py | 187 + .../datasets/webdataset/config/schema.py | 96 + .../datasets/webdataset/dataloader.py | 72 + .../datasets/webdataset/decoders/__init__.py | 14 + .../datasets/webdataset/decoders/depth.py | 153 + .../datasets/webdataset/decoders/image.py | 45 + .../datasets/webdataset/decoders/pickle.py | 33 + .../webdataset/distributors/__init__.py | 26 + .../datasets/webdataset/distributors/basic.py | 158 + .../distributors/multi_aspect_ratio.py | 274 + .../distributors/multi_aspect_ratio_v2.py | 252 + .../weighted_multi_aspect_ratio.py | 168 + .../datasets/webdataset/utils/iterators.py | 617 + .../datasets/webdataset/utils/misc.py | 101 + .../datasets/webdataset/utils/stream.py | 1042 + .../unit_test/RetryingStreamDataLoaderTest.py | 231 + .../utils/unit_test/RetryingStreamMockTest.py | 452 + .../RetryingStreamStatsOverheadBenchmark.py | 1185 + .../RetryingStreamTarIteratorTest.py | 975 + .../utils/unit_test/mpi_rank_worker.py | 369 + .../datasets/webdataset/webdataset.py | 402 + .../datasets/webdataset/webdataset_ext.py | 117 + cosmos3/_src/imaginaire/flags.py | 98 + cosmos3/_src/imaginaire/flops/__init__.py | 30 + cosmos3/_src/imaginaire/flops/omni_mot.py | 498 + cosmos3/_src/imaginaire/flops/wan_vae.py | 134 + .../_src/imaginaire/functional/batch_ops.py | 61 + .../imaginaire/functional/lr_scheduler.py | 178 + .../_src/imaginaire/functional/multi_step.py | 60 + .../_src/imaginaire/functional/runge_kutta.py | 333 + .../_src/imaginaire/lazy_config/__init__.py | 70 + .../_src/imaginaire/lazy_config/file_io.py | 25 + .../imaginaire/lazy_config/instantiate.py | 120 + cosmos3/_src/imaginaire/lazy_config/lazy.py | 377 + .../_src/imaginaire/lazy_config/lazy_call.py | 81 + .../imaginaire/lazy_config/omegaconf_patch.py | 65 + .../_src/imaginaire/lazy_config/registry.py | 91 + cosmos3/_src/imaginaire/model.py | 130 + .../imaginaire/models/abstract_emb_model.py | 104 + cosmos3/_src/imaginaire/modules/camera.py | 663 + .../imaginaire/modules/denoiser_scaling.py | 64 + cosmos3/_src/imaginaire/modules/edm_sde.py | 43 + .../imaginaire/modules/image_embeddings.py | 763 + .../_src/imaginaire/modules/res_sampler.py | 287 + cosmos3/_src/imaginaire/serialization.py | 447 + cosmos3/_src/imaginaire/trainer.py | 447 + cosmos3/_src/imaginaire/utils/__init__.py | 15 + cosmos3/_src/imaginaire/utils/callback.py | 618 + .../_src/imaginaire/utils/checkpoint_db.py | 441 + cosmos3/_src/imaginaire/utils/checkpointer.py | 504 + cosmos3/_src/imaginaire/utils/cluster_env.py | 166 + .../_src/imaginaire/utils/config_helper.py | 219 + .../_src/imaginaire/utils/context_managers.py | 78 + .../_src/imaginaire/utils/context_parallel.py | 255 + cosmos3/_src/imaginaire/utils/count_params.py | 23 + cosmos3/_src/imaginaire/utils/dataloader.py | 105 + .../_src/imaginaire/utils/dataset_utils.py | 345 + .../imaginaire/utils/denoise_prediction.py | 28 + cosmos3/_src/imaginaire/utils/device.py | 152 + .../_src/imaginaire/utils/disabled_train.py | 22 + cosmos3/_src/imaginaire/utils/distributed.py | 491 + .../_src/imaginaire/utils/easy_io/__init__.py | 14 + .../utils/easy_io/backends/__init__.py | 38 + .../utils/easy_io/backends/auto_auth.py | 70 + .../utils/easy_io/backends/base_backend.py | 147 + .../utils/easy_io/backends/boto3_backend.py | 862 + .../utils/easy_io/backends/boto3_client.py | 640 + .../utils/easy_io/backends/http_backend.py | 198 + .../utils/easy_io/backends/local_backend.py | 599 + .../utils/easy_io/backends/msc_backend.py | 1075 + .../utils/easy_io/backends/registry_utils.py | 134 + .../_src/imaginaire/utils/easy_io/easy_io.py | 1117 + .../imaginaire/utils/easy_io/file_client.py | 459 + .../utils/easy_io/handlers/__init__.py | 29 + .../imaginaire/utils/easy_io/handlers/base.py | 44 + .../utils/easy_io/handlers/byte_handler.py | 39 + .../utils/easy_io/handlers/csv_handler.py | 42 + .../utils/easy_io/handlers/gzip_handler.py | 33 + .../easy_io/handlers/imageio_video_handler.py | 168 + .../utils/easy_io/handlers/json_handler.py | 49 + .../utils/easy_io/handlers/jsonl_handler.py | 80 + .../utils/easy_io/handlers/np_handler.py | 89 + .../utils/easy_io/handlers/pandas_handler.py | 31 + .../utils/easy_io/handlers/pickle_handler.py | 42 + .../utils/easy_io/handlers/pil_handler.py | 96 + .../utils/easy_io/handlers/registry_utils.py | 93 + .../utils/easy_io/handlers/tarfile_handler.py | 39 + .../utils/easy_io/handlers/torch_handler.py | 34 + .../easy_io/handlers/torchjit_handler.py | 34 + .../utils/easy_io/handlers/trimesh_handler.py | 36 + .../utils/easy_io/handlers/txt_handler.py | 34 + .../utils/easy_io/handlers/yaml_handler.py | 38 + cosmos3/_src/imaginaire/utils/ema.py | 366 + .../utils/embedding_concat_strategy.py | 25 + .../utils/env_parsers/cred_env_parser.py | 83 + .../env_parsers/customization_env_parser.py | 31 + .../utils/env_parsers/env_parser.py | 127 + .../utils/env_parsers/inference_env_parser.py | 45 + cosmos3/_src/imaginaire/utils/fsdp_helper.py | 159 + cosmos3/_src/imaginaire/utils/fused_adam.py | 383 + .../_src/imaginaire/utils/fused_nan_to_num.py | 24 + cosmos3/_src/imaginaire/utils/graph.py | 444 + .../imaginaire/utils/high_sigma_strategy.py | 28 + cosmos3/_src/imaginaire/utils/launch.py | 179 + cosmos3/_src/imaginaire/utils/log.py | 156 + cosmos3/_src/imaginaire/utils/misc.py | 689 + cosmos3/_src/imaginaire/utils/nsys_wrapper.sh | 19 + cosmos3/_src/imaginaire/utils/object_store.py | 417 + .../imaginaire/utils/optim_instantiate.py | 87 + .../imaginaire/utils/parallel_state_helper.py | 35 + cosmos3/_src/imaginaire/utils/primitives.py | 29 + cosmos3/_src/imaginaire/utils/profiling.py | 188 + cosmos3/_src/imaginaire/utils/progress_bar.py | 76 + cosmos3/_src/imaginaire/utils/registry.py | 160 + .../_src/imaginaire/utils/replace_bg_color.py | 124 + cosmos3/_src/imaginaire/utils/s3_utils.py | 139 + cosmos3/_src/imaginaire/utils/scheduler.py | 64 + .../imaginaire/utils/submit_job_helper.py | 37 + cosmos3/_src/imaginaire/utils/timer.py | 297 + cosmos3/_src/imaginaire/utils/tone_curve.py | 197 + cosmos3/_src/imaginaire/utils/training.py | 171 + cosmos3/_src/imaginaire/utils/validator.py | 512 + .../_src/imaginaire/utils/validator_params.py | 184 + cosmos3/_src/imaginaire/utils/wandb_util.py | 172 + cosmos3/_src/imaginaire/visualize/__init__.py | 16 + cosmos3/_src/imaginaire/visualize/img.py | 149 + cosmos3/_src/imaginaire/visualize/video.py | 96 + cosmos3/_src/vfm/__init__.py | 15 + cosmos3/_src/vfm/callbacks/__init__.py | 15 + .../_src/vfm/callbacks/compile_tokenizer.py | 118 + .../_src/vfm/callbacks/dataloader_state.py | 127 + .../_src/vfm/callbacks/dataloading_monitor.py | 538 + cosmos3/_src/vfm/callbacks/device_monitor.py | 201 + .../every_n_draw_audio_video_sample.py | 429 + .../_src/vfm/callbacks/every_n_draw_sample.py | 438 + .../callbacks/every_n_dur_fps_draw_sample.py | 333 + cosmos3/_src/vfm/callbacks/expert_heatmap.py | 120 + cosmos3/_src/vfm/callbacks/generation.py | 144 + cosmos3/_src/vfm/callbacks/grad_clip.py | 112 + cosmos3/_src/vfm/callbacks/heart_beat.py | 106 + cosmos3/_src/vfm/callbacks/hf_export.py | 471 + cosmos3/_src/vfm/callbacks/iter_speed.py | 120 + cosmos3/_src/vfm/callbacks/load_pretrained.py | 29 + cosmos3/_src/vfm/callbacks/low_precision.py | 53 + cosmos3/_src/vfm/callbacks/mfu.py | 318 + .../callbacks/moe_specialization_callback.py | 203 + .../vfm/callbacks/moe_stability_callback.py | 203 + cosmos3/_src/vfm/callbacks/norm_monitor.py | 345 + cosmos3/_src/vfm/callbacks/ofu.py | 293 + cosmos3/_src/vfm/callbacks/param_count.py | 50 + .../vfm/callbacks/sequence_packing_padding.py | 70 + .../_src/vfm/callbacks/sigma_loss_analysis.py | 356 + cosmos3/_src/vfm/callbacks/skip_nan_step.py | 96 + cosmos3/_src/vfm/callbacks/training_stats.py | 307 + cosmos3/_src/vfm/callbacks/vlm/grad_clip.py | 114 + cosmos3/_src/vfm/callbacks/wandb_log.py | 219 + cosmos3/_src/vfm/callbacks/wandb_log_eval.py | 153 + cosmos3/_src/vfm/checkpointer/__init__.py | 15 + cosmos3/_src/vfm/checkpointer/dcp.py | 861 + cosmos3/_src/vfm/conditioner.py | 578 + cosmos3/_src/vfm/configs/__init__.py | 15 + cosmos3/_src/vfm/configs/base/__init__.py | 15 + cosmos3/_src/vfm/configs/base/config.py | 133 + .../vfm/configs/base/defaults/__init__.py | 15 + .../vfm/configs/base/defaults/callbacks.py | 143 + .../vfm/configs/base/defaults/checkpointer.py | 132 + .../_src/vfm/configs/base/defaults/cluster.py | 58 + .../vfm/configs/base/defaults/conditioner.py | 194 + cosmos3/_src/vfm/configs/base/defaults/ema.py | 40 + .../_src/vfm/configs/base/defaults/model.py | 54 + .../vfm/configs/base/defaults/model_config.py | 389 + .../base/defaults/multiview_dataloader.py | 162 + .../vfm/configs/base/defaults/optimizer.py | 89 + .../vfm/configs/base/defaults/tokenizer.py | 198 + .../vfm/configs/base/defaults/unittest.py | 43 + cosmos3/_src/vfm/configs/base/defaults/vlm.py | 977 + cosmos3/_src/vfm/configs/base/vlm/__init__.py | 15 + cosmos3/_src/vfm/configs/base/vlm/config.py | 73 + .../vfm/configs/base/vlm/defaults/__init__.py | 15 + .../configs/base/vlm/defaults/callbacks.py | 126 + .../configs/base/vlm/defaults/checkpointer.py | 87 + .../vfm/configs/base/vlm/defaults/config.py | 74 + .../configs/base/vlm/defaults/dataloader.py | 92 + .../vlm/defaults/dataloader_weighted_url.py | 570 + .../vfm/configs/base/vlm/defaults/model.py | 38 + .../configs/base/vlm/defaults/optimizer.py | 65 + .../vfm/configs/base/vlm/defaults/training.py | 173 + .../configs/base/vlm/defaults/vlm_policy.py | 170 + .../configs/base/vlm/experiment/__init__.py | 15 + .../experiment/pre_exp012_phase2_vlm_smoke.py | 105 + .../configs/base/vlm/experiment/pre_exp01x.py | 854 + .../vfm/configs/base/vlm/experiment/utils.py | 28 + cosmos3/_src/vfm/datasets/__init__.py | 15 + .../datasets/action/action_normalization.py | 61 + .../_src/vfm/datasets/action/action_spec.py | 247 + .../_src/vfm/datasets/action/dataloaders.py | 160 + .../_src/vfm/datasets/action/domain_utils.py | 47 + .../vfm/datasets/action/json_formatter.py | 306 + .../vfm/datasets/action/libero_dataset.py | 623 + .../vfm/datasets/action/libero_pose_utils.py | 81 + ...bero_native_frame_wise_relative_rot6d.json | 37 + .../_src/vfm/datasets/action/pose_utils.py | 759 + .../_src/vfm/datasets/action/transforms.py | 681 + .../vfm/datasets/action/unified_dataset.py | 594 + .../vfm/datasets/action/viewpoint_utils.py | 126 + .../_src/vfm/datasets/augmentors/__init__.py | 15 + .../augmentors/append_fps_frames_for_image.py | 37 + .../vfm/datasets/augmentors/audio_caption.py | 107 + .../vfm/datasets/augmentors/audio_parsing.py | 149 + .../vfm/datasets/augmentors/caption_filter.py | 173 + .../_src/vfm/datasets/augmentors/cropping.py | 92 + .../duration_fps_text_timestamps.py | 135 + .../augmentors/idle_frames_text_info.py | 192 + .../augmentors/image_editing_transform.py | 382 + .../augmentors/image_resolution_filter.py | 56 + .../augmentors/interleaved_image_transform.py | 278 + .../augmentors/interleaved_video_parsing.py | 223 + .../vfm/datasets/augmentors/merge_datadict.py | 79 + .../vfm/datasets/augmentors/pkl_to_media.py | 360 + .../augmentors/resolution_text_info.py | 134 + .../vfm/datasets/augmentors/sequence_plan.py | 142 + .../augmentors/sound_sequence_plan.py | 107 + .../vfm/datasets/augmentors/text_tokenizer.py | 139 + .../augmentors/text_transforms_for_image.py | 295 + .../augmentors/text_transforms_for_video.py | 511 + .../transfer_control_input/__init__.py | 20 + .../augmentors/transfer_control_input/blur.py | 279 + .../transfer_control_input/control_input.py | 672 + .../transfer_control_input/fast_blur.py | 104 + .../augmentors/transfer_control_input/seg.py | 183 + .../augmentors/transfer_control_transform.py | 329 + .../vfm/datasets/augmentors/video_parsing.py | 853 + cosmos3/_src/vfm/datasets/joint_dataloader.py | 870 + .../vfm/datasets/local_datasets/__init__.py | 15 + .../vfm/datasets/local_datasets/helper.py | 265 + .../datasets/local_datasets/sft_dataset.py | 691 + cosmos3/_src/vfm/datasets/sequence_packing.py | 2987 + cosmos3/_src/vfm/datasets/utils.py | 181 + cosmos3/_src/vfm/diffusion/__init__.py | 15 + cosmos3/_src/vfm/diffusion/rectified_flow.py | 183 + .../_src/vfm/diffusion/samplers/__init__.py | 15 + cosmos3/_src/vfm/diffusion/samplers/edm.py | 295 + .../_src/vfm/diffusion/samplers/fixed_step.py | 143 + .../diffusion/samplers/fm_solvers_unipc.py | 768 + cosmos3/_src/vfm/diffusion/samplers/unipc.py | 124 + cosmos3/_src/vfm/diffusion/samplers/utils.py | 82 + cosmos3/_src/vfm/evaluation/__init__.py | 15 + .../_src/vfm/evaluation/action/__init__.py | 15 + .../vfm/evaluation/action/libero/__init__.py | 15 + .../action/libero/closed_loop_eval.py | 1015 + .../libero/dataset_reply_action_server.py | 665 + cosmos3/_src/vfm/models/__init__.py | 15 + cosmos3/_src/vfm/models/hf_model.py | 336 + cosmos3/_src/vfm/models/llm/__init__.py | 15 + cosmos3/_src/vfm/models/llm/qwen3/__init__.py | 15 + .../models/llm/qwen3/configs/Qwen3-0.6B.json | 30 + .../vfm/models/llm/qwen3/configs/__init__.py | 15 + .../models/llm/qwen3/configuration_qwen3.py | 259 + cosmos3/_src/vfm/models/llm/qwen3/qwen3.py | 751 + .../_src/vfm/models/llm/qwen3/test_qwen3.py | 976 + cosmos3/_src/vfm/models/mot/__init__.py | 15 + cosmos3/_src/vfm/models/mot/attention.py | 445 + .../vfm/models/mot/context_parallel_utils.py | 427 + .../vfm/models/mot/cosmos3_vfm_network.py | 1118 + .../vfm/models/mot/domain_aware_linear.py | 90 + .../vfm/models/mot/dot_product_attention.py | 452 + cosmos3/_src/vfm/models/mot/modeling_utils.py | 407 + .../vfm/models/mot/parallelize_unified_mot.py | 89 + .../vfm/models/mot/parallelize_vfm_network.py | 172 + .../vfm/models/mot/qwen3_vl_unified_mot.py | 34 + .../vfm/models/mot/unified_3dmrope_utils.py | 206 + cosmos3/_src/vfm/models/mot/unified_mot.py | 1041 + cosmos3/_src/vfm/models/omni_mot_model.py | 2924 + cosmos3/_src/vfm/models/parallelize_vlm.py | 98 + cosmos3/_src/vfm/models/utils/__init__.py | 15 + .../vfm/models/utils/data_and_condition.py | 155 + cosmos3/_src/vfm/models/utils/dcp_loader.py | 132 + .../vfm/models/utils/load_balancing_loss.py | 82 + cosmos3/_src/vfm/models/utils/memory.py | 115 + .../vfm/models/utils/safetensors_loader.py | 966 + cosmos3/_src/vfm/models/utils/taylorseer.py | 180 + cosmos3/_src/vfm/models/vlm/__init__.py | 15 + .../vlm/nemotron_3_dense_vl/__init__.py | 15 + .../configs/Nemotron-2B-Dense-VL.json | 31 + .../configuration_nemotron_3_dense_vl.py | 97 + .../nemotron_3_dense_vl.py | 165 + .../_src/vfm/models/vlm/qwen3_vl/__init__.py | 15 + .../configs/Qwen3-VL-2B-Instruct.json | 63 + .../configs/Qwen3-VL-32B-Instruct.json | 62 + .../configs/Qwen3-VL-4B-Instruct.json | 63 + .../configs/Qwen3-VL-8B-Instruct.json | 62 + .../models/vlm/qwen3_vl/configs/__init__.py | 15 + .../vlm/qwen3_vl/configuration_qwen3_vl.py | 298 + .../_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py | 1651 + cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py | 348 + .../vlm/qwen3_vl/video_processing_qwen3_vl.py | 297 + .../vfm/models/vlm/qwen3_vl_moe/__init__.py | 15 + .../configs/Qwen3-VL-235B-A22B-Instruct.json | 68 + .../configs/Qwen3-VL-30B-A3B-Instruct.json | 68 + .../vlm/qwen3_vl_moe/configs/__init__.py | 15 + .../configuration_qwen3_vl_moe.py | 330 + .../_src/vfm/models/vlm/qwen3_vl_moe/moe.py | 261 + .../vfm/models/vlm/qwen3_vl_moe/moe_bench.py | 455 + .../models/vlm/qwen3_vl_moe/moe_kernels.py | 216 + .../models/vlm/qwen3_vl_moe/qwen3_vl_moe.py | 2041 + cosmos3/_src/vfm/models/vlm_model.py | 552 + cosmos3/_src/vfm/tokenizers/__init__.py | 15 + cosmos3/_src/vfm/tokenizers/interface.py | 236 + .../_src/vfm/tokenizers/tokenization_qwen2.py | 340 + .../vfm/tokenizers/wan2pt2_vae_4x16x16.py | 1680 + cosmos3/_src/vfm/utils/__init__.py | 15 + cosmos3/_src/vfm/utils/context_parallel.py | 193 + cosmos3/_src/vfm/utils/data_utils.py | 112 + cosmos3/_src/vfm/utils/dtensor_helper.py | 66 + cosmos3/_src/vfm/utils/flash_attn.py | 57 + cosmos3/_src/vfm/utils/fused_adam.py | 385 + cosmos3/_src/vfm/utils/loss.py | 110 + cosmos3/_src/vfm/utils/misc.py | 72 + cosmos3/_src/vfm/utils/model_loader.py | 226 + cosmos3/_src/vfm/utils/model_weights_stats.py | 64 + cosmos3/_src/vfm/utils/monkey_patch.py | 215 + cosmos3/_src/vfm/utils/optimizer.py | 426 + cosmos3/_src/vfm/utils/parallelism.py | 273 + cosmos3/_src/vfm/utils/rand_state.py | 167 + .../_src/vfm/utils/tokenizer_benchmarking.py | 35 + cosmos3/_src/vfm/utils/vlm/__init__.py | 15 + cosmos3/_src/vfm/utils/vlm/optimizer.py | 336 + .../utils/vlm/pretrained_models_downloader.py | 248 + cosmos3/_test.py | 268 + cosmos3/_test/autoregressive.sh | 21 + cosmos3/_test/distilled.sh | 20 + cosmos3/_test/latency.sh | 21 + cosmos3/_test/omni-super.sh | 21 + cosmos3/_test/omni.sh | 21 + cosmos3/_test/omni_param.sh | 16 + cosmos3/_test/sft.sh | 44 + cosmos3/action.py | 166 + cosmos3/args.py | 832 + cosmos3/args_test.py | 212 + cosmos3/common/__init__.py | 15 + cosmos3/common/args.py | 862 + cosmos3/common/args_test.py | 130 + cosmos3/common/checkpoints.py | 82 + cosmos3/common/config.py | 460 + cosmos3/common/config_test.py | 147 + cosmos3/common/inference.py | 243 + cosmos3/common/inference_test.py | 35 + cosmos3/common/init.py | 259 + .../experiment/action_policy_sft_8b.yaml | 591 + .../experiment/mixed_modality_sft_8b.yaml | 561 + cosmos3/dataset_samples.py | 144 + .../forward_dynamics/sample_args.json | 24 + cosmos3/defaults/image2video/sample_args.json | 21 + .../inverse_dynamics/sample_args.json | 24 + cosmos3/defaults/policy/sample_args.json | 24 + cosmos3/defaults/prompt_upsampler.txt | 69 + cosmos3/defaults/text2image/sample_args.json | 21 + cosmos3/defaults/text2video/sample_args.json | 21 + cosmos3/defaults/video2video/sample_args.json | 21 + cosmos3/defaults/video_captioner.txt | 67 + cosmos3/fixtures/__init__.py | 15 + cosmos3/fixtures/args.py | 92 + cosmos3/fixtures/script.py | 336 + cosmos3/flags.py | 31 + cosmos3/inference.py | 1129 + cosmos3/model.py | 168 + cosmos3/model_test.py | 39 + cosmos3/ray/__init__.py | 15 + cosmos3/ray/configs/latency.yaml | 29 + cosmos3/ray/configs/throughput.yaml | 29 + cosmos3/ray/gradio.py | 274 + cosmos3/ray/serve.py | 277 + cosmos3/ray/serve_test.py | 157 + cosmos3/ray/submit.py | 111 + cosmos3/scripts/__init__.py | 15 + cosmos3/scripts/_test.py | 34 + cosmos3/scripts/_test/convert_model_to_dcp.sh | 43 + .../_test/convert_model_to_diffusers.sh | 35 + cosmos3/scripts/_test/export_config.sh | 19 + cosmos3/scripts/_test/export_model.sh | 26 + cosmos3/scripts/action_policy_server.py | 1324 + cosmos3/scripts/caption_from_video.py | 270 + cosmos3/scripts/captions_to_sft_jsonl.py | 197 + cosmos3/scripts/convert_model_to_dcp.py | 75 + cosmos3/scripts/eval_utils.py | 146 + cosmos3/scripts/export_config.py | 68 + cosmos3/scripts/export_model.py | 155 + cosmos3/scripts/export_schemas.py | 86 + cosmos3/scripts/export_schemas_test.py | 47 + cosmos3/scripts/hydra.py | 33 + cosmos3/scripts/inference.py | 88 + cosmos3/scripts/prefetch_hf_checkpoints.py | 53 + cosmos3/scripts/train.py | 156 + cosmos3/scripts/upsample_prompts.py | 168 + cosmos3/vision.py | 219 + docker/ci.Dockerfile | 63 + docker/entrypoint.sh | 24 + docker/nightly.Dockerfile | 56 + docs/Cosmos3.pdf | Bin 0 -> 617527 bytes docs/action_policy_closed_loop_eval.md | 409 + docs/faq.md | 224 + docs/gallery.md | 67 + docs/inference.md | 301 + docs/inference_online.md | 114 + docs/prompting.md | 97 + docs/setup.md | 246 + docs/training.md | 325 + examples/_test.py | 35 + examples/_test/inference.sh | 16 + examples/_test/inference_pipeline.sh | 16 + examples/inference.py | 82 + examples/inference_pipeline.py | 58 + .../action_forward_dynamics_camera_0.json | 14 + .../action_forward_dynamics_camera_1.json | 14 + .../omni/action_forward_dynamics_robot.json | 15 + inputs/omni/action_inverse_dynamics_av.json | 18 + inputs/omni/action_policy_robot.json | 19 + inputs/omni/i2v.json | 4 + inputs/omni/t2i.json | 4 + inputs/omni/t2v.json | 3 + inputs/t2v_long_prompts.jsonl | 21 + justfile | 232 + pyproject.toml | 341 + pyrefly.toml | 108 + schemas/OmniSampleOverrides.schema.json | 567 + schemas/OmniSampleOverrides.yaml | 97 + schemas/OmniSetupOverrides.schema.json | 1004 + schemas/OmniSetupOverrides.yaml | 168 + uv.lock | 14279 ++++ vllm-cosmos3/README.md | 14 + vllm-cosmos3/pyproject.toml | 28 + vllm-cosmos3/uv.lock | 8 + vllm-cosmos3/vllm_cosmos3/__init__.py | 29 + vllm-cosmos3/vllm_cosmos3/model.py | 89 + 563 files changed, 185289 insertions(+) create mode 100644 .agents/skills/cosmos3-codebase-nav/SKILL.md create mode 100644 .agents/skills/cosmos3-env-troubleshoot/SKILL.md create mode 100644 .agents/skills/cosmos3-inference/SKILL.md create mode 100644 .agents/skills/cosmos3-post-training/SKILL.md create mode 100644 .agents/skills/cosmos3-setup/SKILL.md create mode 100644 .claude/skills/cosmos3-codebase-nav/SKILL.md create mode 100644 .claude/skills/cosmos3-env-troubleshoot/SKILL.md create mode 100644 .claude/skills/cosmos3-inference/SKILL.md create mode 100644 .claude/skills/cosmos3-post-training/SKILL.md create mode 100644 .claude/skills/cosmos3-setup/SKILL.md create mode 100644 .config/rumdl.toml create mode 100644 .coveragerc create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .gitignore create mode 100644 .gitleaks.toml create mode 100644 .pre-commit-config.yaml create mode 100644 .pytest.toml create mode 100644 .python-version create mode 100644 .ruff.toml create mode 100644 AGENTS.md create mode 100644 ATTRIBUTIONS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 ci/.link-check.json create mode 100644 ci/.markdown-toc-creator.toml create mode 100644 ci/.pre-commit-config-base.yaml create mode 100644 ci/license.txt create mode 100755 ci/uv_lock.sh create mode 100755 ci/uv_lock_script.sh create mode 100644 conftest.py create mode 100644 cosmos3/__init__.py create mode 100644 cosmos3/_src/imaginaire/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/README.md create mode 100644 cosmos3/_src/imaginaire/attention/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/backends.py create mode 100644 cosmos3/_src/imaginaire/attention/checks.py create mode 100644 cosmos3/_src/imaginaire/attention/docs/README.md create mode 100644 cosmos3/_src/imaginaire/attention/docs/apis.md create mode 100644 cosmos3/_src/imaginaire/attention/docs/backends.md create mode 100644 cosmos3/_src/imaginaire/attention/docs/features.md create mode 100644 cosmos3/_src/imaginaire/attention/docs/multi-dim.md create mode 100644 cosmos3/_src/imaginaire/attention/flash2/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/flash2/checks.py create mode 100644 cosmos3/_src/imaginaire/attention/flash2/functions.py create mode 100644 cosmos3/_src/imaginaire/attention/flash2/meta.py create mode 100644 cosmos3/_src/imaginaire/attention/flash2/stubs.py create mode 100644 cosmos3/_src/imaginaire/attention/flash3/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/flash3/checks.py create mode 100644 cosmos3/_src/imaginaire/attention/flash3/functions.py create mode 100644 cosmos3/_src/imaginaire/attention/flash3/meta.py create mode 100644 cosmos3/_src/imaginaire/attention/flash3/stubs.py create mode 100644 cosmos3/_src/imaginaire/attention/frontend.py create mode 100644 cosmos3/_src/imaginaire/attention/masks.py create mode 100644 cosmos3/_src/imaginaire/attention/natten/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/natten/checks.py create mode 100644 cosmos3/_src/imaginaire/attention/natten/functions.py create mode 100644 cosmos3/_src/imaginaire/attention/natten/meta.py create mode 100644 cosmos3/_src/imaginaire/attention/natten/stubs.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/determinism.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/environment.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py create mode 100644 cosmos3/_src/imaginaire/attention/utils/version.py create mode 100644 cosmos3/_src/imaginaire/attention/varlen.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py create mode 100644 cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py create mode 100644 cosmos3/_src/imaginaire/callbacks/__init__.py create mode 100644 cosmos3/_src/imaginaire/callbacks/every_n.py create mode 100644 cosmos3/_src/imaginaire/callbacks/image_grad_clip.py create mode 100644 cosmos3/_src/imaginaire/callbacks/manual_gc.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/__init__.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/base.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/ddp.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/dummy.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/tp.py create mode 100644 cosmos3/_src/imaginaire/checkpointer/tp_ema.py create mode 100644 cosmos3/_src/imaginaire/config.py create mode 100644 cosmos3/_src/imaginaire/configs/lr_scheduler.py create mode 100644 cosmos3/_src/imaginaire/datasets/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py create mode 100644 cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py create mode 100644 cosmos3/_src/imaginaire/datasets/decoders/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/decoders/json_loader.py create mode 100644 cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py create mode 100644 cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py create mode 100644 cosmos3/_src/imaginaire/datasets/joint_training.py create mode 100644 cosmos3/_src/imaginaire/datasets/mock_dataset.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py create mode 100644 cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py create mode 100644 cosmos3/_src/imaginaire/flags.py create mode 100644 cosmos3/_src/imaginaire/flops/__init__.py create mode 100644 cosmos3/_src/imaginaire/flops/omni_mot.py create mode 100644 cosmos3/_src/imaginaire/flops/wan_vae.py create mode 100644 cosmos3/_src/imaginaire/functional/batch_ops.py create mode 100644 cosmos3/_src/imaginaire/functional/lr_scheduler.py create mode 100644 cosmos3/_src/imaginaire/functional/multi_step.py create mode 100644 cosmos3/_src/imaginaire/functional/runge_kutta.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/__init__.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/file_io.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/instantiate.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/lazy.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/lazy_call.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py create mode 100644 cosmos3/_src/imaginaire/lazy_config/registry.py create mode 100644 cosmos3/_src/imaginaire/model.py create mode 100644 cosmos3/_src/imaginaire/models/abstract_emb_model.py create mode 100644 cosmos3/_src/imaginaire/modules/camera.py create mode 100644 cosmos3/_src/imaginaire/modules/denoiser_scaling.py create mode 100644 cosmos3/_src/imaginaire/modules/edm_sde.py create mode 100644 cosmos3/_src/imaginaire/modules/image_embeddings.py create mode 100644 cosmos3/_src/imaginaire/modules/res_sampler.py create mode 100644 cosmos3/_src/imaginaire/serialization.py create mode 100644 cosmos3/_src/imaginaire/trainer.py create mode 100644 cosmos3/_src/imaginaire/utils/__init__.py create mode 100644 cosmos3/_src/imaginaire/utils/callback.py create mode 100644 cosmos3/_src/imaginaire/utils/checkpoint_db.py create mode 100644 cosmos3/_src/imaginaire/utils/checkpointer.py create mode 100644 cosmos3/_src/imaginaire/utils/cluster_env.py create mode 100644 cosmos3/_src/imaginaire/utils/config_helper.py create mode 100644 cosmos3/_src/imaginaire/utils/context_managers.py create mode 100644 cosmos3/_src/imaginaire/utils/context_parallel.py create mode 100644 cosmos3/_src/imaginaire/utils/count_params.py create mode 100644 cosmos3/_src/imaginaire/utils/dataloader.py create mode 100644 cosmos3/_src/imaginaire/utils/dataset_utils.py create mode 100644 cosmos3/_src/imaginaire/utils/denoise_prediction.py create mode 100644 cosmos3/_src/imaginaire/utils/device.py create mode 100644 cosmos3/_src/imaginaire/utils/disabled_train.py create mode 100644 cosmos3/_src/imaginaire/utils/distributed.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/__init__.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/easy_io.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/file_client.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py create mode 100644 cosmos3/_src/imaginaire/utils/ema.py create mode 100644 cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py create mode 100644 cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py create mode 100644 cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py create mode 100644 cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py create mode 100644 cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py create mode 100644 cosmos3/_src/imaginaire/utils/fsdp_helper.py create mode 100644 cosmos3/_src/imaginaire/utils/fused_adam.py create mode 100644 cosmos3/_src/imaginaire/utils/fused_nan_to_num.py create mode 100644 cosmos3/_src/imaginaire/utils/graph.py create mode 100644 cosmos3/_src/imaginaire/utils/high_sigma_strategy.py create mode 100644 cosmos3/_src/imaginaire/utils/launch.py create mode 100644 cosmos3/_src/imaginaire/utils/log.py create mode 100644 cosmos3/_src/imaginaire/utils/misc.py create mode 100755 cosmos3/_src/imaginaire/utils/nsys_wrapper.sh create mode 100644 cosmos3/_src/imaginaire/utils/object_store.py create mode 100644 cosmos3/_src/imaginaire/utils/optim_instantiate.py create mode 100644 cosmos3/_src/imaginaire/utils/parallel_state_helper.py create mode 100644 cosmos3/_src/imaginaire/utils/primitives.py create mode 100644 cosmos3/_src/imaginaire/utils/profiling.py create mode 100644 cosmos3/_src/imaginaire/utils/progress_bar.py create mode 100644 cosmos3/_src/imaginaire/utils/registry.py create mode 100644 cosmos3/_src/imaginaire/utils/replace_bg_color.py create mode 100644 cosmos3/_src/imaginaire/utils/s3_utils.py create mode 100644 cosmos3/_src/imaginaire/utils/scheduler.py create mode 100644 cosmos3/_src/imaginaire/utils/submit_job_helper.py create mode 100644 cosmos3/_src/imaginaire/utils/timer.py create mode 100644 cosmos3/_src/imaginaire/utils/tone_curve.py create mode 100644 cosmos3/_src/imaginaire/utils/training.py create mode 100644 cosmos3/_src/imaginaire/utils/validator.py create mode 100644 cosmos3/_src/imaginaire/utils/validator_params.py create mode 100644 cosmos3/_src/imaginaire/utils/wandb_util.py create mode 100644 cosmos3/_src/imaginaire/visualize/__init__.py create mode 100644 cosmos3/_src/imaginaire/visualize/img.py create mode 100644 cosmos3/_src/imaginaire/visualize/video.py create mode 100644 cosmos3/_src/vfm/__init__.py create mode 100644 cosmos3/_src/vfm/callbacks/__init__.py create mode 100644 cosmos3/_src/vfm/callbacks/compile_tokenizer.py create mode 100644 cosmos3/_src/vfm/callbacks/dataloader_state.py create mode 100644 cosmos3/_src/vfm/callbacks/dataloading_monitor.py create mode 100644 cosmos3/_src/vfm/callbacks/device_monitor.py create mode 100644 cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py create mode 100644 cosmos3/_src/vfm/callbacks/every_n_draw_sample.py create mode 100644 cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py create mode 100644 cosmos3/_src/vfm/callbacks/expert_heatmap.py create mode 100644 cosmos3/_src/vfm/callbacks/generation.py create mode 100644 cosmos3/_src/vfm/callbacks/grad_clip.py create mode 100644 cosmos3/_src/vfm/callbacks/heart_beat.py create mode 100644 cosmos3/_src/vfm/callbacks/hf_export.py create mode 100644 cosmos3/_src/vfm/callbacks/iter_speed.py create mode 100644 cosmos3/_src/vfm/callbacks/load_pretrained.py create mode 100644 cosmos3/_src/vfm/callbacks/low_precision.py create mode 100644 cosmos3/_src/vfm/callbacks/mfu.py create mode 100644 cosmos3/_src/vfm/callbacks/moe_specialization_callback.py create mode 100644 cosmos3/_src/vfm/callbacks/moe_stability_callback.py create mode 100644 cosmos3/_src/vfm/callbacks/norm_monitor.py create mode 100644 cosmos3/_src/vfm/callbacks/ofu.py create mode 100644 cosmos3/_src/vfm/callbacks/param_count.py create mode 100644 cosmos3/_src/vfm/callbacks/sequence_packing_padding.py create mode 100644 cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py create mode 100644 cosmos3/_src/vfm/callbacks/skip_nan_step.py create mode 100644 cosmos3/_src/vfm/callbacks/training_stats.py create mode 100644 cosmos3/_src/vfm/callbacks/vlm/grad_clip.py create mode 100644 cosmos3/_src/vfm/callbacks/wandb_log.py create mode 100644 cosmos3/_src/vfm/callbacks/wandb_log_eval.py create mode 100644 cosmos3/_src/vfm/checkpointer/__init__.py create mode 100644 cosmos3/_src/vfm/checkpointer/dcp.py create mode 100644 cosmos3/_src/vfm/conditioner.py create mode 100644 cosmos3/_src/vfm/configs/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/config.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/callbacks.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/checkpointer.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/cluster.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/conditioner.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/ema.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/model.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/model_config.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/optimizer.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/tokenizer.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/unittest.py create mode 100644 cosmos3/_src/vfm/configs/base/defaults/vlm.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/config.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/config.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/model.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/training.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py create mode 100644 cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py create mode 100644 cosmos3/_src/vfm/datasets/__init__.py create mode 100644 cosmos3/_src/vfm/datasets/action/action_normalization.py create mode 100644 cosmos3/_src/vfm/datasets/action/action_spec.py create mode 100644 cosmos3/_src/vfm/datasets/action/dataloaders.py create mode 100644 cosmos3/_src/vfm/datasets/action/domain_utils.py create mode 100644 cosmos3/_src/vfm/datasets/action/json_formatter.py create mode 100644 cosmos3/_src/vfm/datasets/action/libero_dataset.py create mode 100644 cosmos3/_src/vfm/datasets/action/libero_pose_utils.py create mode 100644 cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json create mode 100644 cosmos3/_src/vfm/datasets/action/pose_utils.py create mode 100644 cosmos3/_src/vfm/datasets/action/transforms.py create mode 100644 cosmos3/_src/vfm/datasets/action/unified_dataset.py create mode 100644 cosmos3/_src/vfm/datasets/action/viewpoint_utils.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/__init__.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/audio_caption.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/caption_filter.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/cropping.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py create mode 100644 cosmos3/_src/vfm/datasets/augmentors/video_parsing.py create mode 100644 cosmos3/_src/vfm/datasets/joint_dataloader.py create mode 100644 cosmos3/_src/vfm/datasets/local_datasets/__init__.py create mode 100644 cosmos3/_src/vfm/datasets/local_datasets/helper.py create mode 100644 cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py create mode 100644 cosmos3/_src/vfm/datasets/sequence_packing.py create mode 100644 cosmos3/_src/vfm/datasets/utils.py create mode 100644 cosmos3/_src/vfm/diffusion/__init__.py create mode 100644 cosmos3/_src/vfm/diffusion/rectified_flow.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/__init__.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/edm.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/fixed_step.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/unipc.py create mode 100644 cosmos3/_src/vfm/diffusion/samplers/utils.py create mode 100644 cosmos3/_src/vfm/evaluation/__init__.py create mode 100644 cosmos3/_src/vfm/evaluation/action/__init__.py create mode 100644 cosmos3/_src/vfm/evaluation/action/libero/__init__.py create mode 100644 cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py create mode 100644 cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py create mode 100644 cosmos3/_src/vfm/models/__init__.py create mode 100644 cosmos3/_src/vfm/models/hf_model.py create mode 100644 cosmos3/_src/vfm/models/llm/__init__.py create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/__init__.py create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/qwen3.py create mode 100644 cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py create mode 100644 cosmos3/_src/vfm/models/mot/__init__.py create mode 100644 cosmos3/_src/vfm/models/mot/attention.py create mode 100644 cosmos3/_src/vfm/models/mot/context_parallel_utils.py create mode 100644 cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py create mode 100644 cosmos3/_src/vfm/models/mot/domain_aware_linear.py create mode 100644 cosmos3/_src/vfm/models/mot/dot_product_attention.py create mode 100644 cosmos3/_src/vfm/models/mot/modeling_utils.py create mode 100644 cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py create mode 100644 cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py create mode 100644 cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py create mode 100644 cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py create mode 100644 cosmos3/_src/vfm/models/mot/unified_mot.py create mode 100644 cosmos3/_src/vfm/models/omni_mot_model.py create mode 100644 cosmos3/_src/vfm/models/parallelize_vlm.py create mode 100644 cosmos3/_src/vfm/models/utils/__init__.py create mode 100644 cosmos3/_src/vfm/models/utils/data_and_condition.py create mode 100644 cosmos3/_src/vfm/models/utils/dcp_loader.py create mode 100644 cosmos3/_src/vfm/models/utils/load_balancing_loss.py create mode 100644 cosmos3/_src/vfm/models/utils/memory.py create mode 100644 cosmos3/_src/vfm/models/utils/safetensors_loader.py create mode 100644 cosmos3/_src/vfm/models/utils/taylorseer.py create mode 100644 cosmos3/_src/vfm/models/vlm/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json create mode 100644 cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py create mode 100644 cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py create mode 100644 cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py create mode 100644 cosmos3/_src/vfm/models/vlm_model.py create mode 100644 cosmos3/_src/vfm/tokenizers/__init__.py create mode 100644 cosmos3/_src/vfm/tokenizers/interface.py create mode 100644 cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py create mode 100644 cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py create mode 100644 cosmos3/_src/vfm/utils/__init__.py create mode 100644 cosmos3/_src/vfm/utils/context_parallel.py create mode 100644 cosmos3/_src/vfm/utils/data_utils.py create mode 100644 cosmos3/_src/vfm/utils/dtensor_helper.py create mode 100644 cosmos3/_src/vfm/utils/flash_attn.py create mode 100644 cosmos3/_src/vfm/utils/fused_adam.py create mode 100644 cosmos3/_src/vfm/utils/loss.py create mode 100644 cosmos3/_src/vfm/utils/misc.py create mode 100644 cosmos3/_src/vfm/utils/model_loader.py create mode 100644 cosmos3/_src/vfm/utils/model_weights_stats.py create mode 100644 cosmos3/_src/vfm/utils/monkey_patch.py create mode 100644 cosmos3/_src/vfm/utils/optimizer.py create mode 100644 cosmos3/_src/vfm/utils/parallelism.py create mode 100644 cosmos3/_src/vfm/utils/rand_state.py create mode 100644 cosmos3/_src/vfm/utils/tokenizer_benchmarking.py create mode 100644 cosmos3/_src/vfm/utils/vlm/__init__.py create mode 100644 cosmos3/_src/vfm/utils/vlm/optimizer.py create mode 100644 cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py create mode 100644 cosmos3/_test.py create mode 100644 cosmos3/_test/autoregressive.sh create mode 100644 cosmos3/_test/distilled.sh create mode 100644 cosmos3/_test/latency.sh create mode 100644 cosmos3/_test/omni-super.sh create mode 100644 cosmos3/_test/omni.sh create mode 100644 cosmos3/_test/omni_param.sh create mode 100644 cosmos3/_test/sft.sh create mode 100644 cosmos3/action.py create mode 100644 cosmos3/args.py create mode 100644 cosmos3/args_test.py create mode 100644 cosmos3/common/__init__.py create mode 100644 cosmos3/common/args.py create mode 100644 cosmos3/common/args_test.py create mode 100644 cosmos3/common/checkpoints.py create mode 100644 cosmos3/common/config.py create mode 100644 cosmos3/common/config_test.py create mode 100644 cosmos3/common/inference.py create mode 100644 cosmos3/common/inference_test.py create mode 100644 cosmos3/common/init.py create mode 100644 cosmos3/configs/experiment/action_policy_sft_8b.yaml create mode 100644 cosmos3/configs/experiment/mixed_modality_sft_8b.yaml create mode 100644 cosmos3/dataset_samples.py create mode 100644 cosmos3/defaults/forward_dynamics/sample_args.json create mode 100644 cosmos3/defaults/image2video/sample_args.json create mode 100644 cosmos3/defaults/inverse_dynamics/sample_args.json create mode 100644 cosmos3/defaults/policy/sample_args.json create mode 100644 cosmos3/defaults/prompt_upsampler.txt create mode 100644 cosmos3/defaults/text2image/sample_args.json create mode 100644 cosmos3/defaults/text2video/sample_args.json create mode 100644 cosmos3/defaults/video2video/sample_args.json create mode 100644 cosmos3/defaults/video_captioner.txt create mode 100644 cosmos3/fixtures/__init__.py create mode 100644 cosmos3/fixtures/args.py create mode 100644 cosmos3/fixtures/script.py create mode 100644 cosmos3/flags.py create mode 100644 cosmos3/inference.py create mode 100644 cosmos3/model.py create mode 100644 cosmos3/model_test.py create mode 100644 cosmos3/ray/__init__.py create mode 100644 cosmos3/ray/configs/latency.yaml create mode 100644 cosmos3/ray/configs/throughput.yaml create mode 100644 cosmos3/ray/gradio.py create mode 100644 cosmos3/ray/serve.py create mode 100644 cosmos3/ray/serve_test.py create mode 100644 cosmos3/ray/submit.py create mode 100644 cosmos3/scripts/__init__.py create mode 100644 cosmos3/scripts/_test.py create mode 100644 cosmos3/scripts/_test/convert_model_to_dcp.sh create mode 100644 cosmos3/scripts/_test/convert_model_to_diffusers.sh create mode 100644 cosmos3/scripts/_test/export_config.sh create mode 100644 cosmos3/scripts/_test/export_model.sh create mode 100644 cosmos3/scripts/action_policy_server.py create mode 100644 cosmos3/scripts/caption_from_video.py create mode 100644 cosmos3/scripts/captions_to_sft_jsonl.py create mode 100644 cosmos3/scripts/convert_model_to_dcp.py create mode 100644 cosmos3/scripts/eval_utils.py create mode 100644 cosmos3/scripts/export_config.py create mode 100644 cosmos3/scripts/export_model.py create mode 100644 cosmos3/scripts/export_schemas.py create mode 100644 cosmos3/scripts/export_schemas_test.py create mode 100644 cosmos3/scripts/hydra.py create mode 100644 cosmos3/scripts/inference.py create mode 100644 cosmos3/scripts/prefetch_hf_checkpoints.py create mode 100644 cosmos3/scripts/train.py create mode 100644 cosmos3/scripts/upsample_prompts.py create mode 100644 cosmos3/vision.py create mode 100644 docker/ci.Dockerfile create mode 100755 docker/entrypoint.sh create mode 100644 docker/nightly.Dockerfile create mode 100644 docs/Cosmos3.pdf create mode 100644 docs/action_policy_closed_loop_eval.md create mode 100644 docs/faq.md create mode 100644 docs/gallery.md create mode 100644 docs/inference.md create mode 100644 docs/inference_online.md create mode 100644 docs/prompting.md create mode 100644 docs/setup.md create mode 100644 docs/training.md create mode 100644 examples/_test.py create mode 100644 examples/_test/inference.sh create mode 100644 examples/_test/inference_pipeline.sh create mode 100644 examples/inference.py create mode 100644 examples/inference_pipeline.py create mode 100644 inputs/omni/action_forward_dynamics_camera_0.json create mode 100644 inputs/omni/action_forward_dynamics_camera_1.json create mode 100644 inputs/omni/action_forward_dynamics_robot.json create mode 100644 inputs/omni/action_inverse_dynamics_av.json create mode 100644 inputs/omni/action_policy_robot.json create mode 100644 inputs/omni/i2v.json create mode 100644 inputs/omni/t2i.json create mode 100644 inputs/omni/t2v.json create mode 100644 inputs/t2v_long_prompts.jsonl create mode 100644 justfile create mode 100644 pyproject.toml create mode 100644 pyrefly.toml create mode 100644 schemas/OmniSampleOverrides.schema.json create mode 100644 schemas/OmniSampleOverrides.yaml create mode 100644 schemas/OmniSetupOverrides.schema.json create mode 100644 schemas/OmniSetupOverrides.yaml create mode 100644 uv.lock create mode 100644 vllm-cosmos3/README.md create mode 100644 vllm-cosmos3/pyproject.toml create mode 100644 vllm-cosmos3/uv.lock create mode 100644 vllm-cosmos3/vllm_cosmos3/__init__.py create mode 100644 vllm-cosmos3/vllm_cosmos3/model.py diff --git a/.agents/skills/cosmos3-codebase-nav/SKILL.md b/.agents/skills/cosmos3-codebase-nav/SKILL.md new file mode 100644 index 00000000..810695e4 --- /dev/null +++ b/.agents/skills/cosmos3-codebase-nav/SKILL.md @@ -0,0 +1,99 @@ +--- +name: cosmos3-codebase-nav +description: > + Navigate the Cosmos3 package codebase to find where parameters, configs, defaults, + scripts, and documentation live. Use when the user asks "where is X in cosmos3", + "how do I find the config for Y", "where are the defaults", "where do I change a + parameter", or any question about locating files, modules, or settings. Also use + when the user opens or edits files and needs orientation. +--- + +# Cosmos3 Codebase Navigation + +## When to use this skill + +- Use this skill when an agent is navigating the Cosmos3 package +- Use this skill to answer "where is X", "how do I find the config for Y", or any file-location question +- Use this skill when the user opens or edits cosmos3 files and needs orientation + +## Path convention + +All paths below are relative to this file's location (`.agents/skills/cosmos3-codebase-nav/`). + +## Quick Reference + +### Where parameters and defaults live + +| What you're looking for | File | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Sampling params (num_steps, guidance, shift, fps, etc.) | `../../../cosmos3/args.py` → `SamplingArgs`, `SamplingOverrides` | +| Per-modality default values | `../../../cosmos3/defaults//sample_args.json` | +| Setup params (parallelism, checkpoints, model path) | `../../../cosmos3/args.py` → `OmniSetupArgs`, `OmniSetupOverrides` | +| Common args base classes | `../../../cosmos3/common/args.py` → `ArgsBase`, `OverridesBase` | +| Ray serving parallelism presets | `../../../cosmos3/ray/configs/latency.yaml`, `../../../cosmos3/ray/configs/throughput.yaml` | +| Feature flags | `../../../cosmos3/flags.py` | +| Prompt upsampler system prompt | `../../../cosmos3/defaults/prompt_upsampler.txt` | +| Example inputs | `../../../inputs/omni/t2i.json`, `../../../inputs/omni/t2v.json`, `../../../inputs/omni/i2v.json` | + +Available modality modes for defaults: `text2image`, `text2video`, `image2video`. + +### Config defaults resolution chain + +When a user runs inference, default parameter values are resolved in this order: + +``` +cosmos3/defaults//sample_args.json # 1. Per-modality JSON defaults (num_steps, guidance, shift, fps, etc.) + ↓ +_load_modality_defaults() in cosmos3/args.py # 2. Loaded and cached at import time + ↓ +SamplingArgs / SamplingOverrides # 3. Pydantic models with field-level validation + ↓ +OmniSampleOverrides.build_sample() # 4. Merges user overrides → final resolved args + ↓ +_RESOLUTION_SHIFT_DEFAULTS[model_size, resolution] # 5. Model+resolution shift override (if user didn't set shift) + ↓ +CLI flags (--guidance, --shift, etc.) # 6. User overrides from command line +``` + +The `_RESOLUTION_SHIFT_DEFAULTS` table in `../../../cosmos3/args.py` overrides the default `shift` based on model size and resolution, unless the user explicitly specified `--shift`. + +| Mode | Default file | Key defaults | +| ------------- | -------------------------------------------------------- | ---------------------------------------------- | +| `text2image` | `../../../cosmos3/defaults/text2image/sample_args.json` | `num_frames=1`, `guidance=6.0`, `shift=10.0` | +| `text2video` | `../../../cosmos3/defaults/text2video/sample_args.json` | `num_frames=189`, `guidance=6.0`, `shift=10.0` | +| `image2video` | `../../../cosmos3/defaults/image2video/sample_args.json` | `num_frames=189`, `guidance=6.0`, `shift=10.0` | + +Users can also supply a custom defaults file per-request via the `defaults_file` field in sample arguments (see `../../../docs/inference.md`). + +### Where to make changes + +| Task | Edit | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Change a built-in default value | `../../../cosmos3/defaults//sample_args.json` | +| Add a new CLI parameter | `SamplingArgs` + `SamplingOverrides` in `../../../cosmos3/args.py`, then add to each `sample_args.json` | +| Change parallelism presets | `../../../cosmos3/ray/configs/latency.yaml` or `throughput.yaml` | +| Add a new script | `../../../cosmos3/scripts/` — follow `inference.py` as the pattern | + +### Key entry points + +| Entry point | How to run | +| -------------------- | ------------------------------------------------------ | +| Batch inference | `python -m cosmos3.scripts.inference` | +| Online serving (Ray) | `python -m cosmos3.ray.serve` | +| Submit to Ray server | `python -m cosmos3.ray.submit` | +| Gradio UI | `python -m cosmos3.ray.gradio` | +| Prompt upsampling | `python -m cosmos3.scripts.upsample_prompts` | +| Model export | `python -m cosmos3.scripts.export_model` | +| Diffusers conversion | `python -m cosmos3.scripts.convert_model_to_diffusers` | + +### Documentation + +| Doc | Covers | +| ----------------------------------- | ----------------------------------------------------- | +| `../../../AGENTS.md` | Commands, rules, key file locations (read this first) | +| `../../../README.md` | Overview, quickstart, examples | +| `../../../docs/setup.md` | Installation, environment, checkpoints | +| `../../../docs/inference.md` | Sample args, default values, custom defaults | +| `../../../docs/inference_online.md` | Ray Serve and Gradio | +| `../../../docs/prompting.md` | Prompt engineering, upsampling | +| `../../../docs/faq.md` | FAQ, tips, and troubleshooting | diff --git a/.agents/skills/cosmos3-env-troubleshoot/SKILL.md b/.agents/skills/cosmos3-env-troubleshoot/SKILL.md new file mode 100644 index 00000000..9df15188 --- /dev/null +++ b/.agents/skills/cosmos3-env-troubleshoot/SKILL.md @@ -0,0 +1,106 @@ +--- +name: cosmos3-env-troubleshoot +description: > + Diagnose and fix Cosmos3 environment, installation, and runtime errors. + Use when the user encounters an ImportError, ModuleNotFoundError, CUDA error, + Docker error, checkpoint download failure, or any traceback during setup or inference. +--- + +# Cosmos3 Environment Troubleshooting + +## When to use this skill + +- Use when a user hits an error during installation, environment setup, or first run +- Use when a traceback mentions torch, CUDA, missing modules, or shared libraries +- Use when Docker or container setup fails +- Use when checkpoint downloads fail or HuggingFace auth errors appear + +## Path convention + +All paths below are relative to this file's location (`.agents/skills/cosmos3-env-troubleshoot/`). + +## Step 1: Match against known errors + +Check the error message against the table below. Each row links to the canonical fix in the docs. + +| Error signature | Cause | Fix location | +| ----------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `ImportError: cannot import name '_functionalization' from 'torch._C'` | NGC container library conflict | `../../../docs/setup.md` § PyTorch Import Issue — run `export LD_LIBRARY_PATH=''` | +| `ModuleNotFoundError: No module named 'cosmos3'` | Package not installed | `../../../docs/setup.md` § Dependency Issue — run `uv sync --all-extras --group=cu130 --reinstall` | +| `ModuleNotFoundError: No module named ` | Dependency missing | `../../../docs/setup.md` § Dependency Issue — reinstall venv | +| `fatal error: Python.h: No such file or directory` | Broken Python / uv install | `../../../docs/setup.md` § Python Issue — reinstall uv + venv from scratch | +| `OSError: : cannot open shared object file` | CUDA version mismatch | `../../../docs/setup.md` § CUDA Issue — install matching `cuda-toolkit-` | +| `docker: Error response from daemon: unknown or invalid runtime name: nvidia` | Docker nvidia runtime not configured | `../../../docs/setup.md` § Docker Container — run `sudo nvidia-ctk runtime configure --runtime=docker` | +| HuggingFace 401 / download failures | Auth or license not accepted | `../../../docs/setup.md` § Downloading Checkpoints — check `HF_TOKEN`, accept license agreement | + +## Step 2: If no documented fix matches, try common remediation + +Run these diagnostic commands to collect information, then attempt fixes in order: + +### Diagnostic commands + +```shell +# System +uname -a +cat /etc/os-release | head -5 + +# Python +python --version +which python + +# CUDA +nvidia-smi +python -c "import torch; print(f'torch={torch.__version__}, cuda={torch.version.cuda}')" + +# Package +uv pip list | head -20 +``` + +### Remediation ladder (try in order) + +1. **Clear library path**: `export LD_LIBRARY_PATH=''` +2. **Reinstall venv**: `uv sync --all-extras --group=cu130 --reinstall` +3. **Reinstall uv + venv from scratch**: + + ```shell + curl -LsSf https://astral.sh/uv/install.sh | sh + uv python install --reinstall + rm -rf .venv + uv sync --all-extras --group=cu130 --reinstall + source .venv/bin/activate + ``` + +4. **Check CUDA version alignment**: the major CUDA version from `nvidia-smi` must match `torch.version.cuda` +5. **Try Docker**: if the host environment is too broken, fall back to the Docker container (see `../../../docs/setup.md`) + +## Step 3: If still unresolved, generate a bug report + +If none of the above resolves the issue, collect environment information and present the user with a pre-filled bug report they can submit as a GitHub issue. + +Fill in the template below by running the diagnostic commands and inserting the results: + +````markdown +## Environment + +- **OS**: +- **Python**: +- **CUDA (system)**: +- **CUDA (torch)**: `> +- **torch version**: `> +- **cosmos3 version**: +- **Installation method**: + +## Error + +``` + +``` + +## What was tried + +1. + +## Additional context + + +```` diff --git a/.agents/skills/cosmos3-inference/SKILL.md b/.agents/skills/cosmos3-inference/SKILL.md new file mode 100644 index 00000000..c2c0379a --- /dev/null +++ b/.agents/skills/cosmos3-inference/SKILL.md @@ -0,0 +1,59 @@ +--- +name: cosmos3-inference +description: > + Guide users through running Cosmos3 inference — offline batch generation, online + serving with Ray and Gradio, parallelism options, input formats, sampling parameters, + and prompt upsampling. Use when the user asks "how do I run inference", + "how do I generate a video", "how do I serve the model", "what parameters should I use", + or any question about running the model to produce outputs. +--- + +# Cosmos3 Inference + +## When to use this skill + +- Use when a user wants to generate images or videos with Cosmos3 +- Use when a user asks about inference parameters, input formats, or parallelism +- Use when a user wants to set up online serving (Ray Serve, Gradio) +- Use when a user asks about prompt engineering or upsampling +- For environment or import errors, hand off to **cosmos3-env-troubleshoot** + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` commands should also be run from there. + +## Where to find answers + +| User question | Go to | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| How do I run inference? (single-GPU, multi-GPU) | `README.md` § Offline Batch Inference | +| Which model should I use? (Nano vs Super, memory, shift) | `README.md` § Models | +| Which modality? (t2i, t2v, i2v, examples) | `README.md` § Modalities | +| What parallelism preset? (latency vs throughput) | `README.md` § Offline Batch Inference | +| What input fields are available? (prompt, vision_path, num_frames, ...) | `docs/inference.md` § Sample Arguments | +| What are the default parameter values? | `docs/inference.md` § Default Values | +| How do I use custom defaults? | `docs/inference.md` § Custom Defaults | +| How do I override a parameter? (precedence) | `docs/faq.md` § How do I override a default parameter? | +| What is the shift parameter? | `docs/faq.md` § What is the `shift` parameter? | +| How many frames can I generate? (resolution caps) | `docs/faq.md` § How many frames can I generate? | +| How do I start Ray Serve / Gradio / submit requests? | `docs/inference_online.md` | +| How do I write good prompts? | `docs/prompting.md` | +| How do I upsample short prompts? | `docs/prompting.md` § Upsampling | +| How do I use the low-level API? (examples/) | `docs/inference.md` § Supplementary Examples | +| All CLI flags | `uv run --all-extras --group=cu130 python -m cosmos3.scripts.inference --help` | + +## Things not obvious from the docs + +- **Path resolution**: relative paths in input JSON files are resolved relative to the **JSON file's directory**, not the working directory. +- **Seed**: always pass `--seed` for reproducible results. Without it, a random seed is used each time. +- **Resume**: interrupted runs can be resumed by re-running the same command — existing outputs are skipped automatically. +- **`--keep-going`**: continues processing remaining samples after a per-sample failure (e.g. guardrail rejection). Used in online serving by default. +- **Unique names**: every sample in a run must have a unique `name` field, or the script will error. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------- | +| `../cosmos3-setup/SKILL.md` | Installation and environment setup | +| `../cosmos3-codebase-nav/SKILL.md` | Finding files, parameters, and configs in code | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment and runtime errors | diff --git a/.agents/skills/cosmos3-post-training/SKILL.md b/.agents/skills/cosmos3-post-training/SKILL.md new file mode 100644 index 00000000..2efe73cd --- /dev/null +++ b/.agents/skills/cosmos3-post-training/SKILL.md @@ -0,0 +1,82 @@ +--- +name: cosmos3-post-training +description: > + Guide users through Cosmos3 supervised fine-tuning (SFT) post-training: + preparing the example dataset and DCP base checkpoint, editing the experiment + config, launching distributed training with `torchrun`, running T2V/I2V/V2V + inference with the trained DCP checkpoint, optionally exporting it to + Hugging Face safetensors, and the optional Video Captioning pipeline for + building custom datasets. Use when the user asks "how do I post-train Cosmos3", "how do I + fine-tune on my own video dataset", "how do I export a trained checkpoint", + "how do I caption videos for training", or any question about SFT, the + `cu130-train` / `cu128-train` install groups, the `mixed_modality_sft_8b.yaml` config, + the `convert_model_to_dcp` / `export_model` / `train` / `caption_from_video` + / `captions_to_sft_jsonl` scripts, or where SFT outputs land on disk. +--- + +# Cosmos3 Post-Training (SFT) + +## When to use this skill + +- User wants to fine-tune Cosmos3-Nano on their own video dataset (SFT) +- User asks which fields in `mixed_modality_sft_8b.yaml` to override (lr, FSDP shard, max_iter, jsonl_paths, ...) +- User wants to convert a base Hugging Face checkpoint to DCP, or convert a trained DCP back to safetensors +- User wants to caption raw videos with a VLM to build a training dataset +- User wants to assemble a JSONL manifest from videos + captions +- For installation, `--group=cu130-train` / `cu128-train`, or LD_LIBRARY_PATH issues, hand off to **cosmos3-setup** +- For inference parameters, parallelism presets, or online serving, hand off to **cosmos3-inference** + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` / `torchrun` commands should also be run from there. + +## Where to find answers + +The canonical reference is `docs/training.md`. Use this table to route questions: + +| User question | Go to | +| ------------------------------------------------------------------ | ------------------------------------------------------------------- | +| Full step-by-step SFT workflow | `docs/training.md` | +| Which install group? (`cu130-train` vs `cu128-train`) | `docs/training.md` § Setup, `docs/setup.md` § CUDA Variants | +| How do I download the example bridge dataset? | `docs/training.md` § Step 1 — Prepare data and checkpoint | +| How do I convert a base HF checkpoint to DCP? | `docs/training.md` § Step 1 — Prepare data and checkpoint | +| Which `mixed_modality_sft_8b.yaml` fields are commonly overridden? | `docs/training.md` § Step 2 — Prepare config | +| How do I launch distributed training? | `docs/training.md` § Step 3 — Run training | +| How do I validate the config without actually training? | `docs/training.md` § Step 3 (the `--dry-run` flag) | +| How do I export the trained DCP back to safetensors? | `docs/training.md` § Export checkpoint to Hugging Face safetensors | +| How do I run inference with the trained checkpoint? | `docs/training.md` § Run inference with trained checkpoint | +| Where do training artifacts land? | `docs/training.md` § Outputs | +| How do I caption raw videos for SFT? | `docs/training.md` § Video Captioning for Training Data Processing | +| How do I serve the captioning VLM? | `docs/training.md` § Server setup | +| How do I build a JSONL dataset from captions + videos? | `docs/training.md` § Creating Video Dataset JSONL File for Training | + +## Workflow at a glance + +1. **Setup** — install the training extras: `uv sync --all-extras --group=cu130-train` (or `cu128-train` on older drivers), then `source .venv/bin/activate && export LD_LIBRARY_PATH=`. +2. **Step 1 — Prepare data and checkpoint** — download the example bridge dataset to `$DATASET_PATH` (Hugging Face cache) and `convert_model_to_dcp` the base checkpoint into `$BASE_CHECKPOINT_PATH` (default: `/tmp/$USER/checkpoints/cosmos3_nano`). +3. **Step 2 — Prepare config** — the provided `cosmos3/configs/experiment/mixed_modality_sft_8b.yaml` runs as-is on the example dataset (~100 iterations); override `model.config.parallelism.data_parallel_shard_degree`, `dataloader_train.dataloader.datasets.*.jsonl_paths`, `optimizer.lr`, `trainer.max_iter`, etc. for custom runs. +4. **Step 3 — Run training** — `torchrun --nproc_per_node=8 -m cosmos3.scripts.train -o outputs/train --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml --config-overrides "checkpoint.load_path=$BASE_CHECKPOINT_PATH" "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl"` (use `--dry-run` first when iterating on config). DCP checkpoints land in `outputs/train/job/checkpoints/iter_`. +5. **Inference** — read `outputs/train/job/checkpoints/latest_checkpoint.txt`, point `cosmos3.scripts.inference` at the resulting `outputs/train/job/checkpoints/iter_` DCP path with `--config-file outputs/train/config.yaml`. The example input glob `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` covers T2V, I2V, and V2V (see `cosmos3-inference` skill for presets / input formats). +6. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. + +## Things not obvious from the docs + +- **Training extras are a separate group**: SFT requires the `cu130-train` / `cu128-train` install group, not the inference-only `cu130` / `cu128`. Re-running `uv sync` with the wrong group silently leaves training deps uninstalled. +- **`-o` controls the entire output tree**: passing `-o outputs/train` to `cosmos3.scripts.train` makes everything land under `outputs/train/job/...` (logs, `config.yaml`, `checkpoints/iter_`, callback outputs). Without `-o`, training falls back to `${IMAGINAIRE_OUTPUT_ROOT:-/tmp/imaginaire4-output}/{job.project}/{job.group}/{job.name}/`. +- **Inference uses the DCP checkpoint directly**: the standard flow points `cosmos3.scripts.inference` at `outputs/train/job/checkpoints/iter_` together with `--config-file outputs/train/config.yaml`. The Hugging Face safetensors export (`outputs/train/model`) is optional — only needed if you want a portable single-file checkpoint. +- **Mixed-modality input glob**: the example uses `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` with a `*` so a single command runs T2V, I2V, and V2V (the dataset has `inference_prompt/`, `inference_prompt_i2v/`, `inference_prompt_v2v/` siblings under `val/`). +- **`data_parallel_shard_degree` must equal `WORLD_SIZE`**: it has to match `--nproc_per_node` on the `torchrun` command. Mismatch → FSDP init failure. +- **`--dry-run`**: `cosmos3.scripts.train` accepts `--dry-run` to validate the config end-to-end without launching training. Use it whenever iterating on YAML overrides. +- **Captioning server flags**: `vllm serve ... --allowed-local-media-path /` is required so the VLM can read the `file://` paths the captioning script sends. Use `Qwen/Qwen3-VL-8B-Instruct-FP8` as the recommended model; first launch downloads weights and may take several minutes (server is ready when you see `Application startup complete.`). +- **Captioning input modes**: `cosmos3.scripts.caption_from_video` accepts `--video ` (single file or directory of `.mp4`s) or `-i ` where each line has a `vision_path` field — same JSONL format used downstream by training. +- **Captioning output layout**: each input video produces a directory containing `caption.txt` and `sample_args.json`; `captions_to_sft_jsonl` then assembles those plus the source videos into a training-ready JSONL. +- **`uv run` over bare `python`**: per repo convention, prefer `uv run` for new commands. The `python -m cosmos3.scripts.caption_from_video ...` snippets in `docs/training.md` are a known pending migration. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------------------------------------- | +| `../cosmos3-setup/SKILL.md` | Initial install, CUDA variant selection, container/`LD_LIBRARY_PATH` setup | +| `../cosmos3-inference/SKILL.md` | Inference parameters, parallelism presets, input JSON format, online serving | +| `../cosmos3-codebase-nav/SKILL.md` | Locating configs, scripts, and defaults inside the package | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment / runtime errors during training | diff --git a/.agents/skills/cosmos3-setup/SKILL.md b/.agents/skills/cosmos3-setup/SKILL.md new file mode 100644 index 00000000..f8b9afce --- /dev/null +++ b/.agents/skills/cosmos3-setup/SKILL.md @@ -0,0 +1,62 @@ +--- +name: cosmos3-setup +description: > + Guide users through Cosmos3 installation, environment setup, checkpoint downloading, + and verification. Use when the user asks "how do I install cosmos3", "how do I set up + the environment", "how do I download checkpoints", "how do I use Docker", or any + question about getting the package running for the first time. +--- + +# Cosmos3 Setup + +## When to use this skill + +- Use when a user wants to install Cosmos3 or set up a development environment +- Use when a user asks about system requirements, CUDA versions, or GPU compatibility +- Use when a user needs to download model checkpoints or configure HuggingFace auth +- Use when a user wants to run Cosmos3 inside a Docker container or NGC container +- For errors during setup, hand off to the **cosmos3-env-troubleshoot** skill + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` commands should also be run from there. + +## Where to find answers + +The canonical setup reference is `docs/setup.md`. The README (`README.md` § Setup) has the shortest quickstart. + +| User question | Go to | +| ------------------------------------------------------ | -------------------------------------------------------------------- | +| What are the system requirements? | `docs/setup.md` § System Requirements | +| How do I install with uv? (sync, pip venv, pip system) | `docs/setup.md` § Virtual Environment | +| How do I install with Docker? | `docs/setup.md` § Docker Container | +| Custom torch/CUDA versions or attention backends? | `docs/setup.md` § Advanced | +| Which CUDA version? (cu130 vs cu128) | `docs/setup.md` § CUDA Variants, `docs/faq.md` § Which CUDA version? | +| How do I download checkpoints? | `docs/setup.md` § Downloading Checkpoints | +| NGC container issues? | `docs/setup.md` § PyTorch Import Issue | +| Any installation error | `../cosmos3-env-troubleshoot/SKILL.md` | + +## Setup steps at a glance + +1. **Clone** the repository and `cd` into the project root (the directory containing `pyproject.toml`) +2. **System deps**: `sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget` +3. **Install uv**: `curl -LsSf https://astral.sh/uv/install.sh | sh && source $HOME/.local/bin/env` +4. **Install package**: `uv sync --all-extras --group=cu130 && source .venv/bin/activate` (see docs for alternative methods) +5. **Optional extras**: `.[serve]` for Ray/Gradio, `.[guardrail]` for safety, `.[train]` for post-training +6. **Checkpoints**: auto-downloaded during inference; requires HuggingFace auth (see docs) +7. **Verify**: `uv run --all-extras --group=cu130 python -c "import cosmos3; print('ok')"` + +## Things not obvious from the docs + +- **NGC container caveat**: you must run `export LD_LIBRARY_PATH=''` *before* any Python imports when inside an NGC PyTorch container. Easy to miss. +- **CUDA version alignment**: the major CUDA version from `nvidia-smi` must match `torch.version.cuda`. Mismatches cause cryptic shared-library errors. +- **`HF_HOME`**: controls where checkpoints are cached (default: `~/.cache/huggingface`). Set this if disk space is tight or you want a shared cache. +- **Conflicting env vars**: stale `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN` env vars can silently override CLI auth. Check with `printenv | grep HF_`. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------- | +| `../cosmos3-inference/SKILL.md` | Running inference after setup is complete | +| `../cosmos3-codebase-nav/SKILL.md` | Finding files, parameters, and configs in code | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment and runtime errors | diff --git a/.claude/skills/cosmos3-codebase-nav/SKILL.md b/.claude/skills/cosmos3-codebase-nav/SKILL.md new file mode 100644 index 00000000..810695e4 --- /dev/null +++ b/.claude/skills/cosmos3-codebase-nav/SKILL.md @@ -0,0 +1,99 @@ +--- +name: cosmos3-codebase-nav +description: > + Navigate the Cosmos3 package codebase to find where parameters, configs, defaults, + scripts, and documentation live. Use when the user asks "where is X in cosmos3", + "how do I find the config for Y", "where are the defaults", "where do I change a + parameter", or any question about locating files, modules, or settings. Also use + when the user opens or edits files and needs orientation. +--- + +# Cosmos3 Codebase Navigation + +## When to use this skill + +- Use this skill when an agent is navigating the Cosmos3 package +- Use this skill to answer "where is X", "how do I find the config for Y", or any file-location question +- Use this skill when the user opens or edits cosmos3 files and needs orientation + +## Path convention + +All paths below are relative to this file's location (`.agents/skills/cosmos3-codebase-nav/`). + +## Quick Reference + +### Where parameters and defaults live + +| What you're looking for | File | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Sampling params (num_steps, guidance, shift, fps, etc.) | `../../../cosmos3/args.py` → `SamplingArgs`, `SamplingOverrides` | +| Per-modality default values | `../../../cosmos3/defaults//sample_args.json` | +| Setup params (parallelism, checkpoints, model path) | `../../../cosmos3/args.py` → `OmniSetupArgs`, `OmniSetupOverrides` | +| Common args base classes | `../../../cosmos3/common/args.py` → `ArgsBase`, `OverridesBase` | +| Ray serving parallelism presets | `../../../cosmos3/ray/configs/latency.yaml`, `../../../cosmos3/ray/configs/throughput.yaml` | +| Feature flags | `../../../cosmos3/flags.py` | +| Prompt upsampler system prompt | `../../../cosmos3/defaults/prompt_upsampler.txt` | +| Example inputs | `../../../inputs/omni/t2i.json`, `../../../inputs/omni/t2v.json`, `../../../inputs/omni/i2v.json` | + +Available modality modes for defaults: `text2image`, `text2video`, `image2video`. + +### Config defaults resolution chain + +When a user runs inference, default parameter values are resolved in this order: + +``` +cosmos3/defaults//sample_args.json # 1. Per-modality JSON defaults (num_steps, guidance, shift, fps, etc.) + ↓ +_load_modality_defaults() in cosmos3/args.py # 2. Loaded and cached at import time + ↓ +SamplingArgs / SamplingOverrides # 3. Pydantic models with field-level validation + ↓ +OmniSampleOverrides.build_sample() # 4. Merges user overrides → final resolved args + ↓ +_RESOLUTION_SHIFT_DEFAULTS[model_size, resolution] # 5. Model+resolution shift override (if user didn't set shift) + ↓ +CLI flags (--guidance, --shift, etc.) # 6. User overrides from command line +``` + +The `_RESOLUTION_SHIFT_DEFAULTS` table in `../../../cosmos3/args.py` overrides the default `shift` based on model size and resolution, unless the user explicitly specified `--shift`. + +| Mode | Default file | Key defaults | +| ------------- | -------------------------------------------------------- | ---------------------------------------------- | +| `text2image` | `../../../cosmos3/defaults/text2image/sample_args.json` | `num_frames=1`, `guidance=6.0`, `shift=10.0` | +| `text2video` | `../../../cosmos3/defaults/text2video/sample_args.json` | `num_frames=189`, `guidance=6.0`, `shift=10.0` | +| `image2video` | `../../../cosmos3/defaults/image2video/sample_args.json` | `num_frames=189`, `guidance=6.0`, `shift=10.0` | + +Users can also supply a custom defaults file per-request via the `defaults_file` field in sample arguments (see `../../../docs/inference.md`). + +### Where to make changes + +| Task | Edit | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Change a built-in default value | `../../../cosmos3/defaults//sample_args.json` | +| Add a new CLI parameter | `SamplingArgs` + `SamplingOverrides` in `../../../cosmos3/args.py`, then add to each `sample_args.json` | +| Change parallelism presets | `../../../cosmos3/ray/configs/latency.yaml` or `throughput.yaml` | +| Add a new script | `../../../cosmos3/scripts/` — follow `inference.py` as the pattern | + +### Key entry points + +| Entry point | How to run | +| -------------------- | ------------------------------------------------------ | +| Batch inference | `python -m cosmos3.scripts.inference` | +| Online serving (Ray) | `python -m cosmos3.ray.serve` | +| Submit to Ray server | `python -m cosmos3.ray.submit` | +| Gradio UI | `python -m cosmos3.ray.gradio` | +| Prompt upsampling | `python -m cosmos3.scripts.upsample_prompts` | +| Model export | `python -m cosmos3.scripts.export_model` | +| Diffusers conversion | `python -m cosmos3.scripts.convert_model_to_diffusers` | + +### Documentation + +| Doc | Covers | +| ----------------------------------- | ----------------------------------------------------- | +| `../../../AGENTS.md` | Commands, rules, key file locations (read this first) | +| `../../../README.md` | Overview, quickstart, examples | +| `../../../docs/setup.md` | Installation, environment, checkpoints | +| `../../../docs/inference.md` | Sample args, default values, custom defaults | +| `../../../docs/inference_online.md` | Ray Serve and Gradio | +| `../../../docs/prompting.md` | Prompt engineering, upsampling | +| `../../../docs/faq.md` | FAQ, tips, and troubleshooting | diff --git a/.claude/skills/cosmos3-env-troubleshoot/SKILL.md b/.claude/skills/cosmos3-env-troubleshoot/SKILL.md new file mode 100644 index 00000000..9df15188 --- /dev/null +++ b/.claude/skills/cosmos3-env-troubleshoot/SKILL.md @@ -0,0 +1,106 @@ +--- +name: cosmos3-env-troubleshoot +description: > + Diagnose and fix Cosmos3 environment, installation, and runtime errors. + Use when the user encounters an ImportError, ModuleNotFoundError, CUDA error, + Docker error, checkpoint download failure, or any traceback during setup or inference. +--- + +# Cosmos3 Environment Troubleshooting + +## When to use this skill + +- Use when a user hits an error during installation, environment setup, or first run +- Use when a traceback mentions torch, CUDA, missing modules, or shared libraries +- Use when Docker or container setup fails +- Use when checkpoint downloads fail or HuggingFace auth errors appear + +## Path convention + +All paths below are relative to this file's location (`.agents/skills/cosmos3-env-troubleshoot/`). + +## Step 1: Match against known errors + +Check the error message against the table below. Each row links to the canonical fix in the docs. + +| Error signature | Cause | Fix location | +| ----------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `ImportError: cannot import name '_functionalization' from 'torch._C'` | NGC container library conflict | `../../../docs/setup.md` § PyTorch Import Issue — run `export LD_LIBRARY_PATH=''` | +| `ModuleNotFoundError: No module named 'cosmos3'` | Package not installed | `../../../docs/setup.md` § Dependency Issue — run `uv sync --all-extras --group=cu130 --reinstall` | +| `ModuleNotFoundError: No module named ` | Dependency missing | `../../../docs/setup.md` § Dependency Issue — reinstall venv | +| `fatal error: Python.h: No such file or directory` | Broken Python / uv install | `../../../docs/setup.md` § Python Issue — reinstall uv + venv from scratch | +| `OSError: : cannot open shared object file` | CUDA version mismatch | `../../../docs/setup.md` § CUDA Issue — install matching `cuda-toolkit-` | +| `docker: Error response from daemon: unknown or invalid runtime name: nvidia` | Docker nvidia runtime not configured | `../../../docs/setup.md` § Docker Container — run `sudo nvidia-ctk runtime configure --runtime=docker` | +| HuggingFace 401 / download failures | Auth or license not accepted | `../../../docs/setup.md` § Downloading Checkpoints — check `HF_TOKEN`, accept license agreement | + +## Step 2: If no documented fix matches, try common remediation + +Run these diagnostic commands to collect information, then attempt fixes in order: + +### Diagnostic commands + +```shell +# System +uname -a +cat /etc/os-release | head -5 + +# Python +python --version +which python + +# CUDA +nvidia-smi +python -c "import torch; print(f'torch={torch.__version__}, cuda={torch.version.cuda}')" + +# Package +uv pip list | head -20 +``` + +### Remediation ladder (try in order) + +1. **Clear library path**: `export LD_LIBRARY_PATH=''` +2. **Reinstall venv**: `uv sync --all-extras --group=cu130 --reinstall` +3. **Reinstall uv + venv from scratch**: + + ```shell + curl -LsSf https://astral.sh/uv/install.sh | sh + uv python install --reinstall + rm -rf .venv + uv sync --all-extras --group=cu130 --reinstall + source .venv/bin/activate + ``` + +4. **Check CUDA version alignment**: the major CUDA version from `nvidia-smi` must match `torch.version.cuda` +5. **Try Docker**: if the host environment is too broken, fall back to the Docker container (see `../../../docs/setup.md`) + +## Step 3: If still unresolved, generate a bug report + +If none of the above resolves the issue, collect environment information and present the user with a pre-filled bug report they can submit as a GitHub issue. + +Fill in the template below by running the diagnostic commands and inserting the results: + +````markdown +## Environment + +- **OS**: +- **Python**: +- **CUDA (system)**: +- **CUDA (torch)**: `> +- **torch version**: `> +- **cosmos3 version**: +- **Installation method**: + +## Error + +``` + +``` + +## What was tried + +1. + +## Additional context + + +```` diff --git a/.claude/skills/cosmos3-inference/SKILL.md b/.claude/skills/cosmos3-inference/SKILL.md new file mode 100644 index 00000000..c2c0379a --- /dev/null +++ b/.claude/skills/cosmos3-inference/SKILL.md @@ -0,0 +1,59 @@ +--- +name: cosmos3-inference +description: > + Guide users through running Cosmos3 inference — offline batch generation, online + serving with Ray and Gradio, parallelism options, input formats, sampling parameters, + and prompt upsampling. Use when the user asks "how do I run inference", + "how do I generate a video", "how do I serve the model", "what parameters should I use", + or any question about running the model to produce outputs. +--- + +# Cosmos3 Inference + +## When to use this skill + +- Use when a user wants to generate images or videos with Cosmos3 +- Use when a user asks about inference parameters, input formats, or parallelism +- Use when a user wants to set up online serving (Ray Serve, Gradio) +- Use when a user asks about prompt engineering or upsampling +- For environment or import errors, hand off to **cosmos3-env-troubleshoot** + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` commands should also be run from there. + +## Where to find answers + +| User question | Go to | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| How do I run inference? (single-GPU, multi-GPU) | `README.md` § Offline Batch Inference | +| Which model should I use? (Nano vs Super, memory, shift) | `README.md` § Models | +| Which modality? (t2i, t2v, i2v, examples) | `README.md` § Modalities | +| What parallelism preset? (latency vs throughput) | `README.md` § Offline Batch Inference | +| What input fields are available? (prompt, vision_path, num_frames, ...) | `docs/inference.md` § Sample Arguments | +| What are the default parameter values? | `docs/inference.md` § Default Values | +| How do I use custom defaults? | `docs/inference.md` § Custom Defaults | +| How do I override a parameter? (precedence) | `docs/faq.md` § How do I override a default parameter? | +| What is the shift parameter? | `docs/faq.md` § What is the `shift` parameter? | +| How many frames can I generate? (resolution caps) | `docs/faq.md` § How many frames can I generate? | +| How do I start Ray Serve / Gradio / submit requests? | `docs/inference_online.md` | +| How do I write good prompts? | `docs/prompting.md` | +| How do I upsample short prompts? | `docs/prompting.md` § Upsampling | +| How do I use the low-level API? (examples/) | `docs/inference.md` § Supplementary Examples | +| All CLI flags | `uv run --all-extras --group=cu130 python -m cosmos3.scripts.inference --help` | + +## Things not obvious from the docs + +- **Path resolution**: relative paths in input JSON files are resolved relative to the **JSON file's directory**, not the working directory. +- **Seed**: always pass `--seed` for reproducible results. Without it, a random seed is used each time. +- **Resume**: interrupted runs can be resumed by re-running the same command — existing outputs are skipped automatically. +- **`--keep-going`**: continues processing remaining samples after a per-sample failure (e.g. guardrail rejection). Used in online serving by default. +- **Unique names**: every sample in a run must have a unique `name` field, or the script will error. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------- | +| `../cosmos3-setup/SKILL.md` | Installation and environment setup | +| `../cosmos3-codebase-nav/SKILL.md` | Finding files, parameters, and configs in code | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment and runtime errors | diff --git a/.claude/skills/cosmos3-post-training/SKILL.md b/.claude/skills/cosmos3-post-training/SKILL.md new file mode 100644 index 00000000..2efe73cd --- /dev/null +++ b/.claude/skills/cosmos3-post-training/SKILL.md @@ -0,0 +1,82 @@ +--- +name: cosmos3-post-training +description: > + Guide users through Cosmos3 supervised fine-tuning (SFT) post-training: + preparing the example dataset and DCP base checkpoint, editing the experiment + config, launching distributed training with `torchrun`, running T2V/I2V/V2V + inference with the trained DCP checkpoint, optionally exporting it to + Hugging Face safetensors, and the optional Video Captioning pipeline for + building custom datasets. Use when the user asks "how do I post-train Cosmos3", "how do I + fine-tune on my own video dataset", "how do I export a trained checkpoint", + "how do I caption videos for training", or any question about SFT, the + `cu130-train` / `cu128-train` install groups, the `mixed_modality_sft_8b.yaml` config, + the `convert_model_to_dcp` / `export_model` / `train` / `caption_from_video` + / `captions_to_sft_jsonl` scripts, or where SFT outputs land on disk. +--- + +# Cosmos3 Post-Training (SFT) + +## When to use this skill + +- User wants to fine-tune Cosmos3-Nano on their own video dataset (SFT) +- User asks which fields in `mixed_modality_sft_8b.yaml` to override (lr, FSDP shard, max_iter, jsonl_paths, ...) +- User wants to convert a base Hugging Face checkpoint to DCP, or convert a trained DCP back to safetensors +- User wants to caption raw videos with a VLM to build a training dataset +- User wants to assemble a JSONL manifest from videos + captions +- For installation, `--group=cu130-train` / `cu128-train`, or LD_LIBRARY_PATH issues, hand off to **cosmos3-setup** +- For inference parameters, parallelism presets, or online serving, hand off to **cosmos3-inference** + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` / `torchrun` commands should also be run from there. + +## Where to find answers + +The canonical reference is `docs/training.md`. Use this table to route questions: + +| User question | Go to | +| ------------------------------------------------------------------ | ------------------------------------------------------------------- | +| Full step-by-step SFT workflow | `docs/training.md` | +| Which install group? (`cu130-train` vs `cu128-train`) | `docs/training.md` § Setup, `docs/setup.md` § CUDA Variants | +| How do I download the example bridge dataset? | `docs/training.md` § Step 1 — Prepare data and checkpoint | +| How do I convert a base HF checkpoint to DCP? | `docs/training.md` § Step 1 — Prepare data and checkpoint | +| Which `mixed_modality_sft_8b.yaml` fields are commonly overridden? | `docs/training.md` § Step 2 — Prepare config | +| How do I launch distributed training? | `docs/training.md` § Step 3 — Run training | +| How do I validate the config without actually training? | `docs/training.md` § Step 3 (the `--dry-run` flag) | +| How do I export the trained DCP back to safetensors? | `docs/training.md` § Export checkpoint to Hugging Face safetensors | +| How do I run inference with the trained checkpoint? | `docs/training.md` § Run inference with trained checkpoint | +| Where do training artifacts land? | `docs/training.md` § Outputs | +| How do I caption raw videos for SFT? | `docs/training.md` § Video Captioning for Training Data Processing | +| How do I serve the captioning VLM? | `docs/training.md` § Server setup | +| How do I build a JSONL dataset from captions + videos? | `docs/training.md` § Creating Video Dataset JSONL File for Training | + +## Workflow at a glance + +1. **Setup** — install the training extras: `uv sync --all-extras --group=cu130-train` (or `cu128-train` on older drivers), then `source .venv/bin/activate && export LD_LIBRARY_PATH=`. +2. **Step 1 — Prepare data and checkpoint** — download the example bridge dataset to `$DATASET_PATH` (Hugging Face cache) and `convert_model_to_dcp` the base checkpoint into `$BASE_CHECKPOINT_PATH` (default: `/tmp/$USER/checkpoints/cosmos3_nano`). +3. **Step 2 — Prepare config** — the provided `cosmos3/configs/experiment/mixed_modality_sft_8b.yaml` runs as-is on the example dataset (~100 iterations); override `model.config.parallelism.data_parallel_shard_degree`, `dataloader_train.dataloader.datasets.*.jsonl_paths`, `optimizer.lr`, `trainer.max_iter`, etc. for custom runs. +4. **Step 3 — Run training** — `torchrun --nproc_per_node=8 -m cosmos3.scripts.train -o outputs/train --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml --config-overrides "checkpoint.load_path=$BASE_CHECKPOINT_PATH" "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl"` (use `--dry-run` first when iterating on config). DCP checkpoints land in `outputs/train/job/checkpoints/iter_`. +5. **Inference** — read `outputs/train/job/checkpoints/latest_checkpoint.txt`, point `cosmos3.scripts.inference` at the resulting `outputs/train/job/checkpoints/iter_` DCP path with `--config-file outputs/train/config.yaml`. The example input glob `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` covers T2V, I2V, and V2V (see `cosmos3-inference` skill for presets / input formats). +6. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. + +## Things not obvious from the docs + +- **Training extras are a separate group**: SFT requires the `cu130-train` / `cu128-train` install group, not the inference-only `cu130` / `cu128`. Re-running `uv sync` with the wrong group silently leaves training deps uninstalled. +- **`-o` controls the entire output tree**: passing `-o outputs/train` to `cosmos3.scripts.train` makes everything land under `outputs/train/job/...` (logs, `config.yaml`, `checkpoints/iter_`, callback outputs). Without `-o`, training falls back to `${IMAGINAIRE_OUTPUT_ROOT:-/tmp/imaginaire4-output}/{job.project}/{job.group}/{job.name}/`. +- **Inference uses the DCP checkpoint directly**: the standard flow points `cosmos3.scripts.inference` at `outputs/train/job/checkpoints/iter_` together with `--config-file outputs/train/config.yaml`. The Hugging Face safetensors export (`outputs/train/model`) is optional — only needed if you want a portable single-file checkpoint. +- **Mixed-modality input glob**: the example uses `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` with a `*` so a single command runs T2V, I2V, and V2V (the dataset has `inference_prompt/`, `inference_prompt_i2v/`, `inference_prompt_v2v/` siblings under `val/`). +- **`data_parallel_shard_degree` must equal `WORLD_SIZE`**: it has to match `--nproc_per_node` on the `torchrun` command. Mismatch → FSDP init failure. +- **`--dry-run`**: `cosmos3.scripts.train` accepts `--dry-run` to validate the config end-to-end without launching training. Use it whenever iterating on YAML overrides. +- **Captioning server flags**: `vllm serve ... --allowed-local-media-path /` is required so the VLM can read the `file://` paths the captioning script sends. Use `Qwen/Qwen3-VL-8B-Instruct-FP8` as the recommended model; first launch downloads weights and may take several minutes (server is ready when you see `Application startup complete.`). +- **Captioning input modes**: `cosmos3.scripts.caption_from_video` accepts `--video ` (single file or directory of `.mp4`s) or `-i ` where each line has a `vision_path` field — same JSONL format used downstream by training. +- **Captioning output layout**: each input video produces a directory containing `caption.txt` and `sample_args.json`; `captions_to_sft_jsonl` then assembles those plus the source videos into a training-ready JSONL. +- **`uv run` over bare `python`**: per repo convention, prefer `uv run` for new commands. The `python -m cosmos3.scripts.caption_from_video ...` snippets in `docs/training.md` are a known pending migration. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------------------------------------- | +| `../cosmos3-setup/SKILL.md` | Initial install, CUDA variant selection, container/`LD_LIBRARY_PATH` setup | +| `../cosmos3-inference/SKILL.md` | Inference parameters, parallelism presets, input JSON format, online serving | +| `../cosmos3-codebase-nav/SKILL.md` | Locating configs, scripts, and defaults inside the package | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment / runtime errors during training | diff --git a/.claude/skills/cosmos3-setup/SKILL.md b/.claude/skills/cosmos3-setup/SKILL.md new file mode 100644 index 00000000..f8b9afce --- /dev/null +++ b/.claude/skills/cosmos3-setup/SKILL.md @@ -0,0 +1,62 @@ +--- +name: cosmos3-setup +description: > + Guide users through Cosmos3 installation, environment setup, checkpoint downloading, + and verification. Use when the user asks "how do I install cosmos3", "how do I set up + the environment", "how do I download checkpoints", "how do I use Docker", or any + question about getting the package running for the first time. +--- + +# Cosmos3 Setup + +## When to use this skill + +- Use when a user wants to install Cosmos3 or set up a development environment +- Use when a user asks about system requirements, CUDA versions, or GPU compatibility +- Use when a user needs to download model checkpoints or configure HuggingFace auth +- Use when a user wants to run Cosmos3 inside a Docker container or NGC container +- For errors during setup, hand off to the **cosmos3-env-troubleshoot** skill + +## Path convention + +All paths below are relative to the cosmos3 package root (`../../../` from this skill file). All `uv run` / `python` commands should also be run from there. + +## Where to find answers + +The canonical setup reference is `docs/setup.md`. The README (`README.md` § Setup) has the shortest quickstart. + +| User question | Go to | +| ------------------------------------------------------ | -------------------------------------------------------------------- | +| What are the system requirements? | `docs/setup.md` § System Requirements | +| How do I install with uv? (sync, pip venv, pip system) | `docs/setup.md` § Virtual Environment | +| How do I install with Docker? | `docs/setup.md` § Docker Container | +| Custom torch/CUDA versions or attention backends? | `docs/setup.md` § Advanced | +| Which CUDA version? (cu130 vs cu128) | `docs/setup.md` § CUDA Variants, `docs/faq.md` § Which CUDA version? | +| How do I download checkpoints? | `docs/setup.md` § Downloading Checkpoints | +| NGC container issues? | `docs/setup.md` § PyTorch Import Issue | +| Any installation error | `../cosmos3-env-troubleshoot/SKILL.md` | + +## Setup steps at a glance + +1. **Clone** the repository and `cd` into the project root (the directory containing `pyproject.toml`) +2. **System deps**: `sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget` +3. **Install uv**: `curl -LsSf https://astral.sh/uv/install.sh | sh && source $HOME/.local/bin/env` +4. **Install package**: `uv sync --all-extras --group=cu130 && source .venv/bin/activate` (see docs for alternative methods) +5. **Optional extras**: `.[serve]` for Ray/Gradio, `.[guardrail]` for safety, `.[train]` for post-training +6. **Checkpoints**: auto-downloaded during inference; requires HuggingFace auth (see docs) +7. **Verify**: `uv run --all-extras --group=cu130 python -c "import cosmos3; print('ok')"` + +## Things not obvious from the docs + +- **NGC container caveat**: you must run `export LD_LIBRARY_PATH=''` *before* any Python imports when inside an NGC PyTorch container. Easy to miss. +- **CUDA version alignment**: the major CUDA version from `nvidia-smi` must match `torch.version.cuda`. Mismatches cause cryptic shared-library errors. +- **`HF_HOME`**: controls where checkpoints are cached (default: `~/.cache/huggingface`). Set this if disk space is tight or you want a shared cache. +- **Conflicting env vars**: stale `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN` env vars can silently override CLI auth. Check with `printenv | grep HF_`. + +## Related skills + +| Skill | When to use | +| -------------------------------------- | ---------------------------------------------- | +| `../cosmos3-inference/SKILL.md` | Running inference after setup is complete | +| `../cosmos3-codebase-nav/SKILL.md` | Finding files, parameters, and configs in code | +| `../cosmos3-env-troubleshoot/SKILL.md` | Debugging environment and runtime errors | diff --git a/.config/rumdl.toml b/.config/rumdl.toml new file mode 100644 index 00000000..24e72483 --- /dev/null +++ b/.config/rumdl.toml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://rumdl.dev/global-settings/ +[global] +flavor = "standard" +exclude = [ + "ATTRIBUTIONS.md", + "_src", +] +disable = [ + "MD013", # line-length + "MD033", # inline-html + "MD040", # fenced-code-language +] + +# https://rumdl.dev/rules/ + +[per-file-ignores] +"README.md" = [ + "MD041" # first-line-heading +] + +# ul-style +[MD004] +style = "dash" + +# table-format +[MD060] +enabled = true +style = "aligned" diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..93b0d6e5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,32 @@ +# https://coverage.readthedocs.io/en/latest/subprocess.html + +[run] +data_file = outputs/coverage/coverage +disable_warnings = + module-not-imported + no-data-collected +parallel = True +patch = subprocess + +[report] +exclude_lines = + @overload + def __repr__ + if __name__ == .__main__.: + if TYPE_CHECKING: + pragma: no cover + raise AssertionError + raise NotImplementedError +omit = + *_test.py +skip_empty = True +show_missing = True + +[html] +directory = outputs/coverage/html + +[json] +output = outputs/coverage/coverage.json + +[xml] +output = outputs/coverage/coverage.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0dfe444b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.venv +.git +/checkpoints +/datasets +/output +/examples/**/checkpoints +/examples/**/output +/examples/**/datasets diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..289b63d0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +*.lock linguist-generated=true +tests/data/** linguist-generated=true +ATTRIBUTIONS.md linguist-generated=true + +assets/** filter=lfs diff=lfs merge=lfs -text + +# Video files +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.avi filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.mkv filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text + +# Audio files +*.wav filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.flac filter=lfs diff=lfs merge=lfs -text +*.aac filter=lfs diff=lfs merge=lfs -text + +# Image files +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.tiff filter=lfs diff=lfs merge=lfs -text +*.bmp filter=lfs diff=lfs merge=lfs -text + +# Preserved root assets (not LFS) +cosmos-logo-thumbnail.png !filter !diff !merge text=auto diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..2ee4d0ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,65 @@ +--- +name: Bug Report +about: Report a reproducible bug or unexpected behavior +title: "[BUG] " +labels: 'bug' +assignees: + - spectralflight + - jeanachoi + +--- + +## Bug Description + + + +## Reproduction Steps + +```bash +# Minimal command or script to reproduce +``` + +**Reproducibility:** + +- [ ] Always +- [ ] Intermittently (~___% of the time) +- [ ] Only once + +## Expected vs. Actual Behavior + +| | Description | +| ------------ | --------------------------- | +| **Expected** | What you expected to happen | +| **Actual** | What actually happened | + +## Outputs + +
+Error / Stack Trace + + + +
+ +
+Log Files + + + +
+ +## System Information + +| Field | Value | +| ---------------------------- | ------------------------------------------- | +| **Environment** | | +| **Hardware** | | +| **OS** | | +| **GPU Driver** | | +| **CUDA Version** | | +| **Python Version** | | +| **Package Version / Commit** | | + +## Additional Context + + diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..1b11ee3d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Pre-commit +on: + pull_request: + push: + branches: [main] +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + lfs: true + - uses: actions/setup-python@v6 + - uses: astral-sh/setup-uv@v7 + - run: uvx pre-commit@4.5.1 run -a -c ci/.pre-commit-config-base.yaml + - run: uvx pre-commit@4.5.1 run -a diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3b3378e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +/assets +/credentials +/datasets +/outputs +/tmp +.cuda-name +*.env + +# ------------------------ BELOW IS AUTO-GENERATED FOR PYTHON REPOS ------------------------ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..cf8f9ea5 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[[allowlists]] +regexes = [ + '''Qwen3MoeForCausalLM''' +] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6baa0ec9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +default_language_version: + node: 25.2.1 + python: python3.13 +exclude: (?x)( + ^tests/data/ + ) +repos: + - repo: https://github.com/google/addlicense + rev: v1.2.0 + hooks: + - id: addlicense + args: ["-f", "ci/license.txt"] + - repo: https://github.com/jsh9/markdown-toc-creator + rev: 0.1.3 + hooks: + - id: markdown-toc-creator + args: ["--config=ci/.markdown-toc-creator.toml"] + - repo: https://github.com/rvben/rumdl-pre-commit + rev: v0.1.62 + hooks: + - id: rumdl-fmt + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-symlinks + - id: check-executables-have-shebangs + exclude: /_src/ + - id: check-shebang-scripts-are-executable + exclude: /_src/ + - repo: local + hooks: + - id: uv-lock + name: Generate uv lock files for projects + entry: ./ci/uv_lock.sh + language: script + files: pyproject\.toml$ + - id: uv-lock-script + name: Generate uv lock files for scripts + entry: ./ci/uv_lock_script.sh + language: script + types: [python] + - repo: https://github.com/tcort/markdown-link-check + rev: v3.14.2 + hooks: + - alias: link-check + name: link check + id: markdown-link-check + args: [--config, "ci/.link-check.json", --quiet] + stages: [manual] + exclude: (?x)( + \bATTRIBUTIONS\b| + /_src/ + ) diff --git a/.pytest.toml b/.pytest.toml new file mode 100644 index 00000000..aae7f969 --- /dev/null +++ b/.pytest.toml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[pytest] +python_files = [ + "*_test.py", +] +norecursedirs = [ + "_src", + "cosmos3._src.imaginaire", + "packages", + "projects", +] +addopts = [ + "--suppress-no-test-exit-code", +] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::FutureWarning", +] +markers = [ + "manual: Test requires --manual.", + "level(l): Test level in [0, 1, 2].", + "gpus(n): Test requires GPUs.", +] + +[pytest_env] +COSMOS_VERBOSE = { value = "0", skip_if_set = true } +CUDA_VISIBLE_DEVICES = { unset = true } +PYTORCH_CUDA_ALLOC_CONF = "expandable_segments:True" # Reduce chance of OOM errors +# Limit threading to reduce contention +MKL_NUM_THREADS = "1" +NUMEXPR_NUM_THREADS = "1" +OMP_NUM_THREADS = "1" +OPENBLAS_NUM_THREADS = "1" diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..30bebd5b --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +line-length = 120 +target-version = "py310" + +[lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "TID252", # relative-imports + "T10", # debugger +] +ignore = [ + "E402", # module-import-not-at-top-of-file + "E501", # line-too-long + "E721", # type-comparison + "E741", # ambiguous-variable-name + "F541", # f-string-missing-placeholders + "F811", # redefined-while-unused + "F841", # unused-variable +] +fixable = ["ALL"] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..90f515cb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# AGENTS.md — Cosmos3 Package + +Read this file first — it is the canonical map for navigating the Cosmos3 codebase and stays up to date. + +Cosmos3 is a Mixture-of-Transformer (MoT) world foundation model supporting text-to-image, text-to-video, and image-to-video generation. This package covers inference, online serving, and the public API surface. + +> All paths below are relative to the repository root (the directory containing `pyproject.toml` and the `cosmos3/` Python package). + +## Commands + +| Task | Command | +| ---------------------- | --------------------------------------------------- | +| Lint | `uv run ruff check .` | +| Format check | `uv run ruff format --check .` | +| Auto-fix lint + format | `uv run ruff check --fix . && uv run ruff format .` | +| Type-check | `uv run pyrefly check` | +| Test (all) | `uv run pytest` | +| Test (single file) | `uv run pytest --capture=no ` | + +Config files: `.ruff.toml` (ruff), `pyrefly.toml` (pyrefly). + +## Rules + +- Always answer questions with references to code or documentation in `file:line` format. +- When unsure, point the user to the closest doc rather than guessing. +- Keep this file short. Link out to skills and docs for detail — this file is included in every prompt. + +## Key File Locations + +| What | Where | +| ------------------------ | --------------------------------------------------------------------------------------------- | +| CLI entry point | `cosmos3/scripts/inference.py` | +| Args / param definitions | `cosmos3/args.py` → `SamplingArgs`, `SamplingOverrides`, `OmniSampleArgs`, `OmniSetupArgs` | +| Per-modality defaults | `cosmos3/defaults//sample_args.json` (modes: `text2image`, `text2video`, `image2video`) | +| Model / inference core | `cosmos3/model.py`, `cosmos3/inference.py` | +| Feature flags | `cosmos3/flags.py` | +| Ray serving configs | `cosmos3/ray/configs/latency.yaml`, `cosmos3/ray/configs/throughput.yaml` | +| Example inputs | `inputs/omni/t2i.json`, `inputs/omni/t2v.json`, `inputs/omni/i2v.json` | + +For the full config-defaults resolution chain, modality tables, and "where to make changes" guidance, see the **cosmos3-codebase-nav** skill (`.agents/skills/cosmos3-codebase-nav/SKILL.md`). + +## Documentation + +| Doc | What it covers | +| ------------------------------------------------------ | ----------------------------------------------------- | +| [docs/setup.md](./docs/setup.md) | Installation, environment, NGC container, checkpoints | +| [docs/prompting.md](./docs/prompting.md) | Prompt engineering, upsampling with vLLM | +| [docs/inference.md](./docs/inference.md) | Sample arguments, default values, custom defaults | +| [docs/inference_online.md](./docs/inference_online.md) | Online serving with Ray Serve and Gradio | +| [docs/faq.md](./docs/faq.md) | FAQ, tips, and troubleshooting | + +## Common Tasks + +| Task | Command | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Single-GPU inference | `python -m cosmos3.scripts.inference -i inputs/omni/t2v.json -o outputs/ --checkpoint-path Cosmos3-Nano` | +| Multi-GPU inference | `torchrun --nproc-per-node=4 -m cosmos3.scripts.inference --parallelism-preset=latency -i inputs/omni/t2v.json -o outputs/ --checkpoint-path Cosmos3-Nano` | +| Start online Ray server | `python -m cosmos3.ray.serve --parallelism-preset=latency -o outputs/ray_serve --checkpoint-path Cosmos3-Nano` | +| Launch Gradio UI | `python -m cosmos3.ray.gradio --port=8080` | +| See all CLI flags | `python -m cosmos3.scripts.inference --help` | + +## Inference + +For full parameter reference, input formats, parallelism presets, and online serving, read the **cosmos3-inference** skill (`.agents/skills/cosmos3-inference/SKILL.md`). + +Key things not obvious from the CLI help: + +- **NGC/PyTorch containers**: run `export LD_LIBRARY_PATH=''` before any `python` call or you'll hit a `torch._C` import error. See `docs/setup.md` § PyTorch Import Issue. +- **Reproducibility**: always pass `--seed `. Without it a random seed is used each run. +- **JSON paths**: relative paths inside input JSON files resolve relative to the JSON file's directory, not the working directory. +- **Resume**: re-running the same command skips already-generated outputs automatically. +- **Parameters / defaults**: `docs/inference.md` is the reference for all sampling args and their defaults. diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md new file mode 100644 index 00000000..f832c49f --- /dev/null +++ b/ATTRIBUTIONS.md @@ -0,0 +1,57745 @@ +DISTS-pytorch +MIT +https://github.com/dingkeyan93/DISTS +MIT License + +Copyright (c) 2020 Keyan Ding + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Deprecated +MIT License +https://github.com/laurent-laporte-pro/deprecated +The MIT License (MIT) + +Copyright (c) 2017 Laurent LAPORTE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Flask +BSD-3-Clause +https://github.com/pallets/flask/ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +GitPython +BSD-3-Clause +https://github.com/gitpython-developers/GitPython +Copyright (C) 2008, 2009 Michael Trier and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the GitPython project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ImageIO +BSD-2-Clause +https://github.com/imageio/imageio +Copyright (c) 2014-2025, imageio developers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Jinja2 +BSD License +https://github.com/pallets/jinja/ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Markdown +BSD-3-Clause +https://Python-Markdown.github.io/ +BSD 3-Clause License + +Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +MarkupSafe +BSD-3-Clause +https://github.com/pallets/markupsafe/ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +NATTEN +UNKNOWN +https://natten.org +MIT License + +Copyright (c) 2022 - 2026 Ali Hassani. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +PyDispatcher +BSD License +https://github.com/mcfletch/pydispatcher +UNKNOWN + +PyOpenGL +BSD License +https://mcfletch.github.io/pyopengl/ +UNKNOWN + +PyYAML +MIT License +https://pyyaml.org/ +Copyright (c) 2017-2021 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Pygments +BSD License +https://pygments.org +Copyright (c) 2006-2022 by the respective authors (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +SecretStorage +BSD-3-Clause +https://github.com/mitya57/secretstorage +Copyright 2012-2025 Dmitry Shachnev +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Send2Trash +BSD-3-Clause +https://github.com/arsenetar/send2trash +Copyright (c) 2017, Virgil Dupras +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Werkzeug +BSD-3-Clause +https://github.com/pallets/werkzeug/ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +absl-py +Apache-2.0 +https://github.com/abseil/abseil-py + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +accelerate +Apache Software License +https://github.com/huggingface/accelerate + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +aioboto3 +Apache Software License +https://github.com/terricain/aioboto3 +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015-2016 Nikolai Novik + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +aiobotocore +Apache-2.0 +https://github.com/aio-libs/aiobotocore +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015-2016 Nikolai Novik + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +aiofiles +Apache Software License +https://github.com/Tinche/aiofiles +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +aiohappyeyeballs +Python Software Foundation License +https://github.com/aio-libs/aiohappyeyeballs +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +aiohttp +Apache-2.0 AND MIT +https://github.com/aio-libs/aiohttp + Copyright aio-libs contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +aiohttp-cors +Apache Software License +https://github.com/aio-libs/aiohttp-cors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015-2018 Vladimir Rutsky and aio-libs team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +aioitertools +MIT +https://aioitertools.omnilib.dev/en/latest/changelog.html +MIT License + +Copyright (c) 2022 Amethyst Reese + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +aiosignal +Apache Software License +https://github.com/aio-libs/aiosignal +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2013-2019 Nikolay Kim and Andrew Svetlov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +annotated-doc +MIT +https://github.com/fastapi/annotated-doc +The MIT License (MIT) + +Copyright (c) 2025 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +annotated-types +MIT License +https://github.com/annotated-types/annotated-types +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +antlr4-python3-runtime +BSD +http://www.antlr.org +UNKNOWN + +anyio +MIT License +https://anyio.readthedocs.io/en/stable/versionhistory.html +The MIT License (MIT) + +Copyright (c) 2018 Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +argon2-cffi +MIT +https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the argon2-cffi contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +argon2-cffi-bindings +MIT +https://github.com/hynek/argon2-cffi-bindings/blob/main/CHANGELOG.md +The MIT License (MIT) + +Copyright (c) 2021 Hynek Schlawack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +arrgh +MIT License + +Copyright (c) 2023 Nicholas Sharp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +https://github.com/nmwsharp/arrgh +MIT License + +Copyright (c) 2023 Nicholas Sharp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +arrow +Apache Software License +https://github.com/arrow-py/arrow + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Chris Smith + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +asttokens +Apache 2.0 +https://github.com/gristlabs/asttokens + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +astunparse +BSD License +https://github.com/simonpercivall/astunparse +LICENSE +======= + +Copyright (c) 2014, Simon Percivall +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of AST Unparser nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are retained +in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +async-lru +MIT License +https://github.com/aio-libs/async-lru +The MIT License + +Copyright (c) 2018 aio-libs team https://github.com/aio-libs/ +Copyright (c) 2017 Ocean S. A. https://ocean.io/ +Copyright (c) 2016-2017 WikiBusiness Corporation http://wikibusiness.org/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +attrs +MIT +https://www.attrs.org/en/stable/changelog.html +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +audioop-lts +PSF-2.0 +https://github.com/AbstractUmbra/audioop +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +av +BSD-3-Clause +https://pyav.basswood-io.com +Copyright retained by original committers. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +babel +BSD License +https://babel.pocoo.org/ +Copyright (c) 2013-2026 by the Babel Team, see AUTHORS for more information. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +backports.zstd +PSF-2.0 +https://github.com/rogdham/backports.zstd +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved" +are retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +beautifulsoup4 +MIT License +https://www.crummy.com/software/BeautifulSoup/bs4/ +Beautiful Soup is made available under the MIT license: + + Copyright (c) Leonard Richardson + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Beautiful Soup incorporates code from the html5lib library, which is +also made available under the MIT license. Copyright (c) James Graham +and other contributors + +Beautiful Soup has an optional dependency on the soupsieve library, +which is also made available under the MIT license. Copyright (c) +Isaac Muse + + +better-profanity +MIT License +https://github.com/snguyenthanh/better_profanity +Copyright (c) 2018 The Python Packaging Authority + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +bleach +Apache Software License +https://github.com/mozilla/bleach +Copyright (c) 2014-2017, Mozilla Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +blinker +MIT License +https://github.com/pallets-eco/blinker/ +Copyright 2010 Jason Kirtland + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +blobfile +Public Domain +https://github.com/blobfile/blobfile +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +boto3 +Apache-2.0 +https://github.com/boto/boto3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +botocore +Apache-2.0 +https://github.com/boto/botocore + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +braceexpand +MIT License +https://github.com/trendels/braceexpand +The MIT License (MIT) + +Copyright (c) 2015 Stanis Trendelenburg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +bracex +MIT +https://github.com/facelessuser/bracex +MIT License + +Copyright (c) 2018 - 2025 Isaac Muse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +brotli +MIT +https://github.com/google/brotli +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +cattrs +MIT License +https://catt.rs + +MIT License + +Copyright (c) 2016, Tin Tvrtković + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +certifi +Mozilla Public License 2.0 (MPL 2.0) +https://github.com/certifi/python-certifi +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ + + +cffi +MIT +https://cffi.readthedocs.io/en/latest/whatsnew.html + +Except when otherwise stated (look for LICENSE files in directories or +information at the beginning of each file) all software and +documentation is licensed as follows: + + MIT No Attribution + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the + Software is furnished to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + +charset-normalizer +MIT +https://github.com/jawah/charset_normalizer/blob/master/CHANGELOG.md +MIT License + +Copyright (c) 2025 TAHRI Ahmed R. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +click +BSD-3-Clause +https://github.com/pallets/click/ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +cmake +Apache Software License; BSD License +https://cmake.org +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +colorful +MIT License +http://github.com/timofurrer/colorful +The MIT License (MIT) + +Copyright (c) 2017 Timo Furrer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +comm +BSD License +https://github.com/ipython/comm +BSD 3-Clause License + +Copyright (c) 2022, Jupyter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +contourpy +BSD License +https://github.com/contourpy/contourpy +BSD 3-Clause License + +Copyright (c) 2021-2025, ContourPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +cosmos3 +Apache Software License +https://research.nvidia.com/labs/dir/cosmos3 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +coverage +Apache-2.0 +https://github.com/coveragepy/coveragepy + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +cramjam +MIT +https://github.com/milesgranger/pyrus-cramjam +MIT License + +Copyright (c) 2020 Miles Granger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +cryptography +Apache-2.0 OR BSD-3-Clause +https://github.com/pyca/cryptography +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made +under the terms of *both* these licenses. + + +cuda-bindings +LicenseRef-NVIDIA-SOFTWARE-LICENSE +https://github.com/NVIDIA/cuda-python +NVIDIA SOFTWARE LICENSE + +This license is a legal agreement between you and NVIDIA Corporation ("NVIDIA") and governs your use of the NVIDIA CUDA Python software and materials provided hereunder ("SOFTWARE"). + +This license can be accepted only by an adult of legal age of majority in the country in which the SOFTWARE is used. If you are under the legal age of majority, you must ask your parent or legal guardian to consent to this license. By taking delivery of the SOFTWARE, you affirm that you have reached the legal age of majority, you accept the terms of this license, and you take legal and financial responsibility for the actions of your permitted users. + +You agree to use the SOFTWARE only for purposes that are permitted by (a) this license, and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions. + +1. LICENSE. Subject to the terms of this license, NVIDIA grants you a non-exclusive limited license to: (a) install and use the SOFTWARE, and (b) distribute the SOFTWARE subject to the distribution requirements described in this license. NVIDIA reserves all rights, title and interest in and to the SOFTWARE not expressly granted to you under this license. + +2. DISTRIBUTION REQUIREMENTS. These are the distribution requirements for you to exercise the distribution grant: +a. The terms under which you distribute the SOFTWARE must be consistent with the terms of this license, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA's intellectual property rights. +b. You agree to notify NVIDIA in writing of any known or suspected distribution or use of the SOFTWARE not in compliance with the requirements of this license, and to enforce the terms of your agreements with respect to distributed SOFTWARE. + +3. LIMITATIONS. Your license to use the SOFTWARE is restricted as follows: +a. The SOFTWARE is licensed for you to develop applications only for use in systems with NVIDIA GPUs. +b. You may not reverse engineer, decompile or disassemble, or remove copyright or other proprietary notices from any portion of the SOFTWARE or copies of the SOFTWARE. +c. You may not modify or create derivative works of any portion of the SOFTWARE. +d. You may not bypass, disable, or circumvent any technical measure, encryption, security, digital rights management or authentication mechanism in the SOFTWARE. +e. You may not use the SOFTWARE in any manner that would cause it to become subject to an open source software license. As examples, licenses that require as a condition of use, modification, and/or distribution that the SOFTWARE be (i) disclosed or distributed in source code form; (ii) licensed for the purpose of making derivative works; or (iii) redistributable at no charge. +f. Unless you have an agreement with NVIDIA for this purpose, you may not use the SOFTWARE with any system or application where the use or failure of the system or application can reasonably be expected to threaten or result in personal injury, death, or catastrophic loss. Examples include use in avionics, navigation, military, medical, life support or other life critical applications. NVIDIA does not design, test or manufacture the SOFTWARE for these critical uses and NVIDIA shall not be liable to you or any third party, in whole or in part, for any claims or damages arising from such uses. +g. You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney's fees and costs incident to establishing the right of indemnification) arising out of or related to use of the SOFTWARE outside of the scope of this Agreement, or not in compliance with its terms. + +4. PRE-RELEASE. SOFTWARE versions identified as alpha, beta, preview, early access or otherwise as pre-release may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, availability, and reliability standards relative to commercial versions of NVIDIA software and materials. You may use a pre-release SOFTWARE version at your own risk, understanding that these versions are not intended for use in production or business-critical systems. + +5. OWNERSHIP. The SOFTWARE and the related intellectual property rights therein are and will remain the sole and exclusive property of NVIDIA or its licensors. The SOFTWARE is copyrighted and protected by the laws of the United States and other countries, and international treaty provisions. NVIDIA may make changes to the SOFTWARE, at any time without notice, but is not obligated to support or update the SOFTWARE. + +6. COMPONENTS UNDER OTHER LICENSES. The SOFTWARE may include NVIDIA or third-party components with separate legal notices or terms as may be described in proprietary notices accompanying the SOFTWARE. If and to the extent there is a conflict between the terms in this license and the license terms associated with a component, the license terms associated with the components control only to the extent necessary to resolve the conflict. + +7. FEEDBACK. You may, but don't have to, provide to NVIDIA any Feedback. "Feedback" means any suggestions, bug fixes, enhancements, modifications, feature requests or other feedback regarding the SOFTWARE. For any Feedback that you voluntarily provide, you hereby grant NVIDIA and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) the Feedback without the payment of any royalties or fees to you. NVIDIA will use Feedback at its choice. + +8. NO WARRANTIES. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY EXPRESS OR IMPLIED WARRANTY OF ANY KIND INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, NONINFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. NVIDIA DOES NOT WARRANT THAT THE SOFTWARE WILL MEET YOUR REQUIREMENTS OR THAT THE OPERATION THEREOF WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT ALL ERRORS WILL BE CORRECTED. + +9. LIMITATIONS OF LIABILITY. TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, PROJECT DELAYS, LOSS OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION WITH THIS LICENSE OR THE USE OR PERFORMANCE OF THE SOFTWARE, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF LIABILITY, EVEN IF NVIDIA HAS PREVIOUSLY BEEN ADVISED OF, OR COULD REASONABLY HAVE FORESEEN, THE POSSIBILITY OF SUCH DAMAGES. IN NO EVENT WILL NVIDIA'S AND ITS AFFILIATES TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS LICENSE EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS LIMIT. + +10. TERMINATION. Your rights under this license will terminate automatically without notice from NVIDIA if you fail to comply with any term and condition of this license or if you commence or participate in any legal proceeding against NVIDIA with respect to the SOFTWARE. NVIDIA may terminate this license with advance written notice to you if NVIDIA decides to no longer provide the SOFTWARE in a country or, in NVIDIA's sole discretion, the continued use of it is no longer commercially viable. Upon any termination of this license, you agree to promptly discontinue use of the SOFTWARE and destroy all copies in your possession or control. Your prior distributions in accordance with this license are not affected by the termination of this license. All provisions of this license will survive termination, except for the license granted to you. + +11. APPLICABLE LAW. This license will be governed in all respects by the laws of the United States and of the State of Delaware as those laws are applied to contracts entered into and performed entirely within Delaware by Delaware residents, without regard to the conflicts of laws principles. The United Nations Convention on Contracts for the International Sale of Goods is specifically disclaimed. You agree to all terms of this Agreement in the English language. The state or federal courts residing in Santa Clara County, California shall have exclusive jurisdiction over any dispute or claim arising out of this license. Notwithstanding this, you agree that NVIDIA shall still be allowed to apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +12. NO ASSIGNMENT. This license and your rights and obligations thereunder may not be assigned by you by any means or operation of law without NVIDIA's permission. Any attempted assignment not approved by NVIDIA in writing shall be void and of no effect. + +13. EXPORT. The SOFTWARE is subject to United States export laws and regulations. You agree that you will not ship, transfer or export the SOFTWARE into any country, or use the SOFTWARE in any manner, prohibited by the United States Bureau of Industry and Security or economic sanctions regulations administered by the U.S. Department of Treasury's Office of Foreign Assets Control (OFAC), or any applicable export laws, restrictions or regulations. These laws include restrictions on destinations, end users and end use. By accepting this license, you confirm that you are not a resident or citizen of any country currently embargoed by the U.S. and that you are not otherwise prohibited from receiving the SOFTWARE. + +14. GOVERNMENT USE. The SOFTWARE has been developed entirely at private expense and is "commercial items" consisting of "commercial computer software" and "commercial computer software documentation" provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the U.S. Government or a U.S. Government subcontractor is subject to the restrictions in this license pursuant to DFARS 227.7202-3(a) or as set forth in subparagraphs (b)(1) and (2) of the Commercial Computer Software - Restricted Rights clause at FAR 52.227-19, as applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas Expressway, Santa Clara, CA 95051. + +15. ENTIRE AGREEMENT. This license is the final, complete and exclusive agreement between the parties relating to the subject matter of this license and supersedes all prior or contemporaneous understandings and agreements relating to this subject matter, whether oral or written. If any court of competent jurisdiction determines that any provision of this license is illegal, invalid or unenforceable, the remaining provisions will remain in full force and effect. This license may only be modified in a writing signed by an authorized representative of each party. + +(v. May 12, 2021) + + +cuda-pathfinder +Apache-2.0 +https://github.com/NVIDIA/cuda-python + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +cycler +BSD License +https://matplotlib.org/cycler/ +Copyright (c) 2015, matplotlib project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the matplotlib project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dataclasses-json +MIT License +https://github.com/lidatong/dataclasses-json +MIT License + +Copyright (c) 2019 Charles Li and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +datasets +Apache Software License +https://github.com/huggingface/datasets + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +debugpy +MIT License +https://aka.ms/debugpy + debugpy + + Copyright (c) Microsoft Corporation + All rights reserved. + + MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +decorator +BSD License +UNKNOWN +Copyright (c) 2005-2025, Michele Simionato +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + + +deepdiff +MIT License +https://zepworks.com/deepdiff/ +The MIT License (MIT) + +Copyright (c) 2014 - 2021 Sep Dehpour (Seperman) and contributors +www.zepworks.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +defusedxml +Python Software Foundation License +https://github.com/tiran/defusedxml +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python +alone or in any derivative version, provided, however, that PSF's +License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative +version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + + +diffusers +Apache Software License +https://github.com/huggingface/diffusers + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, Any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +dill +BSD License +https://github.com/uqfoundation/dill +Copyright (c) 2004-2016 California Institute of Technology. +Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. +All rights reserved. + +This software is available subject to the conditions and terms laid +out below. By downloading and using this software you are agreeing +to the following conditions. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the names of the copyright holders nor the names of any of + the contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +distlib +Python Software Foundation License +https://github.com/pypa/distlib +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.2 2.1.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2.1 2.2 2002 PSF yes + 2.2.2 2.2.1 2002 PSF yes + 2.2.3 2.2.2 2003 PSF yes + 2.3 2.2.2 2002-2003 PSF yes + 2.3.1 2.3 2002-2003 PSF yes + 2.3.2 2.3.1 2002-2003 PSF yes + 2.3.3 2.3.2 2002-2003 PSF yes + 2.3.4 2.3.3 2004 PSF yes + 2.3.5 2.3.4 2005 PSF yes + 2.4 2.3 2004 PSF yes + 2.4.1 2.4 2005 PSF yes + 2.4.2 2.4.1 2005 PSF yes + 2.4.3 2.4.2 2006 PSF yes + 2.4.4 2.4.3 2006 PSF yes + 2.5 2.4 2006 PSF yes + 2.5.1 2.5 2007 PSF yes + 2.5.2 2.5.1 2008 PSF yes + 2.5.3 2.5.2 2008 PSF yes + 2.6 2.5 2008 PSF yes + 2.6.1 2.6 2008 PSF yes + 2.6.2 2.6.1 2009 PSF yes + 2.6.3 2.6.2 2009 PSF yes + 2.6.4 2.6.3 2009 PSF yes + 2.6.5 2.6.4 2010 PSF yes + 3.0 2.6 2008 PSF yes + 3.0.1 3.0 2009 PSF yes + 3.1 3.0.1 2009 PSF yes + 3.1.1 3.1 2009 PSF yes + 3.1.2 3.1 2010 PSF yes + 3.2 3.1 2010 PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 +Python Software Foundation; All Rights Reserved" are retained in Python alone or +in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +distro +Apache Software License +https://github.com/python-distro/distro +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +dm-tree +Apache Software License +https://github.com/deepmind/tree + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +docstring_parser +MIT License +https://github.com/rr-/docstring_parser +The MIT License (MIT) + +Copyright (c) 2018 Marcin Kurczewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +donfig +MIT License +https://github.com/pytroll/donfig +Copyright (c) 2018- Donfig Developers +Copyright (c) 2014-2018, Anaconda, Inc. and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +einops +MIT License +https://github.com/arogozhnikov/einops +MIT License + +Copyright (c) 2018 Alex Rogozhnikov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +einx +MIT +https://github.com/fferflo/einx +MIT License + +Copyright (c) 2023- Florian Fervers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +evdev +BSD-3-Clause +https://github.com/gvalkov/python-evdev +Copyright (c) 2012-2025 Georgi Valkov. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + 3. Neither the name of author nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +execnet +MIT +https://execnet.readthedocs.io/en/latest/ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +executing +MIT License +https://github.com/alexmojaki/executing +MIT License + +Copyright (c) 2019 Alex Hall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +fastapi +MIT +https://github.com/fastapi/fastapi +The MIT License (MIT) + +Copyright (c) 2018 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +fastjsonschema +BSD License +https://github.com/horejsek/python-fastjsonschema +Copyright (c) 2018, Michal Horejsek +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +fastparquet +Apache Software License +https://github.com/dask/fastparquet/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +ffmpeg-python +Apache Software License +https://github.com/kkroening/ffmpeg-python + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Karl Kroening + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +ffmpegcv +UNKNOWN +https://github.com/chenxinfeng4/ffmpegcv +UNKNOWN + +ffmpy +MIT +UNKNOWN +UNKNOWN + +filelock +MIT +https://github.com/tox-dev/py-filelock +MIT License + +Copyright (c) 2025 Bernát Gábor and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +flopth +UNKNOWN +UNKNOWN +MIT License + +Copyright (c) 2019 Yunfeng Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +fonttools +MIT +http://github.com/fonttools/fonttools +MIT License + +Copyright (c) 2017 Just van Rossum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +fqdn +Mozilla Public License 2.0 (MPL 2.0) +https://github.com/ypcrts/fqdn +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + + +frozendict +GNU Lesser General Public License v3 (LGPLv3) +https://github.com/Marco-Sulla/python-frozendict + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + +frozenlist +Apache-2.0 +https://github.com/aio-libs/frozenlist +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2013-2019 Nikolay Kim and Andrew Svetlov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +fsspec +BSD-3-Clause +https://github.com/fsspec/filesystem_spec +BSD 3-Clause License + +Copyright (c) 2018, Martin Durant +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ftfy +Apache-2.0 +https://ftfy.readthedocs.io/en/latest/ +Copyright 2023 Robyn Speer + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +future +MIT License +https://python-future.org +Copyright (c) 2013-2024 Python Charmers, Australia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +futureproof +MIT License +https://github.com/yeraydiazdiaz/futureproof +MIT License + +Copyright © 2019, Yeray Díaz Díaz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +fvcore +Apache 2.0 +https://github.com/facebookresearch/fvcore +UNKNOWN + +gast +BSD License +https://github.com/serge-sans-paille/gast/ +Copyright (c) 2016, Serge Guelton +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of HPCProject, Serge Guelton nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + +gitdb +BSD License +https://github.com/gitpython-developers/gitdb +Copyright (C) 2010, 2011 Sebastian Thiel and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the GitDB project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Additional Licenses +------------------- +The files at +gitdb/test/fixtures/packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.idx +and +gitdb/test/fixtures/packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack +are licensed under GNU GPL as part of the git source repository, +see http://en.wikipedia.org/wiki/Git_%28software%29 for more information. + +They are not required for the actual operation, which is why they are not found +in the distribution package. + + +glfw +MIT License +https://github.com/FlorianRhiem/pyGLFW +The MIT License (MIT) + +Copyright (c) 2013-2019 Florian Rhiem + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +google-api-core +Apache Software License +https://github.com/googleapis/google-cloud-python/tree/main/packages/google-api-core + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +google-auth +Apache Software License +https://github.com/googleapis/google-auth-library-python + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +google-cloud-core +Apache Software License +https://github.com/googleapis/python-cloud-core + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +google-cloud-storage +Apache Software License +https://github.com/googleapis/python-storage + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +google-crc32c +UNKNOWN +https://github.com/googleapis/python-crc32c + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +google-resumable-media +Apache Software License +https://github.com/googleapis/google-resumable-media-python + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +googleapis-common-protos +Apache Software License +https://github.com/googleapis/google-cloud-python/tree/main/packages/googleapis-common-protos + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +gradio +Apache-2.0 +https://github.com/gradio-app/gradio + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +gradio_client +Apache-2.0 +https://github.com/gradio-app/gradio +UNKNOWN + +groovy +MIT License +https://github.com/gradio-app/groovy + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +grpcio +Apache-2.0 +https://grpc.io + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +----------------------------------------------------------- + +BSD 3-Clause License + +Copyright 2016, Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------- + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + + +h11 +MIT License +https://github.com/python-hyper/h11 +The MIT License (MIT) + +Copyright (c) 2016 Nathaniel J. Smith and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +h5py +BSD-3-Clause +https://www.h5py.org/ +Copyright (c) 2008 Andrew Collette and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +hatch +MIT +https://hatch.pypa.io/latest/ +MIT License + +Copyright (c) 2017-present Ofek Lev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +hatchling +MIT +https://hatch.pypa.io/latest/ +MIT License + +Copyright (c) 2021-present Ofek Lev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +hf-xet +Apache-2.0 +https://github.com/huggingface/xet-core + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +httpcore +BSD-3-Clause +https://www.encode.io/httpcore/ +Copyright © 2020, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +httptools +MIT +https://github.com/MagicStack/httptools +The MIT License + +Copyright (c) 2015 MagicStack Inc. http://magic.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +httpx +BSD-3-Clause +https://github.com/encode/httpx +Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +huggingface_hub +Apache Software License +https://github.com/huggingface/huggingface_hub + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +hvac +Apache Software License +https://github.com/hvac/hvac + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +hydra-core +MIT License +https://github.com/facebookresearch/hydra +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +hyperlink +MIT License +https://github.com/python-hyper/hyperlink +Copyright (c) 2017 +Glyph Lefkowitz +Itamar Turner-Trauring +Jean Paul Calderone +Adi Roiban +Amber Hawkie Brown +Mahmoud Hashemi +Wilfredo Sanchez Vega + +and others that have contributed code to the public domain. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +idna +BSD-3-Clause +https://github.com/kjd/idna +BSD 3-Clause License + +Copyright (c) 2013-2025, Kim Davies and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +imagecodecs +BSD-3-Clause +https://www.cgohlke.com +BSD-3-Clause license + +Copyright (c) 2008-2026, Christoph Gohlke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +imageio-ffmpeg +BSD License +https://github.com/imageio/imageio-ffmpeg +BSD 2-Clause License + +Copyright (c) 2019-2025, imageio +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +importlib_metadata +Apache-2.0 +https://github.com/python/importlib_metadata +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +iniconfig +MIT +https://github.com/pytest-dev/iniconfig +The MIT License (MIT) + +Copyright (c) 2010 - 2023 Holger Krekel and others + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +iopath +MIT licensed, as found in the LICENSE file +https://github.com/facebookresearch/iopath +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +ipycanvas +BSD License +https://github.com/jupyter-widgets-contrib/ipycanvas +Copyright (c) 2019 Martin Renou +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ipyevents +BSD License +https://github.com/mwcraig/ipyevents +Copyright (c) 2017, Matt Craig +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ipykernel +BSD-3-Clause +https://ipython.org +BSD 3-Clause License + +Copyright (c) 2015, IPython Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ipython +BSD-3-Clause +https://ipython.org +============================= + The IPython licensing terms +============================= + +IPython is licensed under the terms of the Modified BSD License (also known as +New or Revised or 3-Clause BSD). See the LICENSE file. + + +About the IPython Development Team +---------------------------------- + +Fernando Perez began IPython in 2001 based on code from Janko Hauser + and Nathaniel Gray . Fernando is still +the project lead. + +The IPython Development Team is the set of all contributors to the IPython +project. This includes all of the IPython subprojects. + +The core team that coordinates development on GitHub can be found here: +https://github.com/ipython/. + +Our Copyright Policy +-------------------- + +IPython uses a shared copyright model. Each contributor maintains copyright +over their contributions to IPython. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the IPython +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire IPython +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the IPython repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +:: + + # Copyright (c) IPython Development Team. + # Distributed under the terms of the Modified BSD License. + + +ipython_pygments_lexers +BSD License +https://github.com/ipython/ipython-pygments-lexers +BSD 3-Clause License + +- Copyright (c) 2012-Present, IPython Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ipywidgets +BSD License +http://jupyter.org +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +isoduration +ISC License (ISCL) +https://github.com/bolsote/isoduration +Copyright (c) 2020 Víctor Muñoz + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +itsdangerous +BSD License +https://github.com/pallets/itsdangerous/ +Copyright 2011 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jaraco.classes +MIT License +https://github.com/jaraco/jaraco.classes +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +jaraco.context +MIT +https://github.com/jaraco/jaraco.context +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + + +jaraco.functools +MIT +https://github.com/jaraco/jaraco.functools +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + + +jedi +MIT License +https://github.com/davidhalter/jedi +All contributions towards Jedi are MIT licensed. + +------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) <2013> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +jeepney +MIT +https://gitlab.com/takluyver/jeepney +The MIT License (MIT) + +Copyright (c) 2017 Thomas Kluyver + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +jiter +MIT License +https://github.com/pydantic/jiter/ +The MIT License (MIT) + +Copyright (c) 2022 to present Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +jmespath +MIT License +https://github.com/jmespath/jmespath.py +MIT License + +Copyright (c) 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +joblib +BSD-3-Clause +https://joblib.readthedocs.io +BSD 3-Clause License + +Copyright (c) 2008-2021, The joblib developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +json5 +Apache Software License +https://github.com/dpranke/pyjson5 +Files: Everything except for the benchmarks/*.json files. + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- + +File: benchmarks/64KB-min.json + +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +--- + +File: benchmarks/bitly-usa-gov.json + +The MIT License (MIT) + +Copyright (c) 2017 Wes McKinney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +File: benchmarks/twitter.json + +The MIT License (MIT) + +Copyright (c) 2014 Milo Yip + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +jsonlines +BSD License +https://github.com/wbolster/jsonlines +*(This is the OSI approved 3-clause "New BSD License".)* + +Copyright © 2016, wouter bolsterlee + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the author nor the names of the contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jsonpointer +BSD License +https://github.com/stefankoegl/python-json-pointer +Copyright (c) 2011 Stefan Kögl +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +jsonschema +MIT +https://github.com/python-jsonschema/jsonschema +Copyright (c) 2013 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +jsonschema-specifications +MIT +https://github.com/python-jsonschema/jsonschema-specifications +Copyright (c) 2022 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +jupyter-compare-view +MIT License +UNKNOWN +MIT License + +Copyright (c) 2022 Octoframes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +jupyter-events +BSD License +http://jupyter.org +BSD 3-Clause License + +Copyright (c) 2022-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyter-lsp +BSD License +https://github.com/jupyter-lsp/jupyterlab-lsp/issues +BSD 3-Clause License + +Copyright (c) 2022, jupyter-lsp contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyter_client +BSD License +https://jupyter.org +BSD 3-Clause License + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyter_core +BSD-3-Clause +https://jupyter.org +BSD 3-Clause License + +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyter_server +BSD License +https://jupyter-server.readthedocs.io +BSD 3-Clause License + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyter_server_terminals +BSD License +https://jupyter.org +BSD 3-Clause License + +- Copyright (c) 2021-, Jupyter Development Team + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +All rights reserved. + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyterlab +BSD License +https://jupyter.org +Copyright (c) 2015-2025 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + +jupyterlab_pygments +BSD License +https://github.com/jupyterlab/jupyterlab_pygments +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyterlab_server +BSD License +https://jupyterlab-server.readthedocs.io +Copyright (c) 2015-2017, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +jupyterlab_widgets +BSD License +https://github.com/jupyter-widgets/ipywidgets +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +This package bundles several JavaScript npm packages in the +jupyterlab_widgets/static directory. Their licenses (as packaged in their +distributions in the node_modules package installation directory) are copied +below. + +------------------------------------------------------------------------------ +From css-loader/LICENSE: + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------ +From style-loader/LICENSE: + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------ +From backbone/backbone.js + +// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +------------------------------------------------------------------------------ +From base-64/LICENSE + +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------------------ +From lodash/LICENSE + +Copyright OpenJS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + +------------------------------------------------------------------------------ +From d3-format/LICENSE: + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +From noUISlider/LICENSE.md (https://github.com/leongersen/noUiSlider/blob/eca62f9e56aaf02f0841b36e7993adf8db3721d5/LICENSE.md) + +MIT License + +Copyright (c) 2019 Léon Gersen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------ +From jquery/LICENSE.txt + +Copyright JS Foundation and other contributors, https://js.foundation/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------ +From semver/LICENSE: + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------ +From underscore/LICENSE + +Copyright (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +keyring +MIT +https://github.com/jaraco/keyring +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + + +kiwisolver +BSD License +https://github.com/nucleic/kiwi +========================= + The Kiwi licensing terms +========================= +Kiwi is licensed under the terms of the Modified BSD License (also known as +New or Revised BSD), as follows: + +Copyright (c) 2013-2025, Nucleic Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Nucleic Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +About Kiwi +---------- +Chris Colbert began the Kiwi project in December 2013 in an effort to +create a blisteringly fast UI constraint solver. Chris is still the +project lead. + +The Nucleic Development Team is the set of all contributors to the Nucleic +project and its subprojects. + +The core team that coordinates development on GitHub can be found here: +http://github.com/nucleic. The current team consists of: + +* Chris Colbert + +Our Copyright Policy +-------------------- +Nucleic uses a shared copyright model. Each contributor maintains copyright +over their contributions to Nucleic. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Nucleic +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Nucleic +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Nucleic repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +#------------------------------------------------------------------------------ +# Copyright (c) 2013-2025, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +#------------------------------------------------------------------------------ + + +kornia +Apache Software License +https://kornia.github.io/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +kornia_rs +Apache Software License +http://kornia.org + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +lark +MIT License +https://github.com/lark-parser/lark +Copyright © 2017 Erez Shinan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +lazy_loader +BSD License +https://github.com/scientific-python/lazy_loader +BSD 3-Clause License + +Copyright (c) 2022--2023, Scientific Python project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +lerobot +Apache Software License +https://huggingface.co/lerobot +Copyright 2024 The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## Some of lerobot's code is derived from Diffusion Policy, which is subject to the following copyright notice: + +MIT License + +Copyright (c) 2023 Columbia Artificial Intelligence and Robotics Lab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## Some of lerobot's code is derived from FOWM, which is subject to the following copyright notice: + +MIT License + +Copyright (c) 2023 Yunhai Feng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## Some of lerobot's code is derived from simxarm, which is subject to the following copyright notice: + +MIT License + +Copyright (c) 2023 Nicklas Hansen & Yanjie Ze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## Some of lerobot's code is derived from ALOHA, which is subject to the following copyright notice: + +MIT License + +Copyright (c) 2023 Tony Z. Zhao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Some of lerobot's code is derived from DETR, which is subject to the following copyright notice: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 - present, Facebook, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +lightning +Apache Software License +https://github.com/Lightning-AI/lightning + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 William Falcon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +lightning-utilities +Apache-2.0 +https://github.com/Lightning-AI/utilities + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 William Falcon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +loguru +MIT License +https://github.com/Delgan/loguru +UNKNOWN + +lpips +BSD License +https://github.com/richzhang/PerceptualSimilarity +Copyright (c) 2018, Richard Zhang, Phillip Isola, Alexei A. Efros, Eli Shechtman, Oliver Wang +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +lxml +BSD-3-Clause +https://lxml.de/ +BSD 3-Clause License + +Copyright (c) 2004 Infrae. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + 3. Neither the name of Infrae nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +lz4 +BSD License +https://github.com/python-lz4/python-lz4 +Copyright (c) 2012-2013, Steeve Morin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of Steeve Morin nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +makefun +BSD License +https://github.com/smarie/python-makefun +BSD 3-Clause License + +Copyright (c) 2019-2022, Sylvain Marié, Schneider Electric Industries +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +markdown-it-py +MIT License +https://github.com/executablebooks/markdown-it-py +MIT License + +Copyright (c) 2020 ExecutableBookProject + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +marshmallow +MIT License +https://github.com/marshmallow-code/marshmallow +Copyright Steven Loria and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +matplotlib +Python Software Foundation License +https://matplotlib.org +License agreement for matplotlib versions 1.3.0 and later +========================================================= + +1. This LICENSE AGREEMENT is between the Matplotlib Development Team +("MDT"), and the Individual or Organization ("Licensee") accessing and +otherwise using matplotlib software in source or binary form and its +associated documentation. + +2. Subject to the terms and conditions of this License Agreement, MDT +hereby grants Licensee a nonexclusive, royalty-free, world-wide license +to reproduce, analyze, test, perform and/or display publicly, prepare +derivative works, distribute, and otherwise use matplotlib +alone or in any derivative version, provided, however, that MDT's +License Agreement and MDT's notice of copyright, i.e., "Copyright (c) +2012- Matplotlib Development Team; All Rights Reserved" are retained in +matplotlib alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on or +incorporates matplotlib or any part thereof, and wants to +make the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to matplotlib . + +4. MDT is making matplotlib available to Licensee on an "AS +IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB +WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR +LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING +MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF +THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between MDT and +Licensee. This License Agreement does not grant permission to use MDT +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using matplotlib , +Licensee agrees to be bound by the terms and conditions of this License +Agreement. + +License agreement for matplotlib versions prior to 1.3.0 +======================================================== + +1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the +Individual or Organization ("Licensee") accessing and otherwise using +matplotlib software in source or binary form and its associated +documentation. + +2. Subject to the terms and conditions of this License Agreement, JDH +hereby grants Licensee a nonexclusive, royalty-free, world-wide license +to reproduce, analyze, test, perform and/or display publicly, prepare +derivative works, distribute, and otherwise use matplotlib +alone or in any derivative version, provided, however, that JDH's +License Agreement and JDH's notice of copyright, i.e., "Copyright (c) +2002-2011 John D. Hunter; All Rights Reserved" are retained in +matplotlib alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on or +incorporates matplotlib or any part thereof, and wants to +make the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to matplotlib. + +4. JDH is making matplotlib available to Licensee on an "AS +IS" basis. JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB +WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR +LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING +MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF +THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between JDH and +Licensee. This License Agreement does not grant permission to use JDH +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using matplotlib, +Licensee agrees to be bound by the terms and conditions of this License +Agreement. +---- + +This binary distrubution of Matplotlib can also bundle the following software +(depending on the build): + +Name: AMS Fonts +Files: matplotlib/tests/cmr10.pfb +Description: Type-1 version of one of Knuth's Computer Modern fonts +License: OFL-1.1 + The cmr10.pfb file is a Type-1 version of one of Knuth's Computer Modern fonts. + It is included here as test data only, but the following license applies. + + Copyright (c) 1997, 2009, American Mathematical Society (http://www.ams.org). + All Rights Reserved. + + "cmb10" is a Reserved Font Name for this Font Software. + "cmbsy10" is a Reserved Font Name for this Font Software. + "cmbsy5" is a Reserved Font Name for this Font Software. + "cmbsy6" is a Reserved Font Name for this Font Software. + "cmbsy7" is a Reserved Font Name for this Font Software. + "cmbsy8" is a Reserved Font Name for this Font Software. + "cmbsy9" is a Reserved Font Name for this Font Software. + "cmbx10" is a Reserved Font Name for this Font Software. + "cmbx12" is a Reserved Font Name for this Font Software. + "cmbx5" is a Reserved Font Name for this Font Software. + "cmbx6" is a Reserved Font Name for this Font Software. + "cmbx7" is a Reserved Font Name for this Font Software. + "cmbx8" is a Reserved Font Name for this Font Software. + "cmbx9" is a Reserved Font Name for this Font Software. + "cmbxsl10" is a Reserved Font Name for this Font Software. + "cmbxti10" is a Reserved Font Name for this Font Software. + "cmcsc10" is a Reserved Font Name for this Font Software. + "cmcsc8" is a Reserved Font Name for this Font Software. + "cmcsc9" is a Reserved Font Name for this Font Software. + "cmdunh10" is a Reserved Font Name for this Font Software. + "cmex10" is a Reserved Font Name for this Font Software. + "cmex7" is a Reserved Font Name for this Font Software. + "cmex8" is a Reserved Font Name for this Font Software. + "cmex9" is a Reserved Font Name for this Font Software. + "cmff10" is a Reserved Font Name for this Font Software. + "cmfi10" is a Reserved Font Name for this Font Software. + "cmfib8" is a Reserved Font Name for this Font Software. + "cminch" is a Reserved Font Name for this Font Software. + "cmitt10" is a Reserved Font Name for this Font Software. + "cmmi10" is a Reserved Font Name for this Font Software. + "cmmi12" is a Reserved Font Name for this Font Software. + "cmmi5" is a Reserved Font Name for this Font Software. + "cmmi6" is a Reserved Font Name for this Font Software. + "cmmi7" is a Reserved Font Name for this Font Software. + "cmmi8" is a Reserved Font Name for this Font Software. + "cmmi9" is a Reserved Font Name for this Font Software. + "cmmib10" is a Reserved Font Name for this Font Software. + "cmmib5" is a Reserved Font Name for this Font Software. + "cmmib6" is a Reserved Font Name for this Font Software. + "cmmib7" is a Reserved Font Name for this Font Software. + "cmmib8" is a Reserved Font Name for this Font Software. + "cmmib9" is a Reserved Font Name for this Font Software. + "cmr10" is a Reserved Font Name for this Font Software. + "cmr12" is a Reserved Font Name for this Font Software. + "cmr17" is a Reserved Font Name for this Font Software. + "cmr5" is a Reserved Font Name for this Font Software. + "cmr6" is a Reserved Font Name for this Font Software. + "cmr7" is a Reserved Font Name for this Font Software. + "cmr8" is a Reserved Font Name for this Font Software. + "cmr9" is a Reserved Font Name for this Font Software. + "cmsl10" is a Reserved Font Name for this Font Software. + "cmsl12" is a Reserved Font Name for this Font Software. + "cmsl8" is a Reserved Font Name for this Font Software. + "cmsl9" is a Reserved Font Name for this Font Software. + "cmsltt10" is a Reserved Font Name for this Font Software. + "cmss10" is a Reserved Font Name for this Font Software. + "cmss12" is a Reserved Font Name for this Font Software. + "cmss17" is a Reserved Font Name for this Font Software. + "cmss8" is a Reserved Font Name for this Font Software. + "cmss9" is a Reserved Font Name for this Font Software. + "cmssbx10" is a Reserved Font Name for this Font Software. + "cmssdc10" is a Reserved Font Name for this Font Software. + "cmssi10" is a Reserved Font Name for this Font Software. + "cmssi12" is a Reserved Font Name for this Font Software. + "cmssi17" is a Reserved Font Name for this Font Software. + "cmssi8" is a Reserved Font Name for this Font Software. + "cmssi9" is a Reserved Font Name for this Font Software. + "cmssq8" is a Reserved Font Name for this Font Software. + "cmssqi8" is a Reserved Font Name for this Font Software. + "cmsy10" is a Reserved Font Name for this Font Software. + "cmsy5" is a Reserved Font Name for this Font Software. + "cmsy6" is a Reserved Font Name for this Font Software. + "cmsy7" is a Reserved Font Name for this Font Software. + "cmsy8" is a Reserved Font Name for this Font Software. + "cmsy9" is a Reserved Font Name for this Font Software. + "cmtcsc10" is a Reserved Font Name for this Font Software. + "cmtex10" is a Reserved Font Name for this Font Software. + "cmtex8" is a Reserved Font Name for this Font Software. + "cmtex9" is a Reserved Font Name for this Font Software. + "cmti10" is a Reserved Font Name for this Font Software. + "cmti12" is a Reserved Font Name for this Font Software. + "cmti7" is a Reserved Font Name for this Font Software. + "cmti8" is a Reserved Font Name for this Font Software. + "cmti9" is a Reserved Font Name for this Font Software. + "cmtt10" is a Reserved Font Name for this Font Software. + "cmtt12" is a Reserved Font Name for this Font Software. + "cmtt8" is a Reserved Font Name for this Font Software. + "cmtt9" is a Reserved Font Name for this Font Software. + "cmu10" is a Reserved Font Name for this Font Software. + "cmvtt10" is a Reserved Font Name for this Font Software. + "euex10" is a Reserved Font Name for this Font Software. + "euex7" is a Reserved Font Name for this Font Software. + "euex8" is a Reserved Font Name for this Font Software. + "euex9" is a Reserved Font Name for this Font Software. + "eufb10" is a Reserved Font Name for this Font Software. + "eufb5" is a Reserved Font Name for this Font Software. + "eufb7" is a Reserved Font Name for this Font Software. + "eufm10" is a Reserved Font Name for this Font Software. + "eufm5" is a Reserved Font Name for this Font Software. + "eufm7" is a Reserved Font Name for this Font Software. + "eurb10" is a Reserved Font Name for this Font Software. + "eurb5" is a Reserved Font Name for this Font Software. + "eurb7" is a Reserved Font Name for this Font Software. + "eurm10" is a Reserved Font Name for this Font Software. + "eurm5" is a Reserved Font Name for this Font Software. + "eurm7" is a Reserved Font Name for this Font Software. + "eusb10" is a Reserved Font Name for this Font Software. + "eusb5" is a Reserved Font Name for this Font Software. + "eusb7" is a Reserved Font Name for this Font Software. + "eusm10" is a Reserved Font Name for this Font Software. + "eusm5" is a Reserved Font Name for this Font Software. + "eusm7" is a Reserved Font Name for this Font Software. + "lasy10" is a Reserved Font Name for this Font Software. + "lasy5" is a Reserved Font Name for this Font Software. + "lasy6" is a Reserved Font Name for this Font Software. + "lasy7" is a Reserved Font Name for this Font Software. + "lasy8" is a Reserved Font Name for this Font Software. + "lasy9" is a Reserved Font Name for this Font Software. + "lasyb10" is a Reserved Font Name for this Font Software. + "lcircle1" is a Reserved Font Name for this Font Software. + "lcirclew" is a Reserved Font Name for this Font Software. + "lcmss8" is a Reserved Font Name for this Font Software. + "lcmssb8" is a Reserved Font Name for this Font Software. + "lcmssi8" is a Reserved Font Name for this Font Software. + "line10" is a Reserved Font Name for this Font Software. + "linew10" is a Reserved Font Name for this Font Software. + "msam10" is a Reserved Font Name for this Font Software. + "msam5" is a Reserved Font Name for this Font Software. + "msam6" is a Reserved Font Name for this Font Software. + "msam7" is a Reserved Font Name for this Font Software. + "msam8" is a Reserved Font Name for this Font Software. + "msam9" is a Reserved Font Name for this Font Software. + "msbm10" is a Reserved Font Name for this Font Software. + "msbm5" is a Reserved Font Name for this Font Software. + "msbm6" is a Reserved Font Name for this Font Software. + "msbm7" is a Reserved Font Name for this Font Software. + "msbm8" is a Reserved Font Name for this Font Software. + "msbm9" is a Reserved Font Name for this Font Software. + "wncyb10" is a Reserved Font Name for this Font Software. + "wncyi10" is a Reserved Font Name for this Font Software. + "wncyr10" is a Reserved Font Name for this Font Software. + "wncysc10" is a Reserved Font Name for this Font Software. + "wncyss10" is a Reserved Font Name for this Font Software. + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + + ----------------------------------------------------------- + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + ----------------------------------------------------------- + + PREAMBLE + The goals of the Open Font License (OFL) are to stimulate worldwide + development of collaborative font projects, to support the font creation + efforts of academic and linguistic communities, and to provide a free and + open framework in which fonts may be shared and improved in partnership + with others. + + The OFL allows the licensed fonts to be used, studied, modified and + redistributed freely as long as they are not sold by themselves. The + fonts, including any derivative works, can be bundled, embedded, + redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, + however, cannot be released under any other type of license. The + requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + + DEFINITIONS + "Font Software" refers to the set of files released by the Copyright + Holder(s) under this license and clearly marked as such. This may + include source files, build scripts and documentation. + + "Reserved Font Name" refers to any names specified as such after the + copyright statement(s). + + "Original Version" refers to the collection of Font Software components as + distributed by the Copyright Holder(s). + + "Modified Version" refers to any derivative made by adding to, deleting, + or substituting -- in part or in whole -- any of the components of the + Original Version, by changing formats or by porting the Font Software to a + new environment. + + "Author" refers to any designer, engineer, programmer, technical + writer or other person who contributed to the Font Software. + + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Font Software, to use, study, copy, merge, embed, modify, + redistribute, and sell modified and unmodified copies of the Font + Software, subject to the following conditions: + + 1) Neither the Font Software nor any of its individual components, + in Original or Modified Versions, may be sold by itself. + + 2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + + 3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the corresponding + Copyright Holder. This restriction only applies to the primary font name as + presented to the users. + + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + + 5) The Font Software, modified or unmodified, in part or in whole, + must be distributed entirely under this license, and must not be + distributed under any other license. The requirement for fonts to + remain under this license does not apply to any document created + using the Font Software. + + TERMINATION + This license becomes null and void if any of the above conditions are + not met. + + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + OTHER DEALINGS IN THE FONT SOFTWARE. + + + +Name: BaKoMa Fonts +Files: matplotlib/mpl-data/fonts/ttf/cm*.ttf matplotlib/mpl-data/fonts/afm/cm*.afm +Description: Computer Modern Fonts in PostScript Type 1 and TrueType font formats. +License: BaKoMa Fonts Licence + BaKoMa Fonts Licence + -------------------- + + This licence covers two font packs (known as BaKoMa Fonts Collection, + which is available at `CTAN:fonts/cm/ps-type1/bakoma/'): + + 1) BaKoMa-CM (1.1/12-Nov-94) + Computer Modern Fonts in PostScript Type 1 and TrueType font formats. + + 2) BaKoMa-AMS (1.2/19-Jan-95) + AMS TeX fonts in PostScript Type 1 and TrueType font formats. + + Copyright (C) 1994, 1995, Basil K. Malyshev. All Rights Reserved. + + Permission to copy and distribute these fonts for any purpose is + hereby granted without fee, provided that the above copyright notice, + author statement and this permission notice appear in all copies of + these fonts and related documentation. + + Permission to modify and distribute modified fonts for any purpose is + hereby granted without fee, provided that the copyright notice, + author statement, this permission notice and location of original + fonts (http://www.ctan.org/tex-archive/fonts/cm/ps-type1/bakoma) + appear in all copies of modified fonts and related documentation. + + Permission to use these fonts (embedding into PostScript, PDF, SVG + and printing by using any software) is hereby granted without fee. + It is not required to provide any notices about using these fonts. + + Basil K. Malyshev + INSTITUTE FOR HIGH ENERGY PHYSICS + IHEP, OMVT + Moscow Region + 142281 PROTVINO + RUSSIA + + E-Mail: bakoma@mail.ru + or malyshev@mail.ihep.ru + + + + +Name: ColorBrewer Color Schemes +Files: lib/matplotlib/_cm.py +Description: Color schemes from ColorBrewer +License: Apache-2.0 + Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes + + Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. + + +Name: Courier 10 +Files: matplotlib/tests/Courier10PitchBT-Bold.pfb +Description: Courier 10 font, used in tests. +License: Bitstream-Charter + The Courier10PitchBT-Bold.pfb file is a Type-1 version of + Courier 10 Pitch BT Bold by Bitstream, obtained from + . It is included + here as test data only, but the following license applies. + + + (c) Copyright 1989-1992, Bitstream Inc., Cambridge, MA. + + You are hereby granted permission under all Bitstream propriety rights + to use, copy, modify, sublicense, sell, and redistribute the 4 Bitstream + Charter (r) Type 1 outline fonts and the 4 Courier Type 1 outline fonts + for any purpose and without restriction; provided, that this notice is + left intact on all copies of such fonts and that Bitstream's trademark + is acknowledged as shown below on all unmodified copies of the 4 Charter + Type 1 fonts. + + BITSTREAM CHARTER is a registered trademark of Bitstream Inc. + + + +Name: JSXTools resize observer +Files: +Description: Minimal polyfill for the ResizeObserver API +License: CC0-1.0 + # CC0 1.0 Universal + + ## Statement of Purpose + + The laws of most jurisdictions throughout the world automatically confer + exclusive Copyright and Related Rights (defined below) upon the creator and + subsequent owner(s) (each and all, an “owner”) of an original work of + authorship and/or a database (each, a “Work”). + + Certain owners wish to permanently relinquish those rights to a Work for the + purpose of contributing to a commons of creative, cultural and scientific works + (“Commons”) that the public can reliably and without fear of later claims of + infringement build upon, modify, incorporate in other works, reuse and + redistribute as freely as possible in any form whatsoever and for any purposes, + including without limitation commercial purposes. These owners may contribute + to the Commons to promote the ideal of a free culture and the further + production of creative, cultural and scientific works, or to gain reputation or + greater distribution for their Work in part through the use and efforts of + others. + + For these and/or other purposes and motivations, and without any expectation of + additional consideration or compensation, the person associating CC0 with a + Work (the “Affirmer”), to the extent that he or she is an owner of Copyright + and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and + publicly distribute the Work under its terms, with knowledge of his or her + Copyright and Related Rights in the Work and the meaning and intended legal + effect of CC0 on those rights. + + 1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + + 2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + + 3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + + 4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + + For more information, please see + http://creativecommons.org/publicdomain/zero/1.0/. + + +Name: QHull +Files: matplotlib/_qhull.*.so +Description: Convex hull, Delaunay triangulation, Voronoi diagrams, Halfspace intersection +License: Qhull + Qhull, Copyright (c) 1993-2020 + + C.B. Barber + Arlington, MA + + and + + The National Science and Technology Research Center for + Computation and Visualization of Geometric Structures + (The Geometry Center) + University of Minnesota + + email: qhull@qhull.org + + This software includes Qhull from C.B. Barber and The Geometry Center. + Files derived from Qhull 1.0 are copyrighted by the Geometry Center. The + remaining files are copyrighted by C.B. Barber. Qhull is free software + and may be obtained via http from www.qhull.org. It may be freely copied, + modified, and redistributed under the following conditions: + + 1. All copyright notices must remain intact in all files. + + 2. A copy of this text file must be distributed along with any copies + of Qhull that you redistribute; this includes copies that you have + modified, or copies of programs or other software products that + include Qhull. + + 3. If you modify Qhull, you must include a notice giving the + name of the person performing the modification, the date of + modification, and the reason for such modification. + + 4. When distributing modified versions of Qhull, or other software + products that include Qhull, you must provide notice that the original + source code may be obtained as noted above. + + 5. There is no warranty or other guarantee of fitness for Qhull, it is + provided solely "as is". Bug reports or fixes may be sent to + qhull_bug@qhull.org; the authors may or may not act on them as + they desire. + + +Name: Qt4 Editor +Files: matplotlib/backends/qt_editor +Description: Module creating PyQt4 form dialogs/layouts to edit various type of parameters +License: MIT + Module creating PyQt4 form dialogs/layouts to edit various type of parameters + + + formlayout License Agreement (MIT License) + ------------------------------------------ + + Copyright (c) 2009 Pierre Raybaut + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + """ + + +Name: Solarized +Files: matplotlib/mpl-data/stylelib/Solarize_Light2.mplstyle +Description: Solarized color scheme/style +License: MIT + https://github.com/altercation/solarized/blob/master/LICENSE + Copyright (c) 2011 Ethan Schoonover + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +Name: Stix fonts +Files: matplotlib/mpl-data/fonts/ttf/STIX*.ttf +Description: STIX fonts +License: + TERMS AND CONDITIONS + + 1. Permission is hereby granted, free of charge, to any person + obtaining a copy of the STIX Fonts-TM set accompanying this license + (collectively, the "Fonts") and the associated documentation files + (collectively with the Fonts, the "Font Software"), to reproduce and + distribute the Font Software, including the rights to use, copy, merge + and publish copies of the Font Software, and to permit persons to whom + the Font Software is furnished to do so same, subject to the following + terms and conditions (the "License"). + + 2. The following copyright and trademark notice and these Terms and + Conditions shall be included in all copies of one or more of the Font + typefaces and any derivative work created as permitted under this + License: + + Copyright (c) 2001-2005 by the STI Pub Companies, consisting of + the American Institute of Physics, the American Chemical Society, the + American Mathematical Society, the American Physical Society, Elsevier, + Inc., and The Institute of Electrical and Electronic Engineers, Inc. + Portions copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright + (c) 1990 by Elsevier, Inc. All rights reserved. STIX Fonts-TM is a + trademark of The Institute of Electrical and Electronics Engineers, Inc. + + 3. You may (a) convert the Fonts from one format to another (e.g., + from TrueType to PostScript), in which case the normal and reasonable + distortion that occurs during such conversion shall be permitted and (b) + embed or include a subset of the Fonts in a document for the purposes of + allowing users to read text in the document that utilizes the Fonts. In + each case, you may use the STIX Fonts-TM mark to designate the resulting + Fonts or subset of the Fonts. + + 4. You may also (a) add glyphs or characters to the Fonts, or modify + the shape of existing glyphs, so long as the base set of glyphs is not + removed and (b) delete glyphs or characters from the Fonts, provided + that the resulting font set is distributed with the following + disclaimer: "This [name] font does not include all the Unicode points + covered in the STIX Fonts-TM set but may include others." In each case, + the name used to denote the resulting font set shall not include the + term "STIX" or any similar term. + + 5. You may charge a fee in connection with the distribution of the + Font Software, provided that no copy of one or more of the individual + Font typefaces that form the STIX Fonts-TM set may be sold by itself. + + 6. THE FONT SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY + KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK OR OTHER RIGHT. IN NO EVENT SHALL + MICROPRESS OR ANY OF THE STI PUB COMPANIES BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, INCLUDING, BUT NOT LIMITED TO, ANY GENERAL, + SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM OR OUT OF THE USE OR + INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT + SOFTWARE. + + 7. Except as contained in the notice set forth in Section 2, the + names MicroPress Inc. and STI Pub Companies, as well as the names of the + companies/organizations that compose the STI Pub Companies, shall not be + used in advertising or otherwise to promote the sale, use or other + dealings in the Font Software without the prior written consent of the + respective company or organization. + + 8. This License shall become null and void in the event of any + material breach of the Terms and Conditions herein by licensee. + + 9. A substantial portion of the STIX Fonts set was developed by + MicroPress Inc. for the STI Pub Companies. To obtain additional + mathematical fonts, please contact MicroPress, Inc., 68-30 Harrow + Street, Forest Hills, NY 11375, USA - Phone: (718) 575-1816. + + +Name: Yorick Colormaps +Files: lib/matplotlib/_cm.py +Description: Gist/Yorick colormaps +License: + BSD-style license for gist/yorick colormaps. + + Copyright: + + Copyright (c) 1996. The Regents of the University of California. + All rights reserved. + + Permission to use, copy, modify, and distribute this software for any + purpose without fee is hereby granted, provided that this entire + notice is included in all copies of any software which is or includes + a copy or modification of this software and in all copies of the + supporting documentation for such software. + + This work was produced at the University of California, Lawrence + Livermore National Laboratory under contract no. W-7405-ENG-48 between + the U.S. Department of Energy and The Regents of the University of + California for the operation of UC LLNL. + + + DISCLAIMER + + This software was prepared as an account of work sponsored by an + agency of the United States Government. Neither the United States + Government nor the University of California nor any of their + employees, makes any warranty, express or implied, or assumes any + liability or responsibility for the accuracy, completeness, or + usefulness of any information, apparatus, product, or process + disclosed, or represents that its use would not infringe + privately-owned rights. Reference herein to any specific commercial + products, process, or service by trade name, trademark, manufacturer, + or otherwise, does not necessarily constitute or imply its + endorsement, recommendation, or favoring by the United States + Government or the University of California. The views and opinions of + authors expressed herein do not necessarily state or reflect those of + the United States Government or the University of California, and + shall not be used for advertising or product endorsement purposes. + + + AUTHOR + + David H. Munro wrote Yorick and Gist. Berkeley Yacc (byacc) generated + the Yorick parser. The routines in Math are from LAPACK and FFTPACK; + MathC contains C translations by David H. Munro. The algorithms for + Yorick's random number generator and several special functions in + Yorick/include were taken from Numerical Recipes by Press, et. al., + although the Yorick implementations are unrelated to those in + Numerical Recipes. A small amount of code in Gist was adapted from + the X11R4 release, copyright M.I.T. -- the complete copyright notice + may be found in the (unused) file Gist/host.c. + + +matplotlib-inline +UNKNOWN +https://github.com/ipython/matplotlib-inline +BSD 3-Clause License + +Copyright (c) 2019-2022, IPython Development Team. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +mdurl +MIT License +https://github.com/executablebooks/mdurl +Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin. +Copyright (c) 2021 Taneli Hukkinen + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +.parse() is based on Joyent's node.js `url` code: + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +mediapy +Apache Software License +https://github.com/google/mediapy + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +megatron-core +BSD License +https://github.com/NVIDIA/Megatron-LM/megatron/core +The following applies to all files unless otherwise noted: + +# Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- + +This repository also contains code from Hugging Face Inc., Google Research, +Facebook (from their Fairseq, Dino, and ParlAI projects), Microsoft (from their +Swin-Transformer project), Philip Popien, the Mamba project (Tri Dao and +Albert Gu), and the Triton language and compiler project (Philippe Tillet and +OpenAI). Files from these organizations have notices at the top of each file. +Below are licenses used in those files, as indicated. + + +-------------------------------------------------------------------------------------- +-- LICENSE FOR Facebook, huggingface, Google Research, LLaVA, Mamba, TinyZero and vLLM code -- + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- +LICENSE FOR +Facebook, Inc. and its affiliates, +Meta Platforms, Inc. and its affiliates, +Microsoft Corporation, +OpenGVLab/InternVL, +Triton language and compiler, +and DeepSeek. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +LICENSE FOR Thinking Machines Lab + +MIT License + +Copyright 2025 Thinking Machines Lab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +LICENSE FOR +Meta Platforms, Inc. and affiliates. + +BSD License + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Meta nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +mistune +BSD License +https://github.com/lepture/mistune +Copyright (c) 2014, Hsiaoming Yang + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the creator nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ml_dtypes +Apache-2.0 +https://github.com/jax-ml/ml_dtypes + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +more-itertools +MIT +https://github.com/more-itertools/more-itertools +Copyright (c) 2012 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +moviepy +MIT License +UNKNOWN +The MIT License (MIT) + +Copyright (c) 2015 Zulko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +mpmath +BSD License +http://mpmath.org/ +Copyright (c) 2005-2021 Fredrik Johansson and mpmath contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + + +msgpack +Apache-2.0 +https://msgpack.org/ +Copyright (C) 2008-2011 INADA Naoki + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +multi-storage-client +Apache-2.0 +https://github.com/NVIDIA/multi-storage-client +UNKNOWN + +multidict +Apache License 2.0 +https://github.com/aio-libs/multidict + Copyright 2016 Andrew Svetlov and aio-libs contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +multiprocess +BSD License +https://github.com/uqfoundation/multiprocess +Copyright (c) 2006-2008, R Oudkerk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of author nor the names of any contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + +mypy_extensions +MIT +https://github.com/python/mypy_extensions +Mypy extensions are licensed under the terms of the MIT license, reproduced below. + += = = = = + +The MIT License + +Copyright (c) 2016-2017 Jukka Lehtosalo and contributors + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + += = = = = + + +nbclient +BSD License +https://jupyter.org +BSD 3-Clause License + +Copyright (c) 2020-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +nbconvert +BSD License +https://jupyter.org +BSD 3-Clause License + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +nbformat +BSD License +https://jupyter.org +BSD 3-Clause License + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +nest-asyncio +BSD License +https://github.com/erdewit/nest_asyncio +BSD 2-Clause License + +Copyright (c) 2018-2020, Ewald de Wit +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +networkx +BSD-3-Clause +https://networkx.org/ +NetworkX is distributed with the 3-clause BSD license. + +:: + + Copyright (c) 2004-2025, NetworkX Developers + Aric Hagberg + Dan Schult + Pieter Swart + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NetworkX Developers nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +ninja +Apache Software License; BSD License +http://ninja-build.org/ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +nltk +Apache Software License +https://www.nltk.org/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +notebook_shim +BSD License +UNKNOWN +BSD 3-Clause License + +Copyright (c) 2022 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +numcodecs +MIT +https://github.com/zarr-developers/numcodecs +The MIT License (MIT) + +Copyright (c) 2015-2018 Zarr Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +numpy +BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 +https://numpy.org +Copyright (c) 2005-2025, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + + +---- + +This binary distribution of NumPy also bundles the following software: + + +Name: OpenBLAS +Files: numpy.libs/libscipy_openblas*.so +Description: bundled as a dynamically linked library +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause + Copyright (c) 2011-2014, The OpenBLAS Project + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: LAPACK +Files: numpy.libs/libscipy_openblas*.so +Description: bundled in OpenBLAS +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause-Open-MPI + Copyright (c) 1992-2013 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. + Copyright (c) 2000-2013 The University of California Berkeley. All + rights reserved. + Copyright (c) 2006-2013 The University of Colorado Denver. All rights + reserved. + + $COPYRIGHT$ + + Additional copyrights may follow + + $HEADER$ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + + - Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + The copyright holders provide no reassurances that the source code + provided does not infringe any patent, copyright, or any other + intellectual property rights of third parties. The copyright holders + disclaim any liability to any recipient for claims brought against + recipient by any third party for infringement of that parties + intellectual property rights. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: GCC runtime library +Files: numpy.libs/libgfortran*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran +License: GPL-3.0-or-later WITH GCC-exception-3.1 + Copyright (C) 2002-2017 Free Software Foundation, Inc. + + Libgfortran is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + Libgfortran is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Under Section 7 of GPL version 3, you are granted additional + permissions described in the GCC Runtime Library Exception, version + 3.1, as published by the Free Software Foundation. + + You should have received a copy of the GNU General Public License and + a copy of the GCC Runtime Library Exception along with this program; + see the files COPYING3 and COPYING.RUNTIME respectively. If not, see + . + +---- + +Full text of license texts referred to above follows (that they are +listed below does not necessarily imply the conditions apply to the +present binary release): + +---- + +GCC RUNTIME LIBRARY EXCEPTION + +Version 3.1, 31 March 2009 + +Copyright (C) 2009 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This GCC Runtime Library Exception ("Exception") is an additional +permission under section 7 of the GNU General Public License, version +3 ("GPLv3"). It applies to a given file (the "Runtime Library") that +bears a notice placed by the copyright holder of the file stating that +the file is governed by GPLv3 along with this Exception. + +When you use GCC to compile a program, GCC may combine portions of +certain GCC header files and runtime libraries with the compiled +program. The purpose of this Exception is to allow compilation of +non-GPL (including proprietary) programs to use, in this way, the +header files and runtime libraries covered by this Exception. + +0. Definitions. + +A file is an "Independent Module" if it either requires the Runtime +Library for execution after a Compilation Process, or makes use of an +interface provided by the Runtime Library, but is not otherwise based +on the Runtime Library. + +"GCC" means a version of the GNU Compiler Collection, with or without +modifications, governed by version 3 (or a specified later version) of +the GNU General Public License (GPL) with the option of using any +subsequent versions published by the FSF. + +"GPL-compatible Software" is software whose conditions of propagation, +modification and use would permit combination with GCC in accord with +the license of GCC. + +"Target Code" refers to output from any compiler for a real or virtual +target processor architecture, in executable form or suitable for +input to an assembler, loader, linker and/or execution +phase. Notwithstanding that, Target Code does not include data in any +format that is used as a compiler intermediate representation, or used +for producing a compiler intermediate representation. + +The "Compilation Process" transforms code entirely represented in +non-intermediate languages designed for human-written code, and/or in +Java Virtual Machine byte code, into Target Code. Thus, for example, +use of source code generators and preprocessors need not be considered +part of the Compilation Process, since the Compilation Process can be +understood as starting with the output of the generators or +preprocessors. + +A Compilation Process is "Eligible" if it is done using GCC, alone or +with other GPL-compatible software, or if it is done without using any +work based on GCC. For example, using non-GPL-compatible Software to +optimize any GCC intermediate representations would not qualify as an +Eligible Compilation Process. + +1. Grant of Additional Permission. + +You have permission to propagate a work of Target Code formed by +combining the Runtime Library with Independent Modules, even if such +propagation would otherwise violate the terms of GPLv3, provided that +all Target Code was generated by Eligible Compilation Processes. You +may then convey such a combination under terms of your choice, +consistent with the licensing of the Independent Modules. + +2. No Weakening of GCC Copyleft. + +The availability of this Exception does not imply any general +presumption that third-party software is unaffected by the copyleft +requirements of the license of GCC. + +---- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +Name: libquadmath +Files: numpy.libs/libquadmath*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath +License: LGPL-2.1-or-later + + GCC Quad-Precision Math Library + Copyright (C) 2010-2019 Free Software Foundation, Inc. + Written by Francois-Xavier Coudert + + This file is part of the libquadmath library. + Libquadmath is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + Libquadmath is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + + +nvdlfw_inspect +Apache2 +https://github.com/NVIDIA/nvidia-dlfw-inspect + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +nvidia-cublas +LicenseRef-NVIDIA-Proprietary +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cuda-cupti +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cuda-nvrtc +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cuda-runtime +LicenseRef-NVIDIA-Proprietary +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cudnn-cu13 +LicenseRef-NVIDIA-Proprietary +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cudnn-frontend +NVIDIA Proprietary Software +https://github.com/nvidia/cudnn-frontend +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +nvidia-cufft +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cufile +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-curand +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cusolver +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cusparse +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-cusparselt-cu13 +NVIDIA Proprietary Software +https://developer.nvidia.com/cusparselt +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + +This license agreement, including exhibits attached ("Agreement”) is a legal agreement between you and NVIDIA Corporation ("NVIDIA") and governs your use of a NVIDIA software development kit (“SDK”). + +Each SDK has its own set of software and materials, but here is a description of the types of items that may be included in a SDK: source code, header files, APIs, data sets and assets (examples include images, textures, models, scenes, videos, native API input/output files), binary software, sample code, libraries, utility programs, programming code and documentation. + +This Agreement can be accepted only by an adult of legal age of majority in the country in which the SDK is used. + +If you are entering into this Agreement on behalf of a company or other legal entity, you represent that you have the legal authority to bind the entity to this Agreement, in which case “you” will mean the entity you represent. + +If you don’t have the required age or authority to accept this Agreement, or if you don’t accept all the terms and conditions of this Agreement, do not download, install or use the SDK. + +You agree to use the SDK only for purposes that are permitted by (a) this Agreement, and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions. + +1. License. + +1.1 Grant + +Subject to the terms of this Agreement, NVIDIA hereby grants you a non-exclusive, non-transferable license, without the right to sublicense (except as expressly provided in this Agreement) to: + +(i) Install and use the SDK, + +(ii) Modify and create derivative works of sample source code delivered in the SDK, and + +(iii) Distribute those portions of the SDK that are identified in this Agreement as distributable, as incorporated in object code format into a software application that meets the distribution requirements indicated in this Agreement. + +1.2 Distribution Requirements + +These are the distribution requirements for you to exercise the distribution grant: + +(i) Your application must have material additional functionality, beyond the included portions of the SDK. + +(ii) The distributable portions of the SDK shall only be accessed by your application. + +(iii) The following notice shall be included in modifications and derivative works of sample source code distributed: “This software contains source code provided by NVIDIA Corporation.” + +(iv) Unless a developer tool is identified in this Agreement as distributable, it is delivered for your internal use only. + +(v) The terms under which you distribute your application must be consistent with the terms of this Agreement, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA’s intellectual property rights. Additionally, you agree that you will protect the privacy, security and legal rights of your application users. + +(vi) You agree to notify NVIDIA in writing of any known or suspected distribution or use of the SDK not in compliance with the requirements of this Agreement, and to enforce the terms of your agreements with respect to distributed SDK. + +1.3 Authorized Users + +You may allow employees and contractors of your entity or of your subsidiary(ies) to access and use the SDK from your secure network to perform work on your behalf. + +If you are an academic institution you may allow users enrolled or employed by the academic institution to access and use the SDK from your secure network. + +You are responsible for the compliance with the terms of this Agreement by your authorized users. If you become aware that your authorized users didn’t follow the terms of this Agreement, you agree to take reasonable steps to resolve the non-compliance and prevent new occurrences. + +1.4 Pre-Release SDK +The SDK versions identified as alpha, beta, preview or otherwise as pre-release, may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, accessibility, availability, and reliability standards relative to commercial versions of NVIDIA software and materials. Use of a pre-release SDK may result in unexpected results, loss of data, project delays or other unpredictable damage or loss. +You may use a pre-release SDK at your own risk, understanding that pre-release SDKs are not intended for use in production or business-critical systems. +NVIDIA may choose not to make available a commercial version of any pre-release SDK. NVIDIA may also choose to abandon development and terminate the availability of a pre-release SDK at any time without liability. +1.5 Updates + +NVIDIA may, at its option, make available patches, workarounds or other updates to this SDK. Unless the updates are provided with their separate governing terms, they are deemed part of the SDK licensed to you as provided in this Agreement. + +You agree that the form and content of the SDK that NVIDIA provides may change without prior notice to you. While NVIDIA generally maintains compatibility between versions, NVIDIA may in some cases make changes that introduce incompatibilities in future versions of the SDK. + +1.6 Third Party Licenses + +The SDK may come bundled with, or otherwise include or be distributed with, third-party software licensed by a NVIDIA supplier and/or open source software provided under an open source license. Use of third-party software is subject to the third-party license terms, or in the absence of third-party terms, the terms of this Agreement. Copyright to third party software is held by the copyright holders indicated in the third-party software or license. + +1.7 Reservation of Rights + +NVIDIA reserves all rights, title and interest in and to the SDK not expressly granted to you under this Agreement. + +2. Limitations. + +The following license limitations apply to your use of the SDK: + +2.1 You may not reverse engineer, decompile or disassemble, or remove copyright or other proprietary notices from any portion of the SDK or copies of the SDK. + +2.2 Except as expressly provided in this Agreement, you may not copy, sell, rent, sublicense, transfer, distribute, modify, or create derivative works of any portion of the SDK. For clarity, you may not distribute or sublicense the SDK as a stand-alone product. + +2.3 Unless you have an agreement with NVIDIA for this purpose, you may not indicate that an application created with the SDK is sponsored or endorsed by NVIDIA. + +2.4 You may not bypass, disable, or circumvent any encryption, security, digital rights management or authentication mechanism in the SDK. + +2.5 You may not use the SDK in any manner that would cause it to become subject to an open source software license. As examples, licenses that require as a condition of use, modification, and/or distribution that the SDK be (i) disclosed or distributed in source code form; (ii) licensed for the purpose of making derivative works; or (iii) redistributable at no charge. + +2.6 Unless you have an agreement with NVIDIA for this purpose, you may not use the SDK with any system or application where the use or failure of the system or application can reasonably be expected to threaten or result in personal injury, death, or catastrophic loss. Examples include use in avionics, navigation, military, medical, life support or other life critical applications. NVIDIA does not design, test or manufacture the SDK for these critical uses and NVIDIA shall not be liable to you or any third party, in whole or in part, for any claims or damages arising from such uses. + +2.7 You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney’s fees and costs incident to establishing the right of indemnification) arising out of or related to your use of the SDK outside of the scope of this Agreement, or not in compliance with its terms. + +3. Ownership. + +3.1 NVIDIA or its licensors hold all rights, title and interest in and to the SDK and its modifications and derivative works, including their respective intellectual property rights, subject to your rights under Section 3.2. This SDK may include software and materials from NVIDIA’s licensors, and these licensors are intended third party beneficiaries that may enforce this Agreement with respect to their intellectual property rights. + +3.2 You hold all rights, title and interest in and to your applications and your derivative works of the sample source code delivered in the SDK, including their respective intellectual property rights, subject to NVIDIA’s rights under section 3.1. + +3.3 You may, but don’t have to, provide to NVIDIA suggestions, feature requests or other feedback regarding the SDK, including possible enhancements or modifications to the SDK. For any feedback that you voluntarily provide, you hereby grant NVIDIA and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. NVIDIA will use feedback at its choice. NVIDIA is constantly looking for ways to improve its products, so you may send feedback to NVIDIA through the developer portal at https://developer.nvidia.com. + +4. No Warranties. + +THE SDK IS PROVIDED BY NVIDIA “AS IS” AND “WITH ALL FAULTS.” TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF DEALING OR COURSE OF TRADE. + +5. Limitations of Liability. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF LIABILITY. IN NO EVENT WILL NVIDIA’S AND ITS AFFILIATES TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS LIMIT. + +These exclusions and limitations of liability shall apply regardless if NVIDIA or its affiliates have been advised of the possibility of such damages, and regardless of whether a remedy fails its essential purpose. These exclusions and limitations of liability form an essential basis of the bargain between the parties, and, absent any of these exclusions or limitations of liability, the provisions of this Agreement, including, without limitation, the economic terms, would be substantially different. + +6. Termination. + +6.1 This Agreement will continue to apply until terminated by either you or NVIDIA as described below. + +6.2 If you want to terminate this Agreement, you may do so by stopping to use the SDK. + +6.3 NVIDIA may, at any time, terminate this Agreement if: (i) you fail to comply with any term of this Agreement and the non-compliance is not fixed within thirty (30) days following notice from NVIDIA (or immediately if you violate NVIDIA’s intellectual property rights); (ii) you commence or participate in any legal proceeding against NVIDIA with respect to the SDK; or (iii) NVIDIA decides to no longer provide the SDK in a country or, in NVIDIA’s sole discretion, the continued use of it is no longer commercially viable. + +6.4 Upon any termination of this Agreement, you agree to promptly discontinue use of the SDK and destroy all copies in your possession or control. Your prior distributions in accordance with this Agreement are not affected by the termination of this Agreement. Upon written request, you will certify in writing that you have complied with your commitments under this section. Upon any termination of this Agreement all provisions survive except for the licenses granted to you. + +7. General. + +If you wish to assign this Agreement or your rights and obligations, including by merger, consolidation, dissolution or operation of law, contact NVIDIA to ask for permission. Any attempted assignment not approved by NVIDIA in writing shall be void and of no effect. NVIDIA may assign, delegate or transfer this Agreement and its rights and obligations, and if to a non-affiliate you will be notified. + +You agree to cooperate with NVIDIA and provide reasonably requested information to verify your compliance with this Agreement. + +This Agreement will be governed in all respects by the laws of the United States and of the State of Delaware as those laws are applied to contracts entered into and performed entirely within Delaware by Delaware residents, without regard to the conflicts of laws principles. The United Nations Convention on Contracts for the International Sale of Goods is specifically disclaimed. You agree to all terms of this Agreement in the English language. + +The state or federal courts residing in Santa Clara County, California shall have exclusive jurisdiction over any dispute or claim arising out of this Agreement. Notwithstanding this, you agree that NVIDIA shall still be allowed to apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +If any court of competent jurisdiction determines that any provision of this Agreement is illegal, invalid or unenforceable, such provision will be construed as limited to the extent necessary to be consistent with and fully enforceable under the law and the remaining provisions will remain in full force and effect. Unless otherwise specified, remedies are cumulative. + +Each party acknowledges and agrees that the other is an independent contractor in the performance of this Agreement. + +The SDK has been developed entirely at private expense and is “commercial items” consisting of “commercial computer software” and “commercial computer software documentation” provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the U.S. Government or a U.S. Government subcontractor is subject to the restrictions in this Agreement pursuant to DFARS 227.7202-3(a) or as set forth in subparagraphs (b)(1) and (2) of the Commercial Computer Software - Restricted Rights clause at FAR 52.227-19, as applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas Expressway, Santa Clara, CA 95051. + +The SDK is subject to United States export laws and regulations. You agree that you will not ship, transfer or export the SDK into any country, or use the SDK in any manner, prohibited by the United States Bureau of Industry and Security or economic sanctions regulations administered by the U.S. Department of Treasury’s Office of Foreign Assets Control (OFAC), or any applicable export laws, restrictions or regulations. These laws include restrictions on destinations, end users and end use. By accepting this Agreement, you confirm that you are not a resident or citizen of any country currently embargoed by the U.S. and that you are not otherwise prohibited from receiving the SDK. + +Any notice delivered by NVIDIA to you under this Agreement will be delivered via mail, email or fax. You agree that any notices that NVIDIA sends you electronically will satisfy any legal communication requirements. Please direct your legal notices or other correspondence to NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department. + +This Agreement and any exhibits incorporated into this Agreement constitute the entire agreement of the parties with respect to the subject matter of this Agreement and supersede all prior negotiations or documentation exchanged between the parties relating to this subject matter. Any additional and/or conflicting terms on documents issued by you are null, void, and invalid. Any amendment or waiver under this Agreement shall be in writing and signed by representatives of both parties. + +(v. October 12, 2020) + + + + + + + + + + + + + + + +cuSPARSELt SUPPLEMENT TO SOFTWARE LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + +The terms in this supplement govern your use of the NVIDIA cuSPARSELt SDK under the terms of your license agreement (“Agreement”) as modified by this supplement. Capitalized terms used but not defined below have the meaning assigned to them in the Agreement. + +This supplement is an exhibit to the Agreement and is incorporated as an integral part of the Agreement. In the event of conflict between the terms in this supplement and the terms in the Agreement, the terms in this supplement govern. + +1. License Scope. The SDK is licensed for you to develop applications only for use in systems with NVIDIA GPUs. + +2. Distribution. The following portions of the SDK are distributable under the Agreement: the runtimes files ending with .so and .h as part of your application. + +3. Licensing. If the distribution terms in this Agreement are not suitable for your organization, or for any questions regarding this Agreement, please contact NVIDIA at nvidia-compute-license-questions@nvidia.com + +(v. October 12, 2020) + + +nvidia-dali-cuda120 +Apache License 2.0 +https://github.com/NVIDIA/dali + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + +nvidia-libnvcomp-cu12 +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS +https://developer.nvidia.com/nvcomp +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + +This license agreement("Agreement") is a legal agreement between you and NVIDIA Corporation ("NVIDIA") and governs your use of the NVIDIA nvCOMP software development kit as available at NVIDIA's discretion (each, a "SDK"). + +Each SDK has its own set of software and materials, but here is a description of the types of items that may be included in a SDK: source code, header files, APIs, data sets and assets (examples include images, textures, models, scenes, videos, native API input/output files), binary software, sample code, libraries, utility programs, programming code and documentation. + +This Agreement can be accepted only by an adult of legal age of majority in the country in which the SDK is used. + +If you are entering into this Agreement on behalf of a company or other legal entity, you represent that you have the legal authority to bind the entity to this Agreement, in which case "you" will mean the entity you represent. + +If you don't have the required age or authority to accept this Agreement, or if you don't accept all the terms and conditions of this Agreement, do not download, install or use the SDK. + +You agree to use the SDK only for purposes that are permitted by (a) this Agreement, and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions. + +1. License. + +1.1 Grant + +Subject to the terms of this Agreement, NVIDIA hereby grants you a non-exclusive, non-transferable license, without the right to sublicense (except as expressly provided in this Agreement) to: + +(i) Install and use the SDK, + +(ii) Modify and create derivative works of sample source code delivered in the SDK, and + +(ii) Distribute the binary files, files identified as samples, and headers as incorporated into a software application that meets the distribution requirements indicated in this Agreement. + +1.2 Distribution Requirements + +These are the distribution requirements for you to exercise the distribution grant: + +(i) Your application must have material additional functionality, beyond the included portions of the SDK. + +(ii) The distributable portions of the SDK shall only be accessed by your application. + +(iii) The following notice shall be included in modifications and derivative works of sample source code distributed: "This software contains source code provided by NVIDIA Corporation." + +(iv) Unless a developer tool is identified in this Agreement as distributable, it is delivered for your internal use only. + +(v) The terms under which you distribute your application must be consistent with the terms of this Agreement, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA's intellectual property rights. Additionally, you agree that you will protect the privacy, security and legal rights of your application users. + +(vi) You agree to notify NVIDIA in writing of any known or suspected distribution or use of the SDK not in compliance with the requirements of this Agreement, and to enforce the terms of your agreements with respect to distributed SDK. + +1.3 Authorized Users + +You may allow employees and contractors of your entity or of your subsidiary(ies) to access and use the SDK from your secure network to perform work on your behalf. + +If you are an academic institution you may allow users enrolled or employed by the academic institution to access and use the SDK from your secure network. + +You are responsible for the compliance with the terms of this Agreement by your authorized users. If you become aware that your authorized users didn't follow the terms of this Agreement, you agree to take reasonable steps to resolve the non-compliance and prevent new occurrences. + +1.4 Pre-Release SDK + +The SDK versions identified as alpha, beta, preview or otherwise as pre-release, may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, accessibility, availability, and reliability standards relative to commercial versions of NVIDIA software and materials. Use of a pre-release SDK may result in unexpected results, loss of data, project delays or other unpredictable damage or loss. + +You may use a pre-release SDK at your own risk, understanding that pre-release SDKs are not intended for use in production or business-critical systems. + +NVIDIA may choose not to make available a commercial version of any pre-release SDK. NVIDIA may also choose to abandon development and terminate the availability of a pre-release SDK at any time without liability. + +1.5 Updates + +NVIDIA may, at its option, make available patches, workarounds or other updates to this SDK. Unless the updates are provided with their separate governing terms, they are deemed part of the SDK licensed to you as provided in this Agreement. + +You agree that the form and content of the SDK that NVIDIA provides may change without prior notice to you. While NVIDIA generally maintains compatibility between versions, NVIDIA may in some cases make changes that introduce incompatibilities in future versions of the SDK. + +1.6 Components Under Other Licenses. + +The SDK may include NVIDIA or third-party components with separate legal notices or terms as may be described in proprietary notices accompanying the SDK, such as components governed by open source software licenses. If and to the extent there is a conflict between the terms in this license and the license terms associated with a component, the license terms associated with the components control only to the extent necessary to resolve the conflict. + +1.7 Reservation of Rights + +NVIDIA reserves all rights, title and interest in and to the SDK not expressly granted to you under this Agreement. + +2. Limitations. + +The following license limitations apply to your use of the SDK: + +2.1 The SDK is licensed for you to develop applications only for use in systems with NVIDIA GPUs. + +2.2 You may not reverse engineer, decompile or disassemble, or remove copyright or other proprietary notices from any portion of the SDK or copies of the SDK. + +2.3 Except as expressly provided in this Agreement, you may not copy, sell, rent, sublicense, transfer, distribute, modify, or create derivative works of any portion of the SDK. + +2.4 Unless you have an agreement with NVIDIA for this purpose, you may not indicate that an application created with the SDK is sponsored or endorsed by NVIDIA. + +2.5 You may not bypass, disable, or circumvent any encryption, security, digital rights management or authentication mechanism in the SDK. + +2.6 You may not use the SDK in any manner that would cause it to become subject to an open source software license. As examples, licenses that require as a condition of use, modification, and/or distribution that the SDK be (i) disclosed or distributed in source code form; (ii) licensed for the purpose of making derivative works; or (iii) redistributable at no charge. + +2.7 You acknowledge that the SDK as delivered is not tested or certified by NVIDIA for use in connection with the design, construction, maintenance, and/or operation of any system where the use or failure of such system could result in a situation that threatens the safety of human life or results in catastrophic damages (each, a "Critical Application"). Examples of Critical Applications include use in avionics, navigation, autonomous vehicle applications, ai solutions for automotive products, military, medical, life support or other life critical applications. NVIDIA shall not be liable to you or any third party, in whole or in part, for any claims or damages arising from such uses. You are solely responsible for ensuring that any product or service developed with the SDK as a whole includes sufficient features to comply with all applicable legal and regulatory standards and requirements. + +2.8 You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney's fees and costs incident to establishing the right of indemnification) arising out of or related to products or services that use the SDK in or for Critical Applications, and for use of the SDK outside of the scope of this Agreement, or not in compliance with its terms. + +3. Ownership. + +3.1 NVIDIA or its licensors hold all rights, title and interest in and to the SDK and its modifications and derivative works, including their respective intellectual property rights, subject to your rights under Section 3.2. This SDK may include software and materials from NVIDIA's licensors, and these licensors are intended third party beneficiaries that may enforce this Agreement with respect to their intellectual property rights. + +3.2 You hold all rights, title and interest in and to your applications and your derivative works of the sample source code delivered in the SDK, including their respective intellectual property rights, subject to NVIDIA's rights under section 3.1. + +3.3 You may, but don't have to, provide to NVIDIA suggestions, feature requests or other feedback regarding the SDK, including possible enhancements or modifications to the SDK. For any feedback that you voluntarily provide, you hereby grant NVIDIA and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. NVIDIA will use feedback at its choice. + +4. No Warranties. + +THE SDK IS PROVIDED BY NVIDIA "AS IS" AND "WITH ALL FAULTS." TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF DEALING OR COURSE OF TRADE. + +5. Limitations of Liability. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF LIABILITY. IN NO EVENT WILL NVIDIA'S AND ITS AFFILIATES TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS LIMIT. + +These exclusions and limitations of liability shall apply regardless if NVIDIA or its affiliates have been advised of the possibility of such damages, and regardless of whether a remedy fails its essential purpose. These exclusions and limitations of liability form an essential basis of the bargain between the parties, and, absent any of these exclusions or limitations of liability, the provisions of this Agreement, including, without limitation, the economic terms, would be substantially different. + +6. Termination. + +6.1 This Agreement will continue to apply until terminated by either you or NVIDIA as described below. + +6.2 If you want to terminate this Agreement, you may do so by stopping to use the SDK. + +6.3 NVIDIA may, at any time, terminate this Agreement if: (i) you fail to comply with any term of this Agreement and the non-compliance is not fixed within thirty (30) days following notice from NVIDIA (or immediately if you violate NVIDIA's intellectual property rights); (ii) you commence or participate in any legal proceeding against NVIDIA with respect to the SDK; or (iii) NVIDIA decides to no longer provide the SDK in a country or, in NVIDIA's sole discretion, the continued use of it is no longer commercially viable. + +6.4 Upon any termination of this Agreement, you agree to promptly discontinue use of the SDK and destroy all copies in your possession or control. Your prior distributions in accordance with this Agreement are not affected by the termination of this Agreement. Upon written request, you will certify in writing that you have complied with your commitments under this section. Upon any termination of this Agreement all provisions survive except for the licenses granted to you. + +7. General. + +If you wish to assign this Agreement or your rights and obligations, including by merger, consolidation, dissolution or operation of law, contact NVIDIA to ask for permission. Any attempted assignment not approved by NVIDIA in writing shall be void and of no effect. NVIDIA may assign, delegate or transfer this Agreement and its rights and obligations. + +You agree to cooperate with NVIDIA and provide reasonably requested information to verify your compliance with this Agreement. + +This Agreement will be governed in all respects by the laws of the United States and of the State of Delaware as those laws are applied to contracts entered into and performed entirely within Delaware, without regard to the conflicts of laws principles. The United Nations Convention on Contracts for the International Sale of Goods is specifically disclaimed. You agree to all terms of this Agreement in the English language. + +The state or federal courts residing in Santa Clara County, California shall have exclusive jurisdiction over any dispute or claim arising out of this Agreement. Notwithstanding this, you agree that NVIDIA shall still be allowed to apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +If any court of competent jurisdiction determines that any provision of this Agreement is illegal, invalid or unenforceable, such provision will be construed as limited to the extent necessary to be consistent with and fully enforceable under the law and the remaining provisions will remain in full force and effect. Unless otherwise specified, remedies are cumulative. + +Each party acknowledges and agrees that the other is an independent contractor in the performance of this Agreement. + +The SDK has been developed entirely at private expense and is "commercial items" consisting of "commercial computer software" and "commercial computer software documentation" provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the U.S. Government or a U.S. Government subcontractor is subject to the restrictions in this Agreement pursuant to DFARS 227.7202-3(a) or as set forth in subparagraphs (b)(1) and (2) of the Commercial Computer Software - Restricted Rights clause at FAR 52.227-19, as applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas Expressway, Santa Clara, CA 95051. + +The SDK is subject to United States export laws and regulations. You agree that you will not ship, transfer or export the SDK into any country, or use the SDK in any manner, prohibited by the United States Bureau of Industry and Security or economic sanctions regulations administered by the U.S. Department of Treasury's Office of Foreign Assets Control (OFAC), or any applicable export laws, restrictions or regulations. These laws include restrictions on destinations, end users and end use. By accepting this Agreement, you confirm that you are not a resident or citizen of any country currently embargoed by the U.S. and that you are not otherwise prohibited from receiving the SDK. + +Any notice delivered by NVIDIA to you under this Agreement will be delivered via mail, email or fax. You agree that any notices that NVIDIA sends you electronically will satisfy any legal communication requirements. Please direct your legal notices or other correspondence to NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department. + +This Agreement constitutes the entire agreement of the parties with respect to the subject matter of this Agreement and supersedes all prior negotiations or documentation exchanged between the parties relating to this subject matter. Any additional and/or conflicting terms on documents issued by you are null, void, and invalid. Any amendment or waiver under this Agreement shall be in writing and signed by representatives of both parties. + +If the distribution terms in this Agreement are not suitable for your organization, or for any questions regarding this Agreement, please contact NVIDIA at nvidia-compute-license-questions@nvidia.com. + +(v. April 26, 2022) + + +nvidia-ml-py +BSD License +https://forums.developer.nvidia.com +UNKNOWN + +nvidia-nccl-cu13 +LicenseRef-NVIDIA-Proprietary +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-nvimgcodec-cu12 +Apache License 2.0 +https://github.com/NVIDIA/nvImageCodec +NVIDIA Software License Agreement + +IMPORTANT NOTICE – PLEASE READ AND AGREE BEFORE USING THE SOFTWARE. + +This license agreement (“Agreement”) is a legal agreement between you, whether an individual or entity (“you”) and NVIDIA Corporation (“NVIDIA”) and governs the use of the NVIDIA Image Codec and any additional software and materials provided under this Agreement (“Software”). + +This Agreement can be accepted only by an adult of legal age of majority in the country in which the Software is used. + +If you don’t have the required age or authority to accept this Agreement, or if you don’t accept all the terms and conditions of this Agreement, do not use the Software. + +You agree to use the Software only for purposes that are permitted by this Agreement and any applicable law or regulation in the relevant jurisdictions. + +1. License Grant. Subject to the terms of this Agreement, NVIDIA grants you a non-exclusive, revocable, non-transferable, non-sublicensable (except as expressly granted in this Agreement), license to: + +1.1 install and use copies of the Software, + +1.2 modify and create derivative works of sample or example Software provided by NVIDIA in source code format, and + +1.3 distribute the Software in binary format as incorporated into a software application subject to the following distribution requirements: + +(a) Your application must have material additional functionality, beyond the included portions of the Software. + +(b) The distributable portions of the Software shall only be accessed by your application. + +(c) The following notice shall be included in modifications and derivative works of sample source code distributed: “This software contains source code provided by NVIDIA Corporation.” + +(d) Unless a developer tool is identified in this Agreement as distributable, it is delivered for your internal use only. + +(e) The terms under which you distribute your application must be consistent with the terms of this Agreement, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA’s intellectual property rights. Additionally, you agree that you will protect the privacy, security and legal rights of your application users. + +(f) You agree to notify NVIDIA in writing of any known or suspected distribution or use of the Software not in compliance with the requirements of this Agreement, and to enforce the terms of your agreements with respect to distributed Software. + +2. Limitations. Your license to use the Software is restricted as follows: + +2.1 The Software is licensed for you to develop applications only for use in systems with NVIDIA GPUs and NVIDIA CPUs (if and when available). + +2.2 You may not reverse engineer, decompile or disassemble the Software components provided in binary form, nor attempt in any other manner to obtain source code of the Software. + +2.3 You may not change or remove copyright or other proprietary notices in the Software. + +2.4 Except as expressly granted in this Agreement, you may not copy, sell, rent, sublicense, transfer, distribute, modify or create derivative works of the Software, or make its functionality available to others. + +2.5 You may not bypass, disable or circumvent any technical limitation, encryption, security, digital rights management or authentication mechanism in the Software. + +2.6 You may not use the Software in any manner that would cause it to become subject to an open source software license; subject to the terms in the “Components Under Other Licenses” section below . + +2.7 You may not use the Software for the purpose of developing competing products or technologies or assist a third party in such activities. + +2.8 You may not indicate that a product or service developed with the Software is sponsored or endorsed by NVIDIA. + +2.9 You may not replace any NVIDIA software components in the Software that are governed by this Agreement with other software that implements NVIDIA APIs. + +2.10 You may not reverse engineer, decompile or disassemble any portion of the output generated using Software elements for the purpose of translating such output artifacts to target a non-NVIDIA platform. + +2.11 You may not distribute or disclose to third parties the output of the Software where the output reveals functionality or performance data pertinent to NVIDIA hardware or software products, results of benchmarking, competitive analysis, or regression or performance data relating to the Software or NVIDIA GPUs without the prior written permission from NVIDIA. + +2.12 You acknowledge that the Software provided under this Agreement is not designed or tested by NVIDIA for use in any system or application where the use or failure of such system or application developed with NVIDIA’s Software could result in injury, death or catastrophic damage (each, a “Mission Critical Application”). Examples of Mission Critical Applications include use in avionics, navigation, autonomous vehicle applications, AI solutions for automotive products, military, medical, life support or other mission-critical or life-critical applications. NVIDIA will not be liable to you or any third party, in whole or in part, for any claims or damages arising from these uses. You are solely responsible for ensuring that systems and applications developed with the Software include sufficient safety and redundancy features and comply with all applicable legal and regulatory standards and requirements. + +2.13 You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney’s fees and costs incident to establishing the right of indemnification) arising out of or related to (i) products or services that have been developed or deployed with or use the Software, or claims that they violate laws, or infringe, violate, or misappropriate any third party right; or (ii) a violation of the terms and conditions of this Agreement. + +3. Authorized Users. You may allow employees and contractors of your entity or of your subsidiary(ies) to access and use the Software from your secure network to perform the work authorized by this Agreement on your behalf. If you are an academic institution, you may allow users enrolled or employed by the academic institution to access and use the Software as authorized by this Agreement from your secure network. You are responsible for the compliance with the terms of this Agreement by your authorized users. Any act or omission that if committed by you would constitute a breach of this Agreement will be deemed to constitute a breach of this Agreement if committed by your authorized users. + +4. Pre-Release Versions. Software versions or specific features identified as alpha, beta, preview, early access or otherwise as pre-release may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, availability and reliability standards relative to commercial versions of NVIDIA offerings. You may use pre-release Software at your own risk, understanding that such versions are not intended for use in production or business-critical systems. NVIDIA may choose not to make available a commercial version of any pre-release Software. NVIDIA may also choose to abandon development and terminate the availability of pre-release Software at any time without liability. + +5. Updates. NVIDIA may, at its option, make available patches, workarounds or other updates to the Software. Unless the updates are provided with their separate governing terms, they are deemed part of the Software licensed to you as provided in this Agreement. + +6. Components Under Other Licenses. The Software may include or be distributed with components provided with separate legal notices or terms that accompany the components, such as open source software licenses and other license. The components are subject to the applicable other licenses, including any proprietary notices, disclaimers, requirements and extended use rights; except that this Agreement will prevail regarding the use of third-party open source software, unless a third-party open source software license requires its license terms to prevail. Open source software license means any software, data or documentation subject to any license identified as an open source license by the Open Source Initiative (http://opensource.org), Free Software Foundation (http://www.fsf.org) or other similar open source organization or listed by the Software Package Data Exchange (SPDX) Workgroup under the Linux Foundation (http://www.spdx.org). + +7. Termination . This Agreement will automatically terminate without notice from NVIDIA if you fail to comply with any of the terms in this Agreement or if you commence or participate in any legal proceeding against NVIDIA with respect to the Software. Additionally, NVIDIA may terminate this Agreement with prior written notice to you if, in NVIDIA’s sole discretion, the continued use of the Software is no longer commercially viable or creates liabilities for NVIDIA. You agree to cooperate with NVIDIA and provide reasonably requested information to verify your compliance with this Agreement. Upon any termination, you must stop using and destroy all copies of the Software. Upon written request, you will certify in writing that you have complied with your commitments under this section. All provisions will survive termination, except for the licenses granted to you. + +8. Ownership. + +8.1 NVIDIA Ownership. The Software, including all intellectual property rights, is and will remain the sole and exclusive property of NVIDIA or its licensors. Except as expressly granted in this Agreement, (i) NVIDIA reserves all rights, interests and remedies in connection with the Software and (ii) no other license or right is granted to you by implication, estoppel or otherwise. + +8.2 Your Ownership. Subject to the rights of NVIDIA and its suppliers in the Software, you hold all rights, title and interest in and to your services, applications and derivative works of samples or examples you develop as permitted in this Agreement including their respective intellectual property rights. + +8.3 Non-Assert. You agree that you will not, and will not assist or enable any other party to, assert or threaten to assert any intellectual property rights against NVIDIA or its affiliates with respect to new software samples or examples that NVIDIA or its affiliates may develop and make available in the future. + +9. Feedback. You may, but are not obligated to, provide suggestions, requests, fixes, modifications, enhancements or other feedback regarding or in connection with your use of the Software (collectively, “Feedback”). Feedback, even if designated as confidential by you, will not create any confidentiality obligation for NVIDIA or its affiliates. If you provide Feedback, you hereby grant NVIDIA, its affiliates and its designees a non-exclusive, perpetual, irrevocable, sublicensable, worldwide, royalty-free, fully paid-up and transferable license, under your intellectual property rights, to publicly perform, publicly display, reproduce, use, make, have made, sell, offer for sale, distribute (through multiple tiers of distribution), import, create derivative works of and otherwise commercialize and exploit the Feedback at NVIDIA’s discretion. You will not give Feedback (i) that you have reason to believe is subject to any restriction that impairs the exercise of the grant stated in this section, such as third-party intellectual property rights or (ii) subject to license terms which seek to require any product incorporating or developed using such Feedback, or other intellectual property of NVIDIA or its affiliates, to be licensed to or otherwise shared with any third party. + +10. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED BY NVIDIA AS-IS AND WITH ALL FAULTS. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA DISCLAIMS ALL WARRANTIES AND REPRESENTATIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED OR STATUTORY, RELATING TO OR ARISING UNDER THIS AGREEMENT, INCLUDING, WITHOUT LIMITATION, THE WARRANTIES OF TITLE, NONINFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, USAGE OF TRADE AND COURSE OF DEALING. WITHOUT LIMITING THE FOREGOING, NVIDIA DOES NOT WARRANT THAT THE SOFTWARE WILL MEET YOUR REQUIREMENTS; THAT ANY DEFECTS OR ERRORS WILL BE CORRECTED; THAT ANY CERTAIN CONTENT WILL BE AVAILABLE; OR THAT THE SOFTWARE IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. NO INFORMATION OR ADVICE GIVEN BY NVIDIA WILL IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY EXPRESSLY PROVIDED IN THIS AGREEMENT. NVIDIA does not warrant or assume responsibility for the accuracy or completeness of any third-party information, text, graphics or links contained in the Software. + +11. Limitations of Liability. + +11.1 DISCLAIMERS. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL NVIDIA BE LIABLE FOR ANY (I) INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, OR (II) DAMAGES FOR THE (A) COST OF PROCURING SUBSTITUTE GOODS OR (B) LOSS OF PROFITS, REVENUES, USE, DATA OR GOODWILL ARISING OUT OF OR RELATED TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE, AND EVEN IF NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND EVEN IF A PARTY’S REMEDIES FAIL THEIR ESSENTIAL PURPOSE. + +11.2 DAMAGES CAP. ADDITIONALLY, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA’S TOTAL CUMULATIVE AGGREGATE LIABILITY FOR ANY AND ALL LIABILITIES, OBLIGATIONS OR CLAIMS ARISING OUT OF OR RELATED TO THIS AGREEMENT WILL NOT EXCEED FIVE U.S. DOLLARS (US$5). + +12. Governing Law and Jurisdiction. This Agreement will be governed in all respects by the laws of the United States and the laws of the State of Delaware, without regard to conflict of laws principles or the United Nations Convention on Contracts for the International Sale of Goods. The state and federal courts residing in Santa Clara County, California will have exclusive jurisdiction over any dispute or claim arising out of or related to this Agreement, and the parties irrevocably consent to personal jurisdiction and venue in those courts; except that either party may apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +13. General. + +13.1 No Assignment. NVIDIA may assign, delegate or transfer its rights or obligations under this Agreement by any means or operation of law. You may not, without NVIDIA’s prior written consent, assign, delegate or transfer any of your rights or obligations under this Agreement by any means or operation of law, and any attempt to do so is null and void. + +13.2 No Waiver. No waiver of any term of the Agreement will be deemed a further or continuing waiver of such term or any other term, and NVIDIA’s failure to assert any right or provision under the Agreement will not constitute a waiver of such right or provision. + +13.3 Trade and Compliance. You agree to comply with all applicable export, import, trade and economic sanctions laws and regulations, including U.S. Export Administration Regulations and Office of Foreign Assets Control regulations. You confirm that you will not export or reexport any products or technology, directly or indirectly, without first obtaining any required license or other approval from appropriate authorities, (i) to any countries that are subject to any U.S. or local export restrictions (currently including, but not necessarily limited to, Cuba, Iran, North Korea, Syria, the Region of Crimea, Donetsk People’s Republic Region and Luhansk People’s Republic Region); (ii) to any end user who you know or have reason to know will utilize them in the design, development or production of nuclear, chemical or biological weapons, missiles, rocket systems, unmanned air vehicles, or any weapons of mass destruction; (iii) to any end-user who has been prohibited from participating in the U.S. or local export transactions by any governing authority; or (iv) to any known military or military-intelligence end-user or for any known military or military-intelligence end-use in accordance with U.S. trade compliance laws and regulations. Use of the Software under this Agreement must be consistent with NVIDIA’s HumanRightsPolicy.pdf (nvidia.com). + +13.4 Government Rights. The Software, documentation and technology (“Protected Items”) are “Commercial products” as this term is defined at 48 C.F.R. 2.101, consisting of “commercial computer software” and “commercial computer software documentation” as such terms are used in, respectively, 48 C.F.R. 12.212 and 48 C.F.R. 227.7202 & 252.227-7014(a)(1). Before any Protected Items are supplied to the U.S. Government, you will (i) inform the U.S. Government in writing that the Protected Items are and must be treated as commercial computer software and commercial computer software documentation developed at private expense; (ii) inform the U.S. Government that the Protected Items are provided subject to the terms of the Agreement; and (iii) mark the Protected Items as commercial computer software and commercial computer software documentation developed at private expense. In no event will you permit the U.S. Government to acquire rights in Protected Items beyond those specified in 48 C.F.R. 52.227-19(b)(1)-(2) or 252.227-7013(c) except as expressly approved by NVIDIA in writing. + +13.5 Notices. Please direct your legal notices or other correspondence to NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department, with a copy emailed to legalnotices@nvidia.com. If NVIDIA needs to contact you about the Software, you consent to receive the notices by email and agree that such notices will satisfy any legal communication requirements. + +13.6 Force Majeure. Neither party will be liable during any period where an event or circumstance prevents or delays that party from performing its obligations under this Agreement and that event or circumstance: (i) is not within the reasonable control of that party and is not the result of that party’s negligence, and (ii) cannot be overcome or avoided by that party using reasonably diligent efforts. + +13.7 Severability and Amendment. If a court of competent jurisdiction rules that a provision of this Agreement is unenforceable, that provision will be deemed modified to the extent necessary to make it enforceable and the remainder of this Agreement will continue in full force and effect. Any amendment to this Agreement must be in writing and signed by authorized representatives of both parties. + +13.8 Construction. The headings in the Agreement are included solely for convenience and are not intended to affect the meaning or interpretation of the Agreement. As required by the context of the Agreement, the singular of a term includes the plural and vice versa. + +13.9 Entire Agreement. Regarding the subject matter of this Agreement, the parties agree that (i) this Agreement constitutes the entire and exclusive agreement between the parties and supersedes all prior and contemporaneous communications and (ii) any additional or different terms or conditions, whether contained in purchase orders, order acknowledgments, invoices or otherwise, will not be binding and are null and void. + + + +(v. November 28, 2023) + + + + + +nvidia-nvjitlink +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-nvjpeg-cu12 +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-nvjpeg2k-cu12 +Other/Proprietary License +https://developer.nvidia.com/nvjpeg +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + +This license agreement, including exhibits attached ("Agreement”) is a legal agreement between you and NVIDIA Corporation ("NVIDIA") and governs your use of a NVIDIA software development kit (“SDK”). + +Each SDK has its own set of software and materials, but here is a description of the types of items that may be included in a SDK: source code, header files, APIs, data sets and assets (examples include images, textures, models, scenes, videos, native API input/output files), binary software, sample code, libraries, utility programs, programming code and documentation. + +This Agreement can be accepted only by an adult of legal age of majority in the country in which the SDK is used. + +If you are entering into this Agreement on behalf of a company or other legal entity, you represent that you have the legal authority to bind the entity to this Agreement, in which case “you” will mean the entity you represent. + +If you don’t have the required age or authority to accept this Agreement, or if you don’t accept all the terms and conditions of this Agreement, do not download, install or use the SDK. + +You agree to use the SDK only for purposes that are permitted by (a) this Agreement, and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions. + +1. License. + +1.1 Grant + +Subject to the terms of this Agreement, NVIDIA hereby grants you a non-exclusive, non-transferable license, without the right to sublicense (except as expressly provided in this Agreement) to: + +(i) Install and use the SDK, + +(ii) Modify and create derivative works of sample source code delivered in the SDK, and + +(iii) Distribute those portions of the SDK that are identified in this Agreement as distributable, as incorporated in object code format into a software application that meets the distribution requirements indicated in this Agreement. + +1.2 Distribution Requirements + +These are the distribution requirements for you to exercise the distribution grant: + +(i) Your application must have material additional functionality, beyond the included portions of the SDK. + +(ii) The distributable portions of the SDK shall only be accessed by your application. + +(iii) The following notice shall be included in modifications and derivative works of sample source code distributed: “This software contains source code provided by NVIDIA Corporation.” + +(iv) Unless a developer tool is identified in this Agreement as distributable, it is delivered for your internal use only. + +(v) The terms under which you distribute your application must be consistent with the terms of this Agreement, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA’s intellectual property rights. Additionally, you agree that you will protect the privacy, security and legal rights of your application users. + +(vi) You agree to notify NVIDIA in writing of any known or suspected distribution or use of the SDK not in compliance with the requirements of this Agreement, and to enforce the terms of your agreements with respect to distributed SDK. + +1.3 Authorized Users + +You may allow employees and contractors of your entity or of your subsidiary(ies) to access and use the SDK from your secure network to perform work on your behalf. + +If you are an academic institution you may allow users enrolled or employed by the academic institution to access and use the SDK from your secure network. + +You are responsible for the compliance with the terms of this Agreement by your authorized users. If you become aware that your authorized users didn’t follow the terms of this Agreement, you agree to take reasonable steps to resolve the non-compliance and prevent new occurrences. + +1.4 Pre-Release SDK +The SDK versions identified as alpha, beta, preview or otherwise as pre-release, may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, accessibility, availability, and reliability standards relative to commercial versions of NVIDIA software and materials. Use of a pre-release SDK may result in unexpected results, loss of data, project delays or other unpredictable damage or loss. +You may use a pre-release SDK at your own risk, understanding that pre-release SDKs are not intended for use in production or business-critical systems. +NVIDIA may choose not to make available a commercial version of any pre-release SDK. NVIDIA may also choose to abandon development and terminate the availability of a pre-release SDK at any time without liability. +1.5 Updates + +NVIDIA may, at its option, make available patches, workarounds or other updates to this SDK. Unless the updates are provided with their separate governing terms, they are deemed part of the SDK licensed to you as provided in this Agreement. + +You agree that the form and content of the SDK that NVIDIA provides may change without prior notice to you. While NVIDIA generally maintains compatibility between versions, NVIDIA may in some cases make changes that introduce incompatibilities in future versions of the SDK. + +1.6 Third Party Licenses + +The SDK may come bundled with, or otherwise include or be distributed with, third party software licensed by a NVIDIA supplier and/or open source software provided under an open source license. Use of third party software is subject to the third-party license terms, or in the absence of third party terms, the terms of this Agreement. Copyright to third party software is held by the copyright holders indicated in the third-party software or license. + +1.7 Reservation of Rights + +NVIDIA reserves all rights, title and interest in and to the SDK not expressly granted to you under this Agreement. + +2. Limitations. + +The following license limitations apply to your use of the SDK: + +2.1 You may not reverse engineer, decompile or disassemble, or remove copyright or other proprietary notices from any portion of the SDK or copies of the SDK. + +2.2 Except as expressly provided in this Agreement, you may not copy, sell, rent, sublicense, transfer, distribute, modify, or create derivative works of any portion of the SDK. + +2.3 Unless you have an agreement with NVIDIA for this purpose, you may not indicate that an application created with the SDK is sponsored or endorsed by NVIDIA. + +2.4 You may not bypass, disable, or circumvent any encryption, security, digital rights management or authentication mechanism in the SDK. + +2.5 You may not use the SDK in any manner that would cause it to become subject to an open source software license. As examples, licenses that require as a condition of use, modification, and/or distribution that the SDK be (i) disclosed or distributed in source code form; (ii) licensed for the purpose of making derivative works; or (iii) redistributable at no charge. + +2.6 Unless you have an agreement with NVIDIA for this purpose, you may not use the SDK with any system or application where the use or failure of the system or application can reasonably be expected to threaten or result in personal injury, death, or catastrophic loss. Examples include use in avionics, navigation, military, medical, life support or other life critical applications. NVIDIA does not design, test or manufacture the SDK for these critical uses and NVIDIA shall not be liable to you or any third party, in whole or in part, for any claims or damages arising from such uses. + +2.7 You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney’s fees and costs incident to establishing the right of indemnification) arising out of or related to your use of the SDK outside of the scope of this Agreement, or not in compliance with its terms. + +3. Ownership. + +3.1 NVIDIA or its licensors hold all rights, title and interest in and to the SDK and its modifications and derivative works, including their respective intellectual property rights, subject to your rights under Section 3.2. This SDK may include software and materials from NVIDIA’s licensors, and these licensors are intended third party beneficiaries that may enforce this Agreement with respect to their intellectual property rights. + +3.2 You hold all rights, title and interest in and to your applications and your derivative works of the sample source code delivered in the SDK, including their respective intellectual property rights, subject to NVIDIA’s rights under section 3.1. + +3.3 You may, but don’t have to, provide to NVIDIA suggestions, feature requests or other feedback regarding the SDK, including possible enhancements or modifications to the SDK. For any feedback that you voluntarily provide, you hereby grant NVIDIA and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. NVIDIA will use feedback at its choice. NVIDIA is constantly looking for ways to improve its products, so you may send feedback to NVIDIA through the developer portal at https://developer.nvidia.com. + +4. No Warranties. + +THE SDK IS PROVIDED BY NVIDIA “AS IS” AND “WITH ALL FAULTS.” TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF DEALING OR COURSE OF TRADE. + +5. Limitations of Liability. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF LIABILITY. IN NO EVENT WILL NVIDIA’S AND ITS AFFILIATES TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS LIMIT. + +These exclusions and limitations of liability shall apply regardless if NVIDIA or its affiliates have been advised of the possibility of such damages, and regardless of whether a remedy fails its essential purpose. These exclusions and limitations of liability form an essential basis of the bargain between the parties, and, absent any of these exclusions or limitations of liability, the provisions of this Agreement, including, without limitation, the economic terms, would be substantially different. + +6. Termination. + +6.1 This Agreement will continue to apply until terminated by either you or NVIDIA as described below. + +6.2 If you want to terminate this Agreement, you may do so by stopping to use the SDK. + +6.3 NVIDIA may, at any time, terminate this Agreement if: (i) you fail to comply with any term of this Agreement and the non-compliance is not fixed within thirty (30) days following notice from NVIDIA (or immediately if you violate NVIDIA’s intellectual property rights); (ii) you commence or participate in any legal proceeding against NVIDIA with respect to the SDK; or (iii) NVIDIA decides to no longer provide the SDK in a country or, in NVIDIA’s sole discretion, the continued use of it is no longer commercially viable. + +6.4 Upon any termination of this Agreement, you agree to promptly discontinue use of the SDK and destroy all copies in your possession or control. Your prior distributions in accordance with this Agreement are not affected by the termination of this Agreement. Upon written request, you will certify in writing that you have complied with your commitments under this section. Upon any termination of this Agreement all provisions survive except for the licenses granted to you. + +7. General. + +If you wish to assign this Agreement or your rights and obligations, including by merger, consolidation, dissolution or operation of law, contact NVIDIA to ask for permission. Any attempted assignment not approved by NVIDIA in writing shall be void and of no effect. NVIDIA may assign, delegate or transfer this Agreement and its rights and obligations, and if to a non-affiliate you will be notified. + +You agree to cooperate with NVIDIA and provide reasonably requested information to verify your compliance with this Agreement. + +This Agreement will be governed in all respects by the laws of the United States and of the State of Delaware as those laws are applied to contracts entered into and performed entirely within Delaware by Delaware residents, without regard to the conflicts of laws principles. The United Nations Convention on Contracts for the International Sale of Goods is specifically disclaimed. You agree to all terms of this Agreement in the English language. + +The state or federal courts residing in Santa Clara County, California shall have exclusive jurisdiction over any dispute or claim arising out of this Agreement. Notwithstanding this, you agree that NVIDIA shall still be allowed to apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +If any court of competent jurisdiction determines that any provision of this Agreement is illegal, invalid or unenforceable, such provision will be construed as limited to the extent necessary to be consistent with and fully enforceable under the law and the remaining provisions will remain in full force and effect. Unless otherwise specified, remedies are cumulative. + +Each party acknowledges and agrees that the other is an independent contractor in the performance of this Agreement. + +The SDK has been developed entirely at private expense and is “commercial items” consisting of “commercial computer software” and “commercial computer software documentation” provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the U.S. Government or a U.S. Government subcontractor is subject to the restrictions in this Agreement pursuant to DFARS 227.7202-3(a) or as set forth in subparagraphs (b)(1) and (2) of the Commercial Computer Software - Restricted Rights clause at FAR 52.227-19, as applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas Expressway, Santa Clara, CA 95051. + +The SDK is subject to United States export laws and regulations. You agree that you will not ship, transfer or export the SDK into any country, or use the SDK in any manner, prohibited by the United States Bureau of Industry and Security or economic sanctions regulations administered by the U.S. Department of Treasury’s Office of Foreign Assets Control (OFAC), or any applicable export laws, restrictions or regulations. These laws include restrictions on destinations, end users and end use. By accepting this Agreement, you confirm that you are not a resident or citizen of any country currently embargoed by the U.S. and that you are not otherwise prohibited from receiving the SDK. + +Any notice delivered by NVIDIA to you under this Agreement will be delivered via mail, email or fax. You agree that any notices that NVIDIA sends you electronically will satisfy any legal communication requirements. Please direct your legal notices or other correspondence to NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department. + +This Agreement and any exhibits incorporated into this Agreement constitute the entire agreement of the parties with respect to the subject matter of this Agreement and supersede all prior negotiations or documentation exchanged between the parties relating to this SDK license. Any additional and/or conflicting terms on documents issued by you are null, void, and invalid. Any amendment or waiver under this Agreement shall be in writing and signed by representatives of both parties. + +(v. January 28, 2020) + + + + + + + + + + + + + + + +nvJPEG2K SUPPLEMENT TO SOFTWARE LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + +The terms in this supplement govern your use of the NVIDIA nvJPEG2K SDK under the terms of your license agreement (“Agreement”) as modified by this supplement. Capitalized terms used but not defined below have the meaning assigned to them in the Agreement. + +This supplement is an exhibit to the Agreement and is incorporated as an integral part of the Agreement. In the event of conflict between the terms in this supplement and the terms in the Agreement, the terms in this supplement govern. + +4.1 License Scope. The SDK is licensed for you to develop applications only for use in systems with NVIDIA GPUs. + +2. Distribution. The following portions of the SDK are distributable under the Agreement: the runtime files .so and .h, nvjpeg2k_0.dll, nvjpeg2k.lib and libnvjpeg2k_static.a. + +3. Licensing. If the distribution terms in this Agreement are not suitable for your organization, or for any questions regarding this Agreement, please contact NVIDIA at nvidia-compute-license-questions@nvidia.com. + (v. November 14, 2020) + + +OpenJPEG LICENSE + +/* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + + + + + +nvidia-nvshmem-cu13 +LicenseRef-NVIDIA-Proprietary +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvidia-nvtiff-cu12 +Other/Proprietary License +https://developer.nvidia.com/nvtiff +NVIDIA nvTIFF +Software License Agreement | NVIDIA Docs + +Table of Contents +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS iii +Chapter 1. License. 1 +1.1. Grant 1 +1.2. Distribution Requirements 1 +1.3. Authorized Users 2 +1.4. Pre-Release SDK 2 +1.5. Updates 2 +1.6. Third Party Licenses 2 +1.7. Reservation of Rights 3 +Chapter 2. Limitations. 4 +Chapter 3. Ownership. 5 +Chapter 4. No Warranties. 6 +Chapter 5. Limitations of Liability. 7 +Chapter 6. Termination. 8 +Chapter 7. General. 9 + + + +LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + + + +This license agreement, including exhibits attached ("Agreement") is a legal agreement between you and NVIDIA Corporation ("NVIDIA") and governs your use of a NVIDIA software development kit ("SDK"). +Each SDK has its own set of software and materials, but here is a description of the types of items that may be included in a SDK: source code, header files, APIs, data sets and assets (examples include images, textures, models, scenes, videos, native API input/output files), binary software, sample code, libraries, utility programs, programming code and documentation. +This Agreement can be accepted only by an adult of legal age of majority in the country in which the SDK is used. +If you are entering into this Agreement on behalf of a company or other legal entity, you represent that you have the legal authority to bind the entity to this Agreement, in which case "you" will mean the entity you represent. +If you don't have the required age or authority to accept this Agreement, or if you don't accept all the terms and conditions of this Agreement, do not download, install or use the SDK. +You agree to use the SDK only for purposes that are permitted by (a) this Agreement, and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions. + +Chapter 1. License. + +1.1. Grant +Subject to the terms of this Agreement, NVIDIA hereby grants you a non-exclusive, non- transferable license, without the right to sublicense (except as expressly provided in this Agreement) to: +1. Install and use the SDK, +2. Modify and create derivative works of sample source code delivered in the SDK, and +3. Distribute those portions of the SDK that are identified in this Agreement as distributable, as incorporated in object code format into a software application that meets the distribution requirements indicated in this Agreement. + +1.2. Distribution Requirements +These are the distribution requirements for you to exercise the distribution grant: +1. Your application must have material additional functionality, beyond the included portions of the SDK. +2. The distributable portions of the SDK shall only be accessed by your application. +3. The following notice shall be included in modifications and derivative works of sample source code distributed: "This software contains source code provided by NVIDIA Corporation." +4. Unless a developer tool is identified in this Agreement as distributable, it is delivered for your internal use only. +5. The terms under which you distribute your application must be consistent with the terms of this Agreement, including (without limitation) terms relating to the license grant and license restrictions and protection of NVIDIA's intellectual property rights. Additionally, you agree that you will protect the privacy, security and legal rights of your application users. +6. You agree to notify NVIDIA in writing of any known or suspected distribution or use of the SDK not in compliance with the requirements of this Agreement, and to enforce the terms of your agreements with respect to distributed SDK. + + +1.3. Authorized Users +You may allow employees and contractors of your entity or of your subsidiary(ies) to access and use the SDK from your secure network to perform work on your behalf. +If you are an academic institution you may allow users enrolled or employed by the academic institution to access and use the SDK from your secure network. +You are responsible for the compliance with the terms of this Agreement by your authorized users. If you become aware that your authorized users didn't follow the terms of this Agreement, you agree to take reasonable steps to resolve the non-compliance and prevent new occurrences. + +1.4. Pre-Release SDK +The SDK versions identified as alpha, beta, preview or otherwise as pre-release, may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, accessibility, availability, and reliability standards relative to commercial +versions of NVIDIA software and materials. Use of a pre-release SDK may result in unexpected results, loss of data, project delays or other unpredictable damage or loss. +You may use a pre-release SDK at your own risk, understanding that pre-release SDKs are not intended for use in production or business-critical systems. +NVIDIA may choose not to make available a commercial version of any pre-release SDK. NVIDIA may also choose to abandon development and terminate the availability of a pre- release SDK at any time without liability. + +1.5. Updates +NVIDIA may, at its option, make available patches, workarounds or other updates to this SDK. Unless the updates are provided with their separate governing terms, they are deemed part of the SDK licensed to you as provided in this Agreement. +You agree that the form and content of the SDK that NVIDIA provides may change without prior notice to you. While NVIDIA generally maintains compatibility between versions, NVIDIA may in some cases make changes that introduce incompatibilities in future versions of the SDK. + +1.6. Components Under Other Licenses +The SDK may come bundled with, or otherwise include or be distributed with, NVIDIA or third party software licensed with separate legal notices or terms as may be described in proprietary notices accompanying the SDK. If and to the extent there is a conflict between the terms in this Agreement and the license terms associated with the component, the license terms associated with the components control only to the extent necessary to resolve the conflict. + +1.7. Reservation of Rights +NVIDIA reserves all rights, title and interest in and to the SDK not expressly granted to you under this Agreement. + + +Chapter 2. Limitations. + +The following license limitations apply to your use of the SDK: +2.1 You may not reverse engineer, decompile or disassemble, or remove copyright or other proprietary notices from any portion of the SDK or copies of the SDK. +2.2 Except as expressly provided in this Agreement, you may not copy, sell, rent, sublicense, transfer, distribute, modify, or create derivative works of any portion of the SDK. +2.3 Unless you have an agreement with NVIDIA for this purpose, you may not indicate that an application created with the SDK is sponsored or endorsed by NVIDIA. +2.4 You may not bypass, disable, or circumvent any encryption, security, digital rights management or authentication mechanism in the SDK. +2.5 You may not use the SDK in any manner that would cause it to become subject to an open source software license. As examples, licenses that require as a condition of use, modification, and/or distribution that the SDK be (i) disclosed or distributed in source code form; (ii) licensed for the purpose of making derivative works; or (iii) redistributable at no charge. +2.6 You acknowledge that the SDK as delivered is not tested or certified by NVIDIA for use in connection with the design, construction, maintenance, and/or operation of any system where the use or failure of such system could result in a situation that threatens the safety of human life or results in catastrophic damages (each, a "Critical Application"). Examples of Critical Applications include use in avionics, navigation, autonomous vehicle applications, ai solutions for automotive products, military, medical, life support or other life critical applications. NVIDIA shall not be liable to you or any third party, in whole or in part, for any claims or damages arising from such uses. You are solely responsible for ensuring that any product +or service developed with the SDK as a whole includes sufficient features to comply with all applicable legal and regulatory standards and requirements. +2.7 You agree to defend, indemnify and hold harmless NVIDIA and its affiliates, and their respective employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, fines, restitutions and expenses (including but not limited to attorney's fees and costs incident to establishing the right of indemnification) arising out of or related to products or services that use the SDK in or for Critical Applications, and for use of the SDK outside of the scope of this Agreement or not in compliance with its terms. + + +Chapter 3. Ownership. + +3.1 NVIDIA or its licensors hold all rights, title and interest in and to the SDK and its modifications and derivative works, including their respective intellectual property rights, subject to your rights under Section 3.2. This SDK may include software and materials from NVIDIA's licensors, and these licensors are intended third party beneficiaries that may enforce this Agreement with respect to their intellectual property rights. +3.2 You hold all rights, title and interest in and to your applications and your derivative works of the sample source code delivered in the SDK, including their respective intellectual property rights, subject to NVIDIA's rights under section 3.1. +3.3 You may, but don't have to, provide to NVIDIA suggestions, feature requests or other feedback regarding the SDK, including possible enhancements or modifications to the SDK. For any feedback that you voluntarily provide, you hereby grant NVIDIA and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. NVIDIA will use feedback at its choice. NVIDIA is constantly looking for ways to improve its products, so you may send feedback to NVIDIA through the developer portal at https://developer.nvidia.com. + + +Chapter 4. No Warranties. + + +THE SDK IS PROVIDED BY NVIDIA "AS IS" AND "WITH ALL FAULTS." TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF DEALING OR COURSE OF TRADE. + + +Chapter 5. Limitations of Liability. + + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH OF CONTRACT, BREACH OF WARRANTY, +TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF LIABILITY. IN NO EVENT WILL NVIDIA'S AND ITS AFFILIATES TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS LIMIT. +These exclusions and limitations of liability shall apply regardless if NVIDIA or its affiliates have been advised of the possibility of such damages, and regardless of whether a remedy fails its essential purpose. These exclusions and limitations of liability form an essential basis of the bargain between the parties, and, absent any of these exclusions or limitations of liability, the provisions of this Agreement, including, without limitation, the economic terms, would be substantially different. + + +Chapter 6. Termination. + + + +6.1 This Agreement will continue to apply until terminated by either you or NVIDIA as described below. +6.2 If you want to terminate this Agreement, you may do so by stopping to use the SDK. +6.3 NVIDIA may, at any time, terminate this Agreement if: (i) you fail to comply with any term of this Agreement and the non-compliance is not fixed within thirty (30) days following notice from NVIDIA (or immediately if you violate NVIDIA's intellectual property rights); (ii) you commence or participate in any legal proceeding against NVIDIA with respect to the SDK; or +(iii) NVIDIA decides to no longer provide the SDK in a country or, in NVIDIA's sole discretion, the continued use of it is no longer commercially viable. +6.4 Upon any termination of this Agreement, you agree to promptly discontinue use of the SDK and destroy all copies in your possession or control. Your prior distributions in accordance with this Agreement are not affected by the termination of this Agreement. Upon written request, you will certify in writing that you have complied with your commitments under this section. Upon any termination of this Agreement all provisions survive except for the licenses granted to you. + + +Chapter 7. General. + + +If you wish to assign this Agreement or your rights and obligations, including by merger, consolidation, dissolution or operation of law, contact NVIDIA to ask for permission. Any attempted assignment not approved by NVIDIA in writing shall be void and of no effect. NVIDIA may assign, delegate or transfer this Agreement and its rights and obligations, and if to a non- affiliate you will be notified. +You agree to cooperate with NVIDIA and provide reasonably requested information to verify your compliance with this Agreement. +This Agreement will be governed in all respects by the laws of the United States and of the State of Delaware as those laws are applied to contracts entered into and performed entirely within Delaware by Delaware residents, without regard to the conflicts of laws principles. +The United Nations Convention on Contracts for the International Sale of Goods is specifically disclaimed. You agree to all terms of this Agreement in the English language. +The state or federal courts residing in Santa Clara County, California shall have exclusive jurisdiction over any dispute or claim arising out of this Agreement. Notwithstanding this, you agree that NVIDIA shall still be allowed to apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. +If any court of competent jurisdiction determines that any provision of this Agreement is illegal, invalid or unenforceable, such provision will be construed as limited to the extent necessary to be consistent with and fully enforceable under the law and the remaining provisions will remain in full force and effect. Unless otherwise specified, remedies are cumulative. +Each party acknowledges and agrees that the other is an independent contractor in the performance of this Agreement. +The SDK has been developed entirely at private expense and is "commercial items" consisting of "commercial computer software" and "commercial computer software documentation" provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the U.S. Government +or a U.S. Government subcontractor is subject to the restrictions in this Agreement pursuant to DFARS 227.7202-3(a) or as set forth in subparagraphs (b)(1) and (2) of the Commercial Computer Software - Restricted Rights clause at FAR 52.227-19, as applicable. Contractor/ manufacturer is NVIDIA, 2788 San Tomas Expressway, Santa Clara, CA 95051. +The SDK is subject to United States export laws and regulations. You agree that you will not ship, transfer or export the SDK into any country, or use the SDK in any manner, prohibited by the United States Bureau of Industry and Security or economic sanctions regulations administered by the U.S. Department of Treasury's Office of Foreign Assets Control (OFAC), + + + + +or any applicable export laws, restrictions or regulations. These laws include restrictions on destinations, end users and end use. By accepting this Agreement, you confirm that you are not a resident or citizen of any country currently embargoed by the U.S. and that you are not otherwise prohibited from receiving the SDK. +Any notice delivered by NVIDIA to you under this Agreement will be delivered via mail, email or fax. You agree that any notices that NVIDIA sends you electronically will satisfy any legal communication requirements. Please direct your legal notices or other correspondence to +NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department. +This Agreement and any exhibits incorporated into this Agreement constitute the entire agreement of the parties with respect to the subject matter of this Agreement and supersede all prior negotiations or documentation exchanged between the parties relating to this SDK license. Any additional and/or conflicting terms on documents issued by you are null, void, and invalid. Any amendment or waiver under this Agreement shall be in writing and signed by representatives of both parties. +(v. March 31, 2022) + + +Chapter 8. nvTIFF SUPPLEMENT +TO SOFTWARE LICENSE AGREEMENT FOR NVIDIA SOFTWARE DEVELOPMENT KITS + + +The terms in this supplement govern your use of the NVIDIA nvTIFF SDK under the terms of your license agreement ("Agreement") as modified by this supplement. Capitalized terms used but not defined below have the meaning assigned to them in the Agreement. +This supplement is an exhibit to the Agreement and is incorporated as an integral part of the Agreement. In the event of conflict between the terms in this supplement and the terms in the Agreement, the terms in this supplement govern. +1. License Scope. The SDK is licensed for you to develop applications only for use in systems with NVIDIA GPUs. +2. Distribution. The following portions of the SDK are distributable under the Agreement: the runtime files .so and .h, nvTIFF_0.dll, nvTIFF.lib and libnvTIFF_static.a. +3. Licensing. If the distribution terms in this Agreement are not suitable for your organization, or for any questions regarding this Agreement, please contact NVIDIA at nvidia-compute- license-questions@nvidia.com +(v. March 31, 2022) + + + + + + + + + + + +nvidia-nvtx +Other/Proprietary License +https://developer.nvidia.com/cuda-zone +UNKNOWN + +nvtx +Apache Software License +https://github.com/NVIDIA/NVTX +============================================================================== +NVTX is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + + +obstore +MIT License +https://developmentseed.org/obstore +UNKNOWN + +omegaconf +BSD License +https://github.com/omry/omegaconf +BSD 3-Clause License + +Copyright (c) 2018, Omry Yadan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +onnx +Apache-2.0 +https://onnx.ai/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +onnx-ir +Apache-2.0 +https://onnx.ai/ir-py + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +onnxscript +MIT License +https://microsoft.github.io/onnxscript/ +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +open_clip_torch +MIT License +https://github.com/mlfoundations/open_clip +Copyright (c) 2012-2021 Gabriel Ilharco, Mitchell Wortsman, +Nicholas Carlini, Rohan Taori, Achal Dave, Vaishaal Shankar, +John Miller, Hongseok Namkoong, Hannaneh Hajishirzi, Ali Farhadi, +Ludwig Schmidt + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +openai +Apache Software License +https://github.com/openai/openai-python + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 OpenAI + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opencensus +Apache Software License +https://github.com/census-instrumentation/opencensus-python + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opencensus-context +Apache Software License +https://github.com/census-instrumentation/opencensus-python/tree/master/context/opencensus-context +UNKNOWN + +opencv-contrib-python +Apache Software License +https://github.com/opencv/opencv-python +OpenCV library is redistributed within opencv-python package. +This license applies to OpenCV binary in the directory cv2/. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ +libvpx is redistributed within all opencv-python Linux packages. +This license applies to libvpx binary in the directory cv2/. + +Copyright (c) 2010, The WebM Project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google, nor the WebM Project, nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +FFmpeg is redistributed within all opencv-python packages. + +Libbluray, libgnutls, libnettle, libhogweed, libintl, libmp3lame, libp11, +librtmp, libsoxr and libtasn1 are redistributed within all opencv-python macOS packages. + +This license applies to the above library binaries in the directory cv2/. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + +------------------------------------------------------------------------------ +Qt 5 is redistributed within non-headless opencv-python Linux and macOS packages. +libgmp is redistributed within opencv-python macOS packages. +libidn2 is redistributed within opencv-python macOS packages. +libunistring is redistributed within opencv-python macOS packages. +This license applies to the above binaries in the directory cv2/. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +------------------------------------------------------------------------------ +bzip2 is redistributed within all opencv-python Linux packages. +This license applies to libbz2 binary in the directory cv2/. + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2010 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@bzip.org +bzip2/libbzip2 version 1.0.6 of 6 September 2010 + +------------------------------------------------------------------------------ +libcrypto and libssl are redistributed within all opencv-python Linux and macOS packages. +libopencore-amrnb and libopencore-amrwb are redistributed within all opencv-python Linux and macOS packages. +This license applies to above binaries in the directory cv2/. + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are adhered to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the routines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publicly available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +------------------------------------------------------------------------------ +libfontconfig is redistributed within all opencv-python macOS packages. +This license applies to libfontconfig binary in the directory cv2/. + +Copyright © 2000,2001,2002,2003,2004,2006,2007 Keith Packard +Copyright © 2005 Patrick Lam +Copyright © 2009 Roozbeh Pournader +Copyright © 2008,2009 Red Hat, Inc. +Copyright © 2008 Danilo Šegan +Copyright © 2012 Google, Inc. + + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of the author(s) not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. The authors make no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +libfreetype is redistributed within opencv-python Linux and macOS packages. +This license applies to libfreetype binary in the directory cv2/. + + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + +------------------------------------------------------------------------------ +libpng is redistributed within all opencv-python Linux and macOS packages. +This license applies to libpng binary in the directory cv2/. + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2019 The PNG Reference Library Authors. + * Copyright (c) 2018-2019 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. + +------------------------------------------------------------------------------ +libz is redistributed within all opencv-python Linux packages. +This license applies to libz binary in the directory cv2/. + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +------------------------------------------------------------------------------ +libdav1d is redistributed within opencv-python macOS packages. +This license applies to libdav1d binary in the directory cv2/. + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libffi is redistributed within opencv-python macOS packages. +This license applies to libffi binary in the directory cv2/. + +libffi - Copyright (c) 1996-2020 Anthony Green, Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------ +libogg is redistributed within opencv-python macOS packages. +This license applies to libogg binary in the directory cv2/. + +Copyright (c) 2002, Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libopenjp2 is redistributed within opencv-python macOS packages. +This license applies to libopenjp2 binary in the directory cv2/. + +The copyright in this software is being made available under the 2-clauses +BSD License, included below. This software may be subject to other third +party and contributor rights, including patent rights, and no such rights +are granted under this license. + +Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium +Copyright (c) 2002-2014, Professor Benoit Macq +Copyright (c) 2003-2014, Antonin Descampe +Copyright (c) 2003-2009, Francois-Olivier Devaux +Copyright (c) 2005, Herve Drolon, FreeImage Team +Copyright (c) 2002-2003, Yannick Verschueren +Copyright (c) 2001-2003, David Janssens +Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France +Copyright (c) 2012, CS Systemes d'Information, France + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libopus is redistributed within opencv-python macOS packages. +This license applies to libopus binary in the directory cv2/. + +Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, + Jean-Marc Valin, Timothy B. Terriberry, + CSIRO, Gregory Maxwell, Mark Borgerding, + Erik de Castro Lopo + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Opus is subject to the royalty-free patent licenses which are +specified at: + +Xiph.Org Foundation: +https://datatracker.ietf.org/ipr/1524/ + +Microsoft Corporation: +https://datatracker.ietf.org/ipr/1914/ + +Broadcom Corporation: +https://datatracker.ietf.org/ipr/1526/ + +------------------------------------------------------------------------------ +librav1e is redistributed within opencv-python macOS packages. +This license applies to librav1e binary in the directory cv2/. + +BSD 2-Clause License + +Copyright (c) 2017-2020, the rav1e contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libsnappy is redistributed within opencv-python macOS packages. +This license applies to libsnappy binary in the directory cv2/. + +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libspeex is redistributed within opencv-python macOS packages. +This license applies to libspeex binary in the directory cv2/. + +Copyright 2002-2008 Xiph.org Foundation +Copyright 2002-2008 Jean-Marc Valin +Copyright 2005-2007 Analog Devices Inc. +Copyright 2005-2008 Commonwealth Scientific and Industrial Research + Organisation (CSIRO) +Copyright 1993, 2002, 2006 David Rowe +Copyright 2003 EpicGames +Copyright 1992-1994 Jutta Degener, Carsten Bormann + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libsrt is redistributed within opencv-python macOS packages. +This license applies to libsrt binary in the directory cv2/. + +/* + * + * Copyright (c) 2001-2017 Cisco Systems, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * Neither the name of the Cisco Systems, Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + + + Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +------------------------------------------------------------------------------ +libtheoradec and libtheoraenc are redistributed within opencv-python macOS packages. +This license applies to libtheoradec and libtheoraenc binaries in the directory cv2/. + + Copyright (C) 2002-2009 Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libwebp and libwebpmux are redistributed within all opencv-python packages. +This license applies to libwebp and libwebpmux binaries in the directory cv2/. + +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libvorbis and libvorbisenc are redistributed within opencv-python macOS packages. +This license applies to libvorbis and libvorbisenc binaries in the directory cv2/. + +Copyright (c) 2002-2020 Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Libxcb utility libraries are redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb related binaries in the directory cv2/. + +Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett. +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors +or their institutions shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this +Software without prior written authorization from the +authors. + +------------------------------------------------------------------------------ +Libxcb-image is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-image binary in the directory cv2/. + +Copyright © 2007-2008 Bart Massey +Copyright © 2008 Julien Danjou +Copyright © 2008 Keith Packard + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-util is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-util binary in the directory cv2/. + +Copyright © 2008 Bart Massey +Copyright © 2008 Ian Osgood +Copyright © 2008 Jamey Sharp +Copyright © 2008 Josh Triplett +Copyright © 2008-2009 Julien Danjou + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-render-util is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-render-util binary in the directory cv2/. + +Copyright © 2000 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Copyright © 2006 Jamey Sharp. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or their +institutions shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the authors. + +Copyright © 2006 Ian Osgood + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or their +institutions shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-icccm is redistributed within opencv-python non-headless Linux packages. +This license applies to Libxcb-icccm binary in the directory cv2/. + +Copyright © 2008-2011 Arnaud Fontaine +Copyright © 2007-2008 Vincent Torri + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +libXau is redistributed within opencv-python non-headless Linux packages. +This license applies to libXau binary in the directory cv2/. + +Copyright 1988, 1993, 1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +------------------------------------------------------------------------------ +Vulkan headers are redistributed within all opencv-python packages. +This license applies to Vulkan headers in the directory 3rdparty/include/vulkan. + +Copyright (c) 2015-2018 The Khronos Group Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +------------------------------------------------------------------------------ +Libjpeg-turbo is redistributed within all opencv-python packages as build option. + +libjpeg-turbo Licenses +====================== + +libjpeg-turbo is covered by three compatible BSD-style open source licenses: + +- The IJG (Independent JPEG Group) License, which is listed in + [README.ijg](README.ijg) + + This license applies to the libjpeg API library and associated programs + (any code inherited from libjpeg, and any modifications to that code.) + +- The Modified (3-clause) BSD License, which is listed below + + This license covers the TurboJPEG API library and associated programs, as + well as the build system. + +- The [zlib License](https://opensource.org/licenses/Zlib) + + This license is a subset of the other two, and it covers the libjpeg-turbo + SIMD extensions. + + +Complying with the libjpeg-turbo Licenses +========================================= + +This section provides a roll-up of the libjpeg-turbo licensing terms, to the +best of our understanding. + +1. If you are distributing a modified version of the libjpeg-turbo source, + then: + + 1. You cannot alter or remove any existing copyright or license notices + from the source. + + **Origin** + - Clause 1 of the IJG License + - Clause 1 of the Modified BSD License + - Clauses 1 and 3 of the zlib License + + 2. You must add your own copyright notice to the header of each source + file you modified, so others can tell that you modified that file (if + there is not an existing copyright header in that file, then you can + simply add a notice stating that you modified the file.) + + **Origin** + - Clause 1 of the IJG License + - Clause 2 of the zlib License + + 3. You must include the IJG README file, and you must not alter any of the + copyright or license text in that file. + + **Origin** + - Clause 1 of the IJG License + +2. If you are distributing only libjpeg-turbo binaries without the source, or + if you are distributing an application that statically links with + libjpeg-turbo, then: + + 1. Your product documentation must include a message stating: + + This software is based in part on the work of the Independent JPEG + Group. + + **Origin** + - Clause 2 of the IJG license + + 2. If your binary distribution includes or uses the TurboJPEG API, then + your product documentation must include the text of the Modified BSD + License (see below.) + + **Origin** + - Clause 2 of the Modified BSD License + +3. You cannot use the name of the IJG or The libjpeg-turbo Project or the + contributors thereof in advertising, publicity, etc. + + **Origin** + - IJG License + - Clause 3 of the Modified BSD License + +4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be + free of defects, nor do we accept any liability for undesirable + consequences resulting from your use of the software. + + **Origin** + - IJG License + - Modified BSD License + - zlib License + + +The Modified (3-clause) BSD License +=================================== + +Copyright (C)2009-2022 D. R. Commander. All Rights Reserved.
+Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +Why Three Licenses? +=================== + +The zlib License could have been used instead of the Modified (3-clause) BSD +License, and since the IJG License effectively subsumes the distribution +conditions of the zlib License, this would have effectively placed +libjpeg-turbo binary distributions under the IJG License. However, the IJG +License specifically refers to the Independent JPEG Group and does not extend +attribution and endorsement protections to other entities. Thus, it was +desirable to choose a license that granted us the same protections for new code +that were granted to the IJG for code derived from their software. + +------------------------------------------------------------------------------ +Libspng is redistributed within all opencv-python packages as build option. + +BSD 2-Clause License + +Copyright (c) 2018-2022, Randy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +QUIRC library is redistributed within all opencv-python packages. + +quirc -- QR-code recognition library +Copyright (C) 2010-2012 Daniel Beer + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +Flatbuffers library is redistributed within all opencv-python packages. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ +Protobuf library is redistributed within all opencv-python packages. + +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +------------------------------------------------------------------------------ +OpenJPEG library is redistributed within all opencv-python packages. + +/* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +------------------------------------------------------------------------------ +TIFF library is redistributed within all opencv-python packages. + +Copyright (c) 1988-1997 Sam Leffler +Copyright (c) 1991-1997 Silicon Graphics, Inc. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that (i) the above copyright notices and this permission notice appear in +all copies of the software and related documentation, and (ii) the names of +Sam Leffler and Silicon Graphics may not be used in any advertising or +publicity relating to the software without the specific, prior written +permission of Sam Leffler and Silicon Graphics. + +THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +OpenEXR library is redistributed within all opencv-python packages. + +Copyright (c) 2006, Industrial Light & Magic, a division of Lucasfilm +Entertainment Company Ltd. Portions contributed and copyright held by +others as indicated. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with + the distribution. + + * Neither the name of Industrial Light & Magic nor the names of + any other contributors to this software may be used to endorse or + promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Intel(R) IPP ICV library statically linked within x86 and x86_64 opencv-python packages. + +Intel(R) Integrated Performance Primitives 2021 Update 10 + +Intel Simplified Software License (Version October 2022) + +Intel(R) Integrated Performance Primitives (Intel(R) IPP) : Copyright (C) 1997 Intel Corporation + +Use and Redistribution. You may use and redistribute the software, which is +provided in binary form only, (the "Software"), without modification, +provided the following conditions are met: + +* Redistributions must reproduce the above copyright notice and these + terms of use in the Software and in the documentation and/or other materials + provided with the distribution. +* Neither the name of Intel nor the names of its suppliers may be used to + endorse or promote products derived from this Software without specific + prior written permission. +* No reverse engineering, decompilation, or disassembly of the Software is + permitted, nor any modification or alteration of the Software or its operation + at any time, including during execution. + +No other licenses. Except as provided in the preceding section, Intel grants no +licenses or other rights by implication, estoppel or otherwise to, patent, +copyright, trademark, trade name, service mark or other intellectual property +licenses or rights of Intel. + +Third party software. "Third Party Software" means the files (if any) listed +in the "third-party-software.txt" or other similarly-named text file that may +be included with the Software. Third Party Software, even if included with the +distribution of the Software, may be governed by separate license terms, including +without limitation, third party license terms, open source software notices and +terms, and/or other Intel software license terms. These separate license terms +solely govern Your use of the Third Party Software. + +DISCLAIMER. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE +DISCLAIMED. THIS SOFTWARE IS NOT INTENDED FOR USE IN SYSTEMS OR APPLICATIONS +WHERE FAILURE OF THE SOFTWARE MAY CAUSE PERSONAL INJURY OR DEATH AND YOU AGREE +THAT YOU ARE FULLY RESPONSIBLE FOR ANY CLAIMS, COSTS, DAMAGES, EXPENSES, AND +ATTORNEYS' FEES ARISING OUT OF ANY SUCH USE, EVEN IF ANY CLAIM ALLEGES THAT +INTEL WAS NEGLIGENT REGARDING THE DESIGN OR MANUFACTURE OF THE SOFTWARE. + +LIMITATION OF LIABILITY. IN NO EVENT WILL INTEL BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +No support. Intel may make changes to the Software, at any time without notice, +and is not obligated to support, update or provide training for the Software. + +Termination. Your right to use the Software is terminated in the event of your +breach of this license. + +Feedback. Should you provide Intel with comments, modifications, corrections, +enhancements or other input ("Feedback") related to the Software, Intel will be +free to use, disclose, reproduce, license or otherwise distribute or exploit the +Feedback in its sole discretion without any obligations or restrictions of any +kind, including without limitation, intellectual property rights or licensing +obligations. + +Compliance with laws. You agree to comply with all relevant laws and regulations +governing your use, transfer, import or export (or prohibition thereof) of the +Software. + +Governing law. All disputes will be governed by the laws of the United States of +America and the State of Delaware without reference to conflict of law +principles and subject to the exclusive jurisdiction of the state or federal +courts sitting in the State of Delaware, and each party agrees that it submits +to the personal jurisdiction and venue of those courts and waives any +objections. THE UNITED NATIONS CONVENTION ON CONTRACTS FOR THE INTERNATIONAL +SALE OF GOODS (1980) IS SPECIFICALLY EXCLUDED AND WILL NOT APPLY TO THE SOFTWARE. + +------------------------------------------------------------------------------ +Orbbec SDK distributed with arm64 MacOS packages. + +MIT License + +Copyright (c) 2023 OrbbecDeveloper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ + +libavif library and it's dependnecies are redistributed within all opencv-python packages. + +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +aom library and it's dependnecies are redistributed within all opencv-python packages. + +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +KAZE Features library is redistributed within all opencv-python packages. + +Copyright (c) 2012, Pablo Fernández Alcantarilla +All Rights Reserved + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +AKAZE Features library is redistributed within all opencv-python packages. + +Copyright (c) 2014, Pablo Fernandez Alcantarilla, Jesus Nuevo +All Rights Reserved + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +opencv-python +Apache Software License +https://github.com/opencv/opencv-python +OpenCV library is redistributed within opencv-python package. +This license applies to OpenCV binary in the directory cv2/. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ +libvpx is redistributed within all opencv-python Linux packages. +This license applies to libvpx binary in the directory cv2/. + +Copyright (c) 2010, The WebM Project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google, nor the WebM Project, nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +FFmpeg is redistributed within all opencv-python packages. + +Libbluray, libgnutls, libnettle, libhogweed, libintl, libmp3lame, libp11, +librtmp, libsoxr and libtasn1 are redistributed within all opencv-python macOS packages. + +This license applies to the above library binaries in the directory cv2/. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + +------------------------------------------------------------------------------ +Qt 5 is redistributed within non-headless opencv-python Linux and macOS packages. +libgmp is redistributed within opencv-python macOS packages. +libidn2 is redistributed within opencv-python macOS packages. +libunistring is redistributed within opencv-python macOS packages. +This license applies to the above binaries in the directory cv2/. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +------------------------------------------------------------------------------ +bzip2 is redistributed within all opencv-python Linux packages. +This license applies to libbz2 binary in the directory cv2/. + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2010 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@bzip.org +bzip2/libbzip2 version 1.0.6 of 6 September 2010 + +------------------------------------------------------------------------------ +libcrypto and libssl are redistributed within all opencv-python Linux and macOS packages. +libopencore-amrnb and libopencore-amrwb are redistributed within all opencv-python Linux and macOS packages. +This license applies to above binaries in the directory cv2/. + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are adhered to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the routines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publicly available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +------------------------------------------------------------------------------ +libfontconfig is redistributed within all opencv-python macOS packages. +This license applies to libfontconfig binary in the directory cv2/. + +Copyright © 2000,2001,2002,2003,2004,2006,2007 Keith Packard +Copyright © 2005 Patrick Lam +Copyright © 2009 Roozbeh Pournader +Copyright © 2008,2009 Red Hat, Inc. +Copyright © 2008 Danilo Šegan +Copyright © 2012 Google, Inc. + + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of the author(s) not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. The authors make no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +libfreetype is redistributed within opencv-python Linux and macOS packages. +This license applies to libfreetype binary in the directory cv2/. + + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + +------------------------------------------------------------------------------ +libpng is redistributed within all opencv-python Linux and macOS packages. +This license applies to libpng binary in the directory cv2/. + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2019 The PNG Reference Library Authors. + * Copyright (c) 2018-2019 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. + +------------------------------------------------------------------------------ +libz is redistributed within all opencv-python Linux packages. +This license applies to libz binary in the directory cv2/. + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +------------------------------------------------------------------------------ +libdav1d is redistributed within opencv-python macOS packages. +This license applies to libdav1d binary in the directory cv2/. + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libffi is redistributed within opencv-python macOS packages. +This license applies to libffi binary in the directory cv2/. + +libffi - Copyright (c) 1996-2020 Anthony Green, Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------ +libogg is redistributed within opencv-python macOS packages. +This license applies to libogg binary in the directory cv2/. + +Copyright (c) 2002, Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libopenjp2 is redistributed within opencv-python macOS packages. +This license applies to libopenjp2 binary in the directory cv2/. + +The copyright in this software is being made available under the 2-clauses +BSD License, included below. This software may be subject to other third +party and contributor rights, including patent rights, and no such rights +are granted under this license. + +Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium +Copyright (c) 2002-2014, Professor Benoit Macq +Copyright (c) 2003-2014, Antonin Descampe +Copyright (c) 2003-2009, Francois-Olivier Devaux +Copyright (c) 2005, Herve Drolon, FreeImage Team +Copyright (c) 2002-2003, Yannick Verschueren +Copyright (c) 2001-2003, David Janssens +Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France +Copyright (c) 2012, CS Systemes d'Information, France + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libopus is redistributed within opencv-python macOS packages. +This license applies to libopus binary in the directory cv2/. + +Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, + Jean-Marc Valin, Timothy B. Terriberry, + CSIRO, Gregory Maxwell, Mark Borgerding, + Erik de Castro Lopo + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Opus is subject to the royalty-free patent licenses which are +specified at: + +Xiph.Org Foundation: +https://datatracker.ietf.org/ipr/1524/ + +Microsoft Corporation: +https://datatracker.ietf.org/ipr/1914/ + +Broadcom Corporation: +https://datatracker.ietf.org/ipr/1526/ + +------------------------------------------------------------------------------ +librav1e is redistributed within opencv-python macOS packages. +This license applies to librav1e binary in the directory cv2/. + +BSD 2-Clause License + +Copyright (c) 2017-2020, the rav1e contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libsnappy is redistributed within opencv-python macOS packages. +This license applies to libsnappy binary in the directory cv2/. + +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libspeex is redistributed within opencv-python macOS packages. +This license applies to libspeex binary in the directory cv2/. + +Copyright 2002-2008 Xiph.org Foundation +Copyright 2002-2008 Jean-Marc Valin +Copyright 2005-2007 Analog Devices Inc. +Copyright 2005-2008 Commonwealth Scientific and Industrial Research + Organisation (CSIRO) +Copyright 1993, 2002, 2006 David Rowe +Copyright 2003 EpicGames +Copyright 1992-1994 Jutta Degener, Carsten Bormann + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libsrt is redistributed within opencv-python macOS packages. +This license applies to libsrt binary in the directory cv2/. + +/* + * + * Copyright (c) 2001-2017 Cisco Systems, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * Neither the name of the Cisco Systems, Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + + + Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +------------------------------------------------------------------------------ +libtheoradec and libtheoraenc are redistributed within opencv-python macOS packages. +This license applies to libtheoradec and libtheoraenc binaries in the directory cv2/. + + Copyright (C) 2002-2009 Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libwebp and libwebpmux are redistributed within all opencv-python packages. +This license applies to libwebp and libwebpmux binaries in the directory cv2/. + +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +libvorbis and libvorbisenc are redistributed within opencv-python macOS packages. +This license applies to libvorbis and libvorbisenc binaries in the directory cv2/. + +Copyright (c) 2002-2020 Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Libxcb utility libraries are redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb related binaries in the directory cv2/. + +Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett. +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors +or their institutions shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this +Software without prior written authorization from the +authors. + +------------------------------------------------------------------------------ +Libxcb-image is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-image binary in the directory cv2/. + +Copyright © 2007-2008 Bart Massey +Copyright © 2008 Julien Danjou +Copyright © 2008 Keith Packard + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-util is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-util binary in the directory cv2/. + +Copyright © 2008 Bart Massey +Copyright © 2008 Ian Osgood +Copyright © 2008 Jamey Sharp +Copyright © 2008 Josh Triplett +Copyright © 2008-2009 Julien Danjou + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-render-util is redistributed within opencv-python non-headless Linux packages. +This license applies to libxcb-render-util binary in the directory cv2/. + +Copyright © 2000 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Copyright © 2006 Jamey Sharp. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or their +institutions shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the authors. + +Copyright © 2006 Ian Osgood + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or their +institutions shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the authors. + +------------------------------------------------------------------------------ +Libxcb-icccm is redistributed within opencv-python non-headless Linux packages. +This license applies to Libxcb-icccm binary in the directory cv2/. + +Copyright © 2008-2011 Arnaud Fontaine +Copyright © 2007-2008 Vincent Torri + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors or +their institutions shall not be used in advertising or otherwise to +promote the sale, use or other dealings in this Software without +prior written authorization from the authors. + +------------------------------------------------------------------------------ +libXau is redistributed within opencv-python non-headless Linux packages. +This license applies to libXau binary in the directory cv2/. + +Copyright 1988, 1993, 1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +------------------------------------------------------------------------------ +Vulkan headers are redistributed within all opencv-python packages. +This license applies to Vulkan headers in the directory 3rdparty/include/vulkan. + +Copyright (c) 2015-2018 The Khronos Group Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +------------------------------------------------------------------------------ +Libjpeg-turbo is redistributed within all opencv-python packages as build option. + +libjpeg-turbo Licenses +====================== + +libjpeg-turbo is covered by three compatible BSD-style open source licenses: + +- The IJG (Independent JPEG Group) License, which is listed in + [README.ijg](README.ijg) + + This license applies to the libjpeg API library and associated programs + (any code inherited from libjpeg, and any modifications to that code.) + +- The Modified (3-clause) BSD License, which is listed below + + This license covers the TurboJPEG API library and associated programs, as + well as the build system. + +- The [zlib License](https://opensource.org/licenses/Zlib) + + This license is a subset of the other two, and it covers the libjpeg-turbo + SIMD extensions. + + +Complying with the libjpeg-turbo Licenses +========================================= + +This section provides a roll-up of the libjpeg-turbo licensing terms, to the +best of our understanding. + +1. If you are distributing a modified version of the libjpeg-turbo source, + then: + + 1. You cannot alter or remove any existing copyright or license notices + from the source. + + **Origin** + - Clause 1 of the IJG License + - Clause 1 of the Modified BSD License + - Clauses 1 and 3 of the zlib License + + 2. You must add your own copyright notice to the header of each source + file you modified, so others can tell that you modified that file (if + there is not an existing copyright header in that file, then you can + simply add a notice stating that you modified the file.) + + **Origin** + - Clause 1 of the IJG License + - Clause 2 of the zlib License + + 3. You must include the IJG README file, and you must not alter any of the + copyright or license text in that file. + + **Origin** + - Clause 1 of the IJG License + +2. If you are distributing only libjpeg-turbo binaries without the source, or + if you are distributing an application that statically links with + libjpeg-turbo, then: + + 1. Your product documentation must include a message stating: + + This software is based in part on the work of the Independent JPEG + Group. + + **Origin** + - Clause 2 of the IJG license + + 2. If your binary distribution includes or uses the TurboJPEG API, then + your product documentation must include the text of the Modified BSD + License (see below.) + + **Origin** + - Clause 2 of the Modified BSD License + +3. You cannot use the name of the IJG or The libjpeg-turbo Project or the + contributors thereof in advertising, publicity, etc. + + **Origin** + - IJG License + - Clause 3 of the Modified BSD License + +4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be + free of defects, nor do we accept any liability for undesirable + consequences resulting from your use of the software. + + **Origin** + - IJG License + - Modified BSD License + - zlib License + + +The Modified (3-clause) BSD License +=================================== + +Copyright (C)2009-2022 D. R. Commander. All Rights Reserved.
+Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +Why Three Licenses? +=================== + +The zlib License could have been used instead of the Modified (3-clause) BSD +License, and since the IJG License effectively subsumes the distribution +conditions of the zlib License, this would have effectively placed +libjpeg-turbo binary distributions under the IJG License. However, the IJG +License specifically refers to the Independent JPEG Group and does not extend +attribution and endorsement protections to other entities. Thus, it was +desirable to choose a license that granted us the same protections for new code +that were granted to the IJG for code derived from their software. + +------------------------------------------------------------------------------ +Libspng is redistributed within all opencv-python packages as build option. + +BSD 2-Clause License + +Copyright (c) 2018-2022, Randy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +QUIRC library is redistributed within all opencv-python packages. + +quirc -- QR-code recognition library +Copyright (C) 2010-2012 Daniel Beer + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +Flatbuffers library is redistributed within all opencv-python packages. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ +Protobuf library is redistributed within all opencv-python packages. + +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +------------------------------------------------------------------------------ +OpenJPEG library is redistributed within all opencv-python packages. + +/* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +------------------------------------------------------------------------------ +TIFF library is redistributed within all opencv-python packages. + +Copyright (c) 1988-1997 Sam Leffler +Copyright (c) 1991-1997 Silicon Graphics, Inc. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that (i) the above copyright notices and this permission notice appear in +all copies of the software and related documentation, and (ii) the names of +Sam Leffler and Silicon Graphics may not be used in any advertising or +publicity relating to the software without the specific, prior written +permission of Sam Leffler and Silicon Graphics. + +THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + +------------------------------------------------------------------------------ +OpenEXR library is redistributed within all opencv-python packages. + +Copyright (c) 2006, Industrial Light & Magic, a division of Lucasfilm +Entertainment Company Ltd. Portions contributed and copyright held by +others as indicated. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with + the distribution. + + * Neither the name of Industrial Light & Magic nor the names of + any other contributors to this software may be used to endorse or + promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Intel(R) IPP ICV library statically linked within x86 and x86_64 opencv-python packages. + +Intel(R) Integrated Performance Primitives 2021 Update 10 + +Intel Simplified Software License (Version October 2022) + +Intel(R) Integrated Performance Primitives (Intel(R) IPP) : Copyright (C) 1997 Intel Corporation + +Use and Redistribution. You may use and redistribute the software, which is +provided in binary form only, (the "Software"), without modification, +provided the following conditions are met: + +* Redistributions must reproduce the above copyright notice and these + terms of use in the Software and in the documentation and/or other materials + provided with the distribution. +* Neither the name of Intel nor the names of its suppliers may be used to + endorse or promote products derived from this Software without specific + prior written permission. +* No reverse engineering, decompilation, or disassembly of the Software is + permitted, nor any modification or alteration of the Software or its operation + at any time, including during execution. + +No other licenses. Except as provided in the preceding section, Intel grants no +licenses or other rights by implication, estoppel or otherwise to, patent, +copyright, trademark, trade name, service mark or other intellectual property +licenses or rights of Intel. + +Third party software. "Third Party Software" means the files (if any) listed +in the "third-party-software.txt" or other similarly-named text file that may +be included with the Software. Third Party Software, even if included with the +distribution of the Software, may be governed by separate license terms, including +without limitation, third party license terms, open source software notices and +terms, and/or other Intel software license terms. These separate license terms +solely govern Your use of the Third Party Software. + +DISCLAIMER. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE +DISCLAIMED. THIS SOFTWARE IS NOT INTENDED FOR USE IN SYSTEMS OR APPLICATIONS +WHERE FAILURE OF THE SOFTWARE MAY CAUSE PERSONAL INJURY OR DEATH AND YOU AGREE +THAT YOU ARE FULLY RESPONSIBLE FOR ANY CLAIMS, COSTS, DAMAGES, EXPENSES, AND +ATTORNEYS' FEES ARISING OUT OF ANY SUCH USE, EVEN IF ANY CLAIM ALLEGES THAT +INTEL WAS NEGLIGENT REGARDING THE DESIGN OR MANUFACTURE OF THE SOFTWARE. + +LIMITATION OF LIABILITY. IN NO EVENT WILL INTEL BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +No support. Intel may make changes to the Software, at any time without notice, +and is not obligated to support, update or provide training for the Software. + +Termination. Your right to use the Software is terminated in the event of your +breach of this license. + +Feedback. Should you provide Intel with comments, modifications, corrections, +enhancements or other input ("Feedback") related to the Software, Intel will be +free to use, disclose, reproduce, license or otherwise distribute or exploit the +Feedback in its sole discretion without any obligations or restrictions of any +kind, including without limitation, intellectual property rights or licensing +obligations. + +Compliance with laws. You agree to comply with all relevant laws and regulations +governing your use, transfer, import or export (or prohibition thereof) of the +Software. + +Governing law. All disputes will be governed by the laws of the United States of +America and the State of Delaware without reference to conflict of law +principles and subject to the exclusive jurisdiction of the state or federal +courts sitting in the State of Delaware, and each party agrees that it submits +to the personal jurisdiction and venue of those courts and waives any +objections. THE UNITED NATIONS CONVENTION ON CONTRACTS FOR THE INTERNATIONAL +SALE OF GOODS (1980) IS SPECIFICALLY EXCLUDED AND WILL NOT APPLY TO THE SOFTWARE. + +------------------------------------------------------------------------------ +Orbbec SDK distributed with arm64 MacOS packages. + +MIT License + +Copyright (c) 2023 OrbbecDeveloper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ + +libavif library and it's dependnecies are redistributed within all opencv-python packages. + +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +aom library and it's dependnecies are redistributed within all opencv-python packages. + +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +KAZE Features library is redistributed within all opencv-python packages. + +Copyright (c) 2012, Pablo Fernández Alcantarilla +All Rights Reserved + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +AKAZE Features library is redistributed within all opencv-python packages. + +Copyright (c) 2014, Pablo Fernandez Alcantarilla, Jesus Nuevo +All Rights Reserved + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +opentelemetry-api +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opentelemetry-exporter-otlp-proto-common +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-common + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opentelemetry-exporter-otlp-proto-http +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opentelemetry-proto +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opentelemetry-sdk +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +opentelemetry-semantic-conventions +Apache-2.0 +https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +orderly-set +MIT License +https://github.com/seperman/orderly-set +UNKNOWN + +orjson +MPL-2.0 AND (Apache-2.0 OR MIT) +https://github.com/ijl/orjson + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +packaging +Apache Software License; BSD License +https://github.com/pypa/packaging +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. + + +pandas +BSD License +https://pandas.pydata.org +BSD 3-Clause License + +Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2011-2023, Open source contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2010-2019 Keith Goodman +Copyright (c) 2019 Bottleneck Developers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE.Copyright 2017- Paul Ganssle +Copyright 2017- dateutil contributors (see AUTHORS file) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The above license applies to all contributions after 2017-12-01, as well as +all contributions that have been re-licensed (see AUTHORS file for the list of +contributors who have re-licensed their code). +-------------------------------------------------------------------------------- +dateutil - Extensions to the standard Python datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer +Copyright (c) 2012-2014 - Tomi Pieviläinen +Copyright (c) 2014-2016 - Yaron de Leeuw +Copyright (c) 2015- - Paul Ganssle +Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The above BSD License Applies to all code, even that also covered by Apache 2.0.# MIT License + +Copyright (c) 2019 Hadley Wickham; RStudio; and Evan Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +Based on http://opensource.org/licenses/MIT + +This is a template. Complete and ship as file LICENSE the following 2 +lines (only) + +YEAR: +COPYRIGHT HOLDER: + +and specify as + +License: MIT + file LICENSE + +Copyright (c) , + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The MIT License + +Copyright (c) 2008- Attractive Chaos + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- + +Authors/contributors include: + +A. Wilcox +Ada Worcester +Alex Dowad +Alex Suykov +Alexander Monakov +Andre McCurdy +Andrew Kelley +Anthony G. Basile +Aric Belsito +Arvid Picciani +Bartosz Brachaczek +Benjamin Peterson +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Daniel Micay +Daniel Sabogal +Daurnimator +David Carlier +David Edelsohn +Denys Vlasenko +Dmitry Ivanov +Dmitry V. Levin +Drew DeVault +Emil Renner Berthing +Fangrui Song +Felix Fietkau +Felix Janda +Gianluca Anzolin +Hauke Mehrtens +He X +Hiltjo Posthuma +Isaac Dunham +Jaydeep Patil +Jens Gustedt +Jeremy Huntwork +Jo-Philipp Wich +Joakim Sindholt +John Spencer +Julien Ramseier +Justin Cormack +Kaarle Ritvanen +Khem Raj +Kylie McClain +Leah Neukirchen +Luca Barbato +Luka Perkov +M Farkas-Dyck (Strake) +Mahesh Bodapati +Markus Wichmann +Masanori Ogino +Michael Clark +Michael Forney +Mikhail Kremnyov +Natanael Copa +Nicholas J. Kain +orc +Pascal Cuoq +Patrick Oppenlander +Petr Hosek +Petr Skocik +Pierre Carrier +Reini Urban +Rich Felker +Richard Pennington +Ryan Fairfax +Samuel Holland +Segev Finer +Shiz +sin +Solar Designer +Stefan Kristiansson +Stefan O'Rear +Szabolcs Nagy +Timo Teräs +Trutz Behn +Valentin Ochs +Will Dietz +William Haddon +William Pitcock + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The AArch64 memcpy and memset code (src/string/aarch64/*) are +Copyright © 1999-2019, Arm Limited. + +The implementation of DES for crypt (src/crypt/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Valentin Ochs and is licensed under an MIT-style license. + +The x86_64 port was written by Nicholas J. Kain and is licensed under +the standard MIT terms. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The mips64 port was contributed by Imagination Technologies and is +licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +In addition, permission is hereby granted for all public header files +(include/* and arch/*/bits/*) and crt files intended to be linked into +applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit +the copyright notice and permission notice otherwise required by the +license, and to use these files without any requirement of +attribution. These files include substantial contributions from: + +Bobby Bingham +John Spencer +Nicholas J. Kain +Rich Felker +Richard Pennington +Stefan Kristiansson +Szabolcs Nagy + +all of whom have explicitly granted such permission. + +This file previously contained text expressing a belief that most of +the files covered by the above exception were sufficiently trivial not +to be subject to copyright, resulting in confusion over whether it +negated the permissions granted in the license. In the spirit of +permissive licensing, and of not having licensing issues being an +obstacle to adoption, that text has been removed.Copyright (c) 2005-2023, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +Copyright (c) 2014, Al Sweigart +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.Copyright (c) 2017 Anthony Sottile + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.Copyright (c) 2015-2019 Jared Hobbs + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.Developed by ESN, an Electronic Arts Inc. studio. +Copyright (c) 2014, Electronic Arts Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +* Neither the name of ESN, Electronic Arts Inc. nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS INC. BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Portions of code from MODP_ASCII - Ascii transformations (upper/lower, etc) +https://github.com/client9/stringencoders + + Copyright 2005, 2006, 2007 + Nick Galbreath -- nickg [at] modp [dot] com + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + Neither the name of the modp.com nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + This is the standard "new" BSD license: + http://www.opensource.org/licenses/bsd-license.php + +https://github.com/client9/stringencoders/blob/cfd5c1507325ae497ea9bacdacba12c0ffd79d30/COPYING + +---- + +Numeric decoder derived from from TCL library +https://opensource.apple.com/source/tcl/tcl-14/tcl/license.terms + * Copyright (c) 1988-1993 The Regents of the University of California. + * Copyright (c) 1994 Sun Microsystems, Inc. + + This software is copyrighted by the Regents of the University of + California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState + Corporation and other parties. The following terms apply to all files + associated with the software unless explicitly disclaimed in + individual files. + + The authors hereby grant permission to use, copy, modify, distribute, + and license this software and its documentation for any purpose, provided + that existing copyright notices are retained in all copies and that this + notice is included verbatim in any distributions. No written agreement, + license, or royalty fee is required for any of the authorized uses. + Modifications to this software may be copyrighted by their authors + and need not follow the licensing terms described here, provided that + the new terms are clearly indicated on the first page of each file where + they apply. + + IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY + FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY + DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE + IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE + NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR + MODIFICATIONS. + + GOVERNMENT USE: If you are acquiring this software on behalf of the + U.S. government, the Government shall have only "Restricted Rights" + in the software and related documentation as defined in the Federal + Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you + are acquiring the software on behalf of the Department of Defense, the + software shall be classified as "Commercial Computer Software" and the + Government shall have only "Restricted Rights" as defined in Clause + 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the + authors grant the U.S. Government and others acting in its behalf + permission to use and distribute the software in accordance with the + terms specified in this license.Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +pandocfilters +BSD License +http://github.com/jgm/pandocfilters +Copyright (c) 2013, John MacFarlane +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of John Macfarlane nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +parse +MIT +https://github.com/r1chardj0n3s/parse +Copyright (c) 2012-2019 Richard Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +parso +MIT License +https://github.com/davidhalter/parso +All contributions towards parso are MIT licensed. + +Some Python files have been taken from the standard library and are therefore +PSF licensed. Modifications on these files are dual licensed (both MIT and +PSF). These files are: + +- parso/pgen2/* +- parso/tokenize.py +- parso/token.py +- test/test_pgen2.py + +Also some test files under test/normalizer_issue_files have been copied from +https://github.com/PyCQA/pycodestyle (Expat License == MIT License). + +------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) <2013-2017> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------------------- + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved" +are retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +pathspec +Mozilla Public License 2.0 (MPL 2.0) +UNKNOWN +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + + +peft +Apache Software License +https://github.com/huggingface/peft + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +pexpect +ISC License (ISCL) +https://pexpect.readthedocs.io/ +ISC LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2013-2014, Pexpect development team + Copyright (c) 2012, Noah Spurrier + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +pillow +MIT-CMU +https://python-pillow.github.io +The Python Imaging Library (PIL) is + + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright © 2010 by Jeffrey A. Clark and contributors + +Like PIL, Pillow is licensed under the open source MIT-CMU License: + +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: + +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +---- + +AOM + +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +---- + +BROTLI + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---- + +BZIP2 + + +-------------------------------------------------------------------------- + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2019 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@acm.org +bzip2/libbzip2 version 1.0.8 of 13 July 2019 + +-------------------------------------------------------------------------- + + +---- + +DAV1D + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---- + +FREETYPE2 + +The FreeType 2 font engine is copyrighted work and cannot be used +legally without a software license. In order to make this project +usable to a vast majority of developers, we distribute it under two +mutually exclusive open-source licenses. + +This means that *you* must choose *one* of the two licenses described +below, then obey all its terms and conditions when using FreeType 2 in +any of your projects or products. + + - The FreeType License, found in the file `docs/FTL.TXT`, which is + similar to the original BSD license *with* an advertising clause + that forces you to explicitly cite the FreeType project in your + product's documentation. All details are in the license file. + This license is suited to products which don't use the GNU General + Public License. + + Note that this license is compatible to the GNU General Public + License version 3, but not version 2. + + - The GNU General Public License version 2, found in + `docs/GPLv2.TXT` (any later version can be used also), for + programs which already use the GPL. Note that the FTL is + incompatible with GPLv2 due to its advertisement clause. + +The contributed BDF and PCF drivers come with a license similar to +that of the X Window System. It is compatible to the above two +licenses (see files `src/bdf/README` and `src/pcf/README`). The same +holds for the source code files `src/base/fthash.c` and +`include/freetype/internal/fthash.h`; they were part of the BDF driver +in earlier FreeType versions. + +The gzip module uses the zlib license (see `src/gzip/zlib.h`) which +too is compatible to the above two licenses. + +The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code +taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses +the 'Old MIT' license, compatible to the above two licenses. + +The MD5 checksum support (only used for debugging in development +builds) is in the public domain. + +-------------------------------------------------------------------------- + + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + + +--- end of FTL.TXT --- + +The following license details are part of `src/bdf/README`: + +``` +License +******* + +Copyright (C) 2001-2002 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*** Portions of the driver (that is, bdflib.c and bdf.h): + +Copyright 2000 Computing Research Labs, New Mexico State University +Copyright 2001-2002, 2011 Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +This driver is based on excellent Mark Leisher's bdf library. If you +find something good in this driver you should probably thank him, not +me. +``` + +The following license details are part of `src/pcf/README`: + +``` +License +******* + +Copyright (C) 2000 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +Keith Packard wrote the pcf driver found in XFree86. His work is at +the same time the specification and the sample implementation of the +PCF format. Undoubtedly, this driver is inspired from his work. +``` + + +---- + +HARFBUZZ + +HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. +For parts of HarfBuzz that are licensed under different licenses see individual +files names COPYING in subdirectories where applicable. + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +For full copyright notices consult the individual files in the package. + + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + + +---- + +LCMS2 + +Little CMS +Copyright (c) 1998-2020 Marti Maria Saguer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---- + +LIBAVIF + +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---- + +LIBJPEG + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +---- + +LIBLZMA + +XZ Utils Licensing +================== + + Different licenses apply to different files in this package. Here + is a rough summary of which licenses apply to which parts of this + package (but check the individual files to be sure!): + + - liblzma is in the public domain. + + - xz, xzdec, and lzmadec command line tools are in the public + domain unless GNU getopt_long had to be compiled and linked + in from the lib directory. The getopt_long code is under + GNU LGPLv2.1+. + + - The scripts to grep, diff, and view compressed files have been + adapted from gzip. These scripts and their documentation are + under GNU GPLv2+. + + - All the documentation in the doc directory and most of the + XZ Utils specific documentation files in other directories + are in the public domain. + + - Translated messages are in the public domain. + + - The build system contains public domain files, and files that + are under GNU GPLv2+ or GNU GPLv3+. None of these files end up + in the binaries being built. + + - Test files and test code in the tests directory, and debugging + utilities in the debug directory are in the public domain. + + - The extra directory may contain public domain files, and files + that are under various free software licenses. + + You can do whatever you want with the files that have been put into + the public domain. If you find public domain legally problematic, + take the previous sentence as a license grant. If you still find + the lack of copyright legally problematic, you have too many + lawyers. + + As usual, this software is provided "as is", without any warranty. + + If you copy significant amounts of public domain code from XZ Utils + into your project, acknowledging this somewhere in your software is + polite (especially if it is proprietary, non-free software), but + naturally it is not legally required. Here is an example of a good + notice to put into "about box" or into documentation: + + This software includes code from XZ Utils . + + The following license texts are included in the following files: + - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1 + - COPYING.GPLv2: GNU General Public License version 2 + - COPYING.GPLv3: GNU General Public License version 3 + + Note that the toolchain (compiler, linker etc.) may add some code + pieces that are copyrighted. Thus, it is possible that e.g. liblzma + binary wouldn't actually be in the public domain in its entirety + even though it contains no copyrighted code from the XZ Utils source + package. + + If you have questions, don't hesitate to ask the author(s) for more + information. + + +---- + +LIBPNG + +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE +========================================= + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2022 The PNG Reference Library Authors. + * Copyright (c) 2018-2022 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. + + +---- + +LIBTIFF + +Copyright (c) 1988-1997 Sam Leffler +Copyright (c) 1991-1997 Silicon Graphics, Inc. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that (i) the above copyright notices and this permission notice appear in +all copies of the software and related documentation, and (ii) the names of +Sam Leffler and Silicon Graphics may not be used in any advertising or +publicity relating to the software without the specific, prior written +permission of Sam Leffler and Silicon Graphics. + +THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + + +---- + +LIBWEBP + +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---- + +LIBYUV + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---- + +OPENJPEG + +* + * The copyright in this software is being made available under the 2-clauses + * BSD License, included below. This software may be subject to other third + * party and contributor rights, including patent rights, and no such rights + * are granted under this license. + * + * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2014, Professor Benoit Macq + * Copyright (c) 2003-2014, Antonin Descampe + * Copyright (c) 2003-2009, Francois-Olivier Devaux + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France + * Copyright (c) 2012, CS Systemes d'Information, France + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +---- + +RAQM + +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016 Khaled Hosny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---- + +XAU + +Copyright 1988, 1993, 1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + + +---- + +XCB + +Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett. +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the names of the authors +or their institutions shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this +Software without prior written authorization from the +authors. + + +---- + +XDMCP + +Copyright 1989, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +Author: Keith Packard, MIT X Consortium + + +---- + +ZLIB + + (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +If you use the zlib library in a product, we would appreciate *not* receiving +lengthy legal documents to sign. The sources are provided for free but without +warranty of any kind. The library has been entirely written by Jean-loup +Gailly and Mark Adler; it does not include third-party code. + +If you redistribute modified sources, we would appreciate that you include in +the file ChangeLog history information documenting your changes. Please read +the FAQ for more information on the distribution of modified source versions. + + +platformdirs +MIT +https://github.com/tox-dev/platformdirs +MIT License + +Copyright (c) 2010-202x The platformdirs developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pluggy +MIT License +UNKNOWN +The MIT License (MIT) + +Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +plyfile +GNU General Public License v3 or later (GPLv3+) +https://github.com/dranjan/python-plyfile + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + + +polars +MIT License +https://www.pola.rs/ +Copyright (c) 2025 Ritchie Vink +Some portions Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +polars-runtime-32 +MIT License +https://www.pola.rs/ +Copyright (c) 2025 Ritchie Vink +Some portions Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +polyscope +MIT License +https://github.com/nmwsharp/polyscope +MIT License + +Copyright (c) 2017-2020 Nicholas Sharp and the Polyscope contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +portalocker +BSD-3-Clause +https://github.com/wolph/portalocker/ +Copyright 2022 Rick van Hattem + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +proglog +MIT +https://github.com/Edinburgh-Genome-Foundry/proglog +MIT License + +Copyright (c) 2017 Edinburgh Genome Foundry, University of Edinburgh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +prometheus_client +Apache-2.0 AND BSD-2-Clause +https://github.com/prometheus/client_python + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +prompt_toolkit +BSD License +https://github.com/prompt-toolkit/python-prompt-toolkit +Copyright (c) 2014, Jonathan Slenders +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +propcache +Apache Software License +https://github.com/aio-libs/propcache + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +proto-plus +Apache Software License +https://github.com/googleapis/proto-plus-python + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +protobuf +3-Clause BSD License +https://developers.google.com/protocol-buffers/ +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + + +psutil +BSD-3-Clause +https://github.com/giampaolo/psutil +BSD 3-Clause License + +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the psutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +psycopg2-binary +GNU Library or Lesser General Public License (LGPL) +https://psycopg.org/ +psycopg2 and the LGPL +--------------------- + +psycopg2 is free software: you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +psycopg2 is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +License for more details. + +In addition, as a special exception, the copyright holders give +permission to link this program with the OpenSSL library (or with +modified versions of OpenSSL that use the same license as OpenSSL), +and distribute linked combinations including the two. + +You must obey the GNU Lesser General Public License in all respects for +all of the code used other than OpenSSL. If you modify file(s) with this +exception, you may extend this exception to your version of the file(s), +but you are not obligated to do so. If you do not wish to do so, delete +this exception statement from your version. If you delete this exception +statement from all source files in the program, then also delete it here. + +You should have received a copy of the GNU Lesser General Public License +along with psycopg2 (see the doc/ directory.) +If not, see . + + +Alternative licenses +-------------------- + +The following BSD-like license applies (at your option) to the files following +the pattern ``psycopg/adapter*.{h,c}`` and ``psycopg/microprotocol*.{h,c}``: + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product documentation + would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + +ptyprocess +ISC License (ISCL) +https://github.com/pexpect/ptyprocess +Ptyprocess is under the ISC license, as code derived from Pexpect. + http://opensource.org/licenses/ISC + +Copyright (c) 2013-2014, Pexpect development team +Copyright (c) 2012, Noah Spurrier + +PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE +WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE COPYRIGHT NOTICE +AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. THE SOFTWARE IS PROVIDED +"AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT +SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +pure_eval +MIT License +http://github.com/alexmojaki/pure_eval +MIT License + +Copyright (c) 2019 Alex Hall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +py-spy +MIT License +https://github.com/benfred/py-spy +The MIT License (MIT) + +Copyright (c) 2018-2019 Ben Frederickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +py3nvml +BSD License +https://github.com/fbcotter/py3nvml.git +COPYRIGHT +--------- +Copyright (c) 2011-2015, NVIDIA Corporation. All rights reserved. + +LICENSE +------- +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither the name of the NVIDIA Corporation nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + +pyarrow +Apache Software License +https://arrow.apache.org/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +src/arrow/util (some portions): Apache 2.0, and 3-clause BSD + +Some portions of this module are derived from code in the Chromium project, +copyright (c) Google inc and (c) The Chromium Authors and licensed under the +Apache 2.0 License or the under the 3-clause BSD license: + + Copyright (c) 2013 The Chromium Authors. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from Daniel Lemire's FrameOfReference project. + +https://github.com/lemire/FrameOfReference/blob/6ccaf9e97160f9a3b299e23a8ef739e711ef0c71/src/bpacking.cpp +https://github.com/lemire/FrameOfReference/blob/146948b6058a976bc7767262ad3a2ce201486b93/scripts/turbopacking64.py + +Copyright: 2013 Daniel Lemire +Home page: http://lemire.me/en/ +Project page: https://github.com/lemire/FrameOfReference +License: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from the TensorFlow project + +Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the NumPy project. + +https://github.com/numpy/numpy/blob/e1f191c46f2eebd6cb892a4bfe14d9dd43a06c4e/numpy/core/src/multiarray/multiarraymodule.c#L2910 + +https://github.com/numpy/numpy/blob/68fd82271b9ea5a9e50d4e761061dfcca851382a/numpy/core/src/multiarray/datetime.c + +Copyright (c) 2005-2017, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from the Boost project + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +This project includes code from the FlatBuffers project + +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the tslib project + +Copyright 2015 Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the jemalloc project + +https://github.com/jemalloc/jemalloc + +Copyright (C) 2002-2017 Jason Evans . +All rights reserved. +Copyright (C) 2007-2012 Mozilla Foundation. All rights reserved. +Copyright (C) 2009-2017 Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice(s), + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice(s), + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- + +This project includes code from the Go project, BSD 3-clause license + PATENTS +weak patent termination clause +(https://github.com/golang/go/blob/master/PATENTS). + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from the hs2client + +https://github.com/cloudera/hs2client + +Copyright 2016 Cloudera Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +The script ci/scripts/util_wait_for_it.sh has the following license + +Copyright (c) 2016 Giles Hall + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The script r/configure has the following license (MIT) + +Copyright (c) 2017, Jeroen Ooms and Jim Hester + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +cpp/src/arrow/util/logging.cc, cpp/src/arrow/util/logging.h and +cpp/src/arrow/util/logging-test.cc are adapted from +Ray Project (https://github.com/ray-project/ray) (Apache 2.0). + +Copyright (c) 2016 Ray Project (https://github.com/ray-project/ray) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- +The files cpp/src/arrow/vendored/datetime/date.h, cpp/src/arrow/vendored/datetime/tz.h, +cpp/src/arrow/vendored/datetime/tz_private.h, cpp/src/arrow/vendored/datetime/ios.h, +cpp/src/arrow/vendored/datetime/ios.mm, +cpp/src/arrow/vendored/datetime/tz.cpp are adapted from +Howard Hinnant's date library (https://github.com/HowardHinnant/date) +It is licensed under MIT license. + +The MIT License (MIT) +Copyright (c) 2015, 2016, 2017 Howard Hinnant +Copyright (c) 2016 Adrian Colomitchi +Copyright (c) 2017 Florian Dang +Copyright (c) 2017 Paul Thompson +Copyright (c) 2018 Tomasz Kamiński + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The file cpp/src/arrow/util/utf8.h includes code adapted from the page + https://bjoern.hoehrmann.de/utf-8/decoder/dfa/ +with the following license (MIT) + +Copyright (c) 2008-2009 Bjoern Hoehrmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/xxhash/ have the following license +(BSD 2-Clause License) + +xxHash Library +Copyright (c) 2012-2014, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You can contact the author at : +- xxHash homepage: http://www.xxhash.com +- xxHash source repository : https://github.com/Cyan4973/xxHash + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/double-conversion/ have the following license +(BSD 3-Clause License) + +Copyright 2006-2011, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/uriparser/ have the following license +(BSD 3-Clause License) + +uriparser - RFC 3986 URI parsing library + +Copyright (C) 2007, Weijia Song +Copyright (C) 2007, Sebastian Pipping +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + * Neither the name of the nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files under dev/tasks/conda-recipes have the following license + +BSD 3-clause license +Copyright (c) 2015-2018, conda-forge +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/utfcpp/ have the following license + +Copyright 2006-2018 Nemanja Trifunovic + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +This project includes code from Apache Kudu. + + * cpp/cmake_modules/CompilerInfo.cmake is based on Kudu's cmake_modules/CompilerInfo.cmake + +Copyright: 2016 The Apache Software Foundation. +Home page: https://kudu.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Apache Impala (incubating), formerly +Impala. The Impala code and rights were donated to the ASF as part of the +Incubator process after the initial code imports into Apache Parquet. + +Copyright: 2012 Cloudera, Inc. +Copyright: 2016 The Apache Software Foundation. +Home page: http://impala.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Apache Aurora. + +* dev/release/{release,changelog,release-candidate} are based on the scripts from + Apache Aurora + +Copyright: 2016 The Apache Software Foundation. +Home page: https://aurora.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Snappy. + +* cpp/cmake_modules/{SnappyCMakeLists.txt,SnappyConfig.h} are based on code + from Google's Snappy project. + +Copyright: 2009 Google Inc. All rights reserved. +Homepage: https://github.com/google/snappy +License: 3-clause BSD + +-------------------------------------------------------------------------------- + +This project includes code from the manylinux project. + +* python/manylinux1/scripts/{build_python.sh,python-tag-abi-tag.py, + requirements.txt} are based on code from the manylinux project. + +Copyright: 2016 manylinux +Homepage: https://github.com/pypa/manylinux +License: The MIT License (MIT) + +-------------------------------------------------------------------------------- + +This project includes code from the cymove project: + +* python/pyarrow/includes/common.pxd includes code from the cymove project + +The MIT License (MIT) +Copyright (c) 2019 Omer Ozarslan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +The projects includes code from the Ursabot project under the dev/archery +directory. + +License: BSD 2-Clause + +Copyright 2019 RStudio, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project include code from mingw-w64. + +* cpp/src/arrow/util/cpu-info.cc has a polyfill for mingw-w64 < 5 + +Copyright (c) 2009 - 2013 by the mingw-w64 project +Homepage: https://mingw-w64.org +License: Zope Public License (ZPL) Version 2.1. + +--------------------------------------------------------------------------------- + +This project include code from Google's Asylo project. + +* cpp/src/arrow/result.h is based on status_or.h + +Copyright (c) Copyright 2017 Asylo authors +Homepage: https://asylo.dev/ +License: Apache 2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Google's protobuf project + +* cpp/src/arrow/result.h ARROW_ASSIGN_OR_RAISE is based off ASSIGN_OR_RETURN +* cpp/src/arrow/util/bit_stream_utils.h contains code from wire_format_lite.h + +Copyright 2008 Google Inc. All rights reserved. +Homepage: https://developers.google.com/protocol-buffers/ +License: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +-------------------------------------------------------------------------------- + +3rdparty dependency LLVM is statically linked in certain binary distributions. +Additionally some sections of source code have been derived from sources in LLVM +and have been clearly labeled as such. LLVM has the following license: + +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +-------------------------------------------------------------------------------- + +3rdparty dependency gRPC is statically linked in certain binary +distributions, like the python wheels. gRPC has the following license: + +Copyright 2014 gRPC authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency Apache Thrift is statically linked in certain binary +distributions, like the python wheels. Apache Thrift has the following license: + +Apache Thrift +Copyright (C) 2006 - 2019, The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency Apache ORC is statically linked in certain binary +distributions, like the python wheels. Apache ORC has the following license: + +Apache ORC +Copyright 2013-2019 The Apache Software Foundation + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/). + +This product includes software developed by Hewlett-Packard: +(c) Copyright [2014-2015] Hewlett-Packard Development Company, L.P + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency zstd is statically linked in certain binary +distributions, like the python wheels. ZSTD has the following license: + +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency lz4 is statically linked in certain binary +distributions, like the python wheels. lz4 has the following license: + +LZ4 Library +Copyright (c) 2011-2016, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency Brotli is statically linked in certain binary +distributions, like the python wheels. Brotli has the following license: + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- + +3rdparty dependency rapidjson is statically linked in certain binary +distributions, like the python wheels. rapidjson and its dependencies have the +following licenses: + +Tencent is pleased to support the open source community by making RapidJSON +available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. +All rights reserved. + +If you have downloaded a copy of the RapidJSON binary from Tencent, please note +that the RapidJSON binary is licensed under the MIT License. +If you have downloaded a copy of the RapidJSON source code from Tencent, please +note that RapidJSON source code is licensed under the MIT License, except for +the third-party components listed below which are subject to different license +terms. Your integration of RapidJSON into your own projects may require +compliance with the MIT License, as well as the other licenses applicable to +the third-party components included within RapidJSON. To avoid the problematic +JSON license in your own projects, it's sufficient to exclude the +bin/jsonchecker/ directory, as it's the only code under the JSON license. +A copy of the MIT License is included in this file. + +Other dependencies and licenses: + + Open Source Software Licensed Under the BSD License: + -------------------------------------------------------------------- + + The msinttypes r29 + Copyright (c) 2006-2013 Alexander Chemeris + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + + Terms of the MIT License: + -------------------------------------------------------------------- + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +3rdparty dependency snappy is statically linked in certain binary +distributions, like the python wheels. snappy has the following license: + +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Google Inc. nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in testdata/ is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). + +-------------------------------------------------------------------------------- + +3rdparty dependency gflags is statically linked in certain binary +distributions, like the python wheels. gflags has the following license: + +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency glog is statically linked in certain binary +distributions, like the python wheels. glog has the following license: + +Copyright (c) 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +A function gettimeofday in utilities.cc is based on + +http://www.google.com/codesearch/p?hl=en#dR3YEbitojA/COPYING&q=GetSystemTimeAsFileTime%20license:bsd + +The license of this code is: + +Copyright (c) 2003-2008, Jouni Malinen and contributors +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name(s) of the above-listed copyright holder(s) nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency re2 is statically linked in certain binary +distributions, like the python wheels. re2 has the following license: + +Copyright (c) 2009 The RE2 Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency c-ares is statically linked in certain binary +distributions, like the python wheels. c-ares has the following license: + +# c-ares license + +Copyright (c) 2007 - 2018, Daniel Stenberg with many contributors, see AUTHORS +file. + +Copyright 1998 by the Massachusetts Institute of Technology. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of M.I.T. not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +M.I.T. makes no representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. + +-------------------------------------------------------------------------------- + +3rdparty dependency zlib is redistributed as a dynamically linked shared +library in certain binary distributions, like the python wheels. In the future +this will likely change to static linkage. zlib has the following license: + +zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +-------------------------------------------------------------------------------- + +3rdparty dependency openssl is redistributed as a dynamically linked shared +library in certain binary distributions, like the python wheels. openssl +preceding version 3 has the following license: + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +-------------------------------------------------------------------------------- + +This project includes code from the rtools-backports project. + +* ci/scripts/PKGBUILD and ci/scripts/r_windows_build.sh are based on code + from the rtools-backports project. + +Copyright: Copyright (c) 2013 - 2019, Алексей and Jeroen Ooms. +All rights reserved. +Homepage: https://github.com/r-windows/rtools-backports +License: 3-clause BSD + +-------------------------------------------------------------------------------- + +Some code from pandas has been adapted for the pyarrow codebase. pandas is +available under the 3-clause BSD license, which follows: + +pandas license +============== + +Copyright (c) 2011-2012, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2008-2011 AQR Capital Management, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Some bits from DyND, in particular aspects of the build system, have been +adapted from libdynd and dynd-python under the terms of the BSD 2-clause +license + +The BSD 2-Clause License + + Copyright (C) 2011-12, Dynamic NDArray Developers + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Dynamic NDArray Developers list: + + * Mark Wiebe + * Continuum Analytics + +-------------------------------------------------------------------------------- + +Some source code from Ibis (https://github.com/cloudera/ibis) has been adapted +for PyArrow. Ibis is released under the Apache License, Version 2.0. + +-------------------------------------------------------------------------------- + +dev/tasks/homebrew-formulae/apache-arrow.rb has the following license: + +BSD 2-Clause License + +Copyright (c) 2009-present, Homebrew contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------------------------------------------------------------- + +cpp/src/arrow/vendored/base64.cpp has the following license + +ZLIB License + +Copyright (C) 2004-2017 René Nyffenegger + +This source code is provided 'as-is', without any express or implied +warranty. In no event will the author be held liable for any damages arising +from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + +3. This notice may not be removed or altered from any source distribution. + +René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +-------------------------------------------------------------------------------- + +This project includes code from Folly. + + * cpp/src/arrow/vendored/ProducerConsumerQueue.h + +is based on Folly's + + * folly/Portability.h + * folly/lang/Align.h + * folly/ProducerConsumerQueue.h + +Copyright: Copyright (c) Facebook, Inc. and its affiliates. +Home page: https://github.com/facebook/folly +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +The file cpp/src/arrow/vendored/musl/strptime.c has the following license + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +The file cpp/cmake_modules/BuildUtils.cmake contains code from + +https://gist.github.com/cristianadam/ef920342939a89fae3e8a85ca9459b49 + +which is made available under the MIT license + +Copyright (c) 2019 Cristian Adam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/portable-snippets/ contain code from + +https://github.com/nemequ/portable-snippets + +and have the following copyright notice: + +Each source file contains a preamble explaining the license situation +for that file, which takes priority over this file. With the +exception of some code pulled in from other repositories (such as +µnit, an MIT-licensed project which is used for testing), the code is +public domain, released using the CC0 1.0 Universal dedication (*). + +(*) https://creativecommons.org/publicdomain/zero/1.0/legalcode + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/fast_float/ contain code from + +https://github.com/lemire/fast_float + +which is made available under the Apache License 2.0. + +-------------------------------------------------------------------------------- + +The file python/pyarrow/vendored/docscrape.py contains code from + +https://github.com/numpy/numpydoc/ + +which is made available under the BSD 2-clause license. + +-------------------------------------------------------------------------------- + +The file python/pyarrow/vendored/version.py contains code from + +https://github.com/pypa/packaging/ + +which is made available under both the Apache license v2.0 and the +BSD 2-clause license. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/pcg contain code from + +https://github.com/imneme/pcg-cpp + +and have the following copyright notice: + +Copyright 2014-2019 Melissa O'Neill , + and the PCG Project contributors. + +SPDX-License-Identifier: (Apache-2.0 OR MIT) + +Licensed under the Apache License, Version 2.0 (provided in +LICENSE-APACHE.txt and at http://www.apache.org/licenses/LICENSE-2.0) +or under the MIT license (provided in LICENSE-MIT.txt and at +http://opensource.org/licenses/MIT), at your option. This file may not +be copied, modified, or distributed except according to those terms. + +Distributed on an "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, either +express or implied. See your chosen license for details. + +-------------------------------------------------------------------------------- +r/R/dplyr-count-tally.R (some portions) + +Some portions of this file are derived from code from + +https://github.com/tidyverse/dplyr/ + +which is made available under the MIT license + +Copyright (c) 2013-2019 RStudio and others. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The file src/arrow/util/io_util.cc contains code from the CPython project +which is made available under the Python Software Foundation License Version 2. + +-------------------------------------------------------------------------------- + +3rdparty dependency opentelemetry-cpp is statically linked in certain binary +distributions. opentelemetry-cpp is made available under the Apache License 2.0. + +Copyright The OpenTelemetry Authors +SPDX-License-Identifier: Apache-2.0 + +-------------------------------------------------------------------------------- + +ci/conan/ is based on code from Conan Package and Dependency Manager. + +Copyright (c) 2019 Conan.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +3rdparty dependency UCX is redistributed as a dynamically linked shared +library in certain binary distributions. UCX has the following license: + +Copyright (c) 2014-2015 UT-Battelle, LLC. All rights reserved. +Copyright (C) 2014-2020 Mellanox Technologies Ltd. All rights reserved. +Copyright (C) 2014-2015 The University of Houston System. All rights reserved. +Copyright (C) 2015 The University of Tennessee and The University + of Tennessee Research Foundation. All rights reserved. +Copyright (C) 2016-2020 ARM Ltd. All rights reserved. +Copyright (c) 2016 Los Alamos National Security, LLC. All rights reserved. +Copyright (C) 2016-2020 Advanced Micro Devices, Inc. All rights reserved. +Copyright (C) 2019 UChicago Argonne, LLC. All rights reserved. +Copyright (c) 2018-2020 NVIDIA CORPORATION. All rights reserved. +Copyright (C) 2020 Huawei Technologies Co., Ltd. All rights reserved. +Copyright (C) 2016-2020 Stony Brook University. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The file dev/tasks/r/github.packages.yml contains code from + +https://github.com/ursa-labs/arrow-r-nightly + +which is made available under the Apache License 2.0. + +-------------------------------------------------------------------------------- +.github/actions/sync-nightlies/action.yml (some portions) + +Some portions of this file are derived from code from + +https://github.com/JoshPiper/rsync-docker + +which is made available under the MIT license + +Copyright (c) 2020 Joshua Piper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +.github/actions/sync-nightlies/action.yml (some portions) + +Some portions of this file are derived from code from + +https://github.com/burnett01/rsync-deployments + +which is made available under the MIT license + +Copyright (c) 2019-2022 Contention +Copyright (c) 2019-2022 Burnett01 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +java/vector/src/main/java/org/apache/arrow/vector/util/IntObjectHashMap.java +java/vector/src/main/java/org/apache/arrow/vector/util/IntObjectMap.java + +These files are derived from code from Netty, which is made available under the +Apache License 2.0. + +-------------------------------------------------------------------------------- +cpp/src/arrow/util/math_internal.cc (some portions) + +Some portions of this file are derived from + +https://github.com/ankane/dist-rust/ + +which is made available under the MIT license + +The MIT License (MIT) + +Copyright (c) 2021-2023 Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- +The files cpp/src/arrow/vendored/whereami/whereami.h, +cpp/src/arrow/vendored/whereami/whereami.cc are adapted from +Grégory Pakosz's whereami library (https://github.com/gpakosz/whereami) +It is dual licensed under both the WTFPLv2 and MIT licenses. + +The WTFPLv2 License + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + 1. Bla bla bla + 2. Montesqieu et camembert, vive la France, zut alors! + +The MIT License (MIT) +Copyright Gregory Pakosz + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +pyasn1 +BSD-2-Clause +https://github.com/pyasn1/pyasn1 +Copyright (c) 2005-2020, Ilya Etingof +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +pyasn1_modules +BSD License +https://github.com/pyasn1/pyasn1-modules +Copyright (c) 2005-2020, Ilya Etingof +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +pycocotools +FreeBSD +https://github.com/ppwwyyxx/cocoapi +UNKNOWN + +pycparser +BSD-3-Clause +https://github.com/eliben/pycparser +pycparser -- A C parser in Python + +Copyright (c) 2008-2022, Eli Bendersky +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +pycryptodomex +BSD License; Public Domain +https://www.pycryptodome.org +The source code in PyCryptodome is partially in the public domain +and partially released under the BSD 2-Clause license. + +In either case, there are minimal if no restrictions on the redistribution, +modification and usage of the software. + +Public domain +============= + +All code originating from PyCrypto is free and unencumbered software +released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +BSD license +=========== + +All direct contributions to PyCryptodome are released under the following +license. The copyright of each piece belongs to the respective author. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +pydantic +MIT +https://github.com/pydantic/pydantic +The MIT License (MIT) + +Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pydantic_core +MIT +https://github.com/pydantic/pydantic-core +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pydub +MIT License +http://pydub.com +Copyright (c) 2011 James Robert, http://jiaaro.com + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pygltflib +MIT License +https://gitlab.com/dodgyville/pygltflib +Copyright (c) 2018 Luke Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pyinstrument +BSD License +https://github.com/joerick/pyinstrument +Copyright (c) 2014-2020, Joe Rickerby and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +pynput +GNU Lesser General Public License v3 (LGPLv3) +https://github.com/moses-palmer/pynput + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007-2024 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + +pyparsing +MIT +https://github.com/pyparsing/pyparsing/ +Copyright (c) 2003-2025 Paul McGuire + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +pyproject_hooks +MIT License +https://github.com/pypa/pyproject-hooks +The MIT License (MIT) + +Copyright (c) 2017 Thomas Kluyver + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +pyrefly +UNKNOWN +https://pyrefly.org +UNKNOWN + +pyserial +BSD License +https://github.com/pyserial/pyserial +UNKNOWN + +pytest +MIT +https://docs.pytest.org/en/latest/ +The MIT License (MIT) + +Copyright (c) 2004 Holger Krekel and others + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pytest-cov +MIT +https://pytest-cov.readthedocs.io/en/latest/changelog.html +The MIT License + +Copyright (c) 2010 Meme Dough + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +pytest-custom-exit-code +MIT License +https://github.com/yashtodi94/pytest-custom_exit_code + +The MIT License (MIT) + +Copyright (c) 2019 Yash Todi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +pytest-datadir +MIT License +http://github.com/gabrielcnr/pytest-datadir +This is the MIT license: http://www.opensource.org/licenses/mit-license.php + +Copyright (c) 2015-2022 the pytest-datadir authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +pytest-env +MIT License +https://github.com/pytest-dev/pytest-env +MIT License + +Copyright (c) 2010-202x The pytest-env developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +pytest-instafail +BSD License +https://github.com/pytest-dev/pytest-instafail +Copyright (c) 2013-2016, Janne Vanhala + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The names of the contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +pytest-regressions +MIT License +https://github.com/ESSS/pytest-regressions + +The MIT License (MIT) + +Copyright (c) 2018 ESSS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +pytest-xdist +MIT +https://github.com/pytest-dev/pytest-xdist +MIT License + +Copyright (c) 2010 Holger Krekel and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +python-dateutil +Apache Software License; BSD License +https://github.com/dateutil/dateutil +Copyright 2017- Paul Ganssle +Copyright 2017- dateutil contributors (see AUTHORS file) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The above license applies to all contributions after 2017-12-01, as well as +all contributions that have been re-licensed (see AUTHORS file for the list of +contributors who have re-licensed their code). +-------------------------------------------------------------------------------- +dateutil - Extensions to the standard Python datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer +Copyright (c) 2012-2014 - Tomi Pieviläinen +Copyright (c) 2014-2016 - Yaron de Leeuw +Copyright (c) 2015- - Paul Ganssle +Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The above BSD License Applies to all code, even that also covered by Apache 2.0. + +python-discovery +MIT License +https://github.com/tox-dev/python-discovery +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +python-dotenv +BSD-3-Clause +https://github.com/theskumar/python-dotenv +Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of django-dotenv nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +python-json-logger +BSD License +https://nhairs.github.io/python-json-logger +Copyright (c) 2011, Zakaria Zajac and the python-json-logger Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +python-memcached +Python Software Foundation License +https://github.com/linsomniac/python-memcached +UNKNOWN + +python-multipart +Apache-2.0 +https://github.com/Kludex/python-multipart +Copyright 2012, Andrew Dunham + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + +python-xlib +GNU Lesser General Public License v2 or later (LGPLv2+) +https://github.com/python-xlib/python-xlib + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + {signature of Ty Coon}, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + +pytorch-lightning +Apache Software License +https://github.com/Lightning-AI/lightning + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 William Falcon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +pytorch-ranger +MIT License +https://github.com/mpariente/Ranger-Deep-Learning-Optimizer + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +pytz +MIT License +http://pythonhosted.org/pytz +Copyright (c) 2003-2019 Stuart Bishop + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +pyzmq +BSD License +https://pyzmq.readthedocs.org +BSD 3-Clause License + +Copyright (c) 2009-2012, Brian Granger, Min Ragan-Kelley + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +qwen-vl-utils +Apache Software License +https://github.com/QwenLM/Qwen2-VL/tree/main/qwen-vl-utils +UNKNOWN + +ray +Apache 2.0 +https://github.com/ray-project/ray +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +referencing +MIT +https://github.com/python-jsonschema/referencing +Copyright (c) 2022 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +regex +Apache-2.0 AND CNRI-Python +https://github.com/mrabarnett/mrab-regex +This work was derived from the 're' module of CPython 2.6 and CPython 3.1, +copyright (c) 1998-2001 by Secret Labs AB and licensed under CNRI's Python 1.6 +license. + +All additions and alterations are licensed under the Apache 2.0 License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Matthew Barnett + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +requests +Apache Software License +https://requests.readthedocs.io + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + +retinaface-py +MIT License +https://github.com/andresprados/Pytorch_Retinaface +MIT License + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +rfc3339-validator +MIT License +https://github.com/naimetti/rfc3339-validator +MIT License + +Copyright (c) 2019, Nicolas Aimetti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +rfc3986-validator +MIT License +https://github.com/naimetti/rfc3986-validator +MIT License + +Copyright (c) 2019, Nicolas Aimetti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +rfc3987-syntax +MIT +https://github.com/willynilly/rfc3987-syntax +MIT License + +Copyright (c) 2025 Will Riley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +rich +MIT License +https://github.com/Textualize/rich +Copyright (c) 2020 Will McGugan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +robotmq +MIT License +https://github.com/yihuai-gao/robot-message-queue +MIT License + +Copyright (c) 2024 Yihuai Gao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +rpds-py +MIT +https://github.com/crate-py/rpds +Copyright (c) 2023 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +rsa +Apache Software License +https://stuvel.eu/rsa +Copyright 2011 Sybren A. Stüvel + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +ruff +MIT License +https://docs.astral.sh/ruff +MIT License + +Copyright (c) 2022 Charles Marsh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +end of terms and conditions + +The externally maintained libraries from which parts of the Software is derived +are: + +- flake8-comprehensions, licensed as follows: + """ + MIT License + + Copyright (c) 2017 Adam Johnson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-no-pep420, licensed as follows: + """ + MIT License + + Copyright (c) 2020 Adam Johnson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-tidy-imports, licensed as follows: + """ + MIT License + + Copyright (c) 2017 Adam Johnson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-return, licensed as follows: + """ + MIT License + + Copyright (c) 2019 Afonasev Evgeniy + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-2020, licensed as follows: + """ + Copyright (c) 2019 Anthony Sottile + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- pyupgrade, licensed as follows: + """ + Copyright (c) 2017 Anthony Sottile + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- flake8-blind-except, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2014 Elijah Andrews + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ + +- flake8-gettext, licensed as follows: + """ + BSD Zero Clause License + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + """ + +- flake8-implicit-str-concat, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2019 Dylan Turner + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- flake8-debugger, licensed as follows: + """ + MIT License + + Copyright (c) 2016 Joseph Kahn + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-pyi, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2016 Łukasz Langa + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-print, licensed as follows: + """ + MIT License + + Copyright (c) 2016 Joseph Kahn + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-import-conventions, licensed as follows: + """ + MIT License + + Copyright (c) 2021 João Palmeiro + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-simplify, licensed as follows: + """ + MIT License + + Copyright (c) 2020 Martin Thoma + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-slots, licensed as follows: + """ + Copyright (c) 2021 Dominic Davis-Foster + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + OR OTHER DEALINGS IN THE SOFTWARE. + """ + +- flake8-todos, licensed as follows: + """ + Copyright (c) 2019 EclecticIQ. All rights reserved. + Copyright (c) 2020 Gram . All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + """ + +- flake8-unused-arguments, licensed as follows: + """ + MIT License + + Copyright (c) 2019 Nathan Hoad + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- pygrep-hooks, licensed as follows: + """ + Copyright (c) 2018 Anthony Sottile + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- autoflake, licensed as follows: + """ + Copyright (C) 2012-2018 Steven Myint + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- autotyping, licensed as follows: + """ + MIT License + + Copyright (c) 2023 Jelle Zijlstra + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Flake8, licensed as follows: + """ + == Flake8 License (MIT) == + + Copyright (C) 2011-2013 Tarek Ziade + Copyright (C) 2012-2016 Ian Cordasco + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-bugbear, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2016 Łukasz Langa + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-commas, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2017 Thomas Grainger. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + + Portions of this flake8-commas Software may utilize the following + copyrighted material, the use of which is hereby acknowledged. + + Original flake8-commas: https://github.com/trevorcreech/flake8-commas/commit/e8563b71b1d5442e102c8734c11cb5202284293d + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- flynt, licensed as follows: + """ + MIT License + + Copyright (c) 2019-2022 Ilya Kamenshchikov + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- isort, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2013 Timothy Edmund Crosley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- pep8-naming, licensed as follows: + """ + Copyright © 2013 Florent Xicluna + + Licensed under the terms of the Expat License + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- pycodestyle, licensed as follows: + """ + Copyright © 2006-2009 Johann C. Rocholl + Copyright © 2009-2014 Florent Xicluna + Copyright © 2014-2020 Ian Lee + + Licensed under the terms of the Expat License + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- pydocstyle, licensed as follows: + """ + Copyright (c) 2012 GreenSteam, + + Copyright (c) 2014-2020 Amir Rachum, + + Copyright (c) 2020 Sambhav Kothari, + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Pyflakes, licensed as follows: + """ + Copyright 2005-2011 Divmod, Inc. + Copyright 2013-2014 Florent Xicluna + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ + +- flake8-use-pathlib, licensed as follows: + """ + MIT License + + Copyright (c) 2021 Rodolphe Pelloux-Prayer + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- RustPython, licensed as follows: + """ + MIT License + + Copyright (c) 2020 RustPython Team + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-annotations, licensed as follows: + """ + MIT License + + Copyright (c) 2019 - Present S. Co1 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-async, licensed as follows: + """ + MIT License + + Copyright (c) 2022 Cooper Lees + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-type-checking, licensed as follows: + """ + Copyright (c) 2021, Sondre Lillebø Gundersen + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of pytest-{{ cookiecutter.plugin_name }} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + """ + +- flake8-bandit, licensed as follows: + """ + Copyright (c) 2017 Tyler Wince + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- flake8-eradicate, licensed as follows: + """ + MIT License + + Copyright (c) 2018 Nikita Sobolev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-quotes, licensed as follows: + """ + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + +- flake8-logging-format, licensed as follows: + """ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + """ + +- flake8-raise, licensed as follows: + """ + MIT License + + Copyright (c) 2020 Jon Dufresne + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-self, licensed as follows: + """ + MIT License + + Copyright (c) 2023 Korijn van Golen + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-django, licensed under the GPL license. + +- perflint, licensed as follows: + """ + MIT License + + Copyright (c) 2022 Anthony Shaw + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-logging, licensed as follows: + """ + MIT License + + Copyright (c) 2023 Adam Johnson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- flake8-trio, licensed as follows: + """ + MIT License + + Copyright (c) 2022 Zac Hatfield-Dodds + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Pyright, licensed as follows: + """ + MIT License + + Pyright - A static type checker for the Python language + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + """ + +- rust-analyzer/text-size, licensed under the MIT license: + """ + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + """ + +- rome/tools, licensed under the MIT license: + """ + MIT License + + Copyright (c) Rome Tools, Inc. and its affiliates. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- pydoclint, licensed as follows: + """ + MIT License + + Copyright (c) 2023 jsh9 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + + +s3fs +BSD License +http://github.com/fsspec/s3fs/ +Copyright (c) 2016, Continuum Analytics, Inc. and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of Continuum Analytics nor the names of any contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + + +s3transfer +Apache Software License +https://github.com/boto/s3transfer + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +safehttpx +MIT License +https://github.com/gradio-app/safehttpx + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +safetensors +Apache Software License +https://github.com/huggingface/safetensors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +scikit-image +BSD License +https://scikit-image.org +Files: * +Copyright: 2009-2022 the scikit-image team +License: BSD-3-Clause + +Files: doc/source/themes/scikit-image/layout.html +Copyright: 2007-2010 the Sphinx team +License: BSD-3-Clause + +Files: skimage/feature/_canny.py + skimage/filters/edges.py + skimage/filters/_rank_order.py + skimage/morphology/_skeletonize.py + skimage/morphology/tests/test_watershed.py + skimage/morphology/watershed.py + skimage/segmentation/heap_general.pxi + skimage/segmentation/heap_watershed.pxi + skimage/segmentation/_watershed.py + skimage/segmentation/_watershed_cy.pyx +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2003-2005 Peter J. Verveer +License: BSD-3-Clause + +Files: skimage/filters/thresholding.py + skimage/graph/_mcp.pyx + skimage/graph/heap.pyx +Copyright: 2009-2015 Board of Regents of the University of + Wisconsin-Madison, Broad Institute of MIT and Harvard, + and Max Planck Institute of Molecular Cell Biology and + Genetics + 2009 Zachary Pincus + 2009 Almar Klein +License: BSD-2-Clause + +File: skimage/morphology/grayreconstruct.py + skimage/morphology/tests/test_reconstruction.py +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky +License: BSD-3-Clause + +File: skimage/morphology/_grayreconstruct.pyx +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2022 Gregory Lee (added a 64-bit integer variant for large images) +License: BSD-3-Clause + +File: skimage/segmentation/_expand_labels.py +Copyright: 2020 Broad Institute + 2020 CellProfiler team +License: BSD-3-Clause + +File: skimage/exposure/_adapthist.py +Copyright: 1994 Karel Zuiderveld +License: BSD-3-Clause + +Function: skimage/morphology/_skeletonize_various_cy.pyx:_skeletonize_loop +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky +License: BSD-3-Clause + +Function: skimage/_shared/version_requirements.py:_check_version +Copyright: 2013 The IPython Development Team +License: BSD-3-Clause + +Function: skimage/_shared/version_requirements.py:is_installed +Copyright: 2009-2011 Pierre Raybaut +License: MIT + +File: skimage/feature/_fisher_vector.py +Copyright: 2014 2014 Dan Oneata +License: MIT + +File: skimage/_vendored/numpy_lookfor.py +Copyright: 2005-2023, NumPy Developers +License: BSD-3-Clause + +File: skimage/transform/_thin_plate_splines.py +Copyright: 2007 Zachary Pincus +License: BSD-3-Clause + +License: BSD-2-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. +. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +scipy +BSD License +https://scipy.org/ +Copyright (c) 2001-2002 Enthought, Inc. 2003, SciPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +This binary distribution of SciPy can also bundle the following software +(depending on the build): + + +Name: OpenBLAS +Files: scipy.libs/libscipy_openblas*.so +Description: bundled as a dynamically linked library +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause + Copyright (c) 2011-2014, The OpenBLAS Project + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: LAPACK +Files: scipy.libs/libscipy_openblas*.so +Description: bundled in OpenBLAS +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause-Open-MPI + Copyright (c) 1992-2013 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. + Copyright (c) 2000-2013 The University of California Berkeley. All + rights reserved. + Copyright (c) 2006-2013 The University of Colorado Denver. All rights + reserved. + + $COPYRIGHT$ + + Additional copyrights may follow + + $HEADER$ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + + - Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + The copyright holders provide no reassurances that the source code + provided does not infringe any patent, copyright, or any other + intellectual property rights of third parties. The copyright holders + disclaim any liability to any recipient for claims brought against + recipient by any third party for infringement of that parties + intellectual property rights. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: GCC runtime library +Files: scipy.libs/libgfortran*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran +License: GPL-3.0-or-later WITH GCC-exception-3.1 + Copyright (C) 2002-2017 Free Software Foundation, Inc. + + Libgfortran is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + Libgfortran is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Under Section 7 of GPL version 3, you are granted additional + permissions described in the GCC Runtime Library Exception, version + 3.1, as published by the Free Software Foundation. + + You should have received a copy of the GNU General Public License and + a copy of the GCC Runtime Library Exception along with this program; + see the files COPYING3 and COPYING.RUNTIME respectively. If not, see + . + +---- + +Full text of license texts referred to above follows (that they are +listed below does not necessarily imply the conditions apply to the +present binary release): + +---- + +GCC RUNTIME LIBRARY EXCEPTION + +Version 3.1, 31 March 2009 + +Copyright (C) 2009 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This GCC Runtime Library Exception ("Exception") is an additional +permission under section 7 of the GNU General Public License, version +3 ("GPLv3"). It applies to a given file (the "Runtime Library") that +bears a notice placed by the copyright holder of the file stating that +the file is governed by GPLv3 along with this Exception. + +When you use GCC to compile a program, GCC may combine portions of +certain GCC header files and runtime libraries with the compiled +program. The purpose of this Exception is to allow compilation of +non-GPL (including proprietary) programs to use, in this way, the +header files and runtime libraries covered by this Exception. + +0. Definitions. + +A file is an "Independent Module" if it either requires the Runtime +Library for execution after a Compilation Process, or makes use of an +interface provided by the Runtime Library, but is not otherwise based +on the Runtime Library. + +"GCC" means a version of the GNU Compiler Collection, with or without +modifications, governed by version 3 (or a specified later version) of +the GNU General Public License (GPL) with the option of using any +subsequent versions published by the FSF. + +"GPL-compatible Software" is software whose conditions of propagation, +modification and use would permit combination with GCC in accord with +the license of GCC. + +"Target Code" refers to output from any compiler for a real or virtual +target processor architecture, in executable form or suitable for +input to an assembler, loader, linker and/or execution +phase. Notwithstanding that, Target Code does not include data in any +format that is used as a compiler intermediate representation, or used +for producing a compiler intermediate representation. + +The "Compilation Process" transforms code entirely represented in +non-intermediate languages designed for human-written code, and/or in +Java Virtual Machine byte code, into Target Code. Thus, for example, +use of source code generators and preprocessors need not be considered +part of the Compilation Process, since the Compilation Process can be +understood as starting with the output of the generators or +preprocessors. + +A Compilation Process is "Eligible" if it is done using GCC, alone or +with other GPL-compatible software, or if it is done without using any +work based on GCC. For example, using non-GPL-compatible Software to +optimize any GCC intermediate representations would not qualify as an +Eligible Compilation Process. + +1. Grant of Additional Permission. + +You have permission to propagate a work of Target Code formed by +combining the Runtime Library with Independent Modules, even if such +propagation would otherwise violate the terms of GPLv3, provided that +all Target Code was generated by Eligible Compilation Processes. You +may then convey such a combination under terms of your choice, +consistent with the licensing of the Independent Modules. + +2. No Weakening of GCC Copyleft. + +The availability of this Exception does not imply any general +presumption that third-party software is unaffected by the copyleft +requirements of the license of GCC. + +---- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + + +Name: libquadmath +Files: scipy.libs/libquadmath*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath +License: LGPL-2.1-or-later + + GCC Quad-Precision Math Library + Copyright (C) 2010-2019 Free Software Foundation, Inc. + Written by Francois-Xavier Coudert + + This file is part of the libquadmath library. + Libquadmath is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + Libquadmath is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + + +semantic-version +BSD License +https://github.com/rbarrois/python-semanticversion +Copyright (c) The python-semanticversion project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +semver +BSD License +https://python-semver.readthedocs.io/en/latest/changelog.html +Copyright (c) 2013, Konstantine Rybnikov +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the python-semver org nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +sentencepiece +UNKNOWN +https://github.com/google/sentencepiece +UNKNOWN + +sentry-sdk +BSD License +https://github.com/getsentry/sentry-python +MIT License + +Copyright (c) 2018 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +shellingham +ISC License (ISCL) +https://github.com/sarugaku/shellingham +Copyright (c) 2018, Tzu-ping Chung + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +six +MIT License +https://github.com/benjaminp/six +Copyright (c) 2010-2024 Benjamin Peterson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +slangtorch +MIT License +https://github.com/shader-slang/slang +SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +LLVM Exceptions to the Apache 2.0 License + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + +smart_open +MIT License +https://github.com/piskvorky/smart_open +The MIT License (MIT) + +Copyright (c) 2015 Radim Řehůřek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +smmap +BSD License +https://github.com/gitpython-developers/smmap +Copyright (C) 2010, 2011 Sebastian Thiel and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the async project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +sniffio +Apache Software License; MIT License +https://github.com/python-trio/sniffio +This software is made available under the terms of *either* of the +licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are +made under the terms of *both* these licenses. + + +soundfile +BSD License +https://github.com/bastibe/python-soundfile + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + + +soupsieve +MIT +https://github.com/facelessuser/soupsieve +MIT License + +Copyright (c) 2018 - 2026 Isaac Muse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +stack-data +MIT License +http://github.com/alexmojaki/stack_data +MIT License + +Copyright (c) 2019 Alex Hall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +starlette +BSD-3-Clause +https://github.com/Kludex/starlette +Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +sympy +BSD License +https://sympy.org +Copyright (c) 2006-2023 SymPy Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Patches that were taken from the Diofant project (https://github.com/diofant/diofant) +are licensed as: + +Copyright (c) 2006-2018 SymPy Development Team, + 2013-2023 Sergey B Kirpichev + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of Diofant or SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Submodules taken from the multipledispatch project (https://github.com/mrocklin/multipledispatch) +are licensed as: + +Copyright (c) 2014 Matthew Rocklin + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of multipledispatch nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/autolev/tests/pydy-example-repo +are directly copied from PyDy project and are licensed as: + +Copyright (c) 2009-2023, PyDy Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL PYDY AUTHORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/latex +are directly copied from latex2sympy project and are licensed as: + +Copyright 2016, latex2sympy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +tabulate +MIT +https://github.com/astanin/python-tabulate +Copyright (c) 2011-2020 Sergey Astanin and contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +tensorboard +Apache Software License +https://github.com/tensorflow/tensorboard +# TensorBoard License + +TensorBoard is licensed Apache 2.0 and distributed with +vendored content licensed Apache 2.0, MIT, and BSD-3. + +## Table of Contents + +- tensorboard/pip_package/LICENSE.tensorflow +- external/npm/node_modules/d3/LICENSE +- external/com_google_fonts_roboto/LICENSE +- external/org_mozilla_bleach/LICENSE +- external/org_pythonhosted_webencodings/LICENSE +- third_party/bh_tsne.LICENSE + +## Licenses + + + +### tensorboard/pip_package/LICENSE.tensorflow + +Copyright 2017 The TensorFlow Authors. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017, The TensorFlow Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +### external/npm/node_modules/d3/LICENSE + +Copyright 2010-2017 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +### external/com_google_fonts_roboto/LICENSE + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +### external/org_mozilla_bleach/LICENSE + +Copyright (c) 2014-2017, Mozilla Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + +### external/org_pythonhosted_webencodings/LICENSE + +Copyright (c) 2012 by Simon Sapin. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +### third_party/bh_tsne.LICENSE + +The MIT License (MIT) + +Copyright (c) 2015 Andrej Karpathy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +tensorboard-data-server +Apache Software License +https://github.com/tensorflow/tensorboard/tree/master/tensorboard/data/server +UNKNOWN + +tensorstore +Apache-2.0 +https://github.com/google/tensorstore +Files: **/* + +Copyright 2018 The TensorStore Authors. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017, The TensorStore Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +Files: internal/utf8.cc + +Copyright (c) 2008-2009 Bjoern Hoehrmann + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +Files: third_party/snappy/bundled.BUILD.bazel + +Copyright 2011 Google Inc. All Rights Reserved. +Author: sesse@google.com (Steinar H. Gunderson) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Files: tools/cmake/FindAVIF.cmake + +Copyright (C) 2021 Igalia S.L. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + + + +termcolor +MIT +https://github.com/termcolor/termcolor +Copyright (c) 2008-2011 Volvox Development Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +terminado +BSD License +https://github.com/jupyter/terminado +BSD 2-Clause License + +- Copyright (c) 2014-, Jupyter development team +- Copyright (c) 2014, Ramalingam Saravanan + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +tifffile +BSD-3-Clause +https://www.cgohlke.com +BSD-3-Clause license + +Copyright (c) 2008-2026, Christoph Gohlke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +tiktoken +MIT License + +Copyright (c) 2022 OpenAI, Shantanu Jain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +https://github.com/openai/tiktoken +MIT License + +Copyright (c) 2022 OpenAI, Shantanu Jain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +timm +Apache Software License +https://github.com/huggingface/pytorch-image-models + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Ross Wightman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +tinycss2 +BSD License +https://www.courtbouillon.org/tinycss2 +BSD 3-Clause License + +Copyright (c) 2013-2020, Simon Sapin and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +tokenizers +Apache Software License +https://github.com/huggingface/tokenizers +UNKNOWN + +tomli +MIT +https://github.com/hukkin/tomli +MIT License + +Copyright (c) 2021 Taneli Hukkinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +tomli_w +MIT License +https://github.com/hukkin/tomli-w +MIT License + +Copyright (c) 2021 Taneli Hukkinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +tomlkit +MIT License +https://github.com/sdispater/tomlkit +Copyright (c) 2018 Sébastien Eustace + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +torch +BSD-3-Clause +https://pytorch.org +From PyTorch: + +Copyright (c) 2016- Facebook, Inc (Adam Paszke) +Copyright (c) 2014- Facebook, Inc (Soumith Chintala) +Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) +Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) +Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) +Copyright (c) 2011-2013 NYU (Clement Farabet) +Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) +Copyright (c) 2006 Idiap Research Institute (Samy Bengio) +Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + +From Caffe2: + +Copyright (c) 2016-present, Facebook Inc. All rights reserved. + +All contributions by Facebook: +Copyright (c) 2016 Facebook Inc. + +All contributions by Google: +Copyright (c) 2015 Google Inc. +All rights reserved. + +All contributions by Yangqing Jia: +Copyright (c) 2015 Yangqing Jia +All rights reserved. + +All contributions by Kakao Brain: +Copyright 2019-2020 Kakao Brain + +All contributions by Cruise LLC: +Copyright (c) 2022 Cruise LLC. +All rights reserved. + +All contributions by Tri Dao: +Copyright (c) 2024 Tri Dao. +All rights reserved. + +All contributions by Arm: +Copyright (c) 2021, 2023-2025 Arm Limited and/or its affiliates + +All contributions from Caffe: +Copyright(c) 2013, 2014, 2015, the respective contributors +All rights reserved. + +All other contributions: +Copyright(c) 2015, 2016 the respective contributors +All rights reserved. + +Caffe2 uses a copyright model similar to Caffe: each contributor holds +copyright over their contributions to Caffe2. The project versioning records +all such contribution and copyright details. If a contributor wants to further +mark their specific copyright on a particular contribution, they should +indicate their copyright solely in the commit message of the change when it is +committed. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America + and IDIAP Research Institute nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +The PyTorch repository and source distributions bundle several libraries that are +compatibly licensed. We list these here. + +Name: DCGM +License: Apache-2.0 +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/LICENSE + +Name: FP16 +License: MIT +Files: /pytorch/third_party/FP16 + For details, see the files concatenated below: /pytorch/third_party/FP16/LICENSE + +Name: FXdiv +License: MIT +Files: /pytorch/third_party/FXdiv + For details, see the files concatenated below: /pytorch/third_party/FXdiv/LICENSE + +Name: NNPACK +License: BSD-2-Clause +Files: /pytorch/third_party/NNPACK + For details, see the files concatenated below: /pytorch/third_party/NNPACK/LICENSE + +Name: NVTX +License: Apache-2.0 with exception +Files: /pytorch/third_party/NVTX + For details, see the files concatenated below: /pytorch/third_party/NVTX/LICENSE.txt + +Name: VulkanMemoryAllocator +License: MIT +Files: /pytorch/third_party/VulkanMemoryAllocator + For details, see the files concatenated below: /pytorch/third_party/VulkanMemoryAllocator/LICENSE.txt + +Name: XNNPACK +License: BSD-3-Clause +Files: /pytorch/third_party/XNNPACK + For details, see the files concatenated below: /pytorch/third_party/XNNPACK/LICENSE + +Name: aiter +License: MIT +Files: /pytorch/third_party/aiter + For details, see the files concatenated below: /pytorch/third_party/aiter/LICENSE + +Name: benchmark +License: Apache-2.0 +Files: /pytorch/third_party/benchmark, + /pytorch/third_party/opentelemetry-cpp/third_party/benchmark, + /pytorch/third_party/protobuf/third_party/benchmark + For details, see the files concatenated below: /pytorch/third_party/benchmark/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/benchmark/LICENSE, + /pytorch/third_party/protobuf/third_party/benchmark/LICENSE + +Name: boost-vcpkg-helpers +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers/LICENSE.txt + +Name: cJSON +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE + +Name: catch2 +License: BSL-1.0 +Files: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2 + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2/LICENSE.txt + +Name: clog +License: BSD-2-Clause +Files: /pytorch/third_party/cpuinfo/deps/clog, + /pytorch/third_party/fbgemm/external/cpuinfo/deps/clog + For details, see the files concatenated below: /pytorch/third_party/cpuinfo/deps/clog/LICENSE, + /pytorch/third_party/fbgemm/external/cpuinfo/deps/clog/LICENSE + +Name: colorama +License: BSD-3-Clause +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama/LICENSE.txt + +Name: composable_kernel +License: MIT +Files: /pytorch/third_party/aiter/3rdparty/composable_kernel, + /pytorch/third_party/composable_kernel, + /pytorch/third_party/fbgemm/external/composable_kernel, + /pytorch/third_party/flash-attention/csrc/composable_kernel + For details, see the files concatenated below: /pytorch/third_party/aiter/3rdparty/composable_kernel/LICENSE, + /pytorch/third_party/composable_kernel/LICENSE, + /pytorch/third_party/fbgemm/external/composable_kernel/LICENSE, + /pytorch/third_party/flash-attention/csrc/composable_kernel/LICENSE + +Name: cpp-httplib +License: MIT +Files: /pytorch/third_party/cpp-httplib + For details, see the files concatenated below: /pytorch/third_party/cpp-httplib/LICENSE + +Name: cpplint +License: BSD-3-Clause +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint/LICENSE + +Name: cpr +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/LICENSE + +Name: cpuinfo +License: BSD-2-Clause +Files: /pytorch/third_party/cpuinfo, + /pytorch/third_party/fbgemm/external/cpuinfo + For details, see the files concatenated below: /pytorch/third_party/cpuinfo/LICENSE, + /pytorch/third_party/fbgemm/external/cpuinfo/LICENSE + +Name: cudnn_frontend +License: MIT +Files: /pytorch/third_party/cudnn_frontend + For details, see the files concatenated below: /pytorch/third_party/cudnn_frontend/LICENSE.txt + +Name: cutlass +License: BSD-3-Clause +Files: /pytorch/third_party/cutlass, + /pytorch/third_party/fbgemm/external/cutlass, + /pytorch/third_party/flash-attention/csrc/cutlass + For details, see the files concatenated below: /pytorch/third_party/cutlass/LICENSE.txt, + /pytorch/third_party/fbgemm/external/cutlass/LICENSE.txt, + /pytorch/third_party/flash-attention/csrc/cutlass/LICENSE.txt + +Name: dart +License: Apache-2.0 +Files: /pytorch/third_party/flatbuffers/dart + For details, see the files concatenated below: /pytorch/third_party/flatbuffers/dart/LICENSE + +Name: docs +License: Apache-2.0 with exception +Files: /pytorch/third_party/NVTX/docs + For details, see the files concatenated below: /pytorch/third_party/NVTX/docs/LICENSE.txt + +Name: doctest +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest/LICENSE.txt + +Name: duktape-1.5.2 +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2 + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt + +Name: duktape-1.8.0 +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0 + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt + +Name: dynolog +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/LICENSE + +Name: etw +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw/LICENSE + +Name: expected +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected/LICENSE + +Name: fbgemm +License: BSD-3-Clause +Files: /pytorch/third_party/fbgemm + For details, see the files concatenated below: /pytorch/third_party/fbgemm/LICENSE + +Name: ffnvcodec +License: MIT with exception +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec/LICENSE.txt + +Name: flash-attention +License: BSD-3-Clause +Files: /pytorch/third_party/flash-attention + For details, see the files concatenated below: /pytorch/third_party/flash-attention/LICENSE + +Name: flatbuffers +License: Apache-2.0 +Files: /pytorch/third_party/flatbuffers + For details, see the files concatenated below: /pytorch/third_party/flatbuffers/LICENSE + +Name: fmt +License: MIT with exception +Files: /pytorch/third_party/fmt, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/fmt, + /pytorch/third_party/kineto/libkineto/third_party/fmt + For details, see the files concatenated below: /pytorch/third_party/fmt/LICENSE, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/fmt/LICENSE.rst, + /pytorch/third_party/kineto/libkineto/third_party/fmt/LICENSE + +Name: gemmlowp +License: Apache-2.0 +Files: /pytorch/third_party/gemmlowp/gemmlowp + For details, see the files concatenated below: /pytorch/third_party/gemmlowp/gemmlowp/LICENSE + +Name: generator +License: Apache-2.0 +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator, + /pytorch/third_party/protobuf/third_party/googletest/googlemock/scripts/generator, + /pytorch/third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE, + /pytorch/third_party/protobuf/third_party/googletest/googlemock/scripts/generator/LICENSE, + /pytorch/third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator/LICENSE + +Name: gettimeofday +License: Apache-2.0 +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday/LICENSE + +Name: gloo +License: BSD-3-Clause +Files: /pytorch/third_party/gloo + For details, see the files concatenated below: /pytorch/third_party/gloo/LICENSE + +Name: googlemock +License: BSD-3-Clause +Files: /pytorch/third_party/protobuf/third_party/googletest/googlemock, + /pytorch/third_party/tensorpipe/third_party/googletest/googlemock + For details, see the files concatenated below: /pytorch/third_party/protobuf/third_party/googletest/googlemock/LICENSE, + /pytorch/third_party/tensorpipe/third_party/googletest/googlemock/LICENSE + +Name: googletest +License: BSD-3-Clause +Files: /pytorch/third_party/fbgemm/external/googletest, + /pytorch/third_party/googletest, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/googletest, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest, + /pytorch/third_party/kineto/libkineto/third_party/googletest, + /pytorch/third_party/opentelemetry-cpp/third_party/googletest, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest, + /pytorch/third_party/protobuf/third_party/googletest, + /pytorch/third_party/protobuf/third_party/googletest/googletest, + /pytorch/third_party/tensorpipe/third_party/googletest, + /pytorch/third_party/tensorpipe/third_party/googletest/googletest + For details, see the files concatenated below: /pytorch/third_party/fbgemm/external/googletest/LICENSE, + /pytorch/third_party/googletest/LICENSE, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/googletest/LICENSE, + /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest/LICENSE, + /pytorch/third_party/kineto/libkineto/third_party/googletest/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/googletest/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/LICENSE, + /pytorch/third_party/protobuf/third_party/googletest/LICENSE, + /pytorch/third_party/protobuf/third_party/googletest/googletest/LICENSE, + /pytorch/third_party/tensorpipe/third_party/googletest/LICENSE, + /pytorch/third_party/tensorpipe/third_party/googletest/googletest/LICENSE + +Name: gtest +License: BSD-3-Clause +Files: /pytorch/third_party/ideep/mkl-dnn/tests/gtests/gtest + For details, see the files concatenated below: /pytorch/third_party/ideep/mkl-dnn/tests/gtests/gtest/LICENSE + +Name: hipify_torch +License: MIT +Files: /pytorch/third_party/fbgemm/external/hipify_torch + For details, see the files concatenated below: /pytorch/third_party/fbgemm/external/hipify_torch/LICENSE.txt + +Name: hstu +License: BSD-3-Clause +Files: /pytorch/third_party/fbgemm/fbgemm_gpu/experimental/hstu + For details, see the files concatenated below: /pytorch/third_party/fbgemm/fbgemm_gpu/experimental/hstu/LICENSE + +Name: hungarian +License: Permissive (free to use) +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian/LICENSE.txt + +Name: ideep +License: MIT +Files: /pytorch/third_party/ideep + For details, see the files concatenated below: /pytorch/third_party/ideep/LICENSE + +Name: irrlicht +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht/LICENSE.txt + +Name: kineto +License: BSD-3-Clause +Files: /pytorch/third_party/kineto + For details, see the files concatenated below: /pytorch/third_party/kineto/LICENSE + +Name: libnop +License: Apache-2.0 +Files: /pytorch/third_party/tensorpipe/third_party/libnop + For details, see the files concatenated below: /pytorch/third_party/tensorpipe/third_party/libnop/LICENSE + +Name: libstemmer +License: BSD-3-Clause +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer/LICENSE + +Name: libuv +License: MIT +Files: /pytorch/third_party/tensorpipe/third_party/libuv + For details, see the files concatenated below: /pytorch/third_party/tensorpipe/third_party/libuv/LICENSE + +Name: mimalloc +License: MIT +Files: /pytorch/third_party/mimalloc + For details, see the files concatenated below: /pytorch/third_party/mimalloc/LICENSE + +Name: miniz-3.0.2 +License: MIT +Files: /pytorch/third_party/miniz-3.0.2 + For details, see the files concatenated below: /pytorch/third_party/miniz-3.0.2/LICENSE + +Name: mkl-dnn +License: Apache-2.0 +Files: /pytorch/third_party/ideep/mkl-dnn + For details, see the files concatenated below: /pytorch/third_party/ideep/mkl-dnn/LICENSE + +Name: ms-gsl +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/third_party/ms-gsl + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/ms-gsl/LICENSE + +Name: mx +License: MIT +Files: /pytorch/third_party/fbgemm/fbgemm_gpu/src/quantize_ops/mx, + /pytorch/third_party/fbgemm/fbgemm_gpu/test/quantize/mx + For details, see the files concatenated below: /pytorch/third_party/fbgemm/fbgemm_gpu/src/quantize_ops/mx/LICENSE, + /pytorch/third_party/fbgemm/fbgemm_gpu/test/quantize/mx/LICENSE + +Name: onnx +License: Apache-2.0 +Files: /pytorch/third_party/onnx + For details, see the files concatenated below: /pytorch/third_party/onnx/LICENSE + +Name: opentelemetry-cpp +License: Apache-2.0 +Files: /pytorch/third_party/opentelemetry-cpp + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/LICENSE + +Name: opentelemetry-proto +License: Apache-2.0 +Files: /pytorch/third_party/opentelemetry-cpp/third_party/opentelemetry-proto + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/opentelemetry-proto/LICENSE + +Name: opentracing-cpp +License: Apache-2.0 +Files: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/LICENSE + +Name: pdcurses +License: Public Domain for core +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses/LICENSE + +Name: pfs +License: Apache-2.0 +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/pfs + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/pfs/LICENSE + +Name: physac +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/physac + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/physac/LICENSE + +Name: pqp +License: Apache-2.0 +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp/LICENSE + +Name: prometheus-cpp +License: MIT +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/LICENSE, + /pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/LICENSE + +Name: protobuf +License: BSD-3-Clause +Files: /pytorch/third_party/protobuf + For details, see the files concatenated below: /pytorch/third_party/protobuf/LICENSE + +Name: psimd +License: MIT +Files: /pytorch/third_party/psimd + For details, see the files concatenated below: /pytorch/third_party/psimd/LICENSE + +Name: pthreadpool +License: BSD-2-Clause +Files: /pytorch/third_party/pthreadpool + For details, see the files concatenated below: /pytorch/third_party/pthreadpool/LICENSE + +Name: pybind11 +License: BSD-3-Clause +Files: /pytorch/third_party/onnx/third_party/pybind11, + /pytorch/third_party/pybind11, + /pytorch/third_party/tensorpipe/third_party/pybind11 + For details, see the files concatenated below: /pytorch/third_party/onnx/third_party/pybind11/LICENSE, + /pytorch/third_party/pybind11/LICENSE, + /pytorch/third_party/tensorpipe/third_party/pybind11/LICENSE + +Name: python +License: Apache-2.0 with exception +Files: /pytorch/third_party/NVTX/python + For details, see the files concatenated below: /pytorch/third_party/NVTX/python/LICENSE.txt + +Name: python +License: BSD-3-Clause +Files: /pytorch/third_party/cutlass/python + For details, see the files concatenated below: /pytorch/third_party/cutlass/python/LICENSE.txt + +Name: python +License: BSD-3-Clause +Files: /pytorch/third_party/fbgemm/external/cutlass/python + For details, see the files concatenated below: /pytorch/third_party/fbgemm/external/cutlass/python/LICENSE.txt + +Name: python +License: BSD-3-Clause +Files: /pytorch/third_party/flash-attention/csrc/cutlass/python + For details, see the files concatenated below: /pytorch/third_party/flash-attention/csrc/cutlass/python/LICENSE.txt + +Name: python-peachpy +License: BSD-2-Clause +Files: /pytorch/third_party/python-peachpy + For details, see the files concatenated below: /pytorch/third_party/python-peachpy/LICENSE.rst + +Name: sigslot +License: Public Domain +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot/LICENSE + +Name: sleef +License: BSL-1.0 +Files: /pytorch/third_party/sleef + For details, see the files concatenated below: /pytorch/third_party/sleef/LICENSE.txt + +Name: swift +License: Apache-2.0 +Files: /pytorch/third_party/flatbuffers/swift + For details, see the files concatenated below: /pytorch/third_party/flatbuffers/swift/LICENSE + +Name: tb_plugin +License: BSD-3-Clause +Files: /pytorch/third_party/kineto/tb_plugin + For details, see the files concatenated below: /pytorch/third_party/kineto/tb_plugin/LICENSE + +Name: tensorflow-common +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common/LICENSE.txt + +Name: tensorpipe +License: BSD-3-Clause +Files: /pytorch/third_party/tensorpipe + For details, see the files concatenated below: /pytorch/third_party/tensorpipe/LICENSE.txt + +Name: test +License: MIT with exception +Files: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test + For details, see the files concatenated below: /pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test/LICENSE + +Name: variant +License: BSD-3-Clause +Files: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant/LICENSE + +Name: vcpkg +License: MIT +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/LICENSE.txt + +Name: vulkan +License: Apache-2.0 with exception +Files: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan + For details, see the files concatenated below: /pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan/LICENSE.txt + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/LICENSE +---------------------------------------------------------------------------------- +Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +/pytorch/third_party/FP16/LICENSE +--------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2017 Georgia Institute of Technology +Copyright 2019 Google LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/FXdiv/LICENSE +---------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2016-2017 Marat Dukhan + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/NNPACK/LICENSE +----------------------------------- +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2015-2017, Georgia Institute of Technology +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/NVTX/LICENSE.txt +------------------------------------- +============================================================================== +NVTX is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + + +/pytorch/third_party/VulkanMemoryAllocator/LICENSE.txt +------------------------------------------------------ +Copyright (c) 2017-2025 Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/XNNPACK/LICENSE +------------------------------------ +BSD License + +For XNNPACK software + +Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +Copyright 2019 Google LLC + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/aiter/LICENSE +---------------------------------- +Copyright © Advanced Micro Devices, Inc. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/benchmark/LICENSE +-------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/third_party/benchmark/LICENSE +-------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/protobuf/third_party/benchmark/LICENSE +----------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers/LICENSE.txt +---------------------------------------------------------------------------------------- +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE +---------------------------------------------------------------------------------------------------------------------------------- +Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE +--------------------------------------------------------------------------------------------------------------- +Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +/pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2/LICENSE.txt +------------------------------------------------------------------------------------------------------------------- +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/cpuinfo/deps/clog/LICENSE +---------------------------------------------- +Copyright (C) 2018 Marat Dukhan +Copyright (c) 2017-2018 Facebook Inc. +Copyright (c) 2017 Georgia Institute of Technology + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/fbgemm/external/cpuinfo/deps/clog/LICENSE +-------------------------------------------------------------- +Copyright (C) 2018 Marat Dukhan +Copyright (c) 2017-2018 Facebook Inc. +Copyright (c) 2017 Georgia Institute of Technology + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama/LICENSE.txt +----------------------------------------------------------------------------------------------------------------------------- +Copyright (c) 2010 Jonathan Hartley + +Released under the New BSD license (reproduced below), or alternatively you may +use this software under any OSI approved open source license such as those at +http://opensource.org/licenses/alphabetical + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name(s) of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +/pytorch/third_party/aiter/3rdparty/composable_kernel/LICENSE +------------------------------------------------------------- +Copyright (c) 2018- , Advanced Micro Devices, Inc. (Chao Liu, Jing Zhang) +Copyright (c) 2019- , Advanced Micro Devices, Inc. (Letao Qin, Qianfeng Zhang, Liang Huang, Shaojie Wang) +Copyright (c) 2022- , Advanced Micro Devices, Inc. (Anthony Chang, Chunyu Lai, Illia Silin, Adam Osewski, Poyen Chen, Jehandad Khan) +Copyright (c) 2019-2021, Advanced Micro Devices, Inc. (Hanwen Chang) +Copyright (c) 2019-2020, Advanced Micro Devices, Inc. (Tejash Shah) +Copyright (c) 2020 , Advanced Micro Devices, Inc. (Xiaoyan Zhou) +Copyright (c) 2021-2022, Advanced Micro Devices, Inc. (Jianfeng Yan) + +SPDX-License-Identifier: MIT +Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/composable_kernel/LICENSE +---------------------------------------------- +Copyright (c) 2018- , Advanced Micro Devices, Inc. (Chao Liu, Jing Zhang) +Copyright (c) 2019- , Advanced Micro Devices, Inc. (Letao Qin, Qianfeng Zhang, Liang Huang, Shaojie Wang) +Copyright (c) 2022- , Advanced Micro Devices, Inc. (Anthony Chang, Chunyu Lai, Illia Silin, Adam Osewski, Poyen Chen, Jehandad Khan) +Copyright (c) 2019-2021, Advanced Micro Devices, Inc. (Hanwen Chang) +Copyright (c) 2019-2020, Advanced Micro Devices, Inc. (Tejash Shah) +Copyright (c) 2020 , Advanced Micro Devices, Inc. (Xiaoyan Zhou) +Copyright (c) 2021-2022, Advanced Micro Devices, Inc. (Jianfeng Yan) + +SPDX-License-Identifier: MIT +Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/fbgemm/external/composable_kernel/LICENSE +-------------------------------------------------------------- +Copyright (c) 2018- , Advanced Micro Devices, Inc. (Chao Liu, Jing Zhang) +Copyright (c) 2019- , Advanced Micro Devices, Inc. (Letao Qin, Qianfeng Zhang, Liang Huang, Shaojie Wang) +Copyright (c) 2022- , Advanced Micro Devices, Inc. (Anthony Chang, Chunyu Lai, Illia Silin, Adam Osewski, Poyen Chen, Jehandad Khan) +Copyright (c) 2019-2021, Advanced Micro Devices, Inc. (Hanwen Chang) +Copyright (c) 2019-2020, Advanced Micro Devices, Inc. (Tejash Shah) +Copyright (c) 2020 , Advanced Micro Devices, Inc. (Xiaoyan Zhou) +Copyright (c) 2021-2022, Advanced Micro Devices, Inc. (Jianfeng Yan) + +SPDX-License-Identifier: MIT +Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/flash-attention/csrc/composable_kernel/LICENSE +------------------------------------------------------------------- +Copyright (c) 2018- , Advanced Micro Devices, Inc. (Chao Liu, Jing Zhang) +Copyright (c) 2019- , Advanced Micro Devices, Inc. (Letao Qin, Qianfeng Zhang, Liang Huang, Shaojie Wang) +Copyright (c) 2022- , Advanced Micro Devices, Inc. (Anthony Chang, Chunyu Lai, Illia Silin, Adam Osewski, Poyen Chen, Jehandad Khan) +Copyright (c) 2019-2021, Advanced Micro Devices, Inc. (Hanwen Chang) +Copyright (c) 2019-2020, Advanced Micro Devices, Inc. (Tejash Shah) +Copyright (c) 2020 , Advanced Micro Devices, Inc. (Xiaoyan Zhou) +Copyright (c) 2021-2022, Advanced Micro Devices, Inc. (Jianfeng Yan) + +SPDX-License-Identifier: MIT +Copyright (c) 2018-2024, Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/cpp-httplib/LICENSE +---------------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint/LICENSE +------------------------------------------------------------------------------------------------------ +cpplint.py and its corresponding unit tests are Copyright (C) 2009 Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/LICENSE +--------------------------------------------------------------------------------- +This license applies to everything except the contents of the "test" +directory and its subdirectories. + +MIT License + +Copyright (c) 2017-2021 Huu Nguyen +Copyright (c) 2022 libcpr and many other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +/pytorch/third_party/cpuinfo/LICENSE +------------------------------------ +Copyright (c) 2019 Google LLC +Copyright (c) 2017-2018 Facebook Inc. +Copyright (C) 2012-2017 Georgia Institute of Technology +Copyright (C) 2010-2012 Marat Dukhan + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/fbgemm/external/cpuinfo/LICENSE +---------------------------------------------------- +Copyright (c) 2019 Google LLC +Copyright (c) 2017-2018 Facebook Inc. +Copyright (C) 2012-2017 Georgia Institute of Technology +Copyright (C) 2010-2012 Marat Dukhan + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/cudnn_frontend/LICENSE.txt +----------------------------------------------- +/* + * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + + +/pytorch/third_party/cutlass/LICENSE.txt +---------------------------------------- +Copyright (c) 2017 - 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Certain files within this repository are subject to separate licensing terms: + +- The files located in the `python/CuTeDSL` directory are licensed under the + NVIDIA End User License Agreement (EULA). Please refer to + https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html + for the full terms. + + +/pytorch/third_party/fbgemm/external/cutlass/LICENSE.txt +-------------------------------------------------------- +Copyright (c) 2017 - 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Certain files within this repository are subject to separate licensing terms: + +- The files located in the `python/CuTeDSL` directory are licensed under the + NVIDIA End User License Agreement (EULA). Please refer to + https://docs.nvidia.com/cutlass/media/docs/pythonDSL/license.html + for the full terms. + + +/pytorch/third_party/flash-attention/csrc/cutlass/LICENSE.txt +------------------------------------------------------------- +Copyright (c) 2017 - 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/flatbuffers/dart/LICENSE +--------------------------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/NVTX/docs/LICENSE.txt +------------------------------------------ +============================================================================== +NVTX is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest/LICENSE.txt +-------------------------------------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2016-2021 Viktor Kirilov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt +------------------------------------------------------------------------------------------------------------------------------------------------ +=============== +Duktape license +=============== + +(http://opensource.org/licenses/MIT) + +Copyright (c) 2013-2016 by Duktape authors (see AUTHORS.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt +----------------------------------------------------------------------------------------------------------------------------- +=============== +Duktape license +=============== + +(http://opensource.org/licenses/MIT) + +Copyright (c) 2013-2016 by Duktape authors (see AUTHORS.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt +------------------------------------------------------------------------------------------------------------------------------------------------ +=============== +Duktape license +=============== + +(http://opensource.org/licenses/MIT) + +Copyright (c) 2013-2017 by Duktape authors (see AUTHORS.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt +----------------------------------------------------------------------------------------------------------------------------- +=============== +Duktape license +=============== + +(http://opensource.org/licenses/MIT) + +Copyright (c) 2013-2017 by Duktape authors (see AUTHORS.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/LICENSE +----------------------------------------------------------------- +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw/LICENSE +------------------------------------------------------------------------------------------------ +TraceLogging Dynamic for Windows + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected/LICENSE +----------------------------------------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) 2015 Martin Moene +Copyright (c) 2015 Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +/pytorch/third_party/fbgemm/LICENSE +----------------------------------- +BSD License + +For FBGEMM software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec/LICENSE.txt +------------------------------------------------------------------------------ +GNU LESSER GENERAL PUBLIC LICENSE +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] +Preamble +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + +a) The modified work must itself be a software library. +b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. +c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. +d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) +b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. +c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. +d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. +e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. +b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS +How to Apply These Terms to Your New Libraries +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! + +/pytorch/third_party/flash-attention/LICENSE +-------------------------------------------- +BSD 3-Clause License + +Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/flatbuffers/LICENSE +---------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/fmt/LICENSE +-------------------------------- +Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/fmt/LICENSE.rst +------------------------------------------------------------------------------------- +Copyright (c) 2012 - present, Victor Zverovich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. + + +/pytorch/third_party/kineto/libkineto/third_party/fmt/LICENSE +------------------------------------------------------------- +Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. + + +/pytorch/third_party/gemmlowp/gemmlowp/LICENSE +---------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE +--------------------------------------------------------------------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2007] Neal Norwitz + Portions Copyright [2007] Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE +-------------------------------------------------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2007] Neal Norwitz + Portions Copyright [2007] Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/protobuf/third_party/googletest/googlemock/scripts/generator/LICENSE +----------------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2007] Neal Norwitz + Portions Copyright [2007] Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator/LICENSE +------------------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2007] Neal Norwitz + Portions Copyright [2007] Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday/LICENSE +----------------------------------------------------------------------------- +/* + * Copied from PostgreSQL source: + * http://doxygen.postgresql.org/gettimeofday_8c_source.html + * + */ + +/* + * gettimeofday.c + * Win32 gettimeofday() replacement + * + * src/port/gettimeofday.c + * + * Copyright (c) 2003 SRA, Inc. + * Copyright (c) 2003 SKC, Inc. + * + * Permission to use, copy, modify, and distribute this software and + * its documentation for any purpose, without fee, and without a + * written agreement is hereby granted, provided that the above + * copyright notice and this paragraph and the following two + * paragraphs appear in all copies. + * + * IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, + * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS + * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS + * IS" BASIS, AND THE AUTHOR HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, + * SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + */ + + +/pytorch/third_party/gloo/LICENSE +--------------------------------- +BSD License + +For Gloo software + +Copyright (c) 2017-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/protobuf/third_party/googletest/googlemock/LICENSE +----------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/tensorpipe/third_party/googletest/googlemock/LICENSE +------------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/fbgemm/external/googletest/LICENSE +------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/googletest/LICENSE +--------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/googletest/LICENSE +---------------------------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/3rdparty/googletest/LICENSE +---------------------------------------------------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/googletest/LICENSE +-------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/googletest/LICENSE +--------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/LICENSE +--------------------------------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/protobuf/third_party/googletest/LICENSE +------------------------------------------------------------ +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/protobuf/third_party/googletest/googletest/LICENSE +----------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/tensorpipe/third_party/googletest/LICENSE +-------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/tensorpipe/third_party/googletest/googletest/LICENSE +------------------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/ideep/mkl-dnn/tests/gtests/gtest/LICENSE +------------------------------------------------------------- +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/fbgemm/external/hipify_torch/LICENSE.txt +------------------------------------------------------------- +MIT License + +Copyright (c) 2021-2024, Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/fbgemm/fbgemm_gpu/experimental/hstu/LICENSE +---------------------------------------------------------------- +BSD 3-Clause License + +Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* + * SPDX-FileCopyrightText: Copyright (c) <2024> NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: LicenseRef-NvidiaProprietary + * + * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual + * property and proprietary rights in and to this material, related + * documentation and any modifications thereto. Any use, reproduction, + * disclosure or distribution of this material and related documentation + * without an express license agreement from NVIDIA CORPORATION or + * its affiliates is strictly prohibited. + */ + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian/LICENSE.txt +------------------------------------------------------------------------------ +/******************************************************************** + ******************************************************************** + ** + ** libhungarian by Cyrill Stachniss, 2004 + ** + ** + ** Solving the Minimum Assignment Problem using the + ** Hungarian Method. + ** + ** ** This file may be freely copied and distributed! ** + ** + ** Parts of the used code was originally provided by the + ** "Stanford GraphGase", but I made changes to this code. + ** As asked by the copyright node of the "Stanford GraphGase", + ** I hereby proclaim that this file are *NOT* part of the + ** "Stanford GraphGase" distrubition! + ** + ** This file is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied + ** warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + ** PURPOSE. + ** + ******************************************************************** + ********************************************************************/ + + +/pytorch/third_party/ideep/LICENSE +---------------------------------- +Copyright (c) 2018 Intel Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht/LICENSE.txt +----------------------------------------------------------------------------- +The Irrlicht Engine License +=========================== + +Copyright (C) 2002-2015 Nikolaus Gebhardt + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be clearly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +/pytorch/third_party/kineto/LICENSE +----------------------------------- +BSD License + +For Kineto software + +Copyright (c) Meta Platforms, Inc. and affiliates. + +All contributions by Microsoft: +Copyright (c) Microsoft Corporation. (The Azure AI Platform team) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Meta nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/tensorpipe/third_party/libnop/LICENSE +---------------------------------------------------------- +Copyright 2017 The Native Object Protocols Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer/LICENSE +--------------------------------------------------------------------------- +Snowball - License +Except where explicitly noted, all the software given out on this Snowball site is covered by the 3-clause BSD License: + +Copyright (c) 2001, Dr Martin Porter, +Copyright (c) 2002, Richard Boulton. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Essentially, all this means is that you can do what you like with the code, except claim another Copyright for it, or claim that it is issued under a different license. The software is also issued without warranties, which means that if anyone suffers through its use, they cannot come back and sue you. You also have to alert anyone to whom you give the Snowball software to the fact that it is covered by the BSD license. + +We have not bothered to insert the licensing arrangement into the text of the Snowball software. + + +/pytorch/third_party/tensorpipe/third_party/libuv/LICENSE +--------------------------------------------------------- +Copyright (c) 2015-present libuv project contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +/pytorch/third_party/mimalloc/LICENSE +------------------------------------- +MIT License + +Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/miniz-3.0.2/LICENSE +---------------------------------------- +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/ideep/mkl-dnn/LICENSE +------------------------------------------ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + ============================================================================ + + Copyright 2016-2023 Intel Corporation + Copyright 2018 YANDEX LLC + Copyright 2019-2023 FUJITSU LIMITED + Copyright 2020-2023 Arm Ltd. and affiliates + Copyright 2020-2022 Codeplay Software Limited + Copyright 2021 Alanna Tempest + Copyright 2022-2023 IBM Corporation + Copyright 2023 KNS Group LLC (YADRO) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This distribution includes third party software ("third party programs"). + This third party software, even if included with the distribution of + the Intel software, may be governed by separate license terms, including + without limitation, third party license terms, other Intel software license + terms, and open source software license terms. These separate license terms + govern your use of the third party programs as set forth in the + "THIRD-PARTY-PROGRAMS" file. + + +/pytorch/third_party/opentelemetry-cpp/third_party/ms-gsl/LICENSE +----------------------------------------------------------------- +Copyright (c) 2015 Microsoft Corporation. All rights reserved. + +This code is licensed under the MIT License (MIT). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +/pytorch/third_party/fbgemm/fbgemm_gpu/src/quantize_ops/mx/LICENSE +------------------------------------------------------------------ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +/pytorch/third_party/fbgemm/fbgemm_gpu/test/quantize/mx/LICENSE +--------------------------------------------------------------- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +/pytorch/third_party/onnx/LICENSE +--------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/LICENSE +---------------------------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/third_party/opentelemetry-proto/LICENSE +------------------------------------------------------------------------------ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/LICENSE +-------------------------------------------------------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTracing Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses/LICENSE +------------------------------------------------------------------------- +The core package is in the public domain, but small portions of PDCurses are subject to copyright under various licenses. + +The win32 files are released to the public domain. + +If you use PDCurses in an application, an acknowledgement would be appreciated, but is not mandatory. If you make corrections or enhancements to PDCurses, please forward them to the current maintainer for the benefit of other users. + +This software is provided AS IS with NO WARRANTY whatsoever. + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/pfs/LICENSE +--------------------------------------------------------------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2020-present Daniel Trugman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/physac/LICENSE +----------------------------------------------------------------------- +MIT License + +Copyright (c) 2022 Víctor Fisac + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp/LICENSE +-------------------------------------------------------------------- +Copyright 1999 University of North Carolina at Chapel Hill. +All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for educational, research, and non-profit purposes, without fee, +and without a written agreement is hereby granted, provided that the above +copyright notice and the following three paragraphs appear in all copies. + +IN NO EVENT SHALL THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL BE LIABLE TO +ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +DOCUMENTATION, EVEN IF THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL SPECIFICALLY DISCLAIMS ANY +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED +HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA AT +CHAPEL HILL HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, +ENHANCEMENTS, OR MODIFICATIONS. + +The authors may be contacted via: + +US Mail: Eric Larsen, Stefan Gottschalk + Department of Computer Science + Sitterson Hall, CB #3175 + University of North Carolina + Chapel Hill, NC 27599-3175 + +Phone: (919) 962-1749 + +Email: geom@cs.unc.edu + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/prometheus-cpp/LICENSE +-------------------------------------------------------------------------------------------- +MIT License + +Copyright (c) 2016-2021 Jupp Mueller +Copyright (c) 2017-2022 Gregor Jasny + +And many contributors, see +https://github.com/jupp0r/prometheus-cpp/graphs/contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/third_party/prometheus-cpp/LICENSE +------------------------------------------------------------------------- +MIT License + +Copyright (c) 2016-2021 Jupp Mueller +Copyright (c) 2017-2022 Gregor Jasny + +And many contributors, see +https://github.com/jupp0r/prometheus-cpp/graphs/contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/protobuf/LICENSE +------------------------------------- +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + + +/pytorch/third_party/psimd/LICENSE +---------------------------------- +The MIT License (MIT) + +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2014-2017 Georgia Institute of Technology +Copyright 2019 Google LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/pthreadpool/LICENSE +---------------------------------------- +Copyright 2019 Google LLC +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2015-2017 Georgia Institute of Technology +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +/pytorch/third_party/onnx/third_party/pybind11/LICENSE +------------------------------------------------------ +Copyright (c) 2016 Wenzel Jakob , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. + + +/pytorch/third_party/pybind11/LICENSE +------------------------------------- +Copyright (c) 2016 Wenzel Jakob , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. + + +/pytorch/third_party/tensorpipe/third_party/pybind11/LICENSE +------------------------------------------------------------ +Copyright (c) 2016 Wenzel Jakob , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. + + +/pytorch/third_party/NVTX/python/LICENSE.txt +-------------------------------------------- +============================================================================== +NVTX is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + + +/pytorch/third_party/cutlass/python/LICENSE.txt +----------------------------------------------- +Copyright (c) 2017 - 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/fbgemm/external/cutlass/python/LICENSE.txt +--------------------------------------------------------------- +Copyright (c) 2017 - 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/flash-attention/csrc/cutlass/python/LICENSE.txt +-------------------------------------------------------------------- +Copyright (c) 2017 - 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/python-peachpy/LICENSE.rst +----------------------------------------------- +============================== +PeachPy license (2-clause BSD) +============================== + +Copyright (c) 2017, Facebook Inc. +Copyright (c) 2013-2017, Georgia Institute of Technology +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot/LICENSE +------------------------------------------------------------------------ +License +The sigslot library has been placed in the public domain. This means that you are free to use it however you like. + +The author takes no responsibility or liability of any kind for any use that you may make of this library. + +If you screw up, it's your fault. + +If the library screws up, you got it for free, so you should have tested it better - it's still your responsibility. + +/pytorch/third_party/sleef/LICENSE.txt +-------------------------------------- +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/flatbuffers/swift/LICENSE +---------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +/pytorch/third_party/kineto/tb_plugin/LICENSE +--------------------------------------------- +BSD License + +For Kineto software + +Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. + +All contributions by Microsoft: +Copyright (c) Microsoft Corporation. (The Azure AI Platform team) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common/LICENSE.txt +-------------------------------------------------------------------------------------- +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +/pytorch/third_party/tensorpipe/LICENSE.txt +------------------------------------------- +BSD License + +For TensorPipe software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Meta nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +/pytorch/third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test/LICENSE +-------------------------------------------------------------------------------------- +This license applies to everything inside this directory and all +subdirectories. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +/pytorch/third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant/LICENSE +---------------------------------------------------------------------------------------------------------------- +Copyright (c) MapBox +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +- Neither the name "MapBox" nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/LICENSE.txt +-------------------------------------------------------------- +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +/pytorch/third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan/LICENSE.txt +--------------------------------------------------------------------------- +/* +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +=============================================================================================================================================== + +/Copyright (C) 2012 LunarG, Inc. +//All rights reserved. +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions +//are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of LunarG Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +//"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +//LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +//FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +//COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +//INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +//BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +//LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +//ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +//POSSIBILITY OF SUCH DAMAGE. + +=============================================================================================================================================== + +#============================================================================= +# Copyright 2007-2009 Kitware, Inc. +# Copyright 2007-2008 Miguel A. Figueroa-Villanueva +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright_cmake.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distributed this file outside of CMake, substitute the full +# License text for the above reference.) + + +============================================================================================================================================== + +// +// Copyright (C) 2015-2018 Google, Inc. +// Copyright (C) +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of 3Dlabs Inc. Ltd. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// + +========================================================================================================================================== + +Note: This license has also been called the "New BSD License" or "Modified BSD License". See also the 2-clause BSD License. +Copyright +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +========================================================================================================================================== + +/* +* xxHash - Fast Hash algorithm +* Copyright (C) 2012-2016, Yann Collet +* +* BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are +* met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above +* copyright notice, this list of conditions and the following disclaimer +* in the documentation and/or other materials provided with the +* distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* You can contact the author at : +* - xxHash homepage: http://www.xxhash.com +* - xxHash source repository : https://github.com/Cyan4973/xxHash +*/ + + +=========================================================================================================================================== + +# Copyright (C) 2018 Google, Inc. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +========================================================================================================================================== + +/* A Bison parser, made by GNU Bison 3.0.4. */ + +/* Bison implementation for Yacc-like parsers in C +Copyright (C) 1984, 1989-1990, 2000-2015 Free Software Foundation, Inc. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program. If not, see . */ + +/* As a special exception, you may create a larger work that contains +part or all of the Bison parser skeleton and distribute that work +under terms of your choice, so long as that work isn't itself a +parser generator using the skeleton or a modified version thereof +as a parser skeleton. Alternatively, if you modify or redistribute +the parser skeleton itself, you may (at your option) remove this +special exception, which will cause the skeleton and the resulting +Bison output files to be licensed under the GNU General Public +License without this special exception. +This special exception was added by the Free Software Foundation in +version 2.2 of Bison. */ + +/* C LALR(1) parser skeleton written by Richard Stallman, by +simplifying the original so-called "semantic" parser. */ + +/* All symbols defined below should begin with yy or YY, to avoid +infringing on user name space. This should be done even for local +variables, as they might otherwise be expanded by user macros. +There are some unavoidable exceptions within include files to +define necessary library symbols; they are noted "INFRINGES ON +USER NAME SPACE" below. */ + +============================================================================================================================================== + +copyright : [ +Copyright (c) 2017 The Khronos Group Inc., +, +Permission is hereby granted, free of charge, to any person obtaining a copy, +of this software and/or associated documentation files (the \Materials\"),", +to deal in the Materials without restriction, including without limitation, +the rights to use, copy, modify, merge, publish, distribute, sublicense,, +and/or sell copies of the Materials, and to permit persons to whom the, +Materials are furnished to do so, subject to the following conditions:, +, +The above copyright notice and this permission notice shall be included in, +all copies or substantial portions of the Materials., +, +MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS, +STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND, +HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ , +, +THE MATERIALS ARE PROVIDED \AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS", +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL, +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER, +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, +FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS, +IN THE MATERIALS. + +============================================================================================================================================= + +CMake - Cross Platform Makefile Generator +Copyright 2000-2009 Kitware, Inc., Insight Software Consortium +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the names of Kitware, Inc., the Insight Software Consortium, +nor the names of their contributors may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +The above copyright and license notice applies to distributions of +CMake in source and binary form. Some source files contain additional +notices of original copyright by their contributors; see each source +for details. Third-party software packages supplied with CMake under +compatible licenses provide their own copyright notices documented in +corresponding subdirectories. + +------------------------------------------------------------------------------ + +CMake was initially developed by Kitware with the following sponsorship: + +* National Library of Medicine at the National Institutes of Health +as part of the Insight Segmentation and Registration Toolkit (ITK). + +* US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel +Visualization Initiative. + +* National Alliance for Medical Image Computing (NAMIC) is funded by the +National Institutes of Health through the NIH Roadmap for Medical Research, +Grant U54 EB005149. + +* Kitware, Inc. + +======================================================================================================================================== + +The authors of this software are Rob Pike and Ken Thompson. +* Copyright (c) 2002 by Lucent Technologies. +* Permission to use, copy, modify, and distribute this software for any +* purpose without fee is hereby granted, provided that this entire notice +* is included in all copies of any software which is or includes a copy +* or modification of this software and in all copies of the supporting +* documentation for such software. +* THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +* WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR LUCENT TECHNOLOGIES MAKE ANY +* REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +* OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + + +======================================================================================================================================== + +Copyright (c) 2015-2018 Baldur Karlsson + +Copyright (c) 2014 Crytek + +Copyright (c) 1998-2018 Third party code and tools + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================================================================================================================= + +/* +Copyright (c) 2009 Dave Gamble +Copyright (c) 2015-2016 The Khronos Group Inc. +Copyright (c) 2015-2016 Valve Corporation +Copyright (c) 2015-2016 LunarG, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +=========================================================================================================================================== + +Copyright (c) 2005 - 2017 G-Truc Creation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + + +========================================================================================================================================== + +/* +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is +released under the terms of the MIT License (see below). +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: +http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +Copyright (c) 2007-2010 Baptiste Lepilleur +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================================================================================================================== + +/** +* `murmurhash.h' - murmurhash +* +* copyright (c) 2014 joseph werle +* Copyright (c) 2015-2016 The Khronos Group Inc. +* Copyright (c) 2015-2016 Valve Corporation +* Copyright (c) 2015-2016 LunarG, Inc. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and/or associated documentation files (the "Materials"), to +* deal in the Materials without restriction, including without limitation the +* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +* sell copies of the Materials, and to permit persons to whom the Materials are +* furnished to do so, subject to the following conditions: +* +* The above copyright notice(s) and this permission notice shall be included in +* all copies or substantial portions of the Materials. +* +* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE +* USE OR OTHER DEALINGS IN THE MATERIALS. +*/ + +========================================================================================================================================= + +Licenced as X11: http://www.kryogenix.org/code/browser/licence.html +This basically means: do what you want with it. + +========================================================================================================================================= + +/////////////////////////////////////////////////////////////////////////////////// +/// OpenGL Mathematics (glm.g-truc.net) +/// +/// Copyright (c) 2005 - 2014 G-Truc Creation (www.g-truc.net) +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +/// THE SOFTWARE. +/// +/// @ref core +/// @file glm/common.hpp +/// @date 2013-12-24 / 2013-12-24 +/// @author Christophe Riccio +/////////////////////////////////////////////////////////////////////////////////// + + +========================================================================================================================================== + +// LICENSE +// +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevocable license to copy, +// distribute, and modify this file as you see fit. +// + +========================================================================================================================================== + +Simple DirectMedia Layer +Copyright (C) 1997-2018 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +========================================================================================================================================= + +/****************************************************************************\ +Copyright (c) 2002, NVIDIA Corporation. + +NVIDIA Corporation("NVIDIA") supplies this software to you in +consideration of your agreement to the following terms, and your use, +installation, modification or redistribution of this NVIDIA software +constitutes acceptance of these terms. If you do not agree with these +terms, please do not use, install, modify or redistribute this NVIDIA +software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, NVIDIA grants you a personal, non-exclusive +license, under NVIDIA's copyrights in this original NVIDIA software (the +NVIDIA Software), to use, reproduce, modify and redistribute the +NVIDIA Software, with or without modifications, in source and/or binary +forms; provided that if you redistribute the NVIDIA Software, you must +retain the copyright notice of NVIDIA, this notice and the following +text and disclaimers in all such redistributions of the NVIDIA Software. +Neither the name, trademarks, service marks nor logos of NVIDIA +Corporation may be used to endorse or promote products derived from the +NVIDIA Software without specific prior written permission from NVIDIA. +Except as expressly stated in this notice, no other rights or licenses +express or implied, are granted by NVIDIA herein, including but not +limited to any patent rights that may be infringed by your derivative +works or by other works in which the NVIDIA Software may be +incorporated. No hardware is licensed hereunder. + +THE NVIDIA SOFTWARE IS BEING PROVIDED ON AN "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, +INCLUDING WITHOUT LIMITATION, WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +ITS USE AND OPERATION EITHER ALONE OR IN COMBINATION WITH OTHER +PRODUCTS. + +IN NO EVENT SHALL NVIDIA BE LIABLE FOR ANY SPECIAL, INDIRECT, +INCIDENTAL, EXEMPLARY, CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, LOST PROFITS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) OR ARISING IN ANY WAY +OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE +NVIDIA SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, +TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF +NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\****************************************************************************/ + +================================================================================================================================================== + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +================================================================================================================================================== + +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. + +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + + +torch-optimizer +Apache Software License +https://github.com/jettify/pytorch-optimizer +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Nikolay Novik (https://github.com/jettify) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +torch_fidelity +Apache License 2.0 +https://www.github.com/toshas/torch-fidelity +Copyright 2020 Anton Obukhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +torchcodec +BSD 3-Clause License + +Copyright 2024 Meta + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +UNKNOWN +BSD 3-Clause License + +Copyright 2024 Meta + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + + +torchdata +BSD License +https://github.com/pytorch/data +BSD 3-Clause License + +Copyright (c) 2021-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +torchmetrics +Apache Software License +https://github.com/Lightning-AI/torchmetrics + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020-2022 Lightning-AI team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +torchtitan +BSD 3-Clause License + +(c) Meta Platforms, Inc. and affiliates. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +UNKNOWN +BSD 3-Clause License + +(c) Meta Platforms, Inc. and affiliates. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + + +torchvision +BSD +https://github.com/pytorch/vision +BSD 3-Clause License + +Copyright (c) Soumith Chintala 2016, +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +tornado +Apache Software License +http://www.tornadoweb.org/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +tqdm +MPL-2.0 AND MIT +https://tqdm.github.io +`tqdm` is a product of collaborative work. +Unless otherwise stated, all authors (see commit logs) retain copyright +for their respective work, and release the work under the MIT licence +(text below). + +Exceptions or notable authors are listed below +in reverse chronological order: + +* files: * + MPL-2.0 2015-2026 (c) Casper da Costa-Luis + [casperdcl](https://github.com/casperdcl). +* files: tqdm/_tqdm.py + MIT 2016 (c) [PR #96] on behalf of Google Inc. +* files: tqdm/_tqdm.py README.rst .gitignore + MIT 2013 (c) Noam Yorav-Raphael, original author. + +[PR #96]: https://github.com/tqdm/tqdm/pull/96 + + +Mozilla Public Licence (MPL) v. 2.0 - Exhibit A +----------------------------------------------- + +This Source Code Form is subject to the terms of the +Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this project, +You can obtain one at https://mozilla.org/MPL/2.0/. + + +MIT License (MIT) +----------------- + +Copyright (c) 2013 noamraph + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +traitlets +BSD License +https://github.com/ipython/traitlets +BSD 3-Clause License + +- Copyright (c) 2001-, IPython Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +transformer_engine +UNKNOWN +UNKNOWN + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + +transformers +Apache Software License +https://github.com/huggingface/transformers +Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +trimesh +MIT License +https://github.com/mikedh/trimesh +The MIT License (MIT) + +Copyright (c) 2023 Michael Dawson-Haggerty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +triton +MIT License +https://github.com/triton-lang/triton/ +/* +* Copyright 2018-2020 Philippe Tillet +* Copyright 2020-2022 OpenAI +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files +* (the "Software"), to deal in the Software without restriction, +* including without limitation the rights to use, copy, modify, merge, +* publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, +* subject to the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + + +trove-classifiers +Apache Software License +https://github.com/pypa/trove-classifiers + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +typeguard +MIT +UNKNOWN +This is the MIT license: http://www.opensource.org/licenses/mit-license.php + +Copyright (c) Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +typer +MIT License +https://github.com/fastapi/typer +The MIT License (MIT) + +Copyright (c) 2019 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +typing-inspect +MIT License +https://github.com/ilevkivskyi/typing_inspect +The MIT License (MIT) + +Copyright (c) 2017-2019 Ivan Levkivskyi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +typing-inspection +MIT +https://github.com/pydantic/typing-inspection +MIT License + +Copyright (c) Pydantic Services Inc. 2025 to present + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +typing_extensions +PSF-2.0 +https://github.com/python/typing_extensions +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +tyro +MIT License +UNKNOWN +MIT License + +Copyright (c) 2024 Brent Yi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +tzdata +Apache-2.0 +https://github.com/python/tzdata +Apache Software License 2.0 + +Copyright (c) 2020, Paul Ganssle (Google) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +uri-template +MIT License +https://gitlab.linss.com/open-source/python/uri-template +MIT License + +Copyright (c) 2020 Peter Linss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +urllib3 +MIT +https://github.com/urllib3/urllib3/blob/main/CHANGES.rst +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +userpath +MIT +https://github.com/ofek/userpath +MIT License + +Copyright (c) 2017-present Ofek Lev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +uv +Apache Software License; MIT License +https://pypi.org/project/uv/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +uvicorn +BSD-3-Clause +https://uvicorn.dev/ +Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +uvloop +Apache Software License; MIT License +UNKNOWN +Copyright (C) 2016-present the uvloop authors and contributors. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-present MagicStack Inc. http://magic.io + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +virtualenv +MIT +https://github.com/pypa/virtualenv +Copyright (c) 2020-202x The virtualenv developers + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +wandb +MIT License +https://github.com/wandb/wandb +MIT License + +Copyright (c) 2021 Weights and Biases, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +watchfiles +MIT License +https://github.com/samuelcolvin/watchfiles +The MIT License (MIT) + +Copyright (c) 2017 to present Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +wcmatch +MIT +https://github.com/facelessuser/wcmatch +MIT License + +Copyright (c) 2018 - 2025 Isaac Muse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +webcolors +BSD License +UNKNOWN +Copyright (c) James Bennett, and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +webdataset +BSD-3-Clause +http://github.com/webdataset/webdataset +Copyright 2020 NVIDIA CORPORATION. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +webencodings +BSD License +https://github.com/SimonSapin/python-webencodings +UNKNOWN + +websocket-client +Apache Software License +https://github.com/websocket-client/websocket-client.git + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 engn33r + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +websockets +BSD-3-Clause +https://github.com/python-websockets/websockets +Copyright (c) Aymeric Augustin and contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +widgetsnbextension +BSD License +http://jupyter.org +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +wrapt +BSD License +https://github.com/GrahamDumpleton/wrapt +Copyright (c) 2013-2023, Graham Dumpleton +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +xatlas +MIT License + + Copyright (c) 2021 Markus Worchel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +https://github.com/mworchel/xatlas-python +MIT License + +Copyright (c) 2021 Markus Worchel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +xattr +MIT +https://github.com/xattr/xattr +This is the MIT license. This software may also be distributed under the same terms as Python (the PSF license). + +Copyright (c) 2004 Bob Ippolito. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +xmltodict +MIT +https://github.com/martinblech/xmltodict +Copyright (C) 2012 Martin Blech and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +xxhash +BSD License +https://github.com/ifduyue/python-xxhash +Copyright (c) 2014-2024, Yue Du +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +yacs +Apache Software License +https://github.com/rbgirshick/yacs +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +yarl +Apache-2.0 +https://github.com/aio-libs/yarl + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +zarr +MIT +https://github.com/zarr-developers/zarr-python +The MIT License (MIT) + +Copyright (c) 2015-2025 Zarr Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +zipp +MIT +https://github.com/jaraco/zipp +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..563278ea --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Change log + +## Unreleased + +- New features + +- Breaking changes + +## 1.2.1 (May 08, 2026) + +- New features + - Add action policy post-training (SFT). See `docs/training.md` for usage. + +## 1.2.0 (May 05, 2026) + +- New features + - Add action modalities (Forward Dynamics, Inverse Dynamics, Policy) for Cosmos3-Nano model. + - Upgrade Cosmos3-Nano checkpoint to improve T2V, I2V quality. + +## 1.1.1 (May 01, 2026) + +- New features + - Add DCP checkpoint conversion/inference. + +## 1.1.0 (April 29, 2026) + +- New features + - Add Post-Training (Supervised Fine-Tuning). See `docs/training.md` for usage. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d4eaae27 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing + + + +______________________________________________________________________ + +**Table of Contents** + +- [Setup](#setup) +- [Test](#test) + - [Run Linting and Formatting](#run-linting-and-formatting) + - [Run Tests](#run-tests) + - [Run a Single Test](#run-a-single-test) +- [Code Reviews](#code-reviews) +- [Signing Your Work](#signing-your-work) + +______________________________________________________________________ + + + +We'd love to receive your patches and contributions. Please keep your PRs as draft until such time that you would like us to review them. + +## Setup + +Install system dependencies: + +[just](https://just.systems/man/en/pre-built-binaries.html#pre-built-binaries) + +```shell +uv tool install -U rust-just +``` + +To see all available `just` commands, run + +```shell +just +``` + +## Test + +### Run Linting and Formatting + +```shell +just lint +``` + +This will also run auto-fixes and linting. We recommend that you commit your changes first. + +### Run Tests + +```shell +just test +``` + +Test levels (`--levels`): + +0. Smoke tests. Requires >= 1 GPU. +1. Partial E2E tests. Requires >= 8 GPUs. +2. Full E2E tests. Requires >= 8 GPUs. + +Test outputs are saved to `outputs/pytest/`. To monitor a test, open `console.log`/`debug.log`. + +### Run a Single Test + +```shell +# List tests to get the test name +just test-list +# Run the test +just test-single [--pdb] +``` + +## Code Reviews + +All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. + +## Signing Your Work + +- We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. + + - Any contribution which contains commits that are not Signed-Off will not be accepted. + +- To sign off on a commit you simply use the `--signoff` (or `-s`) option when committing your changes: + + ```bash + git commit -s -m "Add cool feature." + ``` + + This will append the following to your commit message: + + ``` + Signed-off-by: Your Name + ``` + +- Full text of the DCO: + + ``` + Developer Certificate of Origin + Version 1.1 + + Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + 1 Letterman Drive + Suite D4700 + San Francisco, CA, 94129 + + Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + ``` + + ``` + Developer's Certificate of Origin 1.1 + + By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or + + (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or + + (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. + + (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. + ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..62cb8378 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Dockerfile using uv environment. + +ARG CUDA_VERSION=13.0.2 +ARG BASE_IMAGE=nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu24.04 +FROM ${BASE_IMAGE} + +# Set the DEBIAN_FRONTEND environment variable to avoid interactive prompts during apt operations. +ENV DEBIAN_FRONTEND=noninteractive + +# Install packages +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ffmpeg \ + git \ + git-lfs \ + tree \ + wget + +# Install uv: https://docs.astral.sh/uv/getting-started/installation/ +# https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile +COPY --from=ghcr.io/astral-sh/uv:0.10.8 /uv /uvx /usr/local/bin/ +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy +# Cache python downloads +ENV UV_PYTHON_CACHE_DIR=/root/.cache/uv/python + +# Install just: https://just.systems/man/en/pre-built-binaries.html +RUN curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin --tag 1.46.0 + +ENV PATH="/root/.local/bin:$PATH" + +WORKDIR /workspace + +# Install python +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=.python-version,target=.python-version \ + uv python install + +# Install into virtual environment +RUN echo "$CUDA_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/cu\1\2/' > /root/.cuda-name +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + --mount=type=bind,source=.python-version,target=.python-version \ + uv sync --locked --no-install-project --no-editable --all-extras --group=$(cat /root/.cuda-name) +ENV PATH="/workspace/.venv/bin:$PATH" + +# Triton bundled ptxas doesn't support latest GPU architectures +ENV TRITON_PTXAS_PATH="/usr/local/cuda/bin/ptxas" + +ENTRYPOINT ["/workspace/docker/entrypoint.sh"] + +CMD ["/bin/bash"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1bffec96 --- /dev/null +++ b/LICENSE @@ -0,0 +1,222 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +================================================================================ + THIRD-PARTY ATTRIBUTIONS +================================================================================ + +This product includes code adapted from HuggingFace Transformers +(https://github.com/huggingface/transformers), licensed under the Apache +License, Version 2.0. + + Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. + Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. + Copyright 2025 The Qwen Team and The HuggingFace Inc. team. + All rights reserved. + +The following files are adapted from HuggingFace Transformers: + + cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py + cosmos3/_src/vfm/models/llm/qwen3/qwen3.py + cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py + cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py + cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py diff --git a/ci/.link-check.json b/ci/.link-check.json new file mode 100644 index 00000000..3546a997 --- /dev/null +++ b/ci/.link-check.json @@ -0,0 +1,10 @@ +{ + "ignorePatterns": [ + { + "pattern": "localhost" + }, + { + "pattern": "^https://github-production-user-asset" + } + ] +} diff --git a/ci/.markdown-toc-creator.toml b/ci/.markdown-toc-creator.toml new file mode 100644 index 00000000..650901b0 --- /dev/null +++ b/ci/.markdown-toc-creator.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[tool.markdown_toc_creator] +proactive = false +exclude = '/_src/' +quiet = true diff --git a/ci/.pre-commit-config-base.yaml b/ci/.pre-commit-config-base.yaml new file mode 100644 index 00000000..3aaa7f72 --- /dev/null +++ b/ci/.pre-commit-config-base.yaml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + args: ['--maxkb=10000'] # 10MB + - id: forbid-submodules + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.0 + hooks: + - id: gitleaks diff --git a/ci/license.txt b/ci/license.txt new file mode 100644 index 00000000..8f179ca4 --- /dev/null +++ b/ci/license.txt @@ -0,0 +1,14 @@ +SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/ci/uv_lock.sh b/ci/uv_lock.sh new file mode 100755 index 00000000..3a6189c9 --- /dev/null +++ b/ci/uv_lock.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# + +# Generate uv lock files for projects. + +set -euo pipefail + +for file in "$@"; do + project_dir="$(dirname "$file")" + if ! uv lock -q --check --project "$project_dir" &>/dev/null; then + echo "Updating lock file for '$project_dir'" >&2 + uv lock -q --project "$project_dir" + fi +done diff --git a/ci/uv_lock_script.sh b/ci/uv_lock_script.sh new file mode 100755 index 00000000..83975315 --- /dev/null +++ b/ci/uv_lock_script.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# + +# Generate uv lock files for scripts. + +set -euo pipefail + +for file in "$@"; do + if head -n1 "$file" | grep -q '^#!/usr/bin/env -S uv run --script'; then + if ! uv lock -q --check --script "$file" &>/dev/null; then + echo "Updating lock file for '$file'" >&2 + uv lock -q --script "$file" + fi + fi +done diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..22543723 --- /dev/null +++ b/conftest.py @@ -0,0 +1,282 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.lazy_config import lazy_call + + +lazy_call._CONVERT_TARGET_TO_STRING = True + +import gc +import os +from functools import cache +from pathlib import Path + +import pytest + +from cosmos3.fixtures.args import ALL_LEVELS, ALL_NUM_GPUS, ALLOWED_GPUS_BY_LEVEL, Args, get_args, init_args + + +@pytest.fixture(scope="module") +def original_datadir(request: pytest.FixtureRequest) -> Path: + root_dir = request.config.rootpath + relative_path = request.path.with_suffix("").relative_to(root_dir) + return root_dir / "tests/data" / relative_path + + +@cache +def _get_available_gpus() -> int: + import pynvml + + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + pynvml.nvmlShutdown() + return device_count + except pynvml.NVMLError as e: + print(f"WARNING: Failed to get available GPUs: {e}") + return 0 + + +def pytest_addoption(parser: pytest.Parser): + parser.addoption("--manual", action="store_true", default=False, help="Run manual tests") + parser.addoption( + "--num-gpus", + default=None, + type=int, + choices=ALL_NUM_GPUS, + help="Run tests with the specified number of GPUs", + ) + parser.addoption("--levels", default=None, help="Run tests with the specified levels (comma-separated list)") + + +def pytest_xdist_auto_num_workers(config: pytest.Config) -> int | None: + num_gpus: int | None = config.option.num_gpus + if num_gpus is None: + return 1 + if num_gpus == 0: + return None + + available_gpus = _get_available_gpus() + if available_gpus < num_gpus: + raise ValueError(f"Not enough GPUs available. Required: {num_gpus}, Available: {available_gpus}") + return available_gpus // num_gpus + + +def pytest_configure(config: pytest.Config): + args = Args.from_config(config) + init_args(args) + + if ( + args.num_gpus is not None + and args.levels is not None + and all(args.num_gpus not in ALLOWED_GPUS_BY_LEVEL[level] for level in args.levels) + ): + pytest.exit(f"No tests for {args.num_gpus} GPUs and levels {args.levels}.", returncode=0) + + if args.worker_id == "master": + return + + if args.worker_index > 1: + if args.num_gpus is None: + raise NotImplementedError(f"Running parallel tests requires --num-gpus to be set.") + + # Check if there are enough GPUs available. + if args.num_gpus is not None and args.num_gpus > 0: + required_gpus = args.num_gpus * (args.worker_index + 1) + else: + required_gpus = 1 + available_gpus = _get_available_gpus() + if available_gpus < required_gpus: + raise ValueError(f"Not enough GPUs available. Required: {required_gpus}, Available: {available_gpus}") + + # Limit threading to reduce contention + import torch + + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + + +def _get_marker(item: pytest.Item, name: str) -> pytest.Mark | None: + markers = list(item.iter_markers(name=name)) + if not markers: + return None + marker = markers[0] + for other_marker in markers[1:]: + if other_marker != marker: + raise ValueError(f"Multiple different markers found for {name}: {markers}") + return marker + + +def _parse_level_marker(mark: pytest.Mark) -> int: + if len(mark.args) != 1: + raise ValueError(f"Invalid arguments: {mark.args}") + if mark.kwargs: + raise ValueError(f"Invalid keyword arguments: {mark.kwargs}") + level = mark.args[0] + if level not in ALL_LEVELS: + raise ValueError(f"Invalid level {level} not in {ALL_LEVELS}") + return level + + +def _parse_gpus_marker(mark: pytest.Mark) -> int: + if len(mark.args) != 1: + raise ValueError(f"Invalid arguments: {mark.args}") + if mark.kwargs: + raise ValueError(f"Invalid keyword arguments: {mark.kwargs}") + required_gpus = int(mark.args[0]) + if required_gpus not in ALL_NUM_GPUS: + raise ValueError(f"Invalid number of GPUs {required_gpus} not in {ALL_NUM_GPUS}") + return required_gpus + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): + args = get_args() + + for item in items: + manual_mark = _get_marker(item, "manual") + level_mark = _get_marker(item, "level") + gpus_mark = _get_marker(item, "gpus") + try: + level = _parse_level_marker(level_mark) if level_mark else 0 + gpus = _parse_gpus_marker(gpus_mark) if gpus_mark else 0 + except ValueError as e: + pytest.fail(f"Invalid marker on test {item.name}: {e}") + assert False, "unreachable" + + allowed_gpus = ALLOWED_GPUS_BY_LEVEL[level] + if gpus not in allowed_gpus: + pytest.fail(f"Level {level} tests must have {allowed_gpus} GPUs, but {item.name} has {gpus} GPUs") + + # Check if the test should be skipped + if not args.enable_manual and manual_mark is not None: + item.add_marker(pytest.mark.skip(reason="test requires --manual")) + if args.levels is not None and level not in args.levels: + item.add_marker(pytest.mark.skip(reason=f"test requires --levels={level}")) + if args.num_gpus is not None and gpus != args.num_gpus: + item.add_marker(pytest.mark.skip(reason=f"test requires --num-gpus={gpus}")) + available_gpus = _get_available_gpus() + if gpus > available_gpus: + item.add_marker( + pytest.mark.skip(reason=f"test requires {gpus} GPUs, but only {available_gpus} are available") + ) + + # Exclude skipped tests + selected_items = [] + deselected_items = [] + for item in items: + if item.get_closest_marker("skip"): + deselected_items.append(item) + continue + selected_items.append(item) + items[:] = selected_items + config.hook.pytest_deselected(items=deselected_items) + + +def pytest_runtest_setup(item: pytest.Item): + import torch + + args = get_args() + + gpus_mark = item.get_closest_marker(name="gpus") + try: + gpus = _parse_gpus_marker(gpus_mark) if gpus_mark else 0 + except ValueError as e: + pytest.fail(f"Invalid marker on test {item.name}: {e}") + assert False, "unreachable" + + # Limit the number of GPUs used by the test + if gpus > 0: + device_start = args.worker_index * gpus + device_end = device_start + gpus + os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, range(device_start, device_end))) + os.environ["NUM_GPUS"] = str(gpus) + else: + device = 0 + os.environ["CUDA_VISIBLE_DEVICES"] = str(device) + os.environ["NUM_GPUS"] = "1" + + test_max_processes = int(os.environ.get("TEST_MAX_PROCESSES", "8")) + device_memory_fraction = 1 / max(args.worker_count, test_max_processes) + os.environ["DEVICE_MEMORY_FRACTION"] = str(device_memory_fraction) + torch.cuda.set_per_process_memory_fraction(device_memory_fraction) + + +@pytest.fixture(autouse=True) +def init_cosmos_test(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + from cosmos3.common.init import _init_log_console, _init_log_files + + monkeypatch.setenv("IMAGINAIRE_OUTPUT_ROOT", str(tmp_path / "imaginaire4-output")) + + _init_log_console() + _init_log_files(tmp_path) + + yield + + +@pytest.fixture(autouse=True) +def init_torch_test(): + import torch + + from cosmos3.common.init import set_seed + + # Reproducibility + set_seed(0) + + yield + + # Cleanup memory + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + + +_WHITELIST_ENV_VARS = { + "LD_LIBRARY_PATH", + "QT_QPA_FONTDIR", + "QT_QPA_PLATFORM_PLUGIN_PATH", + "TORCHINDUCTOR_CACHE_DIR", +} + + +@pytest.fixture(autouse=True) +def detect_env_modifications(): + original_env = dict(os.environ) + + yield + + new_env = dict(os.environ) + + for env in [original_env, new_env]: + for k in list(env.keys()): + if k.startswith("PYTEST_") or k in _WHITELIST_ENV_VARS: + del env[k] + if new_env != original_env: + added, removed, modified = _compare_dict(new_env, original_env) + os.environ.clear() + os.environ.update(original_env) + raise ValueError( + f"Environment variables modified by test! Use 'monkeypatch.setenv' to temporarily modify environment variables. \n" + f"Added: {added}\n" + f"Removed: {removed}\n" + f"Modified: {modified}" + ) + + +def _compare_dict(actual: dict[str, str], expected: dict[str, str]) -> tuple[set[str], set[str], set[str]]: + added = set(actual) - set(expected) + removed = set(expected) - set(actual) + modified = {k for k in expected if k in actual and expected[k] != actual[k]} + return added, removed, modified diff --git a/cosmos3/__init__.py b/cosmos3/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/__init__.py b/cosmos3/_src/imaginaire/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/attention/README.md b/cosmos3/_src/imaginaire/attention/README.md new file mode 100644 index 00000000..750e7c32 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/README.md @@ -0,0 +1,47 @@ +# Imaginaire Attention Subpackage + +A subpackage within cosmos3._src.imaginaire that integrates only the best and most reliable +solutions, and provides simple APIs to end-users. + +For more information, please refer to the [docs](docs/). + +## Basic API + +```python +from cosmos3._src.imaginaire.attention import attention + +output = attention( + query=query, + key=key, + value=value, +) +``` + +- **Optional** `scale`: attention (softmax/dot product) scale. Defaults to `head_dim ** -0.5`. +- **Optional** `return_lse`: returns logsumexp if `True` +- **Optional** `backend`: explicitly set backend instead of automatically selecting the best compatible + +## Tensor layouts + +Imaginaire Attention only supports one tensor memory layout: +heads-last torch contiguous (`torch.contiguous_format`). + +With this layout, input tensors `query`, `key`, and `value` are represented as rank-4 tensors, with +dimension 0 representing batch, dimension 1 representing sequence length, dimension 2 representing +attention heads, and dimension 3 representing head dimension. +This layout is also consistent with the `contiguous_format` memory layout in PyTorch, meaning the +right-most dimension (head dimension) is the major dimension (has stride 1), and tokens from +different heads are interleaved. + +```python +def verify_heads_last_contig_tensor(x: Tensor): + assert x.shape[0] == batch + assert x.shape[1] == seqlen + assert x.shape[2] == heads + assert x.shape[3] == head_dim + + assert x.stride(3) == 1 + assert x.stride(2) == head_dim + assert x.stride(1) == heads * head_dim + assert x.stride(0) == heads * head_dim * seqlen +``` diff --git a/cosmos3/_src/imaginaire/attention/__init__.py b/cosmos3/_src/imaginaire/attention/__init__.py new file mode 100644 index 00000000..72d51932 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/__init__.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + + +""" + +from cosmos3._src.imaginaire.attention.frontend import ( + attention, + merge_attentions, + multi_dimensional_attention, + multi_dimensional_attention_varlen, + spatio_temporal_attention, +) + +__all__ = [ + "attention", + "multi_dimensional_attention", + "multi_dimensional_attention_varlen", + "spatio_temporal_attention", + "merge_attentions", +] diff --git a/cosmos3/_src/imaginaire/attention/backends.py b/cosmos3/_src/imaginaire/attention/backends.py new file mode 100644 index 00000000..bac2e441 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/backends.py @@ -0,0 +1,418 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Frontend APIs +""" + +import torch + +from cosmos3._src.imaginaire.attention.flash2.checks import flash2_attention_check +from cosmos3._src.imaginaire.attention.flash3.checks import flash3_attention_check +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.natten.checks import natten_attention_check, natten_multi_dim_attention_check +from cosmos3._src.imaginaire.attention.utils import get_arch_tag +from cosmos3._src.imaginaire.attention.utils.environment import ( + filter_attention_backends, + filter_multi_dim_attention_backends, +) +from cosmos3._src.imaginaire.attention.utils.safe_ops import log +from cosmos3._src.imaginaire.attention.utils.safe_ops.functools import lru_cache + + +BACKEND_CHECK_MAP = { + "natten": natten_attention_check, + "flash2": flash2_attention_check, + "flash3": flash3_attention_check, +} + +BACKEND_MULTI_DIM_CHECK_MAP = { + "natten": natten_multi_dim_attention_check, +} + + +def is_backend_compatible( + backend: str, + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + causal_type: CausalType | None, + is_varlen: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function a specified backend. + Runs the common and backend-specific checks. Returns False if any checks fail, otherwise True. + + Parameters: + backend (str): selected backend. + + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with the backend. + + """ + if backend is None: + raise ValueError("Cannot pass None backend to is_backend_compatible.") + + if backend not in BACKEND_CHECK_MAP: + raise ValueError(f"Unrecognized backend name {backend}.") + + return BACKEND_CHECK_MAP[backend]( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=raise_error, + ) + + +def get_backend_list(arch_tag: int) -> list[str]: + """ + Returns list of supported backends according to arch tag (attention.utils.get_arch_tag). + Backends are ordered based on their known performance levels, so that the best-performing + compatible backend is selected. + + The returned list can be filtered via environment variable. + See `filter_attention_backends` for details. + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + backend_list (list[str]): a list of backend names (string). Empty if device is not supported. + + """ + + if arch_tag < 75: + log.debug(f"Minimum architecture supported for Attention is 75, got {arch_tag=}.") + return [] + + default_backends = [] + if arch_tag == 90: + default_backends = [ + "flash3", + "natten", + "flash2", + ] + elif arch_tag in [100, 103]: + default_backends = [ + "natten", + "flash2", + ] + elif arch_tag >= 80: + default_backends = [ + "flash2", + "natten", + ] + else: + default_backends = ["natten"] + + # Apply environment variable filtering + return filter_attention_backends(default_backends) + + +@lru_cache +def choose_backend( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + causal_type: CausalType | None, + is_varlen: bool, + deterministic: bool = False, + backend: str | None = None, + raise_error: bool = True, +) -> str | None: + """ + Selects a compatible backend, unless one is already selected, which runs its corresponding + checks. + + Parameters: + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + backend (str | None): selected backend, if any. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is **True**. + + Returns: + backend (str | None): selected backend, or None if no backends are compatible. + + """ + if backend is not None: + if is_backend_compatible( + backend=backend, + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=raise_error, + ): + return backend + return None + + arch_tag = get_arch_tag(device) + backend_list = get_backend_list(arch_tag) + for backend in backend_list: + if is_backend_compatible( + backend=backend, + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=False, + ): + return backend + + if not raise_error: + return None + + raise ValueError( + "Could not find a compatible Attention backend for this use case / device. " + "Try running with debug logs to find out why." + ) + + +def is_multi_dim_backend_compatible( + backend: str, + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function a specified multi-dimensional backend. + Runs the common and backend-specific checks. Returns False if any checks fail, otherwise True. + + Parameters: + backend (str): selected backend. + + query_shape (torch.Size): Shape of 4-D, 5-D, or 6-D query tensor (`[batch, *token_layout_shape, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D, 5-D, or 6-D key tensor (`[batch, *token_layout_shape, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D, 5-D, or 6-D value tensor (`[batch, *token_layout_shape, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with the backend. + + """ + if backend is None: + raise ValueError("Cannot pass None backend to is_backend_compatible.") + + if backend not in BACKEND_MULTI_DIM_CHECK_MAP: + raise ValueError(f"Unrecognized backend name {backend}.") + + return BACKEND_MULTI_DIM_CHECK_MAP[backend]( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + deterministic=deterministic, + raise_error=raise_error, + ) + + +def get_multi_dim_backend_list(arch_tag: int) -> list[str]: + """ + Returns list of supported multi-dimensional backends according to arch tag (attention.utils.get_arch_tag). + Backends are ordered based on their known performance levels, so that the best-performing + compatible backend is selected. + + The returned list can be filtered via environment variable. + See `filter_multi_dim_attention_backends` for details. + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + backend_list (list[str]): a list of backend names (string). Empty if device is not supported. + + """ + + if arch_tag < 75: + log.debug(f"Minimum architecture supported for Multi-Dimensional Attention is 75, got {arch_tag=}.") + return [] + + # NATTEN is the only supported backend for now + default_backends = ["natten"] + + # Apply environment variable filtering + return filter_multi_dim_attention_backends(default_backends) + + +@lru_cache +def choose_multi_dim_backend( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + deterministic: bool = False, + backend: str | None = None, +) -> str: + """ + Selects a compatible multi-dimensional backend, unless one is already selected, which runs its + corresponding checks. + + Parameters: + query_shape (torch.Size): Shape of 4-D, 5-D, or 6-D query tensor (`[batch, *token_layout_shape, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D, 5-D, or 6-D key tensor (`[batch, *token_layout_shape, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D, 5-D, or 6-D value tensor (`[batch, *token_layout_shape, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + deterministic (bool): Deterministic backward pass required. + + backend (str | None): selected backend, if any. + + Returns: + backend (str): selected backend. + + """ + if backend is not None: + assert is_multi_dim_backend_compatible( + backend=backend, + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + deterministic=deterministic, + raise_error=True, + ) + return backend + + arch_tag = get_arch_tag(device) + backend_list = get_multi_dim_backend_list(arch_tag) + for backend in backend_list: + if is_multi_dim_backend_compatible( + backend=backend, + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + deterministic=deterministic, + raise_error=False, + ): + return backend + + raise ValueError( + "Could not find a compatible Multi-Dimensional Attention backend for this use case / device. " + "Try running with debug logs to find out why." + ) diff --git a/cosmos3/_src/imaginaire/attention/checks.py b/cosmos3/_src/imaginaire/attention/checks.py new file mode 100644 index 00000000..0fb4f568 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/checks.py @@ -0,0 +1,634 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Common, op-specific, and backend-specific checks +""" + +from collections.abc import Sequence +from functools import partial +from typing import Any + +import torch +from torch import Tensor + +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.utils import log_or_raise_error +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling +from cosmos3._src.imaginaire.attention.varlen import generate_varlen_parameters + + +def universal_tensor_checks( + query: Tensor, key: Tensor, value: Tensor, raise_error: bool = True +) -> bool: # query/key/value: [B,*,H,D] + """ + Universal tensor validation: checks sparse/nested tensors and ensures device/dtype consistency. + This should be called by users before extracting tensor properties for tensorless APIs. + + Parameters: + query (Tensor): Query tensor. + key (Tensor): Key tensor. + value (Tensor): Value tensor. + raise_error (bool): Whether to raise an error if checks fail. Default is True. + + Returns: + success (bool): Whether all checks pass. + """ + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + if query.is_sparse or key.is_sparse or value.is_sparse: + target_fn("This operation does not support sparse tensors.", exception=NotImplementedError) + return False + + if query.is_nested or key.is_nested or value.is_nested: + target_fn("This operation does not support nested tensors.", exception=NotImplementedError) + return False + + if query.device != key.device or query.device != value.device: + target_fn( + f"Query, key, and value must be on the same device, got {query.device=}, {key.device=}, {value.device=}.", + exception=ValueError, + ) + return False + + if query.dtype != key.dtype or query.dtype != value.dtype: + target_fn( + f"Query, key, and value must assume the same data type, got {query.dtype=}, {key.dtype=}, {value.dtype=}.", + exception=ValueError, + ) + return False + + return True + + +def assert_universal_tensor_checks(query: Tensor, key: Tensor, value: Tensor) -> None: # query/key/value: [B,*,H,D] + """ + Universal tensor validation using assertions for backend functions. + Checks sparse/nested tensors and ensures device/dtype/requires_grad consistency. + + This is intended for internal backend use only. Users should not call backend functions directly. + Assertions are disabled in production (-O flag), so this is appropriate for post-frontend checks. + + Parameters: + query (Tensor): Query tensor. + key (Tensor): Key tensor. + value (Tensor): Value tensor. + """ + assert not query.is_sparse and not key.is_sparse and not value.is_sparse, "Sparse tensors not supported" + assert not query.is_nested and not key.is_nested and not value.is_nested, "Nested tensors not supported" + assert query.device == key.device == value.device, ( + f"Device mismatch: {query.device=}, {key.device=}, {value.device=}" + ) + assert query.dtype == key.dtype == value.dtype, f"Dtype mismatch: {query.dtype=}, {key.dtype=}, {value.dtype=}" + # Disabled: requires_grad may differ if differentiable queries attend to non-differentiable + # keys, e.g. when attending to a KV-cache during training. + # assert query.requires_grad == key.requires_grad == value.requires_grad, ( + # f"requires_grad mismatch: {query.requires_grad=}, {key.requires_grad=}, {value.requires_grad=}" + # ) + + +def _universal_attention_checks( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + requires_grad: bool, + supported_dtypes_forward: list[torch.dtype] | None = None, + supported_dtypes_backward: list[torch.dtype] | None = None, + supports_mla: bool = True, + supports_gqa_mqa: bool = True, + raise_error: bool = True, + backend_name: str | None = None, +) -> bool: + backend_name = backend_name or "Attention" + + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + query_dim = len(query_shape) + key_dim = len(key_shape) + value_dim = len(value_shape) + + if query_dim != key_dim or query_dim != value_dim: + target_fn( + f"Q, K, and V must have the same rank, got {query_dim=}, {key_dim=}, {value_dim=}.", + exception=ValueError, + ) + return False + + if query_shape[0] != key_shape[0] or query_shape[0] != value_shape[0]: + target_fn( + f"Q, K, and V must match in batch size, got {query_shape[0]=}, {key_shape[0]=}, {value_shape[0]=}.", + exception=ValueError, + ) + return False + + if query_shape[-1] != key_shape[-1]: + target_fn( + f"Q and K head dims must match, got {query_shape[-1]=}, {key_shape[-1]=}.", + exception=ValueError, + ) + return False + + if key_shape[-2] != value_shape[-2]: + target_fn( + f"K and V must always have the same number of heads, got {key_shape[-2]=}, {value_shape[-2]=}.", + exception=ValueError, + ) + return False + + if not supports_mla and query_shape[-1] != value_shape[-1]: + target_fn( + f"{backend_name} does not support different head dims for QK and V, got " + f"{query_shape[-1]=}, {value_shape[-1]=}.", + exception=ValueError, + ) + return False + + if not supports_gqa_mqa and (query_shape[-2] != key_shape[-2] or query_shape[-2] != value_shape[-2]): + target_fn( + f"{backend_name} does not support GQA/MQA, therefore number of heads in Q, K, and V " + f"must match, got {query_shape[-2]=}, {key_shape[-2]=}, {value_shape[-2]=}.", + exception=ValueError, + ) + return False + + if supports_gqa_mqa: + heads_q = query_shape[-2] + heads_kv = key_shape[-2] + + if heads_q < heads_kv or heads_q % heads_kv != 0: + target_fn( + f"KV heads must evenly divide Q heads, got {heads_q=}, {heads_kv=}.", + exception=ValueError, + ) + return False + + # Caller must ensure dtype consistency via universal_tensor_checks + if supported_dtypes_forward is not None and dtype not in supported_dtypes_forward: + target_fn( + f"{backend_name} does not support forward pass (inference) with data type {dtype}; " + f"supported dtypes: {supported_dtypes_forward}.", + exception=ValueError, + ) + return False + + if supported_dtypes_backward is not None and requires_grad and dtype not in supported_dtypes_backward: + target_fn( + f"{backend_name} does not support backward pass (training) with data type {dtype}; " + f"supported dtypes: {supported_dtypes_backward}.", + exception=ValueError, + ) + return False + + return True + + +def attention_tensor_checks( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + requires_grad: bool, + supported_dtypes_forward: list[torch.dtype] | None = None, + supported_dtypes_backward: list[torch.dtype] | None = None, + supports_mla: bool = True, + supports_gqa_mqa: bool = True, + raise_error: bool = True, + backend_name: str | None = None, +) -> bool: + backend_name = backend_name or "Attention" + + if not _universal_attention_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=supported_dtypes_forward, + supported_dtypes_backward=supported_dtypes_backward, + supports_mla=supports_mla, + supports_gqa_mqa=supports_gqa_mqa, + raise_error=raise_error, + backend_name=backend_name, + ): + return False + + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + query_dim = len(query_shape) + if query_dim != 4: + target_fn( + f"Attention expects 4-D tensors as inputs, got {query_dim=}.", + exception=ValueError, + ) + return False + + if key_shape[1] != value_shape[1]: + target_fn( + f"K and V must match in sequence length, got {key_shape[1]=}, {value_shape[1]=}.", + exception=ValueError, + ) + return False + + return True + + +def varlen_tensor_checks( + query: Tensor, # [1,S_total_Q,H,D] + key: Tensor, # [1,S_total_KV,H_KV,D] + value: Tensor, # [1,S_total_KV,H_KV,D_V] + seqlens_Q: Tensor | None = None, # [B] + seqlens_KV: Tensor | None = None, # [B] + cumulative_seqlen_Q: Tensor | None = None, # [B+1] + cumulative_seqlen_KV: Tensor | None = None, # [B+1] + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, +) -> ( + tuple[None, None, int, int] | tuple[Tensor, Tensor, int, int] +): # (cumseqlen_Q[B+1], cumseqlen_KV[B+1], max_seqlen_Q, max_seqlen_KV) + if query.shape[0] != key.shape[0] or query.shape[0] != value.shape[0]: + raise ValueError( + f"Q, K, and V must match in batch size, got {query.shape[0]=}, {key.shape[0]=}, {value.shape[0]=}." + ) + + + if not is_torch_compiling(): + # Validate max_seqlen values: neither can be negative, and they must be + # both zero/None (not varlen) or both positive (varlen). + if (max_seqlen_Q is not None and max_seqlen_Q < 0) or (max_seqlen_KV is not None and max_seqlen_KV < 0): + raise ValueError( + f"max_seqlen_Q and max_seqlen_KV cannot be negative, got {max_seqlen_Q=}, {max_seqlen_KV=}." + ) + + if (max_seqlen_Q == 0) != (max_seqlen_KV == 0): + raise ValueError( + "max_seqlen_Q and max_seqlen_KV must either both be 0/None (not varlen) or both be positive " + f"(varlen), got {max_seqlen_Q=}, {max_seqlen_KV=}." + ) + + if all( + x is None + for x in [ + seqlens_Q, + seqlens_KV, + cumulative_seqlen_Q, + cumulative_seqlen_KV, + ] + ) and all( + x is None or x == 0 + for x in [ + max_seqlen_Q, + max_seqlen_KV, + ] + ): + # Not varlen + return None, None, 0, 0 + + if seqlens_Q is not None or seqlens_KV is not None: + # Generate cumulative_seqlen_{Q,KV}, max_seqlen_{Q,KV}, total_seqlen_{Q,KV} + # based on user input + return generate_varlen_parameters( + query=query, + key=key, + value=value, + seqlens_Q=seqlens_Q, + seqlens_KV=seqlens_KV, + ) + + # Validate user-input cumulative_seqlen_{Q,KV}, max_seqlen_{Q,KV}, total_seqlen_{Q,KV} + + # Mismatch (one 0, the other positive) is already caught by the early check above. + # This feature may require support in the backends themselves; see NATTEN PR: + # https://github.com/SHI-Labs/NATTEN/pull/327 + if any( + x is None + for x in [ + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, + ] + ): + raise ValueError( + "Variable length Attention requires all of cumulative_seqlen_{Q,KV} and max_seqlen_{Q,KV} to be set." + ) + + if query.shape[0] != 1: + raise ValueError( + f"Variable length Attention only supports sequence-packed memory layout (batch = 1), got {query.shape[0]=}." + ) + + assert cumulative_seqlen_Q is not None + assert cumulative_seqlen_KV is not None + assert max_seqlen_Q is not None + assert max_seqlen_KV is not None + + if not isinstance(max_seqlen_Q, int) or not isinstance(max_seqlen_KV, int): + raise ValueError( + f"max_seqlen_Q and max_seqlen_KV must be ints, got {type(max_seqlen_Q)=}, {type(max_seqlen_KV)=}." + ) + + total_seqlen_Q = query.shape[1] + total_seqlen_KV = key.shape[1] + + + if not is_torch_compiling(): + # When both max_seqlens are 0, skip bounds checks (skip kernel / empty-batch case). + # Mismatch is already caught by the early check, so at this point either both are 0 or both are positive. + if max_seqlen_Q > 0 or max_seqlen_KV > 0: + if max_seqlen_Q > total_seqlen_Q: + raise ValueError( + f"Maximum sequence length cannot exceed total, got {max_seqlen_Q=}, {total_seqlen_Q=}." + ) + + if max_seqlen_KV > total_seqlen_KV: + raise ValueError( + f"Maximum sequence length cannot exceed total, got {max_seqlen_KV=}, {total_seqlen_KV=}." + ) + + if max_seqlen_Q < 1 or max_seqlen_KV < 1: + raise ValueError( + f"Maximum sequence length cannot be less than 1, got {max_seqlen_Q=}, {max_seqlen_KV=}." + ) + + if not isinstance(cumulative_seqlen_Q, Tensor) or not isinstance(cumulative_seqlen_KV, Tensor): + raise ValueError("cumulative_seqlen_Q and cumulative_seqlen_KV must both be tensors.") + + if cumulative_seqlen_Q.device != query.device or cumulative_seqlen_KV.device != query.device: + raise ValueError( + "cumulative_seqlen_Q and cumulative_seqlen_KV must be on the same device as QKV, but " + f"{cumulative_seqlen_Q.device=}, {cumulative_seqlen_KV.device=}, {query.device=}." + ) + + if cumulative_seqlen_Q.dtype != torch.int32 or cumulative_seqlen_KV.dtype != torch.int32: + raise ValueError( + "cumulative_seqlen_Q and cumulative_seqlen_KV must both be torch.int32 tensors, got " + f"{cumulative_seqlen_Q.dtype=}, {cumulative_seqlen_KV.dtype=}." + ) + + if cumulative_seqlen_Q.dim() != 1 or cumulative_seqlen_KV.dim() != 1: + raise ValueError( + "cumulative_seqlen_Q and cumulative_seqlen_KV must both be 1-D tensors, got " + f"{cumulative_seqlen_Q.dim()=}, {cumulative_seqlen_KV.dim()=}." + ) + + if cumulative_seqlen_Q.shape[0] != cumulative_seqlen_KV.shape[0]: + raise ValueError( + "cumulative_seqlen_Q and cumulative_seqlen_KV must match in size, got " + f"{cumulative_seqlen_Q.shape=}, {cumulative_seqlen_KV.shape=}." + ) + + if cumulative_seqlen_Q.shape[0] < 2: + raise ValueError( + "cumulative_seqlen_Q and cumulative_seqlen_KV must contain at least 2 elements, got " + f"{cumulative_seqlen_Q.shape=}, {cumulative_seqlen_KV.shape=}." + ) + + return ( + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, + ) + + +def attention_param_checks( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + is_causal: bool, + causal_type: CausalType, +): + if is_causal and (causal_type is None or not isinstance(causal_type, CausalType)): + raise ValueError( + f"Argument causal_type must be specified as an enum instance of CausalType when is_causal=True, got {causal_type=}." + ) + + assert len(query_shape) == len(key_shape) == len(value_shape) == 4 + assert key_shape[1] == value_shape[1] + if is_causal and causal_type == CausalType.DontCare and query_shape[1] != key_shape[1]: + raise ValueError( + "Causal mask type DontCare is only valid when seqlen_q == seqlen_kv, got " + f"{query_shape[1]=}, {key_shape[1]=}." + ) + + +def multi_dim_attention_tensor_checks( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + requires_grad: bool, + supported_dtypes_forward: list[torch.dtype] | None = None, + supported_dtypes_backward: list[torch.dtype] | None = None, + supports_mla: bool = True, + supports_gqa_mqa: bool = True, + raise_error: bool = True, + backend_name: str | None = None, +) -> bool: + backend_name = backend_name or "Multi-Dimensional Attention" + + if not _universal_attention_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=supported_dtypes_forward, + supported_dtypes_backward=supported_dtypes_backward, + supports_mla=supports_mla, + supports_gqa_mqa=supports_gqa_mqa, + raise_error=raise_error, + backend_name=backend_name, + ): + return False + + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + query_dim = len(query_shape) + if query_dim not in [4, 5, 6]: + target_fn( + f"Multi-Dimensional Attention supports 4-D, 5-D, or 6-D tensors as inputs, got {query_dim=}.", + exception=ValueError, + ) + return False + + num_dims = query_dim - 3 # minus batch, heads, head_dim + + q_token_layout_shape = query_shape[1 : 1 + num_dims] + k_token_layout_shape = key_shape[1 : 1 + num_dims] + v_token_layout_shape = value_shape[1 : 1 + num_dims] + + if q_token_layout_shape != k_token_layout_shape or q_token_layout_shape != v_token_layout_shape: + target_fn( + "Q, K and V must match in their token layout shapes in multi-dimensional attention, " + f"got {q_token_layout_shape=}, {k_token_layout_shape=}, {v_token_layout_shape=}.", + exception=ValueError, + ) + return False + + return True + + +def check_valid_tuple_or_element( + param: Any, num_dims: int, typename: type, raise_error: bool = False, param_name: str = "unknown" +) -> tuple | None: + if isinstance(param, typename): + return tuple(param for _ in range(num_dims)) + + if isinstance(param, Sequence) and len(param) == num_dims and all(isinstance(x, typename) for x in param): + return tuple(x for x in param) + + if raise_error: + raise ValueError(f"Invalid value for parameter {param_name}: {param}.") + return None + + +def multi_dim_attention_param_filter_tensorless( + token_layout_shape: tuple, + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + is_causal: tuple | bool = False, +) -> tuple[tuple, tuple, tuple, tuple]: + """ + Converts all multi-dimensional parameters to standard types. + """ + + if not isinstance(token_layout_shape, tuple) or any(not isinstance(x, int) for x in token_layout_shape): + raise ValueError(f"token_layout_shape must be an integer tuple, got {token_layout_shape=}.") + + num_dims = len(token_layout_shape) + assert num_dims in [1, 2, 3] + + window_size_ = check_valid_tuple_or_element(window_size, num_dims, int) + if window_size_ is None: + raise ValueError( + f"Parameter 'window_size' must be either an int or tuple of {num_dims} ints, got {window_size=}." + ) + + stride_ = check_valid_tuple_or_element(stride, num_dims, int) + if stride_ is None: + raise ValueError(f"Parameter 'stride' must be either an int or tuple of {num_dims} ints, got {stride=}.") + + dilation_ = check_valid_tuple_or_element(dilation, num_dims, int) + if dilation_ is None: + raise ValueError(f"Parameter 'dilation' must be either an int or tuple of {num_dims} ints, got {dilation=}.") + + is_causal_ = check_valid_tuple_or_element(is_causal, num_dims, bool) + if is_causal_ is None: + raise ValueError( + f"Parameter 'is_causal' must be either a boolean or tuple of {num_dims} booleans, got {is_causal=}." + ) + + # Map -1 windows to corresponding size in token layout + window_size_ = tuple(w if w != -1 else x for x, w in zip(token_layout_shape, window_size_)) + + return window_size_, stride_, dilation_, is_causal_ + + +def multi_dim_attention_param_checks_tensorless( + token_layout_shape: tuple, + window_size: tuple, + stride: tuple, + dilation: tuple, + is_causal: tuple, +): + """ + Validates multi-dimensional parameters. + """ + + if not isinstance(token_layout_shape, tuple) or any(not isinstance(x, int) for x in token_layout_shape): + raise ValueError(f"token_layout_shape must be an integer tuple, got {token_layout_shape=}.") + + num_dims = len(token_layout_shape) + assert num_dims in [1, 2, 3] + + if any(x <= 1 for x in token_layout_shape): + raise ValueError(f"Token layout dimensions must all be >= 2, got {token_layout_shape=}.") + + if any(w <= 1 for w in window_size): + raise ValueError( + "Parameter 'window_size' must be either -1 (no sparsity) or >= 2 along every dimension, " + f"got {window_size=}." + ) + + if any(w * d > x for x, w, d in zip(token_layout_shape, window_size, dilation)): + raise ValueError( + "The product of 'window_size' and 'dilation' cannot be greater than the input " + f"(token layout shape), got {window_size=}, {dilation=}, {token_layout_shape=}." + ) + + if any(s < 1 for s in stride): + raise ValueError(f"Parameter 'stride' allows positive integers only, got {stride=}.") + + if any(s > w for w, s in zip(window_size, stride)): + raise ValueError( + f"Parameter 'stride' cannot be greater than window size along any dimension, got {window_size=}, {stride=}." + ) + + if any(d < 1 for d in dilation): + raise ValueError(f"Parameter 'dilation' allows positive integers only, got {dilation=}.") + + +def multi_dim_attention_param_filter( + query: Tensor, # [B,*token_layout_shape,H,D] + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + is_causal: tuple | bool = False, +) -> tuple[tuple, tuple, tuple, tuple, tuple]: + """ + Converts all multi-dimensional parameters to standard types. + """ + assert query.dim() in [4, 5, 6] + num_dims = query.dim() - 3 + token_layout_shape = tuple(s for s in query.shape[1 : 1 + num_dims]) + + window_size_, stride_, dilation_, is_causal_ = multi_dim_attention_param_filter_tensorless( + token_layout_shape=token_layout_shape, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) + + return token_layout_shape, window_size_, stride_, dilation_, is_causal_ + + +def multi_dim_attention_param_checks( + query: Tensor, # [B,*token_layout_shape,H,D] + window_size: tuple, + stride: tuple, + dilation: tuple, + is_causal: tuple, +): + """ + Validates multi-dimensional parameters. + """ + assert query.dim() in [4, 5, 6] + num_dims = query.dim() - 3 + token_layout_shape = tuple(s for s in query.shape[1 : 1 + num_dims]) + + multi_dim_attention_param_checks_tensorless( + token_layout_shape=token_layout_shape, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) diff --git a/cosmos3/_src/imaginaire/attention/docs/README.md b/cosmos3/_src/imaginaire/attention/docs/README.md new file mode 100644 index 00000000..d72e7535 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/docs/README.md @@ -0,0 +1,10 @@ +# Imaginaire Attention Subpackage Docs + +- [Basic API & Intro](../README.md) +- Docs (you are here) + - [Backends](backends.md) + - Features + - [Basic features](features.md) + - [Multi-dimensional Attention](multi-dim.md) + - [Spatio-Temporal Attention](multi-dim.md#spatio-temporal-attention) + - [APIs](apis.md) diff --git a/cosmos3/_src/imaginaire/attention/docs/apis.md b/cosmos3/_src/imaginaire/attention/docs/apis.md new file mode 100644 index 00000000..ad8dafe9 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/docs/apis.md @@ -0,0 +1,28 @@ +# Imaginaire Attention Subpackage Docs > APIs + +## Attention + +::: cosmos3._src.imaginaire.attention + options: + heading_level: 3 + show_object_full_path: true + members: + - attention + +## Multi-Dimensional Attention + +::: cosmos3._src.imaginaire.attention + options: + heading_level: 3 + show_object_full_path: true + members: + - multi_dimensional_attention + +### Spatio-Temporal Attention + +::: cosmos3._src.imaginaire.attention + options: + heading_level: 3 + show_object_full_path: true + members: + - spatio_temporal_attention diff --git a/cosmos3/_src/imaginaire/attention/docs/backends.md b/cosmos3/_src/imaginaire/attention/docs/backends.md new file mode 100644 index 00000000..ba3f5d88 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/docs/backends.md @@ -0,0 +1,61 @@ +# Imaginaire Attention Subpackage Docs > Backends + +The goal is to support as many stable and reliable backends as possible, both for feature coverage, +and for delivering the best performance. + +## NATTEN + +[NATTEN](https://natten.org) ships standard Attention kernels in addition to sparse / +multi-dimensional kernels. + +Minimum version required: `0.21.5.dev3`. + +### Feature coverage + +| Feat/Backend | Ampere/RTX | Hopper | Blackwell | +| ------------ | ------------------ | ------ | ------------------ | +| Causal mask | :white_check_mark: | | :white_check_mark: | +| Varlen | :white_check_mark: | | :white_check_mark: | +| GQA/MQA | | | :white_check_mark: | +| MLA | :white_check_mark: | | | + +This backend supports torch compile. + +## Flash Attention v2 + +Flash Attention v2 (original C++ kernels) are available under the `flash2` backend. +Requires the `flash_attn` package. + +Minimum version required: `2.7.0`. +Maximum version supported: `2.7.4`. + +This backend supports torch compile. + +### Feature coverage + +| Feat/Backend | Ampere/RTX | +| ------------ | ------------------ | +| Causal mask | :white_check_mark: | +| Varlen | :white_check_mark: | +| GQA/MQA | :white_check_mark: | +| MLA | | + +## Flash Attention v3 + +Flash Attention v3 (original C++ kernels) are available under the `flash3` backend. +Requires the `flash_attn_3` package. + +Version required: `3.0.0.b*`. + +### Feature coverage + +| Feat/Backend | Ampere/RTX | +| ------------ | ------------------ | +| Causal mask | :white_check_mark: | +| Varlen | :white_check_mark: | +| GQA/MQA | :white_check_mark: | +| MLA | | + +MLA is technically supported, but disabled due to an API bug in the backward pass. + +Torch compile is NOT yet supported for this backend. diff --git a/cosmos3/_src/imaginaire/attention/docs/features.md b/cosmos3/_src/imaginaire/attention/docs/features.md new file mode 100644 index 00000000..d14e3f19 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/docs/features.md @@ -0,0 +1,151 @@ +# Imaginaire Attention Subpackage Docs > Features + +## Causal mask + +Causal masking requires explicit indication of causal mask type. +For example, simply passing `is_causal=True` will fail: + +```python +output = attention( + query=query, + key=key, + value=value, + is_causal=True +) +``` + +Result: + +``` +ValueError: Argument causal_type must be specified when is_causal=True. +``` + +There are currently two types of causal masking that are supported, and many popular backends tend +to support only one. It's therefore critical to to choose the correct one for your application. + +```python +from cosmos3._src.imaginaire.attention.masks import CausalType + +# Causal type choices: +# - CausalType.TopLeft +# - CausalType.BottomRight + +output = attention( + query=query, + key=key, + value=value, + is_causal=True, + causal_type=CausalType.TopLeft, +) +``` + +### Top-left causal mask + +Q sequence length = KV sequence length = 5 + +| | K1 | K2 | K3 | K4 | K5 | +| --- | -------- | -------- | -------- | -------- | -------- | +| Q1 | ✓ | ✗ | ✗ | ✗ | ✗ | +| Q2 | ✓ | ✓ | ✗ | ✗ | ✗ | +| Q3 | ✓ | ✓ | ✓ | ✗ | ✗ | +| Q4 | ✓ | ✓ | ✓ | ✓ | ✗ | +| Q5 | ✓ | ✓ | ✓ | ✓ | ✓ | + +Q sequence length = 2, KV sequence length = 5 + +| | K1 | K2 | K3 | K4 | K5 | +| --- | -------- | -------- | -------- | -------- | -------- | +| Q1 | ✓ | ✗ | ✗ | ✗ | ✗ | +| Q2 | ✓ | ✓ | ✗ | ✗ | ✗ | + +Q sequence length = 5, KV sequence length = 2 + +| | K1 | K2 | +| --- | -------- | -------- | +| Q1 | ✓ | ✗ | +| Q2 | ✓ | ✓ | +| Q3 | ✓ | ✓ | +| Q4 | ✓ | ✓ | +| Q5 | ✓ | ✓ | + +### Bottom-right causal mask + +Q sequence length = KV sequence length = 5 + +| | K1 | K2 | K3 | K4 | K5 | +| --- | -------- | -------- | -------- | -------- | -------- | +| Q1 | ✓ | ✗ | ✗ | ✗ | ✗ | +| Q2 | ✓ | ✓ | ✗ | ✗ | ✗ | +| Q3 | ✓ | ✓ | ✓ | ✗ | ✗ | +| Q4 | ✓ | ✓ | ✓ | ✓ | ✗ | +| Q5 | ✓ | ✓ | ✓ | ✓ | ✓ | + +(identical to top-left in this special case) + +Q sequence length = 2, KV sequence length = 5 + +| | K1 | K2 | K3 | K4 | K5 | +| --- | -------- | -------- | -------- | -------- | -------- | +| Q1 | ✓ | ✓ | ✓ | ✓ | ✗ | +| Q2 | ✓ | ✓ | ✓ | ✓ | ✓ | + +Q sequence length = 5, KV sequence length = 2 + +| | K1 | K2 | +| --- | -------- | -------- | +| Q1 | ✗ | ✗ | +| Q2 | ✗ | ✗ | +| Q3 | ✗ | ✗ | +| Q4 | ✓ | ✗ | +| Q5 | ✓ | ✓ | + +## GQA/MQA + +Simply pass `key` and `value` without repeating attention heads. + +**NOTE**: `key`/`value` heads must evenly divide `query` heads. + +**NOTE**: the behavior is similar to `repeat_interleave`, not `repeat`. + +## Variable length + +**(Less efficient option)** Pass sequence lengths directly: + +```python +output = attention( + query=query, + key=key, + value=value, + seqlens_Q=torch.tensor(sequence_length_list_Q, device=query.device), + seqlens_KV=torch.tensor(sequence_length_list_KV, device=query.device), +) +``` + +This will manually compute the maximum sequence lengths, and cumulative sums (with the additional +padding). + +**(More efficient option)** Compute cumulative sequence lengths and maximums once, and reuse it: + +```python +from cosmos3._src.imaginaire.attention.varlen import generate_varlen_parameters + + +# they correspond to. +( + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, +) = generate_varlen_parameters(query, key, value, seqlens_Q, seqlens_KV) + +# in all attention layers that follow: +output = attention( + query=query, + key=key, + value=value, + cumulative_seqlen_Q=cumulative_seqlen_Q, + cumulative_seqlen_KV=cumulative_seqlen_KV, + max_seqlen_Q=max_seqlen_Q, + max_seqlen_KV=max_seqlen_KV, +) +``` diff --git a/cosmos3/_src/imaginaire/attention/docs/multi-dim.md b/cosmos3/_src/imaginaire/attention/docs/multi-dim.md new file mode 100644 index 00000000..fb4bb215 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/docs/multi-dim.md @@ -0,0 +1,113 @@ +# Imaginaire Attention Subpackage Docs > Features > Multi-Dimensional Attention + +Multi-Dimensional Attention is the primary API for handling various complex masks and sparsity +patterns, such as the spatio-temporal mask, and sliding window attention. + +## Basic API + +```python +from cosmos3._src.imaginaire.attention import multi_dimensional_attention + +output = multi_dimensional_attention( + query=query, + key=key, + value=value, +) +``` + +Sparsity parameters: + +- **Optional** `window_size`: allows reducing the attention span by limiting each token's context to + a local sliding window. References: + - [Image Transformer](https://arxiv.org/abs/1802.05751) + - [Stand-alone self-attention](https://arxiv.org/abs/1906.05909) + - [Neighborhood attention transformer](https://arxiv.org/abs/2204.07143) +- **Optional** `dilation`: introduces gaps between the tokens within a sliding window, capturing + global context without more computation. + Reference: [Dilated neighborhood attention transformer](https://arxiv.org/abs/2209.15001) + +Other masking parameters: + +- **Optional** `stride`: introduces delays into the sliding window, for **potential** efficiency + gains. Reference: [Generalized Neighborhood Attention](https://arxiv.org/abs/2504.16922). +- **Optional** `is_causal`: allows causally masking individual dimensions. This parameter can + implement the spatio-temporal mask (causal masking across temporal dimension, bi-directional + along space). + +All sparsity / masking parameters can be specified **per dimension**. +The key feature of `multi_dimensional_attention` over the standard `attention` API is supporting +multi-dimensional layouts of tokens (i.e. multi-dimensional feature maps). + +This means `query`, `key` and `value` are not necessarily 4-D tensors; they can be 4-D, 5-D, or 6-D, +representing 1-D, 2-D, and 3-D token layouts (see [Tensor layouts](#tensor-layouts)). + +- **Optional** `scale`: attention (softmax/dot product) scale. Defaults to `head_dim ** -0.5`. +- **Optional** `return_lse`: returns logsumexp if `True` +- **Optional** `backend`: explicitly set backend instead of automatically selecting the best compatible + +## Tensor layouts + +In addition to requiring the [contiguous heads-last tensor layout](../README.md#tensor-layouts), +Multi-Dimensional Attention also requires the "sequence length" dimension to be unrolled / unfolded +back into its original representation: + +```python +# 1-D case: language, audio +batch, X, heads, head_dim = query_1d.shape +# _ +# ^ +# | +# |-----> token layout shape + +# 2-D case: images +batch, X, Y, heads, head_dim = query_2d.shape +# ____ +# ^ +# | +# |-----> token layout shape + +# 3-D case: videos / 3-D images +batch, X, Y, Z, heads, head_dim = query_3d.shape +# _______ +# ^ +# | +# |------> token layout shape +``` + +Multi-Dimensional Attention also requires the shapes of `query`, `key` and `value` to match along +those dimensions, henceforth called the **token layout shape**: + +```python +assert query_1d.shape[1:2] == key_1d.shape[1:2] == value_1d.shape[1:2] + +assert query_2d.shape[1:3] == key_2d.shape[1:3] == value_2d.shape[1:3] + +assert query_3d.shape[1:4] == key_3d.shape[1:4] == value_3d.shape[1:4] +``` + +This is because of the large number of sparsity / masking features (and their combinations) +supported, which is mainly possible by making the assumption that query and context coordinate +spaces are the same, eliminating the requirement for a mapping between the two. + +Problems with a different query and key/value token layout shape may be supported in the future. + +## Backends + +The only backend supporting multi-dimensional attention for now is `natten`. + +## Spatio-Temporal Attention + +Spatio-Temporal attention (causal masking across the time dimension, and no masking / bi-directional +across spatial dimensions) is a special case of Multi-Dimensional Attention. +You can either implement it by marking `is_causal` as expected in `multi_dimensional_attention`, or +directly use `spatio_temporal_attention`: + +```python +from cosmos3._src.imaginaire.attention import spatio_temporal_attention + +output = spatio_temporal_attention( + query=query, + key=key, + value=value, +) +``` diff --git a/cosmos3/_src/imaginaire/attention/flash2/__init__.py b/cosmos3/_src/imaginaire/attention/flash2/__init__.py new file mode 100644 index 00000000..4e9c6a2a --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash2/__init__.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v2 (flash2) Backend +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log +from cosmos3._src.imaginaire.attention.utils.version import version_in_range + +# We lock to safe releases of Flash 2 +# We will have a separate backend identifier for 2025 releases with CuTeDSL +# kernels. +FLASH_ATTENTION_V2_MIN_VERSION = "2.7.0" +FLASH_ATTENTION_V2_MAX_VERSION = "2.7.4.post1" + + +def flash2_supported() -> bool: + """ + Returns whether Flash Attention is supported in this environment. + Requirements are: + * Presence of CUDA Runtime (via PyTorch) + * Presence of Flash Attention, meeting minimum version requirements + + This check guards imports / dependencies on the Flash Attention package. + """ + if not torch.cuda.is_available(): + log.debug("Flash Attention v2 is not supported because PyTorch did not detect CUDA runtime.") + return False + + try: + import flash_attn + + except ImportError: + log.debug("Flash Attention v2 is not supported because the Python package was not found.") + return False + except Exception as e: + log.debug(f"Flash Attention v2 is not supported because importing the Python package failed: {e}") + return False + + flash2_version_str = None + if not hasattr(flash_attn, "__version__"): + from importlib.metadata import version + + flash2_version_str = version("flash_attn") + else: + flash2_version_str = flash_attn.__version__ + + if not version_in_range(flash2_version_str, FLASH_ATTENTION_V2_MIN_VERSION, FLASH_ATTENTION_V2_MAX_VERSION): + log.debug( + "Flash Attention v2 build is not supported; this backend only supports versions " + f"{FLASH_ATTENTION_V2_MIN_VERSION} through {FLASH_ATTENTION_V2_MAX_VERSION}, got " + f"{flash2_version_str}." + ) + return False + + return True + + +FLASH2_SUPPORTED = flash2_supported() + +if FLASH2_SUPPORTED: + from cosmos3._src.imaginaire.attention.flash2.functions import flash2_attention + +else: + from cosmos3._src.imaginaire.attention.flash2.stubs import flash2_attention + +__all__ = ["flash2_attention", "FLASH2_SUPPORTED"] diff --git a/cosmos3/_src/imaginaire/attention/flash2/checks.py b/cosmos3/_src/imaginaire/attention/flash2/checks.py new file mode 100644 index 00000000..87c44748 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash2/checks.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v2 (flash2) backend checks +""" + +from functools import partial + +import torch + +from cosmos3._src.imaginaire.attention.checks import attention_param_checks, attention_tensor_checks +from cosmos3._src.imaginaire.attention.flash2 import FLASH2_SUPPORTED +from cosmos3._src.imaginaire.attention.flash2.meta import get_bwd_dtypes, get_fwd_dtypes +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.utils import get_arch_tag, log_or_raise_error + + +def flash2_attention_check( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + causal_type: CausalType, + is_varlen: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function for the flash2 backend. + + Parameters: + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with flash2 backend. + + """ + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + if not FLASH2_SUPPORTED: + target_fn( + "Flash Attention v2 (flash2) is not supported in this environment. Run with debug logs to find out why, or choose another backend.", + exception=RuntimeError, + ) + return False + + if is_varlen: + target_fn( + "Flash Attention v2 (flash2) varlen is banned due to instability. Please choose another backend.", + exception=ValueError, + ) + return False + + arch_tag = get_arch_tag(device) + fwd_dtypes = get_fwd_dtypes(arch_tag) + bwd_dtypes = get_bwd_dtypes(arch_tag) + if not attention_tensor_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=fwd_dtypes, + supported_dtypes_backward=bwd_dtypes, + supports_mla=False, + supports_gqa_mqa=True, + raise_error=raise_error, + backend_name="Flash Attention v2 (flash2)", + ): + target_fn("Flash Attention v2 (flash2) does not support the given inputs.", exception=RuntimeError) + return False + + # Verifies causal_type is a CausalType instance when is_causal + # Verifies DontCare is not used unless seqlen_q == seqlen_kv + attention_param_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + is_causal=is_causal, + causal_type=causal_type, + ) + + if is_causal and causal_type not in [CausalType.BottomRight, CausalType.DontCare]: + target_fn("Flash Attention v2 only supports bottom-right causal masking.", exception=RuntimeError) + return False + + return True diff --git a/cosmos3/_src/imaginaire/attention/flash2/functions.py b/cosmos3/_src/imaginaire/attention/flash2/functions.py new file mode 100644 index 00000000..b1dd380d --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash2/functions.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v2 (flash2) Backend: intermediate APIs +Only safe to import when FLASH2_SUPPORTED is True. +""" + +from flash_attn.flash_attn_interface import flash_attn_func, flash_attn_varlen_func +from torch import Tensor + +from cosmos3._src.imaginaire.attention.checks import assert_universal_tensor_checks +from cosmos3._src.imaginaire.attention.flash2.checks import flash2_attention_check +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling + + +def flash2_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + """ + Runs Flash Attention v2 on given operands (Q, K, V) with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`). + + Parameters: + query (Tensor): 4-D query tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`) + + key (Tensor): 4-D key tensor, with the heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor, with heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim_v]`) + + is_causal (bool): whether or not causal masking is enabled. Default is False. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + cumulative_seqlen_Q (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of query tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_KV` and `max_seqlen_{Q,KV}`. + + cumulative_seqlen_KV (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of key/value tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_Q` and `max_seqlen_{Q,KV}`. + + max_seqlen_Q (int | None): (varlen) Optional integer indicating the maximum query + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_KV`. + + max_seqlen_KV (int | None): (varlen) Optional integer indicating the maximum key/value + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_Q`. + + Other Parameters: + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to Flash's + attention operator, if any. + + deterministic (bool): Deterministic backward pass required. + + Returns: + output (Tensor): 4-D output tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, 1]`). Only returned when return_lse is True. + NOTE: this tensor is not contiguous in this backend (Flash2) and it should not be made + contiguous unless we can guarantee its results aren't merged via `merge_attentions`. + """ + + is_varlen = cumulative_seqlen_Q is not None + assert_universal_tensor_checks(query, key, value) + + backend_kwargs = backend_kwargs.copy() if backend_kwargs is not None else {} + # Determinism in backend_kwargs supersedes primary flag, if set to True + if "deterministic" in backend_kwargs: + deterministic = deterministic or backend_kwargs["deterministic"] + del backend_kwargs["deterministic"] + + assert flash2_attention_check( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=True, + ) + + # This check introduces recompiles + if not is_torch_compiling(): + if is_varlen and max_seqlen_Q == max_seqlen_KV == 0: + raise NotImplementedError( + "You're trying to use varlen attention with the flash2 backend and " + "an empty batch, which is not yet supported by flash2." + ) + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + if is_varlen: + assert query.shape[0] == key.shape[0] == value.shape[0] == 1 + q = query.squeeze(0) # [total_tokens,H,D] + k = key.squeeze(0) # [total_tokens,Hkv,D] + v = value.squeeze(0) # [total_tokens,Hkv,Dv] + assert q.dim() == k.dim() == v.dim() == 3 + out, lse_, _ = flash_attn_varlen_func( + q=query.squeeze(0), + k=key.squeeze(0), + v=value.squeeze(0), + cu_seqlens_q=cumulative_seqlen_Q, + cu_seqlens_k=cumulative_seqlen_KV, + max_seqlen_q=max_seqlen_Q, + max_seqlen_k=max_seqlen_KV, + softmax_scale=scale, + causal=is_causal, + return_attn_probs=True, + deterministic=deterministic, + **backend_kwargs, + # window_size=(-1, -1), + # dropout_p=0.0, + # softcap=0.0, # 0.0 means deactivated + # alibi_slopes=None, + # block_table=None, + ) + assert out.dim() == 3 # [total_tokens,H,Dv] + assert lse_.dim() == 2 # [H,total_tokens] + + output = out.unsqueeze(0) # [1,total_tokens,H,Dv] + lse = lse_.unsqueeze(0) # [1,H,total_tokens] + + else: + output, lse, _ = flash_attn_func( # output: [B,N,H,Dv], lse: [B,H,N] + q=query, + k=key, + v=value, + softmax_scale=scale, + causal=is_causal, + return_attn_probs=True, + deterministic=deterministic, + **backend_kwargs, + # window_size=(-1, -1), + # dropout_p=0.0, + # softcap=0.0, # 0.0 means deactivated + # alibi_slopes=None, + ) + + assert isinstance(output, Tensor) + assert isinstance(lse, Tensor) + assert output.dim() == 4 # [B,N,H,Dv] or [1,total_tokens,H,Dv] + assert lse.dim() == 3 # [B,H,N] or [1,H,total_tokens] + + + # incorrect. All output and lse tensors passed into `merge_attentions` must have the same data + # pointer as their corresponding attention autograd ops! + lse = lse.permute(0, 2, 1) # [B,N,H] or [1,total_tokens,H] + + if return_lse: + return output, lse + + return output diff --git a/cosmos3/_src/imaginaire/attention/flash2/meta.py b/cosmos3/_src/imaginaire/attention/flash2/meta.py new file mode 100644 index 00000000..b0a813a5 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash2/meta.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v2 (flash2) Backend: metadata +Always safe to import (as long as torch is available.) +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log + + +def get_fwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for forward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag < 80: + log.debug("Flash Attention v2 (flash2) is not supported because compute capability is below the minimum (8.0).") + return [] + + return [torch.float16, torch.bfloat16] + + +def get_bwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for backward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag < 80: + log.debug("Flash Attention v2 (flash2) is not supported because compute capability is below the minimum (8.0).") + return [] + + return [torch.float16, torch.bfloat16] diff --git a/cosmos3/_src/imaginaire/attention/flash2/stubs.py b/cosmos3/_src/imaginaire/attention/flash2/stubs.py new file mode 100644 index 00000000..05093ad8 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash2/stubs.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v2 (flash2) Backend: intermediate API stubs +Always safe to import (as long as torch is available.) +""" + +from torch import Tensor + +from cosmos3._src.imaginaire.attention.masks import CausalType + + +def flash2_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + raise RuntimeError( + "Tried to run Flash Attention v2, but it is not supported / available. " + "Try running with debug logs enabled to see why." + ) diff --git a/cosmos3/_src/imaginaire/attention/flash3/__init__.py b/cosmos3/_src/imaginaire/attention/flash3/__init__.py new file mode 100644 index 00000000..845e8838 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash3/__init__.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v3 (flash3) Backend +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log +from cosmos3._src.imaginaire.attention.utils.version import version_at_least + +FLASH_ATTENTION_V3_MIN_VERSION = "1.0.3" + + +def flash3_supported() -> bool: + """ + Returns whether Flash Attention is supported in this environment. + Requirements are: + * Presence of CUDA Runtime (via PyTorch) + * Presence of Flash Attention, meeting minimum version requirements + + This check guards imports / dependencies on the Flash Attention package. + """ + if not torch.cuda.is_available(): + log.debug("Flash Attention v3 is not supported because PyTorch did not detect CUDA runtime.") + return False + + try: + # pyrefly: ignore # missing-import + import flash_attn_3_nv + + except ImportError: + log.debug("Flash Attention v3 is not supported because the Python package ('flash_attn_3_nv'_) was not found.") + return False + except Exception as e: + log.debug(f"Flash Attention v3 is not supported because importing the Python package failed: {e}") + return False + + if not version_at_least(flash_attn_3_nv.__version__, FLASH_ATTENTION_V3_MIN_VERSION): + log.debug( + f"Flash Attention v3 ('flash_attn_3_nv') build is not supported; minimum required version is " + f"{FLASH_ATTENTION_V3_MIN_VERSION}, got {flash_attn_3_nv.__version__}." + ) + return False + + return True + + +FLASH3_SUPPORTED = flash3_supported() + + +if FLASH3_SUPPORTED: + from cosmos3._src.imaginaire.attention.flash3.functions import flash3_attention + +else: + from cosmos3._src.imaginaire.attention.flash3.stubs import flash3_attention + +__all__ = ["flash3_attention", "FLASH3_SUPPORTED"] diff --git a/cosmos3/_src/imaginaire/attention/flash3/checks.py b/cosmos3/_src/imaginaire/attention/flash3/checks.py new file mode 100644 index 00000000..94b6d9fc --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash3/checks.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v3 (flash3) backend checks +""" + +from functools import partial + +import torch + +from cosmos3._src.imaginaire.attention.checks import attention_param_checks, attention_tensor_checks +from cosmos3._src.imaginaire.attention.flash3 import FLASH3_SUPPORTED +from cosmos3._src.imaginaire.attention.flash3.meta import get_bwd_dtypes, get_fwd_dtypes +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.utils import get_arch_tag, log_or_raise_error + + +def flash3_attention_check( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + causal_type: CausalType, + is_varlen: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function for the flash3 backend. + + Parameters: + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with flash3 backend. + + """ + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + if not FLASH3_SUPPORTED: + target_fn( + "Flash Attention v3 (flash3) is not supported in this environment. Run with debug logs to find out why, or choose another backend.", + exception=RuntimeError, + ) + return False + + arch_tag = get_arch_tag(device) + fwd_dtypes = get_fwd_dtypes(arch_tag) + bwd_dtypes = get_bwd_dtypes(arch_tag) + if not attention_tensor_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=fwd_dtypes, + supported_dtypes_backward=bwd_dtypes, + # flash3 supports MLA, unlike flash2, but with some constraints + # disabled for now due to API bug + supports_mla=False, + supports_gqa_mqa=True, + raise_error=raise_error, + backend_name="Flash Attention v3 (flash3)", + ): + target_fn("Flash Attention v3 (flash3) does not support the given inputs.", exception=RuntimeError) + return False + + # MLA constraints + if query_shape[-1] != value_shape[-1]: + head_dim_q = query_shape[-1] + head_dim_v = value_shape[-1] + if not ((head_dim_q <= 64 and head_dim_v <= 512) or (128 <= head_dim_q <= 192 and 96 <= head_dim_v <= 128)): + target_fn( + "Flash Attention v3 (flash3) does not support this head dim combination. " + "Expected either head_dim_qk <= 64 and head_dim_v <= 512, or 128 <= head_dim_qk <= 192 " + f"and 96 <= head_dim_v <= 128, got {head_dim_q=}, {head_dim_v=}.", + exception=ValueError, + ) + return False + + # Verifies causal_type is a CausalType instance when is_causal + # Verifies DontCare is not used unless seqlen_q == seqlen_kv + attention_param_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + is_causal=is_causal, + causal_type=causal_type, + ) + + if is_causal and causal_type not in [CausalType.BottomRight, CausalType.DontCare]: + target_fn("Flash Attention v3 only supports bottom-right causal masking.", exception=ValueError) + return False + + return True diff --git a/cosmos3/_src/imaginaire/attention/flash3/functions.py b/cosmos3/_src/imaginaire/attention/flash3/functions.py new file mode 100644 index 00000000..1ac796f8 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash3/functions.py @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v3 (flash3) Backend: intermediate APIs +Only safe to import when FLASH3_SUPPORTED is True. +""" + +import inspect + +# pyrefly: ignore # missing-import +from flash_attn_3_nv.flash_attn_interface import flash_attn_func, flash_attn_varlen_func +from torch import Tensor + + +# reflection of the commit hash in the version, so we have to manually inspect the signatures +HAS_RETURN_ATTN_PROBS = "return_attn_probs" in inspect.signature(flash_attn_func).parameters + +from cosmos3._src.imaginaire.attention.checks import assert_universal_tensor_checks +from cosmos3._src.imaginaire.attention.flash3.checks import flash3_attention_check +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling + + +def flash3_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + """ + Runs Flash Attention v3 on given operands (Q, K, V) with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`). + + Parameters: + query (Tensor): 4-D query tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`) + + key (Tensor): 4-D key tensor, with the heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor, with heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim_v]`) + + is_causal (bool): whether or not causal masking is enabled. Default is False. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + cumulative_seqlen_Q (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of query tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_KV` and `max_seqlen_{Q,KV}`. + + cumulative_seqlen_KV (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of key/value tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_Q` and `max_seqlen_{Q,KV}`. + + max_seqlen_Q (int | None): (varlen) Optional integer indicating the maximum query + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_KV`. + + max_seqlen_KV (int | None): (varlen) Optional integer indicating the maximum key/value + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_Q`. + + Other Parameters: + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to Flash's + attention operator, if any. + + deterministic (bool): Deterministic backward pass required. + + Returns: + output (Tensor): 4-D output tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, 1]`). Only returned when return_lse is True. + NOTE: this tensor is not contiguous in this backend (Flash3) and it should not be made + contiguous unless we can guarantee its results aren't merged via `merge_attentions`. + """ + + is_varlen = cumulative_seqlen_Q is not None + assert_universal_tensor_checks(query, key, value) + + backend_kwargs = backend_kwargs.copy() if backend_kwargs is not None else {} + # Determinism in backend_kwargs supersedes primary flag, if set to True + if "deterministic" in backend_kwargs: + deterministic = deterministic or backend_kwargs["deterministic"] + del backend_kwargs["deterministic"] + + assert flash3_attention_check( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=True, + ) + + # This check introduces recompiles + if not is_torch_compiling(): + if is_varlen and max_seqlen_Q == max_seqlen_KV == 0: + raise NotImplementedError( + "You're trying to use varlen attention with the flash3 backend and " + "an empty batch, which is not yet supported by flash3." + ) + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + if HAS_RETURN_ATTN_PROBS: + backend_kwargs["return_attn_probs"] = True + + if is_varlen: + assert query.shape[0] == key.shape[0] == value.shape[0] == 1 + q = query.squeeze(0) # [total_tokens,H,D] + k = key.squeeze(0) # [total_tokens,Hkv,D] + v = value.squeeze(0) # [total_tokens,Hkv,Dv] + assert q.dim() == k.dim() == v.dim() == 3 + out, lse_ = flash_attn_varlen_func( + q=query.squeeze(0), + k=key.squeeze(0), + v=value.squeeze(0), + cu_seqlens_q=cumulative_seqlen_Q, + cu_seqlens_k=cumulative_seqlen_KV, + max_seqlen_q=max_seqlen_Q, + max_seqlen_k=max_seqlen_KV, + softmax_scale=scale, + causal=is_causal, + deterministic=deterministic, + **backend_kwargs, + # qv=None, + # q_descale=None, k_descale=None, v_descale=None, + # attention_chunk=0, + # num_splits=1, + # pack_gqa=None, + # sm_margin=0, + # window_size=(-1, -1), + # softcap=0.0, # 0.0 means deactivated + ) + assert out.dim() == 3 # [total_tokens,H,Dv] + assert lse_.dim() == 2 # [H,total_tokens] + + output = out.unsqueeze(0) # [1,total_tokens,H,Dv] + lse = lse_.unsqueeze(0) # [1,H,total_tokens] + + else: + output, lse = flash_attn_func( # output: [B,N,H,Dv], lse: [B,H,N] + q=query, + k=key, + v=value, + softmax_scale=scale, + causal=is_causal, + deterministic=deterministic, + **backend_kwargs, + # qv=None, + # q_descale=None, k_descale=None, v_descale=None, + # attention_chunk=0, + # num_splits=1, + # pack_gqa=None, + # sm_margin=0, + # window_size=(-1, -1), + # softcap=0.0, # 0.0 means deactivated + ) + + assert isinstance(output, Tensor) + assert isinstance(lse, Tensor) + assert output.dim() == 4 # [B,N,H,Dv] or [1,total_tokens,H,Dv] + assert lse.dim() == 3 # [B,H,N] or [1,H,total_tokens] + + + # incorrect. All output and lse tensors passed into `merge_attentions` must have the same data + # pointer as their corresponding attention autograd ops! + lse = lse.permute(0, 2, 1) # [B,N,H] or [1,total_tokens,H] + + if return_lse: + return output, lse + + return output diff --git a/cosmos3/_src/imaginaire/attention/flash3/meta.py b/cosmos3/_src/imaginaire/attention/flash3/meta.py new file mode 100644 index 00000000..3229d037 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash3/meta.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v3 (flash3) Backend: metadata +Always safe to import (as long as torch is available.) +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log + + +def get_fwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for forward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag != 90: + log.debug("Flash Attention v3 (flash3) only supports compute capability 9.0 (Hopper).") + return [] + + return [torch.float16, torch.bfloat16] + + +def get_bwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for backward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag != 90: + log.debug("Flash Attention v3 (flash3) only supports compute capability 9.0 (Hopper).") + return [] + + return [torch.float16, torch.bfloat16] diff --git a/cosmos3/_src/imaginaire/attention/flash3/stubs.py b/cosmos3/_src/imaginaire/attention/flash3/stubs.py new file mode 100644 index 00000000..9fcd9774 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/flash3/stubs.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Flash Attention v3 (flash3) Backend: intermediate API stubs +Always safe to import (as long as torch is available.) +""" + +from torch import Tensor + +from cosmos3._src.imaginaire.attention.masks import CausalType + + +def flash3_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + raise RuntimeError( + "Tried to run Flash Attention v3, but it is not supported / available. " + "Try running with debug logs enabled to see why." + ) diff --git a/cosmos3/_src/imaginaire/attention/frontend.py b/cosmos3/_src/imaginaire/attention/frontend.py new file mode 100644 index 00000000..9a001b87 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/frontend.py @@ -0,0 +1,769 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Frontend APIs +""" + +import torch +from torch import Tensor + +from cosmos3._src.imaginaire.attention.backends import choose_backend, choose_multi_dim_backend +from cosmos3._src.imaginaire.attention.checks import ( + attention_param_checks, + attention_tensor_checks, + multi_dim_attention_param_checks, + multi_dim_attention_param_filter, + multi_dim_attention_tensor_checks, + universal_tensor_checks, + varlen_tensor_checks, +) +from cosmos3._src.imaginaire.attention.flash2 import flash2_attention +from cosmos3._src.imaginaire.attention.flash3 import flash3_attention +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.natten import natten_attention, natten_multi_dim_attention +from cosmos3._src.imaginaire.attention.utils.environment import filter_attention_merge_backends +from cosmos3._src.imaginaire.attention.utils.safe_ops import log + + +# Map backend names to their frontend attention API +BACKEND_MAP = { + "natten": natten_attention, + "flash2": flash2_attention, + "flash3": flash3_attention, +} + +MULTI_DIM_BACKEND_MAP = { + "natten": natten_multi_dim_attention, +} + + +def attention( + query: Tensor, # [B,S_Q,H,D] + key: Tensor, # [B,S_KV,H_KV,D] + value: Tensor, # [B,S_KV,H_KV,D_V] + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + # varlen parameters + seqlens_Q: Tensor | None = None, # [B] + seqlens_KV: Tensor | None = None, # [B] + cumulative_seqlen_Q: Tensor | None = None, # [B+1] + cumulative_seqlen_KV: Tensor | None = None, # [B+1] + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + # backend & misc parameters + backend: str | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: # [B,S_Q,H,D_V] or ([B,S_Q,H,D_V], [B,S_Q,H,1]) + """ + Runs Attention on given operands (Q, K, V) with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`). + + Varlen Attention is only supported for the sequence-packed layout: QKV tensors have batch size + 1, and tokens from different batches are concatenated without any padding along the sequence + dimension. Sequence lengths for different batches can be provided in two ways: + 1. `seqlens_Q` and `seqlens_KV` (less efficient): only provide the sequence lengths as + integer tensors (must be on the same device as QKV), and cumulative and maximum sequence + lengths are recomputed on each call. + 2. `cumulative_seqlen_{Q,KV}` and `max_seqlen_{Q,KV}` (more efficient): + compute cumulative and maximum sequence lengths. `cumulative_seqlen_{Q,KV}` are integer + tensors on the same device as QKV containing the cumulative sum of `seqlens_{Q,KV}`, + with an additional `0` element in the beginning, therefore sized `batch+1`. + `max_seqlen_{Q,KV}` are integers (not Tensors) that represent the maximum sequence + lengths for Q and KV among all sequence batches. + You can use `generate_varlen_parameters` to generate these + parameters: + ```python3 + from cosmos3._src.imaginaire.attention.varlen import generate_varlen_parameters + ( + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, + ) = generate_varlen_parameters(q, k, v, seqlens_Q, seqlens_KV) + ``` + + Parameters: + query (Tensor): 4-D query tensor, with the heads-last contiguous layout + (`[batch, seqlen_q, heads, head_dim]`) + + key (Tensor): 4-D key tensor, with the heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor, with heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim_v]`) + + is_causal (bool): whether or not causal masking is enabled. Default is False. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`, `CausalType.DontCare` (only valid when seqlen_q == seqlen_kv). + Required when `is_causal = True`. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + seqlens_Q (Tensor | None): (varlen) Optional 1-D tensor with size `batch` + indicating the number of query tokens in each batch. Must be passed together with + `seqlens_KV`. + + seqlens_KV (Tensor | None): (varlen) Optional 1-D tensor with size `batch` + indicating the number of key/value tokens in each batch. Must be passed together with + `seqlens_Q`. + + cumulative_seqlen_Q (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of query tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_KV` and `max_seqlen_{Q,KV}`. + + cumulative_seqlen_KV (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of key/value tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_Q` and `max_seqlen_{Q,KV}`. + + max_seqlen_Q (int | None): (varlen) Optional integer indicating the maximum query + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_KV`. + + max_seqlen_KV (int | None): (varlen) Optional integer indicating the maximum key/value + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_Q`. + + Other Parameters: + backend (str | None): Backend to run with. If unspecified (default), it will try to + select the best available. + + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to the backend's + attention operator, if any. Only valid when a specific backend is selected (backend is + not None). + + deterministic (bool): Whether to enforce deterministic backward pass. Default is False. + When True, backends are selected based on deterministic support. + + Returns: + output (Tensor): 4-D output tensor, with the heads-last contiguous layout + (`[batch, seqlen_q, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, seqlen_q, heads, 1]`). Only returned when return_lse is True. + NOTE: this tensor is not guaranteed to be contiguous with some backends and it should + not be made contiguous unless we can guarantee its results aren't merged via + `merge_attentions`. + """ + + assert universal_tensor_checks(query=query, key=key, value=value, raise_error=True) + + assert attention_tensor_checks( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + raise_error=True, + ) + + attention_param_checks( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + is_causal=is_causal, + causal_type=causal_type, + ) + + ( + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, + ) = varlen_tensor_checks( + query=query, + key=key, + value=value, + seqlens_Q=seqlens_Q, + seqlens_KV=seqlens_KV, + cumulative_seqlen_Q=cumulative_seqlen_Q, + cumulative_seqlen_KV=cumulative_seqlen_KV, + max_seqlen_Q=max_seqlen_Q, + max_seqlen_KV=max_seqlen_KV, + ) + is_varlen = cumulative_seqlen_Q is not None + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + if backend is None and backend_kwargs is not None: + backend_kwargs = None + log.debug("A backend was not specified, but got backend_kwargs. Ignoring... ") + + if backend is not None and backend not in BACKEND_MAP: + raise ValueError(f"Selected {backend=}, but available choices are {BACKEND_MAP.keys()}. ") + + compatible_backend = choose_backend( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + backend=backend, + raise_error=False, + ) + + # Either incompatible backend specified by user, or no compatible backends found + if compatible_backend is None and backend is None: + raise ValueError( + "Could not find a compatible Attention backend for this use case / device. " + "Try running with debug logs to find out why." + ) + elif compatible_backend is None: + raise ValueError( + f"Selected Attention backend {backend} is incompatible with this use case / device. " + "Try running with debug logs to find out why." + ) + + assert compatible_backend in BACKEND_MAP + return BACKEND_MAP[compatible_backend]( + query=query, + key=key, + value=value, + is_causal=is_causal, + causal_type=causal_type, + scale=scale, + cumulative_seqlen_Q=cumulative_seqlen_Q, + cumulative_seqlen_KV=cumulative_seqlen_KV, + max_seqlen_Q=max_seqlen_Q, + max_seqlen_KV=max_seqlen_KV, + return_lse=return_lse, + backend_kwargs=backend_kwargs, + deterministic=deterministic, + ) + + +def multi_dimensional_attention( + query: Tensor, # [B,*token_layout_shape,H,D] + key: Tensor, # [B,*token_layout_shape,H_KV,D] + value: Tensor, # [B,*token_layout_shape,H_KV,D_V] + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + is_causal: tuple | bool = False, + scale: float | None = None, + # backend & misc parameters + backend: str | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> ( + Tensor | tuple[Tensor, Tensor] +): # [B,*token_layout_shape,H,D_V] or ([B,*token_layout_shape,H,D_V], [B,*token_layout_shape,H]) + """ + Runs Multi-Dimensional Attention on given operands (Q, K, V) with the heads-last contiguous + layout (`[batch, *, heads, head_dim]`). Supports up to and including 3 dimensions: + * 1-D: `[batch, X, heads, head_dim]`, with masking arguments expecting tuples of size 1. + * 2-D: `[batch, X, Y, heads, head_dim]`, with masking arguments expecting tuples of size 2. + * 3-D: `[batch, X, Y, Z, heads, head_dim]`, with masking arguments expecting tuples of size 3. + + The dimensions here refer to the layout of tokens; that is the arrangement of tokens for each + batch/head, or the `[X]`, `[X, Y]`, `[X, Y, Z]` part of the input shape. + We refer to these as the "token layout shape". + + For now, it is always expected that Q, K, and V match in the sizes of those dimensions. + + Masking arguments, all of which can be set uniformly across all dimensions or per dimension, are: + * `window_size`: determines the sliding window size. -1 is interpreted as the maximum window + size. Must be either -1 or at least 2 and at most the token layout shape. + For example, if inputs are `[batch, X, Y, Z, heads_{q,kv}, head_dim_{qk,v}]`, + `window_size` must be either an integer == -1 or an integer <= `min(X, Y, Z)`, + or a tuple of size 3 corresponding to the three dimensions / axes, where: + * `window_size[0] == -1 or 2 <= window_size[0] <= X` + * `window_size[1] == -1 or 2 <= window_size[1] <= Y` + * `window_size[2] == -1 or 2 <= window_size[2] <= Z` + When `window_size` is set to the maximum for any dimension, we're effectively performing + self attention (no sparsity) along that dimension. + Default is -1 (self attention). + + * `stride`: determines the step size of the sliding window. Only matters when the + corresponding `window_size` is not -1 / maximum (self attention). + Default is 1, indicating the smallest sliding window delay. + Larger values trade off translational equivariance for potentially improved efficiency. + Maximum value for `stride` along each dimension is the corresponding `window_size`. + If `stride == window_size` along any dimension, it is equivalent to blocked / windowed + attention (from works such as Swin Transformer, SAM, ViTDet, etc) along that dimension, + meaning no overlap between windows. + For more details, please refer to the GNA paper: + https://arxiv.org/abs/2504.16922 + + * `dilation`: introduces gaps between tokens in a sliding window, similarly to dilated + convolution. + Default is 1, indicating no gaps. + Maximum value is the largest positive integer that satisfies + `window_size * dilation <= token_layout_shape` along that dimension. + Higher dilation means more sparse and global context. Lower dilation means more + locality. + For more details, please refer to the DiNAT paper: + https://arxiv.org/abs/2209.15001 + + * `is_causal`: per-dimension causal mask. + + Parameters: + query (Tensor): 4-D, 5-D, or 6-D query tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads, head_dim]`) + + key (Tensor): 4-D, 5-D, or 6-D key tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads_kv, head_dim]`) + + value (Tensor): 4-D, 5-D, or 6-D value tensor, with heads-last contiguous layout + (`[batch, *token_layout_shape, heads_kv, head_dim_v]`) + + window_size (tuple | int): Attention window (kernel) size / shape. If an + integer, it will be repeated for all dimensions. For example `window_size=3`, when + `len(token_layout_shape) == 3`, is interpreted as `window_size=(3, 3, 3)`. + `-1`s are replaced with the corresponding `token_layout_shape`. + Final window size must satisfy `2 <= window_size <= token_layout_shape`. + Default is -1 (no sparsity). + + stride (tuple | int): Sliding window step size/shape. If an integer, it will be repeated + for all dimensions. For example `stride=2`, when `len(token_layout_shape) == 3`, is + interpreted as `stride=(2, 2, 2)`. + Final stride must satisfy `1 <= stride <= window_size`. + Default is 1. + + dilation (tuple | int): Dilation step size/shape. If an integer, it will be repeated for + all dimensions. For example `dilation=4`, when `len(token_layout_shape) == 3`, is + interpreted as `dilation=(4, 4, 4)`. + Final dilation must satisfy `2 <= dilation * window_size <= token_layout_shape`. + Default is 1. + + is_causal (tuple | bool): Toggle causal masking. If a boolean, it will be repeated for all + dimensions. For example `is_causal=True`, when `len(token_layout_shape) == 3`, is + interpreted as `is_causal=(True, True, True)`. + Default is False. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + Other Parameters: + backend (str | None): Backend to run with. If unspecified (default), it will try to + select the best available. + + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to the backend's + multi-dim / sparse attention operator, if any. Only valid when a specific backend is + selected (backend is not None). + + deterministic (bool): Whether to enforce deterministic backward pass. Default is False. + When True, backends are selected based on deterministic support. + + Returns: + output (Tensor): 4-D, 5-D, or 6-D output tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads]`). Only returned when return_lse is True. + """ + + assert universal_tensor_checks(query=query, key=key, value=value, raise_error=True) + + assert multi_dim_attention_tensor_checks( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + raise_error=True, + ) + + token_layout_shape, window_size, stride, dilation, is_causal = multi_dim_attention_param_filter( + query, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) + num_dims = len(token_layout_shape) + + # Automatic transformation for 1s in token layout + # I.e. Attention over a (1, 16, 32) token layout is identical to over a (16, 32) + + token_layout_ones = [i for i in range(num_dims) if token_layout_shape[i] == 1] + if len(token_layout_ones) > 0: + token_layout_t = tuple(s for i, s in enumerate(token_layout_shape) if i not in token_layout_ones) + window_size_t = tuple(w for i, w in enumerate(window_size) if i not in token_layout_ones) + stride_t = tuple(s for i, s in enumerate(stride) if i not in token_layout_ones) + dilation_t = tuple(d for i, d in enumerate(dilation) if i not in token_layout_ones) + is_causal_t = tuple(c for i, c in enumerate(is_causal) if i not in token_layout_ones) + + assert all(x >= 2 for x in token_layout_t) + assert all(w >= 2 for w in window_size_t) + + query_t = query.reshape( + query.shape[0], *token_layout_t, query.shape[-2], query.shape[-1] + ) # [B,*token_layout_t,H,D] + key_t = key.reshape(key.shape[0], *token_layout_t, key.shape[-2], key.shape[-1]) # [B,*token_layout_t,H_KV,D] + value_t = value.reshape( + value.shape[0], *token_layout_t, value.shape[-2], value.shape[-1] + ) # [B,*token_layout_t,H_KV,D_V] + output_shape = [x for x in query.shape[:-1]] + [value.shape[-1]] + + if not torch.compiler.is_compiling(): + log.debug( + "This Multi-Dimensional Attention problem has 1s in the token layout, which can be simplified from " + f"<{token_layout_shape=}, {window_size=}, {stride=}, {dilation=}, {is_causal=}> into " + f"<{token_layout_t=}, {window_size_t=}, {stride_t=}, {dilation_t=}, {is_causal_t=}>." + ) + + output_t, lse_t = multi_dimensional_attention( + query=query_t, + key=key_t, + value=value_t, + window_size=window_size_t, + stride=stride_t, + dilation=dilation_t, + is_causal=is_causal_t, + scale=scale, + backend=backend, + return_lse=True, + backend_kwargs=backend_kwargs, + deterministic=deterministic, + ) + output = output_t.reshape(*output_shape) # [B,*token_layout_shape,H,D_V] + lse = lse_t.reshape(*output_shape[:-1]) # [B,*token_layout_shape,H] + if return_lse: + return output, lse + return output + + multi_dim_attention_param_checks( + query, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) + + # Fast path for self attention problems + if all(x == w for x, w in zip(token_layout_shape, window_size)) and ( + not any(c for c in is_causal) or num_dims == 1 + ): + if not torch.compiler.is_compiling(): + log.debug( + "This Multi-Dimensional Attention problem is implementable with standard Attention: " + f"{token_layout_shape=}, {window_size=}, {is_causal=}." + ) + if backend is not None: + log.debug(f"Ignoring {backend=} and backend args...") + + query_1d = query.flatten(1, num_dims) # [B,S,H,D] + key_1d = key.flatten(1, num_dims) # [B,S,H_KV,D] + value_1d = value.flatten(1, num_dims) # [B,S,H_KV,D_V] + is_causal_1d = is_causal[0] + output_shape = [x for x in query.shape[:-1]] + [value.shape[-1]] + + output_1d, lse_1d = attention( + query_1d, + key_1d, + value_1d, + scale=scale, + is_causal=is_causal_1d, + causal_type=CausalType.DontCare, + return_lse=True, + deterministic=deterministic, + ) + output = output_1d.reshape(*output_shape) # [B,*token_layout_shape,H,D_V] + lse = lse_1d.reshape(*output_shape[:-1]) # [B,*token_layout_shape,H] + if return_lse: + return output, lse + return output + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + if backend is None and backend_kwargs is not None: + backend_kwargs = None + log.debug("A backend was not specified, but got backend_kwargs. Ignoring... ") + + backend = choose_multi_dim_backend( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + deterministic=deterministic, + backend=backend, + ) + + if backend not in MULTI_DIM_BACKEND_MAP: + raise ValueError(f"Selected {backend=}, but available choices are {MULTI_DIM_BACKEND_MAP.keys()}. ") + + return MULTI_DIM_BACKEND_MAP[backend]( + query=query, + key=key, + value=value, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + scale=scale, + return_lse=return_lse, + backend_kwargs=backend_kwargs, + deterministic=deterministic, + ) + + +def multi_dimensional_attention_varlen( + query: Tensor, # [1,S_total,H,D] + key: Tensor, # [1,S_total,H_KV,D] + value: Tensor, # [1,S_total,H_KV,D_V] + metadata: dict, + scale: float | None = None, + backend: str | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: # [1,S_total,H,D_V] or ([1,S_total,H,D_V], [1,S_total,H]) + """ + Runs Variable-Length Multi-Dimensional Attention on sequence-packed QKV tensors. + + This operation performs sparse/multi-dimensional attention on variable-length sequences + where tokens from different samples with different spatial layouts are concatenated + along the sequence dimension. Each sample can have its own spatial dimensions + (e.g., different height/width for 2D layouts). + + The metadata should be pre-computed using `configure_varlen_metadata` and reused + across forward/backward passes for efficiency. + + **Requires NATTEN >= 0.21.9.dev0 and either Hopper or Blackwell DC-class architecture** + + Parameters: + query (Tensor): 4-D query tensor with sequence-packed layout + (`[1, seqlen_total, heads, head_dim]`) + + key (Tensor): 4-D key tensor with sequence-packed layout + (`[1, seqlen_total, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor with sequence-packed layout + (`[1, seqlen_total, heads_kv, head_dim_v]`) + + metadata (dict): Pre-computed varlen metadata from `imaginaire.varlen.generate_multi_dim_varlen_parameters`. + + scale (float | None): Attention scale. Defaults to head_dim ** -0.5. + + Other Parameters: + backend (str | None): Backend to run with. If unspecified (default), it will try to + select the best available. + + return_lse (bool): Whether to return logsumexp values. Default is False. + + backend_kwargs (dict | None): Backend-specific arguments. + + deterministic (bool): Whether to enforce deterministic backward pass. Default is False. + Not supported for this operation (Hopper and Blackwell FNA backends do not support determinism). + + Returns: + output (Tensor): 4-D output tensor with sequence-packed layout + (`[1, seqlen_total, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor (`[1, seqlen_total, heads]`). + Only returned when return_lse is True. + """ + # For now, NATTEN is the only backend that supports varlen multi-dimensional attention + from cosmos3._src.imaginaire.attention.natten import natten_supported + + if not natten_supported(): + raise RuntimeError("merge_attentions requires NATTEN. Please upgrade NATTEN to use attention merging.") + + if backend is not None and backend != "natten": + raise ValueError( + f"multi_dimensional_attention_varlen currently only supports 'natten' backend, got {backend=}." + ) + + # Import NATTEN's varlen function + from cosmos3._src.imaginaire.attention.natten.functions import natten_multi_dim_attention_varlen + + return natten_multi_dim_attention_varlen( + query=query, + key=key, + value=value, + metadata=metadata, + scale=scale, + return_lse=return_lse, + backend_kwargs=backend_kwargs, + deterministic=deterministic, + ) + + +def spatio_temporal_attention( + query: Tensor, # [B,T,H_spatial,W_spatial,H,D] + key: Tensor, # [B,T,H_spatial,W_spatial,H_KV,D] + value: Tensor, # [B,T,H_spatial,W_spatial,H_KV,D_V] + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + scale: float | None = None, + # backend & misc parameters + backend: str | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> ( + Tensor | tuple[Tensor, Tensor] +): # [B,T,H_spatial,W_spatial,H,D_V] or ([B,T,H_spatial,W_spatial,H,D_V], [B,T,H_spatial,W_spatial,H]) + """ + Runs Spatio-Temporal Attention on unflattened QKV with the heads-last contiguous layout + (`[batch, T, H, W, heads, head_dim]`). + For now, it is always expected that Q, K, and V match in their shapes. + + Parameters: + query (Tensor): 6-D query tensor, with the heads-last contiguous layout + (`[batch, T, H, W, heads, head_dim]`) + + key (Tensor): 6-D key tensor, with the heads-last contiguous layout + (`[batch, T, H, W, heads_kv, head_dim]`) + + value (Tensor): 6-D value tensor, with heads-last contiguous layout + (`[batch, T, H, W, heads_kv, head_dim_v]`) + + window_size (tuple | int): Attention window (kernel) size / shape. If an + integer, it will be repeated for all dimensions. For example `window_size=3` is + interpreted as `window_size=(3, 3, 3)`. + `-1`s are replaced with the corresponding value in `(T, H, W)`. + Default is -1 (no sparsity). + + stride (tuple | int): Sliding window step size/shape. If an integer, it will be repeated + for all dimensions. For example `stride=2` is interpreted as `stride=(2, 2, 2)`. + Final stride must satisfy `1 <= stride <= window_size`. + Default is 1. + + dilation (tuple | int): Dilation step size/shape. If an integer, it will be repeated for + all dimensions. For example `dilation=4` is interpreted as `dilation=(4, 4, 4)`. + Final dilation must satisfy `2 <= dilation * window_size <= (T, H, W)`. + Default is 1. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + Other Parameters: + backend (str | None): Backend to run with. If unspecified (default), it will try to + select the best available. + + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to the backend's + multi-dim / sparse attention operator, if any. Only valid when a specific backend is + selected (backend is not None). + + deterministic (bool): Whether to enforce deterministic backward pass. Default is False. + When True, backends are selected based on deterministic support. + + Returns: + output (Tensor): 6-D output tensor, with the heads-last contiguous layout + (`[batch, T, H, W, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, T, H, W, heads]`). Only returned when return_lse is True. + """ + if query.dim() != 6: + raise ValueError( + "Spatio-Temporal Attention requires 6-D input tensors ([batch, T, H, W, heads, head_dim]), " + f"got {query.shape=})." + ) + + return multi_dimensional_attention( + query=query, + key=key, + value=value, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=(True, False, False), + scale=scale, + backend=backend, + return_lse=return_lse, + backend_kwargs=backend_kwargs, + deterministic=deterministic, + ) + + +def merge_attentions( + outputs: list[Tensor], # list of [B,S,H,D_V] + lse_tensors: list[Tensor], # list of [B,S,H] + torch_compile: bool = False, +) -> tuple[Tensor, Tensor]: # ([B,S,H,D_V], [B,S,H]) + """ + Merges multiple attention outputs that share the same query. + + **NOTE: the user is responsible for ensuring ALL output and LSE tensors have the same data + pointer as the outputs from the corresponding Attention operations for correct backpropagation!** + + **NOTE: requires NATTEN** + + **NOTE: for backpropagation, only two outputs can be merged for now.** + + Takes multiple attention outputs computed from the same set of query but w.r.t. different + key/value pairs, and merges them as if all key/value pairs had been concatenated. + This enables patterns like: + - Combining local and global attention (e.g., sparse + dense context) + - Pipelined context parallelism + + The NATTEN backend can be controlled via environment variable filtering. + See `filter_attention_merge_backends` for details. + + Parameters: + outputs (list[Tensor]): List of 4-D attention output tensors, with the heads-last layout + (`[batch, seqlen, heads, head_dim]`). Must contain at least 2 tensors. + + lse_tensors (list[Tensor]): List of 3-D logsumexp tensors, with the heads-last layout + (`[batch, seqlen, heads]`). Must match length of `outputs`. + + torch_compile (bool): Attempt to use `torch.compile` to fuse the underlying elementwise + operations. Default is False. + + Returns: + output (Tensor): Merged attention output tensor (`[batch, seqlen, heads, head_dim]`). + + logsumexp (Tensor): Updated logsumexp tensor (`[batch, seqlen, heads]`). + """ + # For now, NATTEN is the only backend that provides merge_attentions + + # Check if NATTEN is allowed by environment variable + allowed_backends = filter_attention_merge_backends(["natten"]) + + if "natten" not in allowed_backends: + raise RuntimeError( + "merge_attentions requires NATTEN backend, but it has been disabled via environment variable. " + "NATTEN is currently the only supported backend for merge_attentions." + ) + + from cosmos3._src.imaginaire.attention.natten import natten_supported + + if not natten_supported(): + raise RuntimeError("merge_attentions requires NATTEN. Please upgrade NATTEN to use attention merging.") + + # Import and use NATTEN's merge_attentions + from natten.functional import merge_attentions as natten_merge_attentions + + return natten_merge_attentions( + outputs=outputs, + lse_tensors=lse_tensors, + torch_compile=torch_compile, + use_autograd_fix=True, # Always use autograd fix for correct backprop + ) diff --git a/cosmos3/_src/imaginaire/attention/masks.py b/cosmos3/_src/imaginaire/attention/masks.py new file mode 100644 index 00000000..3cad6c02 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/masks.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Mask utilities +""" + +from enum import Enum + + +class CausalType(Enum): + """ + Different types of causal masking supported by backends of interest. + """ + + # Top-Left: Simplified: mask if q_idx < kv_idx + # CUTLASS / NATTEN default + # Q = 2, KV = 5: + # O____ + # OO___ + # + # Q = 5, KV = 2: + # O_ + # OO + # OO + # OO + # OO + TopLeft = 0 + + # Bottom-right: mask if q_idx + KV - Q < kv_idx + # Flash Attention default + # Q = 2, KV = 5: + # OOOO_ + # OOOOO + # + # Q = 5, KV = 2: + # __ + # __ + # __ + # O_ + # OO + BottomRight = 1 + + # When seqlen_q == seqlen_kv, we don't care about the causal type + # because top-left and bottom-right are equivalent + DontCare = 2 diff --git a/cosmos3/_src/imaginaire/attention/natten/__init__.py b/cosmos3/_src/imaginaire/attention/natten/__init__.py new file mode 100644 index 00000000..ad2492e6 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/natten/__init__.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +NATTEN Backend +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log +from cosmos3._src.imaginaire.attention.utils.version import version_at_least + +# 0.21.5.dev1 patches some varlen issues +# 0.21.5.dev2 adds torch compile support +# 0.21.5.dev3 fixes a few compat issues for older torch versions +# 0.21.5.dev6 gqa/mqa support +# 0.21.5.dev9 fixes attention merging +NATTEN_MIN_VERSION = "0.21.5.dev9" + +# Hopper FMHA causal and varlen support +NATTEN_HOPPER_CAUSAL_VARLEN_VERSION = "0.21.6.dev3" + +# Blackwell-FMHA Deterministic bwd support +NATTEN_BLACKWELL_DETERMINISTIC_VERSION = "0.21.6.dev7" + +# Blackwell-FMHA/FNA support extended to head dims meeting alignment constraint and <= 128 +NATTEN_BLACKWELL_PARTIAL_HEAD_DIM_VERSION = "0.21.6.dev8" + +# 0.21.9.dev0 adds varlen multi-dimensional (sparse) attention +NATTEN_VARLEN_MULTI_DIM_VERSION = "0.21.9.dev0" + + +def get_natten_version() -> str: + try: + import natten + except (ImportError, Exception): + return "0.0.0" + + return natten.__version__ + + +def natten_version_satisfies(min_version_str: str) -> bool: + """ + Check if the installed NATTEN version satisfies a specific minimum version requirement. + + Parameters: + min_version_str (str): Minimum version string (e.g., "0.21.5" or "0.21.5.dev12"). + + Returns: + bool: True if NATTEN is installed and meets the minimum version requirement. + """ + return version_at_least(get_natten_version(), min_version_str) + + +def natten_supported() -> bool: + """ + Returns whether NATTEN is supported in this environment. + Requirements are: + * Presence of CUDA Runtime (via PyTorch) + * Presence of NATTEN, meeting minimum version requirements + + This check guards imports / dependencies on the NATTEN package. + """ + if not torch.cuda.is_available(): + log.debug("NATTEN Attention is not supported because PyTorch did not detect CUDA runtime.") + return False + + try: + import natten + except ImportError: + log.debug("NATTEN Attention is not supported because the Python package was not found.") + return False + except Exception as e: + log.debug(f"NATTEN Attention is not supported because importing the Python package failed: {e}") + return False + + if not version_at_least(natten.__version__, NATTEN_MIN_VERSION): + log.debug( + f"NATTEN Attention is not supported due to insufficient NATTEN version " + f"{natten.__version__}, expected at least {NATTEN_MIN_VERSION}." + ) + return False + + return True + + +NATTEN_SUPPORTED = natten_supported() + +if NATTEN_SUPPORTED: + from cosmos3._src.imaginaire.attention.natten.functions import ( + natten_attention, + natten_multi_dim_attention, + natten_multi_dim_attention_varlen, + ) + +else: + from cosmos3._src.imaginaire.attention.natten.stubs import ( + natten_attention, + natten_multi_dim_attention, + natten_multi_dim_attention_varlen, + ) + +__all__ = [ + "natten_attention", + "natten_multi_dim_attention", + "natten_multi_dim_attention_varlen", + "NATTEN_SUPPORTED", + "NATTEN_MIN_VERSION", + "NATTEN_VARLEN_MULTI_DIM_VERSION", + "get_natten_version", + "natten_version_satisfies", +] diff --git a/cosmos3/_src/imaginaire/attention/natten/checks.py b/cosmos3/_src/imaginaire/attention/natten/checks.py new file mode 100644 index 00000000..c6369cd2 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/natten/checks.py @@ -0,0 +1,521 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +NATTEN backend checks +""" + +from functools import partial + +import torch + +from cosmos3._src.imaginaire.attention.checks import ( + attention_param_checks, + attention_tensor_checks, + multi_dim_attention_tensor_checks, +) +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.natten import ( + NATTEN_BLACKWELL_DETERMINISTIC_VERSION, + NATTEN_BLACKWELL_PARTIAL_HEAD_DIM_VERSION, + NATTEN_HOPPER_CAUSAL_VARLEN_VERSION, + NATTEN_SUPPORTED, + get_natten_version, + natten_version_satisfies, +) +from cosmos3._src.imaginaire.attention.natten.meta import get_bwd_dtypes, get_fwd_dtypes +from cosmos3._src.imaginaire.attention.utils import get_arch_tag, is_fp8, log_or_raise_error +from cosmos3._src.imaginaire.attention.utils.safe_ops import log +from cosmos3._src.imaginaire.attention.utils.safe_ops.functools import lru_cache + + +def dtype_supported( + dtype: torch.dtype, requires_grad: bool, dtypes_fwd: list[torch.dtype], dtypes_bwd: list[torch.dtype] | None = None +) -> bool: + """ + Helper determining whether dtype is supported with different sets of supported dtypes for + training and inference (forward+backward and forward). + + Parameters: + dtype (torch.dtype): tensor element type. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + dtypes_fwd (list[torch.dtype]): list of dtypes allowed for inference only (when not + tensor.requires_grad). + + dtypes_bwd (list[torch.dtype] | None): Optional list of dtypes allowed for training only + (when tensor.requires_grad), if different from dtypes_fwd. + + """ + if requires_grad and dtypes_bwd is not None: + return dtype in dtypes_bwd + return dtype in dtypes_fwd + + +@lru_cache +def choose_natten_backend( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + is_varlen: bool, + deterministic: bool = False, + requires_fna: bool = False, + raise_error: bool = False, +) -> str | None: + """ + Chooses an FMHA backend in NATTEN (cutlass-fmha, hopper-fmha, blackwell-fmha) for the current + use case based on features needed and current GPU architecture. + + Using tensor shapes, it infers whether MLA (head_dim_value != head_dim_qk) or + GQA/MQA (heads_kv != heads_q) are required. + Using device, it infers GPU architecture and compatible backends. + Using arguments is_causal and is_varlen, and other inferred features, it picks the best + available backend. + + It is possible for no backend to be selected, if the combination of features is not available in + any one of the NATTEN backends, in which case it will return None. + + Parameters: + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + requires_fna (bool): Whether the selection is for FNA kernels (sometimes they have different + feature coverage compared to their FMHA counterparts.) + + raise_error (bool): whether to raise an error if no backend is selected, instead of just + returning None. Default is False. + + Returns: + backend (str | None): selected NATTEN backend, if any compatible. + + """ + natten_version = get_natten_version() + + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + + arch_tag = get_arch_tag(device) + + is_mla = query_shape[-1] != value_shape[-1] + head_dim = max(query_shape[-1], value_shape[-1]) + + # banning devices not supported since CUDA 13.0 for simplicity + if arch_tag < 75: + log.debug("NATTEN is not supported because compute capability is below the minimum (7.5).") + return None + + # blackwell-fmha: sm100 and sm103 only. + # limitations: no mla (TBD). + blackwell_fmha_fwd_dtypes = [torch.float16, torch.bfloat16, torch.float8_e5m2, torch.float8_e4m3fn] + blackwell_fmha_bwd_dtypes = [torch.float16, torch.bfloat16] + blackwell_dtype_supported = dtype_supported( + dtype=dtype, + requires_grad=requires_grad, + dtypes_fwd=blackwell_fmha_fwd_dtypes, + dtypes_bwd=blackwell_fmha_bwd_dtypes, + ) + + blackwell_deterministic_supported = natten_version_satisfies(NATTEN_BLACKWELL_DETERMINISTIC_VERSION) + blackwell_deterministic_blocked = deterministic and (requires_fna or not blackwell_deterministic_supported) + + blackwell_partial_head_dim_support = natten_version_satisfies(NATTEN_BLACKWELL_PARTIAL_HEAD_DIM_VERSION) + blackwell_head_dim_alignment_constraint = 16 if is_fp8(dtype) else 8 + blackwell_head_dim_in_range = 0 < head_dim and head_dim <= 128 + blackwell_head_dim_alignment_met = head_dim % blackwell_head_dim_alignment_constraint == 0 + blackwell_head_dim_supported = head_dim in [32, 64, 128] or ( + blackwell_partial_head_dim_support and blackwell_head_dim_in_range and blackwell_head_dim_alignment_met + ) + + if ( + arch_tag in [100, 103] + and not is_mla + and blackwell_dtype_supported + and blackwell_head_dim_supported + and not blackwell_deterministic_blocked + ): + return "blackwell-fmha" + else: + reason = "" + if blackwell_deterministic_blocked: + reason += "Deterministic mode requested but not supported. " + if arch_tag not in [100, 103]: + reason += f"Incompatible architecture ({arch_tag}, expected 100 or 103). " + if is_mla: + reason += "Use case is MLA (head_dim_qk != head_dim_value). " + if not blackwell_dtype_supported: + if requires_grad: + reason += ( + f"Data type {dtype} is not in list of supported dtypes for training: {blackwell_fmha_bwd_dtypes}. " + ) + else: + reason += ( + f"Data type {dtype} is not in list of supported dtypes for inference: {blackwell_fmha_fwd_dtypes}. " + ) + if not blackwell_head_dim_supported: + reason += f"{head_dim=} is not supported with {dtype=} (natten {natten_version})" + log.debug(f"NATTEN backend blackwell-fmha is not compatible. Reason: {reason}") + + # hopper-fmha: sm90 only. + # limitations: no mla. + # varlen and causal masking support was added in NATTEN_HOPPER_CAUSAL_VARLEN_VERSION + hopper_fmha_dtypes = [torch.float16, torch.bfloat16] + dtype_supported_hopper = dtype_supported(dtype=dtype, requires_grad=requires_grad, dtypes_fwd=hopper_fmha_dtypes) + head_dim_supported_hopper = (head_dim in [32, 64, 128, 256] and not requires_grad) or head_dim in [32, 64, 128] + hopper_varlen_causal_supported = natten_version_satisfies(NATTEN_HOPPER_CAUSAL_VARLEN_VERSION) + hopper_varlen_causal_check = hopper_varlen_causal_supported or (not is_varlen and not is_causal) + if ( + arch_tag == 90 + and hopper_varlen_causal_check + and not is_mla + and dtype_supported_hopper + and head_dim_supported_hopper + and not deterministic + ): + return "hopper-fmha" + else: + reason = "" + if deterministic: + reason += "Deterministic mode requested but hopper-fmha does not support it. " + if arch_tag != 90: + reason += f"Incompatible architecture ({arch_tag}, expected 90). " + if is_causal and not hopper_varlen_causal_supported: + reason += ( + "Use case is causal, which is only supported since natten " + + f"{NATTEN_HOPPER_CAUSAL_VARLEN_VERSION}, detected version: {natten_version}. " + ) + if is_varlen and not hopper_varlen_causal_supported: + reason += ( + "Use case is varlen, which is only supported since natten " + + f"{NATTEN_HOPPER_CAUSAL_VARLEN_VERSION}, detected version: {natten_version}. " + ) + if is_mla: + reason += "Use case is MLA (head_dim_qk != head_dim_value). " + if not dtype_supported_hopper: + reason += f"Data type {dtype} is not in list of supported dtypes: {hopper_fmha_dtypes}. " + if not head_dim_supported_hopper: + reason += f"{head_dim=} with {requires_grad=} is not supported. " + log.debug(f"NATTEN backend hopper-fmha is not compatible. Reason: {reason}") + + # cutlass-fmha: targets sm50, sm70, sm75, sm80 (supports sm80+) + # limitations: none. + cutlass_fmha_dtypes = [torch.float32, torch.float16, torch.bfloat16] + dtype_supported_cutlass = dtype_supported(dtype=dtype, requires_grad=requires_grad, dtypes_fwd=cutlass_fmha_dtypes) + head_dim_supported_cutlass = head_dim % 8 == 0 + if dtype_supported_cutlass and head_dim_supported_cutlass: + return "cutlass-fmha" + else: + reason = "" + if not dtype_supported_cutlass: + reason += f"Data type {dtype} is not in list of supported dtypes: {cutlass_fmha_dtypes}. " + if not head_dim_supported_cutlass: + reason += f"{head_dim=} is not supported. " + log.debug(f"NATTEN backend cutlass-fmha is not compatible. Reason: {reason}") + + target_fn( + f"Could not find a compatible NATTEN FMHA backend for {arch_tag=}, {is_causal=}, {is_varlen=}, {is_mla=}.", + exception=RuntimeError, + ) + return None + + +def natten_attention_check( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + is_causal: bool, + causal_type: CausalType, + is_varlen: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function for the NATTEN backend. + Runs the common checks in addition to trying to find a compatible NATTEN backend. If any checks + fail, or no compatible backend is found in NATTEN, returns False. + + Parameters: + query_shape (torch.Size): Shape of 4-D query tensor (`[batch, seqlen, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D key tensor (`[batch, seqlen_kv, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D value tensor (`[batch, seqlen_kv, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + is_causal (bool): whether or not causal masking is enabled. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + is_varlen (bool): whether or not a variable length (varlen) use case. Must be inferred + beforehand based on arguments such as seqlens_{Q,KV} or cumulative_seqlen_{Q,KV} being + passed. + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with NATTEN backend. + + """ + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + if not NATTEN_SUPPORTED: + target_fn( + "NATTEN is not supported in this environment. Run with debug logs to find out why, or choose another backend.", + exception=RuntimeError, + ) + return False + + arch_tag = get_arch_tag(device) + fwd_dtypes = get_fwd_dtypes(arch_tag) + bwd_dtypes = get_bwd_dtypes(arch_tag) + if not attention_tensor_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=fwd_dtypes, + supported_dtypes_backward=bwd_dtypes, + supports_mla=True, + supports_gqa_mqa=True, + raise_error=raise_error, + backend_name="NATTEN Attention", + ): + target_fn("NATTEN does not support the given inputs.", exception=RuntimeError) + return False + + # Verifies causal_type is a CausalType instance when is_causal + # Verifies DontCare is not used unless seqlen_q == seqlen_kv + attention_param_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + is_causal=is_causal, + causal_type=causal_type, + ) + + if is_causal and causal_type not in [CausalType.TopLeft, CausalType.DontCare]: + target_fn("NATTEN Attention only supports top-left causal masking for now.", exception=RuntimeError) + return False + + natten_backend = choose_natten_backend( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + is_causal=is_causal, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=raise_error, + ) + + if natten_backend is None: + return False + + return True + + +@lru_cache +def choose_natten_multi_dim_backend( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> str | None: + """ + Chooses an FNA backend in NATTEN (cutlass-fna, hopper-fna, blackwell-fna) for the current + use case based on features needed and current GPU architecture. + + Using tensor shapes, it infers whether MLA (head_dim_value != head_dim_qk) or + GQA/MQA (heads_kv != heads_q) are required. + Using device, it infers GPU architecture and compatible backends. + Using arguments is_causal and is_varlen, and other inferred features, it picks the best + available backend. + + It is possible for no backend to be selected, if the combination of features is not available in + any one of the NATTEN backends, in which case it will return None. + + Parameters: + query_shape (torch.Size): Shape of 4-D, 5-D, or 6-D query tensor (`[batch, *token_layout_shape, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D, 5-D, or 6-D key tensor (`[batch, *token_layout_shape, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D, 5-D, or 6-D value tensor (`[batch, *token_layout_shape, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if no backend is selected, instead of just + returning None. Default is False. + + Returns: + backend (str | None): selected NATTEN backend, if any compatible. + + """ + + # Reuse choose_natten_backend instead of duplicating code + # NATTEN specifically makes sure the FNA counterparts cover all the features the FMHA kernels + # do. + fmha_backend = choose_natten_backend( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + is_causal=False, # causal masking in supported across all multi-dim (FNA) backends + is_varlen=False, # varlen is undefined (so far) for multi-dim + deterministic=deterministic, + requires_fna=True, + raise_error=raise_error, + ) + + natten_fmha_backend_to_fna_backend = { + "cutlass-fmha": "cutlass-fna", + "hopper-fmha": "hopper-fna", + "blackwell-fmha": "blackwell-fna", + } + + assert fmha_backend in natten_fmha_backend_to_fna_backend + return natten_fmha_backend_to_fna_backend[fmha_backend] + + +def natten_multi_dim_attention_check( + query_shape: torch.Size, + key_shape: torch.Size, + value_shape: torch.Size, + dtype: torch.dtype, + device: torch.device, + requires_grad: bool, + deterministic: bool = False, + raise_error: bool = False, +) -> bool: + """ + Input validation function for the NATTEN multi-dimensional backend. + Runs the common checks in addition to trying to find a compatible NATTEN backend. If any checks + fail, or no compatible backend is found in NATTEN, returns False. + + Parameters: + query_shape (torch.Size): Shape of 4-D, 5-D, or 6-D query tensor (`[batch, *token_layout_shape, heads, head_dim]`). + + key_shape (torch.Size): Shape of 4-D, 5-D, or 6-D key tensor (`[batch, *token_layout_shape, heads_kv, head_dim]`). + + value_shape (torch.Size): Shape of 4-D, 5-D, or 6-D value tensor (`[batch, *token_layout_shape, heads_kv, head_dim_v]`). + + dtype (torch.dtype): Data type of tensors. + + device (torch.device): Device of tensors. + + requires_grad (bool): Whether tensors require gradients (training vs inference). + + deterministic (bool): Deterministic backward pass required. + + raise_error (bool): whether to raise an error if any checks fail or no backend is selected, + instead of just returning False. Default is False. + + Returns: + success (bool): whether use case is compatible with NATTEN backend. + + """ + target_fn = partial(log_or_raise_error, raise_error=raise_error) + + if not NATTEN_SUPPORTED: + target_fn( + "NATTEN is not supported in this environment. Run with debug logs to find out why, or choose another backend.", + exception=RuntimeError, + ) + return False + + arch_tag = get_arch_tag(device) + fwd_dtypes = get_fwd_dtypes(arch_tag) + bwd_dtypes = get_bwd_dtypes(arch_tag) + if not multi_dim_attention_tensor_checks( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + requires_grad=requires_grad, + supported_dtypes_forward=fwd_dtypes, + supported_dtypes_backward=bwd_dtypes, + supports_mla=True, + supports_gqa_mqa=True, + raise_error=raise_error, + backend_name="NATTEN Multi-Dimensional Attention", + ): + target_fn("NATTEN does not support the given inputs.", exception=RuntimeError) + return False + + natten_backend = choose_natten_multi_dim_backend( + query_shape=query_shape, + key_shape=key_shape, + value_shape=value_shape, + dtype=dtype, + device=device, + requires_grad=requires_grad, + deterministic=deterministic, + raise_error=raise_error, + ) + + if natten_backend is None: + return False + + return True diff --git a/cosmos3/_src/imaginaire/attention/natten/functions.py b/cosmos3/_src/imaginaire/attention/natten/functions.py new file mode 100644 index 00000000..c1141986 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/natten/functions.py @@ -0,0 +1,420 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +NATTEN Backend: intermediate APIs +Only safe to import when NATTEN_SUPPORTED is True. +""" + +from contextlib import nullcontext + +from natten.context import set_memory_usage_preference, use_kv_parallelism_in_fused_na +from natten.functional import attention as _natten_attention +from natten.functional import neighborhood_attention_generic as _natten_multi_dim_attention +from torch import Tensor + +from cosmos3._src.imaginaire.attention.checks import ( + assert_universal_tensor_checks, + multi_dim_attention_param_checks, + multi_dim_attention_param_filter, +) +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.imaginaire.attention.natten import NATTEN_VARLEN_MULTI_DIM_VERSION, natten_version_satisfies +from cosmos3._src.imaginaire.attention.natten.checks import ( + choose_natten_backend, + choose_natten_multi_dim_backend, + natten_attention_check, + natten_multi_dim_attention_check, +) +from cosmos3._src.imaginaire.attention.utils import torch_deterministic_mode +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling + +set_memory_usage_preference("unrestricted") +use_kv_parallelism_in_fused_na(True) + + +def natten_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + """ + Runs NATTEN Attention on given operands (Q, K, V) with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`). + + Parameters: + query (Tensor): 4-D query tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim]`) + + key (Tensor): 4-D key tensor, with the heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor, with heads-last contiguous layout + (`[batch, seqlen_kv, heads_kv, head_dim_v]`) + + is_causal (bool): whether or not causal masking is enabled. Default is False. + + causal_type (CausalType): causal masking mode. Choices: `CausalType.TopLeft`, + `CausalType.BottomRight`. Required when `is_causal = True`. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + cumulative_seqlen_Q (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of query tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_KV` and `max_seqlen_{Q,KV}`. + + cumulative_seqlen_KV (Tensor | None): (varlen) Optional 1-D tensor with size `batch + 1` + indicating the cumulative sum of number of key/value tokens in each batch, with an + additional 0 element in the beginning. Must be passed together with + `cumulative_seqlen_Q` and `max_seqlen_{Q,KV}`. + + max_seqlen_Q (int | None): (varlen) Optional integer indicating the maximum query + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_KV`. + + max_seqlen_KV (int | None): (varlen) Optional integer indicating the maximum key/value + sequence length in all batches. Must be passed together with `cumulative_seqlen_{Q,KV}` + and `max_seqlen_Q`. + + Other Parameters: + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to NATTEN's + attention operator, if any. + + deterministic (bool): Deterministic backward pass required. + + Returns: + output (Tensor): 4-D output tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, seqlen, heads, 1]`). Only returned when return_lse is True. + """ + + is_varlen = cumulative_seqlen_Q is not None + assert_universal_tensor_checks(query, key, value) + assert natten_attention_check( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + is_causal=is_causal, + causal_type=causal_type, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=True, + ) + + # This check introduces recompiles + if not is_torch_compiling(): + if is_varlen and max_seqlen_Q == max_seqlen_KV == 0 and not natten_version_satisfies("0.21.6.dev7"): + raise NotImplementedError( + "You're trying to use varlen attention with the NATTEN backend and " + "an empty batch, which is only supported since version " + "0.21.6.dev7. Please upgrade NATTEN." + ) + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + backend_kwargs = backend_kwargs.copy() if backend_kwargs is not None else {} + + natten_backend = None + if "backend" in backend_kwargs: + natten_backend = backend_kwargs["backend"] + del backend_kwargs["backend"] + else: + natten_backend = choose_natten_backend( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + is_causal=is_causal, + is_varlen=is_varlen, + deterministic=deterministic, + raise_error=True, + ) + + assert natten_backend is not None + + # Override NATTEN's default delta reduction method: using PyTorch + # is more accurate, but slightly slower. + # Only affects NATTEN's "cutlass-fmha" backend (Ampere kernels) + backward_use_pt_reduction = True + if "backward_use_pt_reduction" in backend_kwargs: + backward_use_pt_reduction = backend_kwargs["backward_use_pt_reduction"] + del backend_kwargs["backward_use_pt_reduction"] + + with torch_deterministic_mode() if deterministic else nullcontext(): + return _natten_attention( + query=query, + key=key, + value=value, + is_causal=is_causal, + scale=scale, + cumulative_seqlen_Q=cumulative_seqlen_Q, + cumulative_seqlen_KV=cumulative_seqlen_KV, + max_seqlen_Q=max_seqlen_Q, + max_seqlen_KV=max_seqlen_KV, + return_lse=return_lse, + backend=natten_backend, + backward_use_pt_reduction=backward_use_pt_reduction, + **backend_kwargs, + ) + + +def natten_multi_dim_attention( + query: Tensor, + key: Tensor, + value: Tensor, + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + is_causal: tuple | bool = False, + scale: float | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + """ + Runs NATTEN's Multi-Dimensional Attention on given operands (Q, K, V) with the heads-last + contiguous layout (`[batch, *, heads, head_dim]`). Supports up to and including 3 dimensions: + * 1-D: `[batch, X, heads, head_dim]`, with masking arguments expecting tuples of size 1. + * 2-D: `[batch, X, Y, heads, head_dim]`, with masking arguments expecting tuples of size 2. + * 3-D: `[batch, X, Y, Z, heads, head_dim]`, with masking arguments expecting tuples of size 3. + + Parameters: + query (Tensor): 4-D, 5-D, or 6-D query tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads, head_dim]`) + + key (Tensor): 4-D, 5-D, or 6-D key tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads_kv, head_dim]`) + + value (Tensor): 4-D, 5-D, or 6-D value tensor, with heads-last contiguous layout + (`[batch, *token_layout_shape, heads_kv, head_dim_v]`) + + window_size (tuple | int): Attention window (kernel) size / shape. If an + integer, it will be repeated for all dimensions. For example `window_size=3`, when + `len(token_layout_shape) == 3`, is interpreted as `window_size=(3, 3, 3)`. + `-1`s are replaced with the corresponding `token_layout_shape`. + Final window size must satisfy `2 <= window_size <= token_layout_shape`. + Default is -1 (no sparsity). + + stride (tuple | int): Sliding window step size/shape. If an integer, it will be repeated + for all dimensions. For example `stride=2`, when `len(token_layout_shape) == 3`, is + interpreted as `stride=(2, 2, 2)`. + Final stride must satisfy `1 <= stride <= window_size`. + Default is 1. + + dilation (tuple | int): Dilation step size/shape. If an integer, it will be repeated for + all dimensions. For example `dilation=4`, when `len(token_layout_shape) == 3`, is + interpreted as `dilation=(4, 4, 4)`. + Final dilation must satisfy `2 <= dilation * window_size <= token_layout_shape`. + Default is 1. + + is_causal (tuple | bool): Toggle causal masking. If a boolean, it will be repeated for all + dimensions. For example `is_causal=True`, when `len(token_layout_shape) == 3`, is + interpreted as `is_causal=(True, True, True)`. + Default is False. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + Other Parameters: + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Key-value pair for passing arguments specific to NATTEN's + multi-dim / sparse attention operator, if any. + + deterministic (bool): Deterministic backward pass required. + + Returns: + output (Tensor): 4-D, 5-D, or 6-D output tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the heads-last contiguous layout + (`[batch, *token_layout_shape, heads, 1]`). Only returned when return_lse is True. + """ + + assert_universal_tensor_checks(query, key, value) + assert natten_multi_dim_attention_check( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + deterministic=deterministic, + raise_error=True, + ) + + token_layout, window_size, stride, dilation, is_causal = multi_dim_attention_param_filter( + query, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) + + multi_dim_attention_param_checks( + query, + window_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + ) + + scale = scale if scale is not None else query.shape[-1] ** -0.5 + + backend_kwargs = backend_kwargs.copy() if backend_kwargs is not None else {} + + natten_backend = None + if "backend" in backend_kwargs: + natten_backend = backend_kwargs["backend"] + del backend_kwargs["backend"] + else: + natten_backend = choose_natten_multi_dim_backend( + query_shape=query.shape, + key_shape=key.shape, + value_shape=value.shape, + dtype=query.dtype, + device=query.device, + requires_grad=query.requires_grad or key.requires_grad or value.requires_grad, + deterministic=deterministic, + raise_error=True, + ) + + assert natten_backend is not None + + # Override NATTEN's default delta reduction method: using PyTorch + # is more accurate, but slightly slower. + # Only affects NATTEN's "cutlass-fmha" backend (Ampere kernels) + backward_use_pt_reduction = True + if "backward_use_pt_reduction" in backend_kwargs: + backward_use_pt_reduction = backend_kwargs["backward_use_pt_reduction"] + del backend_kwargs["backward_use_pt_reduction"] + + with torch_deterministic_mode() if deterministic else nullcontext(): + return _natten_multi_dim_attention( + query=query, + key=key, + value=value, + kernel_size=window_size, + stride=stride, + dilation=dilation, + is_causal=is_causal, + scale=scale, + backend=natten_backend, + backward_use_pt_reduction=backward_use_pt_reduction, + return_lse=return_lse, + **backend_kwargs, + ) + + +def natten_multi_dim_attention_varlen( + query: Tensor, + key: Tensor, + value: Tensor, + metadata: dict, + scale: float | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + """ + Runs NATTEN's Variable-Length Multi-Dimensional Attention on given operands (Q, K, V) with + sequence-packed layout (`[batch=1, seqlen, heads, head_dim]`). + + This operation is used for sparse/multi-dimensional attention on variable-length sequences, + where tokens from different batches with different spatial layouts are concatenated along + the sequence dimension. + + **Requires NATTEN >= 0.21.9.dev0** + + Parameters: + query (Tensor): 4-D query tensor, with the sequence-packed layout + (`[1, seqlen_total, heads, head_dim]`) + + key (Tensor): 4-D key tensor, with the sequence-packed layout + (`[1, seqlen_total, heads_kv, head_dim]`) + + value (Tensor): 4-D value tensor, with sequence-packed layout + (`[1, seqlen_total, heads_kv, head_dim_v]`) + + metadata (dict): Pre-computed varlen metadata from `generate_multi_dim_varlen_parameters`. + + scale (float | None): Dot product scale (attention scale). Defaults to head_dim ** -0.5. + + Other Parameters: + return_lse (bool): Whether to return the logsumexp values. Default is False. + + backend_kwargs (dict | None): Additional backend-specific arguments. + + deterministic (bool): Deterministic backward pass required. + + Returns: + output (Tensor): 4-D output tensor, with the sequence-packed layout + (`[1, seqlen_total, heads, head_dim_v]`). + + logsumexp (Tensor): logsumexp tensor, with the sequence-packed layout + (`[1, seqlen_total, heads]`). Only returned when return_lse is True. + """ + # Check if NATTEN version supports varlen features + if not natten_version_satisfies(NATTEN_VARLEN_MULTI_DIM_VERSION): + raise RuntimeError( + f"NATTEN's varlen/varsized attention requires NATTEN >= {NATTEN_VARLEN_MULTI_DIM_VERSION}. " + "Please upgrade NATTEN to use this feature." + ) + + # Import NATTEN's varlen function (only available in NATTEN_VARLEN_MULTI_DIM_VERSION+) + from natten.varlen import neighborhood_attention_varlen + + backend_kwargs = backend_kwargs.copy() if backend_kwargs is not None else {} + + if deterministic: + raise ValueError( + "Deterministic mode is not supported for varlen multi-dimensional attention. " + "This operation requires Hopper / Blackwell FNA backends which do not support deterministic mode." + ) + + # Parameter mapping: NATTEN uses kernel_size instead of window_size + outputs = neighborhood_attention_varlen( + query=query, + key=key, + value=value, + metadata=metadata, + scale=scale, + return_lse=return_lse, + **backend_kwargs, + ) + + return outputs diff --git a/cosmos3/_src/imaginaire/attention/natten/meta.py b/cosmos3/_src/imaginaire/attention/natten/meta.py new file mode 100644 index 00000000..15d7d1a1 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/natten/meta.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +NATTEN Backend: metadata +Always safe to import (as long as torch is available.) +""" + +import torch + +from cosmos3._src.imaginaire.attention.utils.safe_ops import log + + +def get_fwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for forward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag < 75: + log.debug("NATTEN is not supported because compute capability is below the minimum (7.5).") + return [] + + if arch_tag in [100, 103]: + return [torch.float32, torch.float16, torch.bfloat16, torch.float8_e5m2, torch.float8_e4m3fn] + + return [torch.float32, torch.float16, torch.bfloat16] + + +def get_bwd_dtypes(arch_tag: int) -> list[torch.dtype]: + """ + Returns data type choices for backward pass according to arch tag (attention.utils.get_arch_tag). + + Parameters: + arch_tag (int): Arch tag for the current CUDA device. Example: 80 for A100, 90 for H100. + + Returns: + data_type_choices (list): a list of PyTorch data types. Empty if device is not supported. + + """ + + if arch_tag < 75: + log.debug("NATTEN is not supported because compute capability is below the minimum (7.5).") + return [] + + return [torch.float32, torch.float16, torch.bfloat16] diff --git a/cosmos3/_src/imaginaire/attention/natten/stubs.py b/cosmos3/_src/imaginaire/attention/natten/stubs.py new file mode 100644 index 00000000..bf2ac8bb --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/natten/stubs.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +NATTEN Backend: intermediate API stubs +Always safe to import (as long as torch is available.) +""" + +from torch import Tensor + +from cosmos3._src.imaginaire.attention.masks import CausalType + + +def natten_attention( + query: Tensor, + key: Tensor, + value: Tensor, + is_causal: bool = False, + causal_type: CausalType | None = None, + scale: float | None = None, + cumulative_seqlen_Q: Tensor | None = None, + cumulative_seqlen_KV: Tensor | None = None, + max_seqlen_Q: int | None = None, + max_seqlen_KV: int | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + raise RuntimeError( + "Tried to run NATTEN attention, but it is not supported / available. " + "Try running with debug logs enabled to see why." + ) + + +def natten_multi_dim_attention( + query: Tensor, + key: Tensor, + value: Tensor, + window_size: tuple | int = -1, + stride: tuple | int = 1, + dilation: tuple | int = 1, + is_causal: tuple | bool = False, + scale: float | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + raise RuntimeError( + "Tried to run NATTEN's Multi-Dimensional attention, but it is not supported / available. " + "Try running with debug logs enabled to see why." + ) + + +def natten_multi_dim_attention_varlen( + query: Tensor, + key: Tensor, + value: Tensor, + metadata: dict, + scale: float | None = None, + return_lse: bool = False, + backend_kwargs: dict | None = None, + deterministic: bool = False, +) -> Tensor | tuple[Tensor, Tensor]: + raise RuntimeError( + "Tried to run NATTEN's variable-length/size Multi-Dimensional attention, but it is not supported / available. " + "Try running with debug logs enabled to see why." + ) diff --git a/cosmos3/_src/imaginaire/attention/utils/__init__.py b/cosmos3/_src/imaginaire/attention/utils/__init__.py new file mode 100644 index 00000000..6c1c97b5 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/__init__.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Utilities: compute capability detection, helpers, and more. +""" + +from typing import Any + +import torch + +from cosmos3._src.imaginaire.attention.utils.determinism import torch_deterministic_mode +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling +from cosmos3._src.imaginaire.attention.utils.safe_ops import log + + +def get_arch_tag(device: torch.device | None = None) -> int: + """ + Returns the compute capability of a given torch device if it's a CUDA device, otherwise returns 0. + + Args: + device (torch.device | None): torch device. Uses default device if None. + + Returns: + device_cc (int): compute capability in the SmXXX format (i.e. 90 for Hopper). + """ + if torch.cuda.is_available() and torch.version.cuda and (device is None or device.type == "cuda"): + major, minor = torch.cuda.get_device_capability(device) + return major * 10 + minor + return 0 + + +def log_or_raise_error(msg: str, raise_error: bool = False, exception: Any = RuntimeError): + if raise_error: + raise exception(msg) + else: + log.debug(msg) + + +def is_full(dtype: torch.dtype) -> bool: + return dtype == torch.float32 + + +def is_half(dtype: torch.dtype) -> bool: + return dtype in [torch.float16, torch.bfloat16] + + +def is_fp8(dtype: torch.dtype) -> bool: + return dtype in [torch.float8_e5m2, torch.float8_e4m3fn] + + +def is_hopper(device: torch.device | None = None) -> bool: + return get_arch_tag(device) == 90 + + +def is_blackwell_dc(device: torch.device | None = None) -> bool: + return get_arch_tag(device) in [100, 103] + + +__all__ = [ + "get_arch_tag", + "log_or_raise_error", + "is_full", + "is_half", + "is_fp8", + "is_hopper", + "is_blackwell_dc", + "is_torch_compiling", + "torch_deterministic_mode", +] diff --git a/cosmos3/_src/imaginaire/attention/utils/determinism.py b/cosmos3/_src/imaginaire/attention/utils/determinism.py new file mode 100644 index 00000000..14e39ad9 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/determinism.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Utilities: deterministic mode helpers. +""" + +from contextlib import contextmanager + +import torch + + +@contextmanager +def torch_deterministic_mode(): + """Context manager that enables ``torch.use_deterministic_algorithms`` and restores the + previous state on exit (including the ``warn_only`` flag).""" + prev_mode = torch.are_deterministic_algorithms_enabled() + prev_warn_only = torch.is_deterministic_algorithms_warn_only_enabled() + torch.use_deterministic_algorithms(True) + try: + yield + finally: + torch.use_deterministic_algorithms(prev_mode, warn_only=prev_warn_only) diff --git a/cosmos3/_src/imaginaire/attention/utils/environment.py b/cosmos3/_src/imaginaire/attention/utils/environment.py new file mode 100644 index 00000000..73b5361b --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/environment.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Environment-related utilities. +""" + +import os + +import torch + +from cosmos3._src.imaginaire.utils import log + + +# Controls all regions guarded against torch compile +# Logs, and certain assertions cause graph breaks. +def is_torch_compiling() -> bool: + try: + return torch.compiler.is_compiling() + except Exception as e: + log.exception(f"Exception occurred checking whether in torch compiled region: {e}") + # Assume too old to support torch compile + return False + + +def parse_backend_filter(env_var_value: str | None, default_backends: list[str]) -> list[str]: + """ + Parse backend filter from environment variable value. + + Logic: + - If env_var_value is None or empty: return default_backends (no filtering) + - If all items start with "-": start with default_backends and remove specified backends (ban-list) + - Otherwise: only include specified backends (allow-list) + - Mixing bans and allows raises a ValueError + - For ban-list: invalid backend names only issue a warning (may not be available on this GPU) + - For allow-list: invalid backend names raise a ValueError (explicit request for unavailable backend) + + Parameters: + env_var_value (str | None): The environment variable value (comma-separated list) + default_backends (list[str]): The default list of backends + + Returns: + list[str]: Filtered list of backends + + Raises: + ValueError: If the list mixes bans and allows, or if any backend name in allow-list is invalid + """ + if not env_var_value: + return default_backends + + # Parse comma-separated list + items = [item.strip() for item in env_var_value.split(",") if item.strip()] + + if not items: + return default_backends + + # Check if items are bans or allows + bans = [item for item in items if item.startswith("-")] + allows = [item for item in items if not item.startswith("-")] + + # Validate: cannot mix bans and allows + if bans and allows: + raise ValueError( + f"Cannot mix ban-list (items starting with '-') and allow-list (items without '-') in backend filter. " + f"Got bans: {bans}, allows: {allows}. " + f"Either specify all bans (e.g., '-flash2,-cudnn') or all allows (e.g., 'natten,flash2')." + ) + + default_backends_set = set(default_backends) + + if bans: + # Ban-list mode: start with defaults and remove specified backends + ban_backends = {item[1:] for item in bans} # Remove "-" prefix + + # Warn about banned backends that don't exist in default list + # (they may not be available on this GPU, which is fine) + invalid_bans = ban_backends - default_backends_set + if invalid_bans: + log.warning( + f"Attempting to ban backend(s) that are not in the available list: {sorted(invalid_bans)}. " + f"Available backends are: {default_backends}. " + f"This may be expected if these backends are not supported on this GPU." + ) + + filtered = [b for b in default_backends if b not in ban_backends] + log.debug(f"Backend filter (ban-list): removing {ban_backends} from {default_backends}, result: {filtered}") + return filtered + else: + # Allow-list mode: only include specified backends + allow_backends = set(allows) + + # Validate: all allowed backends must exist in default list + invalid_allows = allow_backends - default_backends_set + if invalid_allows: + raise ValueError( + f"Invalid backend(s) in allow-list: {sorted(invalid_allows)}. " + f"Available backends are: {default_backends}. " + f"Check for typos in the backend names." + ) + + # Preserve order from default_backends for consistency + filtered = [b for b in default_backends if b in allow_backends] + log.debug(f"Backend filter (allow-list): allowing {allow_backends} from {default_backends}, result: {filtered}") + return filtered + + +def filter_attention_backends(default_backends: list[str]) -> list[str]: + env_var_value = os.environ.get("I4_ATTN_BACKENDS") + return parse_backend_filter(env_var_value, default_backends) + + +def filter_multi_dim_attention_backends(default_backends: list[str]) -> list[str]: + env_var_value = os.environ.get("I4_ATTN_BACKENDS_MULTIDIM") + return parse_backend_filter(env_var_value, default_backends) + + +def filter_attention_merge_backends(default_backends: list[str]) -> list[str]: + env_var_value = os.environ.get("I4_ATTN_BACKENDS_MERGE") + return parse_backend_filter(env_var_value, default_backends) diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py b/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py new file mode 100644 index 00000000..1fdaf020 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Safe operations for torch.compile: operations that should be disabled or modified +when in a torch.compiled regions. +""" + +from cosmos3._src.imaginaire.attention.utils.safe_ops import functools, log + +__all__ = ["log", "functools"] diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py b/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py new file mode 100644 index 00000000..6a498ee8 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +torch.compile-safe functools wrappers (specifically lru_cache). +""" + +import functools + + +def lru_cache(maxsize=128, typed=False): + """ + A torch.compile-safe wrapper around functools.lru_cache. + + This decorator automatically disables caching when inside a torch-compiled region. + torch.compile ignores lru_cache and raises warnings; since torch.compile acts as a + higher-level cache itself, lru_cache becomes redundant and we disable it to avoid warnings. + + When not in a torch-compiled region, behaves exactly like functools.lru_cache. + When in a torch-compiled region it's a no-op. + """ + + def decorator(func): + # Create the cached version using lru_cache + cached_func = functools.lru_cache(maxsize=maxsize, typed=typed)(func) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Check if we're in a torch-compiled region + from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling + + if is_torch_compiling(): + # Bypass cache during compilation + return func(*args, **kwargs) + else: + # Use cached version normally + return cached_func(*args, **kwargs) + + # Expose cache_clear and cache_info methods when not compiling + wrapper.cache_clear = cached_func.cache_clear + wrapper.cache_info = cached_func.cache_info + wrapper.__wrapped__ = func + + return wrapper + + # Support both @lru_cache and @lru_cache() syntax + # If called without parentheses (maxsize is actually the function) + if callable(maxsize): + func = maxsize + maxsize = 128 + return decorator(func) + + return decorator diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py b/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py new file mode 100644 index 00000000..a2b3332e --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +torch.compile-safe log wrappers. +""" + +from cosmos3._src.imaginaire.attention.utils.environment import is_torch_compiling +from cosmos3._src.imaginaire.utils import log + + +def trace(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.trace(message=message, rank0_only=rank0_only) + + +def debug(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.debug(message=message, rank0_only=rank0_only) + + +def info(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.info(message=message, rank0_only=rank0_only) + + +def success(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.success(message=message, rank0_only=rank0_only) + + +def warning(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.warning(message=message, rank0_only=rank0_only) + + +def error(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.critical(message=message, rank0_only=rank0_only) + + +def critical(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.critical(message=message, rank0_only=rank0_only) + + +def exception(message: str, rank0_only: bool = True) -> None: + if not is_torch_compiling(): + log.exception(message=message, rank0_only=rank0_only) diff --git a/cosmos3/_src/imaginaire/attention/utils/version.py b/cosmos3/_src/imaginaire/attention/utils/version.py new file mode 100644 index 00000000..57da5547 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/utils/version.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Utilities: version checking helpers shared across backends. +""" + +from packaging.version import Version + + +def parse_version(version_str: str) -> Version | None: + """Parse a version string into a ``packaging.version.Version``, returning ``None`` on failure.""" + try: + return Version(version_str) + except Exception: + return None + + +def version_at_least(version_str: str, min_version: str) -> bool: + """Return ``True`` if *version_str* >= *min_version*. Returns ``False`` on parse failure.""" + v = parse_version(version_str) + m = parse_version(min_version) + if v is None or m is None: + return False + return v >= m + + +def version_in_range(version_str: str, min_version: str, max_version: str) -> bool: + """Return ``True`` if *min_version* <= *version_str* <= *max_version*. Returns ``False`` on parse failure.""" + v = parse_version(version_str) + lo = parse_version(min_version) + hi = parse_version(max_version) + if v is None or lo is None or hi is None: + return False + return lo <= v <= hi diff --git a/cosmos3/_src/imaginaire/attention/varlen.py b/cosmos3/_src/imaginaire/attention/varlen.py new file mode 100644 index 00000000..596887b9 --- /dev/null +++ b/cosmos3/_src/imaginaire/attention/varlen.py @@ -0,0 +1,225 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Imaginaire4 Attention Subpackage: +Unified implementation for all Attention implementations. + +Varlen utilities +""" + +import torch +from torch import Tensor + +from cosmos3._src.imaginaire.attention.utils import is_torch_compiling + + +def generate_varlen_parameters( + query: Tensor, # [1,S_total_Q,H,D] + key: Tensor, # [1,S_total_KV,H_KV,D] + value: Tensor, # [1,S_total_KV,H_KV,D_V] + seqlens_Q: Tensor | None = None, # [B] + seqlens_KV: Tensor | None = None, # [B] +) -> ( + tuple[None, None, int, int] | tuple[Tensor, Tensor, int, int] +): # (cumseqlen_Q[B+1], cumseqlen_KV[B+1], max_seqlen_Q, max_seqlen_KV) + + # which we launch the varlen kernel) and not device tensors. + # .item() introduces control flow and breaks the graph. + # It is also inefficient to repeat this per-op, and mostly there for convenience. + # generate_varlen_parameters should ideally always be called by the user ahead of model + # forward / backward. + if is_torch_compiling(): + raise RuntimeError( + "Running 'generate_varlen_parameters' in a torch-compiled region is disallowed as it " + "results in graph breaks. Please consider calling ahead of time and pass " + "'cumulative_seqlen_{Q,KV}' and 'max_seqlen_{Q,KV}' instead of 'seqlens_{Q,KV}' to " + "'attention'. " + ) + + if query.shape[0] != key.shape[0] or query.shape[0] != value.shape[0]: + raise ValueError( + f"Q, K, and V must match in batch size, got {query.shape[0]=}, {key.shape[0]=}, {value.shape[0]=}." + ) + + if (seqlens_Q is None) ^ (seqlens_KV is None): + raise ValueError( + "Variable length Attention requires both of seqlens_Q and seqlens_KV to be set, got " + f"{seqlens_Q=}, {seqlens_KV=}." + ) + + if seqlens_Q is None and seqlens_KV is None: + # Not varlen + return None, None, 0, 0 + + assert seqlens_Q is not None + assert seqlens_KV is not None + + if not isinstance(seqlens_Q, Tensor) or not isinstance(seqlens_KV, Tensor): + raise ValueError("seqlens_Q and seqlens_KV must both be tensors.") + + if seqlens_Q.device != query.device or seqlens_KV.device != query.device: + raise ValueError( + "seqlens_Q and seqlens_KV must be on the same device as QKV, but " + f"{seqlens_Q.device=}, {seqlens_KV.device=}, {query.device=}." + ) + + if seqlens_Q.dtype != torch.int32 or seqlens_KV.dtype != torch.int32: + raise ValueError( + f"seqlens_Q and seqlens_KV must both be torch.int32 tensors, got {seqlens_Q.dtype=}, {seqlens_KV.dtype=}." + ) + + if seqlens_Q.dim() != 1 or seqlens_KV.dim() != 1: + raise ValueError( + f"seqlens_Q and seqlens_KV must both be 1-D tensors, got {seqlens_Q.dim()=}, {seqlens_KV.dim()=}." + ) + + if seqlens_Q.shape[0] != seqlens_KV.shape[0]: + raise ValueError(f"seqlens_Q and seqlens_KV must match in size, got {seqlens_Q.shape=}, {seqlens_KV.shape=}.") + + if seqlens_Q.shape[0] < 1: + raise ValueError( + f"seqlens_Q and seqlens_KV must contain at least one element, got {seqlens_Q.shape=}, {seqlens_KV.shape=}." + ) + + if query.shape[0] != 1: + raise ValueError( + f"Variable length attention only supports sequence-packed memory layout (batch = 1), got {query.shape[0]=}." + ) + + assert seqlens_Q.dim() == seqlens_KV.dim() == 1 + assert seqlens_Q.shape[0] == seqlens_KV.shape[0] >= 1 + assert seqlens_Q.dtype == seqlens_KV.dtype == torch.int32 + + max_seqlen_Q = seqlens_Q.max().item() # type: ignore + max_seqlen_KV = seqlens_KV.max().item() # type: ignore + + if max_seqlen_Q < 0 or max_seqlen_KV < 0: + raise ValueError(f"max_seqlen_Q and max_seqlen_KV cannot be negative, got {max_seqlen_Q=}, {max_seqlen_KV=}.") + + + # This feature may require support in the backends themselves; see NATTEN PR: + # https://github.com/SHI-Labs/NATTEN/pull/327 + if (max_seqlen_Q == 0) != (max_seqlen_KV == 0): + raise ValueError( + "max_seqlen_Q and max_seqlen_KV must either both be 0 or both be positive, " + f"but computed {max_seqlen_Q=}, {max_seqlen_KV=} from provided seqlens." + ) + + + z = torch.tensor([0], dtype=torch.int32, device=seqlens_Q.device) # [1] + cumulative_seqlen_Q = torch.cat([z, seqlens_Q.cumsum(0).to(torch.int32)], dim=0) # [B+1] + cumulative_seqlen_KV = torch.cat([z, seqlens_KV.cumsum(0).to(torch.int32)], dim=0) # [B+1] + + assert isinstance(max_seqlen_Q, int) + assert isinstance(max_seqlen_KV, int) + + return ( + cumulative_seqlen_Q, + cumulative_seqlen_KV, + max_seqlen_Q, + max_seqlen_KV, + ) + + +def generate_multi_dim_varlen_parameters( + token_layout_list: list, + head_dim: int, + device: torch.device, + dtype: torch.dtype, + requires_grad: bool, + window_size_list: list | None = None, + stride_list: list | None = None, + dilation_list: list | None = None, + is_causal: tuple | bool = False, + *args, + **kwargs, +) -> dict: + """ + Configures metadata for variable-length multi-dimensional attention operations. + + This function prepares the metadata needed for varlen/varsized sparse attention, + including backend selection and tile configurations. The metadata should be generated + ahead of time (outside of torch.compile regions) and reused across forward/backward passes. + + **Requires NATTEN >= 0.21.9.dev0** + + Parameters: + token_layout_list (list): List of token layout tuples describing the spatial arrangement + of tokens for each sequence. For example, for 2D attention with two sequences of + sizes (H1, W1) and (H2, W2), pass [(H1, W1), (H2, W2)]. + + head_dim (int): Attention head dimension. + + device (torch.device): Target device for runtime. + + dtype (torch.dtype): Tensor element type. + + requires_grad (bool): Whether tensors will require backward pass. + + window_size_list (list | None): Per-sequence window sizes for variable kernel sizes. + + stride_list (list | None): Per-sequence stride values for variable strides. + + dilation_list (list | None): Per-sequence dilation values for variable dilations. + + is_causal (tuple | bool): Toggle causal masking. Default is False. + + Returns: + dict: Runtime metadata for varlen operations. This dict should be passed to + `natten_multi_dimensional_attention_varlen` as the `metadata` parameter. + """ + # For now, NATTEN is the only backend that supports varlen multi-dimensional attention + + from cosmos3._src.imaginaire.attention.natten import NATTEN_VARLEN_MULTI_DIM_VERSION, natten_supported, natten_version_satisfies + + if not natten_supported(): + raise RuntimeError("generate_multi_dim_varlen_parameters requires NATTEN.") + + if not natten_version_satisfies(NATTEN_VARLEN_MULTI_DIM_VERSION): + raise RuntimeError( + f"generate_multi_dim_varlen_parameters requires NATTEN >= {NATTEN_VARLEN_MULTI_DIM_VERSION}. " + "Please upgrade NATTEN to use varlen/varsized attention features." + ) + + from natten.varlen import configure_varlen + + # Map -1s in window size list to full attention + if window_size_list is None: + window_size_list_filtered = [token_layout for token_layout in token_layout_list] + else: + window_size_list_filtered = [] + for window_size, token_layout in zip(window_size_list, token_layout_list): + window_size_filtered = tuple(k if k > 0 else x for k, x in zip(window_size, token_layout)) + window_size_list_filtered.append(window_size_filtered) + + metadata = configure_varlen( + token_layout_list=token_layout_list, + head_dim=head_dim, + device=device, + dtype=dtype, + requires_grad=requires_grad, + is_causal=is_causal, + kernel_size=None, + stride=None, + dilation=None, + kernel_size_list=window_size_list_filtered, + stride_list=stride_list, + dilation_list=dilation_list, + *args, + **kwargs, + ) + + return metadata diff --git a/cosmos3/_src/imaginaire/auxiliary/__init__.py b/cosmos3/_src/imaginaire/auxiliary/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py new file mode 100644 index 00000000..53e3259a --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py @@ -0,0 +1,248 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import re +import string +from difflib import SequenceMatcher + +import nltk +from better_profanity import profanity + +from cosmos3._src.imaginaire.auxiliary.guardrail.blocklist.utils import read_keyword_list_from_dir, to_ascii +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import ( + GUARDRAIL1_CHECKPOINT, + ContentSafetyGuardrail, + GuardrailRunner, +) +from cosmos3._src.imaginaire.utils import log, misc + +CENSOR = misc.Color.red("*") + + +class Blocklist(ContentSafetyGuardrail): + def __init__( + self, + guardrail_partial_match_min_chars: int = 6, + guardrail_partial_match_letter_count: float = 0.4, + ) -> None: + """Blocklist model for text filtering safety check. + + Args: + checkpoint_dir (str): Path to the checkpoint directory. + guardrail_partial_match_min_chars (int, optional): Minimum number of characters in a word to check for partial match. Defaults to 6. + guardrail_partial_match_letter_count (float, optional): Maximum allowed difference in characters for partial match. Defaults to 0.4. + """ + self.checkpoint_dir = os.path.join(GUARDRAIL1_CHECKPOINT.download(), "blocklist") + nltk.data.path.append(os.path.join(self.checkpoint_dir, "nltk_data")) + self.lemmatizer = nltk.WordNetLemmatizer() + self.profanity = profanity + self.guardrail_partial_match_min_chars = guardrail_partial_match_min_chars + self.guardrail_partial_match_letter_count = guardrail_partial_match_letter_count + + # Load blocklist and whitelist keywords + self.blocklist_words = read_keyword_list_from_dir(os.path.join(self.checkpoint_dir, "custom")) + self.whitelist_words = read_keyword_list_from_dir(os.path.join(self.checkpoint_dir, "whitelist")) + self.exact_match_words = read_keyword_list_from_dir(os.path.join(self.checkpoint_dir, "exact_match")) + + self.profanity.load_censor_words(custom_words=self.blocklist_words, whitelist_words=self.whitelist_words) + log.debug(f"Loaded {len(self.blocklist_words)} words/phrases from blocklist") + log.debug(f"Whitelisted {len(self.whitelist_words)} words/phrases from whitelist") + log.debug(f"Loaded {len(self.exact_match_words)} exact match words/phrases from blocklist") + + def uncensor_whitelist(self, input_prompt: str, censored_prompt: str) -> str: + """Explicitly uncensor words that are in the whitelist.""" + input_words = input_prompt.split() + censored_words = censored_prompt.split() + whitelist_words = set(self.whitelist_words) + for i, token in enumerate(input_words): + if token.strip(string.punctuation).lower() in whitelist_words: + censored_words[i] = token + censored_prompt = " ".join(censored_words) + return censored_prompt + + def censor_prompt(self, input_prompt: str) -> tuple[bool, str]: + """Censor the prompt using the blocklist with better-profanity fuzzy matching. + + Args: + input_prompt: input prompt to censor + + Returns: + bool: True if the prompt is blocked, False otherwise + str: A message indicating why the prompt was blocked + """ + censored_prompt = self.profanity.censor(input_prompt, censor_char=CENSOR) + # Uncensor whitelisted words that were censored from blocklist fuzzy matching + censored_prompt = self.uncensor_whitelist(input_prompt, censored_prompt) + if CENSOR in censored_prompt: + return True, f"Prompt blocked by censorship: Censored Prompt: {censored_prompt}" + return False, "" + + @staticmethod + def check_partial_match( + normalized_prompt: str, normalized_word: str, guardrail_partial_match_letter_count: float + ) -> tuple[bool, str]: + """ + Check robustly if normalized word and the matching target have a difference of up to guardrail_partial_match_letter_count characters. + + Args: + normalized_prompt: a string with many words + normalized_word: a string with one or multiple words, its length is smaller than normalized_prompt + guardrail_partial_match_letter_count: maximum allowed difference in characters (float to allow partial characters) + + Returns: + bool: True if a match is found, False otherwise + str: A message indicating why the prompt was blocked + """ + prompt_words = normalized_prompt.split() + word_length = len(normalized_word.split()) + max_similarity_ratio = (len(normalized_word) - float(guardrail_partial_match_letter_count)) / float( + len(normalized_word) + ) + + seq_matcher = SequenceMatcher(None) + seq_matcher.set_seq2(normalized_word) + + for i in range(len(prompt_words) - word_length + 1): + # Extract a substring from the prompt with the same number of words as the normalized_word + substring = " ".join(prompt_words[i : i + word_length]) + seq_matcher.set_seq1(substring) + + # real_quick_ratio and quick_ratio are faster than ratio and both serve as upper bound for similarity ratio. + # If they are less than max_similarity_ratio, it means that also the ratio will be less than max_similarity_ratio and we can skip the expensive ratio computation. + # This saves a lot of time because in practice the tested words are usually dissimilar. + # For details see: https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher + if ( + seq_matcher.real_quick_ratio() < max_similarity_ratio + or seq_matcher.quick_ratio() < max_similarity_ratio + ): + continue + + similarity_ratio = seq_matcher.ratio() + if similarity_ratio >= max_similarity_ratio: + return ( + True, + f"Prompt blocked by partial match blocklist: Prompt: {normalized_prompt}, Partial Match Word: {normalized_word}", + ) + + return False, "" + + @staticmethod + def check_against_whole_word_blocklist( + prompt: str, + blocklist: list[str], + guardrail_partial_match_min_chars: int = 6, + guardrail_partial_match_letter_count: float = 0.4, + ) -> tuple[bool, str]: + """ + Check if the prompt contains any whole words from the blocklist. + The match is case insensitive and robust to multiple spaces between words. + + Args: + prompt: input prompt to check + blocklist: list of words to check against + guardrail_partial_match_min_chars: minimum number of characters in a word to check for partial match + guardrail_partial_match_letter_count: maximum allowed difference in characters for partial match + + Returns: + tuple[bool, str]: (True if a match is found, False otherwise), message indicating why the prompt was blocked + """ + # Normalize spaces and convert to lowercase + normalized_prompt = re.sub(r"\s+", " ", prompt).strip().lower() + + normalized_words_cache = set() + + for word in blocklist: + # Normalize spaces and convert to lowercase for each blocklist word + normalized_word = re.sub(r"\s+", " ", word).strip().lower() + + if normalized_word in normalized_words_cache: + continue + + normalized_words_cache.add(normalized_word) + + # Use word boundaries to ensure whole word match + if re.search(r"\b" + re.escape(normalized_word) + r"\b", normalized_prompt): + return True, f"Prompt blocked by exact match blocklist: Prompt: {prompt}, Exact Match Word: {word}" + + # Roughly 3/4 of the time this function requires is spent on partial matching. + # We could use just one for loop to check both exact and partial matches but doing it in two loops is faster in practice + # because it delays the partial matching as long as possible with a chance of early exit due to exact match. + # Above we cache the normalized words and here we reuse them in the second loop for partial matching. + + for normalized_word in normalized_words_cache: + # Check for partial match if the word is long enough + if len(normalized_word) >= guardrail_partial_match_min_chars: + match, message = Blocklist.check_partial_match( + normalized_prompt, normalized_word, guardrail_partial_match_letter_count + ) + if match: + return True, message + + return False, "" + + def is_safe(self, input_prompt: str = "") -> tuple[bool, str]: + """Check if the input prompt is safe using the blocklist.""" + # Check if the input is empty + if not input_prompt: + return False, "Input is empty" + input_prompt = to_ascii(input_prompt) + + # Check full sentence for censored words + censored, message = self.censor_prompt(input_prompt) + if censored: + return False, message + + # Check lemmatized words for censored words + tokens = nltk.word_tokenize(input_prompt) + lemmas = [self.lemmatizer.lemmatize(token) for token in tokens] + lemmatized_prompt = " ".join(lemmas) + censored, message = self.censor_prompt(lemmatized_prompt) + if censored: + return False, message + + # Check for exact match blocklist words + censored, message = self.check_against_whole_word_blocklist( + input_prompt, + self.exact_match_words, + self.guardrail_partial_match_min_chars, + self.guardrail_partial_match_letter_count, + ) + if censored: + return False, message + + # If all these checks pass, the input is safe + return True, "Input is safe" + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--prompt", type=str, required=True, help="Input prompt") + return parser.parse_args() + + +def main(args): + blocklist = Blocklist() + runner = GuardrailRunner(safety_models=[blocklist]) + with misc.timer("blocklist safety check"): + safety, message = runner.run_safety_check(args.prompt) + log.info(f"Input is: {'SAFE' if safety else 'UNSAFE'}") + log.info(f"Message: {message}") if not safety else None + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py new file mode 100644 index 00000000..ad03cd5b --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +from cosmos3._src.imaginaire.auxiliary.guardrail.blocklist.blocklist import Blocklist +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import GuardrailRunner + +# Test prompts +prompts = [ + "Create a beautiful landscape", + "Generate a detailed cinematic scene of a bustling medieval marketplace with merchants selling colorful spices, fabrics hanging from wooden stalls, cobblestone streets filled with people in period clothing, warm golden hour lighting filtering through ancient stone buildings, and smoke rising from food vendors cooking over open fires", + "Create an image of Adele", + "lot of words" * 100 + "Create an image of Adele", + "lot of words" * 100 + "Create an image of Adeele", + "Make an image of someone getting hurt badly", + "Generate a disturbing scene with graphic violence showing people fighting brutally with weapons, blood splattered everywhere, gruesome injuries visible in high detail, screaming faces contorted in pain and terror, dark atmosphere with dramatic lighting emphasizing the carnage and destruction, bodies lying motionless on the ground surrounded by pools of crimson liquid", + "Show me a peaceful garden with flowers blooming in spring sunlight", + "Design a futuristic space station orbiting Earth with advanced technology, sleek metallic surfaces reflecting starlight, astronauts in cutting-edge spacesuits conducting research, multiple docking bays with various spacecraft, solar panels gleaming in the cosmic void, and Earth's blue marble visible in the background through massive observation windows", +] + +checkpoint_dir = "/path/to/your/checkpoint/dir" # Change this path + +# Initialize +blocklist = Blocklist(checkpoint_dir=checkpoint_dir) +runner = GuardrailRunner(safety_models=[blocklist]) + +# Warm up +_ = runner.run_safety_check(prompts[0]) + + +times = [] +for prompt in prompts: + start = time.time() + safe, message = runner.run_safety_check(prompt) + end = time.time() + + elapsed = end - start + times.append(elapsed) + + print(f"Prompt: '{prompt[:50]}...'") + print(f"Safe: {safe}, Time: {elapsed:.4f}s") + if message: + print(f"Message: {message}") + print("-" * 40) + +print(f"\nAverage time: {sum(times) / len(times):.4f}s") diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py new file mode 100644 index 00000000..9e72484d --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +from cosmos3._src.imaginaire.utils import log + + +def read_keyword_list_from_dir(folder_path: str) -> list[str]: + """Read keyword list from all files in a folder.""" + output_list = [] + file_list = [] + # Get list of files in the folder + for file in os.listdir(folder_path): + if os.path.isfile(os.path.join(folder_path, file)): + file_list.append(file) + + # Process each file + for file in file_list: + file_path = os.path.join(folder_path, file) + try: + with open(file_path) as f: + output_list.extend([line.strip() for line in f.readlines()]) + except Exception as e: + log.error(f"Error reading file {file}: {e!s}") + + return output_list + + +def to_ascii(prompt: str) -> str: + """Convert prompt to ASCII.""" + return re.sub(r"[^\x00-\x7F]+", " ", prompt) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py new file mode 100644 index 00000000..c6bb336f --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +import numpy as np + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.checkpoint_db import ( + CheckpointDirHf, +) + +GUARDRAIL1_CHECKPOINT = CheckpointDirHf( + repository="nvidia/Cosmos-Guardrail1", + revision="d6d4bfa899a71454a700907664f3e88f503950cf", +) + + +class ContentSafetyGuardrail: + def is_safe(self, **kwargs) -> tuple[bool, str]: + raise NotImplementedError("Child classes must implement the is_safe method") + + +class PostprocessingGuardrail: + def postprocess(self, frames: np.ndarray) -> np.ndarray: + raise NotImplementedError("Child classes must implement the postprocess method") + + +class GuardrailRunner: + def __init__( + self, + safety_models: list[ContentSafetyGuardrail] | None = None, + generic_block_msg: str = "", + generic_safe_msg: str = "", + postprocessors: list[PostprocessingGuardrail] | None = None, + ): + self.safety_models = safety_models + self.generic_block_msg = generic_block_msg + self.generic_safe_msg = generic_safe_msg if generic_safe_msg else "Prompt is safe" + self.postprocessors = postprocessors + + def run_safety_check(self, input: Any) -> tuple[bool, str]: + """Run the safety check on the input.""" + if not self.safety_models: + log.warning("No safety models found, returning safe") + return True, self.generic_safe_msg + + for guardrail in self.safety_models: + guardrail_name = str(guardrail.__class__.__name__).upper() + log.debug(f"Running guardrail: {guardrail_name}") + safe, message = guardrail.is_safe(input) + if not safe: + reasoning = self.generic_block_msg if self.generic_block_msg else f"{guardrail_name}: {message}" + return False, reasoning + return True, self.generic_safe_msg + + def postprocess(self, frames: np.ndarray) -> np.ndarray: + """Run the postprocessing on the video frames.""" + if not self.postprocessors: + log.warning("No postprocessors found, returning original frames") + return frames + + for guardrail in self.postprocessors: + guardrail_name = str(guardrail.__class__.__name__).upper() + log.debug(f"Running guardrail: {guardrail_name}") + frames = guardrail.postprocess(frames) + return frames diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py new file mode 100644 index 00000000..8a2183ef --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import glob +from dataclasses import dataclass + +import imageio +import numpy as np + +from cosmos3._src.imaginaire.utils import log + + +@dataclass +class VideoData: + frames: np.ndarray # Shape: [B, H, W, C] + fps: int + duration: int # in seconds + + +def get_video_filepaths(input_dir: str) -> list[str]: + """Get a list of filepaths for all videos in the input directory.""" + paths = glob.glob(f"{input_dir}/**/*.mp4", recursive=True) + paths += glob.glob(f"{input_dir}/**/*.avi", recursive=True) + paths += glob.glob(f"{input_dir}/**/*.mov", recursive=True) + paths = sorted(paths) + log.debug(f"Found {len(paths)} videos") + return paths + + +def read_video(filepath: str) -> VideoData: + """Read a video file and extract its frames and metadata.""" + try: + reader = imageio.get_reader(filepath, "ffmpeg") + except Exception as e: + raise ValueError(f"Failed to read video file: {filepath}") from e + + # Extract metadata from the video file + try: + metadata = reader.get_meta_data() + fps = metadata.get("fps") + duration = metadata.get("duration") + except Exception as e: + reader.close() + raise ValueError(f"Failed to extract metadata from video file: {filepath}") from e + + # Extract frames from the video file + try: + frames = np.array([frame for frame in reader]) + except Exception as e: + raise ValueError(f"Failed to extract frames from video file: {filepath}") from e + finally: + reader.close() + + return VideoData(frames=frames, fps=fps, duration=duration) + + +def save_video(filepath: str, frames: np.ndarray, fps: int) -> None: + """Save a video file from a sequence of frames.""" + try: + writer = imageio.get_writer(filepath, fps=fps, macro_block_size=1) + for frame in frames: + writer.append_data(frame) + except Exception as e: + raise ValueError(f"Failed to save video file to {filepath}") from e + finally: + writer.close() diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py new file mode 100644 index 00000000..1a6a7647 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from cosmos3._src.imaginaire.auxiliary.guardrail.blocklist.blocklist import Blocklist +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import GuardrailRunner +from cosmos3._src.imaginaire.auxiliary.guardrail.face_blur_filter.face_blur_filter import RetinaFaceFilter +from cosmos3._src.imaginaire.auxiliary.guardrail.qwen3guard.qwen3guard import Qwen3Guard +from cosmos3._src.imaginaire.auxiliary.guardrail.video_content_safety_filter.video_content_safety_filter import ( + VideoContentSafetyFilter, +) +from cosmos3._src.imaginaire.utils import log + + +def create_text_guardrail_runner(offload_model_to_cpu: bool = False) -> GuardrailRunner: + """Create the text guardrail runner.""" + return GuardrailRunner( + safety_models=[ + Blocklist(), + Qwen3Guard(offload_model_to_cpu=offload_model_to_cpu), + ] + ) + + +def create_video_guardrail_runner(offload_model_to_cpu: bool = False) -> GuardrailRunner: + """Create the video guardrail runner.""" + return GuardrailRunner( + safety_models=[VideoContentSafetyFilter(offload_model_to_cpu=offload_model_to_cpu)], + postprocessors=[RetinaFaceFilter(offload_model_to_cpu=offload_model_to_cpu)], + ) + + +def run_text_guardrail(prompt: str, guardrail_runner: GuardrailRunner) -> bool: + """Run the text guardrail on the prompt, checking for content safety. + + Args: + prompt: The text prompt. + guardrail_runner: The text guardrail runner. + + Returns: + bool: Whether the prompt is safe. + """ + is_safe, message = guardrail_runner.run_safety_check(prompt) + if not is_safe: + log.critical(f"GUARDRAIL BLOCKED: {message}") + return is_safe + + +def run_video_guardrail(frames: np.ndarray, guardrail_runner: GuardrailRunner) -> np.ndarray | None: + """Run the video guardrail on the frames, checking for content safety and applying face blur. + + Args: + frames: The frames of the generated video. + guardrail_runner: The video guardrail runner. + + Returns: + The processed frames if safe, otherwise None. + """ + is_safe, message = guardrail_runner.run_safety_check(frames) + if not is_safe: + log.critical(f"GUARDRAIL BLOCKED: {message}") + return None + + frames = guardrail_runner.postprocess(frames) + return frames diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py new file mode 100644 index 00000000..d52f69d2 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np + + +def pixelate_face(face_img: np.ndarray, blocks: int = 5) -> np.ndarray: + """ + Pixelate a face region by reducing resolution and then upscaling. + + Args: + face_img: Face region to pixelate + blocks: Number of blocks to divide the face into (in each dimension) + + Returns: + Pixelated face region + """ + h, w = face_img.shape[:2] + # Shrink the image and scale back up to create pixelation effect + temp = cv2.resize(face_img, (blocks, blocks), interpolation=cv2.INTER_LINEAR) + pixelated = cv2.resize(temp, (w, h), interpolation=cv2.INTER_NEAREST) + return pixelated diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py new file mode 100644 index 00000000..657cc99f --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import warnings + +import numpy as np +import torch +from retinaface.data import cfg_re50 +from retinaface.layers.functions.prior_box import PriorBox +from retinaface.models.retinaface import RetinaFace +from tqdm import tqdm + +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import ( + GUARDRAIL1_CHECKPOINT, + GuardrailRunner, + PostprocessingGuardrail, +) +from cosmos3._src.imaginaire.auxiliary.guardrail.common.io_utils import ( + get_video_filepaths, + read_video, + save_video, +) +from cosmos3._src.imaginaire.auxiliary.guardrail.face_blur_filter.blur_utils import pixelate_face +from cosmos3._src.imaginaire.auxiliary.guardrail.face_blur_filter.retinaface_utils import ( + decode_batch, + filter_detected_boxes, + load_model, +) +from cosmos3._src.imaginaire.utils import log, misc + +# RetinaFace model constants from https://github.com/biubug6/Pytorch_Retinaface/blob/master/detect.py +TOP_K = 5_000 +KEEP_TOP_K = 750 +NMS_THRESHOLD = 0.4 + + +class RetinaFaceFilter(PostprocessingGuardrail): + def __init__( + self, + batch_size: int = 1, + confidence_threshold: float = 0.7, + offload_model_to_cpu: bool = True, + ) -> None: + """ + Initialize the RetinaFace model for face detection and blurring. + + Args: + checkpoint: Path to the RetinaFace checkpoint file + batch_size: Batch size for RetinaFace inference and processing + confidence_threshold: Minimum confidence score to consider a face detection + offload_model_to_cpu (bool, optional): Whether to offload the model to CPU. Defaults to True. + """ + self.checkpoint = f"{GUARDRAIL1_CHECKPOINT.download()}/face_blur_filter/Resnet50_Final.pth" + self.cfg = cfg_re50 + self.batch_size = batch_size + self.confidence_threshold = confidence_threshold + self.dtype = torch.float32 + self.offload_model = offload_model_to_cpu + + # Disable loading ResNet pretrained weights + self.cfg["pretrain"] = False + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.net = RetinaFace(cfg=self.cfg, phase="test") + + # Load from RetinaFace pretrained checkpoint + if not offload_model_to_cpu: + self.net = load_model(self.net, self.checkpoint, False) + self.net.to("cuda", dtype=self.dtype).eval() + log.debug("Moved face blur filter to GPU") + else: + self.net = load_model(self.net, self.checkpoint, True) + self.net.to("cpu", dtype=self.dtype).eval() + log.debug("Moved face blur filter to CPU") + + def preprocess_frames(self, frames: np.ndarray) -> torch.Tensor: + """Preprocess a sequence of frames for face detection. + + Args: + frames: Input frames + + Returns: + Preprocessed frames tensor + """ + with torch.no_grad(): + frames_tensor = torch.from_numpy(frames).to("cuda", dtype=self.dtype) # Shape: [T, H, W, C] + frames_tensor = frames_tensor.permute(0, 3, 1, 2) # Shape: [T, C, H, W] + frames_tensor = frames_tensor[:, [2, 1, 0], :, :] # RGB to BGR to match RetinaFace model input + means = torch.tensor([104.0, 117.0, 123.0], device="cuda", dtype=self.dtype).view(1, 3, 1, 1) + frames_tensor = frames_tensor - means # Subtract mean BGR values for each channel + return frames_tensor + + def blur_detected_faces( + self, + frames: np.ndarray, + batch_loc: torch.Tensor, + batch_conf: torch.Tensor, + prior_data: torch.Tensor, + scale: torch.Tensor, + min_size: tuple[int] = (20, 20), + ) -> list[np.ndarray]: + """Blur detected faces in a batch of frames using RetinaFace predictions. + + Args: + frames: Input frames + batch_loc: Batched location predictions + batch_conf: Batched confidence scores + prior_data: Prior boxes for the video + scale: Scale factor for resizing detections + min_size: Minimum size of a detected face region in pixels + + Returns: + Processed frames with pixelated faces + """ + with torch.no_grad(): + batch_boxes = decode_batch(batch_loc, prior_data, self.cfg["variance"]) + batch_boxes = batch_boxes * scale + + blurred_frames = [] + for i, boxes in enumerate(batch_boxes): + boxes = boxes.detach().cpu().numpy() + scores = batch_conf[i, :, 1].detach().cpu().numpy() + + filtered_boxes = filter_detected_boxes( + boxes, + scores, + confidence_threshold=self.confidence_threshold, + nms_threshold=NMS_THRESHOLD, + top_k=TOP_K, + keep_top_k=KEEP_TOP_K, + ) + + frame = frames[i] + for box in filtered_boxes: + x1, y1, x2, y2 = map(int, box) + # Ignore bounding boxes smaller than the minimum size + if x2 - x1 < min_size[0] or y2 - y1 < min_size[1]: + continue + max_h, max_w = frame.shape[:2] + face_roi = frame[max(y1, 0) : min(y2, max_h), max(x1, 0) : min(x2, max_w)] + blurred_face = pixelate_face(face_roi) + frame[max(y1, 0) : min(y2, max_h), max(x1, 0) : min(x2, max_w)] = blurred_face + blurred_frames.append(frame) + + return blurred_frames + + def postprocess(self, frames: np.ndarray) -> np.ndarray: + """Blur faces in a sequence of frames. + + Args: + frames: Input frames + + Returns: + Processed frames with pixelated faces + """ + if self.offload_model: + self.net = self.net.to("cuda") + log.debug("Move face blur filter to GPU") + + num_frames = len(frames) + processed_batches = [] + prior_data, scale = None, None + + for i in range(0, num_frames, self.batch_size): + # Get batch of frames from numpy array (stays on CPU) + batch_frames = frames[i : i + self.batch_size] + + # Preprocess just this batch on GPU + batch_tensor = self.preprocess_frames(batch_frames) + h, w = batch_tensor.shape[-2:] + + with torch.no_grad(): + # Generate priors for the video + if prior_data is None: + priorbox = PriorBox(self.cfg, image_size=(h, w)) + priors = priorbox.forward() + priors = priors.to("cuda", dtype=self.dtype) + prior_data = priors.data + + # Get scale for resizing detections + if scale is None: + scale = torch.Tensor([w, h, w, h]) + scale = scale.to("cuda", dtype=self.dtype) + + batch_loc, batch_conf, _ = self.net(batch_tensor) + + # Blur detected faces in this batch + processed_batches.append(self.blur_detected_faces(batch_frames, batch_loc, batch_conf, prior_data, scale)) + + # Free GPU memory for this batch + del batch_tensor, batch_loc, batch_conf + + processed_frames = [frame for batch in processed_batches for frame in batch] + if self.offload_model: + self.net = self.net.to("cpu") + log.debug("Offload face blur filter to CPU") + return np.array(processed_frames) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--input_dir", type=str, required=True, help="Path containing input videos") + parser.add_argument("--output_dir", type=str, required=True, help="Path for saving processed videos") + return parser.parse_args() + + +def main(args): + filepaths = get_video_filepaths(args.input_dir) + if not filepaths: + log.error(f"No video files found in directory: {args.input_dir}") + return + + face_blur = RetinaFaceFilter() + postprocessing_runner = GuardrailRunner(postprocessors=[face_blur]) + os.makedirs(args.output_dir, exist_ok=True) + + for filepath in tqdm(filepaths): + video_data = read_video(filepath) + with misc.timer("face blur filter"): + frames = postprocessing_runner.postprocess(video_data.frames) + + output_path = os.path.join(args.output_dir, os.path.basename(filepath)) + save_video(output_path, frames, video_data.fps) + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py new file mode 100644 index 00000000..a8531c6b --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch +from retinaface.utils.nms.py_cpu_nms import py_cpu_nms + +from cosmos3._src.imaginaire.utils import log + + +# Adapted from https://github.com/biubug6/Pytorch_Retinaface/blob/master/detect.py +def filter_detected_boxes(boxes, scores, confidence_threshold, nms_threshold, top_k, keep_top_k): + """Filter boxes based on confidence score and remove overlapping boxes using NMS.""" + # Keep detections with confidence above threshold + inds = np.where(scores > confidence_threshold)[0] + boxes = boxes[inds] + scores = scores[inds] + + # Sort by confidence and keep top K detections + order = scores.argsort()[::-1][:top_k] + boxes = boxes[order] + scores = scores[order] + + # Run non-maximum-suppression (NMS) to remove overlapping boxes + dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False) + keep = py_cpu_nms(dets, nms_threshold) + dets = dets[keep, :] + dets = dets[:keep_top_k, :] + boxes = dets[:, :-1] + return boxes + + +# Adapted from https://github.com/biubug6/Pytorch_Retinaface/blob/master/utils/box_utils.py to handle batched inputs +def decode_batch(loc, priors, variances): + """Decode batched locations from predictions using priors and variances. + + Args: + loc (tensor): Batched location predictions for loc layers. + Shape: [batch_size, num_priors, 4] + priors (tensor): Prior boxes in center-offset form. + Shape: [num_priors, 4] + variances: (list[float]): Variances of prior boxes. + + Return: + Decoded batched bounding box predictions + Shape: [batch_size, num_priors, 4] + """ + batch_size = loc.size(0) + priors = priors.unsqueeze(0).expand(batch_size, -1, -1) + + boxes = torch.cat( + ( + priors[:, :, :2] + loc[:, :, :2] * variances[0] * priors[:, :, 2:], + priors[:, :, 2:] * torch.exp(loc[:, :, 2:] * variances[1]), + ), + dim=2, + ) + + boxes[:, :, :2] -= boxes[:, :, 2:] / 2 + boxes[:, :, 2:] += boxes[:, :, :2] + return boxes + + +# Adapted from https://github.com/biubug6/Pytorch_Retinaface/blob/master/detect.py +def _check_keys(model, pretrained_state_dict): + ckpt_keys = set(pretrained_state_dict.keys()) + model_keys = set(model.state_dict().keys()) + used_pretrained_keys = model_keys & ckpt_keys + unused_pretrained_keys = ckpt_keys - model_keys + missing_keys = model_keys - ckpt_keys + log.debug(f"Missing keys:{len(missing_keys)}") + log.debug(f"Unused checkpoint keys:{len(unused_pretrained_keys)}") + log.debug(f"Used keys:{len(used_pretrained_keys)}") + assert len(used_pretrained_keys) > 0, "load NONE from pretrained checkpoint" + return True + + +# Adapted from https://github.com/biubug6/Pytorch_Retinaface/blob/master/detect.py +def _remove_prefix(state_dict, prefix): + """Old version of the model is stored with all names of parameters sharing common prefix 'module.'""" + log.debug(f"Removing prefix '{prefix}'") + + def f(x): + return x.split(prefix, 1)[-1] if x.startswith(prefix) else x + + return {f(key): value for key, value in state_dict.items()} + + +# Adapted from https://github.com/biubug6/Pytorch_Retinaface/blob/master/detect.py +def load_model(model, pretrained_path, load_to_cpu): + log.debug(f"Loading pretrained model from {pretrained_path}") + if load_to_cpu: + pretrained_dict = torch.load(pretrained_path, map_location=lambda storage, loc: storage, weights_only=True) + else: + device = torch.cuda.current_device() + pretrained_dict = torch.load( + pretrained_path, map_location=lambda storage, loc: storage.cuda(device), weights_only=True + ) + if "state_dict" in pretrained_dict.keys(): + pretrained_dict = _remove_prefix(pretrained_dict["state_dict"], "module.") + else: + pretrained_dict = _remove_prefix(pretrained_dict, "module.") + _check_keys(model, pretrained_dict) + model.load_state_dict(pretrained_dict, strict=False) + return model diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py new file mode 100644 index 00000000..f8d5a95d --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +UNSAFE_CATEGORIES = { + "S1": "Violent Crimes.", + "S2": "Non-Violent Crimes.", + "S3": "Sex Crimes.", + "S4": "Child Exploitation.", + "S5": "Defamation.", + "S6": "Specialized Advice.", + "S7": "Privacy.", + "S8": "Intellectual Property.", + "S9": "Indiscriminate Weapons.", + "S10": "Hate.", + "S11": "Self-Harm.", + "S12": "Sexual Content.", + "S13": "Elections.", + "s14": "Code Interpreter Abuse.", +} diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py new file mode 100644 index 00000000..e0e1b8cd --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import ContentSafetyGuardrail, GuardrailRunner +from cosmos3._src.imaginaire.auxiliary.guardrail.llamaGuard3.categories import UNSAFE_CATEGORIES +from cosmos3._src.imaginaire.utils import log, misc + +SAFE = misc.Color.green("SAFE") +UNSAFE = misc.Color.red("UNSAFE") + + +class LlamaGuard3(ContentSafetyGuardrail): + def __init__( + self, + offload_model_to_cpu: bool = True, + ) -> None: + """Llama Guard 3 model for text filtering safety check. + + Args: + checkpoint_dir (str): Path to the checkpoint directory. + offload_model_to_cpu (bool, optional): Whether to offload the model to CPU. Defaults to True. + """ + self.offload_model = offload_model_to_cpu + self.dtype = torch.bfloat16 + + model_id = "meta-llama/Llama-Guard-3-8B" + + self.model = AutoModelForCausalLM.from_pretrained(model_id) + self.tokenizer = AutoTokenizer.from_pretrained(model_id) + + # Move model to GPU unless offload_model_to_cpu is True + if not offload_model_to_cpu: + self.model = self.model.to("cuda", dtype=self.dtype).eval() + log.debug("Moved llamaGuard3 model to GPU") + else: + self.model = self.model.to("cpu", dtype=self.dtype).eval() + log.debug("Moved llamaGuard3 model to CPU") + + def get_llamaGuard3_block_message(self, moderation_output: str) -> str: + """Extract the blocked category from the Llama Guard 3 model output.""" + block_msg = "Prompt blocked by Llama Guard 3." + try: + lines = moderation_output.splitlines() + categories_detected = [] + for line in lines[1:]: + line_stripped = line.split("<|eot_id|>")[0].strip() + for catagory in line_stripped.split(","): + catagory = catagory.strip() + if catagory not in UNSAFE_CATEGORIES: + log.warning(f"Unrecognized category from moderation output: {catagory}") + else: + categories_detected.append(catagory) + if len(categories_detected) > 0: + blocked_catagories = ", ".join([UNSAFE_CATEGORIES[catagory][:-1] for catagory in categories_detected]) + block_msg = f"{block_msg} Violations: {blocked_catagories}." + except Exception as e: + log.warning(f"Unable to extract blocked category from Llama Guard 3 output: {e}") + return block_msg + + def filter_llamaGuard3_output(self, prompt: str) -> tuple[bool, str]: + """Filter the Llama Guard 3 model output and return the safety status and message.""" + conversation = [{"role": "user", "content": prompt}] + if self.offload_model: + self.model = self.model.to("cuda") + log.debug("Move llamaGuard3 model to GPU") + input_ids = self.tokenizer.apply_chat_template( + conversation, categories=UNSAFE_CATEGORIES, return_tensors="pt" + ).to("cuda") + prompt_len = input_ids.shape[1] + output = self.model.generate( + input_ids=input_ids, + max_new_tokens=100, + return_dict_in_generate=True, + pad_token_id=0, + ) + generated_tokens = output.sequences[:, prompt_len:] + moderation_output = self.tokenizer.decode(generated_tokens[0], skip_special_tokens=False).strip() + if self.offload_model: + self.model = self.model.to("cpu") + log.debug("Offload llamaGuard3 model to CPU") + if "unsafe" in moderation_output.lower(): + block_msg = self.get_llamaGuard3_block_message(moderation_output) + return False, block_msg + else: + return True, "" + + def is_safe(self, prompt: str) -> tuple[bool, str]: + """Check if the input prompt is safe according to the Llama Guard 3 model.""" + try: + return self.filter_llamaGuard3_output(prompt) + except Exception as e: + log.error(f"Unexpected error occurred when running Llama Guard 3 guardrail: {e}") + return True, "Unexpected error occurred when running Llama Guard 3 guardrail." + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--prompt", type=str, required=True, help="Input prompt") + return parser.parse_args() + + +def main(args): + llamaGuard3 = LlamaGuard3() + runner = GuardrailRunner(safety_models=[llamaGuard3]) + with misc.timer("Llama Guard 3 safety check"): + safety, message = runner.run_safety_check(args.prompt) + log.info(f"Input is: {'SAFE' if safety else 'UNSAFE'}") + log.info(f"Message: {message}") if not safety else None + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py new file mode 100644 index 00000000..b990489f --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +UNSAFE_CATEGORIES = { + "S1": "Violent", + "S2": "Non-violent Illegal Acts", + "S3": "Sexual Content or Sexual Acts", + "S4": "Suicide & Self-Harm", + "S5": "Unethical Acts", + "S6": "Jailbreak", +} diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py new file mode 100644 index 00000000..e6078bb2 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import re + +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import ContentSafetyGuardrail, GuardrailRunner +from cosmos3._src.imaginaire.auxiliary.guardrail.qwen3guard.categories import UNSAFE_CATEGORIES +from cosmos3._src.imaginaire.utils import log, misc + +SAFE = misc.Color.green("SAFE") +UNSAFE = misc.Color.red("UNSAFE") + + +class Qwen3Guard(ContentSafetyGuardrail): + def __init__( + self, + offload_model_to_cpu: bool = True, + ) -> None: + """Llama Guard 3 model for text filtering safety check. + + Args: + checkpoint_dir (str): Path to the checkpoint directory. + offload_model_to_cpu (bool, optional): Whether to offload the model to CPU. Defaults to True. + """ + self.offload_model = offload_model_to_cpu + self.dtype = torch.bfloat16 + + model_id = "Qwen/Qwen3Guard-Gen-0.6B" + + self.model = AutoModelForCausalLM.from_pretrained(model_id) + self.tokenizer = AutoTokenizer.from_pretrained(model_id) + + # Move model to GPU unless offload_model_to_cpu is True + if not offload_model_to_cpu: + self.model = self.model.to("cuda", dtype=self.dtype).eval() + log.debug("Moved llamaGuard3 model to GPU") + else: + self.model = self.model.to("cpu", dtype=self.dtype).eval() + log.debug("Moved Qwen3Guard model to CPU") + + def extract_label_and_categories(self, prompt): + safe_pattern = r"Safety: (Safe|Unsafe|Controversial)" + category_pattern = r"(" + "|".join(UNSAFE_CATEGORIES.values()) + ")" + messages = [{"role": "user", "content": prompt}] + + text = self.tokenizer.apply_chat_template(messages, tokenize=False) + model_inputs = self.tokenizer([text], return_tensors="pt").to(self.model.device) + generated_ids = self.model.generate(**model_inputs, max_new_tokens=128) + output_ids = generated_ids[0][len(model_inputs.input_ids[0]) :].tolist() + content = self.tokenizer.decode(output_ids, skip_special_tokens=True) + + safe_label_match = re.search(safe_pattern, content) + label = safe_label_match.group(1) if safe_label_match else None + categories = re.findall(category_pattern, content) + if label.lower() == "unsafe": + return False, f"Prompt blocked by Qwen3Guard. Safety: {label}, Categories: {categories}" + else: + return True, "" + + def is_safe(self, prompt: str) -> tuple[bool, str]: + """Check if the input prompt is safe according to the Qwen3Guard model.""" + try: + return self.extract_label_and_categories(prompt) + except Exception as e: + log.error(f"Unexpected error occurred when running Qwen3Guard guardrail: {e}") + return True, "Unexpected error occurred when running Qwen3Guard guardrail." + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--prompt", type=str, required=True, help="Input prompt") + return parser.parse_args() + + +def main(args): + qwen3guard = Qwen3Guard() + runner = GuardrailRunner(safety_models=[qwen3guard]) + with misc.timer("Qwen3Guard safety check"): + safety, message = runner.run_safety_check(args.prompt) + log.info(f"Input is: {'SAFE' if safety else 'UNSAFE'}") + log.info(f"Message: {message}") if not safety else None + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py new file mode 100644 index 00000000..ecb03832 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import attrs +import torch +import torch.nn as nn + +from cosmos3._src.imaginaire.config import make_freezable + + +@make_freezable +@attrs.define(slots=False) +class ModelConfig: + input_size: int = 1152 + num_classes: int = 7 + + +class SafetyClassifier(nn.Module): + def __init__(self, input_size: int = 1024, num_classes: int = 2): + super().__init__() + self.input_size = input_size + self.num_classes = num_classes + self.layers = nn.Sequential( + nn.Linear(self.input_size, 512), + nn.BatchNorm1d(512), + nn.ReLU(), + nn.Linear(512, 256), + nn.BatchNorm1d(256), + nn.ReLU(), + nn.Linear(256, self.num_classes), + # Note: No activation function here; CrossEntropyLoss expects raw logits + ) + + def forward(self, x): + return self.layers(x) + + +class VideoSafetyModel(nn.Module): + def __init__(self, config: ModelConfig) -> None: + super().__init__() + self.config = config + self.num_classes = config.num_classes + self.network = SafetyClassifier(input_size=config.input_size, num_classes=self.num_classes) + + @torch.inference_mode() + def forward(self, data_batch: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + logits = self.network(data_batch["data"].cuda()) + return {"logits": logits} diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py new file mode 100644 index 00000000..fcfe6ab1 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py @@ -0,0 +1,208 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import json +import os +from collections.abc import Iterable + +import torch +from PIL import Image + +from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import ( + GUARDRAIL1_CHECKPOINT, + ContentSafetyGuardrail, + GuardrailRunner, +) +from cosmos3._src.imaginaire.auxiliary.guardrail.common.io_utils import get_video_filepaths, read_video +from cosmos3._src.imaginaire.auxiliary.guardrail.video_content_safety_filter.model import ModelConfig, VideoSafetyModel +from cosmos3._src.imaginaire.auxiliary.guardrail.video_content_safety_filter.vision_encoder import SigLIPEncoder +from cosmos3._src.imaginaire.utils import log, misc + +# Define the class index to class name mapping for multi-class classification +CLASS_IDX_TO_NAME = { + 0: "Safe", + 1: "Sexual_Content", + 3: "Drugs", + 4: "Child_Abuse", + 5: "Hate_and_Harassment", + 6: "Self-Harm", +} + +CUTOFF_UNSAFE_FRAMES_PERCENT = 10 # 10% of frames are unsafe, then the video is unsafe + + +class VideoContentSafetyFilter(ContentSafetyGuardrail): + def __init__( + self, + offload_model_to_cpu: bool = True, + ) -> None: + """Video content safety filter model. + + Args: + checkpoint_dir (str): Path to the checkpoint directory. + offload_model_to_cpu (bool, optional): Whether to offload the model to CPU. Defaults to True. + """ + self.offload_model = offload_model_to_cpu + self.dtype = torch.float32 + self.checkpoint_dir = os.path.join(GUARDRAIL1_CHECKPOINT.download(), "video_content_safety_filter") + + # Use ModelConfig directly for inference configuration + model_config = ModelConfig(input_size=1152, num_classes=7) + + # Load the multi-class classifier and initialize the SigLIP encoder + self.model = VideoSafetyModel(model_config) + safety_filter_local_path = os.path.join(self.checkpoint_dir, "safety_filter.pt") + checkpoint = torch.load(safety_filter_local_path, map_location=torch.device("cpu"), weights_only=True) + self.model.load_state_dict(checkpoint["model"]) + self.encoder = SigLIPEncoder(device="cuda", dtype=self.dtype) + if offload_model_to_cpu: + self.encoder.to("cpu") + self.model = self.model.to("cpu", dtype=self.dtype).eval() + log.debug("Moved video content safety filter to CPU") + else: + self.encoder.to("cuda") + self.model = self.model.to("cuda", dtype=self.dtype).eval() + log.debug("Moved video content safety filter to GPU") + + @torch.inference_mode() + def __infer(self, pil_image: Image.Image) -> int: + """Infer the class of the image.""" + image_embs = self.encoder.encode_image(pil_image) + logits = self.model.network(image_embs) + probabilities = torch.nn.functional.softmax(logits, dim=-1) + predicted_class = int(torch.argmax(probabilities, dim=-1).item()) + return predicted_class + + def _to_cuda_if_offload(self): + if self.offload_model: + self.encoder = self.encoder.to("cuda") + self.model = self.model.to("cuda") + log.debug("Move video content safety filter to GPU") + + def _to_cpu_if_offload(self): + if self.offload_model: + self.encoder = self.encoder.to("cpu") + self.model = self.model.to("cpu") + log.debug("Offload video content safety filter to CPU") + + def is_safe_file(self, filepath: str) -> bool: + """Check if the video file is safe.""" + video_data = read_video(filepath) + + # Sample frames at 2 FPS + sample_rate = 2 # frames per second + frame_interval = int(video_data.fps / sample_rate) + frame_numbers = list(range(0, int(video_data.fps * video_data.duration), frame_interval)) + frames = [video_data.frames[frame_number] for frame_number in frame_numbers] + return self.is_safe_frames(frames) + + def is_safe_frames(self, frames: Iterable) -> bool: + """Check if video frames are safe. Populates ``self.last_diagnostics`` as a side effect; + single-instance serial use only (not thread-safe).""" + is_safe = True + frame_scores: list[dict] = [] + unsafe_frame_count = 0 + total_frame_count = 0 + + self._to_cuda_if_offload() + for frame_number, frame in enumerate(frames): + total_frame_count += 1 + try: + pil_image = Image.fromarray(frame) + predicted_class = self.__infer(pil_image) + class_name = CLASS_IDX_TO_NAME.get(predicted_class, "Unknown") + frame_scores.append({"frame_number": frame_number, "class": class_name}) + + # If any frame considered in the list of unsafe categories, mark the video as unsafe + if class_name != "Safe" and class_name in CLASS_IDX_TO_NAME.values(): + log.warning(f"Unsafe frame detected in frame_number {frame_number}: {class_name}") + unsafe_frame_count += 1 + + except Exception as e: + log.warning(f"Warning: Failed to run safety classifier on frame_number {frame_number}. Exception: {e}") + continue + + unsafe_ratio = unsafe_frame_count / total_frame_count if total_frame_count else 0.0 + if unsafe_ratio > (CUTOFF_UNSAFE_FRAMES_PERCENT / 100): + is_safe = False + log.warning( + f"Unsafe frame count {unsafe_frame_count} is greater than {CUTOFF_UNSAFE_FRAMES_PERCENT}% of total frames {total_frame_count}" + ) + + # .get(..., "Safe") guards against future callers appending partial entries; "Safe" is filtered out. + unsafe_categories = sorted({s.get("class", "Safe") for s in frame_scores if s.get("class", "Safe") != "Safe"}) + self.last_diagnostics: dict = { + "unsafe_frames": unsafe_frame_count, + "total_frames": total_frame_count, + "unsafe_ratio": unsafe_ratio, + "unsafe_categories": unsafe_categories, + "cutoff_percent": CUTOFF_UNSAFE_FRAMES_PERCENT, + } + + video_data = { + "is_safe": is_safe, + "frame_scores": frame_scores, + } + self._to_cpu_if_offload() + log.debug(f"Frames data: {json.dumps(video_data, indent=4)}") + return is_safe + + def _format_block_message(self) -> str: + """Build a diagnostic message for the most recent unsafe classification.""" + d = getattr(self, "last_diagnostics", None) + if not d: + return "unsafe content detected" + return ( + f"unsafe content detected: " + f"{d['unsafe_frames']}/{d['total_frames']} frames " + f"({d['unsafe_ratio']:.1%}) exceed the {d['cutoff_percent']}% cutoff; " + f"categories={d['unsafe_categories']}" + ) + + def is_safe(self, input: str | Iterable) -> tuple[bool, str]: + if isinstance(input, str): + is_safe = self.is_safe_file(input) + return is_safe, "safe video detected" if is_safe else self._format_block_message() + elif isinstance(input, Iterable): + is_safe = self.is_safe_frames(input) + return is_safe, "safe frames detected" if is_safe else self._format_block_message() + else: + raise ValueError(f"Input type {type(input)} not supported.") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--input_dir", type=str, required=True, help="Path containing input videos") + return parser.parse_args() + + +def main(args): + filepaths = get_video_filepaths(args.input_dir) + if not filepaths: + log.error(f"No video files found in directory: {args.input_dir}") + return + + video_filter = VideoContentSafetyFilter() + runner = GuardrailRunner(safety_models=[video_filter], generic_safe_msg="Video is safe") + + for filepath in filepaths: + with misc.timer("video content safety filter"): + _ = runner.run_safety_check(filepath) + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py new file mode 100644 index 00000000..283446e3 --- /dev/null +++ b/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from PIL import Image +from transformers import SiglipModel, SiglipProcessor + + +class SigLIPEncoder(torch.nn.Module): + def __init__( + self, + device="cuda" if torch.cuda.is_available() else "cpu", # noqa: B008 + dtype=torch.float32, + ) -> None: + super().__init__() + self.device = device + self.dtype = dtype + model_id = "google/siglip-so400m-patch14-384" + self.model = SiglipModel.from_pretrained(model_id) + self.processor = SiglipProcessor.from_pretrained(model_id) + self.model.to(self.device, dtype=self.dtype).eval() + + @torch.inference_mode() + def encode_image(self, input_img: Image.Image) -> torch.Tensor: + """Encode an image into a feature vector.""" + with torch.no_grad(): + inputs = self.processor(images=input_img, return_tensors="pt").to(self.device, dtype=self.dtype) + image_features = self.model.get_image_features(**inputs) + image_features /= image_features.norm(dim=-1, keepdim=True) + return image_features diff --git a/cosmos3/_src/imaginaire/callbacks/__init__.py b/cosmos3/_src/imaginaire/callbacks/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/callbacks/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/callbacks/every_n.py b/cosmos3/_src/imaginaire/callbacks/every_n.py new file mode 100644 index 00000000..490de61c --- /dev/null +++ b/cosmos3/_src/imaginaire/callbacks/every_n.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.callback import Callback + + +class EveryN(Callback): + def __init__( + self, + every_n: Optional[int] = None, + step_size: int = 1, + barrier_after_run: bool = True, + run_at_start: bool = False, + ) -> None: + """Constructor for `EveryN`. + + Args: + every_n (int): Frequency with which callback is run during training. + step_size (int): Size of iteration step count. Default 1. + barrier_after_run (bool): Whether to have a distributed barrier after each execution. Default True, to avoid timeouts. + run_at_start (bool): Whether to run at the beginning of training. Default False. + """ + self.every_n = every_n + if self.every_n == 0: + log.warning( + f"every_n is set to 0. Callback {self.__class__.__name__} will be invoked only once in the beginning of the training. Calls happens on_training_step_end will be skipped." + ) + + self.step_size = step_size + self.barrier_after_run = barrier_after_run + self.run_at_start = run_at_start + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # every_n = 0 is a special case which means every_n_impl will be called only once in the beginning of the training + if self.every_n != 0: + trainer = self.trainer + global_step = iteration // self.step_size + should_run = (iteration == 1 and self.run_at_start) or ( + global_step % self.every_n == 0 + ) # (self.every_n - 1) + if should_run: + log.debug(f"Callback {self.__class__.__name__} fired on train_batch_end step {global_step}") + self.every_n_impl(trainer, model, data_batch, output_batch, loss, iteration) + log.debug(f"Callback {self.__class__.__name__} finished on train_batch_end step {global_step}") + # add necessary barrier to avoid timeout + if self.barrier_after_run: + distributed.barrier() + + @abstractmethod + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: ... diff --git a/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py b/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py new file mode 100644 index 00000000..7d6e3a62 --- /dev/null +++ b/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Optional + +import torch +import wandb +from torch.distributed.fsdp import FullyShardedDataParallel as FSDP + +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.callback import Callback + + +@torch.jit.script +def _fused_nan_to_num(params: List[torch.Tensor]): + for param in params: + torch.nan_to_num(param, nan=0.0, posinf=0.0, neginf=0.0, out=param) + + +class GradClip(Callback): + def __init__( + self, clip_norm=1.0, force_finite: bool = True, model_key: Optional[str] = None, fsdp_enabled: bool = False + ): + self.clip_norm = clip_norm + self.force_finite = force_finite + self.model_key = model_key + self.fsdp_enabled = fsdp_enabled + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + del optimizer, scheduler + if isinstance(model_ddp, distributed.DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + + # select sub-network if specified + if self.model_key is not None: + items = self.model_key.split(".") + for item in items: + model = getattr(model, item) + + + if self.force_finite: + params = [] + for param in model.parameters(): + if param.grad is not None: + params.append(param.grad) + # torch.nan_to_num(param.grad, nan=0, posinf=0, neginf=0, out=param.grad) + _fused_nan_to_num(params) + + # check if FSDP is used + if isinstance(model, FSDP) and self.fsdp_enabled: + total_norm = model.clip_grad_norm_(self.clip_norm) + else: + total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), self.clip_norm, foreach=True) + + # log + if iteration % self.config.trainer.logging_iter == 0: + if wandb.run: + wandb.log({"clip_grad_norm": total_norm.item()}, step=iteration) diff --git a/cosmos3/_src/imaginaire/callbacks/manual_gc.py b/cosmos3/_src/imaginaire/callbacks/manual_gc.py new file mode 100644 index 00000000..ee47db5d --- /dev/null +++ b/cosmos3/_src/imaginaire/callbacks/manual_gc.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.utils import log + + +class ManualGarbageCollection(EveryN): + """ + Disable auto gc and manually trigger garbage collection every N iterations + It is super useful for large scale training to reduce gpu sync time! + Can reach 50% speedup. + + It is important to note that this callback only disables gc in main process and have auto gc enabled in subprocesses. + + We start disable gc after warm_up iterations to avoid disabling gc in subprocesses, such as dataloader, which can cause OOM + """ + + def __init__(self, *args, warm_up: int = 5, gc_level: int = 1, **kwargs): + kwargs["barrier_after_run"] = False + super().__init__(*args, **kwargs) + + self.counter = 0 + self.warm = warm_up + self.gc_level = gc_level + + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + del trainer, model, data_batch, output_batch, loss + self.counter += 1 + if self.counter < self.warm: + return + if self.counter == self.warm: + gc.disable() + log.critical("Garbage collection disabled") + + gc.collect(self.gc_level) diff --git a/cosmos3/_src/imaginaire/checkpointer/__init__.py b/cosmos3/_src/imaginaire/checkpointer/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/checkpointer/base.py b/cosmos3/_src/imaginaire/checkpointer/base.py new file mode 100644 index 00000000..80b9c2d2 --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/base.py @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from abc import ABC, abstractmethod +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.config import CheckpointConfig, JobConfig +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import callback +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +class AbstractCheckpointer(ABC): + """The checkpointer class. Supports checkpoint saving/loading to both local disk or object store.""" + + def __init__( + self, + config_checkpoint: CheckpointConfig, + config_job: JobConfig, + callbacks: Optional[callback.CallBackGroup] = None, + ): + """Constructor of the checkpointer. + + Args: + config_checkpoint (CheckpointConfig): The config object for the checkpointer. + """ + self.config_checkpoint = config_checkpoint + # Set the callback functions. + self.callbacks = callbacks + self.save_to_object_store = config_checkpoint.save_to_object_store.enabled + self.load_from_object_store = config_checkpoint.load_from_object_store.enabled + + # Set checkpoint directories for local and object store paths + self._local_dirname = os.path.join(config_job.path_local, "checkpoints") + self._object_store_dirname = os.path.join(config_job.path, "checkpoints") + + self.strict_resume = config_checkpoint.strict_resume + load_path = config_checkpoint.load_path or None + if not INTERNAL: + from cosmos3._src.imaginaire.utils.checkpoint_db import download_checkpoint_v2 + + if load_path: + load_path = download_checkpoint_v2(load_path) + self.load_path = load_path + self.load_training_state = config_checkpoint.load_training_state + self.only_load_scheduler_state = config_checkpoint.only_load_scheduler_state + self.save_thread = None + self.verbose = config_checkpoint.verbose + self.keys_not_to_resume = config_checkpoint.keys_not_to_resume + self.keys_to_skip_loading = getattr(config_checkpoint, "keys_to_skip_loading", []) + self.broadcast_via_filesystem = config_checkpoint.broadcast_via_filesystem + # Create the object store client interface. + if config_checkpoint.load_from_object_store.enabled: + self.load_s3_backend_key = "_ckpt_s3_loader" + easy_io.set_s3_backend( + key="_ckpt_s3_loader", + backend_args={ + "backend": "s3", + "path_mapping": { + "s3://ckpt/": f"s3://{config_checkpoint.load_from_object_store.bucket}/", + }, + "s3_credential_path": config_checkpoint.load_from_object_store.credentials, + }, + ) + else: + self.load_s3_backend_key = None + + if config_checkpoint.save_to_object_store.enabled: + self.save_s3_backend_key = "_ckpt_s3_saver" + easy_io.set_s3_backend( + key="_ckpt_s3_saver", + backend_args={ + "backend": "s3", + "path_mapping": { + "s3://ckpt/": f"s3://{config_checkpoint.save_to_object_store.bucket}/", + }, + "s3_credential_path": config_checkpoint.save_to_object_store.credentials, + }, + ) + else: + self.save_s3_backend_key = None + + @abstractmethod + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + pass + + @abstractmethod + def load( + self, + model: ImaginaireModel, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None, + grad_scaler: Optional[torch.amp.GradScaler] = None, + ) -> int: + pass + + @property + def save_bucket(self): + """Get the bucket name for saving checkpoints.""" + return self.config_checkpoint.save_to_object_store.bucket if self.save_to_object_store else None + + @property + def load_bucket(self): + """Get the bucket name for loading checkpoints.""" + return self.config_checkpoint.load_from_object_store.bucket if self.load_from_object_store else None + + @property + def save_dirname(self): + return ( + f"s3://{self.save_bucket}/{self._object_store_dirname}" + if self.save_to_object_store + else self._local_dirname + ) + + @property + def load_dirname(self): + return ( + f"s3://{self.load_bucket}/{self._object_store_dirname}" + if self.load_from_object_store + else self._local_dirname + ) + + def finalize(self) -> None: + """Finalize the checkpointer.""" + if self.save_thread: + self.save_thread.join() + + def _read_latest_checkpoint_file(self) -> str | None: + """Get the file name of the latest saved checkpoint. If it doesn't exist, return None. + + Returns: + checkpoint_file (str | None): file name of the latest saved checkpoint. + """ + checkpoint_file = None + checkpoint_path = os.path.join(self.load_dirname, "latest_checkpoint.txt") + if easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key): + checkpoint_file = easy_io.load(f"{checkpoint_path}", backend_key=self.load_s3_backend_key).strip() + + return checkpoint_file + + def _write_latest_checkpoint_file(self, checkpoint_file: str) -> None: + """Track the file name of the latest saved checkpoint. + + Args: + checkpoint_file (str): file name of the latest saved checkpoint. + """ + content = f"{checkpoint_file}\n" + checkpoint_path = os.path.join(self.save_dirname, "latest_checkpoint.txt") + easy_io.dump( + content, + checkpoint_path, + backend_key=self.save_s3_backend_key, + ) + + def _check_checkpoint_exists(self, checkpoint_path: str) -> None: + """If the file checkpoint_path does not exist, raise an error. + + Args: + checkpoint_path (str): full path to the checkpoint. + """ + if not easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key): + raise FileNotFoundError(f"File not found (object store): {checkpoint_path}") diff --git a/cosmos3/_src/imaginaire/checkpointer/ddp.py b/cosmos3/_src/imaginaire/checkpointer/ddp.py new file mode 100644 index 00000000..5306cdf8 --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/ddp.py @@ -0,0 +1,457 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import threading +from collections import namedtuple +from typing import Any, Dict, Optional, Set, Tuple, Union + +import torch +import torch.distributed +from megatron.core import parallel_state +from torch.distributed import ProcessGroup, get_process_group_ranks + +from cosmos3._src.imaginaire.checkpointer.base import AbstractCheckpointer +from cosmos3._src.imaginaire.checkpointer.safe_broadcast import broadcast_object +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log, misc +from cosmos3._src.imaginaire.utils.easy_io import easy_io + +StateDictItemPath = namedtuple("StateDictItemPath", ["state_dict", "save_path"]) + + +class Checkpointer(AbstractCheckpointer): + """ + Checkpointer for DDP. + Note: This implementation only supports local filesystem. + """ + + KEYS_TO_SAVE = ["model", "optim", "scheduler", "trainer"] + KEYS_TO_POSTFIX = { + "model": "model", + "optim": "optim", + "scheduler": "scheduler", + "trainer": "", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + pp_world_size = parallel_state.get_pipeline_model_parallel_world_size() + ep_world_size = parallel_state.get_expert_model_parallel_world_size() + assert pp_world_size < 2, "Pipeline Parallelism (PP) is not tested yet." + assert ep_world_size < 2, "Expert Parallelism (EP) is not tested yet." + self.mp_world_size = parallel_state.get_model_parallel_group().size() + if self.mp_world_size > 1 and self.__class__ == Checkpointer: + raise NotImplementedError( + "Model Parallelism (MP) is enabled - you should use TensorParallel Checkpointer instead of DDP Checkpointer." + ) + # DDP rank (with context parallelism considered) + self.rank_dp_w_cp = parallel_state.get_data_parallel_rank(with_context_parallel=True) + # Context parallelism rank + self.cp_rank = parallel_state.get_context_parallel_rank() + # Model parallelism rank (including Tensor+Pipeline+Expert Parallelisms) + self.mp_rank = parallel_state.get_model_parallel_group().rank() + + # self.mp_rank = parallel_state.get_model_parallel_group(with_expert_parallel=ep_world_size > 1).rank() + if self.broadcast_via_filesystem: + log.info("Broadcasting checkpoint data via the local filesystem.") + if not self.strict_resume: + log.warning("Strict resume mode is off. Some model parameters may not be loaded.") + + # collect ranks of all model parallel groups + all_ranks = [None for _ in range(distributed.get_world_size())] + torch.distributed.all_gather_object( + all_ranks, get_process_group_ranks(parallel_state.get_model_parallel_group()) + ) + all_ranks = list(set(tuple(rank) if isinstance(rank, list) else rank for rank in all_ranks)) + for ranks in all_ranks: + group = torch.distributed.new_group(list(ranks), backend="gloo") + if distributed.get_rank() in ranks: + self.mp_gloo_pg = group + + self.print("Checkpointer Initialized.") + + def print(self, message: str): + """ + Print message to the console. Include the parallelism rank information when verbose is set to True. + """ + if self.verbose: + log.info( + f"[Parallelism Rank: DP-{self.rank_dp_w_cp}, TP-{self.mp_rank}, CP-{self.cp_rank}]: {message}", + rank0_only=False, + ) + else: + log.info(message, rank0_only=True) + + def add_type_postfix_to_checkpoint_path(self, key: str, checkpoint_path: str, model: ImaginaireModel) -> str: + del model + assert key in self.KEYS_TO_SAVE + post_fix = self.KEYS_TO_POSTFIX[key] + + if post_fix: + _ckpt_path = checkpoint_path.replace(".pt", f"_{post_fix}.pt") + else: + _ckpt_path = checkpoint_path + return _ckpt_path + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + self.callbacks.on_save_checkpoint_start(model, iteration) + + checkpoint_file = self.format_checkpoint_filename(model, iteration) + state_dict = self.generate_save_state_dict(model, optimizer, scheduler, grad_scaler, iteration) + state_dict = self._map_state_dict_path_during_save(state_dict, checkpoint_file, model) + if state_dict: + # Wait for previous saver thread to end. + if self.save_thread: + self.save_thread.join() + # Run the checkpoint saver in a separate thread. + self.save_thread = threading.Thread( + target=self._save_worker, + daemon=False, + args=(state_dict, checkpoint_file, distributed.get_rank()), + ) + self.save_thread.start() + + # Note: Checkpoints are saved on a separate thread and this callback is not accurate. + # Please check logs from on_save_checkpoint_success() for better accuracy + self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration) + + def _map_state_dict_path_during_save(self, state_dict, checkpoint_file, model) -> dict[str, StateDictItemPath]: + new_dict = {} + for key, _state_dict in state_dict.items(): + _ckpt_path = self.add_type_postfix_to_checkpoint_path(key, checkpoint_file, model) + checkpoint_path = os.path.join(self.save_dirname, _ckpt_path) + new_dict[key] = StateDictItemPath(_state_dict, checkpoint_path) + return new_dict + + @misc.timer("checkpoint saving") + def _save_worker(self, state_dict: dict[str, StateDictItemPath], checkpoint_file: str, rank: int = 0) -> None: + """Worker to upload checkpoint to object store, spawned with a child thread (in parallel with the training). + + Args: + state_dict (dict[str, StateDictItemPath]): The state dict of the model/optimizer/scheduler. + checkpoint_file (str): The file name of the model checkpoint. + rank (int): GPU device (default: 0). + """ + try: + for key, item in state_dict.items(): + self.print(f"Saving {key} to {item.save_path}") + try: + easy_io.dump( + item.state_dict, + item.save_path, + fast_backend=True, # optional for fast backend, cpu heavy + backend_key=self.save_s3_backend_key, + ) + self.print(f"Saved {key} to {item.save_path}") + except Exception as e: + self.print(f"Failed to save {key} to {item.save_path}: {str(e)}") + raise # Re-raise the exception after logging + + # Synchronize only rank 0 of each model parallel group + if self.mp_world_size > 1: + torch.distributed.barrier(group=self.mp_gloo_pg) + + # Only rank 0 of MP group and rank 0 of DP with CP updates latest_checkpoint.txt + if self.mp_rank == 0 and self.rank_dp_w_cp == 0: + self._write_latest_checkpoint_file(checkpoint_file) + + if distributed.get_rank() == 0: # only rank 0 saves trained_data_record + if "trained_data_record" in state_dict["model"].state_dict: + self._write_trained_data_record( + checkpoint_file, state_dict["model"].state_dict["trained_data_record"] + ) + + iteration = int(checkpoint_file.replace("iter_", "").replace(".pt", "")) + self.callbacks.on_save_checkpoint_success(iteration=iteration) + except Exception as e: # noqa: BLE001 + log.exception(f"Checkpoint failed to upload: {e}", rank0_only=not self.verbose) + + def format_checkpoint_filename(self, model: ImaginaireModel, iteration: int) -> str: + """Generate the checkpoint file name. + + Args: + iteration (int): The current iteration number. + + Returns: + checkpoint_file (str): The checkpoint file name. + """ + del self, model + return f"iter_{iteration:09}.pt" + + @misc.timer("generate saving state dict") + def generate_save_state_dict( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> Optional[Dict[str, Any]]: + state_dict = {} + + if self.rank_dp_w_cp == 0: + trainer_state = dict( + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + model_state = model.state_dict() + optim_state = optimizer.state_dict() + scheduler_state = scheduler.state_dict() + self.callbacks.on_save_checkpoint(model, state_dict=trainer_state) + + trainer_state, model_state, optim_state, scheduler_state = misc.to( + [trainer_state, model_state, optim_state, scheduler_state], device="cpu" + ) + + state_dict = { + "model": model_state, + "optim": optim_state, + "scheduler": scheduler_state, + } + if distributed.get_rank() == 0: # only rank 0 saves trainer state + state_dict["trainer"] = trainer_state + return state_dict + return state_dict + + def load_broadcast_state_dict( + self, checkpoint_path: str, model: ImaginaireModel, resume_keys: Set + ) -> dict[str, Any]: + """ + Load state_dict and broadcast. + + The main steps are: + 1. Download TP-rank-specific checkpoints for every GPU of DDP-rank 0 and CP-rank 0. + 2. Each rank loads its corresponding checkpoint from the local cache or receives it via broadcast. + + This approach ensures that each MP rank loads its specific part of the model, which is + crucial for Model Parallelism where different parts of the model are distributed across + multiple GPUs. + + When using Model Parallelism (e.g., Tensor Parallelism), the `broadcast_via_filesystem` option can + be set to True. This allows each rank to load its specific checkpoint from the local filesystem + instead of receiving it via network broadcast, which could be more efficient in some cases. + + For standard DDP without TP, `broadcast_via_filesystem` should remain False (default). + + Args: + checkpoint_path (str): The base path of the checkpoint. + model (ImaginaireModel): The model being loaded. + resume_keys (Set): Set of keys to resume from the checkpoint. + + Returns: + dict[str, Any]: A dictionary containing the loaded state for each resumed key. + """ + state_dict = {} + sorted_resume_keys = sorted(resume_keys) + # Step 1: Download TP-rank-specific checkpoints for every GPU of DDP-rank 0 and CP-rank 0. + if self.rank_dp_w_cp == 0: + for key in sorted_resume_keys: + _ckpt_path = self.add_type_postfix_to_checkpoint_path(key, checkpoint_path, model) + local_cache_path = os.path.join(self.load_dirname, os.path.basename(_ckpt_path)) + if os.path.exists(local_cache_path): + # If the local checkpoint exists, we can directly load it + self.print(f"Checkpoint is already in local cache: {local_cache_path}. Loading...") + _state_dict = easy_io.load(local_cache_path, fast_backend=True) + else: + + _state_dict = easy_io.load(_ckpt_path, fast_backend=True, backend_key=self.load_s3_backend_key) + self.print(f"Downloading checkpoint from: {_ckpt_path}") + if self.broadcast_via_filesystem: + # Save the checkpoint to the local filesystem + easy_io.dump(_state_dict, local_cache_path, fast_backend=True) + state_dict[key] = _state_dict + # Ensure all ranks wait for the download to complete + distributed.barrier() + + # Step 2: Broadcast checkpoint data + log.info( + "Start broadcasting checkpoint from the source rank to all other ranks in the same DDP group.", + rank0_only=True, + ) + for key in sorted_resume_keys: + if self.broadcast_via_filesystem: + # Load the checkpoint from the local filesystem for other ranks + if self.rank_dp_w_cp != 0: + _ckpt_path = self.add_type_postfix_to_checkpoint_path(key, checkpoint_path, model) + local_cache_path = os.path.join(self.load_dirname, os.path.basename(_ckpt_path)) + self.print(f"Loading checkpoint from: {local_cache_path}") + state_dict[key] = easy_io.load(local_cache_path, fast_backend=True) + else: + # Broadcast the checkpoint to all GPUs of the current DDP rank + group: ProcessGroup = parallel_state.get_data_parallel_group(with_context_parallel=True) + min_rank = min(get_process_group_ranks(group)) + + _state_dict = broadcast_object( + state_dict[key] if self.rank_dp_w_cp == 0 else None, + min_rank, + group=group, + device=torch.device(torch.cuda.current_device()), + ) + if self.rank_dp_w_cp == 0: + self.print(f'Broadcasted checkpoint["{key}"] to all other ranks in the same DDP group.') + else: + state_dict[key] = _state_dict + self.print(f'Received checkpoint["{key}"] from source rank {min_rank}.') + + return state_dict + + def keys_to_resume_during_load(self) -> Tuple[Set, Union[str, None]]: + latest_checkpoint_file = self._read_latest_checkpoint_file() + + resume_keys = [] + + if latest_checkpoint_file is not None: + # 1. Resume training from latest_checkpoint.txt under the same name. + checkpoint_path = os.path.join(self.load_dirname, latest_checkpoint_file) + resume_keys.extend(self.KEYS_TO_SAVE) + else: + if self.load_path: + # 2. Load the module weights specified by config_checkpoint.path. + checkpoint_path = self.load_path + if self.load_s3_backend_key: + checkpoint_path = f"s3://ckpt/{checkpoint_path}" + if self.load_training_state: + resume_keys.extend(self.KEYS_TO_SAVE) + else: + resume_keys.append("model") + if self.only_load_scheduler_state: + resume_keys.append("scheduler") + else: + checkpoint_path = None + if len(self.keys_not_to_resume) > 0: + for key in self.keys_not_to_resume: + assert key in self.KEYS_TO_SAVE, f"Invalid key to resume: {key} not in {self.KEYS_TO_SAVE}" + resume_keys = [key for key in resume_keys if key not in self.keys_not_to_resume] + return set(resume_keys), checkpoint_path + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + """Load network weights and optimizer states from a checkpoint in a single process. + + The priority of the checkpoint loading logic is: + 1. Attempt to resume training if possible by looking for latest_checkpoint.txt under the same name. + 2. If no latest checkpoint were found, it loads the model weights specified by config_checkpoint.path. + - This is typically used for inference mode. + - If config_checkpoint.load_optimizer_state is True, then also load the optimizer and scheduler states. + 3. If none of the above, randomly initialize the model parameters and train from scratch. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer | None): The model optimizer (default: None). + scheduler (torch.optim.lr_scheduler.LRScheduler | None): The optimization scheduler (default: None). + grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training). + + Returns: + iteration (int): the iteration number to start/resume from. + """ + self.callbacks.on_load_checkpoint_start(model) + + resume_keys, checkpoint_path = self.keys_to_resume_during_load() + + iteration = 0 + + # Load checkpoint. + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + state_dict = self.load_broadcast_state_dict(checkpoint_path, model, set(resume_keys)) + + if "trainer" in state_dict: + trainer_state = state_dict["trainer"] + log.critical(state_dict.keys(), rank0_only=False) + log.critical(trainer_state, rank0_only=False) + log.info("- Loading the gradient scaler...") + grad_scaler.load_state_dict(trainer_state["grad_scaler"]) + self.callbacks.on_load_checkpoint(model, state_dict=trainer_state) + iteration = trainer_state["iteration"] + if "optim" in state_dict: + assert optimizer + optimizer_state = state_dict["optim"] + log.info("- Loading the optimizer...") + optimizer.load_state_dict(optimizer_state) + if "scheduler" in state_dict: + assert scheduler + scheduler_state = state_dict["scheduler"] + log.info("- Loading the scheduler...") + scheduler.load_state_dict(scheduler_state) + scheduler.last_epoch = iteration + if "model" in state_dict: + model_state = state_dict["model"] + log.info("- Loading the model...") + # Filter out keys_to_skip_loading before loading + if len(self.keys_to_skip_loading) > 0: + filtered_state = {k: v for k, v in model_state.items() if k not in self.keys_to_skip_loading} + skipped_keys = [k for k in model_state.keys() if k in self.keys_to_skip_loading] + if skipped_keys: + log.info(f"\t Skipping {len(skipped_keys)} keys: {skipped_keys}") + model_state = filtered_state + # model.load_state_dict(model_state) + if self.strict_resume: + log.info("\t Strict resume mode is on.") + else: + log.info("\t Strict resume mode is off.") + model_load_info = model.load_state_dict(model_state, strict=self.strict_resume) + log.info(f"\t {model_load_info}") + if not model_load_info.missing_keys and not model_load_info.unexpected_keys: + log.info("\t Checkpoint weights loaded successfully (all keys matched).") + else: + log.warning("\t Checkpoint weights loaded; review missing_keys/unexpected_keys above.") + self.print(f"Loaded checkpoint from {checkpoint_path} in iteration {iteration}") + else: + log.info("Training from scratch.") + torch.cuda.empty_cache() + + self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path) + + return iteration + + def _write_trained_data_record(self, checkpoint_file: str, trained_data_record: dict[str, int]) -> None: + """Write json file to save number of seen samples and number of iterations. + + Args: + checkpoint_file (str): iteration number for the saved checkpoint + trained_data_record (dict[str, int]): example {"image": 0, "video": 0, "iteration": 0}. + """ + # filename: iter_xxxxxxxxx_trained_data_record.json + checkpoint_path = os.path.join( + self.save_dirname, f"{checkpoint_file.replace('.pt', '')}_trained_data_record.json" + ) + easy_io.dump( + trained_data_record, + checkpoint_path, + backend_key=self.save_s3_backend_key, + ) diff --git a/cosmos3/_src/imaginaire/checkpointer/dummy.py b/cosmos3/_src/imaginaire/checkpointer/dummy.py new file mode 100644 index 00000000..ef71842c --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/dummy.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torch.distributed + +from cosmos3._src.imaginaire.checkpointer.base import AbstractCheckpointer +from cosmos3._src.imaginaire.model import ImaginaireModel + + +class Checkpointer(AbstractCheckpointer): + """ + A dummy checkpointer that does not save or load anything. This is useful for debugging jobs or share workload with collobrators. + """ + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + pass + + def load( + self, + model: ImaginaireModel, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None, + grad_scaler: Optional[torch.amp.GradScaler] = None, + ) -> int: + return 0 diff --git a/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py b/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py new file mode 100644 index 00000000..dbf1bc22 --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py @@ -0,0 +1,330 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import time +from contextlib import contextmanager +from typing import Generator, Union +from urllib.parse import urlparse + +from botocore.exceptions import ClientError +from torch.distributed.checkpoint import FileSystemReader, FileSystemWriter +from torch.distributed.checkpoint.filesystem import FileSystemBase + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +class S3Stream(io.BytesIO): + """ + Workaround for PyTorch manually closing the stream before we can upload it to S3. We override the close() as noop + and instead call our own _true_close() method to close the stream after we are done using it. + The commit at fault is https://github.com/pytorch/pytorch/commit/9c909bf3bb122db2cce95e2eb7459bbe50dfa15a + """ + + def close(self): + self.flush() + # No close + + def _true_close(self): + super().close() + + +class S3FileSystem(FileSystemBase): + """Implementation of FileSystemBase for AWS S3 storage.""" + + def __init__( + self, + credential_path: str, + max_attempts: int = 20, + initial_backoff: float = 1.0, + max_backoff: float = 30.0, + backoff_factor: float = 2.0, + enable_gcs_patch_in_boto3: bool = False, + ) -> None: + """ + Initialize S3FileSystem with retry configuration. + + Args: + credential_path: Path to AWS credentials JSON file + max_attempts: Maximum number of retry attempts + initial_backoff: Initial backoff time in seconds + max_backoff: Maximum backoff time in seconds + backoff_factor: Multiplicative factor for backoff time + enable_gcs_patch_in_boto3: Whether to enable GCS patch in boto3 + """ + self.easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": credential_path, + "path_mapping": None, + } + ) + self.max_attempts = max_attempts + self.initial_backoff = initial_backoff + self.max_backoff = max_backoff + self.backoff_factor = backoff_factor + self.enable_gcs_patch_in_boto3 = enable_gcs_patch_in_boto3 + if enable_gcs_patch_in_boto3: + log.info("enable_gcs_patch_in_boto3: True") + + def _retry_with_backoff(self, operation_func, *args, **kwargs): + """ + Execute an operation with exponential backoff retry logic. + + Args: + operation_func: Function to execute + *args: Positional arguments for the function + **kwargs: Keyword arguments for the function + + Returns: + Result of the operation function + + Raises: + Exception: If all retry attempts fail + """ + last_exception = None + backoff = self.initial_backoff + + for attempt in range(self.max_attempts): + try: + return operation_func(*args, **kwargs) + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "") + log.info(f"S3 Filesystem: Received ClientError: {error_code}", rank0_only=False) + + # Handle specific error cases + if error_code in ["SlowDown", "ThrottlingException", "RequestLimitExceeded", "InternalError"]: + last_exception = e + if attempt < self.max_attempts - 1: # Don't sleep on last attempt + current_backoff = min(backoff, self.max_backoff) + log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False) + time.sleep(current_backoff) + backoff *= self.backoff_factor + continue + # For other client errors, raise immediately + raise + except Exception as e: + log.info(f"S3 Filesystem: Received Exception: {str(e)}", rank0_only=False) + last_exception = e + if attempt < self.max_attempts - 1: + current_backoff = min(backoff, self.max_backoff) + log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False) + time.sleep(current_backoff) + backoff *= self.backoff_factor + continue + + # pyrefly: ignore [bad-raise] + raise last_exception + + @contextmanager + def create_stream(self, path: Union[str, os.PathLike], mode: str) -> Generator[io.IOBase, None, None]: + """Create a stream for reading from or writing to S3 with retry logic.""" + path_str = str(path) + bucket, key = self._parse_s3_uri(path_str) + log.info(f"S3 Filesystem: Creating stream for {key} in bucket {bucket}", rank0_only=False) + + if mode == "rb": + stream = io.BytesIO() + try: + + def download_operation(): + stream.write(self.easy_io_backend.get(filepath=path_str)) + stream.seek(0) + + log.info(f"S3 Filesystem: Downloading {key} from bucket {bucket}", rank0_only=False) + self._retry_with_backoff(download_operation) + log.info("S3 Filesystem: Download complete", rank0_only=False) + yield stream + finally: + stream.close() + elif mode == "wb": + stream = S3Stream() + try: + yield stream + + def upload_operation(): + stream.seek(0) + self.easy_io_backend.put(obj=stream, filepath=path_str) + + log.info(f"S3 Filesystem: Uploading {key} to bucket {bucket}", rank0_only=False) + self._retry_with_backoff(upload_operation) + log.info("S3 Filesystem: Upload complete", rank0_only=False) + finally: + stream._true_close() + else: + raise ValueError(f"Unsupported mode: {mode}") + + def concat_path(self, path: Union[str, os.PathLike], suffix: str) -> Union[str, os.PathLike]: + """Concatenate S3 path with suffix.""" + path_str = str(path) + if path_str.endswith("/"): + return f"{path_str}{suffix}" + return f"{path_str}/{suffix}" + + def init_path(self, path: Union[str, os.PathLike]) -> Union[str, os.PathLike]: + """Initialize and validate S3 path.""" + path_str = str(path) + if not path_str.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {path_str}. Must start with 's3://'") + return path_str + + def rename(self, path: Union[str, os.PathLike], new_path: Union[str, os.PathLike]) -> None: + """Rename (move) an object in S3 with retry logic.""" + src_path = str(path) + dst_path = str(new_path) + + def copy_operation(): + self.easy_io_backend.copyfile(src=src_path, dst=dst_path) + + self._retry_with_backoff(copy_operation) + + def delete_operation(): + self.easy_io_backend.remove(filepath=src_path) + + self._retry_with_backoff(delete_operation) + + def mkdir(self, path: Union[str, os.PathLike]) -> None: + """ + Create a "directory" in S3. + + Note: S3 doesn't have real directories, but we can create an empty object + with a trailing slash to simulate a directory. + """ + # Creating same buckets from different ranks can cause rate limit issues in GCP. + # In object store, we don't need to create a directory. + pass + + def ls(self, path: Union[str, os.PathLike]) -> list[str]: + """List objects under the given S3 path (prefix) and return s3:// URIs.""" + path_str = str(path) + return [ + f"{path_str.removesuffix('/')}/{obj_suffix}" + for obj_suffix in self.easy_io_backend.list_dir_or_file(dir_path=path_str, list_dir=False, list_file=True) + ] + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + """Validate if the checkpoint_id is a valid S3 URI.""" + checkpoint_id_str = str(checkpoint_id) + try: + if not checkpoint_id_str.startswith("s3://"): + return False + parsed = urlparse(checkpoint_id_str) + return bool(parsed.netloc and parsed.path) # Must have bucket and key + except Exception: + return False + + def exists(self, path: Union[str, os.PathLike]) -> bool: + """Check if an object exists in S3 with retry logic.""" + try: + + def head_operation() -> bool: + return self.easy_io_backend.exists(filepath=str(path)) + + return self._retry_with_backoff(head_operation) + except ClientError as e: + if e.response.get("Error", {}).get("Code", "") == "404": + return False + raise + + def rm_file(self, path: Union[str, os.PathLike]) -> None: + """Remove a file from S3 with retry logic.""" + + def delete_operation(): + self.easy_io_backend.remove(filepath=str(path)) + + self._retry_with_backoff(delete_operation) + + def _parse_s3_uri(self, uri: str) -> tuple[str, str]: + """ + Parse an S3 URI into bucket and key. + + Args: + uri: S3 URI in the format s3://bucket-name/key + + Returns: + Tuple of (bucket_name, key) + + Raises: + ValueError: If the URI is invalid + """ + uri = uri if isinstance(uri, str) else str(uri) + if not uri.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {uri}. Must start with 's3://'") + + parsed = urlparse(uri) + bucket = parsed.netloc + + # Remove leading slash from key + key = parsed.path.lstrip("/") + + if not bucket: + raise ValueError(f"Invalid S3 URI: {uri}. No bucket specified") + + return bucket, key + + +class S3StorageWriter(FileSystemWriter): + def __init__( + self, + credential_path: str, + path: str, + enable_gcs_patch_in_boto3: bool = False, + **kwargs, + ) -> None: + """ + Initialize an S3 writer for distributed checkpointing. + + Args: + region (str): The AWS region for S3. + path (str): The S3 URI to write checkpoints to. + kwargs (dict): Keyword arguments to pass to the parent :class:`FileSystemWriter`. + enable_gcs_patch_in_boto3 (bool): Whether to enable GCS patch in boto3 + """ + super().__init__( + path=path, + sync_files=False, + **kwargs, + ) + self.fs = S3FileSystem(credential_path, enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3) # type: ignore + self.path = self.fs.init_path(path) + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + return S3FileSystem.validate_checkpoint_id(checkpoint_id) + + +class S3StorageReader(FileSystemReader): + def __init__( + self, credential_path: str, path: Union[str, os.PathLike], enable_gcs_patch_in_boto3: bool = False + ) -> None: + """ + Initialize an S3 reader for distributed checkpointing. + + Args: + region (str): The AWS region for S3. + path (Union[str, os.PathLike]): The S3 path to read checkpoints from. + enable_gcs_patch_in_boto3 (bool): Whether to enable GCS patch in boto3 + """ + super().__init__(path) + self.fs = S3FileSystem(credential_path, enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3) # type: ignore + self.path = self.fs.init_path(path) + self.sync_files = False + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + return S3FileSystem.validate_checkpoint_id(checkpoint_id) diff --git a/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py b/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py new file mode 100644 index 00000000..717ffed9 --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import io +import pickle +from typing import Any + +import torch +import torch.distributed as dist + + +# https://github.com/pytorch/pytorch/blob/main/torch/distributed/optim/zero_redundancy_optimizer.py#L29 + +def broadcast_object( + obj: Any, + src_rank: int, + group: object = dist.group.WORLD, + device: torch.device = torch.device("cpu"), +) -> Any: + r""" + Broadcasts an object to the given group. + + It will be sending the object if called from the source rank and receiving + the object otherwise. + + Arguments: + obj: object to broadcast; only used if called on the source rank. + src_rank (int): source rank. + group (``ProcessGroup``, optional): group used for the broadcast + (default: ``dist.group.WORLD``). + device (``torch.device``, optional): device to send from or receive + to (default: ``torch.device("cpu")``). + + Returns: + The broadcasted object. + """ + if dist.get_rank() == src_rank: + # Send the object + buffer = io.BytesIO() + torch.save(obj, buffer, pickle_protocol=pickle.HIGHEST_PROTOCOL) + data = bytearray(buffer.getbuffer()) + length_tensor = torch.LongTensor([len(data)]).to(device) + data_send_tensor = torch.ByteTensor(data).to(device) + dist.broadcast(length_tensor, src=src_rank, group=group, async_op=False) + dist.broadcast(data_send_tensor, src=src_rank, group=group, async_op=False) + else: + # Receive the object + length_tensor = torch.LongTensor([0]).to(device) + dist.broadcast(length_tensor, src=src_rank, group=group, async_op=False) + data_recv_tensor = torch.empty([int(length_tensor.item())], dtype=torch.uint8, device=device) + dist.broadcast(data_recv_tensor, src=src_rank, group=group, async_op=False) + buffer = io.BytesIO(data_recv_tensor.cpu().numpy()) + obj = torch.load(buffer, map_location=device, weights_only=False) + return obj + + +def _recursive_copy_to_device( + value: Any, + non_blocking: bool, + device: torch.device, +) -> Any: + r""" + Recursively searches lists, tuples, dicts and copies tensors to device if possible. + + Non-tensor values are passed as-is in the result. + + .. note: These are all copies, so if there are two objects that reference + the same object, then after this call, there will be two different objects + referenced on the device. + """ + if isinstance(value, torch.Tensor): + return value.to(device, non_blocking=non_blocking) + + if isinstance(value, (list, tuple)): + values = [_recursive_copy_to_device(val, non_blocking=non_blocking, device=device) for val in value] + return values if isinstance(value, list) else tuple(values) + + if isinstance(value, collections.abc.Mapping): + return { + key: _recursive_copy_to_device(val, non_blocking=non_blocking, device=device) for key, val in value.items() + } + + return value diff --git a/cosmos3/_src/imaginaire/checkpointer/tp.py b/cosmos3/_src/imaginaire/checkpointer/tp.py new file mode 100644 index 00000000..b118dbbe --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/tp.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.checkpointer.ddp import Checkpointer as DDPCheckpointer +from cosmos3._src.imaginaire.model import ImaginaireModel + + +class Checkpointer(DDPCheckpointer): + """ + Checkpointer class for Tensor Parallelism (TP) in distributed training. + + This implementation supports the combination of Tensor Parallelism (TP) and Data Parallel Processing (DDP), with optional Context Parallelism (CP). + + Note: + - Fully Sharded Data Parallelism (FSDP) is not supported by this checkpointer. + - In principle, this implementation is also compatible with Pipeline Parallelism (PP) and Expert Parallelism (EP), which are other forms of model parallelism. However, PP and EP have not been tested yet. + """ + + def add_type_postfix_to_checkpoint_path(self, key: str, checkpoint_path: str, model: ImaginaireModel) -> str: + """ + Overwrite the `add_type_postfix_to_checkpoint_path` function of the base class (DDP checkpointer) + to append the TP-rank postfix to the checkpoint path. + """ + checkpoint_path = super().add_type_postfix_to_checkpoint_path(key, checkpoint_path, model) + if key == "trainer": + return checkpoint_path + else: + checkpoint_path = checkpoint_path.replace(".pt", f"_mp_{self.mp_rank}.pt") + + return checkpoint_path diff --git a/cosmos3/_src/imaginaire/checkpointer/tp_ema.py b/cosmos3/_src/imaginaire/checkpointer/tp_ema.py new file mode 100644 index 00000000..be3a357d --- /dev/null +++ b/cosmos3/_src/imaginaire/checkpointer/tp_ema.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Dict, Optional + +import torch +from megatron.core import parallel_state + +from cosmos3._src.imaginaire.checkpointer.tp import Checkpointer as BaseCheckpointer +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import misc + + +class Checkpointer(BaseCheckpointer): + KEYS_TO_SAVE = ["model", "optim", "trainer", "scheduler", "ema"] + KEYS_TO_POSTFIX = { + "model": "model", + "optim": "optim", + "ema": "ema", + "scheduler": "scheduler", + "trainer": "", + } + + @misc.timer("generate saving state dict") + def generate_save_state_dict( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> Optional[Dict[str, Any]]: + state_dict = {} + if parallel_state.get_data_parallel_rank() == 0: + trainer_state = dict( + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + model_state = model.state_dict() + optim_state = optimizer.state_dict() + scheduler_state = scheduler.state_dict() + self.callbacks.on_save_checkpoint(model, state_dict=trainer_state) + + trainer_state, model_state, optim_state, scheduler_state = misc.to( + [trainer_state, model_state, optim_state, scheduler_state], device="cpu" + ) + + state_dict = { + "trainer": trainer_state, + "model": model_state, + "optim": optim_state, + "scheduler": scheduler_state, + } + + if parallel_state.get_data_parallel_rank() < 3: + ema_state = model.ema.state_dict() + state_dict["ema"] = ema_state + + return state_dict + + def add_type_postfix_to_checkpoint_path(self, key: str, checkpoint_path: str, model: ImaginaireModel) -> str: + + # we need to get which ema should be saved + assert key in self.KEYS_TO_SAVE + post_fix = self.KEYS_TO_POSTFIX[key] + + if post_fix: + checkpoint_path = checkpoint_path.replace(".pt", f"_{post_fix}.pt") + else: + checkpoint_path = checkpoint_path + + if key == "ema": + dp_rank = parallel_state.get_data_parallel_rank() + checkpoint_path = checkpoint_path.replace(".pt", f"{dp_rank}.pt") + + if key == "trainer": + return checkpoint_path + else: + mp_rank = parallel_state.get_model_parallel_group().rank() + checkpoint_path = checkpoint_path.replace(".pt", f"_mp_{mp_rank}.pt") + + return checkpoint_path diff --git a/cosmos3/_src/imaginaire/config.py b/cosmos3/_src/imaginaire/config.py new file mode 100644 index 00000000..c57428ab --- /dev/null +++ b/cosmos3/_src/imaginaire/config.py @@ -0,0 +1,600 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Training config system for Imaginare4""" + +from __future__ import annotations + +import importlib +import os +import time +from typing import Any, Dict, Optional, Type, TypeVar, Union + +import attrs +import torch +from loguru import logger as logging + +from cosmos3._src.imaginaire.flags import TRAINING + +try: + from megatron.core import ModelParallelConfig + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.misc import Color + +T = TypeVar("T") + + +def _is_attrs_instance(obj: object) -> bool: + """ + Helper function to check if an object is an instance of an attrs-defined class. + + Args: + obj: The object to check. + + Returns: + bool: True if the object is an instance of an attrs-defined class, False otherwise. + """ + return hasattr(obj, "__attrs_attrs__") + + +def make_freezable(cls: T) -> T: + """ + A decorator that adds the capability to freeze instances of an attrs-defined class. + + NOTE: This requires the wrapped attrs to be defined with attrs.define(slots=False) because we need + to hack on a "_is_frozen" attribute. + + This decorator enhances an attrs-defined class with the ability to be "frozen" at runtime. + Once an instance is frozen, its attributes cannot be changed. It also recursively freezes + any attrs-defined objects that are attributes of the class. + + Usage: + @make_freezable + @attrs.define(slots=False) + class MyClass: + attribute1: int + attribute2: str + + obj = MyClass(1, 'a') + obj.freeze() # Freeze the instance + obj.attribute1 = 2 # Raises AttributeError + + Args: + cls: The class to be decorated. + + Returns: + The decorated class with added freezing capability. + """ + + if not hasattr(cls, "__dict__"): + raise TypeError( + "make_freezable cannot be used with classes that do not define __dict__. Make sure that the wrapped " + "class was defined with `@attrs.define(slots=False)`" + ) + + original_setattr = cls.__setattr__ + + def setattr_override(self, key, value) -> None: # noqa: ANN001 + """ + Override __setattr__ to allow modifications during initialization + and prevent modifications once the instance is frozen. + """ + if hasattr(self, "_is_frozen") and self._is_frozen and key != "_is_frozen": + raise AttributeError("Cannot modify frozen instance") + original_setattr(self, key, value) # type: ignore + + cls.__setattr__ = setattr_override # type: ignore + + def freeze(self: object) -> None: + """ + Freeze the instance and all its attrs-defined attributes. + """ + for _, value in attrs.asdict(self, recurse=False).items(): + if _is_attrs_instance(value) and hasattr(value, "freeze"): + value.freeze() + self._is_frozen = True # type: ignore + + cls.freeze = freeze # type: ignore + + return cls + + +def _pretty_print_attrs_instance(obj: object, indent: int = 0, use_color: bool = False) -> str: + """ + Recursively pretty prints attrs objects with color. + """ + + assert attrs.has(obj.__class__) + + lines: list[str] = [] + for attribute in attrs.fields(obj.__class__): + value = getattr(obj, attribute.name) + if attrs.has(value.__class__): + if use_color: + lines.append(" " * indent + Color.cyan("* ") + Color.green(attribute.name) + ":") + else: + lines.append(" " * indent + "* " + attribute.name + ":") + lines.append(_pretty_print_attrs_instance(value, indent + 1, use_color)) + else: + if use_color: + lines.append( + " " * indent + Color.cyan("* ") + Color.green(attribute.name) + ": " + Color.yellow(value) + ) + else: + lines.append(" " * indent + "* " + attribute.name + ": " + str(value)) + return "\n".join(lines) + + +def pretty_print_overrides(overrides: Optional[list[str]] = None, use_color: bool = False) -> str: + """ + Pretty prints overrides. + """ + + lines: list[str] = [] + lines.append(Color.cyan("* ") + Color.green("overrides") + ": ") + for override in overrides: + if override == "--": + continue + if override.startswith("~"): + attribute_name = override[1:] + attribute_value = None + else: + attribute_name, attribute_value = override.split("=") + if use_color: + lines.append(" " + Color.cyan("* ") + Color.green(attribute_name) + ": " + Color.yellow(attribute_value)) + else: + lines.append(" " + "* " + attribute_name + ": " + str(attribute_value)) + + return "\n".join(lines) + + +@make_freezable +@attrs.define(slots=False) # slots=False is required for make_freezable. See the make_freezable notes for more info. +class ObjectStoreConfig: + # Whether the file I/O is from object store instead of local disk. + enabled: bool = False + # Path to the object store credentials file. + credentials: str = "" + # Object store bucket to read from / write to the objects. + bucket: str = "" + + +@make_freezable +@attrs.define(slots=False) +class HFExportConfig: + # Whether to enable HuggingFace safetensors export after each DCP checkpoint. + enabled: bool = False + # HuggingFace Hub repo ID to push exported weights to (e.g. "nvidia/cosmos3-qwen3-8b"). + # None means local/S3 only. + hf_repo_id: Optional[str] = None + # Object store for uploading exports. If not enabled, upload is skipped. + # To reuse the DCP checkpoint bucket, copy checkpoint.save_to_object_store here. + upload_to_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + # Export every N DCP checkpoints. Must be >= 1 and ideally a multiple of checkpoint.save_iter. + export_every_n: int = attrs.field(default=1, validator=attrs.validators.ge(1)) + + +@make_freezable +@attrs.define(slots=False) +class JobConfig: + # Project name. + project: str = "" + # Experiment name. + group: str = "" + # Run/job name. + name: str = "" + # W&B mode, can be "online", or "disabled". + wandb_mode: str = "online" + # Cluster configuration (optional, for cluster-specific settings). + cluster: Optional[Any] = None + + @property + def path(self) -> str: + return f"{self.project}/{self.group}/{self.name}" + + @property + def path_local(self) -> str: + local_root = os.environ.get("IMAGINAIRE_OUTPUT_ROOT", "/tmp/imaginaire4-output") + return f"{local_root}/{self.path}" + + +@make_freezable +@attrs.define(slots=False) +class EMAConfig: + # Enable tracking a set of exponential moving average (EMA) weights. + enabled: bool = False + # EMA decay rate. + beta: float = 0.9999 + # Enable removing "_orig_mod-" from buffer names that is added by torch.compile + torch_compile_buffer_renaming: bool = False + + +@make_freezable +@attrs.define(slots=False) +class PowerEMAConfig: + # Enable tracking a set of exponential moving average (EMA) weights. + enabled: bool = False + # EDM2 paper EMA decay rate. + s: float = 0.1 + # Enable removing "_orig_mod-" from buffer names that is added by torch.compile + torch_compile_buffer_renaming: bool = False + + +@make_freezable +@attrs.define(slots=False) +class DDPConfig: + # Traverse the computation graph to find parameters that don't receive gradients. + find_unused_parameters: bool = False + # Set to True if the computation graph does not change during the whole training loop. + static_graph: bool = True + # Set to True if we want to synchronize buffers. Set to False if the sync is going to be handled elsewhere. + broadcast_buffers: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CuDNNConfig: + # Set to True for better reproducibility of the results (only using deterministic cudnn functions). + deterministic: bool = False + # If set to True, cudnn will benchmark several algorithms and pick the fastest one. + benchmark: bool = True + + +@make_freezable +@attrs.define(slots=False) +class JITConfig: + # Enable exporting a JIT compiled model. + enabled: bool = False + # Input tensor shape, for example input. + input_shape: Union[list[int], None] = None + # Device to compile onto. + device: str = "cuda" + # # Data type to compile onto. + dtype: str = "bfloat16" + # Strict mode for PyTorch JIT. + strict: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CheckpointConfig: + # possible checkpoint class + type: Optional[Dict] = None + + # for dcp, whether to use async mode + dcp_async_mode_enabled: bool = False + + # Configs for saving the checkpoints to object store. + save_to_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + + # Save the checkpoint every N iterations. + save_iter: int = 999999999 + + # Load state_dict to the models in strict mode. If True, `allow_partial_load` in dcp + # planner will be set to False. DCP will raise an error if there are missing keys. + # If False, `allow_partial_load` in dcp planner will be set to True. DCP will not + # raise an error if there are missing keys. + strict_resume: bool = True + + # Configs for loading the checkpoints from object store. + load_from_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + + # Path of model weights to resume the checkpoint from. + load_path: str = "" + + # The following 3 flags (load_training_state, only_load_scheduler_state, keys_to_skip_loading) + # only take effect when the checkpoints are loaded from `load_path`. If loading happens from + # the previous checkpoint of the same model, these flags are ignored. + + # Whether to load the training states (optimizer/scheduler/grad-scaler) from the checkpoint path. + load_training_state: bool = False + + # Whether to load the scheduler state only from the checkpoint path. If + # load_training_state is True, this will be ignored. + only_load_scheduler_state: bool = False + + # When loading checkpoints from `load_path`, this list serves as a filter + # to bypass the loading for specific model parameters. A key is considered + # a match—and thus its loading is skipped—if it contains any element of this + # list as a substring. This mechanism allows for broad suppression of entire + # modules or parameter groups without requiring the specification of fully + # qualified names (FQNs). Skipping loading of keys is useful when the new model + # has a different component architecture, e.g. different RoPE embeddings than + # the current model. + keys_to_skip_loading: list[str] = [] + + # Configs for JIT compiling EMA model. + jit: JITConfig = attrs.field(factory=JITConfig) + + # Print detailed information during checkpoint saving/loading. + verbose: bool = True + + # Keys not to resume from the checkpoint, choices: ["model", "optim", "scheduler", "trainer", "dataloader"] + keys_not_to_resume: list[str] = [] + + # Whether to use the local filesystem for broadcasting checkpoint data (used for Tensor Parallel Checkpointer). + broadcast_via_filesystem: bool = False + load_ema_to_reg: bool = False + + # Enable GCS patch in boto3 for loading/saving checkpoints from/to GCS + enable_gcs_patch_in_boto3: bool = False + + # Config for exporting HuggingFace-compatible safetensors after each DCP checkpoint. + hf_export: HFExportConfig = attrs.field(factory=HFExportConfig) + + +@make_freezable +@attrs.define(slots=False) +class NVTXConfig: + """Config for NVTX ranges used in the main training loop. + + See tutorials/nanogpt for more details on how to integrate profiling into your model.""" + + # Enable the NVTX ranges. + enabled: bool = False + # Synchronize everything in each NVTX range. + cuda_synchronize: bool = False + + +@make_freezable +@attrs.define(slots=False) +class StragglerDetectionConfig: + """Config for Straggler detection tool: https://gitlab-master.nvidia.com/dl/gwe/fault_tolerance_related/straggler/-/tree/cupti?ref_type=heads""" + + # Enable the Straggler Detection. + enabled: bool = False + # How frequently should the Straggler reports be generated. + report_freq: int = 100 + # How frequently iterations should be profiled + profile_freq: int = 1 + # What is the maximum relative difference between GPUs after they are considered stragglers + max_diff: float = 2.0 + # Should the error be raised when straggler is detected + raise_error: bool = True + # Analyze kernels in the forward pass. + analyze_forward: bool = True + # Analyze kernels in the backward pass. + analyze_backward: bool = True + # Analyze kernels in the optimizer. + analyze_optimizer: bool = True + # Analyze dataloading time. + analyze_dataloading: bool = True + # Whether to save logs to S3 + save_s3: bool = False + + +@make_freezable +@attrs.define(slots=False) +class Profiling: + # Torch profiler: set this True to dump chrome traces. + enable_profiling: bool = False + # Nsight Systems: set this True AND launch under `nsys profile --capture-range=cudaProfilerApi`. + enable_nsys: bool = False + # CUDA memory snapshot: set this True to dump allocator snapshots. + enable_memory_snapshot: bool = False + save_s3: bool = False + profile_freq: int = 1 + # Number of warmup iterations before the active profile iteration. + profile_warmup: int = 3 + # Target ranks for profiling, each entry must be >=0 and < world_size. + target_ranks: list[int] = list(range(8)) + # The options below apply only to the torch profiler (enable_profiling). + # Set `record_shape` and `profile_memory` to False to reduce profile size. + record_shape: bool = False + profile_memory: bool = False + with_stack: bool = True + with_modules: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CompileConfig: + """ + torch.compile config options passed to set_torch_compile_options function. + """ + + recompile_limit: int = 8 + use_duck_shape: bool = True + + +@make_freezable +@attrs.define(slots=False) +class TrainerConfig: + if TRAINING: + from cosmos3._src.imaginaire.trainer import ImaginaireTrainer + from cosmos3._src.imaginaire.utils import callback + + type: Type[ImaginaireTrainer] = ImaginaireTrainer + # Set the callback class. + # Defaults to the callbacks below. + callbacks: LazyDict = LazyDict( + dict( + ema=L(callback.EMAModelCallback)(), + progress_bar=L(callback.ProgressBarCallback)(), + wandb=L(callback.WandBCallback)(), + ) + ) + + # distributed parallelism strategy + distributed_parallelism: str = "ddp" + # Distributed data parallel configs. + ddp: DDPConfig = attrs.field(factory=DDPConfig) + # cuDNN configs. + cudnn: CuDNNConfig = attrs.field(factory=CuDNNConfig) + # Set the random seed. + seed: int = 0 + # Gradient scaler arguments (for torch.amp.GradScaler). + grad_scaler_args: dict = attrs.field(factory=lambda: dict(enabled=False)) + # Maximum number of iterations to train the model. + max_iter: int = 999999999 + # Maximum number of iterations to validate the model. If None, validate on the entire dataset. + max_val_iter: int | None = None + # How often we log the training stats. + logging_iter: int = 100 + # Whether we want to run the validation routines. + run_validation: bool = True + # How often we evaluate on the validation set. + validation_iter: int = 999999999 + # Whether to run the validation on the start of the training. + run_validation_on_start: bool = False + # Kill the process after N seconds since the last iteration (usually means dead job). + timeout_period: int = 999999999 + # Tensor memory organization format. + memory_format: torch.memory_format = torch.preserve_format + # Gradient accumulation (update step every N iteration). + grad_accum_iter: int = 1 + # Straggler Detection config + straggler_detection: StragglerDetectionConfig = attrs.field(factory=StragglerDetectionConfig) + # Profiling config + profiling: Profiling = attrs.field(factory=Profiling) + compile_config: CompileConfig = attrs.field(factory=CompileConfig) + + # Whether to save the checkpoint at iteration 0. + save_zero_checkpoint: bool = False + + +@make_freezable +@attrs.define(slots=False) +class Config: + """Config for an imaginaire4 job. + + See /README.md/Configuration System for more info. + """ + + # Model configs. + model: LazyDict + # Optimizer configs. + optimizer: LazyDict + # Scheduler configs. + scheduler: LazyDict + # Training data configs. + dataloader_train: LazyDict | None + # Validation data configs. + dataloader_val: LazyDict | None + + # Training job configs. + job: JobConfig = attrs.field(factory=JobConfig) + + # Trainer configs. + trainer: TrainerConfig = attrs.field(factory=TrainerConfig) + + if USE_MEGATRON: + # Megatron-Core configs + model_parallel: ModelParallelConfig = attrs.field(factory=ModelParallelConfig) + else: + model_parallel: None = None + + # Checkpointer configs. + checkpoint: CheckpointConfig = attrs.field(factory=CheckpointConfig) + + # enable upload reproducible setup to s3 + upload_reproducible_setup: bool = False + + def pretty_print(self, use_color: bool = False) -> str: + return _pretty_print_attrs_instance(self, 0, use_color) + + def to_dict(self) -> dict[str, Any]: + return attrs.asdict(self) + + def model_init_kwargs(self) -> dict[str, Any]: + """Live root-level sub-configs to pass into instantiate(self.model). + + Override in subclasses whose model __init__ expects fully-composed + top-level configs (e.g. policy/checkpoint/train) rather than the stale + LazyCall snapshot stored under config.model.* at make_config() time. + """ + return {} + + + def validate(self) -> None: + """Validate that the config has all required fields.""" + + # broadcast job.name across all ranks to make sure it is consistent + # otherwise, unaligned job names leads unaligned path to save checkpoints + job_name_tensor = torch.ByteTensor(bytearray(self.job.name, "utf-8")).cuda() + distributed.broadcast(job_name_tensor, 0) + self.job.name = job_name_tensor.cpu().numpy().tobytes().decode("utf-8") + + + assert self.job.project != "" + assert self.job.group != "" + assert self.job.name != "" + + +def load_config(config_path: str, opts: list[str], enable_one_logger: bool = False) -> Config: + from cosmos3._src.imaginaire.serialization import from_yaml, load_callable + + t1 = time.monotonic_ns() + if config_path.endswith(".yaml"): + config = from_yaml(config_path) + # for registration of dataloaders, etc. + _ = load_callable(config.__module__).make_config() + + from cosmos3._src.imaginaire.utils.config_helper import override + + config = override(config, opts, remove_defaults=True) + else: + config = _load_py_config(config_path, opts, validate=False) + + if enable_one_logger: + try: + # pyrefly: ignore # missing-import + from cosmos3._src.imaginaire.utils.one_logger.one_logger_override_utils import override_one_logger_callback + + ol_t1 = time.monotonic_ns() + config = override_one_logger_callback(config) + ol_t2 = time.monotonic_ns() + logging.debug(f"override_one_logger_callback: took {(ol_t2 - ol_t1) / 1e6:.2f}ms") + except ImportError: + pass + + t2 = time.monotonic_ns() + logging.debug(f"total time to load config: {(t2 - t1) / 1e6:.2f}ms") + return config + + +def _load_py_config(config_path: str, opts: list[str], validate: bool = True) -> Config: + + from cosmos3._src.imaginaire.utils.config_helper import get_config_module, override + + t1 = time.monotonic_ns() + config_module = get_config_module(config_path) + t2 = time.monotonic_ns() + logging.debug(f"get_config_module: took {(t2 - t1) / 1e6:.2f}ms") + + t1 = time.monotonic_ns() + config = importlib.import_module(config_module).make_config() + t2 = time.monotonic_ns() + logging.debug(f"importlib.import_module: took {(t2 - t1) / 1e6:.2f}ms") + + t1 = time.monotonic_ns() + config = override(config, opts) + t2 = time.monotonic_ns() + logging.debug(f"override: took {(t2 - t1) / 1e6:.2f}ms") + + if validate: + t1 = time.monotonic_ns() + config.validate() + t2 = time.monotonic_ns() + logging.debug(f"config.validate: took {(t2 - t1) / 1e6:.2f}ms") + + return config diff --git a/cosmos3/_src/imaginaire/configs/lr_scheduler.py b/cosmos3/_src/imaginaire/configs/lr_scheduler.py new file mode 100644 index 00000000..c81bd5a6 --- /dev/null +++ b/cosmos3/_src/imaginaire/configs/lr_scheduler.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.functional.lr_scheduler import LambdaLinearScheduler +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.lazy_config import LazyDict + +LambdaLinearSchedulerConfig: LazyDict = L(LambdaLinearScheduler)( + warm_up_steps=[1000], + cycle_lengths=[10000000000000], + f_start=[1.0e-6], + f_max=[1.0], + f_min=[1.0], +) diff --git a/cosmos3/_src/imaginaire/datasets/__init__.py b/cosmos3/_src/imaginaire/datasets/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py b/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py new file mode 100644 index 00000000..a5ee4863 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +class DataDictMerger(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Merge the dictionary associated with the input keys into data_dict. Only keys in output_keys are merged. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with dictionary associated with the input keys merged. + """ + for key in self.input_keys: + if key not in data_dict: + log.warning( + f"DataDictMerger dataloader error: missing {key}, {data_dict['__url__']}, {data_dict['__key__']}", + rank0_only=False, + ) + return None + key_dict = data_dict.pop(key) + if key == "depth" and "depth" in self.output_keys: + data_dict["depth"] = key_dict + if key == "human_annotation" and "human_annotation" in self.output_keys: + data_dict["human_annotation"] = key_dict + elif key == "segmentation" and "segmentation" in self.output_keys: + data_dict["segmentation"] = key_dict + elif key == "canny" and "canny" in self.output_keys: + data_dict["canny"] = key_dict + for sub_key in key_dict: + if sub_key in self.output_keys and sub_key not in data_dict: + data_dict[sub_key] = key_dict[sub_key] + del key_dict + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py b/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py new file mode 100644 index 00000000..3adc1041 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Optional + +import numpy as np +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +def pad_and_resize( + arr_np: np.ndarray, ntokens: int, is_mask_all_ones: bool = False +) -> tuple[torch.Tensor, torch.Tensor]: + r"""Function for padding and resizing a numpy array. + Args: + arr (np.ndarray): Input array + ntokens (int): Number of output tokens after padding + is_mask_all_ones (bool): if true, set mask to ones + Returns: + arr_padded (torch.Tensor): Padded output tensor + mask (torch.Tensor): Padding mask + """ + + if isinstance(arr_np, np.ndarray): + arr = torch.from_numpy(arr_np) + elif isinstance(arr_np, torch.Tensor): + arr = arr_np.clone().detach() + else: + raise TypeError("`arr_np` should be a numpy array or torch tensor.") + embed_dim = arr.shape[1] + + arr_padded = torch.zeros(ntokens, embed_dim, device=arr.device, dtype=torch.float32) + + # If the input text is larger than num_text_tokens, clip it. + if arr.shape[0] > ntokens: + arr = arr[0:ntokens] + + mask = torch.LongTensor(ntokens).zero_() + if len(arr.shape) > 1: + mask[0 : arr.shape[0]] = 1 + + if len(arr.shape) > 1: + arr_padded[0 : arr.shape[0]] = arr + + if is_mask_all_ones: + mask.fill_(1) + + return arr_padded, mask + + +def _obtain_embeddings(cfg: dict, embeddings_captions: dict[str, list], caption_idx: int) -> dict: + r"""Function for obtaining text embeddings and text mask. + Args: + cfg (dict): Config dict + embeddings_captions (np.ndarray): Caption embeddings + caption_idx (int): Caption index + Returns: + Dictionary containing embeddings and mask + """ + out_dict = dict() + is_mask_all_ones = cfg["is_mask_all_ones"] + if "byt5_tokens" in cfg: + out_byt5_text, out_byt5_text_mask = pad_and_resize( + embeddings_captions["byt5_fp8"][caption_idx], + cfg["byt5_tokens"]["num"], + is_mask_all_ones=is_mask_all_ones, + ) + out_dict["byt5_text_embeddings"] = out_byt5_text + out_dict["byt5_text_mask"] = out_byt5_text_mask + + if "t5_tokens" in cfg: + out_t5, out_t5_mask = pad_and_resize( + embeddings_captions["t5_xxl_fp8"][caption_idx], + cfg["t5_tokens"]["num"], + is_mask_all_ones=is_mask_all_ones, + ) + out_dict["t5_text_embeddings"] = out_t5 + out_dict["t5_text_mask"] = out_t5_mask + + return out_dict + + +def obtain_data_dict_from_mixed_gt_and_ai_captions(data_dict: dict, input_keys: list, args: Optional[dict] = None): + out_pkl_dict = dict() + + captions_gt = data_dict[input_keys[0]] + decoded_captions_ai = data_dict[input_keys[1]] + embeddings_captions_gt = data_dict[input_keys[2]] + embeddings_captions_ai = data_dict[input_keys[3]] + + assert args is not None, "Please specify args in augmentation" + probabilities = [args["caption_probs"]["ground_truth"], args["caption_probs"]["vfc_fidelity"]] + valid_captions_indices = list(range(len(probabilities))) + caption_idx = random.choices(valid_captions_indices, weights=probabilities, k=1)[0] + + # If VFC Fidelity caption is not valid, we will use the ground truth caption + if caption_idx == 1 and decoded_captions_ai["had_parse_issue"]: + caption_idx = 0 + + # Merging GT and AI caption raw text + captions = captions_gt["text"] + [decoded_captions_ai["captions"]["vfc_fidelity"]] + + # Merging GT and AI caption embeddings + gt_embeddings = [] + for key in ["ground_truth_headline", "ground_truth"]: + if key in embeddings_captions_gt: + if embeddings_captions_gt[key] is not None: + gt_embeddings.append(embeddings_captions_gt[key]) + + # Randomly select one of the GT embeddings + gt_embedding = random.choice(gt_embeddings) + embeddings_captions = {} + for key in embeddings_captions_ai["vfc_fidelity"]["embeddings"].keys(): + embeddings_captions[key] = [ + gt_embedding["embeddings"][key], + embeddings_captions_ai["vfc_fidelity"]["embeddings"][key], + ] + + # Sampling raw caption and embeddings + raw_captions = captions[caption_idx] + data_dict["raw_captions"] = raw_captions + + embeddings_dict = _obtain_embeddings( + cfg=args, + embeddings_captions=embeddings_captions, + caption_idx=caption_idx, + ) + out_pkl_dict.update(embeddings_dict) + + data_dict.update(out_pkl_dict) + for key in input_keys: + del data_dict[key] + + return data_dict + + +class TextTransform(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs camera transformation. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with camera attributes added + """ + return obtain_data_dict_from_mixed_gt_and_ai_captions(data_dict, self.input_keys, self.args) + + +class TextTransformAIOnly(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs text transform for datasets where there are only AI captions (ex., NVCC). + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with camera attributes added + """ + + out_pkl_dict = dict() + decoded_captions_ai = data_dict[self.input_keys[0]] + embeddings_captions_ai = data_dict[self.input_keys[1]] + + assert self.args is not None, "Please specify args in augmentation" + + raw_captions = decoded_captions_ai["captions"]["vfc"] + embeddings_captions = {} + + if decoded_captions_ai["had_parse_issue"]: + raw_captions = decoded_captions_ai["captions"]["kosmos_2"] + _embeddings_captions = embeddings_captions_ai["kosmos2"] + else: + raw_captions = decoded_captions_ai["captions"]["vfc"] + _embeddings_captions = embeddings_captions_ai["vfc_fidelity"] + + for key in _embeddings_captions["embeddings"].keys(): + embeddings_captions[key] = [ + _embeddings_captions["embeddings"][key], + ] + + # Sampling raw caption and embeddings + data_dict["raw_captions"] = raw_captions + embeddings_dict = _obtain_embeddings( + cfg=self.args, + embeddings_captions=embeddings_captions, + caption_idx=0, + ) + out_pkl_dict.update(embeddings_dict) + + data_dict.update(out_pkl_dict) + for key in self.input_keys: + del data_dict[key] + + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/decoders/__init__.py b/cosmos3/_src/imaginaire/datasets/decoders/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/decoders/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py b/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py new file mode 100644 index 00000000..b1aec2be --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +from typing import Optional + + +def json_decoder(key: str, data: bytes) -> Optional[dict]: + r""" + Function to decode a json file. + Args: + key: Data key. + data: Data dict. + """ + extension = re.sub(r".*[.]", "", key) + if extension == "json": + data_dict = json.loads(data) + return data_dict + else: + return None diff --git a/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py b/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py new file mode 100644 index 00000000..0cae27f0 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import re +from typing import Optional + + +def pkl_decoder(key: str, data: bytes) -> Optional[dict]: + r""" + Function to decode a pkl file. + Args: + key: Data key. + data: Data dict. + """ + extension = re.sub(r".*[.]", "", key) + if extension == "pkl": + data_dict = pickle.loads(data) + return data_dict + else: + return None diff --git a/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py b/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py new file mode 100644 index 00000000..f00a364d --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py @@ -0,0 +1,781 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import math +import re +from random import randint +from typing import Callable, List, Tuple + +import numpy as np +import torch +from PIL import Image + +from cosmos3._src.imaginaire.utils import log + +Image.MAX_IMAGE_PIXELS = 933120000 +_VIDEO_EXTENSIONS = "mp4 avi webm mov".split() + +VIDEO_DECODER_OPTIONS = {} + + +def video_decoder_register(key): + def decorator(func): + VIDEO_DECODER_OPTIONS[key] = func + return func + + return decorator + + +@video_decoder_register("video_decoder_metadata") +def video_decoder_metadata(num_threads, **kwargs): + """ + Video decoder using the video's native fps + """ + import decord + + def video_decoder(key: str, data: bytes): + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + video_buffer = io.BytesIO(data) + reader = decord.VideoReader(video_buffer, num_threads=num_threads) + num_frames = len(reader) + video_fps = int(np.round(reader.get_avg_fps())) + length_in_s = float(num_frames) / float(video_fps) + bitrate = video_buffer.getbuffer().nbytes * 8 / length_in_s + video_frames = reader.get_batch([0]).asnumpy() + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + return video_frames, {"fps": video_fps, "num_frames": num_frames, "bitrate": bitrate} + + return video_decoder + + +@video_decoder_register("video_decoder_w_controlled_fps") +def video_decoder_w_controlled_fps( + sequence_length: int = 34, + chunk_size: int = 0, + use_fps_control: bool = False, + min_fps_thres: int = 4, + max_fps_thres: int = 30, + sampling_reweighting: bool = False, + sampling_reweighting_factor: int = 1, + num_threads=4, + limit_fps_range: bool = False, + save_raw: bool = False, +): + """ + Video decoder using with fps control. + This function samples videos with fps in the range [min_fps_thres, max_fps_thres]. + We adjust the fps range if min and max fps cannot be supported to get the sequence length with desired chunk size. + + Parameters: + - sequence_length (int) : Number of frames returned by the function + - chunk_size (int): How the video is divided into chunks. Only return frames within a chunk. chunk_size=0 means we use full video length. Defaults to 0. + - min_fps_thres (int): Minimum fps threshold to sample from. + - max_fps_thres (int): Maximum fps threshold to sample from. + - sampling_reweighting (bool): If False, sample fps weights uniformly. If True, reweight sampling distrubution. + - sampling_reweighting_factor (int): The fps sampling distribution reweighting factor. If sampling_reweighting_factor > 1, sample more on lower fps side. + - num_thread (int): Number of threads for decord. + - save_raw (bool): If True, will also return entire raw video in data_dict key "video_raw_bytes", alongside with the video frames. Only enable this for visualization and debug. + """ + import decord + + def video_decoder( + key: str, + data: bytes, + ): + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + num_target_frames = sequence_length if sequence_length > 0 else len(video_reader) + num_orig_frames = len(video_reader) + + # Obtain the number of chunks + if chunk_size == 0: + curr_chunk_size = num_orig_frames + else: + curr_chunk_size = chunk_size + num_chunks = max(num_orig_frames // curr_chunk_size, 1) + + # Checks to ensure that number of target frames we need is present in the video / chunk. + if num_target_frames > curr_chunk_size: + raise ValueError( + f"Specified sequence_length {num_target_frames} exceeds curr_chunk_size {curr_chunk_size}, num_orig_frames={num_orig_frames}, chunk_size={chunk_size}" + ) + + if num_target_frames > num_orig_frames: + raise ValueError( + f"Specified sequence_length {num_target_frames} exceeds num frames in video {num_orig_frames}." + ) + + # Now obtain min and max fps that we can use within this chunk + video_fps = int(np.round(video_reader.get_avg_fps())) + + if video_fps < 1: + raise ValueError("Video fps lower than 1, skipping") + if limit_fps_range: + if video_fps < min_fps_thres: + raise ValueError(f"Video fps {video_fps} lower than {min_fps_thres}, skipping") + if video_fps > max_fps_thres: + raise ValueError(f"Video fps {video_fps} larger than {max_fps_thres}, skipping") + + # Check if the last chunk has separate window + # This happens only if remainder frames >= curr_chunk_size / 2 [data annotation was done this way] + # Else this is used as a part of previous window. + num_frames_in_last_chunk = num_orig_frames - num_chunks * curr_chunk_size + if num_frames_in_last_chunk >= int(0.5 * curr_chunk_size): + if num_frames_in_last_chunk > num_target_frames: + num_chunks += 1 + + # Sample which chunk to use + chunk_index = randint(0, num_chunks - 1) + + if chunk_index == num_chunks - 1: + # For the last chunk, use all of the remaining frames + num_samples_in_chunk = num_orig_frames - chunk_index * curr_chunk_size + else: + # Else use only the chunk size + num_samples_in_chunk = curr_chunk_size + + if use_fps_control: + # When fps control is provided, sample random fps. + min_fps = max(min_fps_thres, math.ceil(video_fps * float(num_target_frames) / float(num_samples_in_chunk))) + max_fps = min(max_fps_thres, video_fps) + + # Randomly sample a target fps in the range of (min_fps, max_fps) + if max_fps > min_fps: + fps_selections = list(range(min_fps, max_fps + 1)) + + # Sample reweighting favors the smaller fps more + if sampling_reweighting: + dist = [1 / (float(pp) ** sampling_reweighting_factor) for pp in fps_selections] + target_fps = np.random.choice(fps_selections, 1, p=[pp / sum(dist) for pp in dist]) + else: + target_fps = np.random.choice(fps_selections, 1) + else: + target_fps = max_fps + + else: + # If not, use native fps + target_fps = video_fps + + # stride used for subsampling video + stride = int(video_fps / target_fps) + + # This is the actual target fps we obtain after subsampling + target_fps = video_fps / stride + + # Select the frame start index and frame end index + chunk_frame_start = chunk_index * curr_chunk_size + if num_samples_in_chunk <= num_target_frames * stride: + raise ValueError( + f"Decoded video not long enough, num_samples_in_chunk={num_samples_in_chunk}, num_target_frames={num_target_frames}, stride={stride}, video_fps={video_fps}, target_fps={target_fps}, min_fps_thres={min_fps_thres}, max_fps_thres={max_fps_thres}, use_fps_control={use_fps_control}" + ) + # Start index is randomly selected in the chunk + frame_start = chunk_frame_start + int( + np.random.choice(num_samples_in_chunk - int(num_target_frames * stride), 1) + ) + frame_end = frame_start + num_target_frames * stride + + # Subsample the frames + if "depth" in key: + frame_start = video_decoder.frame_start + frame_end = video_decoder.frame_end + stride = video_decoder.stride + chunk_index = video_decoder.chunk_index + else: + video_decoder.frame_start = frame_start + video_decoder.frame_end = frame_end + video_decoder.stride = stride + video_decoder.chunk_index = chunk_index + video_frames = video_reader.get_batch(np.arange(frame_start, frame_end, stride).tolist()).asnumpy() + + # Return the frames and metadata + if num_target_frames is not None and video_frames.shape[0] < num_target_frames: + raise ValueError("Decoded video not long enough, skipping") + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + + ret_dict = { + "video": video_frames, + "fps": float(target_fps), + "num_frames": video_frames.shape[1], + "chunk_index": chunk_index, + "frame_start": frame_start, + "frame_end": frame_end, + "stride": stride, + "orig_num_frames": num_orig_frames, + } + if save_raw: + ret_dict["video_raw_bytes"] = data + return ret_dict + + return video_decoder + + +@video_decoder_register("video_decoder_for_kd_dataset") +def video_decoder_for_kd_dataset( + sequence_length: int = 34, + num_threads: int = 4, + save_raw: bool = False, + **kwargs, +): + """ + Video decoder for Knowledge Distillation dataset. + This function reads in the raw video frames, without any fps control. + + Parameters: + - sequence_length (int) : Number of frames returned by the function + - num_thread (int): Number of threads for decord. + - save_raw (bool): If True, will also return entire raw video in data_dict key "video_raw_bytes", alongside with the video frames. Only enable this for visualization and debug. + """ + import decord + + def video_decoder( + key: str, + data: bytes, + ): + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + num_target_frames = sequence_length if sequence_length > 0 else len(video_reader) + num_orig_frames = len(video_reader) + assert num_target_frames == num_orig_frames, ( + "Number of target frames must be equal to the number of original frames" + ) + + # Now obtain min and max fps that we can use within this chunk + video_fps = int(np.round(video_reader.get_avg_fps())) + assert video_fps == 24, "Generated video FPS should be 24" + + # Sample which chunk to use + chunk_index = 0 + frame_start = 0 + stride = 1 + frame_end = frame_start + num_target_frames * stride + video_frames = video_reader.get_batch(np.arange(frame_start, frame_end, stride).tolist()).asnumpy() + + # Return the frames and metadata + if num_target_frames is not None and video_frames.shape[0] < num_target_frames: + raise ValueError("Decoded video not long enough, skipping") + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + + ret_dict = { + "video": video_frames, + "fps": float(video_fps), + "num_frames": video_frames.shape[1], + "chunk_index": chunk_index, + "frame_start": frame_start, + "frame_end": frame_end, + "stride": stride, + "orig_num_frames": num_orig_frames, + } + if save_raw: + ret_dict["video_raw_bytes"] = data + return ret_dict + + return video_decoder + + +@video_decoder_register("video_decoder_basic") +def video_decoder_basic( + sequence_length: int = 25, + use_fps_control: bool = False, + min_fps_thres: int = 4, + max_fps_thres: int = 30, + num_threads=4, + **kwargs, +) -> Callable[[str, bytes], dict[str, torch.Tensor | int]]: + """Basic video decoder for a specified sequence length. + + If loaded video has fewer frames than requested, temporally pads with the last frame. + Optionally, allows subsampling video with a variable FPS in [`min_fps_thres` .. `max_fps_thres`]. + + Args: + sequence_length (int) : The number of frames to sample from the loaded video. + use_fps_control (bool) : Controls whether to temporally subsample. + min_fps_thres (int): Minimum FPS threshold to sample from. + max_fps_thres (int): Maximum FPS threshold to sample from. + num_thread (int): Number of threads for the decord. + + Returns: + Returns a callable that returns a dictionary of: + - The sampled video(torch.Tensor, torch.uint8), layout (C, T, H, W). + - The FPS (int) of the sample. + """ + import decord + + def video_decoder( + key: str, + data: bytes, + ) -> dict[str, torch.Tensor | int]: + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + + # video and request metadata. + num_target_frames = sequence_length if sequence_length > 0 else len(video_reader) + num_orig_frames = len(video_reader) + assert num_orig_frames > 0, "Video has no frames." + video_fps = max(1, int(video_reader.get_avg_fps() + 0.5)) + + if use_fps_control: + # When fps control is provided, sample random fps. + min_fps = max(min_fps_thres, math.ceil(video_fps * float(num_target_frames) / float(num_orig_frames))) + max_fps = min(max_fps_thres, video_fps) + + # If frame range is valid, sample random fps in the range of (min_fps, max_fps) + if max_fps > min_fps: + fps_selections = list(range(min_fps, max_fps + 1)) + target_fps = np.random.choice(fps_selections, 1) + else: + target_fps = max_fps + else: + target_fps = video_fps + + # This is the actual target fps we obtain after subsampling. + stride = int(video_fps / target_fps) + target_fps = video_fps / stride + num_target_stride_frames = int(num_target_frames * stride) + + # Start index is randomly selected in the + valid_length = max(num_orig_frames - num_target_stride_frames, 1) + frame_start = np.random.choice(valid_length, 1) + frame_end = min(frame_start + num_target_stride_frames, num_orig_frames) + frame_indices = np.arange(frame_start, frame_end, stride).tolist() + + # Grab the frames. + video_frames = video_reader.get_batch(frame_indices).asnumpy() + + # If sampled frames are less than requested, pad with the last frame via replication + if video_frames.shape[0] < num_target_frames: + pad_size = num_target_frames - video_frames.shape[0] + video_frames = np.pad(video_frames, ((0, pad_size), (0, 0), (0, 0), (0, 0)), mode="edge") + + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + return { + "video": video_frames, + "fps": float(target_fps), + } + + return video_decoder + + +@video_decoder_register("video_decoder_still_padding") +def video_decoder_still_padding( + sequence_length: int = 25, + use_fps_control: bool = False, + min_fps_thres: int = 4, + max_fps_thres: int = 30, + num_threads=4, + sampling_reweighting: bool = False, + sampling_reweighting_factor: int = 1, + limit_fps_range: bool = False, + **kwargs, +) -> Callable[[str, bytes], dict[str, torch.Tensor | int]]: + """Video decoder for a specified sequence length. + + If loaded video has fewer frames than requested, temporally pads with the last frame. + Optionally, allows subsampling video with a variable FPS in [`min_fps_thres` .. `max_fps_thres`]. + + Args: + sequence_length (int) : The number of frames to sample from the loaded video. + use_fps_control (bool) : Controls whether to temporally subsample. + min_fps_thres (int): Minimum FPS threshold to sample from. + max_fps_thres (int): Maximum FPS threshold to sample from. + num_thread (int): Number of threads for the decord. + + Returns: + Returns a callable that returns a dictionary of: + - The sampled video(torch.Tensor, torch.uint8), layout (C, T, H, W). + - number of video frames + - frame_start + - frame_end + """ + import decord + + def video_decoder( + key: str, + data: bytes, + ) -> dict[str, torch.Tensor | int]: + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + + # video and request metadata. + num_target_frames = sequence_length if sequence_length > 0 else len(video_reader) + num_orig_frames = len(video_reader) + assert num_orig_frames > 0, "Video has no frames." + + if num_target_frames > num_orig_frames: + log.warning( + f"Specified sequence_length {num_target_frames} exceeds num frames in video {num_orig_frames}. Padding last frame" + ) + # Grab the frames. + video_frames = video_reader.get_batch(range(num_orig_frames)).asnumpy() + + # Pad with the last frame via replication + pad_size = num_target_frames - video_frames.shape[0] + video_frames = np.pad(video_frames, ((0, pad_size), (0, 0), (0, 0), (0, 0)), mode="edge") + + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + return { + "video": video_frames, + "frame_start": 0, + "frame_end": num_orig_frames, + "num_frames": video_frames.shape[1], + } + + video_fps = max(1, int(video_reader.get_avg_fps() + 0.5)) + + if video_fps < 1: + raise ValueError("Video fps lower than 1, skipping") + if limit_fps_range: + if video_fps < min_fps_thres: + raise ValueError(f"Video fps {video_fps} lower than {min_fps_thres}, skipping") + if video_fps > max_fps_thres: + raise ValueError(f"Video fps {video_fps} larger than {max_fps_thres}, skipping") + + if use_fps_control: + # When fps control is provided, sample random fps. + min_fps = max(min_fps_thres, math.ceil(video_fps * float(num_target_frames) / float(num_orig_frames))) + max_fps = min(max_fps_thres, video_fps) + + # If frame range is valid, sample random fps in the range of (min_fps, max_fps) + if max_fps > min_fps: + fps_selections = list(range(min_fps, max_fps + 1)) + + # Sample reweighting favors the smaller fps more + if sampling_reweighting: + dist = [1 / (float(pp) ** sampling_reweighting_factor) for pp in fps_selections] + target_fps = np.random.choice(fps_selections, 1, p=[pp / sum(dist) for pp in dist]) + else: + target_fps = np.random.choice(fps_selections, 1) + else: + target_fps = max_fps + else: + target_fps = video_fps + + # This is the actual target fps we obtain after subsampling. + stride = int(video_fps / target_fps) + target_fps = video_fps / stride + num_target_stride_frames = int(num_target_frames * stride) + + # Start index is randomly selected in the + valid_length = max(num_orig_frames - num_target_stride_frames, 1) + frame_start = np.random.choice(valid_length, 1) + frame_end = min(frame_start + num_target_stride_frames, num_orig_frames) + frame_indices = np.arange(frame_start, frame_end, stride).tolist() + + # Grab the frames. + video_frames = video_reader.get_batch(frame_indices).asnumpy() + + # If sampled frames are less than requested, pad with the last frame via replication + if video_frames.shape[0] < num_target_frames: + pad_size = num_target_frames - video_frames.shape[0] + video_frames = np.pad(video_frames, ((0, pad_size), (0, 0), (0, 0), (0, 0)), mode="edge") + + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + return { + "video": video_frames, + "frame_start": frame_start, + "frame_end": frame_end, + "num_frames": video_frames.shape[1], + } + + return video_decoder + + +def video_decoder_w_lower_fps_get_indices( + num_orig_frames: int, + video_fps: int, + min_fps_thres: int, + max_fps_thres: int, + sequence_length: int, +) -> Tuple[List[int], float]: + """Generates frame indices for video sampling with FPS control. + + This function determines valid stride lengths for sampling frames from a video, + preferring lower FPS (larger strides) when multiple options are available. + It returns both the selected frame indices and the resulting FPS. + + Args: + num_orig_frames: Total number of frames in the original video. + video_fps: Original video frames per second. + min_fps_thres: Minimum allowed frames per second. + max_fps_thres: Maximum allowed frames per second. + sequence_length: Number of frames to sample. + + Returns: + A tuple containing: + - list[int]: Frame indices to sample from the original video. + - float: The resulting frames per second after sampling. + + Raises: + ValueError: If no valid stride options are available given the constraints. + ValueError: If input parameters are invalid (e.g., negative values). + """ + # Validate input parameters + if num_orig_frames <= 0: + raise ValueError("num_orig_frames must be positive") + if video_fps <= 0: + raise ValueError("video_fps must be positive") + if min_fps_thres <= 0: + raise ValueError("min_fps_thres must be positive") + if max_fps_thres < min_fps_thres: + raise ValueError("max_fps_thres must be greater than or equal to min_fps_thres") + if sequence_length <= 1: + raise ValueError("sequence_length must be greater than 1") + if sequence_length > num_orig_frames: + raise ValueError("sequence_length cannot be greater than num_orig_frames") + + # Calculate stride range + min_stride = 1 + max_stride = (num_orig_frames - 1) // (sequence_length - 1) + + valid_strides = [] + for stride in range(min_stride, max_stride + 1): + # Check if we can get sequence_length frames with this stride + if (num_orig_frames - stride * (sequence_length - 1)) > 0: + new_fps = video_fps / stride + if min_fps_thres <= new_fps <= max_fps_thres: + valid_strides.append(stride) + + if not valid_strides: + raise ValueError( + f"No valid stride options available for the given constraints. " + f"stride range = [{min_stride}, {max_stride}]; " + f"original FPS = {video_fps}; " + f"sequence_length = {sequence_length}; " + f"min_fps_thres = {min_fps_thres}; " + f"max_fps_thres = {max_fps_thres}; " + f"original num_frames = {num_orig_frames}" + ) + + # Select stride with weighted probability + if len(valid_strides) >= 2: + stride_choices = valid_strides[-2:] # Taking last two as they're the largest + weights = [0.01, 0.99] # [smaller_stride, larger_stride] + selected_stride = np.random.choice(stride_choices, p=weights) + else: + selected_stride = valid_strides[0] + + # Calculate the maximum valid start index and random start frame + max_start_idx = num_orig_frames - (sequence_length - 1) * selected_stride + frame_start = np.random.randint(0, max_start_idx) + + # Generate frame indices + frame_indices = [frame_start + i * selected_stride for i in range(sequence_length)] + return frame_indices, video_fps / selected_stride + + +@video_decoder_register("video_decoder_w_lower_fps") +def video_decoder_w_lower_fps( + chunk_size: int = 0, + sequence_length: int = 34, + min_fps_thres: int = 4, + max_fps_thres: int = 30, + num_threads: int = 4, + return_frame_indices: bool = False, + **kwargs, +) -> dict: + """ + Simplified video decoder with FPS control and frame sampling. + + Args: + key: Video file name/key + data: Video binary data + min_fps_thres: Minimum FPS threshold + max_fps_thres: Maximum FPS threshold + sequence_length: Number of frames to return + num_threads: Number of threads for decord + limit_fps_range: Whether to enforce FPS limits + return_frame_indices: Whether to return frame indices + + Returns: + dict with video frames tensor and target FPS + """ + import decord + + del kwargs # Unused + + def video_decoder( + key: str, + data: bytes, + ) -> dict[str, torch.Tensor | int]: + # Check video extension + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + # Read video + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + num_target_frames = sequence_length if sequence_length > 0 else len(video_reader) + + # Get video metadata + num_orig_frames = len(video_reader) + video_fps = int(np.round(video_reader.get_avg_fps())) + + # Basic validations + # Obtain the number of chunks + if chunk_size == 0: + curr_chunk_size = num_orig_frames + else: + curr_chunk_size = chunk_size + num_chunks = max(num_orig_frames // curr_chunk_size, 1) + + # Checks to ensure that number of target frames we need is present in the video / chunk. + if num_target_frames > curr_chunk_size: + raise ValueError("Specified sequence_length exceeds curr_chunk_size.") + + if num_target_frames > num_orig_frames: + raise ValueError( + f"Specified sequence_length {num_target_frames} exceeds num frames in video {num_orig_frames}." + ) + + if video_fps < 1: + raise ValueError("Video fps lower than 1, skipping") + if video_fps < min_fps_thres: + raise ValueError(f"Video fps {video_fps} lower than {min_fps_thres}, skipping") + + # Check if the last chunk has separate window + # This happens only if remainder frames >= curr_chunk_size / 2 [data annotation was done this way] + # Else this is used as a part of previous window. + num_frames_in_last_chunk = num_orig_frames - num_chunks * curr_chunk_size + if num_frames_in_last_chunk >= int(0.5 * curr_chunk_size): + if num_frames_in_last_chunk > num_target_frames: + num_chunks += 1 + + # Sample which chunk to use + chunk_index = randint(0, num_chunks - 1) + + if chunk_index == num_chunks - 1: + # For the last chunk, use all of the remaining frames + num_samples_cur_chunk = num_orig_frames - chunk_index * curr_chunk_size + else: + # Else use only the chunk size + num_samples_cur_chunk = curr_chunk_size + idx_first_in_cur_chunk = chunk_index * curr_chunk_size + + frame_indices, adjusted_fps = video_decoder_w_lower_fps_get_indices( + num_orig_frames=num_samples_cur_chunk, + video_fps=video_fps, + min_fps_thres=min_fps_thres, + max_fps_thres=max_fps_thres, + sequence_length=num_target_frames, + ) + frame_indices = [idx_first_in_cur_chunk + idx for idx in frame_indices] + + # Sample frames + video_frames = video_reader.get_batch(frame_indices).asnumpy() + video_frames = torch.from_numpy(video_frames).permute(3, 0, 1, 2) # (T, H, W, C) -> (C, T, H, W) + + # Clean up + video_reader.seek(0) + del video_reader + + output = { + "video": video_frames, + "fps": float(adjusted_fps), + "orig_fps": video_fps, + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "num_frames": video_frames.shape[1], + "orig_num_frames": num_orig_frames, + "chunk_index": chunk_index, + } + if return_frame_indices: + output["frame_indices"] = frame_indices + return output + + return video_decoder + + +@video_decoder_register("video_naive_bytes") +def video_naive_bytes(*args, **kwargs): + """ + do nothing, just return the video bytes + """ + del args, kwargs + + def video_decoder( + key: str, + data: bytes, + ): + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + return data + + return video_decoder + + +def construct_video_decoder( + video_decoder_name: str = "video_decoder_w_controlled_fps", + sequence_length: int = 34, + chunk_size: int = 0, + use_fps_control: bool = False, + min_fps_thres: int = 4, + max_fps_thres: int = 24, + sampling_reweighting: bool = False, + sampling_reweighting_factor: int = 1, + num_threads=4, + limit_fps_range: bool = False, + # if true, video decoder will additionally save the raw video (alongside with processed frames) to the data_dict + # set to true for inference/debugging + save_raw: bool = False, +): + return VIDEO_DECODER_OPTIONS[video_decoder_name]( + sequence_length=sequence_length, + chunk_size=chunk_size, + use_fps_control=use_fps_control, + min_fps_thres=min_fps_thres, + max_fps_thres=max_fps_thres, + sampling_reweighting=sampling_reweighting, + sampling_reweighting_factor=sampling_reweighting_factor, + num_threads=num_threads, + limit_fps_range=limit_fps_range, + save_raw=save_raw, + ) + + +def construct_video_decoder_metadata( + num_threads=4, +): + return VIDEO_DECODER_OPTIONS["video_decoder_metadata"](num_threads=num_threads) diff --git a/cosmos3/_src/imaginaire/datasets/joint_training.py b/cosmos3/_src/imaginaire/datasets/joint_training.py new file mode 100644 index 00000000..255c6dc0 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/joint_training.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility funcitons to use joint dataloader for training.""" + +from typing import Dict, Iterator # For multiview training + +import torch + +import cosmos3._src.imaginaire.config +import cosmos3._src.imaginaire.datasets.webdataset.dataloader +from cosmos3._src.imaginaire.config import Config +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log + + +def create_dataloader_dict( + config: Config, dataloader_train: cosmos3._src.imaginaire.datasets.webdataset.dataloader.DataLoader +) -> Dict: + """Create the dataloader dictionary. + + Example config: + + ``` + config: + joint_train: + data_sample_prob: + dataloader_train: 0.5 # sampling probability for default dataloader + dataloader_1: 0.2 # sampling probability for dataloader_1 + dataloader_2: 0.3 # sampling probability for dataloader_2 + dataloader_1: + ... # dataloader config for dataloader_1 + dataloader_2: + ... # dataloader config for dataloader_2 + ``` + + Args: + config (Config): The config object for the Imaginaire codebase. + + Returns: + dict: The dataloader dictionary. + """ + dataloader_list = list(config.joint_train.data_sample_prob.keys()) + + dataloader_dict = {} + for dataloader_name in dataloader_list: + if dataloader_name == "dataloader_train": + continue + log.info( + f"Creating dataloader: {dataloader_name}, sampling probability: {config.joint_train.data_sample_prob[dataloader_name]}" + ) + dataloader_dict[dataloader_name] = iter(instantiate(getattr(config.joint_train, dataloader_name))) + dataloader_dict["dataloader_train"] = iter(dataloader_train) + return dataloader_dict + + +def data_batch_iterator(dataloader_dict: Dict, data_sample_prob: Dict) -> Iterator[Dict]: + """Sample data batches continuously from the dataloader dictionary based on sampling probabilities.""" + dataloader_list = list(data_sample_prob.keys()) + + while True: + selected_dataloader_id = torch.multinomial( + torch.tensor([data_sample_prob[dataloader_name] for dataloader_name in dataloader_list]), 1 + ).item() + selected_dataloader_name = dataloader_list[selected_dataloader_id] + selected_dataloader = dataloader_dict[selected_dataloader_name] + + try: + data_batch = next(selected_dataloader) + except StopIteration: + # Reinitialize the iterator for the selected dataloader once it is exhausted + dataloader_dict[dataloader_list[selected_dataloader_id]] = iter(selected_dataloader) + data_batch = next(dataloader_dict[dataloader_list[selected_dataloader_id]]) + data_batch["dataloader_name"] = selected_dataloader_name + yield data_batch + + +def init_and_wrap_data_loaders(config: Config, dataloader_train: torch.utils.data.DataLoader) -> Dict: + """Wrap the dataloaders for multiview training. + + Args: + config (Config): The config object for the Imaginaire codebase. + dataloader_train (torch.utils.data.DataLoader): The training data loader. + + Returns: + dict: The dataloader dictionary. + """ + # Create the dataloader dictionary with multiple dataloaders + dataloader_dict = create_dataloader_dict(config, dataloader_train) + + # Create the data batch iterator sample from the dataloader dictionary based on sampling probabilities + dataloader_train = data_batch_iterator(dataloader_dict, config.joint_train.data_sample_prob) + return dataloader_train diff --git a/cosmos3/_src/imaginaire/datasets/mock_dataset.py b/cosmos3/_src/imaginaire/datasets/mock_dataset.py new file mode 100644 index 00000000..8d0c2d7d --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/mock_dataset.py @@ -0,0 +1,186 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Copied from jam_data by Qinsheng Zhang, with unknown license. +""" + +import inspect +from typing import Any, Callable, Dict + +import torch +from torch.utils.data import Dataset + +MAX_LENGTH = 1 << 15 + + +class LambdaDataset(torch.utils.data.Dataset): + """ + A dataset that generates items by applying a function. This allows for creating + dynamic datasets where the items are the result of function calls. The function can optionally + accept an index argument. + + Attributes: + length (int): The total number of items in the dataset. + fn (Callable): The function to generate dataset items. + is_index_in_params (bool): Flag to determine whether 'index' should be passed + to the function `fn`. + """ + + def __init__(self, fn: Callable, length: int = MAX_LENGTH) -> None: + """ + Initializes the LambdaDataset with a function and the total length. + + Args: + fn (Callable): A function that returns a dataset item. It can optionally accept an + index argument to generate data items based on their index. + length (int): The total number of items in the dataset, defaults to MAX_LENGTH. + """ + self.length = length + self.fn = fn + + try: + # Attempt to inspect the function signature to determine if it accepts an 'index' parameter. + signature = inspect.signature(fn) + self.is_index_in_params = "index" in signature.parameters + except ValueError: + # If the function signature is not inspectable, assume 'index' is not a parameter. + self.is_index_in_params = False + + def __len__(self) -> int: + """ + Returns the total length of the dataset. + + Returns: + int: The number of items in the dataset. + """ + return self.length + + def __getitem__(self, index: int) -> Any: + """ + Retrieves an item at a specific index from the dataset by calling the function `fn`. + Passes the index to `fn` if `fn` is designed to accept an index. + + Args: + index (int): The index of the item to retrieve. + + Returns: + Any: The item returned by the function `fn`. + """ + if self.is_index_in_params: + return self.fn(index) # Call fn with index if it accepts an index parameter. + return self.fn() # Call fn without any parameters if it does not accept the index. + + +class RepeatDataset(torch.utils.data.Dataset): + """ + A dataset wrapper that allows repeating access to items from an underlying dataset. + + This dataset can be used to create an artificial extension of the underlying dataset + to a specified `length`. Each item from the original dataset can be accessed + repeatedly up to `num_item` times before it loops back. + + Attributes: + length (int): The total length of the dataset to be exposed. + dataset (Dataset): The original dataset. + num_item (int): Number of times each item is repeated. + cache_item (dict): Cache to store accessed items to avoid recomputation. + """ + + def __init__(self, dataset: Dataset, length: int = MAX_LENGTH, num_item: int = 1) -> None: + """ + Initializes the RepeatDataset with a dataset, length, and number of repeats per item. + + Args: + dataset (Dataset): The dataset to repeat. + length (int): The total length of the dataset to be exposed. Defaults to MAX_LENGTH. + num_item (int): The number of times to repeat each item. Defaults to 1. + """ + self.length = length + self.dataset = dataset + self.num_item = num_item + self.cache_item = {} + + def __len__(self) -> int: + return self.length + + def __getitem__(self, index: int) -> Any: + index = index % self.num_item + if index not in self.cache_item: + self.cache_item[index] = self.dataset[index] + return self.cache_item[index] + + +class CombinedDictDataset(torch.utils.data.Dataset): + """ + A dataset that wraps multiple PyTorch datasets and returns a dictionary of data items from each dataset for a given index. + This dataset ensures that all constituent datasets have the same length by setting the length to the minimum length of the datasets provided. + + Parameters: + ----------- + **datasets : Dict[str, Dataset] + A dictionary where keys are string identifiers for the datasets and values are the datasets instances themselves. + + Attributes: + ----------- + datasets : Dict[str, Dataset] + Stores the input datasets. + max_length : int + The minimum length among all provided datasets, determining the length of this combined dataset. + + Examples: + --------- + >>> dataset1 = torch.utils.data.TensorDataset(torch.randn(100, 3, 32, 32)) + >>> dataset2 = torch.utils.data.TensorDataset(torch.randn(100, 3, 32, 32)) + >>> combined_dataset = CombinedDictDataset(dataset1=dataset1, dataset2=dataset2) + >>> print(len(combined_dataset)) + 100 + >>> data = combined_dataset[50] + >>> print(data.keys()) + dict_keys(['dataset1', 'dataset2']) + """ + + def __init__(self, **datasets: Dict[str, Dataset]) -> None: + """ + Initializes the CombinedDictDataset with multiple datasets. + + Args: + **datasets (Dict[str, Dataset]): Key-value pairs where keys are dataset names and values + are dataset instances. Each key-value pair adds a dataset + under the specified key. + """ + self.datasets = datasets + self.max_length = min([len(dataset) for dataset in datasets.values()]) + + def __len__(self) -> int: + return self.max_length + + def __getitem__(self, index: int) -> Dict[str, Any]: + """ + Retrieves an item from each dataset at the specified index, combines them into a dictionary, + and returns the dictionary. Each key in the dictionary corresponds to one of the dataset names provided + during initialization, and its value is the item from that dataset at the given index. + + Args: + index (int): The index of the items to retrieve across all datasets. + + Returns: + Dict[str, Any]: A dictionary containing data items from all datasets for the given index. + Each key corresponds to a dataset name, and its value is the data item from that dataset. + """ + data = {} + for key, dataset in self.datasets.items(): + data[key] = dataset[index] + return data diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py b/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py new file mode 100644 index 00000000..cb4a7bcc --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Iterable +from typing import Any, Generator, Optional + + +class Augmentor: + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + r"""Base augmentor class + + Args: + input_keys (list): List of input keys + output_keys (list): List of output keys + args (dict): Arguments associated with the augmentation + """ + self.input_keys = input_keys + self.output_keys = output_keys + self.args = args + + def __call__(self, *args: Any, **kwds: Any) -> Any: + raise ValueError("Augmentor not implemented") + + +class IterableAugmentor: + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + r"""Base augmentor class + + Args: + input_keys (list): List of input keys + output_keys (list): List of output keys + args (dict): Arguments associated with the augmentation + """ + self.input_keys = input_keys + self.output_keys = output_keys + self.args = args + self.is_generator = True + + def __call__(self, data: Iterable) -> Generator: + r"""Example usage: + + for data_dict in data: + # Do something to data_dict + data_dict["input"] = data_dict["raw_sequence"][:, :-1] + data_dict["target"] = data_dict["raw_sequence"][:, 1:] + # Skip sample if needed + if data_dict["input"].shape[1] < 64: + continue + # Construct a generator + yield data_dict + """ + raise ValueError("Augmentor not implemented") diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py new file mode 100644 index 00000000..5e45f039 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Camera parameter augmentors for webdataset.""" + +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.modules.camera import Camera + + +class CameraParamDecoder(Augmentor): + """Decodes camera parameters from text files. + + The text file format is: fx fy cx cy qx qy qz qw tx ty tz + where: + - fx, fy: focal lengths + - cx, cy: principal points + - qx, qy, qz, qw: quaternion rotation (world to camera) + - tx, ty, tz: translation vector (world to camera) + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + """Initialize the camera parameter decoder. + + Args: + input_keys: List of input keys (typically ['camera']) + output_keys: List of output keys (typically ['intrinsics', 'world_to_cam']) + args: Additional arguments (not used) + """ + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + """Decode camera parameters from text data. + + Args: + data_dict: Input data dictionary containing camera text data + + Returns: + data_dict: Output data dictionary with decoded camera parameters + """ + # Get the camera text data + camera_text = data_dict[self.input_keys[0]] + + # Convert text to string if it's bytes + if isinstance(camera_text, bytes): + camera_text = camera_text.decode("utf-8") + + # Parse the camera parameters + parts = list(map(float, camera_text.strip().split())) + if len(parts) != 11: + raise ValueError(f"Invalid camera parameter format. Expected 11 values, got {len(parts)}") + + # Extract parameters + fx, fy, cx, cy = parts[0:4] # focal lengths and principal points + quat = parts[4:8] # qx, qy, qz, qw + trans = parts[8:11] # tx, ty, tz + + # Convert intrinsics to 3x3 matrix via helper + intrinsics = Camera.intrinsic_params_to_matrices(torch.tensor([fx, fy, cx, cy], dtype=torch.float32)) + + # Convert quaternion + translation to 4x4 World->Cam matrix via helper + qxyzw_t = torch.tensor([*quat, *trans], dtype=torch.float32) + w2c_3x4 = Camera.extrinsic_params_to_matrices(qxyzw_t) + world_to_cam = torch.eye(4, dtype=torch.float32) + world_to_cam[:3, :] = w2c_3x4 + + # Convert to torch tensors + intrinsics = intrinsics.float() + world_to_cam = world_to_cam.float() + + # Store in output dictionary + data_dict[self.output_keys[0]] = intrinsics + data_dict[self.output_keys[1]] = world_to_cam + + # Remove the original camera text data + data_dict.pop(self.input_keys[0]) + + return data_dict + + +class CameraParamListDecoder(Augmentor): + """Decodes a list of camera parameters from text files. + + The text file format is multiple lines, where each line contains: + fx fy cx cy qx qy qz qw tx ty tz + where: + - fx, fy: focal lengths + - cx, cy: principal points + - qx, qy, qz, qw: quaternion rotation (world to camera) + - tx, ty, tz: translation vector (world to camera) + + Each line corresponds to one frame's camera parameters. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + """Initialize the camera parameter list decoder. + + Args: + input_keys: List of input keys (typically ['camera']) + output_keys: List of output keys (typically ['intrinsics', 'world_to_cam']) + args: Additional arguments (not used) + """ + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + """Decode a list of camera parameters from text data. + + Args: + data_dict: Input data dictionary containing camera text data + + Returns: + data_dict: Output data dictionary with decoded camera parameters as lists + """ + # Get the camera text data + camera_text = data_dict[self.input_keys[0]] + + # Convert text to string if it's bytes + if isinstance(camera_text, bytes): + camera_text = camera_text.decode("utf-8") + + # Split into lines and parse each line + lines = camera_text.strip().split("\n") + num_frames = len(lines) + + if num_frames == 0: + raise ValueError("Empty camera parameter file") + + # Initialize lists to store camera parameters + intrinsics_list = [] + world_to_cam_list = [] + + # Parse each line + for i, line in enumerate(lines): + line = line.strip() + if not line: # Skip empty lines + continue + + parts = list(map(float, line.split())) + if len(parts) != 11: + raise ValueError( + f"Invalid camera parameter format at line {i + 1}. Expected 11 values, got {len(parts)}" + ) + + # Extract parameters + fx, fy, cx, cy = parts[0:4] # focal lengths and principal points + quat = parts[4:8] # qx, qy, qz, qw + trans = parts[8:11] # tx, ty, tz + + # Convert intrinsics and extrinsics via helpers + intrinsics = Camera.intrinsic_params_to_matrices(torch.tensor([fx, fy, cx, cy], dtype=torch.float32)) + qxyzw_t = torch.tensor([*quat, *trans], dtype=torch.float32) + w2c_3x4 = Camera.extrinsic_params_to_matrices(qxyzw_t) + world_to_cam = torch.eye(4, dtype=torch.float32) + world_to_cam[:3, :] = w2c_3x4 + + intrinsics_list.append(intrinsics) + world_to_cam_list.append(world_to_cam) + + # Convert lists to torch tensors with batch dimension + intrinsics_tensor = torch.stack(intrinsics_list).float() # T x 3 x 3 + world_to_cam_tensor = torch.stack(world_to_cam_list).float() # T x 4 x 4 + + # Store in output dictionary + data_dict[self.output_keys[0]] = intrinsics_tensor + data_dict[self.output_keys[1]] = world_to_cam_tensor + + # Remove the original camera text data + data_dict.pop(self.input_keys[0]) + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py new file mode 100644 index 00000000..adc47764 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Depth augmentors for webdataset.""" + +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +class DepthMask(Augmentor): + """Generates a binary mask for valid depth values. + + This augmentor takes a depth image and generates a binary mask indicating + which pixels have valid depth values. A pixel is considered valid if: + 1. Its depth value is greater than min_depth + 2. Its depth value is less than max_depth + 3. Its depth value is not NaN or infinite + 4. Its depth value is not larger than median_multiplier times the median depth + + Args: + min_depth (float): Minimum valid depth value + max_depth (float): Maximum valid depth value + median_multiplier (float): Maximum allowed depth as a multiple of median depth + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + """Initialize the depth mask generator. + + Args: + input_keys: List of input keys (typically ['depth']) + output_keys: List of output keys (typically ['depth_mask']) + args: Additional arguments including: + - min_depth (float): Minimum valid depth value + - max_depth (float): Maximum valid depth value + - median_multiplier (float): Maximum allowed depth as a multiple of median depth + """ + super().__init__(input_keys, output_keys, args) + self.min_depth = args.get("min_depth", 0.1) if args else 0.1 + self.max_depth = args.get("max_depth", 100.0) if args else 100.0 + self.median_multiplier = args.get("median_multiplier", 10) if args else 10 + + def __call__(self, data_dict: dict) -> dict: + """Generate depth mask. + + Args: + data_dict: Input data dictionary containing depth image + + Returns: + data_dict: Output data dictionary with depth mask + """ + # Get depth image + depth = data_dict[self.input_keys[0]] # H x W + + # Create mask for valid depth values + mask = torch.ones_like(depth, dtype=torch.bool) + + # Check for minimum depth + mask = mask & (depth > self.min_depth) + + # Check for maximum depth + mask = mask & (depth < self.max_depth) + + # Check for NaN and infinite values + mask = mask & torch.isfinite(depth) & (~torch.isnan(depth)) + + # Compute median depth from currently valid depths + if mask.any(): + valid_depths = depth[mask] + median_depth = torch.median(valid_depths) + + # Filter out depths larger than median_multiplier times the median + max_allowed_depth = self.median_multiplier * median_depth + mask = mask & (depth <= max_allowed_depth) + + # Store in output dictionary + data_dict[self.output_keys[0]] = mask + data_dict[self.input_keys[0]][~mask] = self.max_depth + return data_dict + + +class ConsecutiveFrameSampler(Augmentor): + """Randomly samples N consecutive frames from a video sequence. + + This augmentor takes a video sequence and randomly samples N consecutive frames + starting from a random position within the valid range. + + Args: + num_frames (int): Number of consecutive frames to sample + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = None, + random_sample: bool = True, + args: Optional[dict] = None, + ) -> None: + """Initialize the consecutive frame sampler. + + Args: + input_keys: List of input keys (typically ['depth', 'points', etc.]) + output_keys: List of output keys (same as input_keys) + args: Additional arguments including: + - num_frames (int): Number of consecutive frames to sample + """ + super().__init__(input_keys, output_keys, args) + self.num_frames = args.get("num_frames", 25) if args else 25 + self.random_sample = random_sample + + def __call__(self, data_dict: dict) -> dict: + """Sample consecutive frames from video sequences. + + Args: + data_dict: Input data dictionary containing video sequences + + Returns: + data_dict: Output data dictionary with sampled frames + """ + + # Get the first input key to determine the temporal dimension + first_key = self.input_keys[0] + video_tensor = data_dict[first_key] + + if video_tensor.dim() == 4: # CxTxHxW + total_frames = video_tensor.shape[1] + elif video_tensor.dim() == 3: # TxHxW + total_frames = video_tensor.shape[0] + else: + raise ValueError(f"Expected 3D (TxHxW) or 4D (CxTxHxW) tensor, got {video_tensor.dim()}D") + + # Calculate valid start indices + max_start_idx = max(0, total_frames - self.num_frames) + if self.num_frames > total_frames: + return None + + if max_start_idx == 0: + # If video is shorter than requested frames, use all available frames + start_idx = 0 + actual_num_frames = total_frames + else: + if self.random_sample: + # Randomly sample start index + start_idx = torch.randint(0, max_start_idx + 1, size=(1,)).item() + else: + start_idx = 0 + actual_num_frames = self.num_frames + + # Sample frames for all input keys + for input_key, output_key in zip(self.input_keys, self.output_keys): + tensor = data_dict[input_key] + + if tensor.dim() == 4: # CxTxHxW + sampled_tensor = tensor[:, start_idx : start_idx + actual_num_frames, :, :] + assert sampled_tensor.shape[1] == actual_num_frames, ( + f"Sampled tensor {input_key} has {sampled_tensor.shape[1]} frames, expected {actual_num_frames}" + ) + elif tensor.dim() == 3: # TxHxW + sampled_tensor = tensor[start_idx : start_idx + actual_num_frames, :, :] + assert sampled_tensor.shape[0] == actual_num_frames, ( + f"Sampled tensor {input_key} has {sampled_tensor.shape[0]} frames, expected {actual_num_frames}" + ) + else: + raise ValueError(f"Expected 3D (TxHxW) or 4D (CxTxHxW) tensor for {input_key}, got {tensor.dim()}D") + + data_dict[output_key] = sampled_tensor + data_dict["frame_start"] = start_idx + data_dict["frame_end"] = start_idx + actual_num_frames + + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py new file mode 100644 index 00000000..9d57829f --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py @@ -0,0 +1,390 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Point cloud augmentors for webdataset.""" + +from typing import Optional + +import torch +from einops import rearrange + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.modules.camera import Camera + + +class DepthToPointcloud(Augmentor): + """Converts depth images to point clouds using camera intrinsics. + + This augmentor takes a depth image and camera intrinsics to generate a point cloud. + The depth image should be in meters and the intrinsics should be a 3x3 matrix. + + Args: + to_world_coords (bool): If True, uses the first frame as the coordinate frame for video sequences + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + """Initialize the depth to point cloud converter. + + Args: + input_keys: List of input keys (typically ['depth', 'intrinsics', 'world_to_cam']) + output_keys: List of output keys (typically ['points']) + args: Additional arguments including: + - to_world_coords (bool): Whether to use first frame as coordinate frame + """ + assert "depth" in input_keys, "Depth image is required for point cloud conversion" + assert "intrinsics" in input_keys, "Intrinsics are required for point cloud conversion" + assert "world_to_cam" in input_keys or not self.to_world_coords, ( + "World to camera matrix is required for point cloud conversion" + ) + super().__init__(input_keys, output_keys, args) + self.to_world_coords = args.get("to_world_coords", False) if args else False + + def __call__(self, data_dict: dict) -> dict: + """Convert depth image to point cloud. + + Args: + data_dict: Input data dictionary containing depth image and camera intrinsics + + Returns: + data_dict: Output data dictionary with point cloud + """ + # Get depth image and intrinsics + depth = data_dict[self.input_keys[0]] # [T,H,W] or [H,W] + intrinsics = data_dict[self.input_keys[1]] # [T,3,3] or [3,3] + + # Check if we're dealing with video sequences (temporal dimension) + if depth.dim() == 3 and intrinsics.dim() == 3: + # Video sequence: T x H x W and T x 3 x 3 + T, H, W = depth.shape + + # Create pixel coordinates (same for all frames) + y, x = torch.meshgrid( + torch.arange(H, device=depth.device), torch.arange(W, device=depth.device), indexing="ij" + ) + pixels = torch.stack([x, y, torch.ones_like(x)], dim=-1).float() # [H,W,3] + pixels_hw3 = pixels.reshape(-1, 3) # [H*W,3] + + # Back-project to camera space using Camera.image2camera + pixels_batched = pixels_hw3.unsqueeze(0).expand(T, -1, -1) # [T,H*W,3] + points_cam = Camera.image2camera(pixels_batched, intrinsics) # [T,H*W,3] + depth_flat = depth.reshape(T, -1) # [T,H*W] + points_cam = points_cam * depth_flat.unsqueeze(-1) # [T,H*W,3] + + # Transform to first frame coordinate system if requested + if self.to_world_coords: + world_to_cam = data_dict[self.input_keys[2]] # [T,4,4] + w2c = world_to_cam[:, :3, :] # [T,3,4] + # relative pose from cam_t to cam_0: rel = w2c_0 ∘ c2w_t + w2c0 = w2c[0] # [3,4] + c2w = Camera.invert_pose(w2c) # [T,3,4] + w2c0_exp = w2c0.unsqueeze(0).expand_as(c2w) # [T,3,4] + rel = Camera.compose_poses([w2c0_exp, c2w]) # [T,3,4] + points = Camera.world2camera(points_cam, rel) # [T,H*W,3] + else: + points = points_cam # [T,H*W,3] + + # Reshape to T x 3 x H x W + points = rearrange(points, "t (h w) c -> c t h w", h=H, w=W, c=3) # [3,T,H,W] + + else: + # Single frame: H x W and 3 x 3 + H, W = depth.shape[-2:] + + # Create pixel coordinates + y, x = torch.meshgrid( + torch.arange(H, device=depth.device), torch.arange(W, device=depth.device), indexing="ij" + ) + + # Create homogeneous coordinates and convert to float + pixels = torch.stack([x, y, torch.ones_like(x)], dim=-1).float() # [H,W,3] + pixels_hw3 = pixels.reshape(-1, 3) # [H*W,3] + depth_flat = depth.reshape(-1) # [H*W] + + # Back-project to camera space + points_cam = Camera.image2camera(pixels_hw3, intrinsics) # [H*W,3] + points_cam = points_cam * depth_flat.unsqueeze(-1) # [H*W,3] + + # For single frame, just use camera coordinates or transform to world coords as before + if self.to_world_coords: + world_to_cam = data_dict[self.input_keys[2]] # [4,4] + w2c = world_to_cam[:3, :] # [3,4] + points = Camera.camera2world(points_cam, w2c) # [H*W,3] + else: + points = points_cam # [H*W,3] + + # Reshape to 3 x H x W + points = rearrange(points, "(h w) c -> c h w", h=H, w=W, c=3) # [3,H,W] + + # Store in output dictionary + data_dict[self.output_keys[0]] = points + + return data_dict + + +class PointcloudRescale(Augmentor): + """Rescales point clouds to have a mean distance of 1 from the origin. + + This augmentor takes a point cloud and rescales it so that the mean distance + of all points from the origin is 1. It also adjusts the world-to-camera + transformation matrix accordingly. + + Args: + input_keys: List of input keys (typically ['points', 'world_to_cam']) + output_keys: List of output keys (typically ['points', 'world_to_cam']) + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = None, + mask_key: Optional[str] = None, + args: Optional[dict] = None, + ) -> None: + """Initialize the point cloud rescaler. + + Args: + input_keys: List of input keys (typically ['points', 'world_to_cam']) + output_keys: List of output keys (typically ['points', 'world_to_cam']) + args: Additional arguments (not used in this augmentor) + """ + assert "points" in input_keys, "Points are required for rescaling" + assert "world_to_cam" in input_keys, "World to camera matrix is required for rescaling" + super().__init__(input_keys, output_keys, args) + self.mask_key = mask_key + + def __call__(self, data_dict: dict) -> dict: + """Rescale point cloud and adjust world-to-camera transformation. + + This augmentor computes the average Euclidean distance of all 3D points to the origin + and uses this scale to normalize both the camera translations and point cloud. + + Args: + data_dict: Input data dictionary containing points and world_to_cam + + Returns: + data_dict: Output data dictionary with rescaled points and adjusted world_to_cam + """ + # Get points and world_to_cam + points = data_dict[self.input_keys[0]] # [3,T,H,W] or [3,H,W] + world_to_cam = data_dict[self.input_keys[1]] # [T,4,4] or [4,4] + + # Check if we're dealing with video sequences (temporal dimension) + if points.dim() == 4 and world_to_cam.dim() == 3: + # Video sequence: 3 x T x H x W and T x 4 x 4 + T = world_to_cam.shape[0] + + # Reshape points to T x N x 3 for easier computation + points_flat = points.permute(1, 0, 2, 3).reshape(T, 3, -1).transpose(1, 2) # [T,N,3] + + # Compute average Euclidean distance to origin across all frames + if self.mask_key is not None: + # Get mask and reshape to match points + mask = data_dict[self.mask_key] # [T,H,W] + mask_flat = mask.reshape(T, -1) # [T,N] + + # Only compute average over valid points across all frames + # Compute squared distances for all frames at once + squared_distances = torch.sum(points_flat**2, dim=2) # [T,N] + + # Apply mask and compute mean across all frames + valid_distances = torch.sqrt(squared_distances[mask_flat]) # [N_valid] + avg_dist = valid_distances.mean() # scalar + else: + # Compute average Euclidean distance to origin for all points across all frames + avg_dist = torch.sqrt(torch.sum(points_flat**2, dim=2)).mean() # scalar + + # Compute scale factor to achieve average distance of 1 across all frames + scale = 1.0 / avg_dist # scalar + + # Rescale points for all frames at once + points_scaled = points * scale # [3,T,H,W] + + # Adjust world_to_cam matrix for all frames at once + # We need to scale the translation component by the same factor + world_to_cam_scaled = world_to_cam.clone() + world_to_cam_scaled[:, :3, 3] *= scale # [T,4,4] + + # Scale depth for all frames at once + depth = data_dict[self.input_keys[2]] # [T,H,W] + depth_scaled = depth * scale # [T,H,W] + else: + # Single frame: 3 x H x W and 4 x 4 + # Reshape points to N x 3 for easier computation + points_flat = points.reshape(3, -1).T # [N,3] + + # Compute average Euclidean distance to origin + if self.mask_key is not None: + # Get mask and reshape to match points + mask = data_dict[self.mask_key] # [H,W] + mask_flat = mask.reshape(-1) # [N] + + # Only compute average over valid points + valid_points = points_flat[mask_flat] # [N_valid,3] + # Compute average Euclidean distance to origin + avg_dist = torch.sqrt(torch.sum(valid_points**2, dim=1)).mean() # scalar + else: + # Compute average Euclidean distance to origin for all points + avg_dist = torch.sqrt(torch.sum(points_flat**2, dim=1)).mean() # scalar + + # Compute scale factor to achieve average distance of 1 + scale = 1.0 / avg_dist # scalar + + # Rescale points + points_scaled = points * scale # [3,H,W] + + # Adjust world_to_cam matrix + # We need to scale the translation component by the same factor + world_to_cam_scaled = world_to_cam.clone() + world_to_cam_scaled[:3, 3] *= scale # [4,4] + + # Scale depth + depth = data_dict[self.input_keys[2]] # [H,W] + depth_scaled = depth * scale # [H,W] + + # Store in output dictionary + data_dict[self.output_keys[0]] = points_scaled + data_dict[self.output_keys[1]] = world_to_cam_scaled + data_dict[self.output_keys[2]] = depth_scaled + return data_dict + + +class PointcloudMaskFill(Augmentor): + """Fills point cloud values with 0 when point cloud mask is False. + + This augmentor takes a point cloud and a point cloud mask, and sets point cloud values to 0 + wherever the mask is False. This is useful for cleaning up point clouds by + removing invalid or unreliable point measurements. + + Args: + input_keys: List of input keys (typically ['points', 'pcd_mask']) + output_keys: List of output keys (typically ['points']) + """ + + def __init__( + self, input_keys: list, output_keys: Optional[list] = None, fill_value: float = 0.0, args: Optional[dict] = None + ) -> None: + """Initialize the point cloud mask filler. + + Args: + input_keys: List of input keys (typically ['points', 'pcd_mask']) + output_keys: List of output keys (typically ['points']) + args: Additional arguments (not used in this augmentor) + """ + super().__init__(input_keys, output_keys, args) + self.fill_value = fill_value + + def __call__(self, data_dict: dict) -> dict: + """Fill point cloud values with 0 where point cloud mask is False. + + Args: + data_dict: Input data dictionary containing point cloud and point cloud mask + + Returns: + data_dict: Output data dictionary with masked point cloud + """ + # Get point cloud and point cloud mask + points = data_dict[self.input_keys[0]] # [3,T,H,W] or [3,H,W] + depth_mask = data_dict[self.input_keys[1]] # [T,H,W] or [H,W] + + # Check if we're dealing with video sequences (temporal dimension) + if points.dim() == 4 and depth_mask.dim() == 3: + # Video sequence: 3 x T x H x W and T x H x W + # Create a copy of the point cloud + points_filled = points.clone() # [3,T,H,W] + + # Expand mask to match points dimensions: 3 x T x H x W + mask_expanded = depth_mask.unsqueeze(0).expand(3, -1, -1, -1) # [3,T,H,W] + + # Set point cloud values to fill_value where mask is False for all channels at once + points_filled[~mask_expanded] = self.fill_value + + else: + # Single frame: 3 x H x W and H x W + # Create a copy of the point cloud + points_filled = points.clone() # [3,H,W] + + # Expand mask to match points dimensions: 3 x H x W + mask_expanded = depth_mask.unsqueeze(0).expand(3, -1, -1) # [3,H,W] + + # Set point cloud values to fill_value where mask is False for all channels at once + points_filled[~mask_expanded] = self.fill_value + + # Store in output dictionary + data_dict[self.output_keys[0]] = points_filled + + return data_dict + + +def verify_backprojection(data_dict: dict, scale: float) -> bool: + """Verify that backprojection of rescaled depth and camera poses matches rescaled point cloud. + + This function checks if the backprojection of the rescaled depth image using + the rescaled camera poses produces the same point cloud as the rescaled point cloud. + + Args: + data_dict: Dictionary containing: + - points_scaled: Rescaled point cloud (3 x H x W) + - depth_scaled: Rescaled depth image (H x W) + - world_to_cam_scaled: Rescaled world to camera matrix (4 x 4) + - intrinsics: Camera intrinsics matrix (3 x 3) + scale: The scale factor used for rescaling + + Returns: + bool: True if backprojection matches rescaled point cloud within tolerance + """ + # Get required data + points_scaled = data_dict["points"] # [3,H,W] + depth_scaled = data_dict["depth"] # [H,W] + world_to_cam_scaled = data_dict["world_to_cam"] # [4,4] + intrinsics = data_dict["intrinsics"] # [3,3] + + # Get image dimensions + H, W = depth_scaled.shape[-2:] + + # Create pixel coordinates + y, x = torch.meshgrid( + torch.arange(H, device=depth_scaled.device), torch.arange(W, device=depth_scaled.device), indexing="ij" + ) + + # Create homogeneous coordinates + pixels = torch.stack([x, y, torch.ones_like(x)], dim=-1).float() # [H,W,3] + + # Reshape for batch processing + pixels = pixels.reshape(-1, 3) # [H*W,3] + depth_flat = depth_scaled.reshape(-1) # [H*W] + + # Get inverse of intrinsics + intrinsics_inv = torch.inverse(intrinsics) # [3,3] + + # Back-project to camera space + points_cam = (intrinsics_inv @ pixels.T).T # [H*W,3] + points_cam = points_cam * depth_flat.unsqueeze(-1) # [H*W,3] + + # Convert to world coordinates + cam_to_world = torch.inverse(world_to_cam_scaled) # [4,4] + points_cam_h = torch.cat([points_cam, torch.ones_like(points_cam[:, :1])], dim=-1) # [H*W,4] + points_world_h = (cam_to_world @ points_cam_h.T).T # [H*W,4] + points_world = points_world_h[:, :3] # [H*W,3] + + # Reshape back to image dimensions + points_world = points_world.reshape(H, W, 3) # [H,W,3] + points_world = points_world.permute(2, 0, 1) # [3,H,W] + + # Compare with rescaled point cloud + # Use a small tolerance for floating point comparison + tolerance = 1e-6 + is_close = torch.allclose(points_world, points_scaled, rtol=tolerance, atol=tolerance) + + return is_close diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py new file mode 100644 index 00000000..4881f9db --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torchvision.transforms.functional as transforms_F +from loguru import logger as logging + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size, obtain_image_size + + +class CenterCrop(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs center crop. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are center cropped. + We also save the cropping parameters in the aug_params dict + so that it will be used by other transforms. + """ + assert (self.args is not None) and ("size" in self.args), "Please specify size in args" + + img_size = obtain_augmentation_size(data_dict, self.args) + width, height = img_size + + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + for key in self.input_keys: + data_dict[key] = transforms_F.center_crop(data_dict[key], [height, width]) + + # We also add the aug params we use. This will be useful for other transforms + crop_x0 = (orig_w - width) // 2 + crop_y0 = (orig_h - height) // 2 + cropping_params = { + "resize_w": orig_w, + "resize_h": orig_h, + "crop_x0": crop_x0, + "crop_y0": crop_y0, + "crop_w": width, + "crop_h": height, + } + + if "aug_params" not in data_dict: + data_dict["aug_params"] = dict() + + data_dict["aug_params"]["cropping"] = cropping_params + return data_dict + + +class RandomCrop(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs random crop. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are center cropped. + We also save the cropping parameters in the aug_params dict + so that it will be used by other transforms. + """ + + img_size = obtain_augmentation_size(data_dict, self.args) + width, height = img_size + + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + # Obtaining random crop coords + try: + crop_x0 = int(torch.randint(0, orig_w - width + 1, size=(1,)).item()) + crop_y0 = int(torch.randint(0, orig_h - height + 1, size=(1,)).item()) + except Exception as e: + logging.warning( + f"Random crop failed. Performing center crop, original_size(wxh): {orig_w}x{orig_h}, random_size(wxh): {width}x{height}" + ) + for key in self.input_keys: + data_dict[key] = transforms_F.center_crop(data_dict[key], [height, width]) + crop_x0 = (orig_w - width) // 2 + crop_y0 = (orig_h - height) // 2 + + # We also add the aug params we use. This will be useful for other transforms + cropping_params = { + "resize_w": orig_w, + "resize_h": orig_h, + "crop_x0": crop_x0, + "crop_y0": crop_y0, + "crop_w": width, + "crop_h": height, + } + + if "aug_params" not in data_dict: + data_dict["aug_params"] = dict() + + data_dict["aug_params"]["cropping"] = cropping_params + + # We must perform same random cropping for all input keys + for key in self.input_keys: + data_dict[key] = transforms_F.crop(data_dict[key], crop_y0, crop_x0, height, width) + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py new file mode 100644 index 00000000..ba6f22af --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +class HorizontalFlip(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs horizontal flipping. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are center cropped. + """ + flip_enabled = getattr(self.args, "enabled", True) + if flip_enabled: + p = getattr(self.args, "prob", 0.5) + coin_flip = torch.rand(1).item() > p + for key in self.input_keys: + if coin_flip: + data_dict[key] = transforms_F.hflip(data_dict[key]) + + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py new file mode 100644 index 00000000..90bddaf7 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +import torch +from PIL import Image + + +def obtain_image_size(data_dict: dict, input_keys: list) -> tuple[int, int]: + r"""Function for obtaining the image size from the data dict. + + Args: + data_dict (dict): Input data dict + input_keys (list): List of input keys + Returns: + width (int): Width of the input image + height (int): Height of the input image + """ + + data1 = data_dict[input_keys[0]] + if isinstance(data1, Image.Image): + width, height = data1.size + elif isinstance(data1, torch.Tensor): + height, width = data1.size()[-2:] + else: + raise ValueError("data to random crop should be PIL Image or tensor") + + return width, height + + +def obtain_augmentation_size(data_dict: dict, augmentor_cfg: dict) -> Union[int, tuple]: + r"""Function for obtaining size of the augmentation. + When dealing with multi-aspect ratio dataloaders, we need to + find the augmentation size from the aspect ratio of the data. + If data_dict contains "_res_size_map" (e.g. from resolution sampling), + that map is used instead of augmentor_cfg["size"]. + + Args: + data_dict (dict): Input data dict + augmentor_cfg (dict): Augmentor config + Returns: + aug_size (int): Size of augmentation + """ + if "__url__" in data_dict and "aspect_ratio" in data_dict["__url__"].meta.opts: + aspect_ratio = data_dict["__url__"].meta.opts["aspect_ratio"] + else: # Non-webdataset format + aspect_ratio = data_dict["aspect_ratio"] + if "_res_size_map" in data_dict: + return data_dict["_res_size_map"][aspect_ratio] + return augmentor_cfg["size"][aspect_ratio] diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py new file mode 100644 index 00000000..629b8519 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +class Normalize(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs data normalization. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are center cropped. + """ + assert self.args is not None, "Please specify args" + + mean = self.args["mean"] + std = self.args["std"] + + for key in self.input_keys: + if isinstance(data_dict[key], torch.Tensor): + data_dict[key] = data_dict[key].to(dtype=torch.get_default_dtype()).div(255) + else: + data_dict[key] = transforms_F.to_tensor(data_dict[key]) # division by 255 is applied in to_tensor() + + data_dict[key] = transforms_F.normalize(tensor=data_dict[key], mean=mean, std=std) + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py new file mode 100644 index 00000000..009b4983 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import omegaconf +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size, obtain_image_size + + +class ReflectionPadding(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs reflection padding. This function also returns a padding mask. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are center cropped. + """ + + assert self.args is not None, "Please specify args in augmentation" + if self.output_keys is None: + self.output_keys = self.input_keys + + # Obtain image and augmentation sizes + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + target_size = obtain_augmentation_size(data_dict, self.args) + + assert isinstance(target_size, (tuple, omegaconf.listconfig.ListConfig)), "Please specify target size as tuple" + target_w, target_h = target_size + + target_w = int(target_w) + target_h = int(target_h) + + # One-sided padding (bottom and right only, content stays at top-left) + padding_right = target_w - orig_w + padding_bottom = target_h - orig_h + padding_vals = [0, 0, padding_right, padding_bottom] + + for inp_key, out_key in zip(self.input_keys, self.output_keys): + if max(padding_vals[0], padding_vals[2]) >= orig_w or max(padding_vals[1], padding_vals[3]) >= orig_h: + # In this case, we can't perform reflection padding. This is because padding values + # are larger than the image size. So, perform edge padding instead. + data_dict[out_key] = transforms_F.pad(data_dict[inp_key], padding_vals, padding_mode="edge") + else: + # Perform reflection padding + data_dict[out_key] = transforms_F.pad(data_dict[inp_key], padding_vals, padding_mode="reflect") + + if out_key != inp_key: + del data_dict[inp_key] + + data_dict["image_size"] = torch.tensor([target_h, target_w, orig_h, orig_w], dtype=torch.float) + + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py new file mode 100644 index 00000000..79f022cc --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import omegaconf +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size, obtain_image_size + + +class ResizeSmallestSide(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs resizing to smaller side + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are resized + """ + + if self.output_keys is None: + self.output_keys = self.input_keys + assert self.args is not None, "Please specify args in augmentations" + + for inp_key, out_key in zip(self.input_keys, self.output_keys): + out_size = obtain_augmentation_size(data_dict, self.args) + assert isinstance(out_size, int), "Arg size in resize should be an integer" + data_dict[out_key] = transforms_F.resize( + data_dict[inp_key], + size=out_size, # type: ignore + interpolation=getattr(self.args, "interpolation", transforms_F.InterpolationMode.BICUBIC), + antialias=True, + ) + if out_key != inp_key: + del data_dict[inp_key] + return data_dict + + +class ResizeLargestSide(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs resizing to larger side + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are resized + """ + + if self.output_keys is None: + self.output_keys = self.input_keys + assert self.args is not None, "Please specify args in augmentations" + + for inp_key, out_key in zip(self.input_keys, self.output_keys): + out_size = obtain_augmentation_size(data_dict, self.args) + assert isinstance(out_size, int), "Arg size in resize should be an integer" + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + + scaling_ratio = min(out_size / orig_w, out_size / orig_h) + target_size = [int(scaling_ratio * orig_h), int(scaling_ratio * orig_w)] + + data_dict[out_key] = transforms_F.resize( + data_dict[inp_key], + size=target_size, + interpolation=getattr(self.args, "interpolation", transforms_F.InterpolationMode.BICUBIC), + antialias=True, + ) + if out_key != inp_key: + del data_dict[inp_key] + return data_dict + + +class ResizeSmallestSideAspectPreserving(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs aspect-ratio preserving resizing. + Image is resized to the dimension which has the smaller ratio of (size / target_size). + First we compute (w_img / w_target) and (h_img / h_target) and resize the image + to the dimension that has the smaller of these ratios. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are resized + """ + + if self.output_keys is None: + self.output_keys = self.input_keys + assert self.args is not None, "Please specify args in augmentations" + + img_size = obtain_augmentation_size(data_dict, self.args) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + scaling_ratio = max((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + + assert target_size[0] >= img_h and target_size[1] >= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + + for inp_key, out_key in zip(self.input_keys, self.output_keys): + data_dict[out_key] = transforms_F.resize( + data_dict[inp_key], + size=target_size, # type: ignore + interpolation=( + self.args["interpolation"] + if "interpolation" in self.args + else transforms_F.InterpolationMode.BICUBIC + ), + antialias=True, + ) + + if out_key != inp_key: + del data_dict[inp_key] + return data_dict + + +class ResizeLargestSideAspectPreserving(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs aspect-ratio preserving resizing. + Image is resized to the dimension which has the larger ratio of (size / target_size). + First we compute (w_img / w_target) and (h_img / h_target) and resize the image + to the dimension that has the larger of these ratios. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict where images are resized + """ + + if self.output_keys is None: + self.output_keys = self.input_keys + assert self.args is not None, "Please specify args in augmentations" + + img_size = obtain_augmentation_size(data_dict, self.args) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + + assert target_size[0] <= img_h and target_size[1] <= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + + for inp_key, out_key in zip(self.input_keys, self.output_keys): + data_dict[out_key] = transforms_F.resize( + data_dict[inp_key], + size=target_size, # type: ignore + interpolation=getattr(self.args, "interpolation", transforms_F.InterpolationMode.BICUBIC), + antialias=True, + ) + + if out_key != inp_key: + del data_dict[inp_key] + return data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py b/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py new file mode 100644 index 00000000..968e7ab3 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Type + +import attrs +from torch.utils.data import IterableDataset + +from cosmos3._src.imaginaire import config +from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +@make_freezable +@attrs.define(slots=False) +class DatasetInfo: + object_store_config: config.ObjectStoreConfig # Object strore config + wdinfo: list[str] # List of wdinfo files + opts: dict = attrs.Factory(dict) # Additional dataset info args + per_dataset_keys: list[str] = attrs.Factory(list) # List of keys per dataset + source: str = "" # data source + + +@make_freezable +@attrs.define(slots=False) +class SampleInfo: + dataset_name: str + sample_rank: int = 0 + sample_worker_id: int = 0 + sample_epoch: int = 0 + sample_index: int = 0 + + +@make_freezable +@attrs.define(slots=False) +class TarSample: + path: str # Path to the sample + root: str # Root folder + keys: list # List of keys to be loaded from the webdataset + meta: DatasetInfo # Metadata + dset_id: str # Dataset id + num_samples: int = 0 # Number of samples in this tar file (data_list_key_count from wdinfo) + sample_keys_full_list: str = None # Path to the file containing full sample keys for the tar file + sample_meta: SampleInfo = None + + +@make_freezable +@attrs.define(slots=False) +class Wdinfo: + tar_files: list[TarSample] # List of all tar samples + total_key_count: int # Total number of elements present in the dataset + chunk_size: int # Number of elements present in each tar + + +@make_freezable +@attrs.define(slots=False) +class AugmentorConfig: + # Type of augmentor + type: Type[Augmentor] + # Input keys used by the augmentor + input_keys: list[str] + # Output keys returned by the augmentor + output_keys: Optional[list[str]] = None + # Additional arguments used by the augmentor + args: Optional[dict] = None + + def make_instance(self) -> Augmentor: + return self.type(input_keys=self.input_keys, output_keys=self.output_keys, args=self.args) + + +@make_freezable +@attrs.define(slots=False) +class DatasetConfig: + keys: list[str] # List of keys used + buffer_size: int # Buffer size used by each worker + dataset_info: list[DatasetInfo] # List of dataset info files, one for each dataset + distributor: IterableDataset # Iterator for returning list of tar files + decoders: list # List of decoder functions for decoding bytestream + augmentation: dict[str, AugmentorConfig] # Dictionary containing all augmentations + streaming_download: bool = True # Whether to use streaming loader + remove_extension_from_keys: bool = True # True: objects will have a key of data_type; False: data_type.extension + sample_keys_full_list_path: Optional[str] = ( + None # Path to the file containing all keys present in the dataset, e.g., "index" + ) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py b/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py new file mode 100644 index 00000000..fec30b40 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import webdataset + +import cosmos3._src.imaginaire.datasets.webdataset.webdataset +from cosmos3._src.imaginaire.utils.distributed import get_world_size + + +class Sampler: + r""" + A sampler function for setting the epoch number and iteration number. + In webdataset, information is propagated using environment flags. + In our case, + WDS_EPOCH_NUM: Epoch number + WDS_START_INDEX: Start index in this epoch. + """ + + def __init__(self, mode: str): + self.mode = mode + assert self.mode in ["train", "val"] + + def set_epoch(self, epoch: int): + if self.mode == "train": + os.environ["WDS_EPOCH_NUM"] = str(epoch) + else: + pass + + def set_iteration(self, start_index: int): + # start_index should be iters * batch_size + # It is the number of samples that have been seen by one GPU + if self.mode == "train": + os.environ["WDS_START_INDEX"] = str(start_index) + else: + pass + + +class DataLoader(webdataset.WebLoader): + r""" + This class is a wrapper on webloader class with a len attribute. + len function is needed in Imaginaire dataloaders. + """ + + def __init__(self, dataset: cosmos3._src.imaginaire.datasets.webdataset.webdataset.Dataset, batch_size: int = 1, *args, **kw): # type: ignore + # Setting data length. Webdataset is an iterable dataset, so it does not have data_len attr. + # So, we compute it from dataset and set it. + dataset_obj = dataset.build_dataset() + world_size = get_world_size() + if dataset_obj.total_images < world_size * batch_size: # type: ignore + data_length = 1 + else: + data_length = dataset_obj.total_images // (world_size * batch_size) # type: ignore + self.data_len = data_length + + super().__init__(dataset_obj, batch_size, *args, **kw) + + def __len__(self) -> int: + return self.data_len diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py new file mode 100644 index 00000000..a77e7287 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Depth decoder for EXR files.""" + +import re +from io import BytesIO + +import numpy as np +import torch + +_EXR_EXTENSIONS = "exr" +MAX_DEPTH = 100000 +_NPZ_EXTENSIONS = "npz" + + +def exr_loader(key, data): + """Load depth data from EXR file. + + Args: + key (str): Key of the data + data (bytes): Raw EXR file data + + Returns: + torch.Tensor: Depth map as tensor + """ + # pyrefly: ignore # import-error + import OpenEXR + + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _EXR_EXTENSIONS: + return None + + # Convert bytes to BytesIO for OpenEXR + exr_file = OpenEXR.InputFile(BytesIO(data)) + + # Get the header information + header = exr_file.header() + dw = header["dataWindow"] + w = dw.max.x - dw.min.x + 1 + h = dw.max.y - dw.min.y + 1 + + # Read the depth data from 'R' channel + depth = np.frombuffer(exr_file.channel("R"), dtype=np.float32).reshape((h, w)) + mask = depth == np.nan + depth = depth.copy() + depth[mask] = MAX_DEPTH + + # Convert to tensor and normalize to [0, 1] + depth = torch.from_numpy(depth).float() + + depth = depth.unsqueeze(0) + return depth + + +def npz_loader(key, data): + """Load depth data from NPZ file.""" + + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _NPZ_EXTENSIONS: + return None + + # Convert bytes to BytesIO for np.load + npz_file = BytesIO(data) + + # Load the NPZ file + with np.load(npz_file) as npz_data: + # Assuming the depth data is stored in the first array + # You may need to adjust this based on your specific NPZ file structure + depth_array = npz_data[list(npz_data.keys())[0]] + # Convert to tensor and normalize to [0, 1] if needed + depth = torch.from_numpy(depth_array).float() + + return depth + + +def construct_videodepth_decoder(): + """Construct videodepth decoder with frame count filtering. + + Args: + min_frames (int): Minimum number of frames required. Samples with fewer frames will be skipped. + + Returns: + callable: Videodepth decoder function that filters by frame count + """ + + def videodepth_decoder(key, data): + """Decode depth video data from NPZ file and filter by frame count. + + Args: + key (str): Key of the data + data (bytes): Raw NPZ file data + + Returns: + torch.Tensor: Depth video tensor if it has enough frames, None otherwise (to skip) + """ + # Load the depth data using npz_loader + depth = npz_loader(key, data) + if depth is None: + return None + + # Check frame count - determine temporal dimension + if depth.dim() == 4: # CxTxHxW + total_frames = depth.shape[1] + elif depth.dim() == 3: # TxHxW + total_frames = depth.shape[0] + else: + # For 2D depth maps (single frame), skip filtering + return depth + + return depth + + return videodepth_decoder + + +def construct_depth_decoder(sequence_length: int = 0): + """Construct depth decoder. + + Args: + sequence_length (int): Number of frames to decode. Set to 0 for single frame. + + Returns: + callable: Depth decoder function + """ + + def depth_decoder(key, sample): + """Decode depth data from sample. + + Args: + key (str): Key of the data + sample (dict): Sample dictionary containing depth data + + Returns: + dict: Sample dictionary with decoded depth data + """ + depth = exr_loader(key, sample) + if depth is None: + return None + return depth + + return depth_decoder diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py new file mode 100644 index 00000000..0b63e5fa --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import re +from typing import Optional + +from PIL import Image + +Image.MAX_IMAGE_PIXELS = 933120000 +_IMG_EXTENSIONS = "jpg jpeg png ppm pgm pbm pnm webp".split() + + +def pil_loader(key: str, data: bytes) -> Optional[Image.Image]: + r""" + Function to load an image. + If the image is corrupt, it returns a black image. + Args: + key (str): Image key. + data (bytes): Image data stream. + Returns: + PIL image + """ + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _IMG_EXTENSIONS: + return None + + with io.BytesIO(data) as stream: + img = Image.open(stream) + img.load() + img = img.convert("RGB") + + return img diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py new file mode 100644 index 00000000..622fedaa --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import re +from typing import Optional + + +def pkl_decoder(key: str, data: bytes) -> Optional[dict]: + r""" + Function to decode a pkl file. + Args: + key: Data key. + data: Data dict. + """ + extension = re.sub(r".*[.]", "", key) + if extension == "pkl" or extension == "pickle": + data_dict = pickle.loads(data) + return data_dict + else: + return None diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py new file mode 100644 index 00000000..f426cf6c --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.datasets.webdataset.distributors.basic import ShardlistBasic +from cosmos3._src.imaginaire.datasets.webdataset.distributors.multi_aspect_ratio import ShardlistMultiAspectRatio +from cosmos3._src.imaginaire.datasets.webdataset.distributors.multi_aspect_ratio_v2 import ShardlistMultiAspectRatioInfinite +from cosmos3._src.imaginaire.datasets.webdataset.distributors.weighted_multi_aspect_ratio import WeightedShardlistMultiAspectRatio + +distributors_list = { + "basic": ShardlistBasic, + "multi_aspect_ratio": ShardlistMultiAspectRatio, + "multi_aspect_ratio_infinite": ShardlistMultiAspectRatioInfinite, + "weighted_multi_aspect_ratio": WeightedShardlistMultiAspectRatio, +} diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py new file mode 100644 index 00000000..4c7c4cf9 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import random +import time + +from webdataset.pytorch import IterableDataset +from webdataset.utils import pytorch_worker_info + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import TarSample +from cosmos3._src.imaginaire.datasets.webdataset.utils.misc import repeat_list +from cosmos3._src.imaginaire.utils import log + + +class ShardlistBasic(IterableDataset): + r""" + An iterable dataset that parses and yields tar files. + The dataset restored from an iteration number and index number. + """ + + def __init__( + self, + shuffle: bool = True, + split_by_node: bool = True, + split_by_worker: bool = True, + resume_flag: bool = True, + verbose: bool = False, + is_infinite_loader: bool = False, + max_epochs: int = 100000, + repeat_url: bool = True, + ): + r"""Create a ShardList. + Args: + shuffle (bool): shuffle samples before iterating. + split_by_node (bool): split shards by node if True + split_by_worker (bool): split shards by worker if True + resume_flag (bool): If enabled, resumes from a specific iteration and epoch number. + verbose (bool): Prints some logs if true + is_infinite_loader (bool): If true, creates an infinite dataloader. + So, the dataset will be only one epoch and will not terminate. + max_epochs (int): Infinite dataloader is created with max_epochs number of epochs. + Should be a very large number. + repeat_url (bool): If true, each worker will receive the same number of batches by repeating urls. + """ + super().__init__() + + self.verbose = verbose + if self.verbose: + log.info("ShardListWithResumes init") + self.epoch = 0 + self.start_index = 0 + self.shuffle = shuffle + self.split_by_node = split_by_node + self.split_by_worker = split_by_worker + self.resume_flag = resume_flag + self.is_infinite_loader = is_infinite_loader + self.max_epochs = max_epochs + self.repeat_url = repeat_url + + def set_urls(self, urls: list[TarSample]): + """Set urls + + Args: + urls (list[TarSample]): a list of tar files along with their metadata + """ + self.urls = urls + + def set_chunk_size(self, chunk_size: int): + """Set chunk size + + Args: + chunk_size (int): chunk size used in webdataset creation + """ + self.chunk_size = chunk_size + + def set_epoch(self, epoch: int, start_index: int): + r"""Set the current epoch. Used for per-node shuffling. + Args: + epoch (int): Epoch number + start_index (int): iteraton number + """ + self.epoch = epoch + self.start_index = start_index + + def obtain_url_list(self): + r"""Return an iterator over the shards.""" + + rank, world_size, worker_id, num_workers = pytorch_worker_info() + + # Setting epoch and start index + if self.resume_flag: + self.epoch = int(os.environ.get("WDS_EPOCH_NUM", 0)) + # This tells us number of chunks that have been seen by one GPU + self.start_index = int(os.environ.get("WDS_START_INDEX", 0)) // self.chunk_size + + urls = self.urls + num_urls = len(urls) + + if self.repeat_url: + # Extending urls so that each workers receive the same number of batches. + # This serves the job of ddp_equalize. + nworkers_all = world_size * num_workers + num_urls_per_process = (num_urls + nworkers_all - 1) // nworkers_all + extended_url_list_size = num_urls_per_process * nworkers_all + urls = repeat_list(urls, extended_url_list_size) + + # Splits the urls by node and worker id. This ensures each worker sees different urls. + if self.split_by_node: + urls = urls[rank::world_size] + if self.split_by_worker: + urls = urls[worker_id::num_workers] + + if self.verbose: + log.info("List of urls (before shuffle)") + log.info(urls[0:10]) + + if self.shuffle: + # Shuffle based on the world worker id. + random.Random(rank * num_workers + worker_id).shuffle(urls) + + # This tells us the number of chunks seen by one worker. + # Do not iterate over the seen chunks. + start_index_per_worker = self.start_index // num_workers + if not self.is_infinite_loader: + urls = urls[start_index_per_worker:] + + if self.verbose: + log.info("List of urls (after shuffle)") + log.info(urls[0:10]) + log.info(f"PytorchShardList got {len(urls)} urls") + + return urls + + def __iter__(self): + url_list = self.obtain_url_list() + + if self.is_infinite_loader: + for _ in range(self.max_epochs): + cur_time = int(time.time()) + random.Random(cur_time).shuffle(url_list) + for url in url_list: + yield dict(url=url) + else: + for url in url_list: + yield dict(url=url) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py new file mode 100644 index 00000000..70c4f352 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py @@ -0,0 +1,274 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script contains the code for multi-aspect ratio shard iterator + +import math +import os +import random +import time +from collections import defaultdict +from copy import deepcopy + +from webdataset.pytorch import IterableDataset +from webdataset.utils import pytorch_worker_info + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import TarSample +from cosmos3._src.imaginaire.datasets.webdataset.utils.misc import repeat_list +from cosmos3._src.imaginaire.utils import log + + +class ShardlistMultiAspectRatio(IterableDataset): + r""" + An iterable dataset that parses and yields tar files. + This distributor handles the multi-aspect ratio case. For the dataloader to be successful, + each worker should load only one aspect ratio. Else, there can be a batch where two + aspect ratios would be present which would raise an error in collate function. + So, we design data distribution strategy so that each worker sees only one aspect ratio. + """ + + def __init__( + self, + shuffle: bool = True, + split_by_node: bool = True, + split_by_worker: bool = True, + chunk_size: int = 1, + resume_flag: bool = True, + verbose: bool = False, + is_infinite_loader: bool = False, + ): + r"""Create a multi-aspect ratio ShardList. + Args: + urls (list[TarSample]): a list of tar files along with their metadata + epoch_shuffle (bool): Shuffles the whole epoch. If disabled, each node will see the same set of urls. + shuffle (bool): shuffle samples before iterating. + split_by_node (bool): split shards by node if True + split_by_worker (bool): split shards by worker if True + chunk_size (int): chunk size used in webdataset creation + resume_flag (bool): If enabled, resumes from a specific iteration and epoch number. + verbose (bool): Prints some logs if true + is_infinite_loader (bool): If true, creates an infinite dataloader. + So, the dataset will be only one epoch and will not terminate. + """ + super().__init__() + + self.verbose = verbose + if self.verbose: + log.info("ShardListWithResumes init") + self.epoch = 0 + self.start_index = 0 + self.shuffle = shuffle + self.split_by_node = split_by_node + self.split_by_worker = split_by_worker + self.chunk_size = chunk_size + self.resume_flag = resume_flag + self.is_infinite_loader = is_infinite_loader + + def set_urls(self, urls: list[TarSample]): + self.urls = urls + self._split_urls_by_aspect_ratio() + + def set_chunk_size(self, chunk_size: int): + """Set chunk size + + Args: + chunk_size (int): chunk size used in webdataset creation + """ + self.chunk_size = chunk_size + + def set_epoch(self, epoch: int, start_index: int): + r"""Set the current epoch. Used for per-node shuffling. + Args: + epoch (int): Epoch number + start_index (int): iteraton number + """ + self.epoch = epoch + self.start_index = start_index + + def _split_urls_by_aspect_ratio(self): + r"""Function for splitting urls by aspect ratio. + We assume that urls are grouped by dataset_id. That is, data belonging to + one dataset_id should have all data in the same aspect ratio. + """ + + url_aspect_split = defaultdict(list) + + for url in self.urls: + dset_info = url.meta + if "aspect_ratio" not in dset_info.opts: + raise ValueError("aspect_ratio should be specified in dataset_info when using multi aspect distributor") + aspect_ratio = dset_info.opts["aspect_ratio"] + url_aspect_split[aspect_ratio].append(url) + + aspect_ratio_with_most_elems = -1 + aspect_ratio_with_least_elems = -1 + max_aspect_ratio_count = -1 + min_aspect_ratio_count = 1000000000 + + for aspect_ratio in url_aspect_split: + # Sort the url list + url_aspect_split[aspect_ratio] = sorted( + url_aspect_split[aspect_ratio], key=lambda tar: (tar.path, tar.root) + ) + + # Finding max and min tar counts per aspect ratio + if len(url_aspect_split[aspect_ratio]) > max_aspect_ratio_count: + aspect_ratio_with_most_elems = aspect_ratio + max_aspect_ratio_count = len(url_aspect_split[aspect_ratio]) + if len(url_aspect_split[aspect_ratio]) < min_aspect_ratio_count: + aspect_ratio_with_least_elems = aspect_ratio + min_aspect_ratio_count = len(url_aspect_split[aspect_ratio]) + + self.url_aspect_split = url_aspect_split + self.aspect_ratio_with_most_elems = aspect_ratio_with_most_elems + self.aspect_ratio_with_least_elems = aspect_ratio_with_least_elems + + def _ddp_equalize( + self, url_aspect_split: dict[str, list[TarSample]], nworkers_all: int + ) -> tuple[dict[str, list[TarSample]], int]: + r"""This function performs tar file equalization. That is, we repeat the number of tars in each aspect + ratio so that when the tars are split across workers, each worker recieves the same number of tars. + This function is important for ddp to terminate well at the end of each epoch. + + Args: + url_aspect_split (dict[list[TarSample]]): TarSample split by aspect ratio + nworkers_all (int): Total number of dataloader workers + + Returns: + url_aspect_split (dict[list[TarSample]]): TarSample split after DDP equalization + num_urls_per_worker (int): Number of tars in each worker + """ + betas = [] + n_total = sum([len(url_aspect_split[aspect_ratio]) for aspect_ratio in url_aspect_split]) + + # Initial assignment + aspect_ind_with_most_elems = 0 + for i, aspect_ratio in enumerate(url_aspect_split): + betas.append(math.ceil((len(url_aspect_split[aspect_ratio]) / n_total) * nworkers_all)) + if aspect_ratio == self.aspect_ratio_with_most_elems: + aspect_ind_with_most_elems = i + + # Constraint that total number of workers is fixed + betas[aspect_ind_with_most_elems] += nworkers_all - sum(betas) + + # Rebalance the number of urls + num_urls_per_worker = math.ceil(n_total / sum(betas)) + for i, aspect_ratio in enumerate(url_aspect_split): + url_aspect_split[aspect_ratio] = repeat_list(url_aspect_split[aspect_ratio], betas[i] * num_urls_per_worker) + + return url_aspect_split, num_urls_per_worker + + def _obtain_node_worker_url_mapping( + self, + url_aspect_split: dict[str, list[TarSample]], + num_urls_per_worker: int, + rank: int, + world_size: int, + worker_id: int, + num_workers: int, + ): + r"""This function obtains the worker-URL mapping. It assigns the tar list seen by + each workers. + + Args: + url_aspect_split (dict[list[TarSample]]: TarSample split by aspect ratio + num_urls_per_worker (int): Number of tar files seen by each worker + rank (int): Rank of the current GPU + world_size (int): Total number of GPUs + worker_id (int): ID for the current worker in the dataloader + num_workers (int): Total number of workers in the dataloader + + Returns: + URL list for the current worker + """ + assert self.split_by_node is True and self.split_by_worker is True + + # First chunk the tars + chunk_mappings = [] + for aspect_ratio in url_aspect_split: + samples_asp = url_aspect_split[aspect_ratio] + nchunks_asp = int(len(samples_asp) / num_urls_per_worker) + for chunk_id in range(nchunks_asp): + chunk_mappings.append((aspect_ratio, samples_asp[chunk_id::nchunks_asp])) + + # Split by rank and workers + chunk_mappings = chunk_mappings[rank::world_size] + chunk_mappings = chunk_mappings[worker_id::num_workers] + + assert len(chunk_mappings) == 1 + return chunk_mappings[0][1] + + def obtain_url_list(self): + r"""Return an iterator over the shards.""" + + rank, world_size, worker_id, num_workers = pytorch_worker_info() + + # Setting epoch and start index + if self.resume_flag: + self.epoch = int(os.environ.get("WDS_EPOCH_NUM", 0)) + + # This tells us number of chunks that have been seen by one GPU + self.start_index = int(os.environ.get("WDS_START_INDEX", 0)) // self.chunk_size + + urls = deepcopy(self.urls) + url_aspect_split = deepcopy(self.url_aspect_split) + + # Splitting the shards by worker and node + if self.verbose: + log.info(f"PytorchShardList rank {rank} of {world_size}") + log.info(f"PytorchShardList worker {worker_id} of {num_workers}") + + nworkers_all = world_size * num_workers + + # Perform DDP equalization + url_aspect_split, num_urls_per_worker = self._ddp_equalize(url_aspect_split, nworkers_all) + + # Form a mapping of url_aspect_split to node and workers + urls = self._obtain_node_worker_url_mapping( + url_aspect_split, num_urls_per_worker, rank, world_size, worker_id, num_workers + ) + + if self.verbose: + log.info("List of urls (before shuffle)") + log.info(urls[0:10]) + + if self.shuffle: + random.Random(rank * num_workers + worker_id).shuffle(urls) + + # This tells us the number of chunks seen by one worker. + # Do not iterate over the seen chunks. + start_index_per_worker = self.start_index // num_workers + if not self.is_infinite_loader: + urls = urls[start_index_per_worker:] + + if self.verbose: + log.info("List of urls (after shuffle)") + log.info(urls[0:10]) + log.info(f"PytorchShardList got {len(urls)} urls") + + return urls + + def __iter__(self): + url_list = self.obtain_url_list() + + if self.is_infinite_loader: + while True: + cur_time = time.time_ns() + random.Random(cur_time).shuffle(url_list) + for url in url_list: + yield dict(url=url) + else: + for url in url_list: + yield dict(url=url) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py new file mode 100644 index 00000000..fae6b327 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py @@ -0,0 +1,252 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script contains the code for multi-aspect ratio shard iterator + +import random +import time +from collections import defaultdict + +import numpy as np +from webdataset.pytorch import IterableDataset +from webdataset.utils import pytorch_worker_info + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import TarSample +from cosmos3._src.imaginaire.utils import log + + +class ShardlistMultiAspectRatioInfinite(IterableDataset): + r""" + An iterable dataset that parses and yields tar files. + This distributor handles the multi-aspect ratio case. For the dataloader to be successful, + each worker should load only one aspect ratio. Else, there can be a batch where two + aspect ratios would be present which would raise an error in collate function. + So, we design data distribution strategy so that each worker sees only one aspect ratio. + + This version only supports infinite loader mode. This enables a simpler code that is faster to initialize + and produces samples better matching the dataset distribution. + """ + + def __init__( + self, + shuffle: bool = True, + split_by_node: bool = True, + split_by_worker: bool = True, + chunk_size: int = 1, + resume_flag: bool = True, + verbose: bool = False, + is_infinite_loader: bool = True, + ): + r"""Create a multi-aspect ratio ShardList. + Args: + urls (list[TarSample]): a list of tar files along with their metadata + epoch_shuffle (bool): Shuffles the whole epoch. If disabled, each node will see the same set of urls. + shuffle (bool): shuffle samples before iterating. + split_by_node (bool): split shards by node if True + split_by_worker (bool): split shards by worker if True + chunk_size (int): Ignored + resume_flag (bool): Ignored + verbose (bool): Prints some logs if true + is_infinite_loader (bool): If true, creates an infinite dataloader. + So, the dataset will be only one epoch and will not terminate. + """ + super().__init__() + + self.verbose = verbose + if self.verbose: + log.info("ShardlistMultiAspectRatioInfinite init") + self.shuffle = shuffle + self.split_by_node = split_by_node + self.split_by_worker = split_by_worker + self.chunk_size = chunk_size # Ignored + self.resume_flag = resume_flag # Ignored + assert is_infinite_loader is True + + def set_urls(self, urls: list[TarSample]): + self.url_aspect_split = self._split_urls_by_aspect_ratio(urls) + + def set_chunk_size(self, chunk_size: int): + """Set chunk size + For backward compatibility. Ignored. + + Args: + chunk_size (int): chunk size used in webdataset creation + """ + self.chunk_size = chunk_size + + def set_epoch(self, epoch: int, start_index: int): + r"""Set the current epoch. Used for per-node shuffling. + For backward compatibility. Ignored. + + Args: + epoch (int): Epoch number + start_index (int): iteraton number + """ + self.epoch = epoch + self.start_index = start_index + + def _split_urls_by_aspect_ratio(self, urls): + r"""Function for splitting urls by aspect ratio. + We assume that urls are grouped by dataset_id. That is, data belonging to + one dataset_id should have all data in the same aspect ratio. + """ + + url_aspect_split = defaultdict(list) + + for url in urls: + dset_info = url.meta + if "aspect_ratio" not in dset_info.opts: + raise ValueError("aspect_ratio should be specified in dataset_info when using multi aspect distributor") + aspect_ratio = dset_info.opts["aspect_ratio"] + url_aspect_split[aspect_ratio].append(url) + + for aspect_ratio in url_aspect_split: + # Sort the url list + url_aspect_split[aspect_ratio] = sorted( + url_aspect_split[aspect_ratio], key=lambda tar: (tar.path, tar.root) + ) + + return url_aspect_split + + def _allocate_workers_to_aspects( + self, url_aspect_split: dict[str, list[TarSample]], num_workers_all: int + ) -> list[tuple[str, int]]: + r"""Allocate workers to each aspect ratio so that: + 1. Each aspect ratio has at least one worker + 2. All the workers have jobs to do + + Args: + url_aspect_split (dict[list[TarSample]]): TarSample split by aspect ratio + num_workers_all (int): Total number of dataloader workers + + Returns: + aspect_worker_allocation (list): List of tuple containing (aspect_key, num_workers) + """ + if self.verbose: + log.info( + f"#URLs for each aspect ratio: {[len(url_aspect_split[aspect_ratio]) for aspect_ratio in url_aspect_split]}" + ) + + # Must have more global workers than the number of aspect ratios, as each global worker can only load a single + # aspect ratio. + num_aspects = len(url_aspect_split) + assert num_workers_all >= num_aspects + + aspect_keys = list(url_aspect_split.keys()) + # Allocate at least one worker per aspect ratios + target_ratio = np.array([len(url_aspect_split[key]) for key in aspect_keys]) + target_ratio = target_ratio / target_ratio.sum() + aspect_worker_allocation = np.ones([num_aspects], dtype=np.int64) + for _i in range(num_workers_all - num_aspects): + current_ratio = aspect_worker_allocation / aspect_worker_allocation.sum() + aspect_worker_allocation[np.argmin(current_ratio - target_ratio)] += 1 + + if self.verbose: + log.info(f"Aspects: {aspect_keys}") + log.info(f"Target ratio: {target_ratio}") + log.info(f"Worker allocation: {aspect_worker_allocation}") + log.info(f"Discrepancy: {aspect_worker_allocation / aspect_worker_allocation.sum() / target_ratio}") + return [(k, v) for k, v in zip(aspect_keys, aspect_worker_allocation.tolist())] + + def _obtain_node_worker_url_mapping( + self, + url_aspect_split: dict[str, list[TarSample]], + aspect_worker_allocation: list[tuple[str, int]], + rank: int, + world_size: int, + worker_id: int, + num_workers: int, + ): + r"""This function obtains the worker-URL mapping. It assigns the tar list seen by + each workers. + + Args: + url_aspect_split (dict[list[TarSample]]: TarSample split by aspect ratio + aspect_worker_allocation (dict): Number of workers allocated to each aspect ratio + rank (int): Rank of the current GPU + world_size (int): Total number of GPUs + worker_id (int): ID for the current worker in the dataloader + num_workers (int): Total number of workers in the dataloader + + Returns: + URL list for the current worker + """ + assert self.split_by_node is True and self.split_by_worker is True + + # First determine the aspect ratio for the current worker + global_worker_id = rank * num_workers + worker_id + + cumulative = 0 + for aspect_key, worker_count in aspect_worker_allocation: + cumulative += worker_count + if global_worker_id < cumulative: + chunk_id = global_worker_id - cumulative + worker_count + break + + if self.verbose: + log.info(f"GID={global_worker_id}, aspect_key={aspect_key}, chunk_id={chunk_id}") + # chunk the urls for the target aspect ratio + urls_asp = url_aspect_split[aspect_key] + if len(urls_asp) >= worker_count: + url_chunk = urls_asp[chunk_id::worker_count] + else: + url_chunk = urls_asp[chunk_id % len(urls_asp) : chunk_id % len(urls_asp) + 1] + + return url_chunk + + def obtain_url_list(self): + r"""Return an iterator over the shards.""" + + rank, world_size, worker_id, num_workers = pytorch_worker_info() + + # Splitting the shards by worker and node + if self.verbose: + log.info(f"PytorchShardList rank {rank} of {world_size}") + log.info(f"PytorchShardList worker {worker_id} of {num_workers}") + + nworkers_all = world_size * num_workers + + # Assigning workers to process each aspect ratio + aspect_worker_allocation = self._allocate_workers_to_aspects(self.url_aspect_split, nworkers_all) + + # Form a mapping of url_aspect_split to node and workers + urls = self._obtain_node_worker_url_mapping( + self.url_aspect_split, aspect_worker_allocation, rank, world_size, worker_id, num_workers + ) + + if self.verbose: + log.info("List of urls (before shuffle)") + log.info(urls[0:10]) + + if self.shuffle: + global_worker_id = rank * num_workers + worker_id + random.Random(global_worker_id).shuffle(urls) + + if self.verbose: + log.info("List of urls (after shuffle)") + log.info(urls[0:10]) + log.info(f"PytorchShardList got {len(urls)} urls") + + return urls + + def __iter__(self): + url_list = self.obtain_url_list() + while True: + if self.shuffle: + cur_time = time.time_ns() + random.Random(cur_time).shuffle(url_list) + assert len(url_list) > 0, "No urls found" + for url in url_list: + yield dict(url=url) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py new file mode 100644 index 00000000..65be2d44 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Weighted multi-aspect ratio shard distributor. +Subclass of ShardlistMultiAspectRatio that adds sampling weighted by data source +within each aspect-ratio partition. Preserves per-worker aspect-ratio assignment and DDP behavior. +""" + +import os +import random +import time +from datetime import datetime + +from webdataset.utils import pytorch_worker_info + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import TarSample +from cosmos3._src.imaginaire.datasets.webdataset.distributors.multi_aspect_ratio import ShardlistMultiAspectRatio +from cosmos3._src.imaginaire.utils import log + + +class WeightedShardlistMultiAspectRatio(ShardlistMultiAspectRatio): + r""" + Multi-aspect ratio shard list with weighted sampling by data source. + Each worker still receives URLs for a single aspect ratio (same as base class). + Within that set, URLs are sampled by datasource according to data_weight_dict. + """ + + def __init__( + self, + data_weight_dict: dict | None = None, + shuffle: bool = True, + split_by_node: bool = True, + split_by_worker: bool = True, + chunk_size: int = 1, + resume_flag: bool = True, + verbose: bool = False, + is_infinite_loader: bool = False, + dump_worker_category_distribution: bool = False, + ): + r"""Create a weighted multi-aspect ratio ShardList. + + Args: + data_weight_dict (dict | None): Mapping from data source name to weight. + If None, behaves like ShardlistMultiAspectRatio (no weighting). + shuffle (bool): Shuffle samples before iterating. + split_by_node (bool): Split shards by node if True. + split_by_worker (bool): Split shards by worker if True. + chunk_size (int): Chunk size used in webdataset creation. + resume_flag (bool): If enabled, resumes from WDS_EPOCH_NUM and WDS_START_INDEX. + verbose (bool): Print extra logs if True. + is_infinite_loader (bool): If True, dataloader runs indefinitely with weighted sampling. + dump_worker_category_distribution (bool): If True, dump the worker category distribution to one csv file per worker. + """ + super().__init__( + shuffle=shuffle, + split_by_node=split_by_node, + split_by_worker=split_by_worker, + chunk_size=chunk_size, + resume_flag=resume_flag, + verbose=verbose, + is_infinite_loader=is_infinite_loader, + ) + self.data_weight_dict = data_weight_dict + self.dump_worker_category_distribution = dump_worker_category_distribution + if self.dump_worker_category_distribution: + self.weight_per_tar_csv_dir = f"outputs/weight_csvs_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + os.makedirs(self.weight_per_tar_csv_dir, exist_ok=True) + + def set_urls(self, urls: list[TarSample]): + super().set_urls(urls) + if self.data_weight_dict: + # Count global *samples* per datasource *before* per-worker splitting so that + # each tar file can be assigned weight = datasource_weight * global_sample_count. + global_sample_counts: dict[str, int] = {} + for url in urls: + src = url.meta.source + global_sample_counts[src] = global_sample_counts.get(src, 0) + url.num_samples + self._global_datasource_sample_counts = global_sample_counts + for src in global_sample_counts: + log.info(f"Global counts for {src}: {global_sample_counts[src]} samples") + if self.verbose: + # Log aspect-ratio split from base class (ratio feature is used per-worker) + if hasattr(self, "url_aspect_split") and self.url_aspect_split: + ratio_summary = {ar: len(entries) for ar, entries in self.url_aspect_split.items()} + log.info( + f"WeightedShardlistMultiAspectRatio: aspect_ratio split (ratio feature active): {ratio_summary}" + ) + if self.data_weight_dict: + log.info(f"data_weight_dict: {self.data_weight_dict}") + + def __iter__(self): + url_list = self.obtain_url_list() + + # Group URLs by datasource within this worker's list + urls_by_datasource: dict[str, list[TarSample]] = {} + for url in url_list: + datasource = url.meta.source + if datasource not in self.data_weight_dict: + raise ValueError( + f"Datasource '{datasource}' from URL not found in data_weight_dict. " + f"Available: {list(self.data_weight_dict.keys())}" + ) + if datasource not in urls_by_datasource: + urls_by_datasource[datasource] = [] + urls_by_datasource[datasource].append(url) + + if self.verbose: + counts = {cat: len(u) for cat, u in urls_by_datasource.items()} + log.info( + f"WeightedShardlistMultiAspectRatio: weighted sampling active — " + f"URLs per datasource (this worker): {counts}, weights={self.data_weight_dict}" + ) + + datasource_names = list(urls_by_datasource.keys()) + + if self.is_infinite_loader: + rank, world_size, worker_id, num_workers = pytorch_worker_info() + # One RNG per worker, seeded once + worker_seed = (rank * num_workers + worker_id) + int(time.time() * 10000) + rng = random.Random(worker_seed) + + # Build a flat list of tar files with per-tar weights. + # Each tar from datasource C gets weight = data_weight_dict[C] * global_samples_C. + flat_urls: list[TarSample] = [] + flat_weights: list[float] = [] + if self.dump_worker_category_distribution: + weight_csv_file = open( + os.path.join(self.weight_per_tar_csv_dir, f"_weight_per_tar_{rank * num_workers + worker_id}.csv"), + "w", + ) + weight_csv_file.write( + "datasource,wdinfo,path,weight,global_samples,data_list_key_count,data_weight_dict\n" + ) + + for datasource in datasource_names: + tars = urls_by_datasource[datasource] + global_samples = self._global_datasource_sample_counts[datasource] + for url in tars: + per_tar_weight = self.data_weight_dict[datasource] / global_samples + + flat_urls.append(url) + flat_weights.append(per_tar_weight) + if self.dump_worker_category_distribution: + weight_csv_file.write( + f"{datasource},{url.meta.wdinfo},{url.path},{per_tar_weight},{global_samples},{url.num_samples},{self.data_weight_dict[datasource]}\n" + ) + if self.dump_worker_category_distribution: + weight_csv_file.close() + + while True: + url = rng.choices(flat_urls, weights=flat_weights, k=1)[0] + yield dict(url=url) + else: + for url in url_list: + yield dict(url=url) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py new file mode 100644 index 00000000..4b070d00 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py @@ -0,0 +1,617 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import random +import sys +import time +from typing import IO, Any, BinaryIO, Callable, Dict, Iterable, Iterator, Optional, Tuple, Union +from urllib.parse import urlparse + +import boto3 +import botocore +import botocore.exceptions +import pandas as pd +import webdataset.gopen as gopen_webdata +import yaml +from webdataset import cache, filters, shardlists +from webdataset.compat import FluidInterface +from webdataset.handlers import reraise_exception +from webdataset.pipeline import DataPipeline +from webdataset.pytorch import IterableDataset +from webdataset.tariterators import group_by_keys, tar_file_iterator + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import TarSample +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream +from cosmos3._src.imaginaire.utils import log + +# Number of attempts to read s3 objects. +_NUM_OBJECT_STORE_READ_ATTEMPTS = 10 + + +def gopen(url: Tuple, mode: str = "rb", bufsize: int = 8192, **kw) -> Union[io.BytesIO, RetryingStream, BinaryIO, IO]: + r"""Open the URL. + This uses the `gopen_schemes` dispatch table to dispatch based + on scheme. + Support for the following schemes is built-in: pipe, file, + http, https, sftp, ftps, scp. + When no scheme is given the url is treated as a file. + You can use the OPEN_VERBOSE argument to get info about + files being opened. + Args: + url (tuple): (source URL, dataset id) + the source URL is join(TarSample.root, one of TarSample.keys, TarSample.path) + e.g. join("openx_short_cmu_playing_with_food_202505/v2.3/resolution_lt_720/aspect_ratio_4_3/duration_5_10/", "videos", "part_000000/000000.tar") + mode (str): the mode ("rb", "r") + bufsize (int): the buffer size + Returns: + Byte streams + """ + global fallback_gopen + verbose = int(os.environ.get("GOPEN_VERBOSE", 0)) + if verbose: + log.info("GOPEN", url, gopen_webdata.info, file=sys.stderr) + + assert mode in ["rb", "wb"], mode + if url == "-": + if mode == "rb": + return sys.stdin.buffer + elif mode == "wb": + return sys.stdout.buffer + else: + raise ValueError(f"unknown mode {mode}") + + # If we specify 'object_store' in keyword arguments, + # then we would load from s3. + if "object_store" in kw and kw["object_store"]: + assert isinstance(url, tuple) + return gopen_s3( + url, + s3_clients=kw["s3_client"], + s3_bucket_name=kw["s3_bucket_name"], + streaming_download=kw["streaming_download"], + ) + + # For all other gopen schemes, use the native webdataset gopen functions. + # pr = gopen_webdata.urlparse(url) + # this should be a path to an existing file on local machine + url = url[0] + assert isinstance(url, str) + pr = urlparse(url) + if pr.scheme == "": + bufsize = int(os.environ.get("GOPEN_BUFFER", -1)) + return open(url, mode, buffering=bufsize) + if pr.scheme == "file": + bufsize = int(os.environ.get("GOPEN_BUFFER", -1)) + return open(pr.path, mode, buffering=bufsize) + handler = gopen_webdata.gopen_schemes["__default__"] + handler = gopen_webdata.gopen_schemes.get(pr.scheme, handler) + return handler(url, mode, bufsize, **kw) # type: ignore + + +def gopen_s3( + url: tuple, + s3_clients: Dict[str, boto3.client], # type: ignore + s3_bucket_name: Dict[str, str], + streaming_download=True, +) -> Union[io.BytesIO, RetryingStream]: + r"""Gopen scheme for s3. + Function for reading urls from s3 + Args: + url (list[TarSample]): the source URL + s3_client (boto3.client): Boto3 client for downloading from S3 + s3_bucket_name (str): Bucket name for the S3 data + Returns: + Byte streams + """ + + attempt = 0 + + url_path = url[0] + dset_id = url[1] + s3_client = s3_clients[dset_id] + bucket = s3_bucket_name[dset_id] + + while attempt < _NUM_OBJECT_STORE_READ_ATTEMPTS: + try: + if streaming_download: + # Downloads in a streaming fashion + s3_stream = RetryingStream(s3_client, bucket=bucket, key=url_path) + return s3_stream + else: + # Downloads the entire file + buffer = io.BytesIO() + s3_client.download_fileobj(bucket, url_path, buffer) + buffer.seek(0) + return buffer + except botocore.exceptions.ClientError as e: + # If there is an exception (usually connectivity error or protocol error), read again + attempt += 1 + retry_interval = min( + 0.1 * 2**attempt + random.uniform(0, 1), 30 + ) # sleep workers randomly to avoid burst of requests + log.info( + f"Got an exception while downloading data {url_path}: attempt={attempt} - {e}. {type(e)}", + rank0_only=False, + ) + log.info(f"Retrying tar file download after {retry_interval}s", rank0_only=False) + time.sleep(retry_interval) + continue + raise ConnectionError("Unable to read {} from PBSS. {} attempts tried.".format(url, attempt)) + + +def url_opener(data: Iterable, handler: Callable = reraise_exception, **kw) -> Iterator[dict]: + r"""Given a stream of url names (packaged in `dict(url=url)`), yield opened streams. + + Args: + data (Iterable): Iterator of dictionaires containing url paths. + handler (Callable): Exception handler. + + Yields: + Dictionaries with this structure: + {"url": ... + "stream": list[Union[io.BytesIO, RetryingStream]]} + """ + for sample in data: + assert isinstance(sample, dict), sample + assert "url" in sample + + url = sample["url"] + assert isinstance(url, TarSample), "URL should be of type TarSample" + try: + stream = [] + for data_key in url.keys: + url_path_full = os.path.join(url.root, data_key, url.path) + url_key = (url_path_full, url.dset_id) + stream.append(gopen(url_key, **kw)) + + sample.update(stream=stream) + yield sample + except Exception as exn: + log.info(f"Got an exception while opening urls - {exn}", rank0_only=False) + exn.args = exn.args + (url,) + if handler(exn): + continue + else: + break + + +def process_sample(sample, url, key_idx): + assert isinstance(sample, dict) and "data" in sample and "fname" in sample + # Edit the url entries + sample["__url__"] = url + # This is the folder name + data_key = url.keys[key_idx] + # Handle the case where data_key has "/" + data_key = data_key.replace("/", "_") + # Edit the fname to include the data_key + fname_splits = sample["fname"].split(".") + if len(fname_splits) == 2: + prefix, suffix = fname_splits # {sample_key}.{suffix} e.g. "id_1410095.json" + else: # if the fname here contains more than one dot, we replace all the dots except the last one with "-" + prefix = "-".join(fname_splits[:-1]) + suffix = fname_splits[-1] + + # e.g. "id_1410095.caption_ai_from_image.json" + sample["fname"] = f"{prefix}.{data_key}.{suffix}" + + return sample + + +def tar_file_expander( + data: Iterable[Dict[str, Any]], + handler: Callable[[Exception], bool] = reraise_exception, + select_files: Optional[Callable[[str], bool]] = None, + rename_files: Optional[Callable[[str], str]] = None, + s3_client: Optional[Dict[str, boto3.client]] = None, # type: ignore + s3_bucket_name: Optional[Dict[str, str]] = None, +) -> Iterator[Dict[str, Any]]: + """Expand tar files. + + Args: + data (Iterable[Iterable[Dict[str, Any]]]): iterator over opened tar file streams. + handler (Callable[[Exception], bool]): exception handler. + select_files (Optional[Callable[[str], bool]]): select files from tarfiles by name (permits skipping files). + rename_files (Optional[Callable[[str], bool]]): Renaming tar files. + + Optional args if reading sample_keys_full_list: + s3_clients (Dict[str, boto3.client]): If loading from object store, specify S3 client. Keys is the dset_id, i.e. dataset id since different dataset could use different s3 client and bucket + s3_bucket_name (Dict[str, str]): If loading from object store, specify S3 bucket name. + + Yields: + a stream of samples. + """ + for source in data: + url = source["url"] + try: + assert isinstance(source, dict) + assert "stream" in source + tar_file_iterator_list = [] + for stream_id in range(len(source["stream"])): + tar_file_iterator_list.append( + tar_file_iterator( + source["stream"][stream_id], + handler=handler, + select_files=select_files, + rename_files=rename_files, + ) + ) + if url.sample_keys_full_list is None: # Original behavior + # tar_file_iterator_list is a list of iterator: [tar_file_iterator_0, tar_file_iterator_1, ... tar_file_iterator_N] + for sample in zip(*tar_file_iterator_list): + # Merging data from all streams + # sample is list of dictionaries, each dictionary contains data and fname + # sample [tar_file_iterator_0[0], tar_file_iterator_1[0], ... tar_file_iterator_N[0]], length = num_of_data_key + for key_idx, sample_key in enumerate(sample): + sample_key = process_sample(sample_key, url, key_idx) + yield sample_key + else: + # Read the index file from pbss + s3_client_cur = s3_client[url.dset_id] + bucket_cur = s3_bucket_name[url.dset_id] + sample_keys_full_list = read_sample_keys_full_list( + url.sample_keys_full_list, s3_client_cur, bucket_cur + ) # e.g. ["has_material_glb_from_obj_v4_1410095_0", "has_material_glb_from_obj_v4_1410095_1", ...] + sample_keys_full_to_index = {element: index for index, element in enumerate(sample_keys_full_list)} + + # Start reading the tar files + target_index = 0 + last_index = [-1] * len(tar_file_iterator_list) # Keep track of the last index of each tar file + sample_list = [] # List of samples from each tar file + while True: # Exit until target_index reach the max value + skip_offset = False + for key_idx, iterator in enumerate(tar_file_iterator_list): + if last_index[key_idx] >= target_index: + # This tar is moving faster than others, skip it and wait for others + continue + + # Read the tar file until current_index >= target_index + sample, current_index = run_iterator_to_index( + iterator, + target_index, + sample_keys_full_to_index, + name=f"{url.sample_keys_full_list}.{url.keys[key_idx]}", + ) + if sample is None: # Iterator {key_idx} already reached the end, exit the for loop + if target_index < len(sample_keys_full_to_index): # Missing keys + missing_info = f"index_path={url.sample_keys_full_list} | id={target_index}, sample_key={sample_keys_full_list[target_index]};" + log.info( + f"[missing keys] found in tar file: data_key={url.keys[key_idx]} | {missing_info}", + rank0_only=False, + ) + sample_list = [] # Reset the sample_list + break + + # Update the last_index + last_index[key_idx] = current_index + + # Process sample dict + sample = process_sample(sample, url=url, key_idx=key_idx) + + # Now check if the current index is matched or ahead + if current_index == target_index: # Nice! + sample_list.append(sample) + elif current_index > target_index: + # This means there is missing keys in this tar, this tar is moving faster than others + + # Log the missing info + missing_info = f"index_path={url.sample_keys_full_list} | " + for missing_idx in range(target_index, current_index): + missing_info += f" id={missing_idx}, sample_key={sample_keys_full_list[missing_idx]}; " + log.info( + f"[missing keys] found in tar file: data_key={url.keys[key_idx]} | {missing_info}", + rank0_only=False, + ) + + # Update the target_index to current_index, skip index inbetween old target_index and current_index + target_index = current_index + + # Reset sample_list, save the sample from this tar into sample_list and wait for others + sample_list = [ + sample + ] # Attnetion: this will change the order of sample_list, we will put them in the right order later + skip_offset = True # Skip the offset of target_index, since we are waiting for others + break + elif current_index < target_index: + # This should not happen + raise ValueError( + "Invalid output from run_iterator_to_index function. current_index should be equal or less than target_index" + ) + + # Decide where to yield the samples + if len(sample_list) == len(tar_file_iterator_list): + # Only yeild the samples if all the tars are preserved + all_prefix = [sample["fname"].split(".")[0] for sample in sample_list] + # Check all the prefix are the same + assert all(prefix == all_prefix[0] for prefix in all_prefix), ( + f"prefixes are not the same: {all_prefix}" + ) + # Correct the order of sample_list + sample_list = correct_order(sample_list, url.keys) + # Yield all the samples + for sample in sample_list: + assert isinstance(sample, dict) and "data" in sample and "fname" in sample + yield sample + sample_list = [] # Reset the sample_list + elif len(sample_list) > 1: + # Unexpected + raise ValueError(f"Unexpected length of sample_list: {len(sample_list)}") + elif len(sample_list) == 0 or len(sample_list) == 1: + # If the sample_list is empty, it means the tar file is exhausted + # If the sample_list has only one element, it means one tar file is moving faster than others + pass # Do nothing + + if not skip_offset: + # If sample_list has one element, we stay at current target_index until others catch up + target_index += 1 # Increase it by 1 + if target_index == len(sample_keys_full_to_index): + break # Reach the maximum index + # Make sure all the iterator are closed + for iterators in tar_file_iterator_list: + try: + next(iterators) + except StopIteration: + pass + + except Exception as exn: + log.info(f"Got an exception while expanding tars - {exn}", rank0_only=False) + exn.args = exn.args + (source.get("stream"), source.get("url")) + if handler(exn): + continue + else: + break + + +def correct_order(sample_list: list[Dict], expected_keys_order: list[str]) -> list[Dict]: + """Make sure the order of samples are the same as the url.keys order.""" + data_keys_per_sample = [sample["fname"].split(".")[1] for sample in sample_list] + expected_keys_order = [key.replace("/", "_") for key in expected_keys_order] + if data_keys_per_sample == expected_keys_order: # Correct order + return sample_list + # Order the sample_list based on the expected_keys_order + sample_list_ordered = [None] * len(expected_keys_order) + for data_key, sample in zip(data_keys_per_sample, sample_list): + idx = expected_keys_order.index(data_key) + sample_list_ordered[idx] = sample + return sample_list_ordered + + +def load_func_parquet(buffer): + data_list = pd.read_parquet(buffer).values.tolist() + names = [data[0] for data in data_list] + return names + + +def _read_sample_keys_full_list(key, s3_client: boto3.client, s3_bucket_name: str): + with io.BytesIO() as buffer: + s3_client.download_fileobj(Bucket=s3_bucket_name, Key=key, Fileobj=buffer) + buffer.seek(0) + sample_keys_full_list = load_func_parquet(buffer) + sample_keys_full_list = [key.split(".")[0] for key in sample_keys_full_list] + return sample_keys_full_list + + +def read_sample_keys_full_list(key: str, s3_client: boto3.client, s3_bucket_name: str, max_attempts=10): + for attempt in range(max_attempts): + try: + return _read_sample_keys_full_list(key, s3_client, s3_bucket_name) + except botocore.exceptions.ClientError as e: + retry_interval = min( + 0.1 * 2**attempt + random.uniform(0, 1), 30 + ) # sleep workers randomly to avoid burst of requests + log.exception( + f"Failed to read sample_keys_full_list {key}, attempt {attempt}. {e}. Retrying after {retry_interval}s." + ) + if attempt < max_attempts - 1: + time.sleep(retry_interval) + raise ConnectionError(f"Unable to read sample_keys_full_list {key} after {max_attempts} attempts.") + + +def run_iterator_to_index(iterator, target_index: int, sample_keys_full_to_index: dict, name: str = ""): + """ + Iterates over samples from an iterator, checking against the index of current sample (current_index) + to target_index, until it finds + 1) the sample key corresponds to the target index + or 2) the target index is passed (i,e, the target keys are missing) + or 3) until the iterator is exhausted. + + This function is designed to handle cases where there are unexpected, duplicated, or missing + sample keys based on the index mapping provided. + + Args: + iterator (iterator): An iterator yielding dictionaries that must include a key 'fname', + which contains the filename. The filename should be in the format 'prefix.suffix', + where 'prefix' will be used as the sample key. + target_index (int): The index of the sample to be retrieved according to the dictionary + mapping sample keys to indices. + sample_keys_full_to_index (dict): A dictionary mapping sample keys (extracted from the + 'fname' prefix of the iterator's samples) to their respective indices. This mapping + dictates the order in which samples are considered valid and should be found. + e.g. {"name_0": 0, "name_1": 1, "name_2": 2} + name (str): Names of the tar file, used to log the progress. + + Returns: + tuple: A tuple containing: + - sample (dict or None): The sample dictionary that matches the target index, or None + if no such sample is found by the time the iterator is exhausted. + - current_index (int or None): The index of the found sample according to the mapping, + or None if no sample is found. + + Raises: + StopIteration: If the iterator is exhausted without finding a matching sample, though this + is caught internally and handled by returning None values. + """ + sample, current_index = None, None + skip_count = 0 + while True: + try: + sample = next(iterator) + prefix, suffix = sample["fname"].split(".") + sample_key = prefix + + if sample_key not in sample_keys_full_to_index: # extra sample_key + log.info( + f"Skipping ({skip_count}) unexpected key {sample_key}; not found in the sample_keys_full_to_index {name} {sample_keys_full_to_index.keys()}" + ) + skip_count += 1 + continue + current_index = sample_keys_full_to_index[sample_key] # can be <,=,> target_index + if current_index < target_index: + # Note: current_index < target_index happens when duplicated keys or it's under catching up process + # e.g. [name_0, name_0, name_1] with target index = 1 + # Pointer at ^ + # Current index is 0, which is less than target index 1 + # In this case, we keep iterating + # log.info(f"[Skip] key {sample_key}; current_index={current_index} < target_index={target_index} {name}") + continue + elif current_index >= target_index: # Note: current_index > targer_index happens when there is missing keys + # Note: current_index > targer_index happens when there is missing keys + # e.g. [name_0, name_2, name_3] with target index 1 + # Pointer at ^ + # Current index is 2, which is greater than target index 1 + # In this case, we return the current_index, set the target_index to 2 and tell other tars to catch up. + # if current_index == target_index: # Matched! + # log.info(f"[Pass!] current_index={current_index} == target_index={target_index}") + # else: # Missing keys + # log.info(f"[Missing key detected!] current_index={current_index} > target_index={target_index} {name}") + break + + except StopIteration: + sample = None + current_index = None + break + return sample, current_index + + +def tarfile_samples( + src: Iterable, + handler: Callable = reraise_exception, + load_from_object_store: bool = False, + s3_client: Dict[str, boto3.client] = None, # type: ignore + s3_bucket_name: Optional[Dict[str, str]] = None, + streaming_download: bool = True, +) -> Iterator[Dict]: + r""" + Given an iterator of filenames, this function opens the URL streams + and groups data by keys. + + Args: + src (Iterable): Iterator of TarSample. + handler (Callable): Exception handler. + load_from_object_store (bool): A boolean flag to specify whether to load from + object store. + s3_client (boto3.client): If loading from object store, specify S3 client. + s3_bucket_name (str): If loading from object store, specify S3 bucket name. + streaming_download(bool): If enabled, performs streaming download. + """ + streams = url_opener( + src, + handler=handler, + object_store=load_from_object_store, + s3_client=s3_client, + s3_bucket_name=s3_bucket_name, + streaming_download=streaming_download, + ) + files = tar_file_expander(streams, handler=handler, s3_client=s3_client, s3_bucket_name=s3_bucket_name) + samples = group_by_keys(files, handler=handler) + return samples + + +tarfile_to_samples = filters.pipelinefilter(tarfile_samples) + + +class WebDataset(DataPipeline, FluidInterface): + r"""Webdataset class modified to support loading from object store.""" + + def __init__( + self, + urls: list[TarSample], + handler: Callable = reraise_exception, + resampled: bool = False, + shardshuffle: Optional[bool] = None, + cache_size: int = -1, + cache_dir: Optional[str] = None, + detshuffle: bool = False, + nodesplitter: Callable = shardlists.single_node_only, + verbose: bool = False, + load_from_object_store: bool = False, + s3_client: Dict[str, boto3.client] = None, # type: ignore + s3_bucket_name: Optional[Dict[str, str]] = None, + streaming_download: bool = True, + ): + r""" + Args: + urls (list[TarSample]): An iterator containing a list of url names. + handler (Callable): Exception handler. + resampled (bool): If true, sample shards from shard list with replacement. + shardshuffle (bool): If true, shuffles the entire shard list. + cache_size (int): Size of cache. + cache_dir (str): Path to store cache. + detshuffle (bool): Whether to use deterministic shuffling when shardshuffle is True. + nodesplitter (Callable): Function for splitting urls among nodes. + verbose (bool): If True, prints logs. + load_from_object_store (bool): A boolean flag to specify whether to load from + object store. + s3_client (boto3.client): If loading from object store, specify S3 client. + s3_bucket_name (str): If loading from object store, specify S3 bucket name. + streaming_download (bool): Whether to do streaming download or full object download. + """ + super().__init__() + if isinstance(urls, IterableDataset): + assert not resampled + self.append(urls) + elif isinstance(urls, str) and (urls.endswith(".yaml") or urls.endswith(".yml")): + with open(urls) as stream: + spec = yaml.safe_load(stream) + assert "datasets" in spec + self.append(shardlists.MultiShardSample(spec)) + elif isinstance(urls, dict): + assert "datasets" in urls + self.append(shardlists.MultiShardSample(urls)) + elif resampled: + self.append(shardlists.ResampledShards(urls)) + else: + self.append(shardlists.SimpleShardList(urls)) + self.append(nodesplitter) + self.append(shardlists.split_by_worker) + if shardshuffle is True: + shardshuffle = 100 # type: ignore + if shardshuffle is not None: + if detshuffle: + self.append(filters.detshuffle(shardshuffle)) + else: + self.append(filters.shuffle(shardshuffle)) + if cache_dir is None or cache_size == 0: + self.append( + tarfile_to_samples( + handler=handler, + load_from_object_store=load_from_object_store, + s3_client=s3_client, + s3_bucket_name=s3_bucket_name, + streaming_download=streaming_download, + ) + ) + else: + # We dont use cache. + assert cache_size == -1 or cache_size > 0 + self.append( + cache.cached_tarfile_to_samples( + handler=handler, + verbose=verbose, + cache_size=cache_size, + cache_dir=cache_dir, + ) + ) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py new file mode 100644 index 00000000..4ab1592b --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from typing import Iterator + +import attrs + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import SampleInfo + + +def repeat_list(x: list, n: int) -> list: + r"""Function to repeat the list to a fixed shape. + n is the desired length of the extended list. + Args: + x (list): Input list + n (int): Desired length + Returns: + Extended list + """ + if n == 0: + return [] + assert len(x) > 0 + + x_extended = [] + while len(x_extended) < n: + x_extended = x_extended + x + x_extended = x_extended[0:n] + + return x_extended + + +def remove_extensions_from_keys(data: Iterator[dict]) -> Iterator[dict]: + r"""Function to remove extension from keys + Args: + data (dict): Input data dict + Returns: + data dict with keys removed + """ + + for data_dict in data: + data_dict_remapped = dict() + + for key in data_dict: + key_split = key.split(".") + if len(key_split) > 1: + key_new = ".".join(key_split[:-1]) + else: + key_new = key + data_dict_remapped[key_new] = data_dict[key] + + yield data_dict_remapped + + +def update_url(data: Iterator[dict]) -> Iterator[dict]: + r"""Function to update the URLs so that the TarSample is removed from data. + Instead, we replace the URL with a string. + Args: + data (dict): Input data dict + Returns: + data dict with URL replaced with a string + """ + for data_dict in data: + # unpack meta information from TarSample + url = data_dict["__url__"] + sample_meta = url.sample_meta + if sample_meta is not None: + assert isinstance(sample_meta, SampleInfo) + data_dict.update(attrs.asdict(sample_meta)) + + # unpack url + data_dict["__url__"] = os.path.join(url.root, url.path) + yield data_dict + + +def skip_keys(data: Iterator[dict]) -> Iterator[dict]: + r""" + Function to skip keys + Args: + data (dict): Input data dict + Returns: + data_dict with keys skipped + """ + + for data_dict in data: + if ("keys_to_skip" in data_dict) and (int(data_dict["keys_to_skip"]) == 1): + # Skip this key if data_dict["skip_key"] is True + continue + else: + yield data_dict diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py new file mode 100644 index 00000000..b5cfa946 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py @@ -0,0 +1,1042 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# PBSS +import atexit +import json +import os +import sys +import threading +import time +import weakref +from dataclasses import dataclass, field +from http.client import IncompleteRead +from pathlib import Path +from typing import Optional + +import boto3 +from botocore.exceptions import ( + ClientError, + ConnectionClosedError, + EndpointConnectionError, + ResponseStreamingError, +) +from botocore.exceptions import ( + ReadTimeoutError as BotocoreReadTimeoutError, +) +from urllib3.exceptions import ProtocolError as URLLib3ProtocolError +from urllib3.exceptions import ReadTimeoutError as URLLib3ReadTimeoutError +from urllib3.exceptions import SSLError as URLLib3SSLError + +from cosmos3._src.imaginaire.utils import log + +# Public API - only these should be imported from this module +__all__ = [ + "RetryingStream", # Main class for S3 streaming with retries + "ENABLE_RETRY_STATS", # Flag to enable/disable statistics (used in tests/benchmarks) + "RETRY_STATS_LOG_INTERVAL", # Interval in seconds between periodic statistics logs + "ENABLE_THROUGHPUT_STATS", # Flag to enable/disable throughput statistics + "THROUGHPUT_STATS_LOG_INTERVAL", # Interval between periodic throughput statistics logs + "ENABLE_STREAM_WANDB", # Flag to enable/disable IPC file writes for wandb metrics + "WATCHDOG_ENABLED", # Enable/disable watchdog reconnects + "WATCHDOG_MIN_THROUGHPUT_MBPS", # Minimum throughput (MB/s) before watchdog reconnects + "RETRYABLE_EXCEPTIONS", # Tuple of exceptions that trigger retries + "collect_throughput_ipc_stats", # Main-process reader for worker throughput IPC files, for wandb logging +] + +# Flag to enable/disable statistics gathering (for performance testing) +# Set to False to disable all statistics overhead for maximum performance. +# When disabled, no thread-local tracking occurs and no logs are generated. +# +# Usage for benchmarking: +# import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.ENABLE_RETRY_STATS = False # Disable stats +# # ... run benchmark ... +# stream_module.ENABLE_RETRY_STATS = True # Re-enable +ENABLE_RETRY_STATS = False + +# Interval in seconds between periodic retry statistics logs +# Default is 300 seconds (5 minutes). Set to a lower value for more frequent logging +# or a higher value to reduce log verbosity. +# +# Usage: +# import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.RETRY_STATS_LOG_INTERVAL = 600 # Log every 10 minutes +RETRY_STATS_LOG_INTERVAL = 300.0 # 5 minutes + +# Flag to enable/disable throughput log messages (verbose per-worker logs). +# Does NOT affect IPC file writes (controlled by ENABLE_STREAM_WANDB). +# +# Usage: +# import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.ENABLE_THROUGHPUT_STATS = False +ENABLE_THROUGHPUT_STATS = False + +# Interval in seconds between periodic throughput statistics logs. +# Independent from retry stats log interval. +# +# Usage: +# import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.THROUGHPUT_STATS_LOG_INTERVAL = 600 # Log every 10 minutes +THROUGHPUT_STATS_LOG_INTERVAL = 300.0 # 5 minutes + +# Enable/disable IPC file writes for cross-worker metrics aggregation (wandb). +# This controls whether workers write cumulative stats to /tmp/throughput_stats/ +# for the main process to collect and log to wandb. Independent from verbose +# log messages (ENABLE_THROUGHPUT_STATS). +# +# Env var: export ENABLE_STREAM_WANDB=0 (to disable; default enabled) +ENABLE_STREAM_WANDB = os.environ.get("ENABLE_STREAM_WANDB", "1") != "0" + + +# Enable/disable the throughput watchdog (reconnects on sustained low throughput). +# Default: True (enabled) +# +# Env var: export RETRYING_STREAM_WATCHDOG=0 +# Python: import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.WATCHDOG_ENABLED = False +WATCHDOG_ENABLED = os.environ.get("RETRYING_STREAM_WATCHDOG", "1") != "0" + +# Minimum throughput (MB/s) before the watchdog triggers a reconnect. +# Default: 10.0 MB/s +# +# Env var: export RETRYING_STREAM_WATCHDOG_MIN_MBPS=50.0 +# Python: import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +# stream_module.WATCHDOG_MIN_THROUGHPUT_MBPS = 50.0 +WATCHDOG_MIN_THROUGHPUT_MBPS = float(os.environ.get("RETRYING_STREAM_WATCHDOG_MIN_MBPS", "50.0")) + + +@dataclass +class GlobalRetryStatistics: + """Per-process statistics aggregator for S3 retry operations. + + Aggregates statistics across all threads within this process (e.g., a DataLoader worker). + Each process maintains its own independent statistics - no cross-process communication. + In distributed training with DataLoader workers: + - Each rank's main process has its own _global_retry_stats instance + - Each DataLoader worker process (spawned via multiprocessing) has its own instance + - Statistics are isolated per-process, ensuring accurate tracking without interference + + Uses WeakSet to track active instances - automatically handles cleanup + even if threads die or exceptions occur during construction. + + Tracks both per-thread and cumulative statistics: + - registered_threads: Per-thread counters (including PID and thread ID) for detailed breakdown + - cumulative_*: Process-local cumulative counters (never reset, for atexit log) + + Statistics terminology: + - operations_started: Number of S3 operations initiated (read/get_length/get_stream calls) + - failed_operations: Operations that failed at least once and required retry + - total_attempts: Sum of all attempts (initial + retries) + + Note: operations_started counts how many operations we started (each gets 1 initial attempt). + total_attempts >= operations_started because failed operations retry multiple times. + + Thread safety: + - Per-thread counters are lock-free (threading.local() ensures isolation) + - Cumulative counters use a lock because += is not atomic in Python + - Lock overhead is negligible (< 0.1% from benchmarks) + - Single-threaded case: Lock acquisition is uncontended (instant, no blocking) + - Multi-threaded case: Lock contention is minimal (only during retries, which are rare) + """ + + registered_threads: dict[int, dict[str, int]] = field(default_factory=dict) + lock: threading.Lock = field(default_factory=threading.Lock) # Protects cumulative counters + last_log_time: float = field(default_factory=time.time) + active_instances: weakref.WeakSet = field(default_factory=weakref.WeakSet) # Tracks active RetryingStream instances + rank: int | None = None # Lazily initialized rank ID (None = not yet initialized) + pid: int | None = None # Lazily initialized process ID (None = not yet initialized, cached to avoid OS calls) + registered_pids: set[int] = field( + default_factory=set + ) # PIDs that have registered atexit handlers (for multiprocessing support) + + # Cumulative counters (never reset, for atexit final log) + # These require the lock because += is not atomic (3 bytecode operations: LOAD, ADD, STORE) + # Without the lock, concurrent increments can cause lost updates + cumulative_operations_started: int = 0 # Number of operations initiated + cumulative_failed_operations: int = 0 # Operations that failed and required retry + cumulative_attempts: int = 0 # Sum of all attempts (initial + retries) + + def get_rank(self) -> int: + """Get rank with lazy initialization. + + Rank is captured on first access, not at module import time. + This ensures torch.distributed is initialized before we try to read it. + + Falls back to RANK environment variable if torch.distributed is not initialized. + This handles DataLoader worker processes which inherit RANK from parent but + don't have torch.distributed initialized. + + Returns: + The rank ID (0 if distributed not available/initialized and no RANK env var) + """ + if self.rank is None: + try: + import torch.distributed as dist + + if dist.is_available() and dist.is_initialized(): + self.rank = dist.get_rank() + else: + # Fallback to RANK environment variable (for DataLoader workers) + self.rank = int(os.environ.get("RANK", "0")) + except Exception: + self.rank = 0 # Fallback if distributed not available + return self.rank + + def get_pid(self) -> int: + """Get process ID with lazy caching (avoids repeated OS calls). + + Returns: + The current process ID (cached after first call) + """ + if self.pid is None: + self.pid = os.getpid() + return self.pid + + +# Conditionally create per-process statistics objects only if stats are enabled +# Each process maintains independent statistics (no cross-process communication) +if ENABLE_RETRY_STATS: + _global_retry_stats = GlobalRetryStatistics() + _thread_local_stats = threading.local() + + # Rank is lazily initialized on first access via get_rank() + # This ensures torch.distributed is initialized before we try to read it + + # Note: atexit handler registration is now done lazily per-process in _get_thread_stats() + # This ensures each DataLoader worker process (spawned via multiprocessing) automatically + # registers its own atexit handler, making multiprocessing support transparent +else: + _global_retry_stats = None # type: ignore + _thread_local_stats = None # type: ignore + + +@dataclass +class GlobalThroughputStatistics: + """Per-process statistics aggregator for shard throughput and watchdog events. + + Same aggregation pattern as GlobalRetryStatistics: each DataLoader worker process + maintains its own independent instance. No cross-process communication. + + Periodic logs show interval values (delta since last log). The atexit final + log shows lifetime cumulative totals. IPC files carry cumulative snapshots + (deltas computed in collect_throughput_ipc_stats). + + Thread safety: Same lock pattern as GlobalRetryStatistics + """ + + lock: threading.Lock = field(default_factory=threading.Lock) + last_log_time: float = field(default_factory=time.time) + rank: int | None = None + pid: int | None = None + registered_pids: set[int] = field(default_factory=set) + + cumulative_bytes_read: int = 0 # log.warning + wandb (via IPC) + cumulative_total_read_time: float = 0.0 # log.warning + wandb (via IPC) + cumulative_watchdog_reconnects: int = 0 # log.warning + wandb (via IPC) + + _prev_bytes: int = 0 + _prev_read_time: float = 0.0 + _prev_watchdog: int = 0 + + def get_rank(self) -> int: + """Get rank with lazy initialization (same logic as GlobalRetryStatistics.get_rank).""" + if self.rank is None: + try: + import torch.distributed as dist + + if dist.is_available() and dist.is_initialized(): + self.rank = dist.get_rank() + else: + self.rank = int(os.environ.get("RANK", "0")) + except Exception: + self.rank = 0 + return self.rank + + def get_pid(self) -> int: + """Get process ID with lazy caching (avoids repeated OS calls).""" + if self.pid is None: + self.pid = os.getpid() + return self.pid + + +if ENABLE_THROUGHPUT_STATS or ENABLE_STREAM_WANDB: + _global_throughput_stats = GlobalThroughputStatistics() +else: + _global_throughput_stats = None # type: ignore + +# Exceptions that should trigger retries for S3 streaming operations +RETRYABLE_EXCEPTIONS = ( + URLLib3ReadTimeoutError, + URLLib3ProtocolError, + URLLib3SSLError, + IncompleteRead, + IOError, + ResponseStreamingError, + ConnectionClosedError, + BotocoreReadTimeoutError, +) + + +def _get_thread_stats() -> dict[str, int]: + """Get or initialize thread-local statistics (lock-free). + + Lazily registers atexit handler on first call per process, making multiprocessing + support transparent (each DataLoader worker process automatically gets its own handler). + + Performance optimizations: + - PID cached in GlobalRetryStatistics.pid (avoids repeated os.getpid() syscalls) + - Thread ID cached in thread-local counters (constant per thread lifetime) + - Rank cached in GlobalRetryStatistics.rank (avoids repeated torch.distributed calls) + + Returns: + Dictionary with thread-local counters for operations and retries. + Returns empty dict if statistics are disabled. + """ + # No-op if statistics are disabled (for performance) + if not ENABLE_RETRY_STATS or _thread_local_stats is None: + return {} # Return empty dict as no-op + + if not hasattr(_thread_local_stats, "counters"): + # Cache PID and thread ID (constant for the lifetime of this thread) + pid = _global_retry_stats.get_pid() # Cached to avoid repeated os.getpid() syscalls + thread_id = threading.get_ident() # Already fast, but cached for consistency + + counters = { + "pid": pid, # Process ID (distinguishes DataLoader workers) + "thread_id": thread_id, # Thread ID within this process + "operations_started": 0, # Number of S3 operations initiated (read/get_length/get_stream) + "failed_operations": 0, # Operations that failed at least once and required retry + "total_attempts": 0, # Sum of all attempts (initial + retries) + } + _thread_local_stats.counters = counters + + # Register this thread's stats for aggregation (only once per thread) + with _global_retry_stats.lock: + _global_retry_stats.registered_threads[thread_id] = counters + + # Lazily register atexit handler once per process (not per thread) + # This provides best-effort final statistics logging when processes exit normally. + # Note: atexit is unreliable in multiprocessing.Process (known Python limitation), + # so tests/critical paths should explicitly call _log_retry_stats_internal(force=True). + if pid not in _global_retry_stats.registered_pids: + _global_retry_stats.registered_pids.add(pid) + + # Register atexit handler with proper error handling + def _atexit_handler(): + try: + if _global_retry_stats: + _log_retry_stats_internal(force=True) + # Flush output to ensure atexit logs are captured + try: + sys.stdout.flush() + sys.stderr.flush() + except Exception: + pass + except Exception as e: + # Fallback: try to print error if logging infrastructure is torn down + try: + print(f"[PID {os.getpid()}] atexit handler error: {e}", flush=True) + except Exception: + pass # Silently fail if stdout is closed + + atexit.register(_atexit_handler) + + return _thread_local_stats.counters + + +def _log_retry_stats_internal(force: bool = False) -> None: + """Internal function to log retry statistics with per-thread breakdown and process-local totals. + + Statistics are aggregated across all threads within this process only. + Each process logs independently - no cross-process communication for zero overhead. + + Args: + force: If True, log cumulative lifetime stats (for atexit). + If False, log periodic snapshot of current stats (counters keep accumulating). + """ + # No-op if statistics are disabled (for performance) + if not ENABLE_RETRY_STATS or _global_retry_stats is None: + return + + current_time = time.time() + + # Quick check without lock (small race condition is acceptable here) + if not force and current_time - _global_retry_stats.last_log_time < RETRY_STATS_LOG_INTERVAL: + return + + # Now acquire lock to read stats + with _global_retry_stats.lock: + # Double-check pattern for periodic logs (skip if time hasn't elapsed) + if not force and current_time - _global_retry_stats.last_log_time < RETRY_STATS_LOG_INTERVAL: + return + + # Get cumulative stats (for final log) or aggregate per-thread stats (for periodic) + if force: + # Final log: use cumulative counters (guaranteed monotonic) + total_ops = _global_retry_stats.cumulative_operations_started + failed_ops = _global_retry_stats.cumulative_failed_operations + total_attempts = _global_retry_stats.cumulative_attempts + per_thread_stats = None # Not needed for final log + else: + # Periodic log: aggregate per-thread stats (snapshot, not cumulative) + # Note: We track per-thread stats internally for correctness (handles rare multi-threaded + # cases and ensures accurate aggregation), but only log the per-process cumulative totals. + # In typical usage, each DataLoader worker process has a single thread doing I/O. + per_thread_stats = {} + total_ops = 0 # Total operations started across all threads + failed_ops = 0 # Failed operations across all threads + total_attempts = 0 # Total attempts across all threads + + for thread_id, thread_stats in _global_retry_stats.registered_threads.items(): + pid = thread_stats["pid"] # Process ID (identifies DataLoader worker) + ops = thread_stats["operations_started"] # S3 operations started in this thread + failed = thread_stats["failed_operations"] # Operations that failed in this thread + attempts = thread_stats["total_attempts"] # All attempts (initial + retries) in this thread + + per_thread_stats[thread_id] = { + "pid": pid, + "thread_id": thread_id, + "operations_started": ops, + "failed_operations": failed, + "total_attempts": attempts, + } + + # Aggregate across all threads + total_ops += ops + failed_ops += failed + total_attempts += attempts + + if total_ops > 0: + failure_percentage = (failed_ops / total_ops) * 100 + avg_attempts_per_op = total_attempts / total_ops + + prefix = "[RetryingStream Stats - Final]" if force else "[RetryingStream Stats]" + # Include rank and PID in message (lazily cached to avoid repeated OS calls) + rank = _global_retry_stats.get_rank() + pid = _global_retry_stats.get_pid() + message = ( + f"{prefix} [Rank {rank}] [PID {pid}] PROCESS-LOCAL: {total_ops} total operations, " + f"{failed_ops} failed operations ({failure_percentage:.1f}%), " + f"avg {avg_attempts_per_op:.2f} attempts/operation" + ) + + # Always use logging infrastructure (with fallback for atexit edge cases) + try: + # Only log the cumulative per-process summary + # (Per-thread stats are still tracked internally for accuracy, just not printed) + log.warning(message, rank0_only=False) + except Exception: + # Fallback to print if logging is torn down (rare edge case during atexit) + try: + print(f"WARNING: {message}", flush=True) + except Exception: + pass # Silently fail if stdout is also closed (multiprocessing edge case) + + # Update last log time (only for periodic logs, not final) + if not force: + _global_retry_stats.last_log_time = current_time + + +def _maybe_log_retry_stats() -> None: + """Log process-local retry statistics if RETRY_STATS_LOG_INTERVAL seconds have elapsed since last log. + + Each process logs independently - no cross-process communication. + The log interval is configurable via the RETRY_STATS_LOG_INTERVAL module variable (default: 300 seconds). + """ + if not ENABLE_RETRY_STATS: + return + _log_retry_stats_internal(force=False) + + +# Throughput statistics helpers (mirrors the retry statistics helpers above) + + +def _register_throughput_atexit() -> None: + """Lazily register atexit handler once per process for final throughput log and IPC flush.""" + if _global_throughput_stats is None: + return + + pid = _global_throughput_stats.get_pid() + with _global_throughput_stats.lock: + if pid not in _global_throughput_stats.registered_pids: + _global_throughput_stats.registered_pids.add(pid) + + def _atexit_handler() -> None: + try: + if _global_throughput_stats: + _log_throughput_stats_internal(force=True) + try: + sys.stdout.flush() + sys.stderr.flush() + except Exception: + pass + except Exception as e: + try: + print(f"[PID {os.getpid()}] throughput atexit error: {e}", flush=True) + except Exception: + pass + + atexit.register(_atexit_handler) + + +def _log_throughput_stats_internal(force: bool = False) -> None: + """Log throughput statistics with process-local totals and write IPC files. + + - force=False: periodic log (if ENABLE_THROUGHPUT_STATS) + IPC write (if ENABLE_STREAM_WANDB) + - force=True: cumulative lifetime stats for atexit + final IPC write + """ + if _global_throughput_stats is None: + return + + current_time = time.time() + + if not force and current_time - _global_throughput_stats.last_log_time < THROUGHPUT_STATS_LOG_INTERVAL: + return + + with _global_throughput_stats.lock: + if not force and current_time - _global_throughput_stats.last_log_time < THROUGHPUT_STATS_LOG_INTERVAL: + return + + s = _global_throughput_stats + if force: + bytes_read = s.cumulative_bytes_read + read_time = s.cumulative_total_read_time + watchdog = s.cumulative_watchdog_reconnects + else: + bytes_read = s.cumulative_bytes_read - s._prev_bytes + read_time = s.cumulative_total_read_time - s._prev_read_time + watchdog = s.cumulative_watchdog_reconnects - s._prev_watchdog + + s._prev_bytes = s.cumulative_bytes_read + s._prev_read_time = s.cumulative_total_read_time + s._prev_watchdog = s.cumulative_watchdog_reconnects + + if bytes_read > 0: + if ENABLE_THROUGHPUT_STATS: + mb_read = bytes_read / (1024**2) + avg_mbps = mb_read / read_time if read_time > 0 else 0 + + prefix = "[Throughput Stats - Final]" if force else "[Throughput Stats]" + rank = _global_throughput_stats.get_rank() + pid = _global_throughput_stats.get_pid() + watchdog_part = f", {watchdog} watchdog reconnects" if WATCHDOG_ENABLED else "" + message = ( + f"{prefix} [Rank {rank}] [PID {pid}] PROCESS-LOCAL: " + f"{mb_read:.2f}MB in {read_time:.3f}s " + f"({avg_mbps:.1f}MB/s avg){watchdog_part}" + ) + + try: + log.warning(message, rank0_only=False) + except Exception: + try: + print(f"WARNING: {message}", flush=True) + except Exception: + pass + + if ENABLE_STREAM_WANDB: + _write_throughput_ipc() + + if not force: + _global_throughput_stats.last_log_time = current_time + + +def _maybe_log_throughput_stats() -> None: + """Log process-local throughput statistics and/or write IPC files if interval has elapsed.""" + if not ENABLE_THROUGHPUT_STATS and not ENABLE_STREAM_WANDB: + return + _log_throughput_stats_internal(force=False) + + +def _write_throughput_ipc() -> None: + """Write cumulative throughput snapshot to a per-worker IPC file (for wandb logging).""" + if _global_throughput_stats is None: + return + try: + s = _global_throughput_stats + rank = s.get_rank() + ipc_dir = Path(f"/tmp/throughput_stats/rank_{rank}") + ipc_dir.mkdir(parents=True, exist_ok=True) + filepath = ipc_dir / f"worker_{os.getpid()}.json" + tmp = filepath.with_suffix(".tmp") + with open(tmp, "w") as f: + json.dump( + { + "bytes": s.cumulative_bytes_read, + "read_time": s.cumulative_total_read_time, + "watchdog": s.cumulative_watchdog_reconnects, + "ts": time.time(), + }, + f, + ) + tmp.rename(filepath) + except Exception: + pass + + +def _is_pid_alive(pid: int) -> bool: + """Check if a process with the given PID is still running (zero-cost signal 0).""" + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +_ipc_prev_per_file: dict[str, dict[str, float]] = {} + + +def collect_throughput_ipc_stats(rank: int | None = None) -> dict[str, float]: + """Read per-worker IPC files and return accurate interval deltas for wandb. + + Deltas are tracked per file so that workers appearing (spawn) or + disappearing (death/respawn with persistent_workers=False) never corrupt + other workers' accounting. + + Dead-worker files are read first (to capture their final cumulative delta), + then deleted. Workers do NOT delete their own files on exit — they only + write a final flush via atexit. This avoids losing the last interval's data. + + Returns {"MBps": ..., "watchdog_reconnects": ...} if ENABLE_STREAM_WANDB is True, otherwise {}. + + Called by DataloadingMonitor callback once per logging window. + """ + if rank is None: + rank = int(os.environ.get("RANK", "0")) + ipc_dir = Path(f"/tmp/throughput_stats/rank_{rank}") + if not ipc_dir.exists(): + return {} + + total_d_bytes = 0 + total_d_time = 0.0 + total_d_watchdog = 0 + seen: set[str] = set() + + for filepath in ipc_dir.glob("worker_*.json"): + try: + pid = int(filepath.stem.split("_", 1)[1]) + except (ValueError, IndexError): + continue + + alive = _is_pid_alive(pid) + + try: + with open(filepath) as f: + data = json.load(f) + except (json.JSONDecodeError, OSError): + if not alive: + try: + filepath.unlink(missing_ok=True) + except OSError: + pass + continue + + fname = filepath.name + seen.add(fname) + + cur = { + "bytes": data.get("bytes", 0), + "read_time": data.get("read_time", 0.0), + "watchdog": data.get("watchdog", 0), + } + prev = _ipc_prev_per_file.get(fname, {"bytes": 0, "read_time": 0.0, "watchdog": 0}) + + d_b = cur["bytes"] - prev["bytes"] + d_t = cur["read_time"] - prev["read_time"] + d_w = cur["watchdog"] - prev["watchdog"] + + if d_b < 0 or d_t < 0 or d_w < 0: + log.info( + f"[Stream IPC] PID reuse detected for {fname}, treating as fresh worker", + rank0_only=False, + ) + d_b, d_t, d_w = cur["bytes"], cur["read_time"], cur["watchdog"] + + total_d_bytes += d_b + total_d_time += d_t + total_d_watchdog += d_w + _ipc_prev_per_file[fname] = cur + + if not alive: + try: + filepath.unlink(missing_ok=True) + log.info( + f"[Stream IPC] Read final stats and removed file for dead worker PID {pid}: {fname}", + rank0_only=False, + ) + except OSError: + pass + + for fname in list(_ipc_prev_per_file): + if fname not in seen: + log.info(f"[Stream IPC] Purging stale tracking entry: {fname}", rank0_only=False) + del _ipc_prev_per_file[fname] + + result: dict[str, float] = { + "MBps": (total_d_bytes / (1024**2)) / total_d_time if total_d_time > 0 else 0, + } + if WATCHDOG_ENABLED: + result["watchdog_reconnects"] = float(total_d_watchdog) + return result + + +@dataclass +class WatchdogConfig: + """Configuration for the throughput watchdog that resets stream connections with sustained low throughput. + + Attributes: + enabled: Master switch. Controlled by env var ``RETRYING_STREAM_WATCHDOG`` + (``"0"`` to disable; default enabled). + min_throughput_mbps: Sustained throughput threshold in MB/s. If the moving + window average drops below this, the connection is reset. Controlled by env var ``RETRYING_STREAM_WATCHDOG_MIN_MBPS`` (default ``50.0``). + min_window_seconds: Minimum accumulated read time (seconds) in the current + window before a throughput check is meaningful. Prevents premature resets. + check_interval: Number of ``read()`` calls between throughput checks to avoid checking overhead. + """ + + enabled: bool = WATCHDOG_ENABLED + min_throughput_mbps: float = WATCHDOG_MIN_THROUGHPUT_MBPS + min_window_seconds: float = 5.0 + check_interval: int = 50 + + +class RetryingStream: + def __init__(self, client: boto3.client, bucket: str, key: str, retries: int = 10): # type: ignore + r"""Class for loading data in a streaming fashion. + Args: + client (boto3.client): Boto3 client + bucket (str): Bucket where data is stored + key (str): Key to read + retries (int): Number of retries + """ + self.client = client + self.bucket = bucket + self.key = key + self.retries = retries + self.name = f"{bucket}/{key}" + + # Cache stats flag as instance variable to avoid module lookup overhead + self._enable_retry_stats = ENABLE_RETRY_STATS + self._enable_throughput_stats = ENABLE_THROUGHPUT_STATS + self._enable_tracking = ENABLE_THROUGHPUT_STATS or ENABLE_STREAM_WANDB + + # Get content length (with retries for transient failures) + self.content_size = self._retry_operation( + operation=self.get_length, + operation_name="get_length", + max_attempts=self.retries, + ) + + # Get initial stream (with retries for transient failures) + self.stream, _ = self._retry_operation( + operation=self.get_stream, + operation_name="get_stream", + max_attempts=self.retries, + ) + + self._amount_read = 0 + + # Per-shard read timing (accumulated across all read() calls) + self._stream_read_time = 0.0 + + self._watchdog = WatchdogConfig() + self._read_count = 0 + self._window_start_read_time: float = 0.0 + self._window_start_bytes: int = 0 + + if self._enable_retry_stats: + with _global_retry_stats.lock: + _global_retry_stats.active_instances.add(self) + + if self._enable_tracking: + _register_throughput_atexit() + + def __del__(self) -> None: + r"""Destructor for cleanup. + + Note: WeakSet automatically removes dead references, so no manual cleanup needed. + Final statistics are logged by the atexit handler when the program exits. + """ + # WeakSet handles cleanup automatically - no action needed + # Final stats logging happens via atexit handler, not destructor + pass + + def _watchdog_reset_stream_if_low_throughput(self, new_position: int) -> None: + """Reset the stream connection if sustained throughput drops below a threshold. + + Cloud object-storage backends (especially GCS) occasionally serve individual connections at far below their healthy capacity (tail latency problem). + + Because the DataLoader blocks on the slowest worker, a single degraded connection can + bottleneck the entire training step, observed as `dataloading spikes` in the training charts. + + This mitigation abandons the slow connection and opens a fresh one from the byte offset where the previous stream left off. + This is proven not to lose bytes (reconnection continues from where the previous stream left off), and doesn't introduce overhead. + + The check runs every `WatchdogConfig.check_interval` read() calls. + It computes a moving-window throughput (bytes read / accumulated read time) and compares it against `WatchdogConfig.min_throughput_mbps`. + A minimum window read time (`WatchdogConfig.min_window_seconds`) prevents premature resets. After a reset, the window counters restart from the current position. + + When disabled (`RETRYING_STREAM_WATCHDOG=0`), this method returns immediately on the first check. + """ + wd = self._watchdog + if ( + not wd.enabled + or self._read_count % wd.check_interval != 0 + or self._read_count == 0 + or new_position >= self.content_size + ): + return + + window_read_time = self._stream_read_time - self._window_start_read_time + window_bytes = new_position - self._window_start_bytes + if window_read_time <= wd.min_window_seconds or window_bytes <= 0: + return + + throughput_mbps = (window_bytes / (1024 * 1024)) / window_read_time + if throughput_mbps >= wd.min_throughput_mbps: + return + + if self._enable_tracking: + with _global_throughput_stats.lock: + _global_throughput_stats.cumulative_watchdog_reconnects += 1 + + if self._enable_throughput_stats: + rank = _global_throughput_stats.get_rank() + pid = _global_throughput_stats.get_pid() + log.warning( + f"[Throughput Watchdog] [Rank {rank}] [PID {pid}] reconnecting: " + f"{throughput_mbps:.1f}MB/s < {wd.min_throughput_mbps}MB/s, " + f"read_time {window_read_time:.1f}s, " + f"{self.name} @ {new_position}/{self.content_size}", + rank0_only=False, + ) + + try: + self.stream, _ = self.get_stream(new_position) + except (EndpointConnectionError, ClientError) as e: + log.warning( + f"[Throughput Watchdog] reconnect failed: {e} {self.name}", + rank0_only=False, + ) + self._window_start_read_time = self._stream_read_time + self._window_start_bytes = new_position + + @staticmethod + def _exponential_backoff_sleep(attempt: int) -> None: + r"""Sleep with exponential backoff based on attempt number. + + Args: + attempt: Zero-indexed attempt number (0 for first retry) + """ + time.sleep(0.5 * 2**attempt) + + def _retry_operation(self, operation, operation_name: str, max_attempts: int = 3): + r"""Retry an operation with exponential backoff for transient failures. + + Args: + operation: Callable to execute + operation_name: Name of operation for logging + max_attempts: Maximum number of attempts + + Returns: + Result of the operation + + Raises: + Exception from the operation if all retries fail + """ + # Track this operation in both thread-local and cumulative statistics + if self._enable_retry_stats: + _maybe_log_retry_stats() # Check if periodic log is due + + # Track this operation (lock-free thread-local counters) + stats = _get_thread_stats() + stats["operations_started"] += 1 # Count this S3 operation being started + stats["total_attempts"] += 1 # Count the initial attempt + + # Also update cumulative counters (requires lock because += is not atomic) + # Lock overhead is negligible: uncontended in single-threaded case, minimal contention in multi-threaded + with _global_retry_stats.lock: + _global_retry_stats.cumulative_operations_started += 1 + _global_retry_stats.cumulative_attempts += 1 + else: + stats = None + + # Include EndpointConnectionError for initialization operations + init_retryable = RETRYABLE_EXCEPTIONS + (EndpointConnectionError,) + + operation_had_retry = False # Track if this operation failed at least once + for attempt in range(max_attempts): + try: + return operation() + except init_retryable as e: + if attempt == max_attempts - 1: # Last attempt + raise + + # Track retry statistics + if stats is not None: + # Mark this operation as failed (only once per operation, lock-free) + if not operation_had_retry: + stats["failed_operations"] += 1 # This operation failed at least once + operation_had_retry = True + # Also update cumulative counter (lock needed because += is not atomic) + with _global_retry_stats.lock: + _global_retry_stats.cumulative_failed_operations += 1 + + # Count this retry attempt (lock-free) + stats["total_attempts"] += 1 # Each retry is an additional attempt + + # Also update cumulative counter (lock needed because += is not atomic) + with _global_retry_stats.lock: + _global_retry_stats.cumulative_attempts += 1 + + # Only log retries after the first one (attempt >= 1) + if attempt >= 1: + log.warning( + f"Transient error in {operation_name} for {self.name} " + f"(attempt {attempt + 1}/{max_attempts}): {type(e).__name__}: {e}", + rank0_only=False, + ) + self._exponential_backoff_sleep(attempt) + + def get_length(self) -> int: + r"""Function for obtaining length of the bytestream""" + head_obj = self.client.head_object(Bucket=self.bucket, Key=self.key) + length = int(head_obj["ContentLength"]) + return length + + def get_stream(self, start_range: int = 0, end_range: Optional[int] = None): + r"""Function for getting stream in a range + Args: + start_range (int): Start index for stream + end_range (int): End index for stream + Returns: + stream (bytes): Stream of data being read + content_size (int): Length of the bytestream read + """ + extra_args = {} + if start_range != 0 or end_range is not None: + # End range in S3 is inclusive + end_str = "" if end_range is None else str(end_range - 1) + extra_args["Range"] = f"bytes={start_range}-{end_str}" + + response = self.client.get_object(Bucket=self.bucket, Key=self.key, **extra_args) + + # FIX: Use the public 'Body' property (StreamingBody) + # It implements .read() and handles internal resource management + return response["Body"], int(response["ContentLength"]) + + def read(self, amt: Optional[int] = None) -> bytes: + r"""Function for reading data from the stream + Args: + amt (int): Amount of data to read + Returns: + chunk (bytes): Data read from the stream + """ + # Track this operation in both thread-local and cumulative statistics + if self._enable_retry_stats: + _maybe_log_retry_stats() # Check if periodic log is due + + # Track this read operation (lock-free thread-local counters) + stats = _get_thread_stats() + stats["operations_started"] += 1 # Count this read() call being started + stats["total_attempts"] += 1 # Count the initial attempt + + # Also update cumulative counters (requires lock) + with _global_retry_stats.lock: + _global_retry_stats.cumulative_operations_started += 1 + _global_retry_stats.cumulative_attempts += 1 + else: + stats = None + + operation_had_retry = False # Track if this read() failed at least once + for cur_retry_idx in range(self.retries): + try: + t_read_start = time.monotonic() + chunk = self.stream.read(amt) + read_dur = time.monotonic() - t_read_start + self._stream_read_time += read_dur # always: used by watchdog + if self._enable_tracking: + with _global_throughput_stats.lock: + _global_throughput_stats.cumulative_bytes_read += len(chunk) + _global_throughput_stats.cumulative_total_read_time += read_dur + self._read_count += 1 + # Check for unexpected end of stream + if amt is not None and amt > 0 and len(chunk) == 0 and self._amount_read != self.content_size: + raise IOError("Premature end of stream detected.") + + # Throughput watchdog + # Periodically check if sustained throughput is too low. + # If so, abandon the slow connection and open a fresh one from where we left off. + new_position = self._amount_read + len(chunk) + self._watchdog_reset_stream_if_low_throughput(new_position) + + # Success: Update pointer and return + self._amount_read += len(chunk) + if self._enable_tracking: + _maybe_log_throughput_stats() + return chunk + + except RETRYABLE_EXCEPTIONS as e: + self._stream_read_time += time.monotonic() - t_read_start + # Track retry statistics + if stats is not None: + # Mark this operation as failed (only once per operation, lock-free) + if not operation_had_retry: + stats["failed_operations"] += 1 # This operation failed at least once + operation_had_retry = True + # Also update cumulative counter (lock needed because += is not atomic) + with _global_retry_stats.lock: + _global_retry_stats.cumulative_failed_operations += 1 + + # Count this retry attempt (lock-free) + stats["total_attempts"] += 1 # Each retry is an additional attempt + + # Also update cumulative counter (lock needed because += is not atomic) + with _global_retry_stats.lock: + _global_retry_stats.cumulative_attempts += 1 + + # Only log retries after the first one (cur_retry_idx >= 1) + if cur_retry_idx >= 1: + log.warning( + f"[read] {type(e).__name__}: {e} {self.name} retry: {cur_retry_idx + 1}/{self.retries}", + rank0_only=False, + ) + + if cur_retry_idx == self.retries - 1: + raise # Re-raise the last exception if all retries fail + + # Exponential backoff: 0.5s, 1s, 2s, 4s, 8s... + self._exponential_backoff_sleep(cur_retry_idx) + + try: + # Close the old stream to prevent resource leaks + if hasattr(self.stream, "close"): + self.stream.close() + # Re-establish the stream from the last successful byte + self.stream, _ = self.get_stream(self._amount_read) + except RETRYABLE_EXCEPTIONS + (EndpointConnectionError,) as e_conn: + # Only log reconnection failures after the first retry + if cur_retry_idx >= 1: + log.warning( + f"Failed to reconnect on attempt {cur_retry_idx + 1}/{self.retries}: " + f"{type(e_conn).__name__}: {e_conn}", + rank0_only=False, + ) + # Loop continues, will retry the entire read operation (including get_stream) next iteration + # Note: self.stream may be in a bad state, but we'll create a fresh one on next iteration + + return b"" # Should theoretically not reach here due to the raise diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py new file mode 100644 index 00000000..a657161e --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py @@ -0,0 +1,231 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# ----------------------------------------------------------------------------- + +"""Test RetryingStream statistics with PyTorch DataLoader workers. + +This test demonstrates that RetryingStream statistics work correctly +with PyTorch DataLoader's multiprocessing workers, which is the typical +production usage pattern. + +Key points tested: +1. Each DataLoader worker process maintains independent statistics +2. Thread-local storage works correctly within each worker +3. Statistics are properly aggregated within each worker process +4. No cross-worker interference or shared state issues +""" + +import sys +import time +from http.client import IncompleteRead +from unittest.mock import MagicMock + +from torch.utils.data import DataLoader, Dataset + +import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream +from cosmos3._src.imaginaire.utils import log + +# Configure faster logging interval for tests (10 seconds instead of 5 minutes) +stream_module.RETRY_STATS_LOG_INTERVAL = 10.0 + + +class MockS3Dataset(Dataset): + """Mock dataset that uses RetryingStream to simulate S3 streaming.""" + + def __init__(self, num_samples: int, retry_rate: float = 0.2): + """Initialize mock dataset. + + Args: + num_samples: Number of samples in the dataset + retry_rate: Fraction of samples that will trigger a retry + """ + self.num_samples = num_samples + self.retry_rate = retry_rate + # Enable statistics + stream_module.ENABLE_RETRY_STATS = True + + def __len__(self) -> int: + return self.num_samples + + def __getitem__(self, idx: int): + """Get a sample, simulating S3 streaming with RetryingStream.""" + # Create mock S3 client + client = MagicMock() + test_data = b"X" * 1024 + + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + # Simulate retry for some samples + mock_body = MagicMock() + if (idx % int(1 / self.retry_rate)) == 0: + # This sample will fail once then succeed + mock_body.read.side_effect = [IncompleteRead(b"partial"), test_data] + else: + # This sample succeeds immediately + mock_body.read.return_value = test_data + + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + # Use RetryingStream (this is what happens in production) + stream = RetryingStream(client, f"test-bucket", f"file-{idx}.tar", retries=5) + + try: + data = stream.read(1024) + return {"idx": idx, "data": data, "size": len(data)} + except Exception as e: + return {"idx": idx, "error": str(e)} + + +def test_dataloader_workers(): + """Test that statistics work correctly with PyTorch DataLoader workers.""" + print("\n" + "=" * 70) + print("DATALOADER WORKER TEST") + print("=" * 70) + + # Test configuration + num_samples = 100 + batch_size = 10 + num_workers = 4 # This creates 4 separate worker processes + retry_rate = 0.2 # 20% of samples will retry + + print(f"Configuration:") + print(f" Dataset size: {num_samples} samples") + print(f" Batch size: {batch_size}") + print(f" Num workers: {num_workers} (separate processes)") + print(f" Retry rate: {retry_rate * 100}%") + print("-" * 70) + + # Create dataset and dataloader + dataset = MockS3Dataset(num_samples=num_samples, retry_rate=retry_rate) + dataloader = DataLoader(dataset, batch_size=batch_size, num_workers=num_workers, shuffle=False, drop_last=False) + + print("\nStarting DataLoader iteration...") + print("(Each worker process maintains independent statistics)") + print("-" * 70) + + # Process all batches + start_time = time.time() + total_samples = 0 + errors = 0 + + for batch_idx, batch in enumerate(dataloader): + batch_size_actual = len(batch["idx"]) + total_samples += batch_size_actual + + # Check for errors + if "error" in batch: + for error in batch["error"]: + if error: + errors += 1 + + # Print progress every 5 batches + if (batch_idx + 1) % 5 == 0: + print(f" Processed {total_samples}/{num_samples} samples...") + + elapsed = time.time() - start_time + print(f"\n✓ Completed in {elapsed:.2f}s") + print(f" Total samples processed: {total_samples}") + print(f" Errors: {errors}") + print("-" * 70) + + # Calculate expected retries + expected_retries = sum(1 for i in range(num_samples) if i % int(1 / retry_rate) == 0) + + print("\nExpected behavior:") + print(f" Each worker process had its own _global_retry_stats instance") + print(f" Each worker independently tracked its subset of {num_samples // num_workers}~ samples") + print(f" Total retries across all workers: ~{expected_retries}") + print(f" Per-worker retries: ~{expected_retries // num_workers}") + + print("\nNote: Statistics are logged per-worker-process during iteration.") + print(" Check the output above for '[RetryingStream Stats]' messages.") + print("=" * 70) + + # Verify no errors + if errors > 0: + print(f"\n❌ FAIL: {errors} errors occurred during processing") + return False + else: + print(f"\n✅ PASS: DataLoader workers processed all samples successfully") + print(" ✓ Each worker maintained independent statistics") + print(" ✓ No cross-worker interference") + print(" ✓ Thread-local storage worked correctly") + return True + + +def test_dataloader_workers_with_threading(): + """Test DataLoader with threading backend (less common, but valid).""" + print("\n" + "=" * 70) + print("DATALOADER THREADING BACKEND TEST") + print("=" * 70) + + # Note: torch.multiprocessing with threads is less common but supported + # This tests the threading.local() aggregation within a single process + num_samples = 50 + batch_size = 5 + retry_rate = 0.2 + + print(f"Configuration:") + print(f" Dataset size: {num_samples} samples") + print(f" Batch size: {batch_size}") + print(f" Threading backend (single process, multiple threads)") + print(f" Retry rate: {retry_rate * 100}%") + print("-" * 70) + + # Create dataset and dataloader with threading (num_workers=0 uses main thread) + dataset = MockS3Dataset(num_samples=num_samples, retry_rate=retry_rate) + + # Process in main thread (num_workers=0) + dataloader = DataLoader(dataset, batch_size=batch_size, num_workers=0, shuffle=False) + + print("\nProcessing in main thread...") + total_samples = 0 + for batch in dataloader: + total_samples += len(batch["idx"]) + + print(f"✓ Processed {total_samples}/{num_samples} samples") + + # Force a stats log + if stream_module.ENABLE_RETRY_STATS: + with stream_module._global_retry_stats.lock: + stream_module._global_retry_stats.last_log_time = 0 + stream_module._maybe_log_retry_stats() + + print("-" * 70) + print("✅ PASS: Single-threaded DataLoader worked correctly") + print("=" * 70) + return True + + +if __name__ == "__main__": + # Initialize logging + log.init_loguru_stdout() + + # Run tests + test1_passed = test_dataloader_workers() + print("\n\n") + time.sleep(1) # Brief pause between tests + + test2_passed = test_dataloader_workers_with_threading() + + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + print(f" DataLoader Workers (multiprocessing): {'✅ PASS' if test1_passed else '❌ FAIL'}") + print(f" DataLoader Threading (single process): {'✅ PASS' if test2_passed else '❌ FAIL'}") + print("=" * 70) + + if test1_passed and test2_passed: + print("\n✅ All DataLoader tests PASSED!") + print("\nVerified:") + print(" ✓ Statistics work with PyTorch DataLoader workers (multiprocessing)") + print(" ✓ Statistics work with single-threaded DataLoader") + print(" ✓ Each worker process maintains independent statistics") + print(" ✓ No cross-worker shared state issues") + print(" ✓ Thread-local aggregation works correctly") + sys.exit(0) + else: + print("\n❌ Some DataLoader tests FAILED") + sys.exit(1) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py new file mode 100644 index 00000000..88505fa6 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py @@ -0,0 +1,452 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from http.client import IncompleteRead +from unittest.mock import MagicMock, patch + +from botocore.exceptions import EndpointConnectionError, ResponseStreamingError +from urllib3.exceptions import ProtocolError as URLLib3ProtocolError +from urllib3.exceptions import ReadTimeoutError as URLLib3ReadTimeoutError +from urllib3.exceptions import SSLError as URLLib3SSLError + +import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream + +# Configure faster logging interval for tests (10 seconds instead of 5 minutes) +stream_module.RETRY_STATS_LOG_INTERVAL = 10.0 + +# Test 1: Simulate IncompleteRead and verify retry works +print("Test 1: Retry on IncompleteRead") +client = MagicMock() +expected_data = b"X" * 100 # 100 bytes of data +client.head_object.return_value = {"ContentLength": str(len(expected_data))} + +# Create mock streams +mock_body_1 = MagicMock() +mock_body_1.close = MagicMock() # Track if close() is called +mock_body_1.read.side_effect = IncompleteRead(b"partial") # First read fails + +mock_body_2 = MagicMock() +mock_body_2.read.return_value = expected_data # Second read succeeds with full data + +# Return different bodies on each get_object call +client.get_object.side_effect = [ + {"Body": mock_body_1, "ContentLength": len(expected_data)}, # First attempt + {"Body": mock_body_2, "ContentLength": len(expected_data)}, # Retry attempt +] + +stream = RetryingStream(client, "test-bucket", "test.tar", retries=3) + +# Mock time.sleep to skip waiting +with patch("time.sleep"): + data = stream.read(100) + +assert data == expected_data, f"Expected {len(expected_data)} bytes but got {len(data)} bytes" +assert len(data) == 100, f"Expected 100 bytes but got {len(data)}" +assert mock_body_1.close.called, "Old stream was not closed" +assert client.get_object.call_count == 2, f"Expected 2 calls but got {client.get_object.call_count}" +print(f"✓ Read succeeded after retry: {len(data)} bytes") +print(f"✓ Old stream was closed: {mock_body_1.close.called}") +print(f"✓ get_object called {client.get_object.call_count} times (initial + retry)") + +# Test 2: Multiple errors before success +print("\nTest 2: Multiple retries before success") +client2 = MagicMock() +expected_data2 = b"Y" * 200 # 200 bytes of data +client2.head_object.return_value = {"ContentLength": str(len(expected_data2))} + +# Create multiple failing streams and one success +failing_bodies = [] +for i in range(2): + body = MagicMock() + body.close = MagicMock() + body.read.side_effect = IncompleteRead(b"fail") + failing_bodies.append(body) + +success_body = MagicMock() +success_body.read.return_value = expected_data2 + +client2.get_object.side_effect = [ + {"Body": failing_bodies[0], "ContentLength": len(expected_data2)}, + {"Body": failing_bodies[1], "ContentLength": len(expected_data2)}, + {"Body": success_body, "ContentLength": len(expected_data2)}, +] + +stream2 = RetryingStream(client2, "test-bucket", "test.tar", retries=5) + +with patch("time.sleep"): + data2 = stream2.read(200) + +assert data2 == expected_data2, f"Expected {len(expected_data2)} bytes but got {len(data2)} bytes" +assert len(data2) == 200, f"Expected 200 bytes but got {len(data2)}" +assert failing_bodies[0].close.called, "First stream was not closed" +assert failing_bodies[1].close.called, "Second stream was not closed" +assert client2.get_object.call_count == 3, f"Expected 3 calls but got {client2.get_object.call_count}" +print(f"✓ Read succeeded after {client2.get_object.call_count - 1} retries: {len(data2)} bytes") +print(f"✓ First stream closed: {failing_bodies[0].close.called}") +print(f"✓ Second stream closed: {failing_bodies[1].close.called}") + +# Test 3: Max retries exceeded +print("\nTest 3: Max retries exceeded") +client3 = MagicMock() +expected_size3 = 150 +client3.head_object.return_value = {"ContentLength": str(expected_size3)} + +# Always fail +always_fail_body = MagicMock() +always_fail_body.close = MagicMock() +always_fail_body.read.side_effect = IncompleteRead(b"always fail") + +client3.get_object.return_value = {"Body": always_fail_body, "ContentLength": expected_size3} + +stream3 = RetryingStream(client3, "test-bucket", "test.tar", retries=2) + +exception_raised = False +try: + with patch("time.sleep"): + data3 = stream3.read(100) + print("✗ Should have raised exception!") + assert False, "Expected IncompleteRead exception to be raised" +except IncompleteRead: + exception_raised = True + print(f"✓ Correctly raised IncompleteRead after {stream3.retries} retries") + +assert exception_raised, "Exception was not raised when it should have been" + +# Test 4: Mix different error types +print("\nTest 4: Mixed error types (IncompleteRead + URLLib3ReadTimeoutError)") +client4 = MagicMock() +expected_data4 = b"Z" * 250 # 250 bytes +client4.head_object.return_value = {"ContentLength": str(len(expected_data4))} + +error_body_1 = MagicMock() +error_body_1.close = MagicMock() +error_body_1.read.side_effect = IncompleteRead(b"incomplete") + +error_body_2 = MagicMock() +error_body_2.close = MagicMock() +error_body_2.read.side_effect = URLLib3ReadTimeoutError(None, None, "timeout") + +success_body_2 = MagicMock() +success_body_2.read.return_value = expected_data4 + +client4.get_object.side_effect = [ + {"Body": error_body_1, "ContentLength": len(expected_data4)}, + {"Body": error_body_2, "ContentLength": len(expected_data4)}, + {"Body": success_body_2, "ContentLength": len(expected_data4)}, +] + +stream4 = RetryingStream(client4, "test-bucket", "test.tar", retries=5) + +with patch("time.sleep"): + data4 = stream4.read(250) + +assert data4 == expected_data4, f"Expected {len(expected_data4)} bytes but got {len(data4)} bytes" +assert len(data4) == 250, f"Expected 250 bytes but got {len(data4)}" +assert error_body_1.close.called, "First error stream was not closed" +assert error_body_2.close.called, "Second error stream was not closed" +assert client4.get_object.call_count == 3, f"Expected 3 calls but got {client4.get_object.call_count}" +print(f"✓ Recovered from mixed errors: {len(data4)} bytes") +print(f"✓ Both error streams were closed: {error_body_1.close.called and error_body_2.close.called}") + +# Test 5: URLLib3ProtocolError +print("\nTest 5: Retry on URLLib3ProtocolError") +client5 = MagicMock() +expected_data5 = b"A" * 128 +client5.head_object.return_value = {"ContentLength": str(len(expected_data5))} + +error_body_5 = MagicMock() +error_body_5.close = MagicMock() +error_body_5.read.side_effect = URLLib3ProtocolError("Connection broken") + +success_body_5 = MagicMock() +success_body_5.read.return_value = expected_data5 + +client5.get_object.side_effect = [ + {"Body": error_body_5, "ContentLength": len(expected_data5)}, + {"Body": success_body_5, "ContentLength": len(expected_data5)}, +] + +stream5 = RetryingStream(client5, "test-bucket", "test.tar", retries=3) + +with patch("time.sleep"): + data5 = stream5.read(128) + +assert data5 == expected_data5, f"Expected {len(expected_data5)} bytes but got {len(data5)} bytes" +assert error_body_5.close.called, "Error stream was not closed" +assert client5.get_object.call_count == 2, f"Expected 2 calls but got {client5.get_object.call_count}" +print(f"✓ Recovered from ProtocolError: {len(data5)} bytes") + +# Test 6: URLLib3SSLError +print("\nTest 6: Retry on URLLib3SSLError") +client6 = MagicMock() +expected_data6 = b"B" * 256 +client6.head_object.return_value = {"ContentLength": str(len(expected_data6))} + +error_body_6 = MagicMock() +error_body_6.close = MagicMock() +error_body_6.read.side_effect = URLLib3SSLError("SSL handshake failed") + +success_body_6 = MagicMock() +success_body_6.read.return_value = expected_data6 + +client6.get_object.side_effect = [ + {"Body": error_body_6, "ContentLength": len(expected_data6)}, + {"Body": success_body_6, "ContentLength": len(expected_data6)}, +] + +stream6 = RetryingStream(client6, "test-bucket", "test.tar", retries=3) + +with patch("time.sleep"): + data6 = stream6.read(256) + +assert data6 == expected_data6, f"Expected {len(expected_data6)} bytes but got {len(data6)} bytes" +assert error_body_6.close.called, "Error stream was not closed" +assert client6.get_object.call_count == 2, f"Expected 2 calls but got {client6.get_object.call_count}" +print(f"✓ Recovered from SSLError: {len(data6)} bytes") + +# Test 7: Generic IOError +print("\nTest 7: Retry on generic IOError") +client7 = MagicMock() +expected_data7 = b"C" * 512 +client7.head_object.return_value = {"ContentLength": str(len(expected_data7))} + +error_body_7 = MagicMock() +error_body_7.close = MagicMock() +error_body_7.read.side_effect = IOError("Generic IO error") + +success_body_7 = MagicMock() +success_body_7.read.return_value = expected_data7 + +client7.get_object.side_effect = [ + {"Body": error_body_7, "ContentLength": len(expected_data7)}, + {"Body": success_body_7, "ContentLength": len(expected_data7)}, +] + +stream7 = RetryingStream(client7, "test-bucket", "test.tar", retries=3) + +with patch("time.sleep"): + data7 = stream7.read(512) + +assert data7 == expected_data7, f"Expected {len(expected_data7)} bytes but got {len(data7)} bytes" +assert error_body_7.close.called, "Error stream was not closed" +assert client7.get_object.call_count == 2, f"Expected 2 calls but got {client7.get_object.call_count}" +print(f"✓ Recovered from IOError: {len(data7)} bytes") + +# Test 8: Premature end of stream detection +print("\nTest 8: Premature end of stream detection") +client8 = MagicMock() +expected_data8 = b"D" * 1024 +client8.head_object.return_value = {"ContentLength": str(len(expected_data8))} + +# First body returns empty when we expect data (premature end) +premature_body = MagicMock() +premature_body.close = MagicMock() +premature_body.read.return_value = b"" # Empty read when we expect data + +success_body_8 = MagicMock() +success_body_8.read.return_value = expected_data8 + +client8.get_object.side_effect = [ + {"Body": premature_body, "ContentLength": len(expected_data8)}, + {"Body": success_body_8, "ContentLength": len(expected_data8)}, +] + +stream8 = RetryingStream(client8, "test-bucket", "test.tar", retries=3) + +with patch("time.sleep"): + data8 = stream8.read(1024) + +assert data8 == expected_data8, f"Expected {len(expected_data8)} bytes but got {len(data8)} bytes" +assert premature_body.close.called, "Premature stream was not closed" +assert client8.get_object.call_count == 2, f"Expected 2 calls but got {client8.get_object.call_count}" +print(f"✓ Recovered from premature end of stream: {len(data8)} bytes") + +# Test 9: EndpointConnectionError during reconnection +print("\nTest 9: EndpointConnectionError during reconnection (continues retry loop)") +client9 = MagicMock() +expected_data9 = b"E" * 2048 +client9.head_object.return_value = {"ContentLength": str(len(expected_data9))} + +# First read fails +error_body_9a = MagicMock() +error_body_9a.close = MagicMock() +error_body_9a.read.side_effect = URLLib3ReadTimeoutError(None, None, "timeout") + +# First reconnection attempt fails with EndpointConnectionError +# Second read also fails +error_body_9b = MagicMock() +error_body_9b.close = MagicMock() +error_body_9b.read.side_effect = URLLib3ReadTimeoutError(None, None, "timeout") + +# Final success +success_body_9 = MagicMock() +success_body_9.read.return_value = expected_data9 + +# Simulate EndpointConnectionError on first reconnection attempt, then succeed +client9.get_object.side_effect = [ + {"Body": error_body_9a, "ContentLength": len(expected_data9)}, # Initial read + EndpointConnectionError(endpoint_url="https://s3.amazonaws.com"), # Reconnection fails + {"Body": error_body_9b, "ContentLength": len(expected_data9)}, # Second attempt after endpoint error + {"Body": success_body_9, "ContentLength": len(expected_data9)}, # Final success +] + +stream9 = RetryingStream(client9, "test-bucket", "test.tar", retries=5) + +with patch("time.sleep"): + data9 = stream9.read(2048) + +assert data9 == expected_data9, f"Expected {len(expected_data9)} bytes but got {len(data9)} bytes" +assert error_body_9a.close.called, "First error stream was not closed" +assert error_body_9b.close.called, "Second error stream was not closed" +# Should be called 4 times: initial + endpoint error + retry after endpoint + final success +assert client9.get_object.call_count == 4, f"Expected 4 calls but got {client9.get_object.call_count}" +print(f"✓ Recovered from EndpointConnectionError during reconnection: {len(data9)} bytes") + +# Test 10: ResponseStreamingError during reconnection (now FIXED) +print("\nTest 10: ResponseStreamingError during reconnection - should retry") +client10 = MagicMock() +expected_data10 = b"F" * 1024 +client10.head_object.return_value = {"ContentLength": str(len(expected_data10))} + +# First read fails with IncompleteRead +error_body_10a = MagicMock() +error_body_10a.close = MagicMock() +error_body_10a.read.side_effect = IncompleteRead(b"incomplete") + +# Reconnection fails with ResponseStreamingError (wrapping IncompleteRead) +reconnect_error = ResponseStreamingError( + error=IncompleteRead(b"x" * 97727), msg="Connection broken: IncompleteRead(97727 bytes read, 143937 more expected)" +) + +# Second attempt after reconnection succeeds +success_body_10 = MagicMock() +success_body_10.read.return_value = expected_data10 + +# Simulate: read fails → reconnect fails with ResponseStreamingError → retry succeeds +client10.get_object.side_effect = [ + {"Body": error_body_10a, "ContentLength": len(expected_data10)}, # Initial read + reconnect_error, # First reconnection fails with ResponseStreamingError (now caught!) + {"Body": success_body_10, "ContentLength": len(expected_data10)}, # Second reconnection succeeds +] + +stream10 = RetryingStream(client10, "test-bucket", "test.tar", retries=5) + +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + data10 = stream10.read(1024) + +# Verify the fix worked +assert data10 == expected_data10, f"Expected {len(expected_data10)} bytes but got {len(data10)} bytes" +assert error_body_10a.close.called, "First error stream was not closed" +assert client10.get_object.call_count == 3, f"Expected 3 calls but got {client10.get_object.call_count}" +print(f"✓ Recovered from ResponseStreamingError during reconnection: {len(data10)} bytes") + +# Test 11: Failure during __init__ get_length() - now WITH retry logic +print("\nTest 11: Failure during __init__ get_length() - retries 3 times then fails") +client11 = MagicMock() + +# head_object fails with ResponseStreamingError on all attempts +client11.head_object.side_effect = ResponseStreamingError( + error=IncompleteRead(b"fail"), msg="Connection broken during head_object" +) + +test11_exception = None +with patch("time.sleep"): # Skip sleep delays in test + try: + stream11 = RetryingStream(client11, "test-bucket", "test.tar", retries=5) + print("✗ Should have raised exception after retries exhausted") + except ResponseStreamingError as e: + test11_exception = e + print(f"✓ ResponseStreamingError raised after retries exhausted") + print(f" Error: {e}") + print(f" head_object was called {client11.head_object.call_count} time(s)") + +assert test11_exception is not None, "Should have raised exception after retries exhausted" +assert client11.head_object.call_count == 5, "Should have retried 5 times (retries=5)" + + +# Test 12: Failure during __init__ get_stream() - now WITH retry logic +print("\nTest 12: Failure during __init__ get_stream() - retries 3 times then fails") +client12 = MagicMock() +client12.head_object.return_value = {"ContentLength": "1024"} + +# get_object fails with ResponseStreamingError during initial stream creation on all attempts +client12.get_object.side_effect = ResponseStreamingError( + error=IncompleteRead(b"fail"), msg="Connection broken during initial get_object" +) + +test12_exception = None +with patch("time.sleep"): # Skip sleep delays in test + try: + stream12 = RetryingStream(client12, "test-bucket", "test.tar", retries=5) + print("✗ Should have raised exception after retries exhausted") + except ResponseStreamingError as e: + test12_exception = e + print(f"✓ ResponseStreamingError raised after retries exhausted") + print(f" Error: {e}") + print(f" get_object was called {client12.get_object.call_count} time(s)") + +assert test12_exception is not None, "Should have raised exception after retries exhausted" +assert client12.get_object.call_count == 5, "Should have retried 5 times (retries=5)" + + +# Test 13: Transient failure during __init__ get_stream() on first attempt, success on retry +print("\nTest 13: Transient failure during __init__ - now succeeds with retry logic!") +client13 = MagicMock() +expected_data13 = b"G" * 512 +client13.head_object.return_value = {"ContentLength": str(len(expected_data13))} + +# First get_object fails, second succeeds (showing network blip during initialization) +success_body_13 = MagicMock() +success_body_13.read.return_value = expected_data13 + +get_object_call_count = [0] + + +def get_object_with_initial_failure(**kwargs): + get_object_call_count[0] += 1 + if get_object_call_count[0] == 1: + # First call during __init__ fails + raise ResponseStreamingError( + error=IncompleteRead(b"transient"), msg="Transient network error during initialization" + ) + else: + # Subsequent calls succeed + return {"Body": success_body_13, "ContentLength": len(expected_data13)} + + +client13.get_object.side_effect = get_object_with_initial_failure + +test13_exception = None +test13_stream = None +with patch("time.sleep"): # Skip sleep delays in test + try: + test13_stream = RetryingStream(client13, "test-bucket", "test.tar", retries=5) + print(f"✓ Object created successfully after transient failure") + print(f" get_object was called {get_object_call_count[0]} time(s)") + except ResponseStreamingError as e: + test13_exception = e + print(f"✗ Unexpected failure: {e}") + +# Verify the retry logic worked +assert test13_exception is None, "Should have succeeded after retry" +assert test13_stream is not None, "Stream should be created" +assert get_object_call_count[0] == 2, "Should have failed once, then succeeded on retry" +data13 = test13_stream.read() +assert data13 == expected_data13, "Should be able to read data successfully" +print(f"✓ Successfully read {len(data13)} bytes after recovering from transient init error") + +print("\n✅ All mock tests passed! Retry logic working correctly.") diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py new file mode 100644 index 00000000..52055cde --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py @@ -0,0 +1,1185 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Benchmark script to measure the performance overhead of retry statistics tracking.""" + +import gc +import os +import statistics +import subprocess +import sys +import threading +import time +from http.client import IncompleteRead +from unittest.mock import MagicMock + +import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream + +# Configure faster logging interval for tests (10 seconds instead of 5 minutes) +stream_module.RETRY_STATS_LOG_INTERVAL = 10.0 + + +def benchmark_iteration(enable_stats: bool, num_operations: int, network_delay_ms: float = 0) -> float: + """Run a single benchmark iteration. + + Args: + enable_stats: Whether to enable statistics tracking + num_operations: Number of read operations to perform + network_delay_ms: Simulated network delay in milliseconds (0 = no delay) + + Returns: + Time taken in seconds + """ + stream_module.ENABLE_RETRY_STATS = enable_stats + + # Setup mock + client = MagicMock() + test_data = b"X" * 1024 # 1KB chunks + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + mock_body = MagicMock() + if network_delay_ms > 0: + # Add simulated network delay to mock read + def mock_read_with_delay(amt): + time.sleep(network_delay_ms / 1000.0) # Convert ms to seconds + return test_data + + mock_body.read = mock_read_with_delay + else: + mock_body.read.return_value = test_data + + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + stream = RetryingStream(client, "benchmark-bucket", "test.tar", retries=3) + + # Disable GC during timing to reduce noise + gc.collect() + gc.disable() + + try: + start_time = time.perf_counter() # Use perf_counter for higher precision + for _ in range(num_operations): + stream.read(1024) + end_time = time.perf_counter() + finally: + gc.enable() + + return end_time - start_time + + +def run_benchmark_suite(name: str, num_operations: int, num_runs: int, network_delay_ms: float = 0): + """Run a complete benchmark suite.""" + print(f"\n{'=' * 70}") + print(f"{name}") + print(f"{'=' * 70}") + print(f"Operations: {num_operations:,} per run, {num_runs} runs") + if network_delay_ms > 0: + print(f"Network delay: {network_delay_ms}ms per read (simulates S3 latency)") + else: + print(f"Network delay: None (synthetic benchmark)") + print(f"GC disabled during timing for accuracy") + print("-" * 70) + + # Interleave runs to reduce system variance + with_stats_times = [] + without_stats_times = [] + + for i in range(num_runs): + # Run both configs in same iteration to reduce variance + elapsed_with = benchmark_iteration(True, num_operations, network_delay_ms) + elapsed_without = benchmark_iteration(False, num_operations, network_delay_ms) + + with_stats_times.append(elapsed_with) + without_stats_times.append(elapsed_without) + + overhead_this_run = ((elapsed_with - elapsed_without) / elapsed_without) * 100 + print( + f"Run {i + 1:2d}: stats ON={elapsed_with:.4f}s ({num_operations / elapsed_with:,.0f} ops/s) | " + f"stats OFF={elapsed_without:.4f}s ({num_operations / elapsed_without:,.0f} ops/s) | " + f"overhead={overhead_this_run:+.1f}%" + ) + + # Calculate statistics (use trimmed mean to reduce outlier impact) + median_with_stats = statistics.median(with_stats_times) + median_without_stats = statistics.median(without_stats_times) + + # Also calculate trimmed mean (remove top/bottom 10%) + sorted_with = sorted(with_stats_times) + sorted_without = sorted(without_stats_times) + trim_count = max(1, len(sorted_with) // 10) + trimmed_with = sorted_with[trim_count:-trim_count] if len(sorted_with) > 2 * trim_count else sorted_with + trimmed_without = sorted_without[trim_count:-trim_count] if len(sorted_without) > 2 * trim_count else sorted_without + + mean_trimmed_with = statistics.mean(trimmed_with) + mean_trimmed_without = statistics.mean(trimmed_without) + + stddev_with_stats = statistics.stdev(with_stats_times) if len(with_stats_times) > 1 else 0 + stddev_without_stats = statistics.stdev(without_stats_times) if len(without_stats_times) > 1 else 0 + + # Calculate coefficient of variation (CV) to show relative stability + cv_with = (stddev_with_stats / median_with_stats) * 100 if median_with_stats > 0 else 0 + cv_without = (stddev_without_stats / median_without_stats) * 100 if median_without_stats > 0 else 0 + + print("-" * 70) + print(f"Stats ON: median={median_with_stats:.4f}s, stddev={stddev_with_stats:.4f}s, CV={cv_with:.1f}%") + print(f"Stats OFF: median={median_without_stats:.4f}s, stddev={stddev_without_stats:.4f}s, CV={cv_without:.1f}%") + + if max(cv_with, cv_without) > 15: + print(f"⚠ High variance detected (CV > 15%) - results may be unreliable due to system noise") + + # Use both median and trimmed mean for overhead calculation + overhead_median = ((median_with_stats - median_without_stats) / median_without_stats) * 100 + overhead_trimmed = ((mean_trimmed_with - mean_trimmed_without) / mean_trimmed_without) * 100 + + print(f"\nMedian overhead: {overhead_median:+.2f}%") + print(f"Trimmed mean overhead: {overhead_trimmed:+.2f}% (outliers removed)") + + # Show per-operation overhead (using trimmed mean for robustness) + per_op_overhead_ns = ((mean_trimmed_with - mean_trimmed_without) / num_operations) * 1e9 + per_op_overhead_us = per_op_overhead_ns / 1000.0 + print(f"Per-operation overhead: {per_op_overhead_ns:.1f} nanoseconds ({per_op_overhead_us:.3f} microseconds)") + + if network_delay_ms > 0: + network_delay_us = network_delay_ms * 1000.0 + overhead_vs_network = (per_op_overhead_us / network_delay_us) * 100 + print(f"Overhead vs network delay: {overhead_vs_network:.4f}% of {network_delay_ms}ms") + + # Use trimmed mean for final assessment (more robust) + if abs(overhead_trimmed) < 1.0: + print("✓ Negligible overhead (< 1%)") + elif abs(overhead_trimmed) < 5.0: + print("✓ Low overhead (< 5%)") + else: + print("⚠ Measurable overhead (>= 5%)") + + return overhead_trimmed + + +def test_multithreaded_stats_correctness(): + """Test that global statistics correctly aggregate across multiple threads and instances.""" + print("\n" + "=" * 70) + print("CORRECTNESS TEST: Multi-threaded Statistics Aggregation") + print("=" * 70) + + # Enable stats for testing + stream_module.ENABLE_RETRY_STATS = True + + # Reset global stats + with stream_module._global_retry_stats.lock: + stream_module._global_retry_stats.registered_threads.clear() + stream_module._global_retry_stats.active_instances.clear() + stream_module._global_retry_stats.cumulative_operations_started = 0 + stream_module._global_retry_stats.cumulative_failed_operations = 0 + stream_module._global_retry_stats.cumulative_attempts = 0 + + # Test configuration + num_threads = 4 + operations_per_thread = 50 + retry_probability = 0.3 # 30% of operations will require a retry + + # Calculate exact expected retries (deterministic based on modulo) + total_read_ops = num_threads * operations_per_thread + retry_every_n = int(1 / retry_probability) # Every 3rd operation retries + expected_retries = sum(1 for i in range(total_read_ops) if i % retry_every_n == 0) + + print(f"Configuration: {num_threads} threads, {operations_per_thread} operations per thread") + print(f"Expected: {total_read_ops} read operations (+ init operations)") + print(f"Expected retries: {expected_retries} operations with retries (every {retry_every_n}th operation)") + print("-" * 70) + + # Counter to track which operation should fail + operation_counter = {"count": 0, "lock": threading.Lock()} + + def thread_worker(thread_id: int): + """Worker function that creates streams and performs operations.""" + client = MagicMock() + test_data = b"X" * 1024 + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + # Track operations in this thread + local_ops = 0 + local_retries = 0 + + # Keep all streams alive until thread completes to prevent premature destructor calls + streams = [] + + for i in range(operations_per_thread): + # Determine if this operation should require a retry + with operation_counter["lock"]: + op_num = operation_counter["count"] + operation_counter["count"] += 1 + should_retry = (op_num % int(1 / retry_probability)) == 0 + + # Create mock body that may fail once then succeed + mock_body = MagicMock() + if should_retry: + # First read fails with IncompleteRead, second succeeds + mock_body.read.side_effect = [ + IncompleteRead(b"partial"), + test_data, + ] + local_retries += 1 + else: + # Always succeeds + mock_body.read.return_value = test_data + + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + # Create stream and perform read + stream = RetryingStream(client, f"bucket-{thread_id}", f"file-{i}.tar", retries=5) + streams.append(stream) # Keep alive to prevent premature destructor calls + try: + data = stream.read(1024) + local_ops += 1 + except Exception as e: + print(f"Thread {thread_id}: Unexpected error: {e}") + + print(f"Thread {thread_id}: Completed {local_ops} operations, {local_retries} with retries") + return local_ops, local_retries + + # Run threads + print("Starting threads...") + threads = [] + results = [] + + def thread_wrapper(thread_id): + result = thread_worker(thread_id) + results.append(result) + + start_time = time.time() + for i in range(num_threads): + t = threading.Thread(target=thread_wrapper, args=(i,)) + threads.append(t) + t.start() + + # Wait for all threads to complete + for t in threads: + t.join() + + elapsed = time.time() - start_time + print(f"All threads completed in {elapsed:.2f}s") + print("-" * 70) + + # Give threads a moment to finish cleanup + time.sleep(0.1) + + # Aggregate local values from thread results (for sanity check) + local_total_ops = sum(r[0] for r in results) + local_ops_with_retries = sum(r[1] for r in results) + + # Get actual stats from global tracker (aggregate from per-thread stats) + with stream_module._global_retry_stats.lock: + actual_ops_started = 0 + actual_failed_ops = 0 + actual_total_attempts = 0 + + for thread_stats in stream_module._global_retry_stats.registered_threads.values(): + actual_ops_started += thread_stats["operations_started"] + actual_failed_ops += thread_stats["failed_operations"] + actual_total_attempts += thread_stats["total_attempts"] + + # Note: actual_ops_started includes init operations (get_length, get_stream) too + # Each RetryingStream.__init__ calls _retry_operation twice + expected_init_ops = num_threads * operations_per_thread * 2 # get_length + get_stream + expected_read_ops = local_total_ops # Should equal num_threads * operations_per_thread + expected_total_with_init = expected_init_ops + expected_read_ops + + print("RESULTS:") + print(f"Local tracking (from threads):") + print(f" Read operations: {local_total_ops}") + print(f" Operations with retries: {local_ops_with_retries}") + print(f"Global tracking (from stats aggregator):") + print(f" Operations started (including init): {actual_ops_started}") + print(f" Expected: {expected_total_with_init} ({expected_init_ops} init + {expected_read_ops} read)") + print(f" Failed operations: {actual_failed_ops}") + print(f" Expected: {expected_retries} (deterministic)") + print(f" Total attempts: {actual_total_attempts}") + print(f" Expected: {expected_total_with_init + expected_retries} (base + retry attempts)") + print("-" * 70) + + # Verify correctness + success = True + + # Sanity check: local tracking should match expected + if local_ops_with_retries != expected_retries: + print(f"⚠ WARNING: Local thread tracking mismatch (bug in test itself!)") + print(f" Expected retries: {expected_retries}, Local tracked: {local_ops_with_retries}") + + # Check that we tracked the right number of operations + if actual_ops_started != expected_total_with_init: + print(f"❌ FAIL: Operations started mismatch!") + print(f" Expected: {expected_total_with_init}, Got: {actual_ops_started}") + success = False + else: + print(f"✓ Operations started tracked correctly") + + # Check that failed operations matches exactly (deterministic) + if actual_failed_ops != expected_retries: + print(f"❌ FAIL: Failed operations mismatch!") + print(f" Expected: {expected_retries}, Got: {actual_failed_ops}") + success = False + else: + print(f"✓ Failed operations tracked correctly") + + # Check that total attempts equals base operations + retry attempts + expected_total_attempts = expected_total_with_init + expected_retries + if actual_total_attempts != expected_total_attempts: + print(f"❌ FAIL: Total attempts mismatch!") + print(f" Expected: {expected_total_attempts}, Got: {actual_total_attempts}") + success = False + else: + print(f"✓ Total attempts tracked correctly") + + # Check that we created thread-local stats for each thread + num_registered_threads = len(stream_module._global_retry_stats.registered_threads) + if num_registered_threads != num_threads: + print(f"❌ FAIL: Incorrect number of threads registered!") + print(f" Expected: {num_threads}, Got: {num_registered_threads}") + success = False + else: + print(f"✓ All {num_threads} threads registered correctly") + + print("-" * 70) + if success: + print("✅ PASS: Multi-threaded statistics aggregation is CORRECT!") + else: + print("❌ FAIL: Multi-threaded statistics aggregation has ERRORS!") + + print("\nNote: Final stats will be logged via atexit handler when the program exits.") + + return success + + +def test_weakref_robustness(): + """Test that WeakSet-based tracking handles failures gracefully. + + Note: Final stats are logged via atexit handler at program exit, + not via destructors, so we won't see "Final" logs during this test. + """ + print("\n" + "=" * 70) + print("ROBUSTNESS TEST: WeakSet Handles Thread Death & Init Failures") + print("=" * 70) + + # Enable stats for testing + stream_module.ENABLE_RETRY_STATS = True + + # Reset global stats + with stream_module._global_retry_stats.lock: + stream_module._global_retry_stats.registered_threads.clear() + stream_module._global_retry_stats.active_instances.clear() + stream_module._global_retry_stats.cumulative_operations_started = 0 + stream_module._global_retry_stats.cumulative_failed_operations = 0 + stream_module._global_retry_stats.cumulative_attempts = 0 + + client = MagicMock() + test_data = b"X" * 1024 + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + # Test 1: Normal construction and destruction + print("Test 1: Normal construction and destruction") + mock_body = MagicMock() + mock_body.read.return_value = test_data + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + stream1 = RetryingStream(client, "bucket", "file1.tar", retries=5) + with stream_module._global_retry_stats.lock: + count = len(stream_module._global_retry_stats.active_instances) + print(f" After creating stream1: {count} active instance(s)") + assert count == 1, f"Expected 1 instance, got {count}" + + del stream1 + gc.collect() # Force garbage collection + + with stream_module._global_retry_stats.lock: + count = len(stream_module._global_retry_stats.active_instances) + print(f" After deleting stream1: {count} active instance(s)") + assert count == 0, f"Expected 0 instances, got {count}" + print(" ✓ Pass: Normal lifecycle works correctly") + + # Test 2: Exception during init (simulated by creating then raising) + print("\nTest 2: WeakSet cleans up even if instance only partially constructed") + stream2 = RetryingStream(client, "bucket", "file2.tar", retries=5) + with stream_module._global_retry_stats.lock: + count_before = len(stream_module._global_retry_stats.active_instances) + print(f" Created stream2: {count_before} active instance(s)") + + # Simulate early destruction (exception path, thread death, etc.) + del stream2 + gc.collect() # Force garbage collection + + with stream_module._global_retry_stats.lock: + count_after = len(stream_module._global_retry_stats.active_instances) + print(f" After destruction: {count_after} active instance(s)") + assert count_after == 0, f"Expected 0 instances after cleanup, got {count_after}" + print(" ✓ Pass: WeakSet automatically cleaned up") + + # Test 3: Multiple instances, destroy in random order + print("\nTest 3: Multiple instances with out-of-order destruction") + # Create streams with explicit references so we can delete specific ones + s0 = RetryingStream(client, "bucket", "file0.tar", retries=5) + s1 = RetryingStream(client, "bucket", "file1.tar", retries=5) + s2 = RetryingStream(client, "bucket", "file2.tar", retries=5) + s3 = RetryingStream(client, "bucket", "file3.tar", retries=5) + s4 = RetryingStream(client, "bucket", "file4.tar", retries=5) + + with stream_module._global_retry_stats.lock: + count = len(stream_module._global_retry_stats.active_instances) + print(f" Created 5 streams: {count} active instance(s)") + assert count == 5, f"Expected 5 instances, got {count}" + + # Delete specific streams (keep s1 and s3 alive) + del s0 + del s2 + del s4 + gc.collect() # Force garbage collection to ensure destructors run + + with stream_module._global_retry_stats.lock: + count = len(stream_module._global_retry_stats.active_instances) + print(f" After deleting 3 streams: {count} active instance(s)") + assert count == 2, f"Expected 2 instances (s1, s3), got {count}" + + # Clean up remaining (s1 and s3) + del s1 + del s3 + gc.collect() # Force garbage collection + + with stream_module._global_retry_stats.lock: + count = len(stream_module._global_retry_stats.active_instances) + print(f" After deleting all: {count} active instance(s)") + assert count == 0, f"Expected 0 instances, got {count}" + print(" ✓ Pass: Out-of-order destruction handled correctly") + + print("-" * 70) + print("✅ PASS: WeakSet-based tracking is ROBUST!") + print(" - Handles normal lifecycle") + print(" - Automatically cleans up dead references") + print(" - Works with arbitrary destruction order") + print(" - No risk of stuck counters or deadlocks") + + return True + + +def test_multi_rank_stats_logging(): + """Test stats logging with multiple ranks AND multiple workers per rank (simulating DataLoader). + + This test uses actual distributed launchers: + 1. Tests with torchrun (PyTorch's distributed launcher) if available + 2. Tests with mpirun (OpenMPI/MPICH) if available (requires mpi4py: `uv pip install mpi4py`) + 3. Skips test if neither available + + The worker script (mpi_rank_worker.py) is launched by real launchers. + Each rank spawns multiple worker processes (via multiprocessing) to simulate + DataLoader workers with num_workers > 0. + + Multi-level testing: + - Multiple ranks (distributed training) + - Multiple workers per rank (DataLoader processes) + - Different workload per worker (simulates real workload imbalance) + - Different failure patterns per worker + + This ensures: + - Each worker process has independent statistics (separate PID, separate _global_retry_stats) + - Each worker logs its own statistics with correct PID + - Worker processes can have the same thread ID but different PIDs + - Statistics are correctly isolated between workers and between ranks + + Note: mpi4py is an optional dependency only needed for mpirun testing. + """ + + print("\n" + "=" * 70) + print("MULTI-RANK (REAL MPI/TORCHRUN) TEST") + print("=" * 70) + + world_size = 10 + + # Get path to worker script + worker_script = os.path.join(os.path.dirname(__file__), "mpi_rank_worker.py") + + # Check which launchers are available + available_launchers = {} + + print("Checking available launchers...") + + # Check torchrun + try: + result = subprocess.run(["torchrun", "--help"], capture_output=True, timeout=5) + if result.returncode == 0: + available_launchers["torchrun"] = [ + "torchrun", + "--standalone", + "--nnodes=1", + f"--nproc_per_node={world_size}", + worker_script, + ] + print(" ✓ torchrun available") + except (FileNotFoundError, subprocess.TimeoutExpired): + print(" ✗ torchrun not found") + + # Check mpirun + try: + result = subprocess.run(["mpirun", "--version"], capture_output=True, timeout=5) + if result.returncode == 0: + available_launchers["mpirun"] = [ + "mpirun", + "--oversubscribe", # Allow more processes than physical cores + "--tag-output", # Prefix output with [rank,node] + "-np", + str(world_size), + sys.executable, + "-u", # Unbuffered Python output + worker_script, + ] + print(" ✓ mpirun available") + except (FileNotFoundError, subprocess.TimeoutExpired): + print(" ✗ mpirun not found") + + if not available_launchers: + print("\n⚠ SKIP: Neither torchrun nor mpirun available") + print(" Install PyTorch (for torchrun) or OpenMPI (for mpirun)") + print("=" * 70) + return True # Not a failure, just skip + + print(f"\nTesting with {len(available_launchers)} launcher(s): {', '.join(available_launchers.keys())}") + print(f" World size: {world_size} ranks per launcher") + print(f" Worker script: {worker_script}") + print("=" * 70) + + # Test with each available launcher + all_passed = True + results = {} + + for launcher_name, launcher_cmd in available_launchers.items(): + print(f"\n{'=' * 70}") + print(f"TESTING WITH: {launcher_name}") + print(f"{'=' * 70}") + print(f"Command: {' '.join(launcher_cmd)}") + print("-" * 70) + + try: + result = subprocess.run( + launcher_cmd, + capture_output=True, # Capture output for verification + text=True, + timeout=120, # 2 minute timeout + ) + returncode = result.returncode + output = result.stdout + result.stderr + + # Print output to terminal + print(output) + + # Note: If a rank fails, torchrun/mpirun will show a stack trace indicating + # which rank failed. This is NORMAL and EXPECTED behavior - it's how + # distributed launchers report failures, not a crash or bug. + + except subprocess.TimeoutExpired: + print(f"\n❌ TIMEOUT: {launcher_name} did not complete within 2 minutes") + results[launcher_name] = "TIMEOUT" + all_passed = False + continue + except Exception as e: + print(f"\n❌ ERROR launching {launcher_name}: {e}") + results[launcher_name] = f"ERROR: {e}" + all_passed = False + continue + + if returncode != 0: + print(f"\n❌ {launcher_name} FAILED: exit code {returncode}") + results[launcher_name] = f"FAILED (exit {returncode})" + all_passed = False + else: + # Verify that each rank reported success + # Each rank now has multiple workers, so we check for rank-level success messages + # Look for the pattern "✅ Rank X: All N workers verified" + # Use regex to count occurrences (not lines) because stdout buffering can concatenate outputs + import re + + rank_summaries = len(re.findall(r"✅ Rank \d+: All \d+ workers verified", output)) + + if rank_summaries == world_size: + print(f"\n✅ {launcher_name} PASSED - All {world_size} ranks with their workers verified") + results[launcher_name] = "PASSED" + else: + print(f"\n❌ {launcher_name} PARTIAL SUCCESS - Only {rank_summaries}/{world_size} ranks verified") + results[launcher_name] = f"PARTIAL ({rank_summaries}/{world_size} ranks OK)" + all_passed = False + + # Print summary + print("\n" + "=" * 70) + print("MULTI-RANK TEST SUMMARY") + print("=" * 70) + + for launcher_name, result in results.items(): + status_icon = "✅" if result == "PASSED" else "❌" + print(f" {status_icon} {launcher_name}: {result}") + + print("-" * 70) + + if all_passed: + print(f"✅ OVERALL: PASS") + print(f"\nAll {world_size} ranks completed successfully with all launchers!") + print(f"\nVerified:") + print(f" ✓ Each rank ran as independent process") + print(f" ✓ Each rank had separate Python interpreter and statistics") + print(f" ✓ Each rank used different failure patterns (rank-specific)") + print(f" ✓ Each rank's statistics matched expected values exactly") + print(f" ✓ Each rank logged its own statistics independently") + print(f" ✓ No shared memory or stat contamination between ranks") + print(f" ✓ Works with both torchrun and mpirun") + print(f"\nThis confirms statistics work correctly in distributed training!") + else: + print(f"❌ OVERALL: FAIL") + print(f"\nSome launchers failed - check output above for details") + + print("=" * 70) + return all_passed + + +def test_rank_failure_robustness(): + """Test that statistics logging doesn't cause deadlocks when ranks fail unexpectedly. + + This is a fault tolerance test that simulates real-world distributed training failures. + + What we test: + - Random ranks are killed mid-execution (os._exit() simulates crash) + - Tests complete within timeout (NO DEADLOCK) + - Statistics logging doesn't hold locks that cause hangs + - Atexit handlers don't deadlock when ranks die + + What we expect: + - Test completes within 60s (key requirement - no hang/deadlock) + - torchrun: Kills all ranks (elastic fail-fast behavior - CORRECT) + - mpirun: May kill all ranks or allow survivors (both OK) + - Non-zero exit code (some ranks died - EXPECTED) + + What we DON'T test: + - Whether survivors complete their work (launcher-dependent) + - Recovery or re-launching (not our responsibility) + + This test is critical because in production, ranks can fail due to: + - Hardware failures (GPU crashes, node failures) + - OOM errors + - Network issues + - Data corruption + + The statistics logging must NOT make the system more brittle by adding deadlock risks. + """ + print("\n" + "=" * 70) + print("RANK FAILURE ROBUSTNESS TEST") + print("=" * 70) + print("Testing that statistics logging doesn't cause deadlocks when ranks fail") + print("=" * 70) + + world_size = 10 + num_failed_ranks = 3 # Kill 30% of ranks + + # Get path to worker script + worker_script = os.path.join(os.path.dirname(__file__), "mpi_rank_worker.py") + + # Randomly select ranks to kill + import random + + random.seed(42) # Deterministic for reproducibility + failed_ranks = random.sample(range(world_size), num_failed_ranks) + failed_ranks_str = ",".join(map(str, failed_ranks)) + + print(f"\n World size: {world_size} ranks") + print(f" Simulating failures: {num_failed_ranks} ranks will be killed mid-execution") + print(f" Failed ranks: {failed_ranks}") + print(f"\n SUCCESS CRITERIA:") + print(f" ✓ Test completes within 60s timeout (NO DEADLOCK)") + print(f" ✓ Job exits with non-zero code (ranks died as expected)") + print(f"\n NOTE: Launchers use fail-fast behavior (kill all ranks when one fails)") + print(f" This is CORRECT and EXPECTED behavior!") + print("=" * 70) + + # Check which launchers are available + available_launchers = {} + + # Check torchrun + try: + result = subprocess.run(["torchrun", "--help"], capture_output=True, timeout=5) + if result.returncode == 0: + available_launchers["torchrun"] = [ + "torchrun", + f"--nproc_per_node={world_size}", + worker_script, + ] + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + # Check mpirun + try: + result = subprocess.run(["mpirun", "--version"], capture_output=True, timeout=5) + if result.returncode == 0: + available_launchers["mpirun"] = [ + "mpirun", + "-np", + str(world_size), + "--oversubscribe", + "--tag-output", + "python", + worker_script, + ] + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + if not available_launchers: + print("\n⚠ SKIPPING: Neither torchrun nor mpirun available") + print("This test requires a distributed launcher.") + return True # Skip test, don't fail + + print(f"\nAvailable launchers: {', '.join(available_launchers.keys())}") + + # Test with each available launcher + all_passed = True + results = {} + + for launcher_name, launcher_cmd in available_launchers.items(): + print(f"\n{'=' * 70}") + print(f"TESTING WITH: {launcher_name}") + print(f"{'=' * 70}") + + # Set environment variables for failure simulation + env = os.environ.copy() + env["SIMULATE_FAILURE_RANKS"] = failed_ranks_str + env["SKIP_BARRIER"] = "1" # Skip barrier to avoid deadlock + + try: + result = subprocess.run( + launcher_cmd, + capture_output=True, + text=True, + timeout=60, # Shorter timeout - should fail fast + env=env, + ) + returncode = result.returncode + output = result.stdout + result.stderr + + # Print output + print(output) + + except subprocess.TimeoutExpired: + print(f"\n❌ TIMEOUT: Test hung for 60 seconds - likely a DEADLOCK!") + print(f"This means the statistics logging caused a deadlock when ranks failed.") + results[launcher_name] = "DEADLOCK" + all_passed = False + continue + except Exception as e: + print(f"\n❌ ERROR launching {launcher_name}: {e}") + results[launcher_name] = f"ERROR: {e}" + all_passed = False + continue + + # Expected behavior: Job should fail (some ranks died) but NOT hang/deadlock + # Key metric: Did it complete within timeout? If yes, NO DEADLOCK! + + # Count ranks that completed their work + success_count = output.count("All statistics verified correctly!") + killed_ranks_confirmed = output.count("SIMULATING RANK FAILURE") + expected_survivors = world_size - num_failed_ranks + + if returncode == 0: + print(f"\n⚠️ WARNING: {launcher_name} succeeded even though ranks were killed") + print(f"This is unexpected - check if failure simulation worked") + results[launcher_name] = "UNEXPECTED_SUCCESS" + all_passed = False + else: + # Job failed as expected (some ranks died) + # Check for deadlock: If we got here without timeout, NO DEADLOCK! + print(f"\n✅ {launcher_name} PASSED - No deadlock detected!") + print(f" Job completed within timeout (no hang/deadlock)") + print(f" Simulated failures: {killed_ranks_confirmed}/{num_failed_ranks} ranks") + print(f" Completed successfully: {success_count} ranks") + + # torchrun kills ALL ranks when ANY rank fails (elastic behavior) + # mpirun may allow survivors to continue + if launcher_name == "torchrun": + if success_count == 0: + print(f" ℹ️ torchrun killed all ranks (expected elastic fail-fast behavior)") + results[launcher_name] = "PASSED" + else: + print(f" ⚠️ Some ranks survived despite torchrun elastic mode") + results[launcher_name] = "PASSED" + else: # mpirun or other + if success_count >= expected_survivors - 1: # Allow 1 off due to timing + print(f" ℹ️ {success_count}/{expected_survivors} expected survivors completed") + results[launcher_name] = "PASSED" + elif success_count > 0: + print(f" ⚠️ Only {success_count}/{expected_survivors} survivors completed") + print(f" Some ranks may have been killed by launcher") + results[launcher_name] = "PASSED" # Still no deadlock + else: + print(f" ℹ️ Launcher killed all ranks (fail-fast behavior)") + results[launcher_name] = "PASSED" + + # Print summary + print("\n" + "=" * 70) + print("RANK FAILURE ROBUSTNESS TEST SUMMARY") + print("=" * 70) + + for launcher_name, result in results.items(): + status_icon = "✅" if result == "PASSED" else "⚠️" if "UNEXPECTED" in result else "❌" + print(f" {status_icon} {launcher_name}: {result}") + + print("-" * 70) + + if all_passed and results: + print(f"✅ OVERALL: PASS - No deadlocks detected!") + print(f"\nAll launchers handled rank failures without deadlock!") + print(f"\nVerified:") + print(f" ✓ {num_failed_ranks} ranks were killed mid-execution (simulated failures)") + print(f" ✓ No deadlocks or hangs detected (completed within timeout)") + print(f" ✓ Statistics logging didn't hold locks that cause deadlocks") + print(f" ✓ Launchers handled failures with fail-fast behavior") + print(f"\nKey finding:") + print(f" • torchrun: Uses elastic fail-fast (kills all ranks when one fails)") + print(f" • mpirun: May allow survivors or use fail-fast depending on config") + print(f" • Both behaviors are CORRECT - no deadlock is the critical requirement") + print(f"\nThis confirms the statistics infrastructure is fault-tolerant!") + elif not results: + print(f"⚠️ OVERALL: SKIP - No launchers available for testing") + else: + print(f"❌ OVERALL: FAIL") + print(f"\nSome tests failed - check for DEADLOCK or TIMEOUT issues above") + print(f"\nIf tests TIMEOUT, that indicates a DEADLOCK problem.") + print(f"If tests complete quickly with 0 survivors, that's fail-fast (OK).") + + print("=" * 70) + return all_passed + + +def test_dataloader_worker_process_isolation(): + """Test to prove DataLoader workers are separate processes with independent statistics. + + This test demonstrates that: + 1. Each DataLoader worker is a separate process (different PID) + 2. Each worker process's main thread can have the same thread ID + 3. Each worker has its own _global_retry_stats instance with independent counters + 4. This explains why production logs show the same thread ID with different operation counts + """ + import multiprocessing + import queue + + from torch.utils.data import DataLoader, Dataset + + print("\n" + "=" * 70) + print("TEST: DataLoader Worker Process Isolation") + print("=" * 70) + + # Queue to collect results from worker processes + result_queue = multiprocessing.Manager().Queue() + + class DataLoaderTestDataset(Dataset): + """Dataset that reports process/thread info from workers.""" + + def __init__(self, num_items: int, result_queue: queue.Queue): + self.num_items = num_items + self.result_queue = result_queue + + def __len__(self) -> int: + return self.num_items + + def __getitem__(self, idx: int) -> dict: + """Each worker performs S3 operations and reports its PID/thread ID/stats.""" + import os + import threading + from unittest.mock import MagicMock + + import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module + from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream + from cosmos3._src.imaginaire.utils import log + + # Initialize logging in worker process (each worker subprocess needs this) + # Only initialize once per worker + if not hasattr(self, "_log_initialized"): + log.init_loguru_stdout() + self._log_initialized = True + + # Get process and thread info + pid = os.getpid() + thread_id = threading.get_ident() + + # Create mock S3 client + client = MagicMock() + test_data = b"X" * 1024 + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + # Vary number of operations per item so each worker gets unique totals + # This makes it clear in logs that each worker has independent statistics + num_ops = 5 + (idx % 4) * 5 # 5, 10, 15, 20 ops depending on idx + + # Perform S3 operations to increment stats + for i in range(num_ops): + mock_body = MagicMock() + mock_body.read.return_value = test_data + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + stream = RetryingStream(client, f"bucket-worker{pid}", f"file-{idx}-{i}.tar", retries=5) + _ = stream.read(1024) + + # Force a log every few items to demonstrate the logging behavior + # This simulates what would happen in production when 60 seconds elapse + if idx % 3 == 0: # Log every 3rd item + # Force logging by resetting the timer + with stream_module._global_retry_stats.lock: + stream_module._global_retry_stats.last_log_time = 0 + + # Trigger periodic log (will show cumulative stats for this worker) + stream_module._maybe_log_retry_stats() + + # Get cumulative stats from this worker's _global_retry_stats + with stream_module._global_retry_stats.lock: + ops_started = stream_module._global_retry_stats.cumulative_operations_started + failed_ops = stream_module._global_retry_stats.cumulative_failed_operations + total_attempts = stream_module._global_retry_stats.cumulative_attempts + + # Report to main process + result = { + "idx": idx, + "pid": pid, + "thread_id": thread_id, + "ops_started": ops_started, + "failed_ops": failed_ops, + "total_attempts": total_attempts, + } + + self.result_queue.put(result) + return result + + # Create DataLoader with multiple workers + num_workers = 4 + items_per_worker = 5 + dataset = DataLoaderTestDataset(num_items=num_workers * items_per_worker, result_queue=result_queue) + + print(f"\nCreating DataLoader with {num_workers} workers...") + print(f"Each worker will process ~{items_per_worker} items") + print(f"Number of S3 operations varies per item: 5, 10, 15, or 20 read ops") + print(f"Each item: (2 * num_read_ops) init operations + num_read_ops read operations") + print(f"This creates DIFFERENT operation counts per worker (proves isolation)") + print(f"\nWorkers will log statistics every 3 items (simulating periodic logs)") + print(f"Watch for logs showing the SAME thread ID but DIFFERENT operation counts!\n") + + dataloader = DataLoader( + dataset, + batch_size=1, + num_workers=num_workers, + shuffle=False, + ) + + # Consume the dataloader (this triggers worker processes) + for batch in dataloader: + pass # Workers send results via queue + + # Collect all results from workers + results = [] + while not result_queue.empty(): + try: + results.append(result_queue.get_nowait()) + except queue.Empty: + break + + # Analyze results + print(f"Collected {len(results)} results from workers\n") + + # Group by PID + by_pid = {} + for result in results: + pid = result["pid"] + if pid not in by_pid: + by_pid[pid] = [] + by_pid[pid].append(result) + + print(f"Found {len(by_pid)} unique worker PIDs:") + + thread_ids_by_pid = {} + for pid, items in sorted(by_pid.items()): + thread_ids = set(item["thread_id"] for item in items) + thread_ids_by_pid[pid] = thread_ids + + # Get final stats for this worker (last item has cumulative total) + final_stats = max(items, key=lambda x: x["ops_started"]) + + print(f" PID {pid}:") + print(f" Thread ID(s): {thread_ids}") + print(f" Processed {len(items)} items") + print( + f" Cumulative stats: {final_stats['ops_started']} ops, " + f"{final_stats['failed_ops']} failed, {final_stats['total_attempts']} attempts" + ) + + # Check for thread ID collisions across processes + all_thread_ids = [] + for thread_ids in thread_ids_by_pid.values(): + all_thread_ids.extend(thread_ids) + + thread_id_counts = {} + for tid in all_thread_ids: + thread_id_counts[tid] = thread_id_counts.get(tid, 0) + 1 + + duplicated_thread_ids = {tid: count for tid, count in thread_id_counts.items() if count > 1} + + print("\n" + "-" * 70) + print("ANALYSIS:") + print("-" * 70) + + if duplicated_thread_ids: + print(f"✅ Found thread ID collision(s): {len(duplicated_thread_ids)} thread IDs shared across processes") + for tid, count in duplicated_thread_ids.items(): + print(f" Thread ID {tid} appears in {count} different worker processes") + print("\nThis proves that the same thread ID can exist in multiple processes!") + else: + print("⚠️ No thread ID collisions found (less common, but still valid)") + print(" Each worker happened to get a unique thread ID") + + print(f"\nEach worker process has INDEPENDENT _global_retry_stats (different counts):") + operation_counts = [] + for pid, items in sorted(by_pid.items()): + final_stats = max(items, key=lambda x: x["ops_started"]) + operation_counts.append(final_stats["ops_started"]) + print(f" PID {pid}: {final_stats['ops_started']} operations (independent counter)") + + # Verify that operation counts are different (proving independence) + if len(set(operation_counts)) > 1: + print(f"\n✅ Workers have DIFFERENT operation counts: {operation_counts}") + print(" This proves each process has its own independent statistics!") + else: + print(f"\n⚠️ Workers have same counts (less typical, but still independent processes)") + + print("\n" + "=" * 70) + print("CONCLUSION:") + print("=" * 70) + print("During the test, you should have seen WARNING logs like:") + print(" [RetryingStream Stats] RANK-LOCAL: X ops, Y failed...") + print(" Thread NNNN: X ops...") + print("\nThese logs likely showed:") + print(" - The SAME thread ID appearing multiple times") + print(" - DIFFERENT operation counts with that same thread ID") + print(" - This EXACTLY matches what you see in production!") + print("\nWhy production logs show:") + print(" - Same thread ID (e.g., 23456244278592) across ALL log entries") + print(" - DIFFERENT/DECREASING operation counts with the same thread ID") + print("\nThe explanation:") + print(" - Each DataLoader worker is a SEPARATE PROCESS with its own memory") + print(" - Each process has its own independent _global_retry_stats instance") + print(" - Thread IDs are process-local (NOT globally unique)") + print(" - The main thread in each process often gets the same thread ID") + print(" - When different workers log at different times → interleaved stats") + print(" - Different workers have different workloads → different operation counts") + print("\nThe solution:") + print(" 1. Use cumulative_* counters (already maintained, protected by lock)") + print(" 2. Add PID to log messages to distinguish which worker is logging") + print(" 3. Understand that 'decreasing' counts are actually different processes") + print("=" * 70) + + +if __name__ == "__main__": + # First, run robustness test + test_passed = test_weakref_robustness() + + if not test_passed: + print("\n⚠ WARNING: Robustness test failed!") + time.sleep(2) + + # Second, run correctness test + test_passed = test_multithreaded_stats_correctness() + + if not test_passed: + print("\n⚠ WARNING: Correctness test failed! Proceeding with benchmarks anyway...") + time.sleep(2) + + # Third, run multi-rank test + test_passed = test_multi_rank_stats_logging() + + if not test_passed: + print("\n⚠ WARNING: Multi-rank test failed! Check errors above.") + time.sleep(2) + + # Fourth, run rank failure robustness test + print("\n\nWaiting 3 seconds before fault tolerance test...") + time.sleep(3) + + test_passed = test_rank_failure_robustness() + + if not test_passed: + print("\n⚠ WARNING: Rank failure robustness test failed!") + print("This means the statistics logging may cause deadlocks when ranks fail.") + time.sleep(2) + + # Fifth, run DataLoader worker isolation test + print("\n\nWaiting 3 seconds before DataLoader worker isolation test...") + time.sleep(3) + + test_dataloader_worker_process_isolation() + + # Warmup (longer to stabilize JIT and caches) + print("\n" + "=" * 70) + print("PERFORMANCE BENCHMARKS") + print("=" * 70) + print("\nWarming up...") + for _ in range(10): + benchmark_iteration(True, 1000, 0) + benchmark_iteration(False, 1000, 0) + + # Benchmark 1: Synthetic (no network delay) - shows maximum overhead + run_benchmark_suite( + name="BENCHMARK 1: Synthetic (No Network Delay)", num_operations=100000, num_runs=10, network_delay_ms=0 + ) + + # Benchmark 2: Aggressive in-region latency (1ms per read) + # This represents same-region VM-to-S3/GCS with high-bandwidth network + print("\n\nWaiting 2 seconds before realistic benchmark...") + time.sleep(2) + + run_benchmark_suite( + name="BENCHMARK 2: In-Region VM to S3/GCS (1ms latency)", + num_operations=1000, # 1 second total with 1ms each + num_runs=10, + network_delay_ms=1.0, + ) + + # Benchmark 3: Typical cross-region or slower network (10ms per read) + print("\n\nWaiting 2 seconds before final benchmark...") + time.sleep(2) + + run_benchmark_suite( + name="BENCHMARK 3: Cross-Region or Slower Network (10ms latency)", + num_operations=200, # 2 seconds total with 10ms each + num_runs=10, + network_delay_ms=10.0, + ) + + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + print("The synthetic benchmark (no delay) shows maximum theoretical overhead.") + print("The realistic benchmarks show actual production impact:") + print(" - 1ms: Aggressive same-region VM to S3/GCS") + print(" - 10ms: Typical cross-region or shared network") + print("\nIn real-world S3/GCS usage, the overhead is completely negligible") + print("because network I/O dominates (1-100ms vs ~200ns overhead).") + + print("\n" + "-" * 70) + print("NOTE: Synthetic benchmark variance (5-20%) is normal and caused by:") + print(" - CPU frequency scaling (1.2GHz → 4.5GHz dynamically)") + print(" - Thermal throttling as CPU heats up") + print(" - Background OS processes") + print(" - Cache effects (cold vs warm)") + print("\nThis variance does NOT exist in production with real network I/O!") + print("The ~200ns overhead is consistent; only the baseline varies.") + + # Re-enable stats for normal operation + stream_module.ENABLE_RETRY_STATS = True diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py new file mode 100644 index 00000000..65ad87c5 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py @@ -0,0 +1,975 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Integration tests for RetryingStream compatibility with tar_file_iterator. +These tests ensure that RetryingStream works correctly when used as a stream +for webdataset's tar_file_iterator function. + +These tests simulate various failure scenarios to verify retry behavior: +- Early failures during tar header reading +- Multiple consecutive failures requiring multiple retries +- Failures during file data block reading +- Exhausted retries leading to error propagation +- Different types of network exceptions (URLLib3, IncompleteRead) +- botocore ResponseStreamingError (wraps IncompleteRead from production logs) +- botocore ConnectionClosedError (connection closed unexpectedly) +- botocore ReadTimeoutError (read timeout on boto3 layer, distinct from urllib3) +""" + +import io +import tarfile +from http.client import IncompleteRead +from unittest.mock import MagicMock, patch + +from botocore.exceptions import ConnectionClosedError, ResponseStreamingError +from botocore.exceptions import ReadTimeoutError as BotocoreReadTimeoutError +from urllib3.exceptions import ProtocolError as URLLib3ProtocolError +from urllib3.exceptions import ReadTimeoutError as URLLib3ReadTimeoutError +from webdataset.tariterators import tar_file_iterator + +import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream + +# Configure faster logging interval for tests (10 seconds instead of 5 minutes) +stream_module.RETRY_STATS_LOG_INTERVAL = 10.0 + +# Test 1: Basic compatibility - RetryingStream works with tar_file_iterator +print("Test 1: RetryingStream basic compatibility with tar_file_iterator") + +# Create a real tar file in memory +tar_buffer = io.BytesIO() +with tarfile.open(fileobj=tar_buffer, mode="w") as tar: + # Add sample files to the tar + sample1_data = b"This is sample 1 content" + sample1_info = tarfile.TarInfo(name="sample001.txt") + sample1_info.size = len(sample1_data) + tar.addfile(sample1_info, io.BytesIO(sample1_data)) + + sample2_data = b"This is sample 2 content with more data" + sample2_info = tarfile.TarInfo(name="sample002.txt") + sample2_info.size = len(sample2_data) + tar.addfile(sample2_info, io.BytesIO(sample2_data)) + +# Get the tar file bytes +tar_bytes = tar_buffer.getvalue() +tar_size = len(tar_bytes) + +# Create a mock S3 client that returns this tar file +client = MagicMock() +client.head_object.return_value = {"ContentLength": str(tar_size)} + +# Create a mock body that simulates boto3's StreamingBody +mock_body = MagicMock() +mock_body._raw_stream = io.BytesIO(tar_bytes) + + +def mock_read(amt=None): + """Simulate StreamingBody.read() behavior""" + return mock_body._raw_stream.read(amt) + + +mock_body.read = mock_read + +client.get_object.return_value = {"Body": mock_body, "ContentLength": tar_size} + +# Create a RetryingStream +retrying_stream = RetryingStream(client, "test-bucket", "test.tar", retries=3) + +# Pass the RetryingStream to tar_file_iterator +samples = [] +try: + for sample in tar_file_iterator(retrying_stream): + samples.append(sample) + print(f" ✓ Extracted: {sample['fname']}, size: {len(sample['data'])} bytes") +except Exception as e: + print(f" ✗ Error during tar iteration: {e}") + raise + +# Verify results +assert len(samples) == 2, f"Expected 2 samples but got {len(samples)}" +assert samples[0]["fname"] == "sample001.txt", f"Expected 'sample001.txt' but got {samples[0]['fname']}" +assert samples[0]["data"] == b"This is sample 1 content", "Sample 1 data mismatch" +assert samples[1]["fname"] == "sample002.txt", f"Expected 'sample002.txt' but got {samples[1]['fname']}" +assert samples[1]["data"] == b"This is sample 2 content with more data", "Sample 2 data mismatch" + +print(f"✓ Successfully extracted {len(samples)} samples from tar via RetryingStream") +print("✓ All samples have correct filenames and content") + + +# Test 2: Multiple files in tar +print("\nTest 2: RetryingStream with multiple files in tar") + +tar_buffer2 = io.BytesIO() +with tarfile.open(fileobj=tar_buffer2, mode="w") as tar: + # Add multiple test files + for i in range(5): + data = f"Sample {i} data content with index {i}".encode() + info = tarfile.TarInfo(name=f"sample{i:03d}.json") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes2 = tar_buffer2.getvalue() +tar_size2 = len(tar_bytes2) + +client2 = MagicMock() +client2.head_object.return_value = {"ContentLength": str(tar_size2)} + +mock_body2 = MagicMock() +mock_body2._raw_stream = io.BytesIO(tar_bytes2) +mock_body2.read = lambda amt=None: mock_body2._raw_stream.read(amt) + +client2.get_object.return_value = {"Body": mock_body2, "ContentLength": tar_size2} + +retrying_stream2 = RetryingStream(client2, "test-bucket", "test2.tar", retries=3) + +samples2 = [] +for sample in tar_file_iterator(retrying_stream2): + samples2.append(sample) + +assert len(samples2) == 5, f"Expected 5 samples but got {len(samples2)}" +for i, sample in enumerate(samples2): + expected_name = f"sample{i:03d}.json" + expected_data = f"Sample {i} data content with index {i}".encode() + assert sample["fname"] == expected_name, f"Sample {i}: Expected '{expected_name}' but got {sample['fname']}" + assert sample["data"] == expected_data, f"Sample {i}: Data mismatch" + print(f" ✓ Sample {i}: {sample['fname']} ({len(sample['data'])} bytes)") + +print(f"✓ Successfully extracted all {len(samples2)} samples") + + +# Test 3: Retry during tar_file_iterator reading - Early failure in tar header +print("\nTest 3: RetryingStream retries during early tar header reading") + +# Create a tar file +tar_buffer3 = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3, mode="w") as tar: + for i in range(3): + data = f"Sample {i} data content".encode() + info = tarfile.TarInfo(name=f"file{i}.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3 = tar_buffer3.getvalue() +tar_size3 = len(tar_bytes3) +print(f" Test tar size: {tar_size3} bytes") + +# Mock client with simulated failure during first tar header read +client3 = MagicMock() +client3.head_object.return_value = {"ContentLength": str(tar_size3)} + +# Track state across stream instances +state = {"bytes_read": 0, "read_count": 0, "fail_triggered": False} + +# First body fails after partial read of first tar header (512 bytes) +mock_body_fail = MagicMock() +mock_body_fail.close = MagicMock() + + +def failing_read(amt=None): + """Simulate a failure during tar header reading""" + state["read_count"] += 1 + # Read some bytes successfully first + if state["bytes_read"] < 256: # Read half of first tar header + chunk_size = min(256 - state["bytes_read"], amt if amt else 1024) + chunk = tar_bytes3[state["bytes_read"] : state["bytes_read"] + chunk_size] + state["bytes_read"] += len(chunk) + print(f" Read attempt {state['read_count']}: read {len(chunk)} bytes (total: {state['bytes_read']})") + return chunk + else: + # Fail on next read + print(f" Read attempt {state['read_count']}: SIMULATING FAILURE at byte {state['bytes_read']}") + state["fail_triggered"] = True + raise IncompleteRead(b"partial") + + +mock_body_fail.read = failing_read + +# Second body succeeds from the retry point +mock_body_success = MagicMock() + + +def success_read(amt=None): + """Successful read from retry point""" + if amt is None or amt < 0: + chunk = tar_bytes3[state["bytes_read"] :] + state["bytes_read"] = len(tar_bytes3) + else: + chunk = tar_bytes3[state["bytes_read"] : state["bytes_read"] + amt] + state["bytes_read"] += len(chunk) + if len(chunk) > 0: + print(f" Retry read: {len(chunk)} bytes (total: {state['bytes_read']})") + return chunk + + +mock_body_success.read = success_read + +client3.get_object.side_effect = [ + {"Body": mock_body_fail, "ContentLength": tar_size3}, + {"Body": mock_body_success, "ContentLength": tar_size3 - state["bytes_read"]}, +] + +retrying_stream3 = RetryingStream(client3, "test-bucket", "test3.tar", retries=5) + +# Try to read from tar_file_iterator with retry +samples3 = [] +with patch("time.sleep"): # Skip sleep delays + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): # Suppress retry logs + try: + for sample in tar_file_iterator(retrying_stream3): + samples3.append(sample) + print(f" ✓ Extracted: {sample['fname']}") + print(f"✓ Successfully recovered and extracted {len(samples3)} samples after network error") + print(f"✓ Failure was triggered: {state['fail_triggered']}") + print(f"✓ Stream reconnected and continued from byte {256}") + assert len(samples3) == 3, f"Expected 3 samples but got {len(samples3)}" + except Exception as e: + print(f" ℹ Partial recovery scenario: {type(e).__name__}: {e}") + print(f" ℹ Bytes read before failure: {256}") + print(f" ℹ RetryingStream retries at byte-level, but tar may need full restart") + assert state["fail_triggered"], "Failure should have been triggered" + print(" ✓ RetryingStream attempted retry as expected") + + +# Test 3b: Multiple failures before success +print("\nTest 3b: Multiple retry attempts before success") + +tar_buffer3b = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3b, mode="w") as tar: + data = b"Test data for multiple retries" + info = tarfile.TarInfo(name="retry_test.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3b = tar_buffer3b.getvalue() +tar_size3b = len(tar_bytes3b) + +client3b = MagicMock() +client3b.head_object.return_value = {"ContentLength": str(tar_size3b)} + +# State for multiple retries +state3b = {"bytes_read": 0, "failure_count": 0, "get_stream_calls": 0} + + +# Create multiple failing bodies +def create_failing_body(fail_after_bytes: int) -> MagicMock: + """Create a body that fails after reading specific number of bytes""" + body = MagicMock() + body.close = MagicMock() + body_state = {"local_read": 0} + + def read_then_fail(amt=None): + if body_state["local_read"] >= fail_after_bytes: + state3b["failure_count"] += 1 + print(f" Failure #{state3b['failure_count']} at byte {state3b['bytes_read']}") + raise IncompleteRead(b"fail") + chunk_size = min(fail_after_bytes - body_state["local_read"], amt if amt else 1024) + chunk = tar_bytes3b[state3b["bytes_read"] : state3b["bytes_read"] + chunk_size] + body_state["local_read"] += len(chunk) + state3b["bytes_read"] += len(chunk) + return chunk + + body.read = read_then_fail + return body + + +# First body fails after 100 bytes, second after 150, third succeeds +def get_object_multi_fail(**kwargs): + state3b["get_stream_calls"] += 1 + if state3b["get_stream_calls"] == 1: + return {"Body": create_failing_body(100), "ContentLength": tar_size3b} + elif state3b["get_stream_calls"] == 2: + return {"Body": create_failing_body(150), "ContentLength": tar_size3b - state3b["bytes_read"]} + else: + # Final success body + body = MagicMock() + + def success_read_final(amt=None): + chunk = tar_bytes3b[state3b["bytes_read"] :] + state3b["bytes_read"] = len(tar_bytes3b) + return chunk + + body.read = success_read_final + return {"Body": body, "ContentLength": tar_size3b - state3b["bytes_read"]} + + +client3b.get_object.side_effect = get_object_multi_fail + +retrying_stream3b = RetryingStream(client3b, "test-bucket", "multi_retry.tar", retries=5) + +samples3b = [] +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3b): + samples3b.append(sample) + print(f" ✓ Extracted after {state3b['failure_count']} failures: {sample['fname']}") + assert len(samples3b) == 1, f"Expected 1 sample but got {len(samples3b)}" + print(f"✓ Successfully handled {state3b['failure_count']} failures with automatic retry") + print(f"✓ Total stream reconnections: {state3b['get_stream_calls'] - 1}") + except Exception as e: + print(f" ✗ Multiple retry scenario failed: {type(e).__name__}") + print(f" ✗ Failures encountered: {state3b['failure_count']}") + print(f" ✗ Retry attempts made: {state3b['get_stream_calls'] - 1}") + raise AssertionError(f"Test 3b failed: Multiple retries did not recover - {type(e).__name__}: {e}") from e + + +# Test 3c: Failure during file data reading (not header) +print("\nTest 3c: Retry during file data block reading") + +tar_buffer3c = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3c, mode="w") as tar: + # Create file with larger data to ensure failure happens during data read + large_data = b"X" * 2048 # 2KB of data + info = tarfile.TarInfo(name="largefile.bin") + info.size = len(large_data) + tar.addfile(info, io.BytesIO(large_data)) + +tar_bytes3c = tar_buffer3c.getvalue() +tar_size3c = len(tar_bytes3c) +print(f" Test tar size: {tar_size3c} bytes (512 header + 2048 data)") + +client3c = MagicMock() +client3c.head_object.return_value = {"ContentLength": str(tar_size3c)} + +state3c = {"bytes_read": 0, "failed": False} + +# First body: read header successfully, fail during data +mock_body_fail_data = MagicMock() +mock_body_fail_data.close = MagicMock() + + +def fail_during_data(amt=None): + """Read tar header OK, fail during data block""" + # Let header pass (512 bytes) + if state3c["bytes_read"] < 512: + chunk = tar_bytes3c[state3c["bytes_read"] : 512] + state3c["bytes_read"] = 512 + print(f" Read tar header: 512 bytes") + return chunk + # Fail during data block + if state3c["bytes_read"] < 1024: + chunk = tar_bytes3c[state3c["bytes_read"] : 1024] + state3c["bytes_read"] = 1024 + print(f" Read partial data: {len(chunk)} bytes") + return chunk + # Now fail + print(f" FAILURE during data block at byte {state3c['bytes_read']}") + state3c["failed"] = True + raise IncompleteRead(b"data_fail") + + +mock_body_fail_data.read = fail_during_data + +# Success body continues from retry point +mock_body_success_data = MagicMock() + + +def success_read_data(amt=None): + chunk_size = min(amt if amt else 4096, tar_size3c - state3c["bytes_read"]) + chunk = tar_bytes3c[state3c["bytes_read"] : state3c["bytes_read"] + chunk_size] + state3c["bytes_read"] += len(chunk) + if len(chunk) > 0: + print(f" Retry continuing: read {len(chunk)} bytes (total: {state3c['bytes_read']})") + return chunk + + +mock_body_success_data.read = success_read_data + +client3c.get_object.side_effect = [ + {"Body": mock_body_fail_data, "ContentLength": tar_size3c}, + {"Body": mock_body_success_data, "ContentLength": tar_size3c - state3c["bytes_read"]}, +] + +retrying_stream3c = RetryingStream(client3c, "test-bucket", "datablock.tar", retries=5) + +samples3c = [] +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3c): + samples3c.append(sample) + print(f" ✓ Extracted: {sample['fname']} ({len(sample['data'])} bytes)") + assert len(samples3c) == 1, f"Expected 1 sample but got {len(samples3c)}" + assert samples3c[0]["data"] == b"X" * 2048, "Data integrity check failed" + print(f"✓ Successfully recovered from failure during data block reading") + print(f"✓ Data integrity maintained: {len(samples3c[0]['data'])} bytes verified") + assert state3c["failed"], "Failure should have been triggered during data read" + except Exception as e: + print(f" ✗ Data block failure scenario failed: {type(e).__name__}: {str(e)[:100]}") + print(f" ✗ Failure occurred at: {'during data read' if state3c['failed'] else 'unexpected location'}") + assert state3c["failed"], "Should have triggered failure during data read" + raise AssertionError(f"Test 3c failed: Data block retry did not recover - {type(e).__name__}: {e}") from e + + +# Test 3d: Exhausted retries - all attempts fail +print("\nTest 3d: Exhausted retries - failure propagates after max attempts") + +tar_buffer3d = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3d, mode="w") as tar: + data = b"Test data that will never be read" + info = tarfile.TarInfo(name="unreachable.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3d = tar_buffer3d.getvalue() +tar_size3d = len(tar_bytes3d) + +client3d = MagicMock() +client3d.head_object.return_value = {"ContentLength": str(tar_size3d)} + +state3d = {"bytes_read": 0, "attempt_count": 0} + + +def always_fail_read(amt=None): + """Always fail after reading a bit""" + state3d["attempt_count"] += 1 + if state3d["bytes_read"] < 100: + chunk = tar_bytes3d[state3d["bytes_read"] : 100] + state3d["bytes_read"] = 100 + return chunk + print(f" Attempt {state3d['attempt_count']}: FAILING") + raise IncompleteRead(b"always_fails") + + +# All stream attempts will fail +def always_fail_get_object(**kwargs): + body = MagicMock() + body.close = MagicMock() + body.read = always_fail_read + return {"Body": body, "ContentLength": tar_size3d - state3d["bytes_read"]} + + +client3d.get_object.side_effect = always_fail_get_object + +retrying_stream3d = RetryingStream(client3d, "test-bucket", "fail.tar", retries=3) + +exception_caught = False +exception_type = None +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + samples3d = list(tar_file_iterator(retrying_stream3d)) + except Exception as e: + exception_caught = True + exception_type = type(e).__name__ + print(f" ✓ Exception propagated after exhausting retries: {exception_type}") + print(f" ✓ Total retry attempts: {state3d['attempt_count'] - 1}") + +assert exception_caught, "Should have raised exception after exhausting retries" +assert state3d["attempt_count"] > 1, f"Should have retried multiple times, got {state3d['attempt_count']}" +print(f"✓ Correctly exhausted retries and propagated error") + + +# Test 3e: Different exception types (URLLib3 errors) +print("\nTest 3e: Retry on different network exception types") + +tar_buffer3e = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3e, mode="w") as tar: + data = b"Data for exception type testing" + info = tarfile.TarInfo(name="exception_test.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3e = tar_buffer3e.getvalue() +tar_size3e = len(tar_bytes3e) + +# Test with ReadTimeoutError then ProtocolError +client3e = MagicMock() +client3e.head_object.return_value = {"ContentLength": str(tar_size3e)} + +state3e = {"bytes_read": 0, "exceptions_raised": [], "get_object_calls": 0} + +# First body: reads some data then raises ReadTimeoutError +mock_body_fail1 = MagicMock() +mock_body_fail1.close = MagicMock() + + +def first_error_read(amt=None): + """Raise ReadTimeoutError immediately without reading""" + state3e["exceptions_raised"].append("ReadTimeoutError") + print(f" Raising ReadTimeoutError at byte {state3e['bytes_read']}") + raise URLLib3ReadTimeoutError(None, None, "Timeout") + + +mock_body_fail1.read = first_error_read + +# Second body: reads some data then raises ProtocolError +mock_body_fail2 = MagicMock() +mock_body_fail2.close = MagicMock() + + +def second_error_read(amt=None): + """Raise ProtocolError immediately without reading""" + state3e["exceptions_raised"].append("ProtocolError") + print(f" Raising ProtocolError at byte {state3e['bytes_read']}") + raise URLLib3ProtocolError("Connection broken") + + +mock_body_fail2.read = second_error_read + +# Final success body +mock_body_success = MagicMock() + + +def final_success(amt=None): + """Successful read from current position""" + if amt is None or amt < 0: + chunk = tar_bytes3e[state3e["bytes_read"] :] + state3e["bytes_read"] = len(tar_bytes3e) + else: + chunk = tar_bytes3e[state3e["bytes_read"] : state3e["bytes_read"] + amt] + state3e["bytes_read"] += len(chunk) + if len(chunk) > 0: + print(f" Read {len(chunk)} bytes (total: {state3e['bytes_read']})") + return chunk + + +mock_body_success.read = final_success + + +def get_with_different_errors(**kwargs): + """Return different bodies that raise different errors""" + state3e["get_object_calls"] += 1 + if state3e["get_object_calls"] == 1: + return {"Body": mock_body_fail1, "ContentLength": tar_size3e} + elif state3e["get_object_calls"] == 2: + return {"Body": mock_body_fail2, "ContentLength": tar_size3e - state3e["bytes_read"]} + else: + return {"Body": mock_body_success, "ContentLength": tar_size3e - state3e["bytes_read"]} + + +client3e.get_object.side_effect = get_with_different_errors + +retrying_stream3e = RetryingStream(client3e, "test-bucket", "exceptions.tar", retries=5) + +samples3e = [] +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3e): + samples3e.append(sample) + print(f" ✓ Extracted: {sample['fname']}") + print(f"✓ Successfully handled multiple exception types:") + for exc in state3e["exceptions_raised"]: + print(f" - {exc}") + assert len(samples3e) == 1, f"Expected 1 sample but got {len(samples3e)}" + assert len(state3e["exceptions_raised"]) == 2, "Should have raised 2 different exceptions" + except Exception as e: + # Byte-level retries can cause tar corruption - verify exceptions were at least caught + print(f" ⚠ Exception during tar parsing: {type(e).__name__}: {str(e)[:80]}") + print(f" ℹ Exception types that triggered retries: {state3e['exceptions_raised']}") + print(f" ℹ S3 reconnection attempts: {state3e['get_object_calls']}") + # Verify that we at least caught and retried the exceptions + if len(state3e["exceptions_raised"]) >= 2: + print(f" ✓ Successfully caught and retried multiple exception types") + print(f" ℹ Note: Tar parsing may fail due to byte-level retry limitations") + else: + raise AssertionError( + f"Test 3e failed: Expected to catch 2 exceptions but only caught {len(state3e['exceptions_raised'])}" + ) from e + + +# Test 3f: ResponseStreamingError from botocore (EXPECTED TO FAIL until fixed) +print("\nTest 3f: botocore.exceptions.ResponseStreamingError handling") + +tar_buffer3f = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3f, mode="w") as tar: + data = b"Data that will trigger ResponseStreamingError" + info = tarfile.TarInfo(name="streaming_error_test.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3f = tar_buffer3f.getvalue() +tar_size3f = len(tar_bytes3f) + +client3f = MagicMock() +client3f.head_object.return_value = {"ContentLength": str(tar_size3f)} + +state3f = {"bytes_read": 0, "error_raised": False, "retry_attempted": False} + +mock_body_streaming_error = MagicMock() +mock_body_streaming_error.close = MagicMock() + + +def raise_response_streaming_error(amt=None): + """Raise ResponseStreamingError as seen in production""" + if state3f["bytes_read"] == 0: + # Read some data first + chunk = tar_bytes3f[: min(512, len(tar_bytes3f))] + state3f["bytes_read"] = len(chunk) + print(f" First read: {len(chunk)} bytes") + return chunk + else: + # Now raise the ResponseStreamingError wrapping IncompleteRead + state3f["error_raised"] = True + print(f" Raising ResponseStreamingError at byte {state3f['bytes_read']}") + # This simulates the actual error from the logs: + # botocore.exceptions.ResponseStreamingError: ('Connection broken: IncompleteRead(81920 bytes read, 563200 more expected)' + inner_error = IncompleteRead(b"x" * 81920) + error_msg = ( + f"Connection broken: IncompleteRead(81920 bytes read, {tar_size3f - state3f['bytes_read']} more expected)" + ) + raise ResponseStreamingError(error=inner_error, msg=error_msg) + + +mock_body_streaming_error.read = raise_response_streaming_error + +# Success body for retry +mock_body_success_3f = MagicMock() + + +def success_read_3f(amt=None): + """Successful read after retry""" + state3f["retry_attempted"] = True + chunk = tar_bytes3f[state3f["bytes_read"] :] + state3f["bytes_read"] = len(tar_bytes3f) + print(f" Retry successful: read {len(chunk)} bytes") + return chunk + + +mock_body_success_3f.read = success_read_3f + +client3f.get_object.side_effect = [ + {"Body": mock_body_streaming_error, "ContentLength": tar_size3f}, + {"Body": mock_body_success_3f, "ContentLength": tar_size3f - state3f["bytes_read"]}, +] + +retrying_stream3f = RetryingStream(client3f, "test-bucket", "streaming_error.tar", retries=5) + +samples3f = [] +exception_caught_3f = None +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3f): + samples3f.append(sample) + print(f" ✓ Extracted: {sample['fname']}") + print(f"✓ SUCCESS: ResponseStreamingError was caught and retried!") + print(f" - Error was raised: {state3f['error_raised']}") + print(f" - Retry was attempted: {state3f['retry_attempted']}") + print(f" - Samples extracted: {len(samples3f)}") + assert len(samples3f) == 1, f"Expected 1 sample but got {len(samples3f)}" + assert state3f["error_raised"], "ResponseStreamingError should have been raised" + assert state3f["retry_attempted"], "Retry should have been attempted" + except ResponseStreamingError as e: + exception_caught_3f = e + print(f" ✗ EXPECTED FAILURE: ResponseStreamingError was NOT caught by RetryingStream") + print(f" ✗ Error message: {e}") + print(f" ✗ Error was raised: {state3f['error_raised']}") + print(f" ✗ Retry was attempted: {state3f['retry_attempted']}") + print(f" ℹ This error needs to be added to the exception handler in stream.py") + assert state3f["error_raised"], "Should have raised ResponseStreamingError" + assert not state3f["retry_attempted"], "Retry should NOT have happened (error not caught)" + except Exception as e: + exception_caught_3f = e + print(f" ⚠ Unexpected exception type: {type(e).__name__}: {e}") + +if exception_caught_3f is not None: + print(f"\n⚠ Test 3f demonstrates the bug: ResponseStreamingError is not handled") + print(f" Fix required: Add ResponseStreamingError to exception handler in stream.py") +else: + print(f"\n✓ Test 3f passed: ResponseStreamingError is properly handled") + + +# Test 3g: ConnectionClosedError from botocore (EXPECTED TO FAIL until fixed) +print("\nTest 3g: botocore.exceptions.ConnectionClosedError handling") + +tar_buffer3g = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3g, mode="w") as tar: + data = b"Data that will trigger ConnectionClosedError" + info = tarfile.TarInfo(name="connection_closed_test.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3g = tar_buffer3g.getvalue() +tar_size3g = len(tar_bytes3g) + +client3g = MagicMock() +client3g.head_object.return_value = {"ContentLength": str(tar_size3g)} + +state3g = {"bytes_read": 0, "error_raised": False, "retry_attempted": False} + +mock_body_conn_closed = MagicMock() +mock_body_conn_closed.close = MagicMock() + + +def raise_connection_closed_error(amt=None): + """Raise ConnectionClosedError as seen in production""" + if state3g["bytes_read"] == 0: + # Read some data first + chunk = tar_bytes3g[: min(512, len(tar_bytes3g))] + state3g["bytes_read"] = len(chunk) + print(f" First read: {len(chunk)} bytes") + return chunk + else: + # Now raise the ConnectionClosedError + state3g["error_raised"] = True + print(f" Raising ConnectionClosedError at byte {state3g['bytes_read']}") + # This simulates: Connection was closed before we received a valid response from endpoint + raise ConnectionClosedError(endpoint_url="https://s3.amazonaws.com/bucket/key") + + +mock_body_conn_closed.read = raise_connection_closed_error + +# Success body for retry +mock_body_success_3g = MagicMock() + + +def success_read_3g(amt=None): + """Successful read after retry""" + state3g["retry_attempted"] = True + chunk = tar_bytes3g[state3g["bytes_read"] :] + state3g["bytes_read"] = len(tar_bytes3g) + print(f" Retry successful: read {len(chunk)} bytes") + return chunk + + +mock_body_success_3g.read = success_read_3g + +client3g.get_object.side_effect = [ + {"Body": mock_body_conn_closed, "ContentLength": tar_size3g}, + {"Body": mock_body_success_3g, "ContentLength": tar_size3g - state3g["bytes_read"]}, +] + +retrying_stream3g = RetryingStream(client3g, "test-bucket", "conn_closed.tar", retries=5) + +samples3g = [] +exception_caught_3g = None +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3g): + samples3g.append(sample) + print(f" ✓ Extracted: {sample['fname']}") + print(f"✓ SUCCESS: ConnectionClosedError was caught and retried!") + print(f" - Error was raised: {state3g['error_raised']}") + print(f" - Retry was attempted: {state3g['retry_attempted']}") + print(f" - Samples extracted: {len(samples3g)}") + assert len(samples3g) == 1, f"Expected 1 sample but got {len(samples3g)}" + assert state3g["error_raised"], "ConnectionClosedError should have been raised" + assert state3g["retry_attempted"], "Retry should have been attempted" + except ConnectionClosedError as e: + exception_caught_3g = e + print(f" ✗ EXPECTED FAILURE: ConnectionClosedError was NOT caught by RetryingStream") + print(f" ✗ Error message: {e}") + print(f" ✗ Error was raised: {state3g['error_raised']}") + print(f" ✗ Retry was attempted: {state3g['retry_attempted']}") + print(f" ℹ This error needs to be added to the exception handler in stream.py") + assert state3g["error_raised"], "Should have raised ConnectionClosedError" + assert not state3g["retry_attempted"], "Retry should NOT have happened (error not caught)" + except Exception as e: + exception_caught_3g = e + print(f" ⚠ Unexpected exception type: {type(e).__name__}: {e}") + +if exception_caught_3g is not None: + print(f"\n⚠ Test 3g demonstrates the bug: ConnectionClosedError is not handled") + print(f" Fix required: Add ConnectionClosedError to exception handler in stream.py") +else: + print(f"\n✓ Test 3g passed: ConnectionClosedError is properly handled") + + +# Test 3h: ReadTimeoutError from botocore (EXPECTED TO FAIL until fixed) +print("\nTest 3h: botocore.exceptions.ReadTimeoutError handling") + +tar_buffer3h = io.BytesIO() +with tarfile.open(fileobj=tar_buffer3h, mode="w") as tar: + data = b"Data that will trigger botocore ReadTimeoutError" + info = tarfile.TarInfo(name="read_timeout_test.txt") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes3h = tar_buffer3h.getvalue() +tar_size3h = len(tar_bytes3h) + +client3h = MagicMock() +client3h.head_object.return_value = {"ContentLength": str(tar_size3h)} + +state3h = {"bytes_read": 0, "error_raised": False, "retry_attempted": False} + +mock_body_read_timeout = MagicMock() +mock_body_read_timeout.close = MagicMock() + + +def raise_botocore_read_timeout_error(amt=None): + """Raise botocore ReadTimeoutError (different from urllib3 version)""" + if state3h["bytes_read"] == 0: + # Read some data first + chunk = tar_bytes3h[: min(400, len(tar_bytes3h))] + state3h["bytes_read"] = len(chunk) + print(f" First read: {len(chunk)} bytes") + return chunk + else: + # Now raise botocore's ReadTimeoutError + state3h["error_raised"] = True + print(f" Raising botocore.exceptions.ReadTimeoutError at byte {state3h['bytes_read']}") + # This simulates: Read timeout on endpoint URL + raise BotocoreReadTimeoutError(endpoint_url="https://s3.amazonaws.com/bucket/key") + + +mock_body_read_timeout.read = raise_botocore_read_timeout_error + +# Success body for retry +mock_body_success_3h = MagicMock() + + +def success_read_3h(amt=None): + """Successful read after retry""" + state3h["retry_attempted"] = True + chunk = tar_bytes3h[state3h["bytes_read"] :] + state3h["bytes_read"] = len(tar_bytes3h) + print(f" Retry successful: read {len(chunk)} bytes") + return chunk + + +mock_body_success_3h.read = success_read_3h + +client3h.get_object.side_effect = [ + {"Body": mock_body_read_timeout, "ContentLength": tar_size3h}, + {"Body": mock_body_success_3h, "ContentLength": tar_size3h - state3h["bytes_read"]}, +] + +retrying_stream3h = RetryingStream(client3h, "test-bucket", "read_timeout.tar", retries=5) + +samples3h = [] +exception_caught_3h = None +with patch("time.sleep"): + with patch("cosmos3._src.imaginaire.datasets.webdataset.utils.stream.log"): + try: + for sample in tar_file_iterator(retrying_stream3h): + samples3h.append(sample) + print(f" ✓ Extracted: {sample['fname']}") + print(f"✓ SUCCESS: botocore ReadTimeoutError was caught and retried!") + print(f" - Error was raised: {state3h['error_raised']}") + print(f" - Retry was attempted: {state3h['retry_attempted']}") + print(f" - Samples extracted: {len(samples3h)}") + assert len(samples3h) == 1, f"Expected 1 sample but got {len(samples3h)}" + assert state3h["error_raised"], "ReadTimeoutError should have been raised" + assert state3h["retry_attempted"], "Retry should have been attempted" + except BotocoreReadTimeoutError as e: + exception_caught_3h = e + print(f" ✗ EXPECTED FAILURE: botocore ReadTimeoutError was NOT caught by RetryingStream") + print(f" ✗ Error message: {e}") + print(f" ✗ Error was raised: {state3h['error_raised']}") + print(f" ✗ Retry was attempted: {state3h['retry_attempted']}") + print(f" ℹ This error needs to be added to the exception handler in stream.py") + assert state3h["error_raised"], "Should have raised ReadTimeoutError" + assert not state3h["retry_attempted"], "Retry should NOT have happened (error not caught)" + except Exception as e: + exception_caught_3h = e + print(f" ⚠ Unexpected exception type: {type(e).__name__}: {e}") + +if exception_caught_3h is not None: + print(f"\n⚠ Test 3h demonstrates the bug: botocore ReadTimeoutError is not handled") + print(f" Fix required: Add ReadTimeoutError to exception handler in stream.py") +else: + print(f"\n✓ Test 3h passed: botocore ReadTimeoutError is properly handled") + + +# Test 4: Large tar file with chunked reads +print("\nTest 4: RetryingStream with large tar file (chunked reads)") + +tar_buffer4 = io.BytesIO() +with tarfile.open(fileobj=tar_buffer4, mode="w") as tar: + # Add files with larger content to force multiple read() calls + for i in range(3): + # Each file is 10KB + data = (f"Large sample {i} content " * 500).encode()[:10240] + info = tarfile.TarInfo(name=f"large{i:03d}.bin") + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +tar_bytes4 = tar_buffer4.getvalue() +tar_size4 = len(tar_bytes4) + +client4 = MagicMock() +client4.head_object.return_value = {"ContentLength": str(tar_size4)} + +mock_body4 = MagicMock() +mock_body4._raw_stream = io.BytesIO(tar_bytes4) +mock_body4.read = lambda amt=None: mock_body4._raw_stream.read(amt) + +client4.get_object.return_value = {"Body": mock_body4, "ContentLength": tar_size4} + +retrying_stream4 = RetryingStream(client4, "test-bucket", "large.tar", retries=3) + +samples4 = [] +for sample in tar_file_iterator(retrying_stream4): + samples4.append(sample) + +assert len(samples4) == 3, f"Expected 3 samples but got {len(samples4)}" +for i, sample in enumerate(samples4): + expected_name = f"large{i:03d}.bin" + assert sample["fname"] == expected_name, f"Sample {i}: Expected '{expected_name}' but got {sample['fname']}" + assert len(sample["data"]) == 10240, f"Sample {i}: Expected 10240 bytes but got {len(sample['data'])}" + print(f" ✓ Large file {i}: {sample['fname']} ({len(sample['data'])} bytes)") + +print(f"✓ Successfully handled large tar file with {len(samples4)} files") + + +print("\n" + "=" * 70) +print("Test Summary:") +print(" ✓ Test 1: Basic compatibility - RetryingStream works with tar_file_iterator") +print(" ✓ Test 2: Multiple files extraction - Handles tar files with multiple entries") +print(" ✓ Test 3: Early header failure - Retries during tar header reading") +print(" ✓ Test 3b: Multiple retries - Handles consecutive failures before success") +print(" ✓ Test 3c: Data block failure - Retries during file data reading") +print(" ✓ Test 3d: Exhausted retries - Properly propagates errors after max attempts") +print(" ✓ Test 3e: Multiple exception types - Handles various network errors") + +# Check which botocore exception tests failed +failed_tests = [] +if exception_caught_3f is None: + print(" ✓ Test 3f: ResponseStreamingError - Properly caught and retried") +else: + print(" ✗ Test 3f: ResponseStreamingError - NOT caught (needs fix in stream.py)") + failed_tests.append("ResponseStreamingError") + +if exception_caught_3g is None: + print(" ✓ Test 3g: ConnectionClosedError - Properly caught and retried") +else: + print(" ✗ Test 3g: ConnectionClosedError - NOT caught (needs fix in stream.py)") + failed_tests.append("ConnectionClosedError") + +if exception_caught_3h is None: + print(" ✓ Test 3h: botocore ReadTimeoutError - Properly caught and retried") +else: + print(" ✗ Test 3h: botocore ReadTimeoutError - NOT caught (needs fix in stream.py)") + failed_tests.append("botocore.ReadTimeoutError") + +if failed_tests: + print("\n" + "=" * 70) + print(f"FAILURE: {len(failed_tests)} botocore exception(s) not properly handled") + print("=" * 70) + print("Missing exception handlers:") + for exc in failed_tests: + print(f" - {exc}") + print("\nFix required in stream.py:") + print(" Add these exceptions to the exception handler in RetryingStream.read()") + print("=" * 70) + raise AssertionError( + f"Tests failed: {', '.join(failed_tests)} not caught. " + "Fix required in stream.py: Add these exceptions to exception handler." + ) + +print(" ✓ Test 4: Large files - Correctly handles chunked reads for large tar files") +print("\n" + "=" * 70) +print("✓ ALL TESTS PASSED!") +print("=" * 70) +print("\nConclusion:") +print(" RetryingStream successfully implements byte-level retry logic that works") +print(" seamlessly with tar_file_iterator, recovering from transient network errors") +print(" during tar file streaming and decompression.") +print("=" * 70) diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py new file mode 100644 index 00000000..1b6fc7a7 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# ----------------------------------------------------------------------------- + +"""Worker script for MPI/torchrun multi-rank statistics testing. + +This script is launched by torchrun or mpirun with multiple ranks. +Each rank spawns multiple worker processes to simulate DataLoader workers. +Each worker independently tests RetryingStream statistics tracking. + +Dependencies: + - torch.distributed (required) + - mpi4py (optional, only needed for mpirun testing) + +Usage with torchrun (no extra dependencies): + torchrun --nproc_per_node=10 mpi_rank_worker.py + +Usage with mpirun (requires mpi4py): + uv pip install mpi4py # Only if using mpirun + mpirun -np 10 --oversubscribe python mpi_rank_worker.py + +Environment variables: + SIMULATE_FAILURE_RANKS: Comma-separated list of ranks to kill (e.g., "2,5,7") + SKIP_BARRIER: If set to "1", skips the final barrier (for failure tests) + NUM_WORKERS_PER_RANK: Number of worker processes per rank (default: 3, simulates DataLoader workers) +""" + +import multiprocessing +import os +import random +import sys +from http.client import IncompleteRead +from unittest.mock import MagicMock + +import cosmos3._src.imaginaire.datasets.webdataset.utils.stream as stream_module +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import RetryingStream +from cosmos3._src.imaginaire.utils import log + +# Configure faster logging interval for tests (10 seconds instead of 5 minutes) +stream_module.RETRY_STATS_LOG_INTERVAL = 10.0 + + +def init_distributed(): + """Initialize distributed environment (torchrun or mpirun). + + Returns: + tuple: (rank, world_size, launcher_type) + launcher_type: 'torchrun' or 'mpirun' + + Raises: + RuntimeError: If neither torchrun nor mpirun is available + """ + import torch.distributed as dist + + # Try torchrun first + if "TORCHELASTIC_RUN_ID" in os.environ or ("RANK" in os.environ and "WORLD_SIZE" in os.environ): + # Initialize PyTorch distributed + if not dist.is_initialized(): + dist.init_process_group(backend="gloo") # Use gloo for CPU testing + + rank = dist.get_rank() + world_size = dist.get_world_size() + return rank, world_size, "torchrun" + + # Try mpirun + if "OMPI_COMM_WORLD_RANK" in os.environ or "PMI_RANK" in os.environ: + try: + from mpi4py import MPI # type: ignore # Optional dependency for MPI testing + except ImportError as e: + raise RuntimeError( + "mpirun detected but mpi4py is not installed!\n" + "Install with: uv pip install mpi4py\n" + "Or use torchrun instead:\n" + " torchrun --nproc_per_node=N mpi_rank_worker.py" + ) from e + + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + world_size = comm.Get_size() + + # Initialize torch.distributed using MPI backend so log module can detect rank + if not dist.is_initialized(): + # Set environment variables for torch.distributed + os.environ["RANK"] = str(rank) + os.environ["WORLD_SIZE"] = str(world_size) + os.environ["MASTER_ADDR"] = "127.0.0.1" + os.environ["MASTER_PORT"] = "29500" + + # Initialize with gloo backend (MPI backend requires special build) + dist.init_process_group(backend="gloo", rank=rank, world_size=world_size) + + return rank, world_size, "mpirun" + + # Neither launcher detected + raise RuntimeError( + "Neither torchrun nor mpirun detected!\n" + "This script must be launched with:\n" + " torchrun --nproc_per_node=N mpi_rank_worker.py\n" + " OR\n" + " mpirun -np N python mpi_rank_worker.py" + ) + + +def worker_process( + worker_id: int, + rank: int, + world_size: int, + num_operations: int, + failure_interval: int, + should_fail: bool, + result_queue: multiprocessing.Queue, +) -> None: + """Worker process function that simulates a DataLoader worker. + + Each worker process: + 1. Has its own PID and independent _global_retry_stats instance + 2. Performs a different number of operations (worker-specific workload) + 3. Uses worker-specific failure patterns + 4. Logs its own statistics independently + + Args: + worker_id: Unique worker ID within this rank (0, 1, 2, ...) + rank: Distributed rank this worker belongs to + world_size: Total number of distributed ranks + num_operations: Base number of operations (will be varied per worker) + failure_interval: How often operations fail + should_fail: Whether this worker should simulate a crash + result_queue: Queue to report results back to parent + """ + try: + # Initialize logging in worker process + # Note: Worker processes spawned via multiprocessing.Process don't have torch.distributed + # initialized, which is the same behavior as real PyTorch DataLoader workers. + # They inherit the RANK environment variable from their parent, which RetryingStream's + # get_rank() will use as a fallback. This ensures statistics log with the correct rank. + log.init_loguru_stdout() + + # Enable statistics + stream_module.ENABLE_RETRY_STATS = True + + # Note: atexit handler registration is now automatic (handled lazily in _get_thread_stats) + # Each worker process automatically registers its own handler on first RetryingStream use + + # Each worker does a different amount of work to simulate real workload imbalance + # Worker 0: 100% of base ops, Worker 1: 80%, Worker 2: 120%, Worker 3: 60% + workload_multipliers = [1.0, 0.8, 1.2, 0.6, 1.1, 0.9] + multiplier = workload_multipliers[worker_id % len(workload_multipliers)] + worker_num_operations = int(num_operations * multiplier) + + # Each worker has a slightly different failure pattern + worker_failure_interval = failure_interval + worker_id # Offset by worker_id + + print( + f"Rank {rank} Worker {worker_id} (PID={os.getpid()}): " + f"{worker_num_operations} ops, fail every {worker_failure_interval}th", + flush=True, + ) + + # Calculate expected stats for this worker + expected_init_ops = worker_num_operations * 2 + expected_read_ops = worker_num_operations + expected_total_ops = expected_init_ops + expected_read_ops + + # Count failures + if worker_num_operations > 0: + expected_failed_reads = (worker_num_operations - 1) // worker_failure_interval + 1 + else: + expected_failed_reads = 0 + expected_total_failed = expected_failed_reads + expected_total_attempts = expected_total_ops + expected_failed_reads + + # Setup mock S3 client + client = MagicMock() + test_data = b"X" * 1024 + client.head_object.return_value = {"ContentLength": str(len(test_data))} + + # Simulate crash at random point if requested + failure_point = random.randint(30, 70) if should_fail else None + if should_fail: + print( + f"⚠️ Rank {rank} Worker {worker_id}: WILL CRASH at operation {failure_point}/{worker_num_operations}", + flush=True, + ) + + # Perform operations + for i in range(worker_num_operations): + if should_fail and i == failure_point: + print(f"💥 Rank {rank} Worker {worker_id}: SIMULATING CRASH (killed at operation {i})", flush=True) + sys.stdout.flush() + sys.stderr.flush() + os._exit(1) + + mock_body = MagicMock() + + # Fail based on worker-specific interval + if i % worker_failure_interval == 0: + mock_body.read.side_effect = [IncompleteRead(b"partial"), test_data] + else: + mock_body.read.return_value = test_data + + client.get_object.return_value = {"Body": mock_body, "ContentLength": len(test_data)} + + stream = RetryingStream(client, f"bucket-rank{rank}-w{worker_id}", f"file-{i}.tar", retries=5) + try: + _ = stream.read(1024) + except Exception as e: + print(f"Rank {rank} Worker {worker_id}: ERROR: {e}", flush=True) + result_queue.put({"worker_id": worker_id, "success": False, "error": str(e)}) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + # Force stats logging for this worker process + with stream_module._global_retry_stats.lock: + stream_module._global_retry_stats.last_log_time = 0 + stream_module._log_retry_stats_internal(force=False) + + # Get and verify cumulative stats + with stream_module._global_retry_stats.lock: + actual_ops_started = stream_module._global_retry_stats.cumulative_operations_started + actual_failed_ops = stream_module._global_retry_stats.cumulative_failed_operations + actual_total_attempts = stream_module._global_retry_stats.cumulative_attempts + + # Verify silently unless there's a mismatch + + # Verify stats + success = ( + actual_ops_started == expected_total_ops + and actual_failed_ops == expected_total_failed + and actual_total_attempts == expected_total_attempts + ) + + if not success: + print(f"❌ Rank {rank} Worker {worker_id}: Statistics mismatch!", flush=True) + result_queue.put({"worker_id": worker_id, "success": False, "error": "stats_mismatch"}) + # Log final statistics even on failure + stream_module._log_retry_stats_internal(force=True) + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + print(f"✅ Rank {rank} Worker {worker_id}: Verified", flush=True) + result_queue.put({"worker_id": worker_id, "success": True}) + + # Explicitly log final statistics before exit + # Note: atexit handlers are unreliable in multiprocessing.Process (even with sys.exit(0)) + # This is a known Python limitation, so we explicitly call the final log + stream_module._log_retry_stats_internal(force=True) + + # Explicitly flush all output streams before exiting + sys.stdout.flush() + sys.stderr.flush() + + # Exit cleanly + sys.exit(0) + + except Exception as e: + print(f"❌ Rank {rank} Worker {worker_id}: Unexpected error: {e}", flush=True) + result_queue.put({"worker_id": worker_id, "success": False, "error": str(e)}) + # Log final statistics even on error + try: + stream_module._log_retry_stats_internal(force=True) + except Exception: + pass # Don't let logging errors mask the original error + sys.stdout.flush() + sys.stderr.flush() + sys.exit(1) + + +def main(): + """Main function: spawns multiple worker processes per rank to simulate DataLoader workers.""" + # Initialize distributed environment (torchrun or mpirun) + rank, world_size, launcher = init_distributed() + + # Get configuration from environment + simulate_failure_ranks = os.environ.get("SIMULATE_FAILURE_RANKS", "") + should_fail = str(rank) in simulate_failure_ranks.split(",") if simulate_failure_ranks else False + skip_barrier = os.environ.get("SKIP_BARRIER", "0") == "1" + num_workers = int(os.environ.get("NUM_WORKERS_PER_RANK", "3")) # Default: 3 workers per rank + + try: + # Initialize logging in main process + log.init_loguru_stdout() + + # Base operations per worker + num_operations = 50 + + # Rank-specific failure pattern + failure_intervals = [10, 7, 5, 4, 3, 3, 2, 2, 2, 2] + failure_interval = failure_intervals[rank % len(failure_intervals)] + + print( + f"Rank {rank}/{world_size} ({launcher}): Starting {num_workers} workers", + flush=True, + ) + + # Create queue for worker results + result_queue = multiprocessing.Queue() + + # Spawn worker processes (simulating DataLoader workers) + workers = [] + for worker_id in range(num_workers): + # Only simulate failure in the first worker of a failing rank + worker_should_fail = should_fail and worker_id == 0 + + p = multiprocessing.Process( + target=worker_process, + args=( + worker_id, + rank, + world_size, + num_operations, + failure_interval, + worker_should_fail, + result_queue, + ), + ) + p.start() + workers.append(p) + + # Wait for all workers to complete + all_success = True + for p in workers: + p.join() + if p.exitcode != 0: + print(f"❌ Rank {rank}: Worker with PID {p.pid} failed with exit code {p.exitcode}", flush=True) + all_success = False + + # Collect results from queue + worker_results = [] + while not result_queue.empty(): + worker_results.append(result_queue.get()) + + # Verify all workers succeeded + success_count = sum(1 for r in worker_results if r.get("success", False)) + + if not all_success or success_count != num_workers: + print(f"❌ Rank {rank}: {success_count}/{num_workers} workers succeeded", flush=True) + sys.exit(1) + + print(f"✅ Rank {rank}: All {num_workers} workers verified", flush=True) + + # Synchronize all ranks before exit (silent) + if not skip_barrier: + try: + import torch.distributed as dist + + if dist.is_initialized(): + dist.barrier() + except Exception: + pass # Ignore barrier failures + + finally: + # Cleanup distributed environment + if launcher == "torchrun": + try: + import torch.distributed as dist + + if dist.is_initialized(): + dist.destroy_process_group() + except (ImportError, Exception): + pass + # mpi4py calls MPI.Finalize() automatically at exit, no cleanup needed + + +if __name__ == "__main__": + main() diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py b/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py new file mode 100644 index 00000000..cb39bb52 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py @@ -0,0 +1,402 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import threading +import time +import traceback +import warnings +from collections.abc import Iterable +from concurrent.futures import ThreadPoolExecutor, as_completed +from functools import partial +from typing import Callable + +import omegaconf +import torch.distributed as dist +import webdataset as wds +from webdataset.handlers import reraise_exception + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import AugmentorConfig, DatasetConfig, DatasetInfo, TarSample, Wdinfo +from cosmos3._src.imaginaire.datasets.webdataset.utils.iterators import WebDataset +from cosmos3._src.imaginaire.datasets.webdataset.utils.misc import remove_extensions_from_keys, skip_keys, update_url +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.distributed import get_rank, get_world_size +from cosmos3._src.imaginaire.utils.object_store import ObjectStore + + +def wrap_augmentor_func_as_generator(func: Callable, data: Iterable): + for data_dict in data: + data_dict_out = func(data_dict) + if data_dict_out is None: + # Skip "unhealthy" samples + continue + yield data_dict_out + + +def _sample_timer(data: Iterable) -> Iterable: + """Pipeline stage that measures total per-sample production time. + + Must be the LAST stage appended to the dataset pipeline. When the + DataLoader worker calls ``next()`` on this iterator the call propagates + through the entire upstream chain (I/O -> decode -> augment -> ...), + so the elapsed time captures the full cost of producing one sample. + """ + it = iter(data) + while True: + t_start = time.monotonic() + try: + sample = next(it) + except StopIteration: + return + sample["_sample_time"] = time.monotonic() - t_start + yield sample + + +class Dataset: + def __init__( + self, + config: DatasetConfig, + handler: Callable = reraise_exception, + ): + r"""Webdataloader class + + Args: + config: Dataset config + world_size: Total number of GPUs + """ + super().__init__() + + self.config = config + + self.world_size = get_world_size() + + dataset_info = config.dataset_info + self.streaming_download = config.streaming_download + + self.s3_client = dict() + self.bucket = dict() + self.use_object_store = False + self.data_keys = config.keys + + # Parse the metadata + self.wdinfo = Wdinfo([], 0, 0) + self.parse_dataset_info(dataset_info=dataset_info, use_multithread=True) + self.handler = handler + self.augmentors = dict() + + def parse_dataset_info(self, dataset_info: list[DatasetInfo], use_multithread: bool = True): + r"""Parse metadata about the list of tar files. + + When ``torch.distributed`` is initialized, only rank 0 fetches the + wdinfo JSONs (in parallel via a thread pool) and broadcasts the parsed + metadata to every other rank. + + Args: + dataset_info (list): List of dictionaries containing paths to metadata files. + use_multithread (bool): Whether to use multi-threaded parsing across datasets. Default: True. + """ + rank = get_rank() + world_size = get_world_size() + use_broadcast = world_size > 1 and dist.is_available() and dist.is_initialized() + log.info(f"Start parsing dataset info with {len(dataset_info)} entries, use multithread = {use_multithread}") + tic = time.time() + + # Thread-local ObjectStore cache for per-thread ObjectStore construction. + thread_local_stores = threading.local() + + def get_thread_local_store(dset_info: DatasetInfo) -> ObjectStore: + """Get or create a thread-local ObjectStore for a dataset.""" + cache = getattr(thread_local_stores, "cache", None) + if cache is None: + cache = thread_local_stores.cache = {} + key = (dset_info.object_store_config.credentials, dset_info.object_store_config.bucket) + if key not in cache: + cache[key] = ObjectStore(config_object_storage=dset_info.object_store_config) + return cache[key] + + def process_single_dataset(dset_num: int, dset_info: DatasetInfo): + # For each dataset, we parse the file paths and store them as a list of TarSample. + # TarSample will then be used by each worker to load the data. + use_object_store = dset_info.object_store_config.enabled + dset_id = "dset: {}".format(dset_num) + if use_object_store: + object_store_reader = get_thread_local_store(dset_info) + # Create PBSS config if data is loaded from PBSS + bucket_dset = dset_info.object_store_config.bucket + else: + object_store_reader = None + bucket_dset = None + + tar_samples = [] + total_key_count = 0 + chunk_sizes = [] + + # Read all wdinfo files and obtain the DataSample list + for wdinfo_path in dset_info.wdinfo: + if use_object_store: + if not object_store_reader.object_exists(wdinfo_path): + raise FileNotFoundError(f"{wdinfo_path} not found") + cur_dset_info = object_store_reader.load_object(key=wdinfo_path, type="json") # type: ignore + else: + with open(wdinfo_path, "r") as fp: + cur_dset_info = json.load(fp) + + data_root = cur_dset_info["root"] + # Strip s3://bucket/ prefix from root if present, as the bucket is specified separately + if data_root.startswith("s3://"): + # Remove s3://bucket/ prefix (e.g., "s3://debug/path/" -> "path/") + parts = data_root[5:].split("/", 1) # Split after "s3://" + if len(parts) > 1: + data_root = parts[1] # Take everything after bucket name + else: + data_root = "" + tar_files_list = cur_dset_info["data_list"] + # Use per-tar actual sample counts from data_list_key_count when available; + # fall back to evenly distributing total_key_count across tars. + # chunk_size is only the nominal tar capacity and is not reliable. + per_tar_key_counts = cur_dset_info.get( + "data_list_key_count", + [cur_dset_info["total_key_count"] // max(len(tar_files_list), 1)] * len(tar_files_list), + ) + local_tar_samples = [ + TarSample( + path=tar_file, + root=data_root, + keys=( + dset_info.per_dataset_keys if dset_info.per_dataset_keys else self.data_keys + ), # use per dataset keys if available + meta=dset_info, + dset_id=dset_id, + num_samples=n_samples, + sample_keys_full_list=None, + ) + for tar_file, n_samples in zip(tar_files_list, per_tar_key_counts) + ] + tar_samples.extend(local_tar_samples) + total_key_count += cur_dset_info["total_key_count"] + # Fall back to average samples-per-tar when chunk_size is absent (e.g. SILA wdinfos). + default_chunk_size = cur_dset_info["total_key_count"] // max(len(tar_files_list), 1) + chunk_sizes.append(cur_dset_info.get("chunk_size", default_chunk_size)) + + # boto3 clients are not picklable, so they can't ride along in the + # broadcast payload; we rebuild them locally on every rank below. + return { + "dset_num": dset_num, + "dset_id": dset_id, + "tar_samples": tar_samples, + "total_key_count": total_key_count, + "chunk_sizes": chunk_sizes, + "has_object_store": use_object_store, + "bucket": bucket_dset, + } + + # Step 1: rank 0 (or single-process runs) fetches every wdinfo JSON. + fetch_elapsed = 0.0 + broadcast_elapsed = 0.0 + if rank == 0 or not use_broadcast: + fetch_tic = time.time() + try: + dataset_results = [] + tasks: list[tuple[int, DatasetInfo]] = [] + for i, dset_info in enumerate(dataset_info): + if len(dset_info.wdinfo) == 0: + log.warning(f"No wdinfo found for dataset {i}, skipping...") + continue + tasks.append((i, dset_info)) + if use_multithread and len(tasks) > 1: + # Only rank 0 runs this in distributed mode, so we can + # over-subscribe the pool: wdinfo fetches are I/O-bound, + # so ~2x CPU count keeps the (per-thread) connection pools + num_workers = min(2 * (os.cpu_count() or 16), len(tasks)) + log.info(f"Fetching {len(tasks)} datasets with {num_workers} threads") + with ThreadPoolExecutor(max_workers=num_workers) as executor: + futures = [executor.submit(process_single_dataset, *task) for task in tasks] + for future in as_completed(futures): + dataset_results.append(future.result()) + else: + for task in tasks: + dataset_results.append(process_single_dataset(*task)) + payload = {"ok": True, "dataset_results": dataset_results} + except Exception as exc: + payload = { + "ok": False, + "error_type": type(exc).__name__, + "error_message": str(exc), + "traceback": traceback.format_exc(), + } + fetch_elapsed = time.time() - fetch_tic + else: + payload = None + + # Step 2: broadcast the parsed metadata (or error sentinel) to all ranks. + if use_broadcast: + obj_list = [payload] + broadcast_tic = time.time() + dist.broadcast_object_list(obj_list, src=0) + broadcast_elapsed = time.time() - broadcast_tic + payload = obj_list[0] + + assert payload is not None # for type checkers + if not payload["ok"]: + raise RuntimeError( + f"Rank 0 failed while fetching wdinfo metadata: " + f"{payload['error_type']}: {payload['error_message']}\n" + f"{payload['traceback']}" + ) + dataset_results = payload["dataset_results"] + + # Step 3: every rank merges results and rebuilds ObjectStore instances + # locally (boto3 clients aren't picklable, so they can't ride along in + # the broadcast payload). Each cache entry holds a full ObjectStore; + # we key by (credentials, bucket) so configs with thousands of + # DatasetInfo entries sharing the same auth + bucket reuse a single + # ObjectStore per rank instead of building one per DatasetInfo. + self.use_object_store = any(result["has_object_store"] for result in dataset_results) + local_object_stores: dict[tuple[str, str], ObjectStore] = {} + for result in dataset_results: + dset_id = result["dset_id"] + self.wdinfo.tar_files.extend(result["tar_samples"]) + self.wdinfo.total_key_count += result["total_key_count"] + if len(set(result["chunk_sizes"])) > 1: + warnings.warn( + f"Multiple chunk_size values found in {dset_id}: {result['chunk_sizes']}. Using the first one." + ) + self.wdinfo.chunk_size = result["chunk_sizes"][0] + if result["has_object_store"]: + dset_info = dataset_info[result["dset_num"]] + cache_key = (dset_info.object_store_config.credentials, dset_info.object_store_config.bucket) + if cache_key not in local_object_stores: + local_object_stores[cache_key] = ObjectStore(config_object_storage=dset_info.object_store_config) + self.s3_client[dset_id] = local_object_stores[cache_key].client + if result["bucket"]: + self.bucket[dset_id] = result["bucket"] + + toc = time.time() + log.info( + f"Parsed dataset info with {len(dataset_info)} wdinfos " + f"(num_keys = {self.wdinfo.total_key_count}, num_tars = {len(self.wdinfo.tar_files)}) " + f"and multithread = {use_multithread}, took {(toc - tic):.2f} seconds " + f"(fetch = {fetch_elapsed:.2f}s [rank 0 only], broadcast = {broadcast_elapsed:.2f}s, " + f"world_size = {world_size})" + ) + + @staticmethod + # This is the function that calls each augmentor in sequence. + def augmentor_fn(data, augmentations): + def _stamp_pre_aug(upstream): + for sample in upstream: + sample["_pre_aug_time"] = time.monotonic() + sample["_aug_step_last"] = sample["_pre_aug_time"] + yield sample + + def _checkpoint(upstream, step_name): + for sample in upstream: + now = time.monotonic() + last = sample.get("_aug_step_last", now) + sample.setdefault("_aug_step_times", {})[step_name] = now - last + sample["_aug_step_last"] = now + yield sample + + # Build augmentor chain + data = _stamp_pre_aug(data) + for aug_fn in augmentations: + # Use generator function as augmentor + # (recommended, allows skipping or replicating samples inside the augmentor) + name = getattr(aug_fn, "__name__", None) or type(aug_fn).__name__ + if getattr(aug_fn, "is_generator", False): + data = aug_fn(data) + else: # Use regular function as augmentor (backward compatibility) + data = wrap_augmentor_func_as_generator(aug_fn, data) + data = _checkpoint(data, name) + for sample in data: + sample.pop("_aug_step_last", None) + pre = sample.pop("_pre_aug_time", None) + if pre is not None: + sample["_aug_time"] = time.monotonic() - pre + yield sample + + def build_data_augmentor(self, augmentor_cfg: dict[str, AugmentorConfig]) -> Callable: + r"""Function for building data augmentors from augmentor config.""" + augmentations = [] + for aug in augmentor_cfg.keys(): + augmentations.append(instantiate(augmentor_cfg[aug])) + + # This is the function that calls each augmentor in sequence. + return partial(Dataset.augmentor_fn, augmentations=augmentations) + + def build_dataset(self, **kwargs) -> WebDataset: + tar_list = self.wdinfo.tar_files + num_tars = len(tar_list) + assert num_tars > 0, "Did not find any data." + + shuffle_buffer_size = getattr(self.config, "buffer_size", self.wdinfo.chunk_size) + + # update distributor urls and chunk size + distributor_fn = self.config.distributor + + distributor_fn.set_urls(tar_list) + distributor_fn.set_chunk_size(self.wdinfo.chunk_size) + + dataset = WebDataset( + distributor_fn, + load_from_object_store=self.use_object_store, + s3_client=self.s3_client, + s3_bucket_name=self.bucket, + streaming_download=self.streaming_download, + handler=self.handler, + ) + + # Creating a shuffle buffer + if shuffle_buffer_size > 0: + dataset.append(wds.shuffle(shuffle_buffer_size)) + + # Adding decoders + # Decoders are functions that decode the input IO stream + decoder_list = getattr(self.config, "decoders", []) + decoder_functions = [] + for decoder in decoder_list: + # If the specified decoder is a string, use the webdataset decoder + # If its a callable function, use the defined function to decode data + assert isinstance(decoder, str) or callable(decoder), "Decoder should either be callable or a str" + decoder_functions.append(decoder) + dataset.append(wds.decode(*decoder_functions)) + + # After the decoders are added, remove extension from the keys + # Extensions in the data keys are needed for auto-detection of decoders in webdataset. + if self.config.remove_extension_from_keys: + dataset.append(remove_extensions_from_keys) + + # Function to skip keys + dataset.append(skip_keys) + # Building augmentors + augmentor_cfg = getattr(self.config, "augmentation", None) + assert isinstance(augmentor_cfg, (dict, omegaconf.dictconfig.DictConfig)), ( + f"getting type: {type(augmentor_cfg)}" + ) + augmentation_fn = self.build_data_augmentor(augmentor_cfg) + dataset.append(augmentation_fn) + + # Updates URL names so that the collate function can handle + dataset.append(update_url) + + dataset.append(_sample_timer) + + dataset.total_images = self.wdinfo.total_key_count # type: ignore + log.info("Total number of training shards: %d" % num_tars) + log.info("Total training key count: %d" % dataset.total_images) # type: ignore + + return dataset diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py b/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py new file mode 100644 index 00000000..d8120793 --- /dev/null +++ b/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Optional + +import omegaconf +import webdataset as wds +from webdataset import filters +from webdataset.handlers import reraise_exception + +from cosmos3._src.imaginaire.datasets.webdataset.config.schema import DatasetConfig +from cosmos3._src.imaginaire.datasets.webdataset.utils.iterators import WebDataset +from cosmos3._src.imaginaire.datasets.webdataset.utils.misc import remove_extensions_from_keys, skip_keys, update_url +from cosmos3._src.imaginaire.datasets.webdataset.webdataset import Dataset as BaseDataset +from cosmos3._src.imaginaire.datasets.webdataset.webdataset import _sample_timer +from cosmos3._src.imaginaire.utils import log + + +class Dataset(BaseDataset): + def __init__( + self, + config: DatasetConfig, + handler: Callable = reraise_exception, + decoder_handler: Optional[Callable] = None, + detshuffle: bool = False, + ): + r"""Webdataloader class + + Args: + config: Dataset config + handler (Callable): Error handler for webdataset class + decoder_handler (Callable): Error handler during decoding + """ + super().__init__(config=config, handler=handler) + self.decoder_handler = decoder_handler + self.detshuffle = detshuffle + + def build_dataset(self, **kwargs) -> WebDataset: + r""" + Build the dataset object. + The function only diffs from BaseDataset.build_dataset by only adding the decoder_handler to the WebDataset object. + """ + tar_list = self.wdinfo.tar_files + num_tars = len(tar_list) + assert num_tars > 0, "Did not find any data." + + shuffle_buffer_size = getattr(self.config, "buffer_size", self.wdinfo.chunk_size) + + # update distributor urls and chunk size + distributor_fn = self.config.distributor + + distributor_fn.set_urls(tar_list) + distributor_fn.set_chunk_size(self.wdinfo.chunk_size) + + dataset = WebDataset( + distributor_fn, + load_from_object_store=self.use_object_store, + s3_client=self.s3_client, + s3_bucket_name=self.bucket, + streaming_download=self.streaming_download, + handler=self.handler, + ) + + # Creating a shuffle buffer + if self.detshuffle: + dataset.append(filters.detshuffle(shuffle_buffer_size)) + else: + dataset.append(wds.shuffle(shuffle_buffer_size)) + + # Adding decoders + # Decoders are functions that decode the input IO stream + decoder_list = getattr(self.config, "decoders", []) + decoder_functions = [] + for decoder in decoder_list: + # If the specified decoder is a string, use the webdataset decoder + # If its a callable function, use the defined function to decode data + assert isinstance(decoder, str) or callable(decoder), "Decoder should either be callable or a str" + decoder_functions.append(decoder) + dataset.append(wds.decode(*decoder_functions, handler=self.decoder_handler)) + + # After the decoders are added, remove extension from the keys + # Extensions in the data keys are needed for auto-detection of decoders in webdataset. + if self.config.remove_extension_from_keys: + dataset.append(remove_extensions_from_keys) + + # Function to skip keys + dataset.append(skip_keys) + # Building augmentors + augmentor_cfg = getattr(self.config, "augmentation", None) + assert isinstance(augmentor_cfg, (dict, omegaconf.dictconfig.DictConfig)), ( + f"getting type: {type(augmentor_cfg)}" + ) + augmentation_fn = self.build_data_augmentor(augmentor_cfg) + dataset.append(augmentation_fn) + + # Updates URL names so that the collate function can handle + dataset.append(update_url) + + dataset.append(_sample_timer) + + dataset.total_images = self.wdinfo.total_key_count # type: ignore + log.info("Total number of training shards: %d" % num_tars) + log.info("Total training key count: %d" % dataset.total_images) # type: ignore + + return dataset diff --git a/cosmos3/_src/imaginaire/flags.py b/cosmos3/_src/imaginaire/flags.py new file mode 100644 index 00000000..52aa127a --- /dev/null +++ b/cosmos3/_src/imaginaire/flags.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Feature flags.""" + +import os +from dataclasses import dataclass +from enum import Enum +from typing import Final + + +class StrEnum(str, Enum): + """Backport of StrEnum from Python 3.11.""" + + def __str__(self) -> str: + return self.value + + @staticmethod + def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str: + return name.lower() + + +def _parse_bool(value: str) -> bool: + """Parse string to a boolean.""" + return value.lower() in ["true", "1", "yes", "y"] + + +def _get_bool(name: str, default: bool) -> bool: + """Get a boolean flag from the environment.""" + value = os.environ.get(name, "") + if not value: + return default + return _parse_bool(value) + + +TRAINING: Final[bool] = _get_bool("COSMOS_TRAINING", False) +"""Whether to enable training features. + +This is used to make training dependencies optional. +""" + +INTERNAL: Final[bool] = _get_bool("COSMOS_INTERNAL", False) +"""Whether to use internal (nvidia-only) resources (e.g. S3).""" + +SMOKE: Final[bool] = _get_bool("COSMOS_SMOKE", False) +"""Whether to enable smoke test. + +Sets parameters to minimum values (e.g. num_steps=1, num_layers=2). +""" + + +class Device(StrEnum): + CUDA = "cuda" + CPU = "cpu" + META = "meta" + + +DEVICE: Final[Device] = Device(os.environ.get("COSMOS_DEVICE", "cuda").lower()) +"""Torch device to use. + +Used for checkpoint conversion and smoke tests. +""" + +VERBOSE: Final[bool] = _get_bool("COSMOS_VERBOSE", INTERNAL) +"""Whether to enable verbose console output.""" + +EXPERIMENTAL_CHECKPOINTS: Final[bool] = _get_bool("COSMOS_EXPERIMENTAL_CHECKPOINTS", INTERNAL) +"""Whether to enable experimental checkpoints.""" + + +if INTERNAL: + TRAINING = True + + +@dataclass +class Flags: + internal: bool = INTERNAL + training: bool = TRAINING + smoke: bool = SMOKE + device: Device = DEVICE + verbose: bool = VERBOSE + experimental_checkpoints: bool = EXPERIMENTAL_CHECKPOINTS + + +FLAGS = Flags() +"""Convenience object for accessing flags.""" diff --git a/cosmos3/_src/imaginaire/flops/__init__.py b/cosmos3/_src/imaginaire/flops/__init__.py new file mode 100644 index 00000000..c3b1cab5 --- /dev/null +++ b/cosmos3/_src/imaginaire/flops/__init__.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Reusable FLOPs estimation utilities for model architectures.""" + +from cosmos3._src.imaginaire.flops.omni_mot import ( + OmniMoTModelDescriptor, + compute_omni_mot_flops_per_batch, + get_omni_mot_model_descriptor, +) +from cosmos3._src.imaginaire.flops.wan_vae import compute_wan_vae_encoder_flops + +__all__ = [ + "OmniMoTModelDescriptor", + "compute_omni_mot_flops_per_batch", + "compute_wan_vae_encoder_flops", + "get_omni_mot_model_descriptor", +] diff --git a/cosmos3/_src/imaginaire/flops/omni_mot.py b/cosmos3/_src/imaginaire/flops/omni_mot.py new file mode 100644 index 00000000..999022e1 --- /dev/null +++ b/cosmos3/_src/imaginaire/flops/omni_mot.py @@ -0,0 +1,498 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FLOPs estimation for the OmniMoT (Mixture-of-Tokens) dual-pathway transformer.""" + +from decimal import Decimal +from typing import NamedTuple + +from cosmos3._src.imaginaire.utils import log + + +class OmniMoTModelDescriptor(NamedTuple): + """ + Holds information about the OmniMoT model architecture needed for the custom flops formula. + + This captures the dual-pathway (MoT) transformer with support for vision, action, and sound + modalities, and optional Mixture of Experts (MoE) layers. + """ + + # LLM / Transformer core + hidden_size: int # D: hidden dimension of the transformer (e.g. 2048, 3584) + num_hidden_layers: int # number of transformer decoder layers + num_attention_heads: int # number of Q heads + num_key_value_heads: int # number of K/V heads (GQA when < num_attention_heads) + head_dim: int # dimension per head + intermediate_size: int # dense MLP intermediate size (gate_proj / up_proj output dim) + vocab_size: int # vocabulary size for embed_tokens and lm_head + + # MoE parameters + use_moe: bool # whether MoE layers are used + num_experts: int # total number of experts per MoE layer + num_experts_per_tok: int # top-k experts activated per token + moe_intermediate_size: int # intermediate size inside each expert + decoder_sparse_step: int # every `decoder_sparse_step`-th layer is MoE + mlp_only_layers: list[int] # layers forced to use dense MLP even in MoE config + + # Vision modality + latent_patch_size: int # spatial patch size for latent patchification (default 2) + latent_channel_size: int # number of channels in the VAE latent (default 16) + + # Action modality + action_dim: int # action token dimension (default 32) + + # Sound modality + sound_dim: int # sound token dimension + + # TimestepEmbedder + frequency_embedding_size: int # sinusoidal frequency embedding dim (default 256) + + # Text prediction + predict_text_tokens: bool # whether lm_head is applied for text CE loss + + +def get_omni_mot_model_descriptor( + hidden_size: int = 2048, + num_hidden_layers: int = 24, + num_attention_heads: int = 16, + num_key_value_heads: int = 16, + head_dim: int | None = None, + intermediate_size: int = 5632, + vocab_size: int = 151936, + use_moe: bool = True, + num_experts: int = 60, + num_experts_per_tok: int = 4, + moe_intermediate_size: int = 1408, + decoder_sparse_step: int = 1, + mlp_only_layers: list[int] | None = None, + latent_patch_size: int = 2, + latent_channel_size: int = 16, + action_dim: int = 32, + sound_dim: int = 64, + frequency_embedding_size: int = 256, + predict_text_tokens: bool = False, +) -> OmniMoTModelDescriptor: + if head_dim is None: + head_dim = hidden_size // num_attention_heads + if mlp_only_layers is None: + mlp_only_layers = [] + return OmniMoTModelDescriptor( + hidden_size=hidden_size, + num_hidden_layers=num_hidden_layers, + num_attention_heads=num_attention_heads, + num_key_value_heads=num_key_value_heads, + head_dim=head_dim, + intermediate_size=intermediate_size, + vocab_size=vocab_size, + use_moe=use_moe, + num_experts=num_experts, + num_experts_per_tok=num_experts_per_tok, + moe_intermediate_size=moe_intermediate_size, + decoder_sparse_step=decoder_sparse_step, + mlp_only_layers=mlp_only_layers, + latent_patch_size=latent_patch_size, + latent_channel_size=latent_channel_size, + action_dim=action_dim, + sound_dim=sound_dim, + frequency_embedding_size=frequency_embedding_size, + predict_text_tokens=predict_text_tokens, + ) + + +def _pct(part: Decimal, whole: Decimal) -> str: + """Return percentage string, guarding against division by zero.""" + if whole == 0: + return "0" + return str(round(part / whole * 100, 1)) + + +def _extract_padding_tokens( + split_lens: list[int], + attn_modes: list[str], +) -> int: + """Return the total number of padding tokens in a packed sequence. + + Padding splits are lone ``"causal"`` entries that do not form a + ``(causal, full)`` pair with the next split. In practice, finalize() + appends at most one such split at the end. + """ + padding = 0 + i = 0 + while i < len(split_lens): + if i + 1 < len(split_lens) and attn_modes[i] == "causal" and attn_modes[i + 1] == "full": + i += 2 + else: + if attn_modes[i] == "causal": + padding += split_lens[i] + i += 1 + return padding + + +def _compute_per_sample_attn_flops( + n_heads: int, + d_head: int, + B: int | Decimal, + S_und: int | Decimal, + S_gen: int | Decimal, + split_lens: list[int] | None = None, + attn_modes: list[str] | None = None, + include_padding: bool = False, +) -> tuple[Decimal, Decimal]: + """Compute per-layer attention dot-product FLOPs (QK^T + Attn*V). + + The MoT attention pattern is: + - Und tokens (causal): each sample's text tokens self-attend causally. + - Gen tokens (full): each sample's gen tokens attend to ALL tokens in + that sample (und + gen) with full (non-causal) attention. + + When ``split_lens``/``attn_modes`` are provided (packed-sequence mode), + per-sample lengths are extracted from the alternating (causal, full) pairs. + Otherwise, ``B`` uniform samples each with ``S_und`` and ``S_gen`` tokens + are assumed. + + Args: + include_padding: If True, lone ``"causal"`` splits (padding tokens + appended by finalize()) are counted as additional causal + self-attention windows. + + Returns: + (und_attn_flops, gen_attn_flops) for a single layer (QK^T + Attn*V). + """ + if split_lens is not None and attn_modes is not None: + und_attn = Decimal(0) + gen_attn = Decimal(0) + i = 0 + while i < len(split_lens): + if i + 1 < len(split_lens) and attn_modes[i] == "causal" and attn_modes[i + 1] == "full": + s_und_i = split_lens[i] + s_gen_i = split_lens[i + 1] + und_attn += 4 * n_heads * d_head * s_und_i * s_und_i + gen_attn += 4 * n_heads * d_head * s_gen_i * (s_und_i + s_gen_i) + i += 2 + else: + if include_padding and attn_modes[i] == "causal": + s_pad = split_lens[i] + und_attn += 4 * n_heads * d_head * s_pad * s_pad + i += 1 + return und_attn, gen_attn + + und_attn = Decimal(4 * B * n_heads * d_head * S_und * S_und) + gen_attn = Decimal(4 * B * n_heads * d_head * S_gen * (S_und + S_gen)) + return und_attn, gen_attn + + +def compute_omni_mot_flops_per_batch( + cfg: OmniMoTModelDescriptor, + B: int | Decimal, + text_tokens: int = 512, + vision_tokens: int = 0, + action_tokens: int = 0, + sound_tokens: int = 0, + freeze_und: bool = False, + vision_gen: bool = True, + action_gen: bool = False, + sound_gen: bool = False, + backwardpass_ratio: float = 2.0, + split_lens: list[int] | None = None, + attn_modes: list[str] | None = None, + include_padding: bool = False, + use_activation_checkpointing: bool = False, +) -> Decimal: + """Compute training FLOPs for a single batch of the OmniMoT model. + + This is a standalone function that can be called from calculators or callbacks. + It accounts for all parts of the dual-pathway (MoT) transformer, including: + - Modality-specific embedding/projection layers (vae2llm, llm2vae, action2llm, + llm2action, sound2llm, llm2sound). + - TimestepEmbedder MLPs. + - lm_head for text prediction. + - Transformer blocks with dual-pathway attention (separate Q/K/V/O projections + for und and gen pathways). + - Per-sample attention: und tokens self-attend causally, gen tokens attend to + all tokens in their sample with full attention. + - Attention softmax FLOPs (~5 ops per element of the attention matrix). + - Dual-pathway MLPs (dense SwiGLU or MoE per layer). + - RMSNorm at all positions (4 per layer + Q/K norms + 2 final norms). + - Backward pass with special handling for freeze_und. + - Activation checkpointing forward recomputation during backward. + + Args: + cfg: Model architecture descriptor. + B: Batch size. For the packed-sequence path (``split_lens`` provided), + set ``B=1`` and let ``text_tokens``/``vision_tokens`` be the totals + across all packed samples. + text_tokens: Total number of text (understanding) tokens across all samples. + vision_tokens: Total number of vision generation tokens (after patchification) + across all samples. + action_tokens: Total number of action tokens across all samples. + sound_tokens: Total number of sound tokens across all samples. + freeze_und: If True, understanding pathway is frozen (no backward FLOPs for und). + vision_gen: Whether vision generation is active. + action_gen: Whether action generation is active. + sound_gen: Whether sound generation is active. + backwardpass_ratio: Multiplier for backward pass FLOPs relative to forward + (default 2.0). + split_lens: Per-split token lengths from the packed sequence. Alternating + ``[und_0, gen_0, und_1, gen_1, ...]`` with matching ``attn_modes``. + When provided, per-sample attention FLOPs are computed correctly + instead of assuming one big attention window. + attn_modes: Attention mode for each split (``"causal"`` or ``"full"``). + Must have the same length as ``split_lens``. + include_padding: If True, padding tokens (lone ``"causal"`` splits at + the end of ``split_lens``) are included in FLOPs for attention, + projections, MLP, and norms. Useful for measuring total GPU FLOPs + including wasted work on padding. + use_activation_checkpointing: If True, add FLOPs for the forward + recomputation of each transformer layer during the backward pass. + Activation checkpointing discards intermediate activations and + recomputes them on-the-fly, adding ~1x layer forward FLOPs. + + Returns: + Total training FLOPs (forward + backward) as a Decimal. + """ + bp_ratio = Decimal(backwardpass_ratio) + D = cfg.hidden_size + n_heads = cfg.num_attention_heads + n_kv_heads = cfg.num_key_value_heads + d_head = cfg.head_dim + n_layers = cfg.num_hidden_layers + + # =================================================================== + # Token counts + # =================================================================== + L_vision = vision_tokens if vision_gen else 0 + + S_und = text_tokens + S_gen = L_vision + (action_tokens if action_gen else 0) + (sound_tokens if sound_gen else 0) + + # Padding tokens follow the causal (und) path. When include_padding is + # set, add them to S_und so projections, MLP, and norms account for the + # extra work the GPU performs on padding. + S_pad = 0 + if include_padding and split_lens is not None and attn_modes is not None: + S_pad = _extract_padding_tokens(split_lens, attn_modes) + S_und = S_und + S_pad + + # =================================================================== + # 1. Embedding / Projection Layers (outside transformer blocks) + # =================================================================== + embedding_flops = Decimal(0) + + if vision_gen and L_vision > 0: + patch_latent_dim = cfg.latent_patch_size**2 * cfg.latent_channel_size + embedding_flops += 2 * B * L_vision * patch_latent_dim * D + + if vision_gen and L_vision > 0: + embedding_flops += 2 * B * L_vision * D * patch_latent_dim + + if action_gen and action_tokens > 0: + embedding_flops += 2 * B * action_tokens * cfg.action_dim * D + + if action_gen and action_tokens > 0: + embedding_flops += 2 * B * action_tokens * D * cfg.action_dim + + if sound_gen and sound_tokens > 0 and cfg.sound_dim is not None: + embedding_flops += 2 * B * sound_tokens * cfg.sound_dim * D + + if sound_gen and sound_tokens > 0 and cfg.sound_dim is not None: + embedding_flops += 2 * B * sound_tokens * D * cfg.sound_dim + + # TimestepEmbedder MLP: Linear(freq_dim, D) -> SiLU -> Linear(D, D) + freq_dim = cfg.frequency_embedding_size + timestep_mlp_flops_per_call = 2 * freq_dim * D + 2 * D * D + n_timestep_calls = 0 + if vision_gen and L_vision > 0: + n_timestep_calls += 1 + if action_gen and action_tokens > 0: + n_timestep_calls += 1 + if sound_gen and sound_tokens > 0: + n_timestep_calls += 1 + embedding_flops += n_timestep_calls * B * timestep_mlp_flops_per_call + + if cfg.predict_text_tokens: + embedding_flops += 2 * B * text_tokens * D * cfg.vocab_size + + log.debug(f"embedding_flops: {embedding_flops}") + + # =================================================================== + # Pre-compute per-sample attention dot-product FLOPs (shared by + # forward and backward). Und tokens self-attend causally, + # gen tokens attend to all tokens in their sample. + # =================================================================== + und_attn_dot, gen_attn_dot = _compute_per_sample_attn_flops( + n_heads, + d_head, + B, + S_und, + S_gen, + split_lens, + attn_modes, + include_padding=include_padding, + ) + + # Softmax FLOPs: ~5 ops per element of the S_q x S_k attention matrix + # (subtract max, exp, sum, divide, plus the mask/scale). + # Same sequence-length dependency as dot product but with coefficient + # 5 * n_heads instead of 4 * n_heads * d_head. + softmax_ratio = Decimal(5) / Decimal(4 * d_head) + und_softmax = und_attn_dot * softmax_ratio + gen_softmax = gen_attn_dot * softmax_ratio + + # =================================================================== + # 2. Transformer Blocks + # =================================================================== + total_block_flops = Decimal(0) + total_attn_dot_fwd = Decimal(0) + total_softmax_fwd = Decimal(0) + q_dim = n_heads * d_head + kv_dim = n_kv_heads * d_head + + def _dense_mlp_flops(seq_len: int | Decimal) -> Decimal: + return Decimal(6 * B * seq_len * D * cfg.intermediate_size) + + def _moe_mlp_flops(seq_len: int | Decimal) -> Decimal: + gate_flops = 2 * B * seq_len * D * cfg.num_experts + expert_flops = cfg.num_experts_per_tok * 6 * B * seq_len * D * cfg.moe_intermediate_size + return Decimal(gate_flops + expert_flops) + + for layer_idx in range(n_layers): + is_moe_layer = ( + cfg.use_moe + and cfg.num_experts > 0 + and layer_idx not in cfg.mlp_only_layers + and (layer_idx + 1) % cfg.decoder_sparse_step == 0 + ) + + # 2a. Attention (PackedAttentionMoT) + attn_und_proj = 2 * B * S_und * D * q_dim + 2 * B * S_und * D * kv_dim + 2 * B * S_und * D * kv_dim + attn_gen_proj = 2 * B * S_gen * D * q_dim + 2 * B * S_gen * D * kv_dim + 2 * B * S_gen * D * kv_dim + attn_dot = und_attn_dot + gen_attn_dot + attn_o_proj = 2 * B * S_und * q_dim * D + 2 * B * S_gen * q_dim * D + attn_qk_norm = ( + 5 * B * S_und * n_heads * d_head + + 5 * B * S_und * n_kv_heads * d_head + + 5 * B * S_gen * n_heads * d_head + + 5 * B * S_gen * n_kv_heads * d_head + ) + layer_attn_flops = attn_und_proj + attn_gen_proj + attn_qk_norm + attn_dot + attn_o_proj + + # 2b. MLP (separate for und and gen pathways) + mlp_und_flops = _moe_mlp_flops(S_und) if is_moe_layer else _dense_mlp_flops(S_und) + mlp_gen_flops = _moe_mlp_flops(S_gen) if is_moe_layer else _dense_mlp_flops(S_gen) + layer_mlp_flops = mlp_und_flops + mlp_gen_flops + + # 2c. RMSNorm (4 layer norms per decoder layer, dimension D) + layer_norm_flops = 5 * B * S_und * D + 5 * B * S_gen * D + 5 * B * S_und * D + 5 * B * S_gen * D + + # 2d. Attention softmax + layer_softmax_flops = und_softmax + gen_softmax + + layer_flops = layer_attn_flops + layer_mlp_flops + layer_norm_flops + layer_softmax_flops + total_block_flops += layer_flops + total_attn_dot_fwd += attn_dot + total_softmax_fwd += layer_softmax_flops + + if layer_idx == 0: + log.debug(f"Layer 0 breakdown (MoE={is_moe_layer}):") + log.debug(f" attn_und_proj: {attn_und_proj}") + log.debug(f" attn_gen_proj: {attn_gen_proj}") + log.debug(f" attn_qk_norm: {attn_qk_norm}") + log.debug(f" attn_dot: {attn_dot}") + log.debug(f" attn_softmax: {layer_softmax_flops}") + log.debug(f" attn_o_proj: {attn_o_proj}") + log.debug(f" mlp_und: {mlp_und_flops}") + log.debug(f" mlp_gen: {mlp_gen_flops}") + log.debug(f" layer_norms: {layer_norm_flops}") + log.debug(f" total layer: {layer_flops}") + + # =================================================================== + # 3. Final norms (applied to und and gen separately after all layers) + # =================================================================== + final_norm_flops = Decimal(5 * B * S_und * D + 5 * B * S_gen * D) + + log.debug(f"final_norm_flops: {final_norm_flops}") + + # =================================================================== + # 4. Forward pass total + # =================================================================== + fp = embedding_flops + total_block_flops + final_norm_flops + + log.debug(f"Forward pass FLOPs: {fp}") + log.debug(f" embedding_flops: {embedding_flops} ({_pct(embedding_flops, fp)}%)") + log.debug(f" transformer_blocks: {total_block_flops} ({_pct(total_block_flops, fp)}%)") + log.debug(f" final_norms: {final_norm_flops} ({_pct(final_norm_flops, fp)}%)") + + # =================================================================== + # 5. Backward pass + # =================================================================== + + if freeze_und: + # When freeze_und is True, the understanding pathway gradients are detached. + # Backward cost: gen-pathway projections/MLPs, gen-side attention (gen Q + # attends to the full sample), gen norms, and gen embedding layers. + # Causal (und) attention has zero backward cost. + gen_proj_mlp_flops = Decimal(0) + gen_norm_flops = Decimal(0) + for layer_idx in range(n_layers): + is_moe_layer = ( + cfg.use_moe + and cfg.num_experts > 0 + and layer_idx not in cfg.mlp_only_layers + and (layer_idx + 1) % cfg.decoder_sparse_step == 0 + ) + gen_proj_mlp_flops += ( + 2 * B * S_gen * D * q_dim + + 2 * B * S_gen * D * kv_dim + + 2 * B * S_gen * D * kv_dim + + 2 * B * S_gen * q_dim * D + ) + gen_proj_mlp_flops += _moe_mlp_flops(S_gen) if is_moe_layer else _dense_mlp_flops(S_gen) + + gen_norm_flops += 5 * B * S_gen * D * 2 + gen_norm_flops += 5 * B * S_gen * n_heads * d_head + 5 * B * S_gen * n_kv_heads * d_head + + gen_norm_flops += 5 * B * S_gen * D + + gen_embedding_flops = embedding_flops # conservative: count all embedding flops + + backward_attn_flops = gen_attn_dot * n_layers + backward_softmax_flops = gen_softmax * n_layers + + bp = ( + gen_proj_mlp_flops + backward_attn_flops + backward_softmax_flops + gen_norm_flops + gen_embedding_flops + ) * bp_ratio + + else: + bp = fp * bp_ratio + + # =================================================================== + # 6. Activation checkpointing recomputation + # =================================================================== + # When activation checkpointing is enabled, each transformer layer's + # forward pass is fully recomputed during the backward pass. This adds + # ~1x of the transformer-block forward FLOPs (projections, attention + # dot products, softmax, MLP, and norms — everything inside the layer). + ac_recomp = Decimal(0) + if use_activation_checkpointing: + ac_recomp = total_block_flops + + total = fp + bp + ac_recomp + + log.debug(f"Backward pass FLOPs: {bp}") + if use_activation_checkpointing: + log.debug(f"Activation checkpointing recomp FLOPs: {ac_recomp}") + log.debug(f"Total FLOPs: {total}") + + return total diff --git a/cosmos3/_src/imaginaire/flops/wan_vae.py b/cosmos3/_src/imaginaire/flops/wan_vae.py new file mode 100644 index 00000000..11aff78c --- /dev/null +++ b/cosmos3/_src/imaginaire/flops/wan_vae.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FLOPs estimation for the Wan 2.2 VAE encoder (Encoder3d).""" + +from decimal import Decimal + + +def compute_wan_vae_encoder_flops( + B: int | Decimal, + T: int, + H: int, + W: int, + *, + dim: int = 160, + z_dim: int = 48, + dim_mult: list[int] | None = None, + num_res_blocks: int = 2, + temperal_downsample: list[bool] | None = None, +) -> Decimal: + """Compute forward-pass FLOPs for the Wan 2.2 VAE encoder (Encoder3d). + + The encoder converts a pixel-space video [B, 3, T, H, W] into a latent + [B, z_dim, T//4, H//16, W//16]. It is frozen during training so only + forward-pass FLOPs are counted (no backward). + + The architecture: patchify(2) -> conv1 -> 4 downsample stages (each with + ``num_res_blocks`` residual blocks + optional spatial/temporal downsample) + -> middle block (ResBlock + single-head spatial attention + ResBlock) + -> head (RMSNorm + SiLU + conv) -> pointwise 1x1 conv. + + Args: + B: Batch size. + T: Number of pixel-space temporal frames. + H: Pixel-space height (must be divisible by 16). + W: Pixel-space width (must be divisible by 16). + dim: Base channel dimension of the encoder (default 160). + z_dim: Latent channel dimension (default 48, encoder outputs 2*z_dim). + dim_mult: Channel multiplier per stage (default [1, 2, 4, 4]). + num_res_blocks: Residual blocks per downsample stage (default 2). + temperal_downsample: Per-stage temporal downsampling flags (default + [False, True, True]). + + Returns: + Total forward-pass FLOPs as a Decimal. + """ + if dim_mult is None: + dim_mult = [1, 2, 4, 4] + if temperal_downsample is None: + temperal_downsample = [False, True, True] + + B = int(B) + flops = Decimal(0) + + def _causalconv3d_flops(c_in: int, c_out: int, kt: int, kh: int, kw: int, bt: int, bh: int, bw: int) -> int: + return 2 * c_out * c_in * kt * kh * kw * B * bt * bh * bw + + def _resblock_flops(in_dim: int, out_dim: int, bt: int, bh: int, bw: int) -> int: + vol = B * bt * bh * bw + f = 0 + f += 5 * in_dim * vol # RMS_norm(in_dim) + f += 2 * out_dim * in_dim * 27 * vol # CausalConv3d(in_dim, out_dim, 3) + f += 5 * out_dim * vol # RMS_norm(out_dim) + f += 2 * out_dim * out_dim * 27 * vol # CausalConv3d(out_dim, out_dim, 3) + if in_dim != out_dim: + f += 2 * out_dim * in_dim * vol # shortcut CausalConv3d(in_dim, out_dim, 1) + return f + + def _attnblock_flops(d: int, bt: int, bh: int, bw: int) -> int: + vol = B * bt * bh * bw + seq = bh * bw + f = 0 + f += 5 * d * vol # RMS_norm + f += 2 * (d * 3) * d * vol # to_qkv Conv2d(d, 3d, 1) + f += 4 * B * bt * seq * seq * d # QK^T + Attn*V + f += 2 * d * d * vol # proj Conv2d(d, d, 1) + return f + + # After patchify(patch_size=2): [B, 12, T, H/2, W/2] + t, h, w = T, H // 2, W // 2 + + # conv1: CausalConv3d(12, dims[0], 3) + dims = [dim * u for u in [1] + dim_mult] # [160, 160, 320, 640, 640] + flops += _causalconv3d_flops(12, dims[0], 3, 3, 3, t, h, w) + + # Downsample stages + for i, (in_d, out_d) in enumerate(zip(dims[:-1], dims[1:])): + t_down = temperal_downsample[i] if i < len(temperal_downsample) else False + down_flag = i != len(dim_mult) - 1 + + cur_in = in_d + for _ in range(num_res_blocks): + flops += _resblock_flops(cur_in, out_d, t, h, w) + cur_in = out_d + + if down_flag: + if t_down: + h_new, w_new = h // 2, w // 2 + flops += 2 * out_d * out_d * 9 * B * t * h_new * w_new # spatial conv2d + t_new = t // 2 + flops += 2 * out_d * out_d * 3 * B * t_new * h_new * w_new # temporal conv3d(3,1,1) + t, h, w = t_new, h_new, w_new + else: + h_new, w_new = h // 2, w // 2 + flops += 2 * out_d * out_d * 9 * B * t * h_new * w_new + h, w = h_new, w_new + + # Middle block: ResBlock + AttentionBlock + ResBlock + mid_dim = dims[-1] + flops += _resblock_flops(mid_dim, mid_dim, t, h, w) + flops += _attnblock_flops(mid_dim, t, h, w) + flops += _resblock_flops(mid_dim, mid_dim, t, h, w) + + # Head: RMS_norm + SiLU + CausalConv3d(mid_dim, z_dim*2, 3) + enc_out_dim = z_dim * 2 + flops += 5 * mid_dim * B * t * h * w # RMS_norm + flops += _causalconv3d_flops(mid_dim, enc_out_dim, 3, 3, 3, t, h, w) + + # WanVAE_.conv1: CausalConv3d(z_dim*2, z_dim*2, 1) — pointwise 1x1 + flops += _causalconv3d_flops(enc_out_dim, enc_out_dim, 1, 1, 1, t, h, w) + + return Decimal(flops) diff --git a/cosmos3/_src/imaginaire/functional/batch_ops.py b/cosmos3/_src/imaginaire/functional/batch_ops.py new file mode 100644 index 00000000..e60fce3a --- /dev/null +++ b/cosmos3/_src/imaginaire/functional/batch_ops.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Functions for performing operations with broadcasting to the right axis +# +# Example +# input1: tensor of size (N1, N2) +# input2: tensor of size (N1, N2, N3, N4) +# batch_mul(input1, input2) = input1[:, :, None, None] * input2 +# +# If the common dimensions don't match, we raise an assertion error. + +from torch import Tensor + + +def common_broadcast(x: Tensor, y: Tensor) -> tuple[Tensor, Tensor]: + ndims1 = x.ndim + ndims2 = y.ndim + + common_ndims = min(ndims1, ndims2) + for axis in range(common_ndims): + assert x.shape[axis] == y.shape[axis], "Dimensions not equal at axis {}".format(axis) + + if ndims1 < ndims2: + x = x.reshape(x.shape + (1,) * (ndims2 - ndims1)) # x broadcast-padded to ndims2: [*x.shape,1,...] + elif ndims2 < ndims1: + y = y.reshape(y.shape + (1,) * (ndims1 - ndims2)) # y broadcast-padded to ndims1: [*y.shape,1,...] + + return x, y + + +def batch_add(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x + y # broadcast result shape + + +def batch_mul(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x * y # broadcast result shape + + +def batch_sub(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x - y # broadcast result shape + + +def batch_div(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x / y # broadcast result shape diff --git a/cosmos3/_src/imaginaire/functional/lr_scheduler.py b/cosmos3/_src/imaginaire/functional/lr_scheduler.py new file mode 100644 index 00000000..ef9cfb5e --- /dev/null +++ b/cosmos3/_src/imaginaire/functional/lr_scheduler.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import numpy as np + +from cosmos3._src.imaginaire.utils import distributed, log + + +class TeroPolyScheduler: + def __init__( + self, + total_Mimg: int, + batch_size: int, + ref_Mimg: Optional[int] = None, + ref_batches: float = 70e3 / 1024, + max_lr_ratio: Optional[float] = 1.0, + min_lr_ratio: Optional[float] = None, + rampup_Mimg: float = 0, + rampdown_Mimg: int = 0, + verbosity_interval: int = 0, + formula: str = "poly", + poly_exp: float = 0.5, + ): + self.total_Mimg = total_Mimg + self.batch_size = batch_size * distributed.get_world_size() + self.ref_Mimg = ref_Mimg or ref_batches * batch_size / 1e6 + self.ref_batches = ref_batches + self.max_lr_ratio = max_lr_ratio + self.min_lr_ratio = min_lr_ratio + self.rampup_Mimg = rampup_Mimg + self.rampdown_Mimg = rampdown_Mimg + self.verbosity_interval = verbosity_interval + self.formula = formula + self.poly_exp = poly_exp + + self._model = None + + @property + def model(self): + return self._model + + @model.setter + def model(self, model): + self._model = model + + def schedule(self, n, **kwargs): + cur_Mimg = getattr(self.model, "sample_counter", 0) / 1e6 + + if self.formula == "constant": + lr = 1.0 + elif self.formula == "poly": + lr = max(cur_Mimg / self.ref_Mimg, 1e-8) ** -self.poly_exp + else: + raise ValueError(f'Invalid learning rate formula "{self.formula}"') + + if self.max_lr_ratio is not None: + lr = min(lr, self.max_lr_ratio) + if self.min_lr_ratio is not None: + lr = max(lr, self.min_lr_ratio) + + if self.rampup_Mimg > 0 and cur_Mimg < self.rampup_Mimg: + lr *= cur_Mimg / self.rampup_Mimg + if self.rampdown_Mimg > 0 and cur_Mimg > self.total_Mimg - self.rampdown_Mimg: + lr *= (self.total_Mimg - cur_Mimg) / self.rampdown_Mimg + + return lr + + def __call__(self, n, **kwargs): + return self.schedule(n, **kwargs) + + +class LambdaWarmUpCosineScheduler: + """ + A learning rate scheduler that combines warm-up with a cosine decay schedule for multiple cycles. + It supports different configurations for each cycle, including the number of warm-up steps, minimum + and maximum scaling factors for the learning rate. + + The scheduler is intended to be used with a base learning rate of 1.0, where the actual learning + rate at any step is the base learning rate multiplied by the scaling factor computed by the scheduler. + + Parameters: + warm_up_steps (list[int]): List of integers where each element represents the number of warm-up + steps for the corresponding cycle. + f_min (list[float]): List of the minimum scaling factors for each cycle after warm-up. + f_max (list[float]): List of the maximum scaling factors at the start and end of each cosine cycle. + f_start (list[float]): List of starting scaling factors for each warm-up phase. + cycle_lengths (list[int]): List of the total lengths of each cycle, including warm-up steps. + verbosity_interval (int, optional): Interval of training steps at which to print current step and + scaling factor information. Set to 0 by default to disable verbosity. + + Examples: + >>> scheduler = LambdaWarmUpCosineScheduler2( + warm_up_steps=[10, 10], + f_min=[0.1, 0.1], + f_max=[1.0, 1.0], + f_start=[0.01, 0.01], + cycle_lengths=[50, 50], + verbosity_interval=10) + >>> for step in range(100): + >>> lr_multiplier = scheduler(step) + >>> print(f"Step {step}: LR Multiplier = {lr_multiplier}") + """ + + def __init__(self, warm_up_steps, f_min, f_max, f_start, cycle_lengths, verbosity_interval=0): + assert len(warm_up_steps) == len(f_min) == len(f_max) == len(f_start) == len(cycle_lengths) + self.lr_warm_up_steps = warm_up_steps + self.f_start = f_start + self.f_min = f_min + self.f_max = f_max + self.cycle_lengths = cycle_lengths + self.cum_cycles = np.cumsum([0] + list(self.cycle_lengths)) + self.last_f = 0.0 + self.verbosity_interval = verbosity_interval + + def find_in_interval(self, n): + interval = 0 + for cl in self.cum_cycles[1:]: + if n <= cl: + return interval + interval += 1 + + def schedule(self, n, **kwargs): + cycle = self.find_in_interval(n) + n = n - self.cum_cycles[cycle] + if self.verbosity_interval > 0: + if n % self.verbosity_interval == 0: + log.info(f"current step: {n}, recent lr-multiplier: {self.last_f}, current cycle {cycle}") + if n < self.lr_warm_up_steps[cycle]: + f = (self.f_max[cycle] - self.f_start[cycle]) / self.lr_warm_up_steps[cycle] * n + self.f_start[cycle] + self.last_f = f + return f + else: + t = (n - self.lr_warm_up_steps[cycle]) / (self.cycle_lengths[cycle] - self.lr_warm_up_steps[cycle]) + t = min(t, 1.0) + f = self.f_min[cycle] + 0.5 * (self.f_max[cycle] - self.f_min[cycle]) * (1 + np.cos(t * np.pi)) + self.last_f = f + return f + + def __call__(self, n, **kwargs): + return self.schedule(n, **kwargs) + + +class LambdaLinearScheduler(LambdaWarmUpCosineScheduler): + """ + Linear instead of cosine decay for the main part of the cycle. + """ + + def schedule(self, n, **kwargs): + cycle = self.find_in_interval(n) + n = n - self.cum_cycles[cycle] + if self.verbosity_interval > 0: + if n % self.verbosity_interval == 0: + log.info(f"current step: {n}, recent lr-multiplier: {self.last_f}, current cycle {cycle}") + + if n < self.lr_warm_up_steps[cycle]: + f = (self.f_max[cycle] - self.f_start[cycle]) / self.lr_warm_up_steps[cycle] * n + self.f_start[cycle] + self.last_f = f + return f + else: + f = self.f_min[cycle] + (self.f_max[cycle] - self.f_min[cycle]) * (self.cycle_lengths[cycle] - n) / ( + self.cycle_lengths[cycle] - self.lr_warm_up_steps[cycle] + ) + self.last_f = f + return f diff --git a/cosmos3/_src/imaginaire/functional/multi_step.py b/cosmos3/_src/imaginaire/functional/multi_step.py new file mode 100644 index 00000000..48f96ff0 --- /dev/null +++ b/cosmos3/_src/imaginaire/functional/multi_step.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Impl of multistep methods to solve the ODE in the diffusion model. +""" + +from typing import Callable, List, Tuple + +import torch + +from cosmos3._src.imaginaire.functional.runge_kutta import reg_x0_euler_step, res_x0_rk2_step + + +def order2_fn( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_s: torch.Tensor, x0_preds: torch.Tensor +) -> Tuple[torch.Tensor, List[torch.Tensor]]: + """ + impl the second order multistep method in https://arxiv.org/pdf/2308.02157 + Adams Bashforth approach! + """ + if x0_preds: + x0_s1, s1 = x0_preds[0] + x_t = res_x0_rk2_step(x_s, t, s, x0_s, s1, x0_s1) + else: + x_t = reg_x0_euler_step(x_s, s, t, x0_s)[0] + return x_t, [(x0_s, s)] + + +# key: method name, value: method function +# key: order + algorithm name +MULTISTEP_FNs = { + "2ab": order2_fn, +} + + +def get_multi_step_fn(name: str) -> Callable: + if name in MULTISTEP_FNs: + return MULTISTEP_FNs[name] + methods = "\n\t".join(MULTISTEP_FNs.keys()) + raise RuntimeError("Only support multistep method\n" + methods) + + +def is_multi_step_fn_supported(name: str) -> bool: + """ + Check if the multistep method is supported. + """ + return name in MULTISTEP_FNs diff --git a/cosmos3/_src/imaginaire/functional/runge_kutta.py b/cosmos3/_src/imaginaire/functional/runge_kutta.py new file mode 100644 index 00000000..fcb667c3 --- /dev/null +++ b/cosmos3/_src/imaginaire/functional/runge_kutta.py @@ -0,0 +1,333 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Tuple + +import torch + +from cosmos3._src.imaginaire.functional.batch_ops import batch_mul + + +def phi1(t: torch.Tensor) -> torch.Tensor: + """ + Compute the first order phi function: (exp(t) - 1) / t. + + Args: + t: Input tensor. + + Returns: + Tensor: Result of phi1 function. + """ + input_dtype = t.dtype + t = t.to(dtype=torch.float64) + return (torch.expm1(t) / t).to(dtype=input_dtype) + + +def phi2(t: torch.Tensor) -> torch.Tensor: + """ + Compute the second order phi function: (phi1(t) - 1) / t. + + Args: + t: Input tensor. + + Returns: + Tensor: Result of phi2 function. + """ + input_dtype = t.dtype + t = t.to(dtype=torch.float64) + return ((phi1(t) - 1.0) / t).to(dtype=input_dtype) + + +def res_x0_rk2_step( + x_s: torch.Tensor, + t: torch.Tensor, + s: torch.Tensor, + x0_s: torch.Tensor, + s1: torch.Tensor, + x0_s1: torch.Tensor, +) -> torch.Tensor: + """ + Perform a residual-based 2nd order Runge-Kutta step. + + Args: + x_s: Current state tensor. + t: Target time tensor. + s: Current time tensor. + x0_s: Prediction at current time. + s1: Intermediate time tensor. + x0_s1: Prediction at intermediate time. + + Returns: + Tensor: Updated state tensor. + + Raises: + AssertionError: If step size is too small. + """ + s = -torch.log(s) # scalar or [B] + t = -torch.log(t) # scalar or [B] + m = -torch.log(s1) # scalar or [B] + + dt = t - s # scalar or [B] + assert not torch.any(torch.isclose(dt, torch.zeros_like(dt), atol=1e-6)), "Step size is too small" + assert not torch.any(torch.isclose(m - s, torch.zeros_like(dt), atol=1e-6)), "Step size is too small" + + c2 = (m - s) / dt # scalar or [B] + phi1_val, phi2_val = phi1(-dt), phi2(-dt) # scalar or [B] each + + # Handle edge case where t = s = m + b1 = torch.nan_to_num(phi1_val - 1.0 / c2 * phi2_val, nan=0.0) # scalar or [B] + b2 = torch.nan_to_num(1.0 / c2 * phi2_val, nan=0.0) # scalar or [B] + + return batch_mul(torch.exp(-dt), x_s) + batch_mul(dt, batch_mul(b1, x0_s) + batch_mul(b2, x0_s1)) # [B,...] + + +def reg_x0_euler_step( + x_s: torch.Tensor, + s: torch.Tensor, + t: torch.Tensor, + x0_s: torch.Tensor, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a regularized Euler step based on x0 prediction. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_s: Prediction at current time. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current prediction. + """ + coef_x0 = (s - t) / s # scalar or [B] + coef_xs = t / s # scalar or [B] + return batch_mul(coef_x0, x0_s) + batch_mul(coef_xs, x_s), x0_s # [B,...], [B,...] + + +def reg_eps_euler_step( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, eps_s: torch.Tensor +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a regularized Euler step based on epsilon prediction. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + eps_s: Epsilon prediction at current time. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current x0 prediction. + """ + return x_s + batch_mul(eps_s, t - s), x_s + batch_mul(eps_s, 0 - s) # [B,...], [B,...] + + +def rk1_euler( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a first-order Runge-Kutta (Euler) step. + + Recommended for diffusion models with guidance or model undertrained + Usually more stable at the cost of a bit slower convergence. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + x0_s = x0_fn(x_s, s) # [B,...] + return reg_x0_euler_step(x_s, s, t, x0_s) # [B,...], [B,...] + + +def rk2_mid_stable( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a stable second-order Runge-Kutta (midpoint) step. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + s1 = torch.sqrt(s * t) # scalar or [B] + x_s1, _ = rk1_euler(x_s, s, s1, x0_fn) # [B,...] + + x0_s1 = x0_fn(x_s1, s1) # [B,...] + return reg_x0_euler_step(x_s, s, t, x0_s1) # [B,...], [B,...] + + +def rk2_mid(x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a second-order Runge-Kutta (midpoint) step. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + s1 = torch.sqrt(s * t) # scalar or [B] + x_s1, x0_s = rk1_euler(x_s, s, s1, x0_fn) # [B,...], [B,...] + + x0_s1 = x0_fn(x_s1, s1) # [B,...] + + return res_x0_rk2_step(x_s, t, s, x0_s, s1, x0_s1), x0_s1 # [B,...], [B,...] + + +def rk_2heun_naive( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive second-order Runge-Kutta (Heun's method) step. + Impl based on rho-rk-deis solvers, https://github.com/qsh-zh/deis + Recommended for diffusion models without guidance and relative large NFE + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + x_t, x0_s = rk1_euler(x_s, s, t, x0_fn) # [B,...], [B,...] + eps_s = batch_mul(1.0 / s, x_t - x0_s) # [B,...] + x0_t = x0_fn(x_t, t) # [B,...] + eps_t = batch_mul(1.0 / t, x_t - x0_t) # [B,...] + + avg_eps = (eps_s + eps_t) / 2 # [B,...] + + return reg_eps_euler_step(x_s, s, t, avg_eps) # [B,...], [B,...] + + +def rk_2heun_edm( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive second-order Runge-Kutta (Heun's method) step. + Impl based no EDM second order Heun method + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + x_t, x0_s = rk1_euler(x_s, s, t, x0_fn) # [B,...], [B,...] + x0_t = x0_fn(x_t, t) # [B,...] + + avg_x0 = (x0_s + x0_t) / 2 # [B,...] + + return reg_x0_euler_step(x_s, s, t, avg_x0) # [B,...], [B,...] + + +def rk_3kutta_naive( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive third-order Runge-Kutta step. + Impl based on rho-rk-deis solvers, https://github.com/qsh-zh/deis + Recommended for diffusion models without guidance and relative large NFE + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + c2, c3 = 0.5, 1.0 # stage time fractions + a31, a32 = -1.0, 2.0 # Butcher tableau coefficients + b1, b2, b3 = 1.0 / 6, 4.0 / 6, 1.0 / 6 # quadrature weights + + delta = t - s # scalar or [B] + + s1 = c2 * delta + s # scalar or [B] + s2 = c3 * delta + s # scalar or [B] + x_s1, x0_s = rk1_euler(x_s, s, s1, x0_fn) # [B,...], [B,...] + eps_s = batch_mul(1.0 / s, x_s - x0_s) # [B,...] + x0_s1 = x0_fn(x_s1, s1) # [B,...] + eps_s1 = batch_mul(1.0 / s1, x_s1 - x0_s1) # [B,...] + + _eps = a31 * eps_s + a32 * eps_s1 # [B,...] + x_s2, _ = reg_eps_euler_step(x_s, s, s2, _eps) # [B,...] + + x0_s2 = x0_fn(x_s2, s2) # [B,...] + eps_s2 = batch_mul(1.0 / s2, x_s2 - x0_s2) # [B,...] + + avg_eps = b1 * eps_s + b2 * eps_s1 + b3 * eps_s2 # [B,...] + return reg_eps_euler_step(x_s, s, t, avg_eps) # [B,...], [B,...] + + +# key : order + name +RK_FNs = { + "1euler": rk1_euler, + "2mid": rk2_mid, + "2mid_stable": rk2_mid_stable, + "2heun_edm": rk_2heun_edm, + "2heun_naive": rk_2heun_naive, + "3kutta_naive": rk_3kutta_naive, +} + + +def get_runge_kutta_fn(name: str) -> Callable: + """ + Get the specified Runge-Kutta function. + + Args: + name: Name of the Runge-Kutta method. + + Returns: + Callable: The specified Runge-Kutta function. + + Raises: + RuntimeError: If the specified method is not supported. + """ + if name in RK_FNs: + return RK_FNs[name] + methods = "\n\t".join(RK_FNs.keys()) + raise RuntimeError(f"Only support the following Runge-Kutta methods:\n\t{methods}") + + +def is_runge_kutta_fn_supported(name: str) -> bool: + """ + Check if the specified Runge-Kutta function is supported. + + Args: + name: Name of the Runge-Kutta method. + + Returns: + bool: True if the method is supported, False otherwise. + """ + return name in RK_FNs diff --git a/cosmos3/_src/imaginaire/lazy_config/__init__.py b/cosmos3/_src/imaginaire/lazy_config/__init__.py new file mode 100644 index 00000000..43f67a1d --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/__init__.py @@ -0,0 +1,70 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +import os + +from omegaconf import DictConfig, OmegaConf + +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.lazy_config.instantiate import instantiate +from cosmos3._src.imaginaire.lazy_config.lazy_call import LazyCall +from cosmos3._src.imaginaire.lazy_config.omegaconf_patch import to_object + +OmegaConf.to_object = to_object + +PLACEHOLDER = None + + +class LazyDict(DictConfig): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +__all__ = ["instantiate", "LazyCall", "PLACEHOLDER", "LazyDict"] +if TRAINING: + from cosmos3._src.imaginaire.lazy_config.lazy import LazyConfig + + __all__ += ["LazyConfig"] + + +DOC_BUILDING = os.getenv("_DOC_BUILDING", False) # set in docs/conf.py + + +def fixup_module_metadata(module_name, namespace, keys=None): + """ + Fix the __qualname__ of module members to be their exported api name, so + when they are referenced in docs, sphinx can find them. Reference: + https://github.com/python-trio/trio/blob/6754c74eacfad9cc5c92d5c24727a2f3b620624e/trio/_util.py#L216-L241 + """ + if not DOC_BUILDING: + return + seen_ids = set() + + def fix_one(qualname, name, obj): + # avoid infinite recursion (relevant when using + # typing.Generic, for example) + if id(obj) in seen_ids: + return + seen_ids.add(id(obj)) + + mod = getattr(obj, "__module__", None) + if mod is not None and (mod.startswith(module_name) or mod.startswith("fvcore.")): + obj.__module__ = module_name + # Modules, unlike everything else in Python, put fully-qualitied + # names into their __name__ attribute. We check for "." to avoid + # rewriting these. + if hasattr(obj, "__name__") and "." not in obj.__name__: + obj.__name__ = name + obj.__qualname__ = qualname + if isinstance(obj, type): + for attr_name, attr_value in obj.__dict__.items(): + fix_one(objname + "." + attr_name, attr_name, attr_value) + + if keys is None: + keys = namespace.keys() + for objname in keys: + if not objname.startswith("_"): + obj = namespace[objname] + fix_one(objname, objname, obj) + + +fixup_module_metadata(__name__, globals(), __all__) +del fixup_module_metadata diff --git a/cosmos3/_src/imaginaire/lazy_config/file_io.py b/cosmos3/_src/imaginaire/lazy_config/file_io.py new file mode 100644 index 00000000..0c6693f4 --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/file_io.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from iopath.common.file_io import HTTPURLHandler, OneDrivePathHandler, PathHandler +from iopath.common.file_io import PathManager as PathManagerBase + +__all__ = ["PathManager", "PathHandler"] + + +PathManager = PathManagerBase() +PathManager.register_handler(HTTPURLHandler()) +PathManager.register_handler(OneDrivePathHandler()) diff --git a/cosmos3/_src/imaginaire/lazy_config/instantiate.py b/cosmos3/_src/imaginaire/lazy_config/instantiate.py new file mode 100644 index 00000000..66d81a8c --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/instantiate.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections.abc as abc +import dataclasses +import logging +from typing import Any + +import attrs + +from cosmos3._src.imaginaire.lazy_config.registry import _convert_target_to_string, locate + +__all__ = ["dump_dataclass", "instantiate"] + + +def is_dataclass_or_attrs(target): + return dataclasses.is_dataclass(target) or attrs.has(target) + + +def dump_dataclass(obj: Any): + """ + Dump a dataclass recursively into a dict that can be later instantiated. + + Args: + obj: a dataclass object + + Returns: + dict + """ + assert dataclasses.is_dataclass(obj) and not isinstance(obj, type), ( + "dump_dataclass() requires an instance of a dataclass." + ) + ret = {"_target_": _convert_target_to_string(type(obj))} + for f in dataclasses.fields(obj): + v = getattr(obj, f.name) + if dataclasses.is_dataclass(v): + v = dump_dataclass(v) + if isinstance(v, (list, tuple)): + v = [dump_dataclass(x) if dataclasses.is_dataclass(x) else x for x in v] + ret[f.name] = v + return ret + + +def instantiate(cfg, *args, **kwargs): + """ + Recursively instantiate objects defined in dictionaries by + "_target_" and arguments. + + Args: + cfg: a dict-like object with "_target_" that defines the caller, and + other keys that define the arguments + args: Optional positional parameters pass-through. + kwargs: Optional named parameters pass-through. + + Returns: + object instantiated by cfg + """ + from omegaconf import DictConfig, ListConfig, OmegaConf + + if isinstance(cfg, ListConfig): + lst = [instantiate(x) for x in cfg] + return ListConfig(lst, flags={"allow_objects": True}) + if isinstance(cfg, list): + # Specialize for list, because many classes take + # list[objects] as arguments, such as ResNet, DatasetMapper + return [instantiate(x) for x in cfg] + + # If input is a DictConfig backed by dataclasses (i.e. omegaconf's structured config), + # instantiate it to the actual dataclass. + if isinstance(cfg, DictConfig) and is_dataclass_or_attrs(cfg._metadata.object_type): + return OmegaConf.to_object(cfg) + + if isinstance(cfg, abc.Mapping) and "_target_" in cfg: + # conceptually equivalent to hydra.utils.instantiate(cfg) with _convert_=all, + # but faster: https://github.com/facebookresearch/hydra/issues/1200 + is_recursive = getattr(cfg, "_recursive_", True) + if is_recursive: + cfg = {k: instantiate(v) for k, v in cfg.items()} + else: + cfg = {k: v for k, v in cfg.items()} + # pop the _recursive_ key to avoid passing it as a parameter + if "_recursive_" in cfg: + cfg.pop("_recursive_") + cls = cfg.pop("_target_") + cls = instantiate(cls) + + if isinstance(cls, str): + cls_name = cls + cls = locate(cls_name) + assert cls is not None, cls_name + else: + try: + cls_name = cls.__module__ + "." + cls.__qualname__ + except Exception: + # target could be anything, so the above could fail + cls_name = str(cls) + assert callable(cls), f"_target_ {cls} does not define a callable object" + try: + # override config with kwargs + instantiate_kwargs = {} + instantiate_kwargs.update(cfg) + instantiate_kwargs.update(kwargs) + return cls(*args, **instantiate_kwargs) + except TypeError: + logger = logging.getLogger(__name__) + logger.error(f"Error when instantiating {cls_name}!") + raise + return cfg # return as-is if don't know what to do diff --git a/cosmos3/_src/imaginaire/lazy_config/lazy.py b/cosmos3/_src/imaginaire/lazy_config/lazy.py new file mode 100644 index 00000000..846c0117 --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/lazy.py @@ -0,0 +1,377 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ast +import builtins +import importlib +import inspect +import logging +import os +import pickle +import uuid +from collections import OrderedDict +from contextlib import contextmanager +from copy import deepcopy +from typing import Any, Dict, List, Tuple, Union + +import attrs +import yaml +from omegaconf import DictConfig, ListConfig, OmegaConf + +try: + import dill as dill_pickle +except ImportError: + dill_pickle = None + +try: + import cloudpickle +except ImportError: + cloudpickle = None + +from cosmos3._src.imaginaire.lazy_config.file_io import PathManager +from cosmos3._src.imaginaire.lazy_config.lazy_call import LazyCall, get_default_params + +__all__ = ["LazyCall", "LazyConfig"] + + +def sort_dict(d: Dict[str, Any]) -> OrderedDict[str, Any]: + return OrderedDict(sorted(d.items(), key=lambda x: x[0])) + + +def dict_representer(dumper: yaml.Dumper, data: OrderedDict[str, Any]) -> yaml.nodes.MappingNode: + return dumper.represent_mapping("tag:yaml.org,2002:map", data.items()) + + +def sort_recursive(obj: Union[Dict[str, Any], List[Any], Any]) -> Union[OrderedDict[str, Any], List[Any], Any]: + if isinstance(obj, dict): + return sort_dict({k: sort_recursive(v) for k, v in obj.items()}) + elif isinstance(obj, list): + return [sort_recursive(item) for item in obj] + return obj + + +yaml.add_representer(OrderedDict, dict_representer) + +OmegaConf.register_new_resolver("add", lambda *vals: sum(vals)) +OmegaConf.register_new_resolver("subtract", lambda *vals: vals[0] - sum(vals[1:])) + + +def _visit_dict_config(cfg, func): + """ + Apply func recursively to all DictConfig in cfg. + """ + if isinstance(cfg, DictConfig): + func(cfg) + for v in cfg.values(): + _visit_dict_config(v, func) + elif isinstance(cfg, ListConfig): + for v in cfg: + _visit_dict_config(v, func) + + +def _validate_py_syntax(filename): + # see also https://github.com/open-mmlab/mmcv/blob/master/mmcv/utils/config.py + with PathManager.open(filename, "r") as f: + content = f.read() + try: + ast.parse(content) + except SyntaxError as e: + raise SyntaxError(f"Config file {filename} has syntax error!") from e + + +def _cast_to_config(obj): + # if given a dict, return DictConfig instead + if isinstance(obj, dict): + return DictConfig(obj, flags={"allow_objects": True}) + return obj + + +_CFG_PACKAGE_NAME = "detectron2._cfg_loader" +""" +A namespace to put all imported config into. +""" + + +def _random_package_name(filename): + # generate a random package name when loading config files + return _CFG_PACKAGE_NAME + str(uuid.uuid4())[:4] + "." + os.path.basename(filename) + + +@contextmanager +def _patch_import(): + """ + Enhance relative import statements in config files, so that they: + 1. locate files purely based on relative location, regardless of packages. + e.g. you can import file without having __init__ + 2. do not cache modules globally; modifications of module states has no side effect + 3. support other storage system through PathManager, so config files can be in the cloud + 4. imported dict are turned into omegaconf.DictConfig automatically + """ + old_import = builtins.__import__ + + def find_relative_file(original_file, relative_import_path, level): + + # if such import should produce `x` as a python module or DictConfig. + # This can be discussed further if needed. + relative_import_err = """ +Relative import of directories is not allowed within config files. +Within a config file, relative import can only import other config files. +""".replace("\n", " ") + if not len(relative_import_path): + raise ImportError(relative_import_err) + + cur_file = os.path.dirname(original_file) + for _ in range(level - 1): + cur_file = os.path.dirname(cur_file) + cur_name = relative_import_path.lstrip(".") + for part in cur_name.split("."): + cur_file = os.path.join(cur_file, part) + if not cur_file.endswith(".py"): + cur_file += ".py" + if not PathManager.isfile(cur_file): + cur_file_no_suffix = cur_file[: -len(".py")] + if PathManager.isdir(cur_file_no_suffix): + raise ImportError(f"Cannot import from {cur_file_no_suffix}." + relative_import_err) + else: + raise ImportError( + f"Cannot import name {relative_import_path} from {original_file}: {cur_file} does not exist." + ) + return cur_file + + def new_import(name, globals=None, locals=None, fromlist=(), level=0): + if ( + # Only deal with relative imports inside config files + level != 0 and globals is not None and (globals.get("__package__", "") or "").startswith(_CFG_PACKAGE_NAME) + ): + cur_file = find_relative_file(globals["__file__"], name, level) + _validate_py_syntax(cur_file) + spec = importlib.machinery.ModuleSpec(_random_package_name(cur_file), None, origin=cur_file) + module = importlib.util.module_from_spec(spec) + module.__file__ = cur_file + with PathManager.open(cur_file) as f: + content = f.read() + exec(compile(content, cur_file, "exec"), module.__dict__) + for name in fromlist: # turn imported dict into DictConfig automatically + val = _cast_to_config(module.__dict__[name]) + module.__dict__[name] = val + return module + return old_import(name, globals, locals, fromlist=fromlist, level=level) + + builtins.__import__ = new_import + yield new_import + builtins.__import__ = old_import + + +class LazyConfig: + """ + Provide methods to save, load, and overrides an omegaconf config object + which may contain definition of lazily-constructed objects. + """ + + @staticmethod + def load_rel(filename: str, keys: Union[None, str, Tuple[str, ...]] = None): + """ + Similar to :meth:`load()`, but load path relative to the caller's + source file. + + This has the same functionality as a relative import, except that this method + accepts filename as a string, so more characters are allowed in the filename. + """ + caller_frame = inspect.stack()[1] + caller_fname = caller_frame[0].f_code.co_filename + assert caller_fname != "", "load_rel Unable to find caller" + caller_dir = os.path.dirname(caller_fname) + filename = os.path.join(caller_dir, filename) + return LazyConfig.load(filename, keys) + + @staticmethod + def load(filename: str, keys: Union[None, str, Tuple[str, ...]] = None): + """ + Load a config file. + + Args: + filename: absolute path or relative path w.r.t. the current working directory + keys: keys to load and return. If not given, return all keys + (whose values are config objects) in a dict. + """ + has_keys = keys is not None + filename = filename.replace("/./", "/") # redundant + if os.path.splitext(filename)[1] not in [".py", ".yaml", ".yml"]: + raise ValueError(f"Config file {filename} has to be a python or yaml file.") + if filename.endswith(".py"): + _validate_py_syntax(filename) + + with _patch_import(): + # Record the filename + module_namespace = { + "__file__": filename, + "__package__": _random_package_name(filename), + } + with PathManager.open(filename) as f: + content = f.read() + # Compile first with filename to: + # 1. make filename appears in stacktrace + # 2. make load_rel able to find its parent's (possibly remote) location + exec(compile(content, filename, "exec"), module_namespace) + + ret = module_namespace + else: + with PathManager.open(filename) as f: + obj = yaml.unsafe_load(f) + ret = OmegaConf.create(obj, flags={"allow_objects": True}) + + if has_keys: + if isinstance(keys, str): + return _cast_to_config(ret[keys]) + else: + return tuple(_cast_to_config(ret[a]) for a in keys) + else: + if filename.endswith(".py"): + # when not specified, only load those that are config objects + ret = DictConfig( + { + name: _cast_to_config(value) + for name, value in ret.items() + if isinstance(value, (DictConfig, ListConfig, dict)) and not name.startswith("_") + }, + flags={"allow_objects": True}, + ) + return ret + + @staticmethod + def save_pkl(cfg, filename: str) -> str: + """ + Saves a Config object to a file using pickle serialization. This method is typically used + when the configuration object contains complex objects, such as lambdas, that are not supported by + simpler serialization methods like YAML. The function attempts to create a deep copy of the configuration + object before serialization to ensure that the original object remains unmodified. + + Args: + cfg: A Config object to be serialized and saved. + filename: The path and name of the file where the configuration should be saved. The function + assumes the file extension indicates a pickle format (e.g., .pkl). + + Returns: + str: The filename to which the configuration was saved. This can be used to verify the file location + or log the outcome. + + Notes: + - The function logs a warning if the configuration is successfully saved using pickle. + - If saving fails, an error is logged with the exception details. + """ + logger = logging.getLogger(__name__) + try: + cfg = deepcopy(cfg) + except Exception: + pass + + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(cfg, f) + logger.warning(f"Config is saved using pickle at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}. Trying dill or cloudpickle instead") + if dill_pickle: + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(dill_pickle.dumps(cfg, recurse=True), f) + logger.warning(f"Config is saved using dill at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}.") + if cloudpickle: + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(cloudpickle.dumps(cfg), f) + logger.warning(f"Config is saved using cloudpickle at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}.") + else: + logger.error("cloudpickle is not available. Cannot save the config.") + raise e + + return filename + + @staticmethod + def save_yaml(cfg, filename: str) -> str: + """ + Saves a Config object to a file using YAML serialization. This method is beneficial when the configuration object's content needs to be human-readable and easily editable. YAML is suitable for configurations that do not contain complex types like lambdas, which must be handled differently. The function converts unserializable items to strings before saving to ensure compatibility with YAML serialization. + + Args: + cfg: A Config object to be serialized and saved. It handles both DictConfig and ListConfig types. + filename: The path and name of the file where the configuration should be saved. The function does not require a specific file extension but typically uses '.yaml'. + + Returns: + str: The filename to which the configuration was saved. This can be used to verify the file location or log the outcome. + + Notes: + - The function logs a warning if the configuration is successfully saved using YAML. + - If saving fails, an error is logged with the exception details. + """ + logger = logging.getLogger(__name__) + try: + cfg = deepcopy(cfg) + except Exception: + pass + + # Define a function to check if an item is serializable to YAML + def is_serializable(item): + try: + OmegaConf.to_yaml(item) + return True + except Exception as e: + return False + + # Function to convert unserializable items to strings + def serialize_config(config): + if isinstance(config, DictConfig): + for key, value in config.items(): + if isinstance(value, (DictConfig, ListConfig)): + try: + if "_target_" in value: + default_params = get_default_params(value["_target_"]) + for default_key, default_v in default_params.items(): + if default_key not in value: + value[default_key] = default_v + except Exception as e: + logger.error(f"Failed to add default argument values: {e}") + + serialize_config(value) + else: + if not is_serializable(value) and value is not None: + config[key] = str(value) + elif isinstance(config, ListConfig): + for i, item in enumerate(config): + if isinstance(item, (DictConfig, ListConfig)): + serialize_config(item) + else: + if not is_serializable(item) and item is not None: + config[i] = str(item) + else: + raise NotImplementedError("Input config must be a DictConfig or ListConfig.") + return config + + # Convert Config object to a DictConfig object. + config_dict = attrs.asdict(cfg) + config_omegaconf = DictConfig(content=config_dict, flags={"allow_objects": True}) + + # Serialize the DictConfig object by converting non-serializable objects to strings. + config_omegaconf = serialize_config(config_omegaconf) + + config_dict: Dict[str, Any] = OmegaConf.to_container(config_omegaconf, resolve=True) + sorted_config: OrderedDict[str, Any] = sort_recursive(config_dict) + with open(filename, "w") as f: + yaml.dump(sorted_config, f, default_flow_style=False) + logger.warning(f"Config is saved using omegaconf at {filename}.") + return filename diff --git a/cosmos3/_src/imaginaire/lazy_config/lazy_call.py b/cosmos3/_src/imaginaire/lazy_config/lazy_call.py new file mode 100644 index 00000000..48824cf1 --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/lazy_call.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections.abc as abc +import inspect +from dataclasses import is_dataclass +from typing import ClassVar + +import attrs +from omegaconf import DictConfig + +from cosmos3._src.imaginaire.lazy_config.registry import convert_target_to_string + +__all__ = ["LazyCall"] + + +def get_default_params(cls_or_func): + if callable(cls_or_func): + # inspect signature for function + signature = inspect.signature(cls_or_func) + else: + # inspect signature for class + signature = inspect.signature(cls_or_func.__init__) + params = signature.parameters + default_params = { + name: param.default for name, param in params.items() if param.default is not inspect.Parameter.empty + } + return default_params + + +_CONVERT_TARGET_TO_STRING: ClassVar[bool] = False +"""Used by tests to enforce conversion of target to string.""" + + +class LazyCall: + """ + Wrap a callable so that when it's called, the call will not be executed, + but returns a dict that describes the call. + + LazyCall object has to be called with only keyword arguments. Positional + arguments are not yet supported. + + Examples: + :: + from detectron2.config import instantiate, LazyCall + + layer_cfg = LazyCall(nn.Conv2d)(in_channels=32, out_channels=32) + layer_cfg.out_channels = 64 # can edit it afterwards + layer = instantiate(layer_cfg) + """ + + def __init__(self, target): + if not (callable(target) or isinstance(target, (str, abc.Mapping))): + raise TypeError(f"target of LazyCall must be a callable or defines a callable! Got {target}") + self._target = target + + def __call__(self, **kwargs): + if _CONVERT_TARGET_TO_STRING or is_dataclass(self._target) or attrs.has(self._target): + # omegaconf object cannot hold dataclass type + # https://github.com/omry/omegaconf/issues/784 + target = convert_target_to_string(self._target) + else: + target = self._target + kwargs["_target_"] = target + + _final_params = get_default_params(self._target) + _final_params.update(kwargs) + + return DictConfig(content=_final_params, flags={"allow_objects": True}) diff --git a/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py b/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py new file mode 100644 index 00000000..39dca42a --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Dict, List, Union + +from omegaconf import OmegaConf +from omegaconf.base import DictKeyType, SCMode +from omegaconf.dictconfig import DictConfig # pragma: no cover + + +def to_object(cfg: Any) -> Union[Dict[DictKeyType, Any], List[Any], None, str, Any]: + """ + Converts an OmegaConf configuration object to a native Python container (dict or list), unless + the configuration is specifically created by LazyCall, in which case the original configuration + is returned directly. + + This function serves as a modification of the original `to_object` method from OmegaConf, + preventing DictConfig objects created by LazyCall from being automatically converted to Python + dictionaries. This ensures that configurations meant to be lazily evaluated retain their intended + structure and behavior. + + Differences from OmegaConf's original `to_object`: + - Adds a check at the beginning to return the configuration unchanged if it is created by LazyCall. + + Reference: + - Original OmegaConf `to_object` method: https://github.com/omry/omegaconf/blob/master/omegaconf/omegaconf.py#L595 + + Args: + cfg (Any): The OmegaConf configuration object to convert. + + Returns: + Union[Dict[DictKeyType, Any], List[Any], None, str, Any]: The converted Python container if + `cfg` is not a LazyCall created configuration, otherwise the unchanged `cfg`. + + Examples: + >>> cfg = DictConfig({"key": "value", "_target_": "Model"}) + >>> to_object(cfg) + DictConfig({"key": "value", "_target_": "Model"}) + + >>> cfg = DictConfig({"list": [1, 2, 3]}) + >>> to_object(cfg) + {'list': [1, 2, 3]} + """ + if isinstance(cfg, DictConfig) and "_target_" in cfg.keys(): + return cfg + + return OmegaConf.to_container( + cfg=cfg, + resolve=True, + throw_on_missing=True, + enum_to_str=False, + structured_config_mode=SCMode.INSTANTIATE, + ) diff --git a/cosmos3/_src/imaginaire/lazy_config/registry.py b/cosmos3/_src/imaginaire/lazy_config/registry.py new file mode 100644 index 00000000..4621fa82 --- /dev/null +++ b/cosmos3/_src/imaginaire/lazy_config/registry.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import pydoc +from typing import Any + +""" +``Registry`` and `locate` provide ways to map a string (typically found +in config files) to callable objects. +""" + +__all__ = ["locate", "convert_target_to_string"] + +try: + from fvcore.common.registry import Registry # for backward compatibility. + + __all__ += ["Registry"] +except Exception: + pass + + +def convert_target_to_string(t: Any) -> str: + """ + Inverse of ``locate()``. + + Args: + t: any object with ``__module__`` and ``__qualname__`` + """ + if hasattr(t, "__self__") and inspect.isclass(t.__self__): + # classmethod + cls = t.__self__ + module = cls.__module__ + qualname = f"{cls.__name__}.{t.__name__}" + else: + module = t.__module__ + qualname = t.__qualname__ + + # Compress the path to this object, e.g. ``module.submodule._impl.class`` + # may become ``module.submodule.class``, if the later also resolves to the same + # object. This simplifies the string, and also is less affected by moving the + # class implementation. + module_parts = module.split(".") + for k in range(1, len(module_parts)): + prefix = ".".join(module_parts[:k]) + candidate = f"{prefix}.{qualname}" + try: + if locate(candidate) is t: + return candidate + except ImportError: + pass + return f"{module}.{qualname}" + + +_convert_target_to_string = convert_target_to_string # for backward compatibility. + + +def locate(name: str) -> Any: + """ + Locate and return an object ``x`` using an input string ``{x.__module__}.{x.__qualname__}``, + such as "module.submodule.class_name". + + Raise Exception if it cannot be found. + """ + obj = pydoc.locate(name) + + # Some cases (e.g. torch.optim.sgd.SGD) not handled correctly + # by pydoc.locate. Try a private function from hydra. + if obj is None: + try: + # from hydra.utils import get_method - will print many errors + + from hydra.utils import _locate + except ImportError as e: + raise ImportError(f"Cannot dynamically locate object {name}!") from e + else: + obj = _locate(name) # it raises if fails + + return obj diff --git a/cosmos3/_src/imaginaire/model.py b/cosmos3/_src/imaginaire/model.py new file mode 100644 index 00000000..b595ba1c --- /dev/null +++ b/cosmos3/_src/imaginaire/model.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +import torch + +from cosmos3._src.imaginaire.lazy_config import LazyDict, instantiate + + +class ImaginaireModel(torch.nn.Module): + """The base model class of Imaginaire. It is inherited from torch.nn.Module. + + All models in Imaginaire should inherit ImaginaireModel. It should include the implementions for all the + computation graphs. All inheriting child classes should implement the following methods: + - training_step(): The training step of the model, including the loss computation. + - validation_step(): The validation step of the model, including the loss computation. + - forward(): The computation graph for model inference. + The following methods have default implementations in ImaginaireModel: + - init_optimizer_scheduler(): Creates the optimizer and scheduler for the model. + """ + + def __init__(self) -> None: + super().__init__() + self.parallel_dims = None + + def init_optimizer_scheduler( + self, optimizer_config: LazyDict, scheduler_config: LazyDict + ) -> tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LRScheduler]: + """Creates the optimizer and scheduler for the model. + + Args: + config_model (ModelConfig): The config object for the model. + + Returns: + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + """ + optimizer_config.params = self.parameters() + optimizer = instantiate(optimizer_config) + scheduler_config.optimizer = optimizer + scheduler = instantiate(scheduler_config) + return optimizer, scheduler + + def training_step( + self, data_batch: dict[str, torch.Tensor], iteration: int + ) -> tuple[dict[str, torch.Tensor], torch.Tensor]: + """The training step of the model, including the loss computation. + + Args: + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + iteration (int): Current iteration number. + + Returns: + output_batch (dict[str, torch.Tensor]): Auxiliary model output from the training batch. + loss (torch.Tensor): The total loss for backprop (weighted sum of various losses). + """ + raise NotImplementedError + + @torch.no_grad() + def validation_step( + self, data_batch: dict[str, torch.Tensor], iteration: int + ) -> tuple[dict[str, torch.Tensor], torch.Tensor]: + """The validation step of the model, including the loss computation. + + Args: + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + iteration (int): Current iteration number. + + Returns: + output_batch (dict[str, torch.Tensor]): Auxiliary model output from the validation batch. + loss (torch.Tensor): The total loss (weighted sum of various losses). + """ + raise NotImplementedError + + @torch.inference_mode() + def forward(self, *args: Any, **kwargs: Any) -> Any: + """The computation graph for model inference. + + Args: + *args: Whatever you decide to pass into the forward method. + **kwargs: Keyword arguments are also possible. + + Return: + Your model's output. + """ + raise NotImplementedError + + def on_train_start(self, memory_format: torch.memory_format = torch.preserve_format) -> None: + """The model preparation before the training is launched + + Args: + memory_format (torch.memory_format): Memory format of the model. + """ + pass + + def on_before_zero_grad( + self, optimizer: torch.optim.Optimizer, scheduler: torch.optim.lr_scheduler.LRScheduler, iteration: int + ) -> None: + """Hook before zero_grad() is called. + + Args: + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + iteration (int): Current iteration number. + """ + pass + + def on_after_backward(self, iteration: int = 0) -> None: + """Hook after loss.backward() is called. + + This method is called immediately after the backward pass, allowing for custom operations + or modifications to be performed on the gradients before the optimizer step. + + Args: + iteration (int): Current iteration number. + """ + pass diff --git a/cosmos3/_src/imaginaire/models/abstract_emb_model.py b/cosmos3/_src/imaginaire/models/abstract_emb_model.py new file mode 100644 index 00000000..d4ae38b9 --- /dev/null +++ b/cosmos3/_src/imaginaire/models/abstract_emb_model.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional, Union + +import torch +import torch.nn as nn + +from cosmos3._src.imaginaire.functional.batch_ops import batch_mul +from cosmos3._src.imaginaire.utils.count_params import count_params + + +class AbstractEmbModel(nn.Module): + def __init__(self) -> None: + super().__init__() + + self._is_trainable = None + self._dropout_rate = None + self._input_key = None + self._return_dict = False + + @property + def is_trainable(self) -> bool: + return self._is_trainable + + @property + def dropout_rate(self) -> Union[float, torch.Tensor]: + return self._dropout_rate + + @property + def input_key(self) -> str: + return self._input_key + + @property + def is_return_dict(self) -> bool: + return self._return_dict + + @is_trainable.setter + def is_trainable(self, value: bool) -> None: + self._is_trainable = value + + @dropout_rate.setter + def dropout_rate(self, value: Union[float, torch.Tensor]) -> None: + self._dropout_rate = value + + @input_key.setter + def input_key(self, value: str) -> None: + self._input_key = value + + @is_return_dict.setter + def is_return_dict(self, value: bool) -> None: + self._return_dict = value + + @is_trainable.deleter + def is_trainable(self) -> None: + del self._is_trainable + + @dropout_rate.deleter + def dropout_rate(self) -> None: + del self._dropout_rate + + @input_key.deleter + def input_key(self) -> None: + del self._input_key + + @is_return_dict.deleter + def is_return_dict(self) -> None: + del self._return_dict + + def random_dropout_input( + self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None + ) -> torch.Tensor: + del key + dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate + return batch_mul( + torch.bernoulli((1.0 - dropout_rate) * torch.ones(in_tensor.shape[0])).type_as(in_tensor), + in_tensor, + ) + + def details(self) -> str: + return "" + + def summary(self) -> str: + input_key = self.input_key if self.input_key is not None else getattr(self, "input_keys", None) + return ( + f"{self.__class__.__name__} \n\tinput key: {input_key}" + f"\n\tParam count: {count_params(self, False)} \n\tTrainable: {self.is_trainable}" + f"\n\tDropout rate: {self.dropout_rate}" + f"\n\t{self.details()}" + ) diff --git a/cosmos3/_src/imaginaire/modules/camera.py b/cosmos3/_src/imaginaire/modules/camera.py new file mode 100644 index 00000000..b8bf556d --- /dev/null +++ b/cosmos3/_src/imaginaire/modules/camera.py @@ -0,0 +1,663 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch + + +def _recursive_to_numpy(x): + if isinstance(x, torch.Tensor): + return x.detach().cpu().numpy() + if isinstance(x, (list, tuple)): + return type(x)(_recursive_to_numpy(v) for v in x) + if isinstance(x, dict): + return {k: _recursive_to_numpy(v) for k, v in x.items()} + return x + + +def supports_numpy(arg_names, use_no_grad: bool = True): + """Decorator to transparently support numpy inputs. + + - Converts the specified named args from numpy arrays to torch tensors on entry + - Runs the wrapped function (optionally under no_grad) + - Converts returns back to numpy iff the FIRST targeted arg was a numpy array + """ + import functools + import inspect + + def decorator(fn): + sig = inspect.signature(fn) + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + bound = sig.bind(*args, **kwargs) + bound.apply_defaults() + first_is_numpy = False + for idx, name in enumerate(arg_names): + if name in bound.arguments: + val = bound.arguments[name] + # Handle direct ndarray + if isinstance(val, np.ndarray): + if idx == 0: + first_is_numpy = True + bound.arguments[name] = torch.from_numpy(val) + continue + # Handle list/tuple of ndarrays -> list/tuple of tensors + if isinstance(val, (list, tuple)): + saw_numpy_first_elem = False + converted = [] + for i, el in enumerate(val): + if isinstance(el, np.ndarray): + if i == 0: + saw_numpy_first_elem = True + converted.append(torch.from_numpy(el)) + else: + converted.append(el) + if idx == 0 and saw_numpy_first_elem: + first_is_numpy = True + bound.arguments[name] = type(val)(converted) + + ctx = torch.no_grad() if use_no_grad else torch.enable_grad() + with ctx: + out = fn(*bound.args, **bound.kwargs) + return _recursive_to_numpy(out) if first_is_numpy else out + + return wrapper + + return decorator + + +class Camera: + """A class with a collection of common ops for camera transformations (Pytorch tensors). + + All poses are expected to have shape [...,3,4], where (...) indicates batch sizes of various ranks. + The last two dimensions (of size (3,4)) correspond to the extrinsic matrix [R|t] in OpenCV format. + + Convention: cam_pose is always a world-to-camera transform (world2cam): x_cam = R @ x_world + t. + This module operates on row-vector points with homogeneous coordinates on the right, so we apply + transformations as: points_hom @ cam_pose^T. + """ + + @staticmethod + @supports_numpy(["cam_pose"], use_no_grad=True) + def _check_valid_pose(cam_pose: torch.Tensor | np.ndarray) -> None: + """Checks whether the input tensor is a valid camera pose. + + Args: + cam_pose (torch.Tensor [...,3,4]): Input camera pose in world2cam [R|t] (OpenCV) format. + """ + assert cam_pose.shape[-2:] == (3, 4), "Camera pose is not of shape (3,4)." + R = cam_pose[..., :3] + # Compute determinant in float32 for numerical stability and allow dtype-dependent tolerance. + det_R = torch.linalg.det(R.to(torch.float32)) + one = torch.tensor(1.0, dtype=torch.float32, device=cam_pose.device) + if cam_pose.dtype in (torch.bfloat16, torch.float16): + rtol, atol = 1e-2, 1e-2 + else: + rtol, atol = 1e-4, 1e-6 + finite = bool(torch.isfinite(det_R).all()) + close = torch.allclose(det_R, one, rtol=rtol, atol=atol) + assert finite and close, ( + f"Rotation component in camera pose is invalid (det != 1 within tol). " + f"dtype={cam_pose.dtype}, rtol={rtol}, atol={atol}, det_mean={det_R.mean().item():.6f}" + ) + + @staticmethod + @supports_numpy(["cam_pose"], use_no_grad=True) + def invert_pose(cam_pose: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Invert a camera pose. + + Args: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): Input camera pose (world2cam [R|t]). + + Returns: + cam_pose_inv (torch.Tensor/np.ndarray [...,3,4]): The inverted camera pose (cam2world [R|t]). + """ + Camera._check_valid_pose(cam_pose) + in_dtype = cam_pose.dtype if isinstance(cam_pose, torch.Tensor) else torch.float32 + R, t = cam_pose[..., :3], cam_pose[..., 3:] # [...,3,3], [...,3,1] + # Compute in float32 for numerical stability, cast back at the end + R32 = R.to(torch.float32) # [...,3,3] + t32 = t.to(torch.float32) # [...,3,1] + # For rotation matrices, inverse equals transpose; prefer transpose for stability and speed + R_inv32 = R32.transpose(-1, -2) # [...,3,3] + t_inv32 = -R_inv32 @ t32 # [...,3,1] + cam_pose_inv32 = torch.cat([R_inv32, t_inv32], dim=-1) # [...,3,4] + return cam_pose_inv32.to(in_dtype) + + @staticmethod + @supports_numpy(["cam_poses"], use_no_grad=True) + def compose_poses(cam_poses: list[torch.Tensor | np.ndarray]) -> torch.Tensor | np.ndarray: + """Compose a sequence of camera transformations together. + + pose_new = compose_poses([pose_1, pose_2, ... pose_N]) + pose_new(x) = pose_N o ... o pose_2 o pose_1(x) + + Args: + cam_poses (list[torch.Tensor/np.ndarray [...,3,4]]): Sequence of rigid transforms [R|t]. + When used as camera extrinsics in this module, each pose is assumed to be world2cam. + The composition follows the same row-vector convention: points_hom @ pose^T. + List items may be numpy arrays; they will be converted to torch internally. + + Returns: + cam_pose_new (torch.Tensor/np.ndarray [...,3,4]): The composed transformation [R|t]. + """ + cam_pose_new = cam_poses[0] + Camera._check_valid_pose(cam_pose_new) + out_dtype = cam_pose_new.dtype if isinstance(cam_pose_new, torch.Tensor) else torch.float32 + R_new, t_new = ( + cam_pose_new[..., :3].to(torch.float32), + cam_pose_new[..., 3:].to(torch.float32), + ) # [...,3,3], [...,3,1] + for cam_pose in cam_poses[1:]: + Camera._check_valid_pose(cam_pose) + # pose_new(x) = pose o pose_new(x) + R, t = cam_pose[..., :3].to(torch.float32), cam_pose[..., 3:].to(torch.float32) # [...,3,3], [...,3,1] + R_new = R @ R_new # [...,3,3] + t_new = R @ t_new + t # [...,3,1] + cam_pose_new32 = torch.cat([R_new, t_new], dim=-1) # [...,3,4] + return cam_pose_new32.to(out_dtype) + + @staticmethod + @supports_numpy(["cam_pose", "cam_intr"], use_no_grad=True) + def get_camera_rays( + cam_pose: torch.Tensor | np.ndarray, + cam_intr: torch.Tensor | np.ndarray, + image_size: tuple[int, int], + ) -> torch.Tensor | np.ndarray: + """Get unit-norm camera rays in world coordinates for each pixel center. + + Args: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): Camera pose (world2cam [R|t]). + cam_intr (torch.Tensor/np.ndarray [...,3,3]): Camera intrinsics. + image_size (Tuple[int, int]): Image size (height, width). + + Returns: + rays_world (torch.Tensor/np.ndarray [...,HW,3]): Unit direction rays from camera center through pixel centers, flattened over pixels. + """ + H, W = image_size + with torch.no_grad(): + # Compute image coordinate grid (in float32 for stability). + y_range = torch.arange(H, dtype=torch.float32, device=cam_pose.device).add_(0.5) # [H] + x_range = torch.arange(W, dtype=torch.float32, device=cam_pose.device).add_(0.5) # [W] + y_grid, x_grid = torch.meshgrid(y_range, x_range, indexing="ij") # [H,W] each + xy_grid = torch.stack([x_grid, y_grid], dim=-1).view(-1, 2) # [HW,2] + xy_grid = xy_grid.repeat(*cam_pose.shape[:-2], 1, 1) # [...,HW,2] + # Pixel centers in camera coordinates at depth 1 (flattened HW) + grid_camera = Camera.image2camera(Camera.to_homogeneous(xy_grid), cam_intr) # [...,HW,3] + # Transform sample points and center to world + grid_world = Camera.camera2world(grid_camera, cam_pose) # [...,HW,3] + center_world = Camera.get_camera_center(cam_pose).unsqueeze(-2).expand_as(grid_world) # [...,HW,3] + rays_world = grid_world - center_world # [...,HW,3] + # Normalize to unit vectors + eps = 1e-8 + if cam_pose.dtype in (torch.bfloat16, torch.float16): + eps = 1e-2 + norms32 = rays_world.to(torch.float32).norm(dim=-1, keepdim=True).clamp_min(eps) # [...,HW,1] + rays_world = rays_world / norms32.to(rays_world.dtype) # [...,HW,3] + # Cast back to input dtype for consistency + rays_world = rays_world.to(cam_pose.dtype) # [...,HW,3] + # Keep flattened shape [...,HW,3] + return rays_world + + @staticmethod + @supports_numpy(["cam_pose", "cam_intr"], use_no_grad=True) + def get_plucker_rays( + cam_pose: torch.Tensor | np.ndarray, + cam_intr: torch.Tensor | np.ndarray, + image_size: tuple[int, int], + ) -> torch.Tensor | np.ndarray: + """Get Plücker coordinates (moment, direction) for each pixel center. + + Args: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): Camera pose (world2cam [R|t]). + cam_intr (torch.Tensor/np.ndarray [...,3,3]): Camera intrinsics. + image_size (Tuple[int, int]): Image size (height, width). + + Returns: + plucker (torch.Tensor/np.ndarray [...,HW,6]): Plücker coordinates [m | d], where + d is a unit direction vector and m = o × d with o the camera center in world. + """ + H, W = image_size + rays_world = Camera.get_camera_rays(cam_pose, cam_intr, image_size) # [...,HW,3] + # Expand center to [...,HW,3] + center_hw = Camera.get_camera_center(cam_pose).unsqueeze(-2).expand_as(rays_world) # [...,HW,3] + moment = torch.linalg.cross(center_hw, rays_world) # [...,HW,3] + plucker = torch.cat([moment, rays_world], dim=-1) # [...,HW,6] + return plucker + + @staticmethod + @supports_numpy(["cam_pose"], use_no_grad=True) + def get_relative_poses_wrt_frame0( + cam_pose: torch.Tensor | np.ndarray, + ) -> torch.Tensor | np.ndarray: + """Compute poses relative to the first frame (index 0). + + All poses are world-to-camera [R|t] with shape [...,3,4]. The returned poses are expressed + in the coordinate system of the first camera, so the first pose is identity [I|0]. For the + i-th pose: pose_rel_i = compose(pose_i, inverse(pose_ref)). + + Args: + cam_pose (torch.Tensor/np.ndarray [...,V,3,4]): World-to-camera extrinsics per view. + + Returns: + cam_pose_rel (torch.Tensor/np.ndarray [...,V,3,4]): Relative world-to-camera extrinsics in the first frame. + """ + # supports_numpy handles numpy + assert cam_pose.shape[-2:] == (3, 4), "cam_pose must have shape [..., V, 3, 4]." + # Reference pose and its inverse + pose_ref = cam_pose.select(dim=-3, index=0) # [...,3,4] + pose_ref_inv = Camera.invert_pose(pose_ref) # [...,3,4] + # Compose with broadcasting: pose_rel = pose ∘ pose_ref_inv + cam_pose_rel = Camera.compose_poses([pose_ref_inv, cam_pose]) # [...,V,3,4] + return cam_pose_rel + + @staticmethod + @supports_numpy(["cam_pose"], use_no_grad=True) + def get_camera_center(cam_pose: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Get the camera center in world coordinates for a given world2cam pose. + + Args: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): Camera pose (world2cam [R|t]). + + Returns: + center_world (torch.Tensor/np.ndarray [...,3]): Camera center in world coordinates. + """ + Camera._check_valid_pose(cam_pose) + R, t = cam_pose[..., :3], cam_pose[..., 3:] # [...,3,3], [...,3,1] + center_world32 = (-R.to(torch.float32).transpose(-1, -2) @ t.to(torch.float32)).squeeze(-1) # [...,3] + return center_world32.to(R.dtype) + + @staticmethod + @supports_numpy(["points"], use_no_grad=True) + def to_homogeneous(points: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Get homogeneous coordinates of the input points. + + Args: + points (torch.Tensor/np.ndarray [...,K]): Input coordinates. + + Returns: + points_hom (torch.Tensor/np.ndarray [...,K+1]): Homogeneous coordinates. + """ + # Compute homogeneous coordinate in float32 for stability, then cast back + one32 = torch.ones_like( + points[..., :1], dtype=torch.float32, device=(points.device if isinstance(points, torch.Tensor) else None) + ) # [...,1] + points_hom = torch.cat([points, one32.to(points.dtype)], dim=-1) # [...,K+1] + return points_hom + + @staticmethod + @supports_numpy(["points", "cam_pose"], use_no_grad=True) + def world2camera( + points: torch.Tensor | np.ndarray, cam_pose: torch.Tensor | np.ndarray + ) -> torch.Tensor | np.ndarray: + """Given the camera pose, transform input 3D points from world coordinates to camera coordinates. + + Args: + points (torch.Tensor/np.ndarray [...,N,3]): Input 3D points. + cam_pose (torch.Tensor/np.ndarray [...,3,4]/[3,4]): (Batched) camera pose (world2cam [R|t]). + + Returns: + points_new (torch.Tensor/np.ndarray [...,N,3]): Transformed 3D points. + """ + points_hom = Camera.to_homogeneous(points).to(torch.float32) # [...,N,4] + points_new32 = points_hom @ cam_pose.to(torch.float32).transpose(-1, -2) # [...,N,3] + return points_new32.to(points.dtype) # [...,N,3] + + @staticmethod + @supports_numpy(["points", "cam_pose"], use_no_grad=True) + def camera2world( + points: torch.Tensor | np.ndarray, cam_pose: torch.Tensor | np.ndarray + ) -> torch.Tensor | np.ndarray: + """Given the camera pose, transform input 3D points from camera coordinates to world coordinates. + + Args: + points (torch.Tensor/np.ndarray [...,N,3]): Input 3D points. + cam_pose (torch.Tensor/np.ndarray [...,3,4]/[3,4]): (Batched) camera pose (world2cam [R|t]). + + Returns: + points_new (torch.Tensor/np.ndarray [...,N,3]): Transformed 3D points. + """ + points_hom = Camera.to_homogeneous(points).to(torch.float32) # [...,N,4] + pose_inv = Camera.invert_pose(cam_pose) # [...,3,4] + points_new32 = points_hom @ pose_inv.to(torch.float32).transpose(-1, -2) # [...,N,3] + # To reduce double-quantization error on low-precision dtypes (e.g., bf16 on CPU), + # keep high precision on output for transform back to world space. + if isinstance(points, torch.Tensor) and points.dtype in (torch.bfloat16, torch.float16): + return points_new32 # [...,N,3] + return points_new32.to(points.dtype) # [...,N,3] + + @staticmethod + @supports_numpy(["points", "cam_intr"], use_no_grad=True) + def camera2image( + points: torch.Tensor | np.ndarray, cam_intr: torch.Tensor | np.ndarray + ) -> torch.Tensor | np.ndarray: + """Given the camera intrinsics, calibrate input 3D points from camera frame to image (pixel) frame. + + Args: + points (torch.Tensor/np.ndarray [...,N,3]): Input 3D points. + cam_intr (torch.Tensor/np.ndarray [...,3,3]/[3,3]): (Batched) camera intrinsic matrix. + + Returns: + points_new (torch.Tensor/np.ndarray [...,N,3]): Transformed 3D points. + """ + points32 = points.to(torch.float32) # [...,N,3] + points_new32 = points32 @ cam_intr.to(torch.float32).transpose(-1, -2) # [...,N,3] + return points_new32.to(points.dtype) # [...,N,3] + + @staticmethod + @supports_numpy(["points", "cam_intr"], use_no_grad=True) + def image2camera( + points: torch.Tensor | np.ndarray, cam_intr: torch.Tensor | np.ndarray + ) -> torch.Tensor | np.ndarray: + """Given the camera intrinsics, calibrate input 3D points from image (pixel) frame to camera frame. + + Args: + points (torch.Tensor/np.ndarray [...,N,3]): Input 3D points. + cam_intr (torch.Tensor/np.ndarray [...,3,3]/[3,3]): (Batched) camera intrinsic matrix. + + Returns: + points_new (torch.Tensor/np.ndarray [...,N,3]): Transformed 3D points. + """ + K_inv32 = torch.linalg.inv(cam_intr.to(torch.float32)) # [...,3,3] + points32 = points.to(torch.float32) # [...,N,3] + points_new32 = points32 @ K_inv32.transpose(-1, -2) # [...,N,3] + return points_new32.to(points.dtype) # [...,N,3] + + @staticmethod + @supports_numpy(["params"], use_no_grad=True) + def intrinsic_params_to_matrices(params: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Convert (fx, fy, cx, cy) parameters to camera intrinsic matrix/matrices. + + Args: + params (torch.Tensor/np.ndarray [...,4]): Intrinsic parameters (fx, fy, cx, cy). + + Returns: + K (torch.Tensor/np.ndarray [...,3,3]): Camera intrinsic matrices. + """ + assert params.shape[-1] == 4, "Intrinsic params must have shape (..., 4) for (fx, fy, cx, cy)." + fx, fy, cx, cy = params.unbind(dim=-1) # [...] each + one = torch.ones_like(fx) # [...] + zero = torch.zeros_like(fx) # [...] + row0 = torch.stack([fx, zero, cx], dim=-1) # [...,3] + row1 = torch.stack([zero, fy, cy], dim=-1) # [...,3] + row2 = torch.stack([zero, zero, one], dim=-1) # [...,3] + K = torch.stack([row0, row1, row2], dim=-2) # [...,3,3] + return K + + @staticmethod + @supports_numpy(["cam_intr"], use_no_grad=True) + def intrinsic_matrices_to_params( + cam_intr: torch.Tensor | np.ndarray, atol: float = 1e-6 + ) -> torch.Tensor | np.ndarray: + """Extract (fx, fy, cx, cy) from camera intrinsic matrix/matrices. + + Args: + cam_intr (torch.Tensor/np.ndarray [...,3,3]): Camera intrinsic matrices. + atol (float): Tolerance when checking the bottom row against [0,0,1]. + + Returns: + params (torch.Tensor/np.ndarray [...,4]): Intrinsic parameters (fx, fy, cx, cy). + """ + assert cam_intr.shape[-2:] == (3, 3), "Intrinsic matrix must have shape (..., 3, 3)." + row32 = cam_intr[..., 2, :].to(torch.float32) + target32 = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float32, device=cam_intr.device) + rtol = 1e-5 + atol_eff = atol + if cam_intr.dtype in (torch.bfloat16, torch.float16): + rtol = 1e-2 + atol_eff = max(atol, 1e-2) + if not torch.allclose(row32, target32, rtol=rtol, atol=atol_eff): + # Still proceed but warn via assertion message if strictness is desired. + pass + fx = cam_intr[..., 0, 0] # [...] + fy = cam_intr[..., 1, 1] # [...] + cx = cam_intr[..., 0, 2] # [...] + cy = cam_intr[..., 1, 2] # [...] + params = torch.stack([fx, fy, cx, cy], dim=-1) # [...,4] + return params + + @staticmethod + @supports_numpy(["qxyzw_t"], use_no_grad=True) + def extrinsic_params_to_matrices(qxyzw_t: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Convert (x,y,z,w, tx,ty,tz) to world2cam extrinsic matrix/matrices [R|t]. + + Args: + qxyzw_t (torch.Tensor/np.ndarray [...,7]): Quaternion (xyzw) and translation stacked. + + Returns: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): World-to-camera extrinsic [R|t]. + """ + assert qxyzw_t.shape[-1] == 7, "Input must have shape (..., 7) for (qx,qy,qz,qw,tx,ty,tz)." + q = qxyzw_t[..., :4] # [...,4] + t = qxyzw_t[..., 4:7] # [...,3] + # Enforce unit quaternion + Quaternion._check_valid_quaternion(q, require_normalized=True) + R = Quaternion.to_rotation_matrix(q) # [...,3,3] + cam_pose = torch.cat([R, t.unsqueeze(-1)], dim=-1) # [...,3,4] + return cam_pose + + @staticmethod + @supports_numpy(["cam_pose"], use_no_grad=True) + def extrinsic_matrices_to_params(cam_pose: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Convert world2cam extrinsic matrix/matrices [R|t] to (x,y,z,w, tx,ty,tz). + + Args: + cam_pose (torch.Tensor/np.ndarray [...,3,4]): World-to-camera extrinsic [R|t]. + + Returns: + qxyzw_t (torch.Tensor/np.ndarray [...,7]): Quaternion (xyzw) and translation stacked. + """ + Camera._check_valid_pose(cam_pose) + R = cam_pose[..., :3] # [...,3,3] + t = cam_pose[..., 3:].squeeze(-1) # [...,3] + q = Quaternion.from_rotation_matrix(R) # [...,4] + qxyzw_t = torch.cat([q, t], dim=-1) # [...,7] + return qxyzw_t + + +class Quaternion: + """A collection of common quaternion operations (Pytorch tensors). + + Convention (STRICT): Quaternions are represented in (x, y, z, w) order (xyzw) and are unit-norm. + The last dimension must be size 4. + """ + + @staticmethod + @supports_numpy(["q"], use_no_grad=True) + def _check_valid_quaternion( + q: torch.Tensor | np.ndarray, require_normalized: bool = True, atol: float = 1e-5 + ) -> torch.Tensor | np.ndarray: + """Checks whether the input tensor is a valid quaternion. + + Args: + q (torch.Tensor [...,4]): Input quaternion(s) in (x, y, z, w) order. + require_normalized (bool): If True, assert unit-norm within atol. Defaults to True. + atol (float): Absolute tolerance for the unit-norm check. + """ + assert q.shape[-1] == 4, "Quaternion is not of shape (..., 4)." + if require_normalized: + norms32 = q.to(torch.float32).norm(dim=-1) # [...] + ones32 = torch.ones_like(norms32) # [...] + tol = max(atol, 1e-2) if q.dtype in (torch.bfloat16, torch.float16) else atol + assert torch.allclose(norms32, ones32, atol=tol), "Quaternion must be unit length." + return q + + @staticmethod + @supports_numpy(["q"], use_no_grad=True) + def normalize(q: torch.Tensor | np.ndarray, eps: float = 1e-8) -> torch.Tensor | np.ndarray: + """Normalize quaternion(s) to unit length. + + Args: + q (torch.Tensor [...,4]): Input quaternion(s). + eps (float): Small epsilon to avoid division by zero. + + Returns: + q_norm (torch.Tensor [...,4]): Unit quaternions. + """ + # Allow non-normalized input here, since this function normalizes + Quaternion._check_valid_quaternion(q, require_normalized=False) + eps_eff = eps + if q.dtype in (torch.bfloat16, torch.float16): + eps_eff = max(eps, 1e-2) + norm32 = q.to(torch.float32).norm(dim=-1, keepdim=True).clamp_min(eps_eff) # [...,1] + out32 = q.to(torch.float32) / norm32 # [...,4] + out = out32.to(q.dtype) # [...,4] + return out + + @staticmethod + @supports_numpy(["q"], use_no_grad=True) + def to_rotation_matrix(q: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Convert quaternion(s) to rotation matrix/matrices. + + Args: + q (torch.Tensor [...,4]): Quaternion(s) (x, y, z, w). + + Returns: + R (torch.Tensor [...,3,3]): Rotation matrix/matrices. + """ + # Enforce unit quaternions for rotations + Quaternion._check_valid_quaternion(q, require_normalized=True) + q32 = q.to(torch.float32) # [...,4] + qx, qy, qz, qw = q32.unbind(dim=-1) # [...] each + two = torch.tensor(2.0, dtype=torch.float32, device=q32.device) + + r00 = 1 - two * (qy * qy + qz * qz) # [...] + r01 = two * (qx * qy - qz * qw) # [...] + r02 = two * (qx * qz + qy * qw) # [...] + r10 = two * (qx * qy + qz * qw) # [...] + r11 = 1 - two * (qx * qx + qz * qz) # [...] + r12 = two * (qy * qz - qx * qw) # [...] + r20 = two * (qx * qz - qy * qw) # [...] + r21 = two * (qx * qw + qy * qz) # [...] + r22 = 1 - two * (qx * qx + qy * qy) # [...] + + R32 = torch.stack( + [ + torch.stack([r00, r01, r02], dim=-1), # [...,3] + torch.stack([r10, r11, r12], dim=-1), # [...,3] + torch.stack([r20, r21, r22], dim=-1), # [...,3] + ], + dim=-2, + ) # [...,3,3] + return R32.to(q.dtype) + + @staticmethod + @supports_numpy(["R"], use_no_grad=True) + def from_rotation_matrix(R: torch.Tensor | np.ndarray, eps: float = 1e-8) -> torch.Tensor | np.ndarray: + """Convert rotation matrix/matrices to quaternion(s). + + Args: + R (torch.Tensor [...,3,3]): Rotation matrix/matrices. + eps (float): Numerical stability epsilon. + + Returns: + q (torch.Tensor [...,4]): Quaternion(s) in (x, y, z, w) order. + """ + assert R.shape[-2:] == (3, 3), "Rotation matrix is not of shape (..., 3, 3)." + R32 = R.to(torch.float32) # [...,3,3] + m00 = R32[..., 0, 0] # [...] + m11 = R32[..., 1, 1] # [...] + m22 = R32[..., 2, 2] # [...] + trace = m00 + m11 + m22 # [...] + + q32 = torch.empty(*R32.shape[:-2], 4, dtype=torch.float32, device=R32.device) # [...,4] + + cond0 = trace > 0 # [...] + eps_eff = max(eps, 1e-6) + s0 = torch.sqrt(trace + 1.0 + eps_eff) * 2.0 # [...] + qw0 = 0.25 * s0 # [...] + qx0 = (R[..., 2, 1] - R[..., 1, 2]) / s0 # [...] + qy0 = (R[..., 0, 2] - R[..., 2, 0]) / s0 # [...] + qz0 = (R[..., 1, 0] - R[..., 0, 1]) / s0 # [...] + + cond1 = (~cond0) & (m00 > m11) & (m00 > m22) # [...] + s1 = torch.sqrt(1.0 + m00 - m11 - m22 + eps_eff) * 2.0 # [...] + qw1 = (R[..., 2, 1] - R[..., 1, 2]) / s1 # [...] + qx1 = 0.25 * s1 # [...] + qy1 = (R[..., 0, 1] + R[..., 1, 0]) / s1 # [...] + qz1 = (R[..., 0, 2] + R[..., 2, 0]) / s1 # [...] + + cond2 = (~cond0) & (~cond1) & (m11 > m22) # [...] + s2 = torch.sqrt(1.0 + m11 - m00 - m22 + eps_eff) * 2.0 # [...] + qw2 = (R[..., 0, 2] - R[..., 2, 0]) / s2 # [...] + qx2 = (R[..., 0, 1] + R[..., 1, 0]) / s2 # [...] + qy2 = 0.25 * s2 # [...] + qz2 = (R[..., 1, 2] + R[..., 2, 1]) / s2 # [...] + + cond3 = (~cond0) & (~cond1) & (~cond2) # [...] + s3 = torch.sqrt(1.0 + m22 - m00 - m11 + eps_eff) * 2.0 # [...] + qw3 = (R[..., 1, 0] - R[..., 0, 1]) / s3 # [...] + qx3 = (R[..., 0, 2] + R[..., 2, 0]) / s3 # [...] + qy3 = (R[..., 1, 2] + R[..., 2, 1]) / s3 # [...] + qz3 = 0.25 * s3 # [...] + + qw = torch.where(cond0, qw0, torch.where(cond1, qw1, torch.where(cond2, qw2, qw3))) # [...] + qx = torch.where(cond0, qx0, torch.where(cond1, qx1, torch.where(cond2, qx2, qx3))) # [...] + qy = torch.where(cond0, qy0, torch.where(cond1, qy1, torch.where(cond2, qy2, qy3))) # [...] + qz = torch.where(cond0, qz0, torch.where(cond1, qz1, torch.where(cond2, qz2, qz3))) # [...] + + q32[..., 0] = qx + q32[..., 1] = qy + q32[..., 2] = qz + q32[..., 3] = qw + + q_out = Quaternion.normalize(q32).to(R.dtype) # [...,4] + return q_out + + @staticmethod + @supports_numpy(["q"], use_no_grad=True) + def invert(q: torch.Tensor | np.ndarray, eps: float = 1e-8) -> torch.Tensor | np.ndarray: + """Inverse of quaternion(s). Equivalent to conjugate of quaternion. + + For unit quaternions, the inverse equals the conjugate. For non-unit quaternions, + q^{-1} = conjugate(q) / ||q||^2. + + Args: + q (torch.Tensor [...,4]): Input quaternion(s). + eps (float): Small epsilon to avoid division by zero. + + Returns: + q_inv (torch.Tensor [...,4]): Inverted quaternion(s). + """ + # Enforce unit quaternions; inverse equals conjugate + Quaternion._check_valid_quaternion(q, require_normalized=True) + qx, qy, qz, qw = q.unbind(dim=-1) # [...] each + return torch.stack([-qx, -qy, -qz, qw], dim=-1) # [...,4] + + @staticmethod + @supports_numpy(["q1", "q2"], use_no_grad=True) + def multiply(q1: torch.Tensor | np.ndarray, q2: torch.Tensor | np.ndarray) -> torch.Tensor | np.ndarray: + """Hamilton product of two quaternion sets. + + Args: + q1 (torch.Tensor [...,4]): Left quaternion(s) in (x, y, z, w) order. + q2 (torch.Tensor [...,4]): Right quaternion(s) in (x, y, z, w) order. + + Returns: + q (torch.Tensor [...,4]): Product quaternion(s) q = q1 ⊗ q2 in (x, y, z, w) order. + """ + # Enforce unit inputs + Quaternion._check_valid_quaternion(q1, require_normalized=True) + Quaternion._check_valid_quaternion(q2, require_normalized=True) + q1x, q1y, q1z, q1w = q1.unbind(dim=-1) # [...] each + q2x, q2y, q2z, q2w = q2.unbind(dim=-1) # [...] each + qx = q1w * q2x + q2w * q1x + q1y * q2z - q1z * q2y # [...] + qy = q1w * q2y + q2w * q1y + q1z * q2x - q1x * q2z # [...] + qz = q1w * q2z + q2w * q1z + q1x * q2y - q1y * q2x # [...] + qw = q1w * q2w - (q1x * q2x + q1y * q2y + q1z * q2z) # [...] + q = torch.stack([qx, qy, qz, qw], dim=-1) # [...,4] + # Re-normalize to counteract numerical drift and enforce constraint + return Quaternion.normalize(q) # [...,4] diff --git a/cosmos3/_src/imaginaire/modules/denoiser_scaling.py b/cosmos3/_src/imaginaire/modules/denoiser_scaling.py new file mode 100644 index 00000000..6d843ef4 --- /dev/null +++ b/cosmos3/_src/imaginaire/modules/denoiser_scaling.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Tuple + +import torch + + +class EDMScaling: + def __init__(self, sigma_data: float = 0.5): + self.sigma_data = sigma_data + + def __call__(self, sigma: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + c_skip = self.sigma_data**2 / (sigma**2 + self.sigma_data**2) + c_out = sigma * self.sigma_data / (sigma**2 + self.sigma_data**2) ** 0.5 + c_in = 1 / (sigma**2 + self.sigma_data**2) ** 0.5 + c_noise = 0.25 * sigma.log() + return c_skip, c_out, c_in, c_noise + + +class RectifiedFlowScaling: + def __init__(self, sigma_data: float = 1.0, t_scaling_factor: float = 1.0, loss_weight_uniform: bool = True): + assert abs(sigma_data - 1.0) < 1e-6, "sigma_data must be 1.0 for RectifiedFlowScaling" + self.t_scaling_factor = t_scaling_factor + self.loss_weight_uniform = loss_weight_uniform + if loss_weight_uniform is False: + # using huan lin suggested one here. which put more weight on the middle of the timesteps. + self.num_steps = 1000 + t = torch.linspace(0, 1, self.num_steps) + y = torch.exp(-2 * (t - 0.5) ** 2) + shift = y - y.min() + weights = shift * (self.num_steps / shift.sum()) # make sure the avg weights is 1.0 + self.weights = weights + + def sigma_loss_weights(self, sigma: torch.Tensor) -> torch.Tensor: + if self.loss_weight_uniform: + return (1.0 + sigma) ** 2 / sigma**2 + else: + t = sigma / (sigma + 1) + index = (t * self.num_steps).round().long() + # Clamp index to valid range [0, num_steps-1] to avoid out of bounds + index = torch.clamp(index, 0, self.num_steps - 1) + weights_on_device = self.weights.to(sigma.device) + return weights_on_device[index].type_as(sigma) + + def __call__(self, sigma: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + t = sigma / (sigma + 1) + c_skip = 1.0 - t + c_out = -t + c_in = 1.0 - t + c_noise = t * self.t_scaling_factor + return c_skip, c_out, c_in, c_noise diff --git a/cosmos3/_src/imaginaire/modules/edm_sde.py b/cosmos3/_src/imaginaire/modules/edm_sde.py new file mode 100644 index 00000000..3d08a822 --- /dev/null +++ b/cosmos3/_src/imaginaire/modules/edm_sde.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from statistics import NormalDist + +import numpy as np +import torch + + +class EDMSDE: + def __init__( + self, + p_mean: float = -1.2, + p_std: float = 1.2, + sigma_max: float = 80.0, + sigma_min: float = 0.002, + ): + self.gaussian_dist = NormalDist(mu=p_mean, sigma=p_std) + self.sigma_max = sigma_max + self.sigma_min = sigma_min + + def sample_t(self, batch_size: int) -> torch.Tensor: + cdf_vals = np.random.uniform(size=(batch_size)) + samples_interval_gaussian = [self.gaussian_dist.inv_cdf(cdf_val) for cdf_val in cdf_vals] + + log_sigma = torch.tensor(samples_interval_gaussian, device="cuda") + return torch.exp(log_sigma) + + def marginal_prob(self, x0: torch.Tensor, sigma: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """This is trivial in the base class, but may be used by derived classes in a more interesting way""" + return x0, sigma diff --git a/cosmos3/_src/imaginaire/modules/image_embeddings.py b/cosmos3/_src/imaginaire/modules/image_embeddings.py new file mode 100644 index 00000000..2d3d0524 --- /dev/null +++ b/cosmos3/_src/imaginaire/modules/image_embeddings.py @@ -0,0 +1,763 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Forked from https://github.com/openai/CLIP/blob/main/clip/model.py +This file differs in that it exposes the prepooled patch tokens alongside the global image tokens +when calling the CLIP model +""" + +import hashlib +import os +import urllib +import warnings +from collections import OrderedDict +from typing import Any, List, Tuple, Union # noqa: F401 + +import numpy as np +import torch +import torch.nn.functional as F +from PIL import Image +from pkg_resources import packaging +from torch import nn +from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToTensor +from tqdm import tqdm + +try: + from torchvision.transforms import InterpolationMode + + BICUBIC = InterpolationMode.BICUBIC +except ImportError: + BICUBIC = Image.BICUBIC + + +if packaging.version.parse(torch.__version__) < packaging.version.parse("1.7.1"): + warnings.warn("PyTorch version 1.7.1 or higher is recommended") + +__all__ = ["available_models", "load"] + +_MODELS = { + "RN50": "https://openaipublic.azureedge.net/clip/models/afeb0e10f9e5a86da6080e35cf09123aca3b358a0c3e3b6c78a7b63bc04b6762/RN50.pt", # noqa: E501 + "RN101": "https://openaipublic.azureedge.net/clip/models/8fa8567bab74a42d41c5915025a8e4538c3bdbe8804a470a72f30b0d94fab599/RN101.pt", # noqa: E501 + "RN50x4": "https://openaipublic.azureedge.net/clip/models/7e526bd135e493cef0776de27d5f42653e6b4c8bf9e0f653bb11773263205fdd/RN50x4.pt", # noqa: E501 + "RN50x16": "https://openaipublic.azureedge.net/clip/models/52378b407f34354e150460fe41077663dd5b39c54cd0bfd2b27167a4a06ec9aa/RN50x16.pt", # noqa: E501 + "RN50x64": "https://openaipublic.azureedge.net/clip/models/be1cfb55d75a9666199fb2206c106743da0f6468c9d327f3e0d0a543a9919d9c/RN50x64.pt", # noqa: E501 + "ViT-B/32": "https://openaipublic.azureedge.net/clip/models/40d365715913c9da98579312b702a82c18be219cc2a73407c4526f58eba950af/ViT-B-32.pt", # noqa: E501 + "ViT-B/16": "https://openaipublic.azureedge.net/clip/models/5806e77cd80f8b59890b7e101eabd078d9fb84e6937f9e85e4ecb61988df416f/ViT-B-16.pt", # noqa: E501 + "ViT-L/14": "https://openaipublic.azureedge.net/clip/models/b8cca3fd41ae0c99ba7e8951adf17d267cdb84cd88be6f7c2e0eca1737a03836/ViT-L-14.pt", # noqa: E501 + "ViT-L/14@336px": "https://openaipublic.azureedge.net/clip/models/3035c92b350959924f9f00213499208652fc7ea050643e8b385c2dac08641f02/ViT-L-14-336px.pt", # noqa: E501 +} + + +class Bottleneck(nn.Module): + """Bottleneck residual block for ResNet.""" + + expansion = 4 + + def __init__(self, inplanes, planes, stride=1): + super().__init__() + + # all conv layers have stride 1. an avgpool is performed after the second convolution when stride > 1 + self.conv1 = nn.Conv2d(inplanes, planes, 1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.relu1 = nn.ReLU(inplace=True) + + self.conv2 = nn.Conv2d(planes, planes, 3, padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + self.relu2 = nn.ReLU(inplace=True) + + self.avgpool = nn.AvgPool2d(stride) if stride > 1 else nn.Identity() + + self.conv3 = nn.Conv2d(planes, planes * self.expansion, 1, bias=False) + self.bn3 = nn.BatchNorm2d(planes * self.expansion) + self.relu3 = nn.ReLU(inplace=True) + + self.downsample = None + self.stride = stride + + if stride > 1 or inplanes != planes * Bottleneck.expansion: + # downsampling layer is prepended with an avgpool, and the subsequent convolution has stride 1 + self.downsample = nn.Sequential( + OrderedDict( + [ + ("-1", nn.AvgPool2d(stride)), + ("0", nn.Conv2d(inplanes, planes * self.expansion, 1, stride=1, bias=False)), + ("1", nn.BatchNorm2d(planes * self.expansion)), + ] + ) + ) + + def forward(self, x: torch.Tensor): + identity = x + + out = self.relu1(self.bn1(self.conv1(x))) + out = self.relu2(self.bn2(self.conv2(out))) + out = self.avgpool(out) + out = self.bn3(self.conv3(out)) + + if self.downsample is not None: + identity = self.downsample(x) + + out += identity + out = self.relu3(out) + return out + + +class AttentionPool2d(nn.Module): + """Attention pooling layer for 2D feature maps.""" + + def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None): + super().__init__() + self.positional_embedding = nn.Parameter(torch.randn(spacial_dim**2 + 1, embed_dim) / embed_dim**0.5) + self.k_proj = nn.Linear(embed_dim, embed_dim) + self.q_proj = nn.Linear(embed_dim, embed_dim) + self.v_proj = nn.Linear(embed_dim, embed_dim) + self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim) + self.num_heads = num_heads + + def forward(self, x): + x = x.reshape(x.shape[0], x.shape[1], x.shape[2] * x.shape[3]).permute(2, 0, 1) # [HW,B,C] + x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0) # [HW+1,B,C] + x = x + self.positional_embedding[:, None, :].to(x.dtype) # [HW+1,B,C] + x, _ = F.multi_head_attention_forward( # x: [HW+1,B,output_dim] + query=x, + key=x, + value=x, + embed_dim_to_check=x.shape[-1], + num_heads=self.num_heads, + q_proj_weight=self.q_proj.weight, + k_proj_weight=self.k_proj.weight, + v_proj_weight=self.v_proj.weight, + in_proj_weight=None, + in_proj_bias=torch.cat([self.q_proj.bias, self.k_proj.bias, self.v_proj.bias]), # [3*embed_dim] + bias_k=None, + bias_v=None, + add_zero_attn=False, + dropout_p=0, + out_proj_weight=self.c_proj.weight, + out_proj_bias=self.c_proj.bias, + use_separate_proj_weight=True, + training=self.training, + need_weights=False, + ) + + return x[0] # [B,output_dim] + + +class ModifiedResNet(nn.Module): + """ + A ResNet class that is similar to torchvision's but contains the following changes: + - There are now 3 "stem" convolutions as opposed to 1, with an average pool instead of a max pool. + - Performs anti-aliasing strided convolutions, where an avgpool is prepended to convolutions with stride > 1 + - The final pooling layer is a QKV attention instead of an average pool + """ + + def __init__(self, layers, output_dim, heads, input_resolution=224, width=64): + super().__init__() + self.output_dim = output_dim + self.input_resolution = input_resolution + + # the 3-layer stem + self.conv1 = nn.Conv2d(3, width // 2, kernel_size=3, stride=2, padding=1, bias=False) + self.bn1 = nn.BatchNorm2d(width // 2) + self.relu1 = nn.ReLU(inplace=True) + self.conv2 = nn.Conv2d(width // 2, width // 2, kernel_size=3, padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(width // 2) + self.relu2 = nn.ReLU(inplace=True) + self.conv3 = nn.Conv2d(width // 2, width, kernel_size=3, padding=1, bias=False) + self.bn3 = nn.BatchNorm2d(width) + self.relu3 = nn.ReLU(inplace=True) + self.avgpool = nn.AvgPool2d(2) + + # residual layers + self._inplanes = width # this is a *mutable* variable used during construction + self.layer1 = self._make_layer(width, layers[0]) + self.layer2 = self._make_layer(width * 2, layers[1], stride=2) + self.layer3 = self._make_layer(width * 4, layers[2], stride=2) + self.layer4 = self._make_layer(width * 8, layers[3], stride=2) + + embed_dim = width * 32 # the ResNet feature dimension + self.attnpool = AttentionPool2d(input_resolution // 32, embed_dim, heads, output_dim) + + def _make_layer(self, planes, blocks, stride=1): + """ + Create a layer of residual blocks. + - planes: the number of output channels for this layer + - blocks: the number of residual blocks in this layer + - stride: the stride to use for the first convolution of the layer + """ + layers = [Bottleneck(self._inplanes, planes, stride)] + + self._inplanes = planes * Bottleneck.expansion + for _ in range(1, blocks): + layers.append(Bottleneck(self._inplanes, planes)) + + return nn.Sequential(*layers) + + def forward(self, x): + def stem(x): + """ + The stem convolutions at the beginning of the network. + Performs 3 convolutions and an average pool. + """ + x = self.relu1(self.bn1(self.conv1(x))) + x = self.relu2(self.bn2(self.conv2(x))) + x = self.relu3(self.bn3(self.conv3(x))) + x = self.avgpool(x) + return x + + x = x.type(self.conv1.weight.dtype) # [B,3,H,W] + x = stem(x) # [B,width,H/4,W/4] + x = self.layer1(x) # [B,width*4,H/4,W/4] + x = self.layer2(x) # [B,width*8,H/8,W/8] + x = self.layer3(x) # [B,width*16,H/16,W/16] + x = self.layer4(x) # [B,width*32,H/32,W/32] + x = self.attnpool(x) # [B,output_dim] + + return x + + +class LayerNorm(nn.LayerNorm): + """Subclass torch's LayerNorm to handle fp16.""" + + def forward(self, x: torch.Tensor): + orig_type = x.dtype + ret = super().forward(x.type(torch.float32)) + return ret.type(orig_type) + + +class QuickGELU(nn.Module): + """GELU activation function approximation""" + + def forward(self, x: torch.Tensor): + return x * torch.sigmoid(1.702 * x) + + +class ResidualAttentionBlock(nn.Module): + def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None): + super().__init__() + + self.attn = nn.MultiheadAttention(d_model, n_head) + self.ln_1 = LayerNorm(d_model) + self.mlp = nn.Sequential( + OrderedDict( + [ + ("c_fc", nn.Linear(d_model, d_model * 4)), + ("gelu", QuickGELU()), + ("c_proj", nn.Linear(d_model * 4, d_model)), + ] + ) + ) + self.ln_2 = LayerNorm(d_model) + self.attn_mask = attn_mask + + def attention(self, x: torch.Tensor): + """Perform multi-head attention on the input tensor.""" + self.attn_mask = self.attn_mask.to(dtype=x.dtype, device=x.device) if self.attn_mask is not None else None + return self.attn(x, x, x, need_weights=False, attn_mask=self.attn_mask)[0] + + def forward(self, x: torch.Tensor): + x = x + self.attention(self.ln_1(x)) + x = x + self.mlp(self.ln_2(x)) + return x + + +class Transformer(nn.Module): + def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None): + super().__init__() + self.width = width + self.layers = layers + self.resblocks = nn.Sequential(*[ResidualAttentionBlock(width, heads, attn_mask) for _ in range(layers)]) + + def forward(self, x: torch.Tensor): + return self.resblocks(x) + + +class VisionTransformer(nn.Module): + def __init__(self, input_resolution: int, patch_size: int, width: int, layers: int, heads: int, output_dim: int): + super().__init__() + self.input_resolution = input_resolution + self.output_dim = output_dim + self.conv1 = nn.Conv2d(in_channels=3, out_channels=width, kernel_size=patch_size, stride=patch_size, bias=False) + + scale = width**-0.5 + self.class_embedding = nn.Parameter(scale * torch.randn(width)) + self.positional_embedding = nn.Parameter(scale * torch.randn((input_resolution // patch_size) ** 2 + 1, width)) + self.ln_pre = LayerNorm(width) + + self.transformer = Transformer(width, layers, heads) + + self.ln_post = LayerNorm(width) + self.proj = nn.Parameter(scale * torch.randn(width, output_dim)) + + def forward(self, x: torch.Tensor): + x = self.conv1(x) # [B,width,grid,grid] + x = x.reshape(x.shape[0], x.shape[1], -1) # [B,width,grid**2] + x = x.permute(0, 2, 1) # [B,grid**2,width] + x = torch.cat( + [ + self.class_embedding.to(x.dtype) + + torch.zeros(x.shape[0], 1, x.shape[-1], dtype=x.dtype, device=x.device), + x, + ], + dim=1, + ) # [B,grid**2+1,width] + x = x + self.positional_embedding.to(x.dtype) # [B,grid**2+1,width] + x = self.ln_pre(x) # [B,grid**2+1,width] + + x = x.permute(1, 0, 2) # [grid**2+1,B,width] + x = self.transformer(x) # [grid**2+1,B,width] + x = x.permute(1, 0, 2) # [B,grid**2+1,width] + + x_pre_pooling = x # [B,grid**2+1,width] + x = self.ln_post(x[:, 0, :]) # [B,width] + + if self.proj is not None: + x = x @ self.proj # [B,output_dim] + + return x, x_pre_pooling + + +class CLIP(nn.Module): + """ + This CLIP module combines a visual encoder and a text encoder. It initializes the parameters, builds the attention mask, + and provides methods to encode images and text separately. The forward method computes the cosine similarity between + the encoded image and text features. + """ + + def __init__( + self, + embed_dim: int, + # vision + image_resolution: int, + vision_layers: Union[Tuple[int, int, int, int], int], + vision_width: int, + vision_patch_size: int, + # text + context_length: int, + vocab_size: int, + transformer_width: int, + transformer_heads: int, + transformer_layers: int, + ): + super().__init__() + + self.context_length = context_length + + if isinstance(vision_layers, (tuple, list)): + vision_heads = vision_width * 32 // 64 + self.visual = ModifiedResNet( + layers=vision_layers, + output_dim=embed_dim, + heads=vision_heads, + input_resolution=image_resolution, + width=vision_width, + ) + else: + vision_heads = vision_width // 64 + self.visual = VisionTransformer( + input_resolution=image_resolution, + patch_size=vision_patch_size, + width=vision_width, + layers=vision_layers, + heads=vision_heads, + output_dim=embed_dim, + ) + + self.transformer = Transformer( + width=transformer_width, + layers=transformer_layers, + heads=transformer_heads, + attn_mask=self.build_attention_mask(), + ) + + self.vocab_size = vocab_size + self.token_embedding = nn.Embedding(vocab_size, transformer_width) + self.positional_embedding = nn.Parameter(torch.empty(self.context_length, transformer_width)) + self.ln_final = LayerNorm(transformer_width) + + self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim)) + self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07)) + + self.initialize_parameters() + + def initialize_parameters(self): + """Initialize the parameters of the CLIP module.""" + nn.init.normal_(self.token_embedding.weight, std=0.02) + nn.init.normal_(self.positional_embedding, std=0.01) + + if isinstance(self.visual, ModifiedResNet): + if self.visual.attnpool is not None: + std = self.visual.attnpool.c_proj.in_features**-0.5 + nn.init.normal_(self.visual.attnpool.q_proj.weight, std=std) + nn.init.normal_(self.visual.attnpool.k_proj.weight, std=std) + nn.init.normal_(self.visual.attnpool.v_proj.weight, std=std) + nn.init.normal_(self.visual.attnpool.c_proj.weight, std=std) + + for resnet_block in [self.visual.layer1, self.visual.layer2, self.visual.layer3, self.visual.layer4]: + for name, param in resnet_block.named_parameters(): + if name.endswith("bn3.weight"): + nn.init.zeros_(param) + + proj_std = (self.transformer.width**-0.5) * ((2 * self.transformer.layers) ** -0.5) + attn_std = self.transformer.width**-0.5 + fc_std = (2 * self.transformer.width) ** -0.5 + for block in self.transformer.resblocks: + nn.init.normal_(block.attn.in_proj_weight, std=attn_std) + nn.init.normal_(block.attn.out_proj.weight, std=proj_std) + nn.init.normal_(block.mlp.c_fc.weight, std=fc_std) + nn.init.normal_(block.mlp.c_proj.weight, std=proj_std) + + if self.text_projection is not None: + nn.init.normal_(self.text_projection, std=self.transformer.width**-0.5) + + def build_attention_mask(self): + # lazily create causal attention mask, with full attention between the vision tokens + # pytorch uses additive attention mask; fill with -inf + mask = torch.empty(self.context_length, self.context_length) # [context_length,context_length] + mask.fill_(float("-inf")) + mask.triu_(1) # zero out the lower diagonal + return mask + + @property + def dtype(self): + return self.visual.conv1.weight.dtype + + def encode_image(self, image): + """ + Encode an image using the visual encoder. + + Args: + image (torch.Tensor): The input image. + + Returns: + torch.Tensor: The encoded image features. + """ + return self.visual(image.type(self.dtype)) + + def encode_text(self, text): + """ + Encode text using the text encoder. + + Args: + text (torch.Tensor): The input text. + + Returns: + torch.Tensor: The encoded text features. + """ + x = self.token_embedding(text).type(self.dtype) # [B,n_ctx,transformer_width] + + x = x + self.positional_embedding.type(self.dtype) # [B,n_ctx,transformer_width] + x = x.permute(1, 0, 2) # [n_ctx,B,transformer_width] + x = self.transformer(x) # [n_ctx,B,transformer_width] + x = x.permute(1, 0, 2) # [B,n_ctx,transformer_width] + x = self.ln_final(x).type(self.dtype) # [B,n_ctx,transformer_width] + + # take features from the eot embedding (eot_token is the highest number in each sequence) + x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.text_projection # [B,embed_dim] + + return x + + def forward(self, image, text): + """ + Forward pass of the CLIP module. + + Args: + image (torch.Tensor): The input image. + text (torch.Tensor): The input text. + + Returns: + tuple: A tuple containing the logits per image and logits per text. + """ + image_features, _ = self.encode_image(image) # [B,embed_dim] + text_features = self.encode_text(text) # [B,embed_dim] + + # normalized features + image_features = image_features / image_features.norm(dim=1, keepdim=True) # [B,embed_dim] + text_features = text_features / text_features.norm(dim=1, keepdim=True) # [B,embed_dim] + + # cosine similarity as logits + logit_scale = self.logit_scale.exp() # scalar + logits_per_image = logit_scale * image_features @ text_features.t() # [B,B] + logits_per_text = logits_per_image.t() # [B,B] + + return logits_per_image, logits_per_text + + +def convert_weights(model: nn.Module): + """Convert applicable model parameters to fp16""" + + def _convert_weights_to_fp16(ll): + if isinstance(ll, (nn.Conv1d, nn.Conv2d, nn.Linear)): + ll.weight.data = ll.weight.data.half() + if ll.bias is not None: + ll.bias.data = ll.bias.data.half() + + if isinstance(ll, nn.MultiheadAttention): + for attr in [*[f"{s}_proj_weight" for s in ["in", "q", "k", "v"]], "in_proj_bias", "bias_k", "bias_v"]: + tensor = getattr(ll, attr) + if tensor is not None: + tensor.data = tensor.data.half() + + for name in ["text_projection", "proj"]: + if hasattr(ll, name): + attr = getattr(ll, name) + if attr is not None: + attr.data = attr.data.half() + + model.apply(_convert_weights_to_fp16) + + +def build_model(state_dict: dict): + """Build the CLIP model from a state dictionary.""" + vit = state_dict.get("visual.proj") is not None + + if vit: + vision_width = state_dict["visual.conv1.weight"].shape[0] + vision_layers = len( + [k for k in state_dict.keys() if k.startswith("visual.") and k.endswith(".attn.in_proj_weight")] + ) + vision_patch_size = state_dict["visual.conv1.weight"].shape[-1] + grid_size = round((state_dict["visual.positional_embedding"].shape[0] - 1) ** 0.5) + image_resolution = vision_patch_size * grid_size + else: + counts: list = [ + len(set(k.split(".")[2] for k in state_dict if k.startswith(f"visual.layer{b}"))) for b in [1, 2, 3, 4] + ] + vision_layers = tuple(counts) + vision_width = state_dict["visual.layer1.0.conv1.weight"].shape[0] + output_width = round((state_dict["visual.attnpool.positional_embedding"].shape[0] - 1) ** 0.5) + vision_patch_size = None + assert output_width**2 + 1 == state_dict["visual.attnpool.positional_embedding"].shape[0] + image_resolution = output_width * 32 + + embed_dim = state_dict["text_projection"].shape[1] + context_length = state_dict["positional_embedding"].shape[0] + vocab_size = state_dict["token_embedding.weight"].shape[0] + transformer_width = state_dict["ln_final.weight"].shape[0] + transformer_heads = transformer_width // 64 + transformer_layers = len(set(k.split(".")[2] for k in state_dict if k.startswith("transformer.resblocks"))) + + model = CLIP( + embed_dim, + image_resolution, + vision_layers, + vision_width, + vision_patch_size, + context_length, + vocab_size, + transformer_width, + transformer_heads, + transformer_layers, + ) + + for key in ["input_resolution", "context_length", "vocab_size"]: + if key in state_dict: + del state_dict[key] + + convert_weights(model) + model.load_state_dict(state_dict) + return model.eval() + + +def _download(url: str, root: str): + """ + Download a file from a URL and place it in root. + + Args: + url (str): URL to download file from. + root (str): Directory to place the downloaded file. + + Returns: + str: Path to the downloaded file. + """ + os.makedirs(root, exist_ok=True) + filename = os.path.basename(url) + + expected_sha256 = url.split("/")[-2] + download_target = os.path.join(root, filename) + + if os.path.exists(download_target) and not os.path.isfile(download_target): + raise RuntimeError(f"{download_target} exists and is not a regular file") + + if os.path.isfile(download_target): + if hashlib.sha256(open(download_target, "rb").read()).hexdigest() == expected_sha256: + return download_target + else: + warnings.warn(f"{download_target} exists, but the SHA256 checksum does not match; re-downloading the file") + + with urllib.request.urlopen(url) as source, open(download_target, "wb") as output: + with tqdm( + total=int(source.info().get("Content-Length")), ncols=80, unit="iB", unit_scale=True, unit_divisor=1024 + ) as loop: + while True: + buffer = source.read(8192) + if not buffer: + break + + output.write(buffer) + loop.update(len(buffer)) + + if hashlib.sha256(open(download_target, "rb").read()).hexdigest() != expected_sha256: + raise RuntimeError("Model has been downloaded but the SHA256 checksum does not not match") + + return download_target + + +def _convert_image_to_rgb(image): + """ + Convert an image to RGB format. + + Args: + image (PIL.Image): The image to convert. + + Returns: + PIL.Image: The converted RGB image. + """ + return image.convert("RGB") + + +def _transform(n_px): + """ + Create a transformation pipeline for preprocessing images. + + Args: + n_px (int): The desired size of the transformed image. + + Returns: + Compose: A torchvision transform pipeline. + """ + return Compose( + [ + Resize(n_px, interpolation=BICUBIC), + CenterCrop(n_px), + _convert_image_to_rgb, + ToTensor(), + Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)), + ] + ) + + +def available_models() -> List[str]: + """Returns the names of available CLIP models""" + return list(_MODELS.keys()) + + +def load( + name: str, + device: Union[str, torch.device] = "cuda" if torch.cuda.is_available() else "cpu", + jit: bool = False, + download_root: str = None, +): + """Load a CLIP model + + Parameters + ---------- + name : str + A model name listed by `clip.available_models()`, or the path to a model checkpoint containing the state_dict + + device : Union[str, torch.device] + The device to put the loaded model + + jit : bool + Whether to load the optimized JIT model or more hackable non-JIT model (default). + + download_root: str + path to download the model files; by default, it uses "~/.cache/clip" + + Returns + ------- + model : torch.nn.Module + The CLIP model + + preprocess : Callable[[PIL.Image], torch.Tensor] + A torchvision transform that converts a PIL image into a tensor that the returned model can take as its input + """ + if name in _MODELS: + model_path = _download(_MODELS[name], download_root or os.path.expanduser("~/.cache/clip")) + elif os.path.isfile(name): + model_path = name + else: + raise RuntimeError(f"Model {name} not found; available models = {available_models()}") + + with open(model_path, "rb") as opened_file: + try: + # loading JIT archive + model = torch.jit.load(opened_file, map_location=device if jit else "cpu").eval() + state_dict = None + except RuntimeError: + # loading saved state dict + if jit: + warnings.warn(f"File {model_path} is not a JIT archive. Loading as a state dict instead") + jit = False + state_dict = torch.load(opened_file, map_location="cpu") + + if not jit: + model = build_model(state_dict or model.state_dict()).to(device) + if str(device) == "cpu": + model.float() + return model, _transform(model.visual.input_resolution) + + # patch the device names + device_holder = torch.jit.trace(lambda: torch.ones([]).to(torch.device(device)), example_inputs=[]) + device_node = [n for n in device_holder.graph.findAllNodes("prim::Constant") if "Device" in repr(n)][-1] + + def patch_device(module): + try: + graphs = [module.graph] if hasattr(module, "graph") else [] + except RuntimeError: + graphs = [] + + if hasattr(module, "forward1"): + graphs.append(module.forward1.graph) + + for graph in graphs: + for node in graph.findAllNodes("prim::Constant"): + if "value" in node.attributeNames() and str(node["value"]).startswith("cuda"): + node.copyAttributes(device_node) + + model.apply(patch_device) + patch_device(model.encode_image) + patch_device(model.encode_text) + + # patch dtype to float32 on CPU + if str(device) == "cpu": + float_holder = torch.jit.trace(lambda: torch.ones([]).float(), example_inputs=[]) + float_input = list(float_holder.graph.findNode("aten::to").inputs())[1] + float_node = float_input.node() + + def patch_float(module): + try: + graphs = [module.graph] if hasattr(module, "graph") else [] + except RuntimeError: + graphs = [] + + if hasattr(module, "forward1"): + graphs.append(module.forward1.graph) + + for graph in graphs: + for node in graph.findAllNodes("aten::to"): + inputs = list(node.inputs()) + for i in [1, 2]: # dtype can be the second or third argument to aten::to() + if inputs[i].node()["value"] == 5: + inputs[i].node().copyAttributes(float_node) + + model.apply(patch_float) + patch_float(model.encode_image) + patch_float(model.encode_text) + + model.float() + + return model, _transform(model.input_resolution.item()) diff --git a/cosmos3/_src/imaginaire/modules/res_sampler.py b/cosmos3/_src/imaginaire/modules/res_sampler.py new file mode 100644 index 00000000..dd76d1b1 --- /dev/null +++ b/cosmos3/_src/imaginaire/modules/res_sampler.py @@ -0,0 +1,287 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A general framework for various sampling algorithm from a diffusion model. +Impl based on +* Refined Exponential Solver (RES) in https://arxiv.org/pdf/2308.02157 +* also clude other impl, DDIM, DEIS, DPM-Solver, EDM sampler. +Most of sampling algorihtm, Runge-Kutta, Multi-step, etc, can be impl in this framework by \ + adding new step function in get_runge_kutta_fn or get_multi_step_fn. +""" + +import math +from typing import Any, Callable, List, Literal, Optional, Tuple, Union + +import attrs +import torch + +from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.imaginaire.functional.multi_step import get_multi_step_fn, is_multi_step_fn_supported +from cosmos3._src.imaginaire.functional.runge_kutta import get_runge_kutta_fn, is_runge_kutta_fn_supported +from cosmos3._src.imaginaire.utils import log + +COMMON_SOLVER_OPTIONS = Literal["2ab", "2mid", "1euler"] + + +@make_freezable +@attrs.define(slots=False) +class SolverConfig: + is_multi: bool = False + rk: str = "2mid" + multistep: str = "2ab" + # following parameters control stochasticity, see EDM paper + # BY default, we use deterministic with no stochasticity + s_churn: float = 0.0 + s_t_max: float = float("inf") + s_t_min: float = 0.05 + s_noise: float = 1.0 + + +@make_freezable +@attrs.define(slots=False) +class SolverTimestampConfig: + nfe: int = 50 + t_min: float = 0.002 + t_max: float = 80.0 + order: float = 7.0 + is_forward: bool = False # whether generate forward or backward timestamps + + +@make_freezable +@attrs.define(slots=False) +class SamplerConfig: + solver: SolverConfig = attrs.field(factory=SolverConfig) + timestamps: SolverTimestampConfig = attrs.field(factory=SolverTimestampConfig) + sample_clean: bool = True # whether run one last step to generate clean image + + +def get_rev_ts( + t_min: float, t_max: float, num_steps: int, ts_order: Union[int, float], is_forward: bool = False +) -> torch.Tensor: + """ + Generate a sequence of reverse time steps. + + Args: + t_min (float): The minimum time value. + t_max (float): The maximum time value. + num_steps (int): The number of time steps to generate. + ts_order (Union[int, float]): The order of the time step progression. + is_forward (bool, optional): If True, returns the sequence in forward order. Defaults to False. + + Returns: + torch.Tensor: A tensor containing the generated time steps in reverse or forward order. + + Raises: + ValueError: If `t_min` is not less than `t_max`. + TypeError: If `ts_order` is not an integer or float. + """ + if t_min >= t_max: + raise ValueError("t_min must be less than t_max") + + if not isinstance(ts_order, (int, float)): + raise TypeError("ts_order must be an integer or float") + + step_indices = torch.arange(num_steps + 1, dtype=torch.float64) + time_steps = ( + t_max ** (1 / ts_order) + step_indices / num_steps * (t_min ** (1 / ts_order) - t_max ** (1 / ts_order)) + ) ** ts_order + + if is_forward: + return time_steps.flip(dims=(0,)) + + return time_steps + + +class Sampler(torch.nn.Module): + def __init__(self, cfg: Optional[SamplerConfig] = None): + super().__init__() + if cfg is None: + cfg = SamplerConfig() + self.cfg = cfg + + @torch.no_grad() + def forward( + self, + x0_fn: Callable, + x_sigma_max: torch.Tensor, + num_steps: int = 35, + sigma_min: float = 0.002, + sigma_max: float = 80, + rho: float = 7, + S_churn: float = 0, + S_min: float = 0, + S_max: float = float("inf"), + S_noise: float = 1, + solver_option: str = "2ab", + ) -> torch.Tensor: + in_dtype = x_sigma_max.dtype + + def float64_x0_fn(x_B_StateShape: torch.Tensor, t_B: torch.Tensor) -> torch.Tensor: + return x0_fn(x_B_StateShape.to(in_dtype), t_B.to(in_dtype)).to(torch.float64) + + is_multistep = is_multi_step_fn_supported(solver_option) + is_rk = is_runge_kutta_fn_supported(solver_option) + assert is_multistep or is_rk, f"Only support multistep or Runge-Kutta method, got {solver_option}" + + solver_cfg = SolverConfig( + s_churn=S_churn, + s_t_max=S_max, + s_t_min=S_min, + s_noise=S_noise, + is_multi=is_multistep, + rk=solver_option, + multistep=solver_option, + ) + timestamps_cfg = SolverTimestampConfig(nfe=num_steps, t_min=sigma_min, t_max=sigma_max, order=rho) + sampler_cfg = SamplerConfig(solver=solver_cfg, timestamps=timestamps_cfg, sample_clean=True) + + return self._forward_impl(float64_x0_fn, x_sigma_max, sampler_cfg).to(in_dtype) + + @torch.no_grad() + def _forward_impl( + self, + denoiser_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + noisy_input_B_StateShape: torch.Tensor, + sampler_cfg: Optional[SamplerConfig] = None, + callback_fns: Optional[List[Callable]] = None, + ) -> torch.Tensor: + """ + Internal implementation of the forward pass. + + Args: + denoiser_fn: Function to denoise the input. + noisy_input_B_StateShape: Input tensor with noise. + sampler_cfg: Configuration for the sampler. + callback_fns: List of callback functions to be called during sampling. + + Returns: + torch.Tensor: Denoised output tensor. + """ + sampler_cfg = self.cfg if sampler_cfg is None else sampler_cfg + solver_order = 1 if sampler_cfg.solver.is_multi else int(sampler_cfg.solver.rk[0]) + num_timestamps = sampler_cfg.timestamps.nfe // solver_order + + sigmas_L = get_rev_ts( + sampler_cfg.timestamps.t_min, sampler_cfg.timestamps.t_max, num_timestamps, sampler_cfg.timestamps.order + ).to(noisy_input_B_StateShape.device) + + denoised_output = differential_equation_solver( + denoiser_fn, sigmas_L, sampler_cfg.solver, callback_fns=callback_fns + )(noisy_input_B_StateShape) + + if sampler_cfg.sample_clean: + # Override denoised_output with fully denoised version + ones = torch.ones(denoised_output.size(0), device=denoised_output.device, dtype=denoised_output.dtype) + denoised_output = denoiser_fn(denoised_output, sigmas_L[-1] * ones) + + return denoised_output + + +def fori_loop(lower: int, upper: int, body_fun: Callable[[int, Any], Any], init_val: Any) -> Any: + """ + Implements a for loop with a function. + + Args: + lower: Lower bound of the loop (inclusive). + upper: Upper bound of the loop (exclusive). + body_fun: Function to be applied in each iteration. + init_val: Initial value for the loop. + + Returns: + The final result after all iterations. + """ + val = init_val + for i in range(lower, upper): + # Add log during sampling to meet APS job health requirement of one log every 2mins + if i % 10 == 0: + log.info(f"fori_loop: {i}") + val = body_fun(i, val) + return val + + +def differential_equation_solver( + x0_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + sigmas_L: torch.Tensor, + solver_cfg: SolverConfig, + callback_fns: Optional[List[Callable]] = None, +) -> Callable[[torch.Tensor], torch.Tensor]: + """ + Creates a differential equation solver function. + + Args: + x0_fn: Function to compute x0 prediction. + sigmas_L: Tensor of sigma values with shape [L,]. + solver_cfg: Configuration for the solver. + callback_fns: Optional list of callback functions. + + Returns: + A function that solves the differential equation. + """ + num_step = len(sigmas_L) - 1 + + if solver_cfg.is_multi: + update_step_fn = get_multi_step_fn(solver_cfg.multistep) + else: + update_step_fn = get_runge_kutta_fn(solver_cfg.rk) + + eta = min(solver_cfg.s_churn / (num_step + 1), math.sqrt(1.2) - 1) + + def sample_fn(input_xT_B_StateShape: torch.Tensor) -> torch.Tensor: + """ + Samples from the differential equation. + + Args: + input_xT_B_StateShape: Input tensor with shape [B, StateShape]. + + Returns: + Output tensor with shape [B, StateShape]. + """ + ones_B = torch.ones(input_xT_B_StateShape.size(0), device=input_xT_B_StateShape.device, dtype=torch.float64) + + def step_fn( + i_th: int, state: Tuple[torch.Tensor, Optional[List[torch.Tensor]]] + ) -> Tuple[torch.Tensor, Optional[List[torch.Tensor]]]: + input_x_B_StateShape, x0_preds = state + sigma_cur_0, sigma_next_0 = sigmas_L[i_th], sigmas_L[i_th + 1] + + # algorithm 2: line 4-6 + if solver_cfg.s_t_min < sigma_cur_0 < solver_cfg.s_t_max: + hat_sigma_cur_0 = sigma_cur_0 + eta * sigma_cur_0 + input_x_B_StateShape = input_x_B_StateShape + ( + hat_sigma_cur_0**2 - sigma_cur_0**2 + ).sqrt() * solver_cfg.s_noise * torch.randn_like(input_x_B_StateShape) + sigma_cur_0 = hat_sigma_cur_0 + + if solver_cfg.is_multi: + x0_pred_B_StateShape = x0_fn(input_x_B_StateShape, sigma_cur_0 * ones_B) + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_pred_B_StateShape, x0_preds + ) + else: + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_fn + ) + + if callback_fns: + for callback_fn in callback_fns: + callback_fn(**locals()) + + return output_x_B_StateShape, x0_preds + + x_at_eps, _ = fori_loop(0, num_step, step_fn, [input_xT_B_StateShape, None]) + return x_at_eps + + return sample_fn diff --git a/cosmos3/_src/imaginaire/serialization.py b/cosmos3/_src/imaginaire/serialization.py new file mode 100644 index 00000000..3a17f60f --- /dev/null +++ b/cosmos3/_src/imaginaire/serialization.py @@ -0,0 +1,447 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import functools +import importlib +import json +import os +from collections.abc import Callable as Callable2 +from collections.abc import Mapping, Sequence +from dataclasses import fields, is_dataclass +from types import UnionType +from typing import Any, List, Optional, TypeVar, Union, get_args, get_origin + +import attrs +import torch +import yaml +from omegaconf import DictConfig, ListConfig, OmegaConf + +from cosmos3._src.imaginaire.lazy_config import LazyCall, LazyDict, instantiate +from cosmos3._src.imaginaire.lazy_config.lazy import get_default_params + +T = TypeVar("T") + + +def from_dict( + x: dict, clazz: str | type | None = None, force_construct_target: bool | None = None, field_name: str = "" +) -> T: ... +def to_dict(x: T, field_name: str = "", hydra_compat: bool = True) -> dict: ... +def from_yaml(path: str | None = None, clazz: type | None = None, file_like_or_str=None) -> T: + if path: + assert os.path.exists(path), f"{path} does not exist" + with open(path) as in_f: + return from_dict(yaml.safe_load(in_f), clazz=clazz) + elif file_like_or_str: + return from_dict(yaml.safe_load(file_like_or_str), clazz=clazz) + else: + raise ValueError("expected file_like_or_str or path to not be None") + + +def _yaml_safe(obj: Any) -> Any: + # primitives + if obj is None or isinstance(obj, (bool, int, float, str)): + return obj + + # dict-like + if isinstance(obj, Mapping): + return {str(k): _yaml_safe(v) for k, v in obj.items()} + + # list/tuple-like (but not strings/bytes) + if isinstance(obj, Sequence) and not isinstance(obj, (str, bytes, bytearray)): + return [_yaml_safe(v) for v in obj] + + # classes / functions / bound methods -> import path + if hasattr(obj, "__module__") and hasattr(obj, "__qualname__"): + return f"{obj.__module__}.{obj.__qualname__}" + + # torch dtype, Path, enums, dataclasses, etc. + return str(obj) + + +def to_yaml(config: T, out_path: str | None = None) -> str | None: + config_dict = to_dict(config) + safe_dict = _yaml_safe(config_dict) + + if out_path is not None: + with open(out_path, "w") as f: + yaml.safe_dump( + safe_dict, + f, + sort_keys=False, + default_flow_style=False, + allow_unicode=True, + ) + return None + + return yaml.safe_dump( + safe_dict, + sort_keys=False, + default_flow_style=False, + allow_unicode=True, + ) + + +def load_callable(name: str) -> Callable2 | None: + if not name: + return None + + idx = name.rfind(".") + assert idx != -1, "expected ." + module_name = name[0:idx] + fn_name = name[idx + 1 :] + mod = importlib.import_module(module_name) + return getattr(mod, fn_name) + + +def maybe_load_callable(name: str | Callable2 | None) -> Callable2 | None: + if isinstance(name, str): + return load_callable(name) + + return name + + +def maybe_idx(x: Any, idx: int) -> Any: + if idx < 0 or idx >= len(x): + return None + return x[idx] + + +def is_attrs(x: Any) -> bool: + return hasattr(x, "__attrs_attrs__") + + +def to_qualitified_name(x) -> str: + # Handle functools.partial explicitly + if isinstance(x, functools.partial): + fn = x.func + fn_name = to_qualitified_name(fn) + + # args/keywords may contain non-serializable stuff; stringify safely + args = [] + if x.args: + args = [repr(a) for a in x.args] + + kwargs = {} + if x.keywords: + kwargs = {str(k): repr(v) for k, v in x.keywords.items()} + + if args or kwargs: + return f"functools.partial({fn_name}, args={args}, kwargs={kwargs})" + return f"functools.partial({fn_name})" + + # Normal callable/class/module qualified name + mod = getattr(x, "__module__", None) + qn = getattr(x, "__qualname__", None) + + if mod and qn: + return f"{mod}.{qn}" + + # Some callables only have __name__ + name = getattr(x, "__name__", None) + if mod and name: + return f"{mod}.{name}" + + # Fallback: repr + return repr(x) + + +def is_optional(x: type) -> bool: + origin = get_origin(x) + args = get_args(x) + return origin is Optional or (origin in (Union, UnionType) and len(args) == 2 and type(None) in args) + + +def _to_dict_value(x: T, field_type: type, metadata: dict, field_name: str = ""): + + t = type(x) + + # attrs specific + if x is attrs.NOTHING or x is None: + return None + # torch specifics + elif field_type in (torch.memory_format, torch.dtype): + return str(x) + # i4 specific types + elif field_type == LazyCall: + result = _to_dict_value(x, field_type._target, metadata, field_name) + return result + elif field_type in (DictConfig, LazyDict): + if "_target_" in x: + default_params = get_default_params(x["_target_"]) + for default_key, default_v in default_params.items(): + if default_key not in x: + x[default_key] = default_v + result = _to_dict_value(x, dict, metadata, field_name) + object_type = getattr(x._metadata, "object_type", None) + if object_type and (is_dataclass(object_type) or is_attrs(object_type)): + result.setdefault("_target_", to_qualitified_name(object_type)) + return result + elif field_type == ListConfig: + return _to_dict_value(x, list, metadata, field_name) + # general python types + dataclasses + attrs + # * meta types + elif field_type == type or field_type == abc.ABCMeta: + + return to_qualitified_name(x) + elif get_origin(field_type) is type: + return to_qualitified_name(x) + elif callable(x) or get_origin(field_type) is Callable2: + if callable(x): + return to_qualitified_name(x) + else: + assert isinstance(x, str), f"{x.__class__=}" + return x + elif is_dataclass(t) or is_attrs(t): + return to_dict(x, field_name=field_name) + # * built-in composites types + elif is_optional(field_type): + return _to_dict_value(x, get_args(field_type)[0], metadata) + elif get_origin(field_type) in (Union, UnionType): + raise AssertionError("unions are not implemented yet!") + # * primitives + elif t in (dict,) or field_type in (dict,) or get_origin(field_type) in (dict,): + return { + _to_dict_value( + k, + maybe_idx(get_args(field_type), 0) or type(k), + metadata, + field_name=f"{field_name}.{k}.key", + ): _to_dict_value( + v, + maybe_idx(get_args(field_type), 1) or type(v), + metadata, + field_name=f"{field_name}.{k}", + ) + for k, v in x.items() + } + elif ( + t + in ( + tuple, + list, + ) + or field_type + in ( + tuple, + list, + ) + or get_origin(field_type) in (tuple, list) + ): + if field_type is None or field_type not in ( + tuple, + list, + ): + field_type = list + + return field_type( + [ + _to_dict_value(xx, maybe_idx(get_args(field_type), 0) or type(xx), metadata, field_name + f"[{i}]") + for i, xx in enumerate(x) + ] + ) + elif field_type in (int, str, float, bool): + result = field_type(x) + return result + else: # catch all for everything else + return x + + +def to_dict(x: T, field_name: str = "", hydra_compat: bool = True) -> dict: + if is_dataclass(x): + result = {} + if hydra_compat: + result["_target_"] = to_qualitified_name(x.__class__) + for f in fields(x): + + if hydra_compat and f.name == "defaults": + continue + result[f.name] = _to_dict_value( + x.__dict__[f.name], + f.type, + f.metadata, + field_name=field_name + f".{f.name}" if field_name else f.name, + ) + return result + elif is_attrs(x): + # references: + # - https://github.com/python-attrs/attrs/blob/main/src/attr/_funcs.py + attrs.resolve_types(x.__class__) + + result = {} + if hydra_compat: + result["_target_"] = to_qualitified_name(x.__class__) + for f in attrs.fields(x.__class__): + + if hydra_compat and f.name == "defaults": + continue + result[f.name] = _to_dict_value( + getattr(x, f.name), + f.type, + f.metadata, + field_name=field_name + f".{f.name}" if field_name else f.name, + ) + return result + + +def _from_dict_value( + x: T, + field_type: type, + concrete_type: type, + field_name: str, + force_construct_target: bool | None = None, +): + + + is_dc_type = is_dataclass(field_type) + is_attrs_type = is_attrs(field_type) + origin = get_origin(field_type) or field_type + args = get_args(field_type) + + if x is None: + return None + elif field_type in (torch.memory_format, torch.dtype): + return maybe_load_callable(x) + elif field_type == LazyCall: + return _from_dict_value(x, field_type._target, concrete_type, field_name=field_name) + elif is_dc_type or is_attrs_type: + if concrete_type == str: + assert isinstance(x, str) + if x.endswith(".json"): + json_value = json.loads(x) + return from_dict( + json_value, field_type, force_construct_target=force_construct_target, field_name=field_name + ) + elif x.endswith(".yaml"): + yaml_value = yaml.safe_load(x) + return from_dict( + yaml_value, field_type, force_construct_target=force_construct_target, field_name=field_name + ) + else: + raise AssertionError(f"unexpected string: {x}") + else: + assert not isinstance(x, str) + return from_dict(x, field_type, field_name=field_name) + elif field_type in (DictConfig, LazyDict) or origin in (dict,): + + construct_target = x.get("_recursive_", field_type == DictConfig) + if force_construct_target is not None: + construct_target = force_construct_target + + target_value = x.get("_target_") + target_cls = maybe_load_callable(target_value) + + if target_value and construct_target and (is_dataclass(target_cls) or is_attrs(target_cls)): + result = from_dict(x, target_cls, force_construct_target=force_construct_target, field_name=field_name) + else: + result = { + _from_dict_value( + k, + maybe_idx(get_args(field_type), 0) or type(k), + type(k), + field_name=f"{field_name}.{k}.key", + force_construct_target=construct_target, + ): _from_dict_value( + v, + maybe_idx(get_args(field_type), 1) or type(v), + type(v), + field_name=f"{field_name}.{k}", + force_construct_target=construct_target, + ) + for k, v in x.items() + } + if field_type in (DictConfig, LazyDict): + result = OmegaConf.structured(result, flags={"allow_objects": True}) + if construct_target: + result = instantiate(result) + if "_target_" in result: + result["_target_"] = maybe_load_callable(result["_target_"]) + elif construct_target and target_cls: # instantiate a regular class from a dict + special_keys = { + "_target_", + "_recursive_", + "_convert_", + "_args_", + "_kwargs_", + } + constructable_items = { + k: v for k, v in result.items() if not (isinstance(k, str) and k in special_keys) + } + result = target_cls(**constructable_items) + return result + elif field_type is ListConfig or origin in ( + list, + List, + ): + return [ + _from_dict_value( + xx, maybe_idx(get_args(field_type), 0) or type(xx), type(xx), field_name=f"{field_type}[{i}]" + ) + for i, xx in enumerate(x) + ] + elif is_optional(field_type): + return _from_dict_value(x, args[0], type(x), field_name=field_name) + elif origin in (Union, UnionType): + raise AssertionError("unions are not implemented yet!") + elif origin is Callable2 or origin is type: + return maybe_load_callable(x) + elif field_type in (int, float, str, bool): + return x + elif field_type is type(None) or field_type == Any: # no typing + return x + else: + raise TypeError( + f"unexpected type: {field_type} (origin={origin}, concrete_type={concrete_type}, args={args}, x={x})" + ) + + +def from_dict( + x: dict, clazz: type | None = None, force_construct_target: bool | None = None, field_name: str = "" +) -> T: + if clazz is None: + assert "_target_" in x + clazz = maybe_load_callable(x["_target_"]) + + assert is_dataclass(clazz) or is_attrs(clazz), f"{clazz} is not a dataclass or attrs" + if is_dataclass(clazz): + construct_args = {} + for f in fields(clazz): + if f.name in x: + construct_args[f.name] = _from_dict_value( + x[f.name], + f.type, + type(x[f.name]), + field_name=field_name + "." + f.name if field_name else f.name, + force_construct_target=force_construct_target, + ) + elif is_optional(f.type): + construct_args[f.name] = None + return clazz(**construct_args) + elif is_attrs(clazz): + attrs.resolve_types(clazz) + + construct_args = {} + for f in attrs.fields(clazz): + if f.name in x: + construct_args[f.name] = _from_dict_value( + x[f.name], + f.type, + type(x[f.name]), + field_name=field_name + "." + f.name if field_name else f.name, + force_construct_target=force_construct_target, + ) + elif is_optional(f.type): + construct_args[f.name] = None + return clazz(**construct_args) diff --git a/cosmos3/_src/imaginaire/trainer.py b/cosmos3/_src/imaginaire/trainer.py new file mode 100644 index 00000000..e0f9a675 --- /dev/null +++ b/cosmos3/_src/imaginaire/trainer.py @@ -0,0 +1,447 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import inspect +import os +import signal + +import torch +import torch.distributed as dist +import torch.utils.data + +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.utils.context_managers import distributed_init +from cosmos3._src.imaginaire.utils.profiling import maybe_enable_memory_snapshot, maybe_enable_nsys_profiling, maybe_enable_profiling + +try: + from megatron.core import parallel_state + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + + +from cosmos3._src.imaginaire.lazy_config import LazyConfig, instantiate +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import callback, distributed, ema, log, misc +from cosmos3._src.imaginaire.utils.checkpointer import Checkpointer +from cosmos3._src.imaginaire.utils.misc import StragglerDetectorV2 + + + +class ImaginaireTrainer: + """The base trainer class of Imaginaire. + + All trainers in Imaginaire should inherit ImaginaireTrainer. It contains the basic functionality for model training + (particularly suited for large-scale training), including data parallel (DDP/FSDP), model weight average (EMA), + mixed-precision training (fp16/bf16). + + Attributes: + checkpointer (Checkpointer): checkpointer object to save/load model weights and optimizer states. + training_timer (misc.Timer): Timer object to time code blocks and functions. + """ + + def __init__(self, config): + """Constructor of the trainer. + + Args: + config (Config): The config object for the Imaginaire codebase. + """ + super().__init__() + self.config = config + # Set up the distributed computing environment. + with distributed_init(): + distributed.init() + # Set up parallel states. + if hasattr(config.model, "context_parallel_size"): + if config.model_parallel.context_parallel_size > 1: + raise ValueError( + "Both config.model.context_parallel_size and config.model_parallel.context_parallel_size are set. " + "config.model.context_parallel_size is deprecated. Please only set config.model_parallel.context_parallel_size." + ) + else: + log.critical( + "Using deprecated config.model.context_parallel_size. Please use config.model_parallel.context_parallel_size instead." + ) + config.model_parallel.context_parallel_size = config.model.context_parallel_size + if USE_MEGATRON: + if ( + "create_gloo_process_groups" + in inspect.signature(parallel_state.initialize_model_parallel).parameters + ): + parallel_state.initialize_model_parallel( + pipeline_model_parallel_size=config.model_parallel.pipeline_model_parallel_size, + tensor_model_parallel_size=config.model_parallel.tensor_model_parallel_size, + context_parallel_size=config.model_parallel.context_parallel_size, + create_gloo_process_groups=False, + ) + else: + parallel_state.initialize_model_parallel( + pipeline_model_parallel_size=config.model_parallel.pipeline_model_parallel_size, + tensor_model_parallel_size=config.model_parallel.tensor_model_parallel_size, + context_parallel_size=config.model_parallel.context_parallel_size, + ) + # `config.model_parallel.sequence_parallel` is a bool that indicates whether to use sequence parallelism. + # It is not part of the original `parallel_state` API, so we need to set it manually. + parallel_state.sequence_parallel = config.model_parallel.sequence_parallel + if parallel_state.sequence_parallel: + os.environ["CUDA_DEVICE_MAX_CONNECTIONS"] = "1" + + # Create the local job directory, save the config file, and pipe to a local log. + if distributed.is_rank0(): + os.makedirs(config.job.path_local, exist_ok=True) + # Save the config as .pkl for reproducibility. + LazyConfig.save_pkl(config, f"{config.job.path_local}/config.pkl") + # Save the config as .yaml for reading or parsing experiment hyperparameters. + LazyConfig.save_yaml(config, f"{config.job.path_local}/config.yaml") + dist.barrier() + if INTERNAL: + log.init_loguru_file(f"{config.job.path_local}/stdout.log") + if distributed.is_rank0(): + # Print important environment variables and the effective config. + log.info("Config:\n" + config.pretty_print(use_color=True)) + misc.print_environ_variables(["TORCH_HOME", "IMAGINAIRE_OUTPUT_ROOT", "ENABLE_ONELOGGER"]) + else: + misc.print_environ_variables(["HF_HOME", "IMAGINAIRE_OUTPUT_ROOT"]) + # Set the random seed. If multi-GPU, different ranks are set with different seeds. + misc.set_random_seed(seed=config.trainer.seed, by_rank=True) + # Initialize cuDNN. + torch.backends.cudnn.deterministic = config.trainer.cudnn.deterministic + torch.backends.cudnn.benchmark = config.trainer.cudnn.benchmark + # Initialize the callback functions. + self.callbacks = callback.CallBackGroup(config=config, trainer=self) + # Initialize the model checkpointer. + if config.checkpoint.type is None: + self.checkpointer = Checkpointer(config.checkpoint, config.job, callbacks=self.callbacks) + else: + self.checkpointer: Checkpointer = instantiate( + config.checkpoint.type, config.checkpoint, config.job, callbacks=self.callbacks + ) + # Initialize the timer for speed benchmarking. + self.training_timer = misc.TrainingTimer() + # Initialize Straggler Detection + self.straggler_detector = StragglerDetectorV2( + enabled=self.config.trainer.straggler_detection.enabled, + report_freq=self.config.trainer.straggler_detection.report_freq, + profile_freq=self.config.trainer.straggler_detection.profile_freq, + max_diff=self.config.trainer.straggler_detection.max_diff, + raise_error=self.config.trainer.straggler_detection.raise_error, + save_s3=self.config.trainer.straggler_detection.save_s3, + ) + misc.set_torch_compile_options( + self.config.trainer.compile_config.recompile_limit, self.config.trainer.compile_config.use_duck_shape + ) + self.straggler_detector.initialize() + # Send a TimeoutError if a training step takes over timeout_period seconds. + signal.signal(signal.SIGALRM, functools.partial(misc.timeout_handler, config.trainer.timeout_period)) # type: ignore + + def _fetch_and_broadcast_data( + self, + model: ImaginaireModel, + dataloader_iter, + iteration: int, + ): + """ + Fetches data from the dataloader on the batch owner rank and broadcasts it to all other ranks in the Context Parallel group if CP is enabled. + When CP is disabled, data is fetched from the dataloader on the current rank and no broadcasting is needed. + + Args: + model (ImaginaireModel): The model containing parallel dimensions info. + dataloader_iter: Iterator for the dataloader. + iteration (int): Current iteration number to determine the batch owner. + + Returns: + tuple: (data_batch, stop_signal) + - data_batch: The fetched data batch (or None if stopped/not owner). + - stop_signal (bool): True if StopIteration was encountered. + """ + parallel_dims = getattr(model, "parallel_dims", None) + if parallel_dims is None or not parallel_dims.cp_enabled: + try: + return next(dataloader_iter), False + except StopIteration: + return None, True + + # To prevent redundant data loading among the Context Parallel ranks, + # one of the Context Parallel ranks (round-robin) broadcasts the data to all other cp ranks. + batch_owner_rank = iteration % parallel_dims.cp_mesh.size() + stop_signal = False + data_batch = None + + if parallel_dims.cp_rank == batch_owner_rank: + try: + data_batch = next(dataloader_iter) + except StopIteration: + stop_signal = True + data_batch = None + + objs = [data_batch, stop_signal] + + # Calculate the global rank of the batch owner within the CP group + global_src_rank = dist.get_global_rank(parallel_dims.cp_mesh.get_group(), batch_owner_rank) + + dist.broadcast_object_list( + objs, + src=global_src_rank, + group=parallel_dims.cp_mesh.get_group(), + ) + + return objs[0], objs[1] + + def train( + self, + model: ImaginaireModel, + dataloader_train: torch.utils.data.DataLoader, + dataloader_val: torch.utils.data.DataLoader, + ) -> None: + """The training function. + + Args: + model (ImaginaireModel): The PyTorch model. + dataloader_train (torch.utils.data.DataLoader): The training data loader. + dataloader_val (torch.utils.data.DataLoader): The validation data loader. + """ + # Leaving this for backward compability for now, but we can think about moving this to model.on_train_start for all models. + model = model.to("cuda", memory_format=self.config.trainer.memory_format) # type: ignore + model.on_train_start(self.config.trainer.memory_format) + + # Initialize the optimizer, scheduler, and grad_scaler. + self.callbacks.on_optimizer_init_start() + optimizer, scheduler = model.init_optimizer_scheduler(self.config.optimizer, self.config.scheduler) + grad_scaler = torch.amp.GradScaler("cuda", **self.config.trainer.grad_scaler_args) + self.callbacks.on_optimizer_init_end() + # Load the model checkpoint and get the starting iteration number. + iteration = self.checkpointer.load(model, optimizer, scheduler, grad_scaler) + if hasattr(dataloader_train, "set_start_iteration"): + dataloader_train.set_start_iteration(iteration * self.config.trainer.grad_accum_iter) + grad_accum_iter = 0 + log.critical(f"Distributed parallelism mode: {self.config.trainer.distributed_parallelism}") + if self.config.trainer.distributed_parallelism == "ddp": + # Create a DDP model wrapper. + model_ddp = distributed.parallel_model_wrapper(self.config.trainer.ddp, model) + elif self.config.trainer.distributed_parallelism == "fsdp": + model_ddp = model + else: + raise ValueError(f"Unknown distributed parallelism mode: {self.config.trainer.distributed_parallelism}") + + log.info("Starting training...") + sm_carveout = int(os.environ.get("GROUPED_MM_SM_CARVEOUT", "0")) + if sm_carveout: + torch._C._set_sm_carveout_experimental(sm_carveout) + log.info(f"Set SM carveout to {sm_carveout}") + self.callbacks.on_train_start(model, iteration=iteration) + # Initial validation. + if self.config.trainer.run_validation and iteration == 0 and self.config.trainer.run_validation_on_start: + self.validate(model, dataloader_val, iteration=iteration) + + if self.config.trainer.save_zero_checkpoint and iteration == 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=0) + + _end_training = False + with ( + maybe_enable_profiling(self.config, global_step=iteration) as torch_profiler, + maybe_enable_memory_snapshot(self.config, global_step=iteration) as memory_profiler, + maybe_enable_nsys_profiling(self.config, global_step=iteration) as nsys_profiler, + ): + while True: + dataloader_train_iter = iter(dataloader_train) + while True: + self.callbacks.on_before_dataloading(iteration) + try: + with ( + self.training_timer("dataloader_train"), + self.straggler_detector.profile_section( + "dataloading", + self.config.trainer.straggler_detection.analyze_dataloading, + profile_cuda=False, + ), + ): + data_batch, stop_signal = self._fetch_and_broadcast_data( + model, + dataloader_train_iter, + iteration, + ) + if stop_signal: + raise StopIteration + except StopIteration: + break + finally: + self.callbacks.on_after_dataloading(iteration) + # If max_iter is reached, exit the training loop. + if iteration >= self.config.trainer.max_iter: + _end_training = True + break + # Move all tensors in the data batch to GPU device. + data_batch = misc.to(data_batch, device="cuda") + # The actual training step. + self.callbacks.on_training_step_start(model, data_batch, iteration=iteration) + self.callbacks.on_training_step_batch_start(model, data_batch, iteration=iteration) + if not model.training: + model_ddp.train() + assert model_ddp.training, "model_ddp is not in training mode." + assert model.training, "model is not in training mode." + output_batch, loss, grad_accum_iter = self.training_step( + model_ddp, + optimizer, + scheduler, + grad_scaler, + data_batch, + iteration=iteration, + grad_accum_iter=grad_accum_iter, + ) + self.callbacks.on_training_step_batch_end( + model, data_batch, output_batch, loss, iteration=iteration + ) + # If the gradients are still being accumulated, continue to load the next training batch. + if grad_accum_iter != 0: + continue + # Do the following when an actual optimizer (update) step has been made. + iteration += 1 + # Save checkpoint. + if iteration % self.config.checkpoint.save_iter == 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_training_step_end(model, data_batch, output_batch, loss, iteration=iteration) + # Validation. + if self.config.trainer.run_validation and iteration % self.config.trainer.validation_iter == 0: + self.validate(model, dataloader_val, iteration=iteration) + # This iteration is successful; reset the timeout signal. + signal.alarm(self.config.trainer.timeout_period) + self.straggler_detector.generate_report(iteration) + if torch_profiler: + torch_profiler.step() + if memory_profiler: + memory_profiler.step() + if nsys_profiler: + nsys_profiler.step() + if _end_training: + break + log.success("Done with training.") + if sm_carveout: + torch._C._set_sm_carveout_experimental(None) + if iteration % self.config.checkpoint.save_iter != 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_train_end(model, iteration=iteration) + self.checkpointer.finalize() + distributed.barrier() + self.callbacks.on_app_end() + if dist.is_available() and dist.is_initialized(): + dist.destroy_process_group() + + def training_step( + self, + model_ddp: torch.nn.Module | distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + data: dict[str, torch.Tensor], + iteration: int = 0, + grad_accum_iter: int = 0, + ) -> tuple[dict[str, torch.Tensor], torch.Tensor, int]: + """The training step. + + Args: + model_ddp (torch.nn.Module | distributed.DistributedDataParallel): The model with a DDP wrapper or, the bare + module, depending on whether distributed training is enabled or not. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + iteration (int): Current iteration number. + grad_accum_iter (int): Number of gradient accumulation iterations. + + Returns: + output (dict[str, torch.Tensor]): The model output from the training data batch (dictionary of tensors). + loss (torch.Tensor): The total loss of the training data batch. + """ + # Only let DDP sync gradient at the last iteration of the gradient accumulation window + with distributed.ddp_sync_grad(model_ddp, grad_accum_iter == self.config.trainer.grad_accum_iter - 1): + self.callbacks.on_before_forward(iteration=iteration) + with self.training_timer("forward"): + with self.straggler_detector.profile_section( + "fwd", self.config.trainer.straggler_detection.analyze_forward + ): + output_batch, loss = model_ddp.training_step(data, iteration) + self.callbacks.on_after_forward(iteration=iteration) + self.callbacks.on_before_backward(model_ddp, loss, iteration=iteration) + with self.training_timer("backward"): + with self.straggler_detector.profile_section( + "bwd", self.config.trainer.straggler_detection.analyze_backward + ): + loss_scaled = grad_scaler.scale(loss / self.config.trainer.grad_accum_iter) + loss_scaled.backward() + if self.config.trainer.distributed_parallelism == "ddp": + model_ddp.module.on_after_backward() + else: + model_ddp.on_after_backward() + self.callbacks.on_after_backward(model_ddp, iteration=iteration) + grad_accum_iter += 1 + if grad_accum_iter == self.config.trainer.grad_accum_iter: + with self.training_timer("optimizer_step"): + with self.straggler_detector.profile_section( + "opt", self.config.trainer.straggler_detection.analyze_optimizer + ): + self.callbacks.on_before_optimizer_step( + model_ddp, optimizer, scheduler, grad_scaler, iteration=iteration + ) + model = model_ddp.module if self.config.trainer.distributed_parallelism == "ddp" else model_ddp + self._optimizer_step(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_before_zero_grad(model_ddp, optimizer, scheduler, iteration=iteration) + if self.config.trainer.distributed_parallelism == "ddp": + model_ddp.module.on_before_zero_grad(optimizer, scheduler, iteration=iteration) + else: + model_ddp.on_before_zero_grad(optimizer, scheduler, iteration=iteration) + self._zero_grad(model, optimizer, iteration) + grad_accum_iter = 0 + return output_batch, loss, grad_accum_iter + + def _optimizer_step( + self, + model: torch.nn.Module, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Execute the optimizer step. Override to customise (e.g. PhaseOptimizer).""" + grad_scaler.step(optimizer) + grad_scaler.update() + scheduler.step() + + def _zero_grad(self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, iteration: int) -> None: + """Zero gradients. Override to customise (e.g. PhaseOptimizer).""" + optimizer.zero_grad(set_to_none=True) + + @torch.no_grad() + def validate(self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0) -> None: + """Validate on the full validation dataset. + + Args: + model (ImaginaireModel): The PyTorch model. + dataloader_val (torch.utils.data.DataLoader): The validation data loader. + iteration (int): Current iteration number. + """ + self.callbacks.on_validation_start(model, dataloader_val, iteration=iteration) + model.eval() + # Evaluate on the full validation set. + with ema.ema_scope(model, enabled=model.config.ema.enabled): + for val_iter, data_batch in enumerate(dataloader_val): + if self.config.trainer.max_val_iter is not None and val_iter >= self.config.trainer.max_val_iter: + break + data_batch = misc.to(data_batch, device="cuda") + self.callbacks.on_validation_step_start(model, data_batch, iteration=iteration) + output_batch, loss = model.validation_step(data_batch, iteration) + self.callbacks.on_validation_step_end(model, data_batch, output_batch, loss, iteration=iteration) + self.callbacks.on_validation_end(model, iteration=iteration) diff --git a/cosmos3/_src/imaginaire/utils/__init__.py b/cosmos3/_src/imaginaire/utils/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/imaginaire/utils/callback.py b/cosmos3/_src/imaginaire/utils/callback.py new file mode 100644 index 00000000..fe12f063 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/callback.py @@ -0,0 +1,618 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import sys +import time +import warnings +from typing import TYPE_CHECKING, Any, Callable, Optional + +import omegaconf +import torch +import torch.distributed as dist +import torch.utils.data +import tqdm +import wandb + +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import distributed, log, misc, wandb_util +from cosmos3._src.imaginaire.utils.misc import get_local_tensor_if_DTensor + + +try: + from megatron.core import parallel_state +except ImportError: + parallel_state = None + + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import Config + from cosmos3._src.imaginaire.model import ImaginaireModel + from cosmos3._src.imaginaire.trainer import ImaginaireTrainer + + +class CallBackGroup: + """A class for hosting a collection of callback objects. + + It is used to execute callback functions of multiple callback objects with the same method name. + When callbackgroup.func(args) is executed, internally it loops through the objects in self._callbacks and runs + self._callbacks[0].func(args), self._callbacks[1].func(args), etc. The method name and arguments should match. + + Attributes: + _callbacks (list[Callback]): List of callback objects. + """ + + def __init__(self, config: Config, trainer: ImaginaireTrainer) -> None: + """Initializes the list of callback objects. + + Args: + config (Config): The config object for the Imaginaire codebase. + trainer (ImaginaireTrainer): The main trainer. + """ + self._callbacks = [] + callback_configs = config.trainer.callbacks + if callback_configs: + if isinstance(callback_configs, list) or isinstance(callback_configs, omegaconf.listconfig.ListConfig): + warnings.warn( + "The 'config.trainer.callbacks' parameter should be a dict instead of a list. " + "Please update your code", + DeprecationWarning, + stacklevel=2, + ) + callback_configs = {f"callback_{i}": v for i, v in enumerate(callback_configs)} + for callback_name, current_callback_cfg in callback_configs.items(): + if "_target_" not in current_callback_cfg: + log.critical( + f"Callback {callback_name} is missing the '_target_' field. \n SKip {current_callback_cfg}" + ) + continue + log.critical(f"Instantiating callback {callback_name}: {current_callback_cfg}") + _callback = instantiate(current_callback_cfg) + assert isinstance(_callback, Callback), f"{current_callback_cfg} is not a valid callback." + _callback.config = config + _callback.trainer = trainer + self._callbacks.append(_callback) + + def __getattr__(self, method_name: str) -> Callable: + """Loops through the callback objects to call the corresponding callback function. + + Args: + method_name (str): Callback method name. + """ + + def multi_callback_wrapper(*args, **kwargs) -> None: + for callback in self._callbacks: + assert hasattr(callback, method_name) + method = getattr(callback, method_name) + assert callable(method) + _ = method(*args, **kwargs) + + return multi_callback_wrapper + + +class Callback: + """The base class for all callbacks. + + All callbacks should inherit from this class and adhere to the established method names and signatures. + """ + + def __init__(self, config: Optional["Config"] = None, trainer: Optional["ImaginaireTrainer"] = None): + """Initializes a Callback object. + + Args: + config (Optional[Config]): The configuration object for the Imaginaire codebase, if available. + trainer (Optional[ImaginaireTrainer]): The main trainer handling the training loop, if available. + + Notes: + The config and trainer parameters are optional to maintain backward compatibility. + In future releases, these parameters will be removed. Upon using these parameters, a deprecation + warning will be issued. + + """ + if config is not None or trainer is not None: + warnings.warn( + "The 'config' and 'trainer' parameters are deprecated and will be removed in a future release. " + "Please update your code to create Callback instances without these parameters.", + DeprecationWarning, + stacklevel=2, + ) + del config, trainer + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + """ + Called before the training step, for each batch. This is paired with on_training_step_end() but note that + when using gradient accumulation, while on_training_step_end() is only called when the optimizer is updated, + this function is called for every batch. + Use on_training_step_batch_start and on_training_step_batch_end if you need callbacks that are called + for every batch, albeit with the same iteration number. + FIXME - should this either be deprecated, or called only when a new training step is started after having updated + the optimizer? + """ + pass + + def on_training_step_batch_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + """ + Called before the training step, for each batch, similarly to on_training_step_start(). This function is paired with + on_training_step_batch_end(), and both functions are called for every batch even when using gradient accumulation. + Note that the iteration is only updated when the optimizer is updated, and therefore it may be the same for multiple invocations. + """ + pass + + def on_before_forward(self, iteration: int = 0) -> None: + pass + + def on_after_forward(self, iteration: int = 0) -> None: + pass + + def on_before_backward( + self, model_ddp: distributed.DistributedDataParallel, loss: torch.Tensor, iteration: int = 0 + ) -> None: + pass + + def on_after_backward(self, model_ddp: distributed.DistributedDataParallel, iteration: int = 0) -> None: + pass + + def on_before_dataloading(self, iteration: int = 0) -> None: + pass + + def on_after_dataloading(self, iteration: int = 0) -> None: + pass + + def on_optimizer_init_start(self) -> None: + pass + + def on_optimizer_init_end(self) -> None: + pass + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + pass + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + pass + + def on_training_step_batch_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + """ + Called at the end of a training step for every batch even when using gradient accumulation. + This is paired with on_training_step_batch_start(). Note that the iteration is only updated when the optimizer is updated, + and therefore it may be the same for multiple batches. + """ + pass + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + """ + Called at the end of a training step, but note that when using gradient accumulation, this is only called + when the optimizer is updated, and the iteration incremented, whereas on_training_step_start is called every time. + Use on_training_step_batch_start and on_training_step_batch_end if you need callbacks that are called + for every batch. + """ + pass + + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + pass + + def on_validation_step_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + pass + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + pass + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_load_checkpoint_start(self, model: ImaginaireModel) -> None: + pass + + def on_load_checkpoint_end( + self, model: ImaginaireModel, iteration: int = 0, checkpoint_path: Optional[str] = None + ) -> None: + pass + + def on_load_checkpoint(self, model: ImaginaireModel, state_dict: dict[Any]) -> None: + """ + Called when checkpoint loading is about to start, but after on_save_checkpoint_start(). + FIXME - why do we need this callback, can't we just use on_save_checkpoint_start()? + """ + pass + + def on_save_checkpoint_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + """ + Called when checkpoint saving is about to start. + """ + pass + + def on_save_checkpoint_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + """ + Called when the synchronous part of checkpointing is finished, this function can be used + along with on_save_checkpoint_start() to measure the exposed (synchronous) checkpoint time. + Note that for asynchronous checkpoint, the checkpoint may still be ongoing, so this function + does not mean the checkpoint is finished for the asynchronous case, use on_save_checkpoint_success() + for that. + """ + pass + + def on_save_checkpoint_success(self, iteration: int = 0, elapsed_time: float = 0) -> None: + """ + Called when checkpoint saving is fully finished, and succeeded. Not called if checkpoint failed. + For synchronous checkpoint, it is called at the same time as on_save_checkpoint_end(), but for asynchronous + checkpoint, it is called after the asynchronous part has also finished. For checkpointers with out-of-process + checkpointing, this function is called as soon as the notification is received from the checkpointer process, + which may not be immediately after the checkpoint has completed but later on. Therefore, if you need to measure + the full checkpoint duration for the asynchronous part, use the elapsed_time parameter, do not measure it directly + as this would be a significant overestimate. + """ + pass + + def on_save_checkpoint(self, model: ImaginaireModel, state_dict: dict[Any]) -> None: + pass + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_app_end(self) -> None: + pass + + +class EMAModelCallback(Callback): + """The callback class for tracking EMA model weights.""" + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + # Set up the EMA model weight tracker. + if model.config.ema.enabled: + assert hasattr(model, "ema"), "EMA should be initialized from ImaginaireModel" + # EMA model must be kept in FP32 precision. + model.ema = model.ema.to(dtype=torch.float32) + else: + assert not hasattr(model, "ema"), "There should be no EMA initialized." + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # Update the EMA model with the new regular weights. + if model.config.ema.enabled: + model.ema.update_average(model, iteration) + + +class ProgressBarCallback(Callback): + """The callback class for visualizing the training/validation progress bar in the console.""" + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.train_pbar = tqdm.trange(self.config.trainer.max_iter, initial=iteration, desc="Training") + + @distributed.rank0_only + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self.train_pbar.update() + + @distributed.rank0_only + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + if self.config.trainer.max_val_iter is not None: + num_iter = self.config.trainer.max_val_iter + else: + num_iter = len(dataloader_val) + assert num_iter is not None and num_iter > 0, f"Invalid number of validation iterations: {num_iter}" + self.val_pbar = tqdm.trange(num_iter, desc="Validating", position=1, leave=False) + + @distributed.rank0_only + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self.val_pbar.update() + + @distributed.rank0_only + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.val_pbar.close() + + @distributed.rank0_only + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.trainer.checkpointer.finalize() + self.train_pbar.close() + + +class IterationLoggerCallback(Callback): + """The callback class for visualizing the training/validation progress bar in the console.""" + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + # self.train_pbar = tqdm.trange(self.config.trainer.max_iter, initial=iteration, desc="Training") + self.start_iteration_time = time.time() + self.elapsed_iteration_time = 0 + + @distributed.rank0_only + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + self.start_iteration_time = time.time() + + @distributed.rank0_only + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + + # but this is only called when the optimizer is updated, so it's only the time for the last batch. + self.elapsed_iteration_time += time.time() - self.start_iteration_time + + if iteration % self.config.trainer.logging_iter == 0: + avg_time = self.elapsed_iteration_time / self.config.trainer.logging_iter + log.info(f"Iteration: {iteration}, average iter time: {avg_time:2f}, total loss {loss.item():4f}") + + self.elapsed_iteration_time = 0 + + +class WandBCallback(Callback): + """The callback class for logging to Weights and Biases (W&B). + + By default, WandBCallback logs the following training stats to W&B every config.trainer.logging_iter: + - iteration: The current iteration number (useful for visualizing the training progress over time). + - train/loss: The computed overall loss in the training batch. + - optim/lr: The current learning rate. + - timer/*: The averaged timing results of each code block recorded by trainer.training_timer. + For validation, WandBCallback logs: + - val/loss: The computed overall loss in the validation dataset. + """ + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + wandb_util.init_wandb(self.config, model=model) + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: # Log the curent learning rate. + if iteration % self.config.trainer.logging_iter == 0 and distributed.is_rank0(): + wandb.log({"optim/lr": scheduler.get_last_lr()[0]}, step=iteration) + wandb.log({"optim/grad_scale": grad_scaler.get_scale()}, step=iteration) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: # Log the timing results (over a number of iterations) and the training loss. + if iteration % self.config.trainer.logging_iter == 0: + timer_results = self.trainer.training_timer.compute_average_results() + + # reduce loss + sample_size = torch.tensor(misc.get_data_batch_size(data_batch), device="cuda") + loss_sum = loss * sample_size + dist.all_reduce(loss_sum, op=dist.ReduceOp.SUM) + dist.all_reduce(sample_size, op=dist.ReduceOp.SUM) + avg_loss = loss_sum.item() / sample_size.item() + + if distributed.is_rank0(): + wandb.log({f"timer/{key}": value for key, value in timer_results.items()}, step=iteration) + wandb.log({"train/loss": avg_loss}, step=iteration) + wandb.log({"iteration": iteration}, step=iteration) + self.trainer.training_timer.reset() + + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + # Cache for collecting data/output batches. + self._val_cache: dict[str, Any] = dict( + data_batches=[], + output_batches=[], + loss=torch.tensor(0.0, device="cuda"), + sample_size=torch.tensor(0, device="cuda"), + ) + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: # Collect the validation batch and aggregate the overall loss. + # Collect the validation batch and aggregate the overall loss. + batch_size = misc.get_data_batch_size(data_batch) + self._val_cache["loss"] += loss * batch_size + self._val_cache["sample_size"] += batch_size + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + # Compute the average validation loss across all devices. + dist.all_reduce(self._val_cache["loss"], op=dist.ReduceOp.SUM) + dist.all_reduce(self._val_cache["sample_size"], op=dist.ReduceOp.SUM) + loss = self._val_cache["loss"].item() / self._val_cache["sample_size"] + # Log data/stats of validation set to W&B. + if distributed.is_rank0(): + log.info(f"Validation loss (iteration {iteration}): {loss:4f}") + wandb.log({"val/loss": loss}, step=iteration) + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + wandb.finish() + + +class LowPrecisionCallback(Callback): + """The callback class handling low precision training""" + + def __init__(self, config: Config, trainer: ImaginaireTrainer, update_iter: int): + self.update_iter = update_iter + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + if model.precision == torch.float32: + log.critical("Using fp32. We should disable master weights update.") + self.update_iter = sys.maxsize + else: + assert model.precision in [ + torch.bfloat16, + torch.float16, + torch.half, + ], "LowPrecisionCallback must use a low precision dtype." + self.precision_type = model.precision + + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + for k, v in data.items(): + if isinstance(v, torch.Tensor) and torch.is_floating_point(data[k]): + data[k] = v.to(dtype=self.precision_type) + + def on_validation_step_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + for k, v in data.items(): + if isinstance(v, torch.Tensor) and torch.is_floating_point(data[k]): + data[k] = v.to(dtype=self.precision_type) + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + if iteration % self.update_iter == 0: + if getattr(optimizer, "master_weights", False): + params, master_params = [], [] + for group, group_master in zip(optimizer.param_groups, optimizer.param_groups_master): + for p, p_master in zip(group["params"], group_master["params"]): + params.append(get_local_tensor_if_DTensor(p).data) + master_params.append(get_local_tensor_if_DTensor(p_master).data) + torch._foreach_copy_(params, master_params) + + +class NVTXCallback(Callback): + """The callback for creating NVTX ranges""" + + def __init__( + self, + synchronize: bool = False, + config: Optional["Config"] = None, + trainer: Optional["ImaginaireTrainer"] = None, + ): + super().__init__(config, trainer) + self.synchronize = synchronize + + def on_before_forward(self, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("forward") + + def on_after_forward(self, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_backward( + self, model_ddp: distributed.DistributedDataParallel, loss: torch.Tensor, iteration: int = 0 + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("backward") + + def on_after_backward(self, model_ddp: distributed.DistributedDataParallel, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("optimizer_step") + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_dataloading(self, iteration: int = 0) -> None: + torch.cuda.nvtx.range_push("dataloading") + + def on_after_dataloading(self, iteration: int = 0) -> None: + torch.cuda.nvtx.range_pop() + + diff --git a/cosmos3/_src/imaginaire/utils/checkpoint_db.py b/cosmos3/_src/imaginaire/utils/checkpoint_db.py new file mode 100644 index 00000000..ee648cff --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/checkpoint_db.py @@ -0,0 +1,441 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Database of released checkpoints. + +The database maps checkpoint internal URIs to public URIs and associates metadata (e.g. +experiment name). + +## Usage + +Register a checkpoint: + +```python +CheckpointConfig( + uuid="0e8177cc-0db5-4cfd-a8a4-b820c772f4fc", + s3=CheckpointDirS3( + uri="s3://bucket/path/to/checkpoint", + ), + hf=CheckpointDirHf( + repository="org/repo", + revision="revision", + subdirectory="path/to/checkpoint", + ), +).register() +``` + +Checkpoints can be referenced by UUID, S3 URI, or local path. Optionally, use `get_checkpoint_uri` to validate and normalize the URI. + +```python +# S3 URI +checkpoint_uri = get_checkpoint_uri("s3://bucket/path/to/checkpoint") +# UUID +checkpoint_uri = get_checkpoint_uri("0e8177cc-0db5-4cfd-a8a4-b820c772f4fc") +# Local path +checkpoint_uri = get_checkpoint_uri("/path/to/checkpoint", check_exists=True) +``` + +When the checkpoint is loaded, call 'download_checkpoint': + +```python +from cosmos3._src.imaginaire.flags import INTERNAL + +if not INTERNAL: + from cosmos3._src.imaginaire.utils.checkpoint_db import download_checkpoint + + checkpoint_uri = download_checkpoint(checkpoint_uri) + +load_checkpoint(checkpoint_uri) +``` +""" + +import functools +import json +import os +import shlex +import subprocess +import uuid +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Annotated, TypeAlias + +import pydantic +from typing_extensions import Self, override + +from cosmos3._src.imaginaire.flags import EXPERIMENTAL_CHECKPOINTS, INTERNAL +from cosmos3._src.imaginaire.utils import log + +HF_VERSION = "1.13.0" + + +def _is_uuid(checkpoint_uri: str) -> bool: + """Return True if the URI is a UUID.""" + try: + uuid.UUID(str(checkpoint_uri)) + return True + except ValueError: + return False + + +def _is_path(checkpoint_uri: str) -> bool: + """Return True if the URI is a local path.""" + return not ("://" in checkpoint_uri or _is_uuid(checkpoint_uri)) + + +def normalize_uri(checkpoint_uri: str) -> str: + """Normalize checkpoint URI.""" + checkpoint_uri = checkpoint_uri.rstrip("/") + if checkpoint_uri.startswith("s3://"): + checkpoint_uri = checkpoint_uri.removesuffix("/model") + return checkpoint_uri + + +def sanitize_uri(checkpoint_uri: str) -> str: + """Sanitize checkpoint URI.""" + checkpoint_uri = normalize_uri(checkpoint_uri) + if checkpoint_uri.startswith("s3://"): + checkpoint_uri = checkpoint_uri.removeprefix("s3://").split("/", 1)[1] + checkpoint_uri = f"s3://bucket/{checkpoint_uri}" + return checkpoint_uri + + +class _CheckpointUri(pydantic.BaseModel): + """Config for checkpoint file/directory.""" + + model_config = pydantic.ConfigDict(extra="forbid", frozen=True) + + metadata: dict = pydantic.Field(default_factory=dict) + """File metadata. + + Only used for debugging. + """ + + +def _validate_s3_uri(uri: str) -> str: + """Validate and normalize S3 URI.""" + if not uri.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {uri}. Must start with 's3://'") + return normalize_uri(uri) + + +S3Uri = Annotated[str, pydantic.AfterValidator(_validate_s3_uri)] + + +class _CheckpointS3(_CheckpointUri): + """Config for checkpoint on S3.""" + + uri: S3Uri + """S3 URI.""" + + +class CheckpointFileS3(_CheckpointS3): + """Config for checkpoint file on S3.""" + + +class CheckpointDirS3(_CheckpointS3): + """Config for checkpoint directory on S3.""" + + +CheckpointS3: TypeAlias = CheckpointFileS3 | CheckpointDirS3 + + +def _hf_download(cmd_args: list[str]) -> str: + """Run Hugging Face CLI download command and return the local path. + + Uses a newer Hugging Face CLI version to download checkpoint. The dependency + version is very old and not robust. + """ + is_rank0 = os.environ.get("RANK", "0") == "0" + cmd = [ + "uvx", + f"hf@{HF_VERSION}", + "download", + "--format=json", + *cmd_args, + ] + log.info(f"{shlex.join(cmd)}") + output = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=None if is_rank0 else subprocess.PIPE, + text=True, + check=True, + ) + return json.loads(output.stdout)["path"] + + +class _CheckpointHf(_CheckpointUri, ABC): + """Config for checkpoint on Hugging Face.""" + + repository: str + """Repository id (organization/repository).""" + revision: str + """Git revision id which can be a branch name, a tag, or a commit hash.""" + + _path: str | None = None + """Local path.""" + + @abstractmethod + def _download(self) -> str: ... + + def download(self) -> str: + """Download checkpoint and return the local path.""" + if self._path is None: + self._path = self._download() + return self._path + + +class CheckpointFileHf(_CheckpointHf): + """Config for checkpoint file on Hugging Face.""" + + filename: str + """File name.""" + + @override + def _download(self) -> str: + """Download checkpoint and return the local path.""" + cmd_args = [ + self.repository, + "--repo-type", + "model", + "--revision", + self.revision, + self.filename, + ] + path = _hf_download(cmd_args) + assert os.path.exists(path), path + return path + + +class CheckpointDirHf(_CheckpointHf): + """Config for checkpoint directory on Hugging Face.""" + + subdirectory: str = "" + """Repository subdirectory.""" + include: tuple[str, ...] = () + """Include patterns. + + See https://huggingface.co/docs/huggingface_hub/en/guides/download#filter-files-to-download + """ + exclude: tuple[str, ...] = () + """Exclude patterns. + + See https://huggingface.co/docs/huggingface_hub/en/guides/download#filter-files-to-download + """ + + @override + def _download(self) -> str: + """Download checkpoint and return the local path.""" + include = list(self.include) or ["*"] + exclude = list(self.exclude) + if self.subdirectory: + for patterns in [include, exclude]: + for i, pattern in enumerate(patterns): + patterns[i] = os.path.join(self.subdirectory, pattern) + + cmd_args = [ + self.repository, + "--repo-type", + "model", + "--revision", + self.revision, + ] + for pattern in include: + cmd_args.extend(["--include", pattern]) + for pattern in exclude: + cmd_args.extend(["--exclude", pattern]) + path = _hf_download(cmd_args) + if self.subdirectory: + path = os.path.join(path, self.subdirectory) + assert os.path.exists(path), path + return path + + +CheckpointHf: TypeAlias = CheckpointFileHf | CheckpointDirHf + + +class CheckpointConfig(pydantic.BaseModel): + """Config for checkpoint.""" + + model_config = pydantic.ConfigDict(extra="forbid", frozen=True) + + uuid: str + """Checkpoint UUID.""" + name: str + """Checkpoint name. + + Only used for debugging. + """ + metadata: dict = pydantic.Field(default_factory=dict) + """Checkpoint metadata. + + Only used for debugging. + """ + experiment: str | None = None + """Hydra experiment name.""" + config_file: str | None = None + """Hydra config file.""" + + s3: CheckpointS3 + """Config for checkpoint on S3.""" + hf: CheckpointHf + """Config for checkpoint on Hugging Face.""" + + @property + def full_name(self) -> str: + """Return full name for debugging.""" + return f"{self.name}({self.uuid})" + + def download(self) -> str: + """Download checkpoint and return the local path.""" + if INTERNAL: + return self.s3.uri + + log.info(f"Downloading checkpoint {self.full_name}") + return self.hf.download() + + @classmethod + def maybe_from_uri(cls, uri: str) -> Self | None: + """Return checkpoint config for URI if found, otherwise None.""" + uri = normalize_uri(uri) + return _CHECKPOINTS.get(uri, None) + + @classmethod + def from_uri(cls, uri: str) -> Self: + """Return checkpoint config for URI if found, otherwise raise an error.""" + self = cls.maybe_from_uri(uri) + if self is None: + raise ValueError( + f"Checkpoint '{uri}' not found. Set 'export COSMOS_EXPERIMENTAL_CHECKPOINTS=1' to include experimental checkpoints." + ) + return self + + def register(self): + """Register checkpoint config.""" + register_checkpoint(self) + + +_CHECKPOINTS: dict[str, CheckpointConfig] = {} +"""Mapping from checkpoint URI to checkpoint config.""" + + +def register_checkpoint(checkpoint_config: CheckpointConfig): + """Register checkpoint config. + + DEPRECATED: Use 'CheckpointConfig.register' instead. + """ + if not EXPERIMENTAL_CHECKPOINTS: + if checkpoint_config.hf.repository in ["nvidia/Cosmos-Experimental", "nvidia-cosmos-ea/Cosmos-Experimental"]: + # Don't register experimental checkpoints. An exception will be + # raised in CI if the checkpoint is used without + # EXPERIMENTAL_CHECKPOINTS. + return + for uri in [checkpoint_config.uuid, checkpoint_config.s3.uri]: + if uri in _CHECKPOINTS: + raise ValueError(f"Checkpoint '{uri}' already registered.") + _CHECKPOINTS[uri] = checkpoint_config + + +def get_checkpoint_uri(checkpoint_uri: str, *, check_exists: bool = False) -> str: + """Validate and normalize checkpoint URI.""" + checkpoint_uri = normalize_uri(checkpoint_uri) + if (checkpoint := CheckpointConfig.maybe_from_uri(checkpoint_uri)) is not None: + return checkpoint.s3.uri + if checkpoint_uri.startswith("hf://"): + return checkpoint_uri + if _is_path(checkpoint_uri): + if check_exists: + checkpoint_path = Path(checkpoint_uri).expanduser().absolute() + if not checkpoint_path.exists(): + raise ValueError(f"Checkpoint '{checkpoint_path}' does not exist.") + checkpoint_uri = str(checkpoint_path) + return checkpoint_uri + if INTERNAL: + return checkpoint_uri + raise ValueError( + f"Checkpoint '{checkpoint_uri}' not found. Set 'export COSMOS_EXPERIMENTAL_CHECKPOINTS=1' to include experimental checkpoints." + ) + + +@functools.lru_cache +def _download_hf_checkpoint(checkpoint_hf: str) -> str: + # Parse hf://org/repo/path/to/file.pth + assert checkpoint_hf.startswith("hf://"), f"Not a HuggingFace URI: {checkpoint_hf}" + hf_path = checkpoint_hf.removeprefix("hf://") + # Split into repo_id (org/repo) and filename (path/to/file.pth) + parts = hf_path.split("/") + if len(parts) < 3: + raise ValueError( + f"Invalid HuggingFace URI format: {checkpoint_hf}. Expected format: hf://org/repo/path/to/file.pth" + ) + repo_id = "/".join(parts[:2]) # org/repo + filename = "/".join(parts[2:]) # path/to/file.pth + return CheckpointFileHf( + repository=repo_id, + revision="main", + filename=filename, + ).download() + + +@functools.lru_cache +def download_checkpoint(checkpoint_uri: str, *, check_exists: bool = True) -> str: + """Download a checkpoint by URI and return the local path. + + DEPRECATED: Use 'download_checkpoint_v2' instead. + + This should only be used when the checkpoint is loaded. If you just need a + URI, use 'get_checkpoint_uri' instead. + + Downloaded checkpoints are cached, so calling this multiple times will + return the same path. + + Supports: + - Checkpoint UUID: 0e8177cc-0db5-4cfd-a8a4-b820c772f4fc + - S3 URI: s3://bucket/path/to/checkpoint + - HuggingFace URI: hf://org/repo/path/to/file.pth + - Local path: /path/to/checkpoint + """ + if INTERNAL: + return checkpoint_uri + if (checkpoint := CheckpointConfig.maybe_from_uri(checkpoint_uri)) is not None: + return checkpoint.download() + if checkpoint_uri.startswith("hf://"): + return _download_hf_checkpoint(checkpoint_uri) + if check_exists and not os.path.exists(checkpoint_uri): + raise ValueError(f"Checkpoint path {checkpoint_uri} does not exist.") + return checkpoint_uri + + +@functools.lru_cache +def download_checkpoint_v2(checkpoint_uri: str, *, check_exists: bool = True) -> str: + """Maybe download a checkpoint by URI and return the local path. + + Similar to 'download_checkpoint', but unknown S3 URIs are passed through. + """ + if INTERNAL: + return checkpoint_uri + if (checkpoint := CheckpointConfig.maybe_from_uri(sanitize_uri(checkpoint_uri))) is not None: + return checkpoint.download() + if checkpoint_uri.startswith("s3://"): + return checkpoint_uri + if checkpoint_uri.startswith("hf://"): + return _download_hf_checkpoint(checkpoint_uri) + if check_exists and not os.path.exists(checkpoint_uri): + raise ValueError(f"Checkpoint path {checkpoint_uri} does not exist.") + return checkpoint_uri + + +get_checkpoint_path = download_checkpoint +"""DEPRECATED: Use 'download_checkpoint' instead.""" diff --git a/cosmos3/_src/imaginaire/utils/checkpointer.py b/cosmos3/_src/imaginaire/utils/checkpointer.py new file mode 100644 index 00000000..34c6dae5 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/checkpointer.py @@ -0,0 +1,504 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +import threading +from typing import TYPE_CHECKING, List, NamedTuple, Tuple + +import torch + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import callback, distributed, log, misc, object_store + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import CheckpointConfig, JobConfig + +TORCH_VERSION: Tuple[int, ...] = tuple(int(x) for x in torch.__version__.split(".")[:2]) +if TORCH_VERSION >= (1, 11): + from torch.ao import quantization + from torch.ao.quantization import FakeQuantizeBase, ObserverBase +elif ( + TORCH_VERSION >= (1, 8) + and hasattr(torch.quantization, "FakeQuantizeBase") + and hasattr(torch.quantization, "ObserverBase") +): + from torch import quantization + from torch.quantization import FakeQuantizeBase, ObserverBase + + +class Checkpointer: + """The checkpointer class. Supports checkpoint saving/loading to both local disk or object store.""" + + def __init__(self, config_checkpoint: CheckpointConfig, config_job: JobConfig, callbacks: callback.CallBackGroup): + """Constructor of the checkpointer. + + Args: + config_checkpoint (CheckpointConfig): The config object for the checkpointer. + """ + # Set the callback functions. + self.callbacks = callbacks + + + + self.checkpoint_dir_local = f"{config_job.path_local}/checkpoints" + self.checkpoint_dir_object_store = f"{config_job.path}/checkpoints" + self.save_to_object_store = config_checkpoint.save_to_object_store.enabled + self.load_from_object_store = config_checkpoint.load_from_object_store.enabled + self.strict_resume = config_checkpoint.strict_resume + self.load_path = config_checkpoint.load_path or None + self.load_training_state = config_checkpoint.load_training_state + self.only_load_scheduler_state = config_checkpoint.only_load_scheduler_state + self.save_thread = None + # Create the object store client interface. + if self.save_to_object_store: + self.object_store_saver = object_store.ObjectStore(config_checkpoint.save_to_object_store) + if self.load_from_object_store: + self.object_store_loader = object_store.ObjectStore(config_checkpoint.load_from_object_store) + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + self.callbacks.on_save_checkpoint_start(model, iteration) + + checkpoint_file = f"iter_{iteration:09}.pt" + + if distributed.get_rank() == 0: + state_dict = dict( + model=model.state_dict(), + optimizer=optimizer.state_dict(), + scheduler=scheduler.state_dict(), + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + state_dict = misc.to(state_dict, device="cpu") + self.callbacks.on_save_checkpoint(model, state_dict=state_dict) + # Wait for previous saver thread to end. + if self.save_thread: + self.save_thread.join() + # Run the checkpoint saver in a separate thread. + self.save_thread = threading.Thread( + target=self._save_worker_object_store if self.save_to_object_store else self._save_worker_local, + daemon=False, + args=(state_dict, checkpoint_file, distributed.get_rank()), + ) + self.save_thread.start() + + # Note: Checkpoints are saved on a separate thread and this callback is not accurate. + # Please check logs from on_save_checkpoint_success() for better accuracy + self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration) + + @misc.timer("checkpoint saving (local)") + def _save_worker_local(self, state_dict: dict[str, torch.Tensor], checkpoint_file: str, rank: int = 0) -> None: + """Worker to save checkpoint to local disk, spawned with a child thread (runs in parallel with the training). + + Args: + state_dict (dict[str, torch.Tensor]): The state dict of the model/optimizer/scheduler. + checkpoint_file (str): The file name of the model checkpoint. + rank (int): GPU device (default: 0). + """ + checkpoint_path = os.path.join(self.checkpoint_dir_local, checkpoint_file) + os.makedirs(self.checkpoint_dir_local, exist_ok=True) + try: + torch.save(state_dict, checkpoint_path) + if rank == 0: + self._write_latest_checkpoint_file(checkpoint_file) + log.success(f"Saved checkpoint (local): {checkpoint_path}") + iteration = int(checkpoint_file.replace("iter_", "").replace(".pt", "")) + self.callbacks.on_save_checkpoint_success(iteration=iteration) + except Exception as e: # noqa: BLE001 + log.exception(f"Checkpoint failed to save (local): {e}") + + @misc.timer("checkpoint saving (object store)") + def _save_worker_object_store( + self, state_dict: dict[str, torch.Tensor], checkpoint_file: str, rank: int = 0 + ) -> None: + """Worker to upload checkpoint to object store, spawned with a child thread (in parallel with the training). + + Args: + state_dict (dict[str, torch.Tensor]): The state dict of the model/optimizer/scheduler. + checkpoint_file (str): The file name of the model checkpoint. + rank (int): GPU device (default: 0). + """ + checkpoint_path = os.path.join(self.checkpoint_dir_object_store, checkpoint_file) + try: + self.object_store_saver.save_object(state_dict, key=checkpoint_path, type="torch") + if rank == 0: + self._write_latest_checkpoint_file(checkpoint_file) + log.success(f"Saved checkpoint (object store): {checkpoint_path}") + iteration = int(checkpoint_file.replace("iter_", "").replace(".pt", "")) + self.callbacks.on_save_checkpoint_success(iteration=iteration) + except Exception as e: # noqa: BLE001 + log.exception(f"Checkpoint failed to upload (object store): {e}") + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + """Load network weights and optimizer states from a checkpoint in a single process. + + The priority of the checkpoint loading logic is: + 1. Attempt to resume training if possible by looking for latest_checkpoint.txt under the same name. + 2. If no latest checkpoint were found, it loads the model weights specified by config_checkpoint.path. + - This is typically used for inference mode. + - If config_checkpoint.load_optimizer_state is True, then also load the optimizer and scheduler states. + 3. If none of the above, randomly initialize the model parameters and train from scratch. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer | None): The model optimizer (default: None). + scheduler (torch.optim.lr_scheduler.LRScheduler | None): The optimization scheduler (default: None). + grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training). + + Returns: + iteration (int): the iteration number to start/resume from. + """ + self.callbacks.on_load_checkpoint_start(model) + + latest_checkpoint_file = self._read_latest_checkpoint_file() + if latest_checkpoint_file is not None: + # 1. Resume training from latest_checkpoint.txt under the same name. + checkpoint_dir = ( + self.checkpoint_dir_object_store if self.load_from_object_store else self.checkpoint_dir_local + ) + checkpoint_path = os.path.join(checkpoint_dir, latest_checkpoint_file) + resume = True + only_resume_scheduler = True + else: + if self.load_path: + # 2. Load the module weights specified by config_checkpoint.path. + checkpoint_path = self.load_path + resume = self.load_training_state + only_resume_scheduler = self.only_load_scheduler_state + else: + # 3. Randomly initialize the model parameters and train from scratch. + checkpoint_path = None + resume = False + only_resume_scheduler = False + # Load checkpoint. + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + if self.load_from_object_store: + log.info(f"Loading checkpoint (object store): {checkpoint_path}") + state_dict = self.object_store_loader.load_object(key=checkpoint_path, type="torch") + log.success(f"Complete loading checkpoint (object store): {checkpoint_path}") + else: + log.info(f"Loading checkpoint (local): {checkpoint_path}") + state_dict = torch.load(checkpoint_path, map_location=lambda storage, loc: storage, weights_only=False) + log.success(f"Complete loading checkpoint (local): {checkpoint_path}") + self.callbacks.on_load_checkpoint(model, state_dict=state_dict) + # Load the state dicts. + log.info("- Loading the model...") + model.load_state_dict(state_dict["model"], strict=self.strict_resume) + if resume or only_resume_scheduler: + iteration = state_dict["iteration"] + assert scheduler + log.info("- Loading the scheduler...") + scheduler.load_state_dict(state_dict["scheduler"]) + scheduler.last_epoch = iteration + else: + iteration = 0 + if resume: + assert optimizer + log.info("- Loading the optimizer...") + optimizer.load_state_dict(state_dict["optimizer"]) + log.info("- Loading the gradient scaler...") + grad_scaler.load_state_dict(state_dict["grad_scaler"]) + log.success(f"Done with loading the checkpoint (iteration {iteration}).") + else: + log.success("Done with loading the checkpoint.") + else: + # Checkpoint not found and not specified. We will train everything from scratch. + iteration = 0 + log.info("Training from scratch.") + torch.cuda.empty_cache() + + self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path) + + return iteration + + def _read_latest_checkpoint_file(self) -> str | None: + """Get the file name of the latest saved checkpoint. If it doesn't exist, return None. + + Returns: + checkpoint_file (str | None): file name of the latest saved checkpoint. + """ + checkpoint_file = None + if self.load_from_object_store: + latest_path = os.path.join(self.checkpoint_dir_object_store, "latest_checkpoint.txt") + if self.object_store_loader.object_exists(key=latest_path): + checkpoint_file = self.object_store_loader.load_object(key=latest_path, type="text").strip() + else: + latest_path = os.path.join(self.checkpoint_dir_local, "latest_checkpoint.txt") + if os.path.isfile(latest_path): + checkpoint_file = open(latest_path).read().strip() + return checkpoint_file + + def _write_latest_checkpoint_file(self, checkpoint_file: str) -> None: + """Track the file name of the latest saved checkpoint. + + Args: + checkpoint_file (str): file name of the latest saved checkpoint. + """ + content = f"{checkpoint_file}\n" + if self.save_to_object_store: + latest_path = os.path.join(self.checkpoint_dir_object_store, "latest_checkpoint.txt") + self.object_store_saver.save_object(content, key=latest_path, type="text") + else: + latest_path = os.path.join(self.checkpoint_dir_local, "latest_checkpoint.txt") + with open(latest_path, "w") as file: + file.write(content) + + def _check_checkpoint_exists(self, checkpoint_path: str) -> None: + """If the file checkpoint_path does not exist, raise an error. + + Args: + checkpoint_path (str): full path to the checkpoint. + """ + if self.load_from_object_store: + if not self.object_store_loader.object_exists(key=checkpoint_path): + raise FileNotFoundError(f"File not found (object store): {checkpoint_path}") + else: + if not os.path.exists(checkpoint_path): + raise FileNotFoundError(f"File not found (local): {checkpoint_path}") + + def finalize(self) -> None: + """Finalize the checkpointer.""" + if self.save_thread: + self.save_thread.join() + + +class _IncompatibleKeys( + NamedTuple( + "IncompatibleKeys", + [ + ("missing_keys", List[str]), + ("unexpected_keys", List[str]), + ("incorrect_shapes", List[Tuple[str, Tuple[int], Tuple[int]]]), + ], + ) +): + pass + + +class MultiRankCheckpointer(Checkpointer): + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + # checkpoint_file = f"iter_{iteration:09}.pt" + postfix, _, total_ema_num = model.get_ckpt_postfix() + checkpoint_file = f"iter_{iteration:09}{postfix}.pt" + save_ranks = list(range(total_ema_num)) + for _rank in save_ranks: + if distributed.get_rank() == _rank: + state_dict = dict( + model=model.state_dict(), + optimizer=optimizer.state_dict(), + scheduler=scheduler.state_dict(), + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + state_dict = misc.to(state_dict, device="cpu") + self.callbacks.on_save_checkpoint(model, state_dict=state_dict) + # Wait for previous saver thread to end. + if self.save_thread: + self.save_thread.join() + # Run the checkpoint saver in a separate thread. + self.save_thread = threading.Thread( + target=self._save_worker_object_store if self.save_to_object_store else self._save_worker_local, + daemon=False, + args=(state_dict, checkpoint_file, distributed.get_rank()), + ) + self.save_thread.start() + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + """Load network weights and optimizer states from a checkpoint in a single process. + + The priority of the checkpoint loading logic is: + 1. Attempt to resume training if possible by looking for latest_checkpoint.txt under the same name. + 2. If no latest checkpoint were found, it loads the model weights specified by config_checkpoint.path. + - This is typically used for inference mode. + - If config_checkpoint.load_optimizer_state is True, then also load the optimizer and scheduler states. + 3. If none of the above, randomly initialize the model parameters and train from scratch. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer | None): The model optimizer (default: None). + scheduler (torch.optim.lr_scheduler.LRScheduler | None): The optimization scheduler (default: None). + grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training). + + Returns: + iteration (int): the iteration number to start/resume from. + """ + latest_checkpoint_file = self._read_latest_checkpoint_file() + if latest_checkpoint_file is not None: + # different from base checkpointer, this support multi-EMA + postfix, _, total_ema_num = model.get_ckpt_postfix() + latest_checkpoint_file = latest_checkpoint_file.replace(".pt", f"{postfix}.pt") + # 1. Resume training from latest_checkpoint.txt under the same name. + checkpoint_dir = ( + self.checkpoint_dir_object_store if self.load_from_object_store else self.checkpoint_dir_local + ) + checkpoint_path = os.path.join(checkpoint_dir, latest_checkpoint_file) + resume = True + else: + if self.load_path: + # 2. Load the module weights specified by config_checkpoint.path. + checkpoint_path = self.load_path + # different from base checkpointer, this support multi-EMA + postfix, _, total_ema_num = model.get_ckpt_postfix() + checkpoint_path = checkpoint_path.replace(".pt", f"{postfix}.pt") + resume = self.load_training_state + else: + # 3. Randomly initialize the model parameters and train from scratch. + checkpoint_path = None + resume = False + # Load checkpoint. + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + if self.load_from_object_store: + log.info(f"Loading checkpoint (object store): {checkpoint_path}") + state_dict = self.object_store_loader.load_object(key=checkpoint_path, type="torch") + log.success(f"Complete loading checkpoint (object store): {checkpoint_path}") + else: + log.info(f"Loading checkpoint (local): {checkpoint_path}") + state_dict = torch.load(checkpoint_path, map_location=lambda storage, loc: storage) + log.success(f"Complete loading checkpoint (local): {checkpoint_path}") + self.callbacks.on_load_checkpoint(model, state_dict=state_dict) + # Load the state dicts. + log.info("- Loading the model...") + log.critical(model.load_state_dict(state_dict["model"], strict=self.strict_resume)) + if resume: + iteration = state_dict["iteration"] + assert optimizer and scheduler + log.info("- Loading the optimizer...") + optimizer.load_state_dict(state_dict["optimizer"]) + log.info("- Loading the scheduler...") + scheduler.load_state_dict(state_dict["scheduler"]) + scheduler.last_epoch = iteration + log.info("- Loading the gradient scaler...") + grad_scaler.load_state_dict(state_dict["grad_scaler"]) + log.success(f"Done with loading the checkpoint (iteration {iteration}).") + else: + iteration = 0 + log.success("Done with loading the checkpoint.") + else: + # Checkpoint not found and not specified. We will train everything from scratch. + iteration = 0 + log.info("Training from scratch.") + torch.cuda.empty_cache() + return iteration + + +# https://github.com/facebookresearch/fvcore/blob/9d683aae73fb899dd35d6cf6720e5ef567761c57/fvcore/common/checkpoint.py +def non_strict_load_model(model: torch.nn.Module, checkpoint_state_dict: dict) -> _IncompatibleKeys: + # workaround https://github.com/pytorch/pytorch/issues/24139 + model_state_dict = model.state_dict() + incorrect_shapes = [] + for k in list(checkpoint_state_dict.keys()): + if k in model_state_dict: + if "_extra_state" in k: # Key introduced by TransformerEngine for FP8 + log.warning(f"Skipping key {k} introduced by TransformerEngine for FP8 in the checkpoint.") + continue + model_param = model_state_dict[k] + # Allow mismatch for uninitialized parameters + if TORCH_VERSION >= (1, 8) and isinstance(model_param, torch.nn.parameter.UninitializedParameter): + continue + if not isinstance(model_param, torch.Tensor): + raise ValueError( + f"Find non-tensor parameter {k} in the model. type: {type(model_param)} {type(checkpoint_state_dict[k])}, please check if this key is safe to skip or not." + ) + + shape_model = tuple(model_param.shape) + shape_checkpoint = tuple(checkpoint_state_dict[k].shape) + if shape_model != shape_checkpoint: + has_observer_base_classes = ( + TORCH_VERSION >= (1, 8) + and hasattr(quantization, "ObserverBase") + and hasattr(quantization, "FakeQuantizeBase") + ) + if has_observer_base_classes: + # Handle the special case of quantization per channel observers, + # where buffer shape mismatches are expected. + def _get_module_for_key(model: torch.nn.Module, key: str) -> torch.nn.Module: + # foo.bar.param_or_buffer_name -> [foo, bar] + key_parts = key.split(".")[:-1] + cur_module = model + for key_part in key_parts: + cur_module = getattr(cur_module, key_part) + return cur_module + + cls_to_skip = ( + ObserverBase, + FakeQuantizeBase, + ) + target_module = _get_module_for_key(model, k) + if isinstance(target_module, cls_to_skip): + # Do not remove modules with expected shape mismatches + # them from the state_dict loading. They have special logic + # in _load_from_state_dict to handle the mismatches. + continue + + incorrect_shapes.append((k, shape_checkpoint, shape_model)) + checkpoint_state_dict.pop(k) + incompatible = model.load_state_dict(checkpoint_state_dict, strict=False) + # Remove keys with "_extra_state" suffix, which are non-parameter items introduced by TransformerEngine for FP8 handling + missing_keys = [k for k in incompatible.missing_keys if "_extra_state" not in k] + unexpected_keys = [k for k in incompatible.unexpected_keys if "_extra_state" not in k] + return _IncompatibleKeys( + missing_keys=missing_keys, + unexpected_keys=unexpected_keys, + incorrect_shapes=incorrect_shapes, + ) diff --git a/cosmos3/_src/imaginaire/utils/cluster_env.py b/cosmos3/_src/imaginaire/utils/cluster_env.py new file mode 100644 index 00000000..55787cf5 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/cluster_env.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from enum import Enum +from functools import lru_cache +from typing import Dict + + +class ClusterType(Enum): + LOCAL = "local" + NGC = "ngc" + SLURM = "slurm" + + +class ClusterEnvInfo(Enum): + BASIC = "basic" + DETAILED = "detailed" + ALL = "all" + + +NGC_ENV_BASIC_VARS = [ + "NGC_JOB_ID", + "NGC_ARRAY_SIZE", + "NGC_GPUS_PER_NODE", +] + +SLURM_ENV_BASIC_VARS = [ + "SLURM_JOB_USER", + "SLURM_JOB_PARTITION", + "SLURM_LOG_DIR", + "SLURM_JOBID", + "SLURM_NNODES", + "SLURM_JOB_NAME", + "SLURM_JOB_NODELIST", + "SLURMD_NODENAME", +] + + +@lru_cache() +def is_local() -> bool: + """ + Check if the code is running on a local machine. + """ + return not is_ngc() and not is_slurm() + + +@lru_cache() +def is_ngc() -> bool: + """ + Check if the code is running on NGC. + """ + return "NGC_ARRAY_SIZE" in os.environ + + +@lru_cache() +def is_slurm() -> bool: + """ + Check if the code is running on SLURM. + """ + return "SLURM_JOB_ID" in os.environ + + +def get_ngc_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves NVIDIA GPU Cloud (NGC) environment variables based on the specified detail level. + The function filters environment variables to include only those relevant to NGC, + differentiated by the detail level specified. + + Parameters: + level (ClusterInfoLevel): The level of detail for the information returned. + Defaults to ClusterInfoLevel.BASIC. + + Returns: + dict: A dictionary containing the environment variables. If the level is BASIC, + it includes only predefined key variables that are considered basic. + If the level is DETAILED, it includes all environment variables that start + with "NGC_". + + Raises: + ValueError: If an unknown level is specified, an exception is raised indicating that the + level is not recognized. + """ + if level == ClusterEnvInfo.BASIC: + return {k: os.environ[k] for k in NGC_ENV_BASIC_VARS if k in os.environ} + elif level == ClusterEnvInfo.DETAILED: + return {k: os.environ[k] for k in os.environ if k.startswith("NGC_")} + elif level == ClusterEnvInfo.ALL: + return {k: v for k, v in os.environ} + else: + raise ValueError(f"Unknown level {level}") + + +def get_slurm_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves SLURM environment variables based on the specified detail level. + This function filters the environment variables related to the SLURM job scheduler + environment based on the provided detail level of the cluster information. + + Parameters: + level (ClusterEnvInfo): The detail level of the environment variables to retrieve. + This can be BASIC, DETAILED, or ALL. Defaults to BASIC. + + Returns: + Dict[str, str]: A dictionary containing the SLURM environment variables. The contents of + the dictionary vary based on the level: + - BASIC: Returns predefined key variables important for basic SLURM variables. + - DETAILED: Includes all variables that start with "SLURM_". + - ALL: Returns all environment variables available in the current session. + + Raises: + ValueError: If an unknown level is specified, it raises an exception indicating + that the level is not recognized. + """ + if level == ClusterEnvInfo.BASIC: + return {k: os.environ[k] for k in SLURM_ENV_BASIC_VARS if k in os.environ} + elif level == ClusterEnvInfo.DETAILED: + return {k: os.environ[k] for k in os.environ if k.startswith("SLURM_")} + elif level == ClusterEnvInfo.ALL: + return {k: v for k, v in os.environ.items()} + else: + raise ValueError(f"Unknown level {level}") + + +def get_cluster_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves a combination of environment variables from the cluster, merging information from + both NVIDIA GPU Cloud (NGC) and SLURM environments based on the specified detail level. + This function provides a unified dictionary of environment settings that are crucial for + applications running in clustered computing environments. + + Parameters: + level (ClusterEnvInfo): The level of detail for the environment variables to be retrieved. + The level can be BASIC, DETAILED, or ALL. Defaults to BASIC. + - BASIC: Gathers basic environment variables from both NGC and SLURM. + - DETAILED: Includes more detailed information from both NGC and SLURM. + - ALL: Combines all available environment variables from the system + with NGC and SLURM specific ones. + + Returns: + Dict[str, str]: A dictionary containing key-value pairs of environment variables. + Initially includes the current working directory under the key 'PWD'. + """ + env_info = { + "PWD": os.getcwd(), # Always include the present working directory. + } + if level == ClusterEnvInfo.ALL: + env_info.update(os.environ) # Adds all system environment variables. + return env_info + + # For BASIC and DETAILED levels, merge environment variables from NGC and SLURM: + env_info.update(get_ngc_env(level)) + env_info.update(get_slurm_env(level)) + return env_info diff --git a/cosmos3/_src/imaginaire/utils/config_helper.py b/cosmos3/_src/imaginaire/utils/config_helper.py new file mode 100644 index 00000000..e026fca1 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/config_helper.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import importlib.util +import os +import pkgutil +import sys +from dataclasses import fields as dataclass_fields +from dataclasses import is_dataclass +from typing import Any, Dict, Optional + +import attr +import attrs +from hydra import compose, initialize +from hydra.core.config_store import ConfigStore +from hydra.core.global_hydra import GlobalHydra +from omegaconf import DictConfig, OmegaConf + +from cosmos3._src.imaginaire.config import Config +from cosmos3._src.imaginaire.utils import log + + +def is_attrs_or_dataclass(obj) -> bool: + """ + Check if the object is an instance of an attrs class or a dataclass. + + Args: + obj: The object to check. + + Returns: + bool: True if the object is an instance of an attrs class or a dataclass, False otherwise. + """ + return is_dataclass(obj) or attr.has(type(obj)) + + +def get_fields(obj): + """ + Get the fields of an attrs class or a dataclass. + + Args: + obj: The object to get fields from. Must be an instance of an attrs class or a dataclass. + + Returns: + list: A list of field names. + + Raises: + ValueError: If the object is neither an attrs class nor a dataclass. + """ + if is_dataclass(obj): + return [field.name for field in dataclass_fields(obj)] + elif attr.has(type(obj)): + return [field.name for field in attr.fields(type(obj))] + else: + raise ValueError("The object is neither an attrs class nor a dataclass.") + + +def override(config: Config, overrides: Optional[list[str]] = None, remove_defaults: bool = False) -> Config: + """ + :param config: the instance of class `Config` (usually from `make_config`) + :param overrides: list of overrides for config + :return: the composed instance of class `Config` + """ + # Store the class of the config for reconstruction after overriding. + # config_class = type(config) + + def remove_defaults_filter(f, _): + return f.name != "defaults" + + # Convert Config object to a DictConfig object + config_dict = attrs.asdict(config, filter=remove_defaults_filter if remove_defaults else None) + config_omegaconf = DictConfig(content=config_dict, flags={"allow_objects": True}) + # Enforce "--" separator between the script arguments and overriding configs. + if overrides: + if overrides[0] != "--": + raise ValueError( + f'Hydra config overrides must be separated with a "--" token. but got overrides={overrides}, and overrides[0]={overrides[0]}' + ) + overrides = overrides[1:] + # Use Hydra to handle overrides + cs = ConfigStore.instance() + cs.store(name="config", node=config_omegaconf) + if not GlobalHydra().is_initialized(): + with initialize(version_base=None): + config_omegaconf = compose(config_name="config", overrides=overrides) + OmegaConf.resolve(config_omegaconf) + else: + config_omegaconf = compose(config_name="config", overrides=overrides) + OmegaConf.resolve(config_omegaconf) + + def config_from_dict(ref_instance: Any, kwargs: Any) -> Any: + """ + Construct an instance of the same type as ref_instance using the provided dictionary or data or unstructured data + + Args: + ref_instance: The reference instance to determine the type and fields when needed + kwargs: A dictionary of keyword arguments to use for constructing the new instance or primitive data or unstructured data + + Returns: + Any: A new instance of the same type as ref_instance constructed using the provided kwargs or the primitive data or unstructured data + + Raises: + AssertionError: If the fields do not match or if extra keys are found. + Exception: If there is an error constructing the new instance. + """ + is_type = is_attrs_or_dataclass(ref_instance) + if not is_type: + return kwargs + else: + ref_fields = set(get_fields(ref_instance)) + assert isinstance(kwargs, dict) or isinstance(kwargs, DictConfig), ( + "kwargs must be a dictionary or a DictConfig" + ) + keys = set(kwargs.keys()) + + # ref_fields must equal to or include all keys + extra_keys = keys - ref_fields + assert ref_fields == keys or keys.issubset(ref_fields), ( + f"Fields mismatch: {ref_fields} != {keys}. Extra keys found: {extra_keys} \n \t when constructing {type(ref_instance)} with {keys}" + ) + + resolved_kwargs: Dict[str, Any] = {} + for f in keys: + resolved_kwargs[f] = config_from_dict(getattr(ref_instance, f), kwargs[f]) + try: + new_instance = type(ref_instance)(**resolved_kwargs) + except Exception as e: + log.error(f"Error when constructing {type(ref_instance)} with {resolved_kwargs}") + log.error(e) + raise e + return new_instance + + config = config_from_dict(config, config_omegaconf) + + return config + + +def get_config_module(config_file: str) -> str: + if not config_file.endswith(".py"): + log.error("Config file cannot be specified as module.") + log.error("Please provide the path to the Python config file (relative to the Imaginaire4 root).") + # Convert to importable module format. + config_module = config_file.replace("/", ".").replace(".py", "") + if importlib.util.find_spec(config_module) is None: + raise ValueError(f"Imaginaire4 config module ({config_module}) not found.") + return config_module + + +def import_module(full_module_name: str, reload: bool = False): + """ + Import a module by name. + + Args: + full_module_name: The fully qualified name of the module to import. + reload: If True, reload the module if it's already imported. + """ + if full_module_name in sys.modules and reload: + importlib.reload(sys.modules[full_module_name]) + else: + importlib.import_module(full_module_name) + + +def import_all_modules_from_package(package_path: str, reload: bool = False, skip_underscore: bool = True) -> None: + """ + Import all modules from the specified package path recursively. + + This function is typically used in conjunction with Hydra to ensure that all modules + within a specified package are imported, which is necessary for registering configurations. + + Example usage: + ```python + import_all_modules_from_package("projects.cosmos.diffusion.v1.config.experiment", reload=True, skip_underscore=False) + ``` + + Args: + package_path (str): The dotted path to the package from which to import all modules. + reload (bool): Flag to determine whether to reload modules if they're already imported. + skip_underscore (bool): If True, skips importing modules that start with an underscore. + """ + log.critical(f"{'Reloading' if reload else 'Importing'} all modules from package {package_path}") + package = importlib.import_module(package_path) + package_directory = package.__path__ + + def import_modules_recursively(directory: str, prefix: str) -> None: + """ + Recursively imports or reloads all modules in the given directory. + + Args: + directory (str): The file system path to the current package directory. + prefix (str): The module prefix (e.g., 'projects.cosmos.diffusion.v1.config'). + """ + for _, module_name, is_pkg in pkgutil.iter_modules([directory]): + if skip_underscore and module_name.startswith("_"): + log.debug(f"Skipping module {module_name} as it starts with an underscore") + continue + + full_module_name = f"{prefix}.{module_name}" + log.debug(f"{'Reloading' if reload else 'Importing'} module {full_module_name}") + + import_module(full_module_name, reload=reload) + + if is_pkg: + sub_package_directory = os.path.join(directory, module_name) + import_modules_recursively(sub_package_directory, full_module_name) + + for directory in package_directory: + import_modules_recursively(directory, package_path) diff --git a/cosmos3/_src/imaginaire/utils/context_managers.py b/cosmos3/_src/imaginaire/utils/context_managers.py new file mode 100644 index 00000000..63deee1d --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/context_managers.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import ExitStack, contextmanager +from typing import Generator + +import torch + +from cosmos3._src.imaginaire.utils.misc import timer + + + +@contextmanager +def disable_tf32() -> Generator[None, None, None]: + """Context manager to temporarily disable TF32 for CUDA matrix multiplications. + + This is useful for ensuring full FP32 precision in numerical computations, + particularly when debugging or comparing results between different implementations. + + Example: + with disable_tf32(): + result = torch.matmul(a, b) # Uses full FP32 precision + """ + old_allow_tf32_matmul = torch.backends.cuda.matmul.allow_tf32 + try: + torch.backends.cuda.matmul.allow_tf32 = False + with torch.backends.cudnn.flags(enabled=None, benchmark=None, deterministic=None, allow_tf32=False): + yield + finally: + torch.backends.cuda.matmul.allow_tf32 = old_allow_tf32_matmul + + +@contextmanager +def data_loader_init() -> Generator[None, None, None]: + """ + Wrap the data loader initialization with multiple context managers used for telemetry and one logger. + """ + contexts = [ + timer("init_data_loader"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] + + +@contextmanager +def model_init(set_barrier: bool = False) -> Generator[None, None, None]: + """ + Wrap the instantiation of the model with multiple context managers used for telemetry and one logger. + """ + contexts = [ + timer("init_model"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] + + +@contextmanager +def distributed_init() -> Generator[None, None, None]: + """ + Wrap the distributed initialization, used for telemetry and timers + """ + contexts = [ + timer("init_distributed"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] diff --git a/cosmos3/_src/imaginaire/utils/context_parallel.py b/cosmos3/_src/imaginaire/utils/context_parallel.py new file mode 100644 index 00000000..1eb1cfc4 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/context_parallel.py @@ -0,0 +1,255 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Optional + +try: + import megatron.core.parallel_state as parallel_state + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + +import torch +from torch import Tensor +from torch.distributed import ProcessGroup, all_gather, broadcast_object_list, get_process_group_ranks, get_world_size +from torch.distributed.utils import _verify_param_shape_across_processes + +from cosmos3._src.imaginaire.utils import distributed + + +def split_inputs_cp(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Split input tensor along the sequence dimension for checkpoint parallelism. + + This function divides the input tensor into equal parts along the specified + sequence dimension, based on the number of ranks in the checkpoint parallelism group. + It then selects the part corresponding to the current rank. + + Args: + x: Input tensor to be split. + seq_dim: The dimension along which to split the input (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A slice of the input tensor corresponding to the current rank. + + Raises: + AssertionError: If the sequence dimension is not divisible by the number of ranks. + """ + cp_ranks = get_process_group_ranks(cp_group) + cp_size = len(cp_ranks) + + assert x.shape[seq_dim] % cp_size == 0, f"{x.shape[seq_dim]} cannot divide cp_size {cp_size}" + x = x.view(*x.shape[:seq_dim], cp_size, x.shape[seq_dim] // cp_size, *x.shape[(seq_dim + 1) :]) + seq_idx = torch.tensor([cp_group.rank()], device=x.device) + x = x.index_select(seq_dim, seq_idx) + # Note that the new sequence length is the original sequence length / cp_size + x = x.view(*x.shape[:seq_dim], -1, *x.shape[(seq_dim + 2) :]) + return x + + +@torch.compiler.disable +def cat_outputs_cp(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Concatenate outputs from different ranks in the checkpoint parallelism group. + + This function gathers tensors from all ranks in the checkpoint parallelism group + and concatenates them along the specified sequence dimension. + + The function is decorated with @torch.compiler.disable because it contains distributed + operations and dynamic tensor creation based on runtime rank information that seem to be + incompatible with torch.compile's static graph compilation. + + Args: + x: Input tensor to be concatenated. + seq_dim: The dimension along which to concatenate the tensors (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A tensor that is the concatenation of tensors from all ranks in the cp_group. + + Raises: + RuntimeError: If the gather operation fails. + """ + # Get the world size (number of processes in the group) + world_size = get_world_size(cp_group) + + # Create a list to store tensors from all ranks + gathered_tensors = [torch.zeros_like(x) for _ in range(world_size)] + + # Gather tensors from all ranks + try: + all_gather(gathered_tensors, x, group=cp_group) + except RuntimeError as e: + raise RuntimeError(f"Failed to gather tensors: {e}") + + # Concatenate the gathered tensors along the specified dimension + return torch.cat(gathered_tensors, dim=seq_dim) + + +def cat_outputs_cp_with_grad(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Concatenate outputs from different ranks in the context parallelism group. + + This function gathers tensors from all ranks in the checkpoint parallelism group + and concatenates them along the specified sequence dimension. + + It retains computational graph locally for each rank by replacing the portion of the tensor with original output. + + Args: + x: Input tensor to be concatenated. + seq_dim: The dimension along which to concatenate the tensors (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A tensor that is the concatenation of tensors from all ranks in the cp_group. + + Raises: + RuntimeError: If the gather operation fails. + """ + # Get the world size (number of processes in the group) + cp_size = cp_group.size() + assert cp_size > 0, "cp_size should be greater than 0" + + # Create a list to store tensors from all ranks + gathered_tensors = [torch.zeros_like(x) for _ in range(cp_size)] + + # Gather tensors from all ranks + try: + all_gather(gathered_tensors, x, group=cp_group) + except RuntimeError as e: + raise RuntimeError(f"Failed to gather tensors: {e}") + + rank = cp_group.rank() + gathered_tensors[rank] = x + # Concatenate the gathered tensors along the specified dimension + return torch.cat(gathered_tensors, dim=seq_dim) + + +@torch.compiler.disable +def robust_broadcast(tensor: torch.Tensor, src: int, pg: ProcessGroup, is_check_shape: bool = False) -> torch.Tensor: + """ + Perform a robust broadcast operation that works regardless of tensor shapes on different ranks. + + The function is decorated with @torch.compiler.disable because it contains distributed + operations and dynamic tensor creation based on runtime rank information that seem to be + incompatible with torch.compile's static graph compilation. + + Args: + tensor (torch.Tensor): The tensor to broadcast (on src rank) or receive (on other ranks). + src (int): The source rank for the broadcast. Defaults to 0. + + Returns: + torch.Tensor: The broadcasted tensor on all ranks. + """ + # First, broadcast the shape of the tensor + if distributed.get_rank() == src: + shape = torch.tensor(tensor.shape, dtype=torch.long).cuda() + else: + shape = torch.empty(tensor.dim(), dtype=torch.long).cuda() + if is_check_shape: + _verify_param_shape_across_processes(pg, [shape]) + torch.distributed.broadcast(shape, src, group=pg) + + # Resize the tensor on non-src ranks if necessary + if distributed.get_rank() != src: + tensor = tensor.new_empty(shape.tolist()).type_as(tensor) + + # Now broadcast the tensor data + torch.distributed.broadcast(tensor, src, group=pg) + + return tensor + + +def broadcast( + item: torch.Tensor | str | None, process_group: Optional[ProcessGroup] = None +) -> torch.Tensor | str | None: + """ + Broadcast the item from the minimum rank in the specified group(s). + """ + if process_group is None: + return item + + min_rank = min(get_process_group_ranks(process_group)) + if isinstance(item, torch.Tensor): # assume the device is cuda + item = robust_broadcast(item, min_rank, process_group) + elif item is not None: + broadcastable_list = [item] + broadcast_object_list(broadcastable_list, min_rank, group=process_group) + item = broadcastable_list[0] + return item + + +def broadcast_split_tensor( + tensor: torch.Tensor, + seq_dim: int, + process_group: Optional[ProcessGroup] = None, +) -> torch.Tensor: + """ + Broadcast the tensor from the minimum rank in the specified group(s). + """ + if tensor is None: + return tensor + min_rank = min(get_process_group_ranks(process_group)) + tensor = robust_broadcast(tensor, min_rank, process_group) + return split_inputs_cp(tensor, seq_dim, process_group) + + +def find_split( + shape_tensor: torch.Size, cp_size: int, patch_values: tuple[int, int, int] = (1, 2, 2), view_factor: int = 1 +) -> torch.Size: + """ + Find the shape of input tensor for post-CP split, taking into account both temporal and spatial split, as well as patching values. + The split by width is not possible currently, due to memory stride issues, which break quality. This is checked + by an assert. + + The spatial split is achieved by flattening the input video into a single dimension before CP split is performed, + and rearranging it back into [T, H, W] format after the CP split, since the input passed to the model must still be in [T, H, W] format. + + Args: + shape_tensor (torch.Size): The shape of the Tensor that we want to split. Needs to be in [B, C, T, H, W] format. + cp_size (int): The Context Parallelism size that we want to use. + patch_values (tuple[int, int, int], optional): The patch values that are applied inside the Diffusion model. + First element of the tuple is temporal patch size. Two next elements are the spatial patch sizes. + The default value is (1, 2, 2) + view_factor (int, optional): The number of views that are present in the temporal dimension. Default value is 1. + + Returns: + The torch.Size of how the post-split tensor should look like in [T, H, W] dimensions. + + """ + if not USE_MEGATRON: + raise ImportError("No megatron.core package found, which is required for Context Parallelism usage.") + B, C, T, H, W = shape_tensor + ret = [] + assert T % view_factor == 0 + T = T // view_factor + cp_size_t = 1 + for i, size in enumerate([T, H, W]): + if i == 2 and cp_size > 1: + raise ValueError( + f"Split by width dimension is not currently supported due to quality issues. Width dimension would be split by a factor of {cp_size}. Lower the CP size to avoid splitting by width." + ) + patch_size = patch_values[i] + gcd = math.gcd(size // patch_size, cp_size) + cp_size = cp_size // gcd + if i == 0: + cp_size_t = gcd + ret.append(size // gcd) + # Saving the CP size in the temporal dimension for VideoPositionEmb embeddings calculation + parallel_state.cp_size_t = cp_size_t + return torch.Size(ret) diff --git a/cosmos3/_src/imaginaire/utils/count_params.py b/cosmos3/_src/imaginaire/utils/count_params.py new file mode 100644 index 00000000..79c03199 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/count_params.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from torch import nn + + +def count_params(model: nn.Module, verbose=False) -> int: + total_params = sum(p.numel() for p in model.parameters() if p.requires_grad) + if verbose: + print(f"{model.__class__.__name__} has {total_params * 1.0e-6:.2f} M params.") + return total_params diff --git a/cosmos3/_src/imaginaire/utils/dataloader.py b/cosmos3/_src/imaginaire/utils/dataloader.py new file mode 100644 index 00000000..b2bac676 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/dataloader.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterable, Iterator + +import torch +import torch.distributed as dist +import torch.utils.data + + +class MultiEpochsDataLoader(torch.utils.data.DataLoader): + """A dataloader that relentlessly samples from the dataset. + + This eliminates the overhead of prefetching data before each epoch. + Ref: https://github.com/rwightman/pytorch-image-models/blob/master/timm/data/loader.py + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._DataLoader__initialized = False + if self.batch_sampler is None: + self.sampler = _RepeatSampler(self.sampler) # type: ignore + else: + self.batch_sampler = _RepeatSampler(self.batch_sampler) # type: ignore + self._DataLoader__initialized = True + self.iterator = super().__iter__() + + def __len__(self) -> int: + return len(self.sampler) if self.batch_sampler is None else len(self.batch_sampler.sampler) # type: ignore + + def __iter__(self) -> Iterable: + for _ in range(len(self)): + yield next(self.iterator) + + +class _RepeatSampler: + """A sampler wrapper that repeats data sampling forever. + + Args: + sampler (Sampler): Data sampler object. + """ + + def __init__(self, sampler: torch.utils.data.Sampler): + self.sampler = sampler + + def __iter__(self) -> Iterator: + while True: + yield from iter(self.sampler) + + +class DistributedEvalSampler(torch.utils.data.Sampler): + """Distributed data sampler for evaluation. + + Ref: https://github.com/SeungjunNah/DeepDeblur-PyTorch/blob/master/src/data/sampler.py (by snah) + DistributedEvalSampler is different from DistributedSampler in that it does not pad extra samples to make it + evenly divisible. It should not be used for training, or the distributed processes could hang forever. + DistributedEvalSampler is for evaluation purpose where synchronization does not happen every epoch. + Synchronization should be done outside the dataloader loop. + """ + + def __init__(self, dataset: torch.utils.data.Dataset, shuffle: bool = False, seed: int = 0): + """Constructor of DistributedEvalSampler, + + Args: + dataset (torch.utils.data.Dataset): Dataset used for sampling. + shuffle (bool): Whether to shuffle the indices (default: False). + seed (int): Random seed for shuffling if enabled (default: 0). + """ + self.dataset = dataset + self.num_replicas = dist.get_world_size() + self.rank = dist.get_rank() + self.dataset_size = len(self.dataset) # type: ignore + indices = list(range(self.dataset_size)) + indices = indices[self.rank : self.dataset_size : self.num_replicas] + self.num_samples = len(indices) + self.shuffle = shuffle + self.seed = seed + + def __iter__(self) -> Iterator: + if self.shuffle: + # Deterministically shuffle based on epoch and seed. + gen = torch.Generator() + gen.manual_seed(self.seed) + indices = torch.randperm(self.dataset_size, generator=gen).tolist() + else: + indices = list(range(self.dataset_size)) + # Subsample. + indices = indices[self.rank : self.dataset_size : self.num_replicas] + assert len(indices) == self.num_samples + return iter(indices) + + def __len__(self) -> int: + return self.num_samples diff --git a/cosmos3/_src/imaginaire/utils/dataset_utils.py b/cosmos3/_src/imaginaire/utils/dataset_utils.py new file mode 100644 index 00000000..04e313ec --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/dataset_utils.py @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Adapted from: +https://github.com/bytedance/IRASim/blob/main/dataset/dataset_util.py +""" + +import base64 +import math +import os +from io import BytesIO + +import numpy as np +import torch +import torch.distributed as dist +import torchvision.transforms.functional as F +from PIL import Image + + +def is_dist_avail_and_initialized(): + if not dist.is_available(): + return False + if not dist.is_initialized(): + return False + return True + + +def get_rank(): + if not is_dist_avail_and_initialized(): + return 0 + return dist.get_rank() + + +def get_1d_sincos_pos_embed_from_grid(embed_dim, pos): + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size (M,) + out: (M, D) + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=np.float32) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # (D/2,) + + pos = pos.reshape(-1) # (M,) + out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product + + emb_sin = np.sin(out) # (M, D/2) + emb_cos = np.cos(out) # (M, D/2) + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D) + return emb + + +def get_2d_sincos_pos_embed_from_grid(embed_dim, grid): + assert embed_dim % 2 == 0 + + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2) + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2) + + emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D) + return emb + + +def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False): + """ + grid_size: int of the grid height and width + return: + pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token) + """ + grid_h = np.arange(grid_size, dtype=np.float32) + grid_w = np.arange(grid_size, dtype=np.float32) + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + + grid = grid.reshape([2, 1, grid_size, grid_size]) + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token: + pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0) + return pos_embed + + +def b64_2_img(data: str): + image_b64 = base64.b64decode(data) + img = Image.open(BytesIO(image_b64)).convert("RGB") + return img + + +def get_continuous_action(d_acts, c_act_max, c_act_min, n_bins): + c_act_max = c_act_max.to(d_acts.device) + c_act_min = c_act_min.to(d_acts.device) + c_acts = d_acts / (n_bins - 1) * (c_act_max - c_act_min) + c_act_min + return c_acts + + +def alpha2rotm(a): + """Alpha euler angle to rotation matrix.""" + rotm = np.array([[1, 0, 0], [0, np.cos(a), -np.sin(a)], [0, np.sin(a), np.cos(a)]]) + return rotm + + +def beta2rotm(b): + """Beta euler angle to rotation matrix.""" + rotm = np.array([[np.cos(b), 0, np.sin(b)], [0, 1, 0], [-np.sin(b), 0, np.cos(b)]]) + return rotm + + +def gamma2rotm(c): + """Gamma euler angle to rotation matrix.""" + rotm = np.array([[np.cos(c), -np.sin(c), 0], [np.sin(c), np.cos(c), 0], [0, 0, 1]]) + return rotm + + +def euler2rotm(euler_angles): + """Euler angle (ZYX) to rotation matrix.""" + alpha = euler_angles[0] + beta = euler_angles[1] + gamma = euler_angles[2] + + rotm_a = alpha2rotm(alpha) + rotm_b = beta2rotm(beta) + rotm_c = gamma2rotm(gamma) + + rotm = rotm_c @ rotm_b @ rotm_a + + return rotm + + +def isRotm(R): + # Checks if a matrix is a valid rotation matrix. + # Forked from Andy Zeng + Rt = np.transpose(R) + shouldBeIdentity = np.dot(Rt, R) + I = np.identity(3, dtype=R.dtype) + n = np.linalg.norm(I - shouldBeIdentity) + return n < 1e-6 + + +def rotm2euler(R): + # Forked from: https://learnopencv.com/rotation-matrix-to-euler-angles/ + # R = Rz * Ry * Rx + assert isRotm(R) + sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) + singular = sy < 1e-6 + + if not singular: + x = math.atan2(R[2, 1], R[2, 2]) + y = math.atan2(-R[2, 0], sy) + z = math.atan2(R[1, 0], R[0, 0]) + else: + x = math.atan2(-R[1, 2], R[1, 1]) + y = math.atan2(-R[2, 0], sy) + z = 0 + + # (-pi , pi] + while x > np.pi: + x -= 2 * np.pi + while x <= -np.pi: + x += 2 * np.pi + while y > np.pi: + y -= 2 * np.pi + while y <= -np.pi: + y += 2 * np.pi + while z > np.pi: + z -= 2 * np.pi + while z <= -np.pi: + z += 2 * np.pi + return np.array([x, y, z]) + + +def quat2rotm(quat): + """Quaternion to rotation matrix. + + Args: + quat (4, numpy array): quaternion x, y, z, w + Returns: + rotm (3x3 numpy array): rotation matrix + """ + w = quat[3] + x = quat[0] + y = quat[1] + z = quat[2] + + s = w * w + x * x + y * y + z * z + + rotm = np.array( + [ + [1 - 2 * (y * y + z * z) / s, 2 * (x * y - z * w) / s, 2 * (x * z + y * w) / s], + [2 * (x * y + z * w) / s, 1 - 2 * (x * x + z * z) / s, 2 * (y * z - x * w) / s], + [2 * (x * z - y * w) / s, 2 * (y * z + x * w) / s, 1 - 2 * (x * x + y * y) / s], + ] + ) + + return rotm + + +def rotm2quat(R): + """Convert 3x3 rotation matrix to quaternion (w, x, y, z).""" + R = np.array(R, dtype=float) + trace = np.trace(R) + + if trace > 0: + s = 0.5 / np.sqrt(trace + 1.0) + w = 0.25 / s + x = (R[2, 1] - R[1, 2]) * s + y = (R[0, 2] - R[2, 0]) * s + z = (R[1, 0] - R[0, 1]) * s + else: + if R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]: + s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) + w = (R[2, 1] - R[1, 2]) / s + x = 0.25 * s + y = (R[0, 1] + R[1, 0]) / s + z = (R[0, 2] + R[2, 0]) / s + elif R[1, 1] > R[2, 2]: + s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) + w = (R[0, 2] - R[2, 0]) / s + x = (R[0, 1] + R[1, 0]) / s + y = 0.25 * s + z = (R[1, 2] + R[2, 1]) / s + else: + s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) + w = (R[1, 0] - R[0, 1]) / s + x = (R[0, 2] + R[2, 0]) / s + y = (R[1, 2] + R[2, 1]) / s + z = 0.25 * s + + return np.array([w, x, y, z]) + + +def get_converted_fp32_paths(deepspeed_ckpt_path): + deepspeed_ckpt_path = deepspeed_ckpt_path.rstrip("/") + ckpt_dir = os.path.dirname(deepspeed_ckpt_path) + ckpt_name = os.path.basename(deepspeed_ckpt_path) + fp32_ckpt_name = f"{ckpt_name}.fp32.pt" + converted_path = os.path.join(ckpt_dir, fp32_ckpt_name) + return converted_path + + +class Resize_Preprocess: + def __init__(self, size): + """ + Initialize the preprocessing class with the target size. + Args: + size (tuple): The target height and width as a tuple (height, width). + """ + self.size = size + + def __call__(self, video_frames): + """ + Apply the transformation to each frame in the video. + Args: + video_frames (torch.Tensor): A tensor representing a batch of video frames. + Returns: + torch.Tensor: The transformed video frames. + """ + # Resize each frame in the video + resized_frames = torch.stack([F.resize(frame, self.size, antialias=True) for frame in video_frames]) + return resized_frames + + +class Preprocess: + def __init__(self, size): + self.size = size + + def __call__(self, clip): + clip = Preprocess.resize_scale(clip, self.size[0], self.size[1], interpolation_mode="bilinear") + return clip + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(size={self.size})" + + @staticmethod + def resize_scale(clip, target_height, target_width, interpolation_mode): + target_ratio = target_height / target_width + H = clip.size(-2) + W = clip.size(-1) + clip_ratio = H / W + if clip_ratio > target_ratio: + scale_ = target_width / W + else: + scale_ = target_height / H + return torch.nn.functional.interpolate(clip, scale_factor=scale_, mode=interpolation_mode, align_corners=False) + + +class ToTensorVideo: + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + """ + + def __init__(self): + pass + + def __call__(self, clip): + """ + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (T, C, H, W) + Return: + clip (torch.tensor, dtype=torch.float): Size is (T, C, H, W) + """ + return to_tensor(clip) + + def __repr__(self) -> str: + return self.__class__.__name__ + + +def to_tensor(clip): + """ + Convert tensor data type from uint8 to float, divide value by 255.0 and + permute the dimensions of clip tensor + Args: + clip (torch.tensor, dtype=torch.uint8): Size is (T, C, H, W) + Return: + clip (torch.tensor, dtype=torch.float): Size is (T, C, H, W) + """ + _is_tensor_video_clip(clip) + if not clip.dtype == torch.uint8: + raise TypeError("clip tensor should have data type uint8. Got %s" % str(clip.dtype)) + # return clip.float().permute(3, 0, 1, 2) / 255.0 + return clip.float() / 255.0 + + +def _is_tensor_video_clip(clip): + if not torch.is_tensor(clip): + raise TypeError("clip should be Tensor. Got %s" % type(clip)) + + if not clip.ndimension() == 4: + raise ValueError("clip should be 4D. Got %dD" % clip.dim()) + + return True diff --git a/cosmos3/_src/imaginaire/utils/denoise_prediction.py b/cosmos3/_src/imaginaire/utils/denoise_prediction.py new file mode 100644 index 00000000..a209db0e --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/denoise_prediction.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +import torch + + +@dataclass +class DenoisePrediction: + x0: torch.Tensor # clean data prediction + eps: Optional[torch.Tensor] = None # noise prediction + logvar: Optional[torch.Tensor] = None # log variance of noise prediction, can be used a confidence / uncertainty diff --git a/cosmos3/_src/imaginaire/utils/device.py b/cosmos3/_src/imaginaire/utils/device.py new file mode 100644 index 00000000..3f186f39 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/device.py @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc +import math +import os +from functools import wraps + +import pynvml +from loguru import logger as logging + + +def get_gpu_architecture(): + """ + Retrieves the GPU architecture of the available GPUs. + + Returns: + str: The GPU architecture, which can be "H100", "A100", or "Other". + """ + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + for i in range(device_count): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + model_name = pynvml.nvmlDeviceGetName(handle) + if isinstance(model_name, bytes): + model_name = model_name.decode("utf-8") + print(f"GPU {i}: Model: {model_name}") + + # Check for specific models like H100 or A100 + if "H100" in model_name or "H200" in model_name: + return "H100" + elif "A100" in model_name: + return "A100" + elif "L40S" in model_name: + return "L40S" + elif "B200" in model_name: + return "B200" + except pynvml.NVMLError as error: + print(f"Failed to get GPU info: {error}") + finally: + pynvml.nvmlShutdown() + + # return "Other" incase of non hopper/ampere or error + return "Other" + + +class GPUArchitectureNotSupported(Exception): + """ + Custom exception raised when the expected GPU architecture is not supported. + """ + + pass + + +def print_gpu_mem(str=None): + try: + pynvml.nvmlInit() + meminfo = pynvml.nvmlDeviceGetMemoryInfo(pynvml.nvmlDeviceGetHandleByIndex(0)) + logging.info( + f"{str}: {meminfo.used / 1024 / 1024}/{meminfo.total / 1024 / 1024}MiB used ({meminfo.free / 1024 / 1024}MiB free)" + ) + except pynvml.NVMLError as error: + print(f"Failed to get GPU memory info: {error}") + + +def force_gc(): + print_gpu_mem() + print("gc()") + gc.collect() + print_gpu_mem() + print("empty cuda cache") + # print(torch.cuda.memory_summary()) + print_gpu_mem() + + +def gpu0_has_80gb_or_less(): + try: + pynvml.nvmlInit() + meminfo = pynvml.nvmlDeviceGetMemoryInfo(pynvml.nvmlDeviceGetHandleByIndex(0)) + return meminfo.total / 1024 / 1024 / 1024 <= 80 + except pynvml.NVMLError as error: + print(f"Failed to get GPU memory info: {error}") + + +class Device: + + + _nvml_affinity_elements = math.ceil(os.cpu_count() / 64) # type: ignore + + def __init__(self, device_idx: int): + + super().__init__() + self.handle = pynvml.nvmlDeviceGetHandleByIndex(device_idx) + + def get_name(self) -> str: + + return pynvml.nvmlDeviceGetName(self.handle) + + def get_cpu_affinity(self) -> list[int]: + + affinity_string = "" + for j in pynvml.nvmlDeviceGetCpuAffinity(self.handle, Device._nvml_affinity_elements): + # assume nvml returns list of 64 bit ints + affinity_string = "{:064b}".format(j) + affinity_string + affinity_list = [int(x) for x in affinity_string] + affinity_list.reverse() # so core 0 is in 0th element of list + return [i for i, e in enumerate(affinity_list) if e != 0] + + +def with_torch_device(device): + """ + Decorator factory that wraps a function to execute within a specific torch device context. + + This decorator ensures that all tensor allocations and operations within the decorated + function use the specified device by default. + + Args: + device: The torch device to use (e.g., 'cuda', 'cuda:0', 'cpu', or torch.device object). + + Returns: + A decorator function that wraps the target function with the specified device context. + + Example: + @with_torch_device('cuda:0') + def create_tensors(): + x = torch.randn(10, 10) # Will be created on cuda:0 + return x + """ + import torch + + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + with torch.device(device): + return fn(*args, **kwargs) + + return wrapper + + return decorator diff --git a/cosmos3/_src/imaginaire/utils/disabled_train.py b/cosmos3/_src/imaginaire/utils/disabled_train.py new file mode 100644 index 00000000..af961d31 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/disabled_train.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + + +def disabled_train(self: Any, mode: bool = True) -> Any: + """Overwrite model.train with this function to make sure train/eval mode + does not change anymore.""" + return self diff --git a/cosmos3/_src/imaginaire/utils/distributed.py b/cosmos3/_src/imaginaire/utils/distributed.py new file mode 100644 index 00000000..6a54442a --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/distributed.py @@ -0,0 +1,491 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +import collections.abc +import ctypes +import functools +import os +from contextlib import contextmanager +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Callable, Container, Optional + +import pynvml +import torch +import torch.distributed as dist +from torch.distributed import get_process_group_ranks + +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.utils.device import Device + +if dist.is_available(): + from torch.distributed.distributed_c10d import _get_default_group + from torch.distributed.utils import _sync_module_states, _verify_param_shape_across_processes + +from cosmos3._src.imaginaire.utils import log + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import DDPConfig + + +def init() -> int | None: + """Initialize distributed training.""" + if dist.is_initialized(): + return torch.cuda.current_device() + + # Set GPU affinity. + pynvml.nvmlInit() + local_rank = int(os.getenv("LOCAL_RANK", 0)) + try: + device = Device(local_rank) + os.sched_setaffinity(0, device.get_cpu_affinity()) + except pynvml.NVMLError as e: + log.warning(f"Failed to set device affinity: {e}") + # Set up NCCL communication. + os.environ["TORCH_NCCL_BLOCKING_WAIT"] = "0" + os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "1" + if dist.is_available(): + torch.cuda.set_device(local_rank) + # Get the timeout value from environment variable + timeout_seconds = os.getenv("TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC", 1800) + # Convert the timeout to an integer (if it isn't already) and then to a timedelta + timeout_timedelta = timedelta(seconds=int(timeout_seconds)) + dist.init_process_group(backend="nccl", init_method="env://", timeout=timeout_timedelta) + log.critical( + f"Initialized distributed training with local rank {local_rank} with timeout {timeout_seconds}", + rank0_only=False, + ) + # Increase the L2 fetch granularity for faster speed. + # For oss, we need to search for the library in site-packages. + if INTERNAL: + _libcudart = ctypes.CDLL("libcudart.so") + # Set device limit on the current device. + p_value = ctypes.cast((ctypes.c_int * 1)(), ctypes.POINTER(ctypes.c_int)) + _libcudart.cudaDeviceSetLimit(ctypes.c_int(0x05), ctypes.c_int(128)) + _libcudart.cudaDeviceGetLimit(p_value, ctypes.c_int(0x05)) + log.info(f"Training with {get_world_size()} GPUs.") + + +def get_rank(group: Optional[dist.ProcessGroup] = None) -> int: + """Get the rank (GPU device) of the worker. + + Returns: + rank (int): The rank of the worker. + """ + rank = 0 + if dist.is_available() and dist.is_initialized(): + rank = dist.get_rank(group) + return rank + + +def get_world_size(group: Optional[dist.ProcessGroup] = None) -> int: + """Get world size. How many GPUs are available in this job. + + Returns: + world_size (int): The total number of GPUs available in this job. + """ + world_size = 1 + if dist.is_available() and dist.is_initialized(): + world_size = dist.get_world_size(group) + return world_size + + +def is_rank0() -> bool: + """Check if current process is the master GPU. + + Returns: + (bool): True if this function is called from the master GPU, else False. + """ + return get_rank() == 0 + + +def is_local_rank0() -> bool: + """Check if current process is the local master GPU in the current node. + + Returns: + (bool): True if this function is called from the local master GPU, else False. + """ + return torch.cuda.current_device() == 0 + + +def rank0_only(func: Callable) -> Callable: + """Apply this function only to the master GPU. + + Example usage: + @rank0_only + def func(x): + return x + 3 + + Args: + func (Callable): a function. + + Returns: + (Callable): A function wrapper executing the function only on the master GPU. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + if is_rank0(): + return func(*args, **kwargs) + else: + return None + + return wrapper + + +def barrier() -> None: + """Barrier for all GPUs.""" + if dist.is_available() and dist.is_initialized(): + dist.barrier() + + +def rank0_first(func: Callable) -> Callable: + """run the function on rank 0 first, then on other ranks.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + if is_rank0(): + result = func(*args, **kwargs) + barrier() + if not is_rank0(): + result = func(*args, **kwargs) + return result + + return wrapper + + +def parallel_model_wrapper(config_ddp: DDPConfig, model: torch.nn.Module) -> torch.nn.Module | DistributedDataParallel: + """Wraps the model to enable data parallalism for training across multiple GPU devices. + + Args: + config_ddp (DDPConfig): The data parallel config. + model (torch.nn.Module): The PyTorch module. + + Returns: + model (torch.nn.Module | DistributedDataParallel): The data parallel model wrapper + if distributed environment is available, otherwise return the original model. + """ + if dist.is_available() and dist.is_initialized(): + local_rank = int(os.getenv("LOCAL_RANK", 0)) + try: + from megatron.core import parallel_state + + ddp_group = parallel_state.get_data_parallel_group(with_context_parallel=True) + except Exception as e: + log.info(e) + log.info("parallel_state not initialized, treating all GPUs equally for DDP") + ddp_group = None + + model = DistributedDataParallel( + model, + device_ids=[local_rank], + output_device=local_rank, + find_unused_parameters=config_ddp.find_unused_parameters, + static_graph=config_ddp.static_graph, + broadcast_buffers=config_ddp.broadcast_buffers, + process_group=ddp_group, + ) + return model + + +class DistributedDataParallel(torch.nn.parallel.DistributedDataParallel): + """This extends torch.nn.parallel.DistributedDataParallel with .training_step(). + + This borrows the concept of `forward-redirection` from Pytorch lightning. It wraps an ImaginaireModel such that + model.training_step() would be executed when calling self.training_step(), while preserving the behavior of calling + model() for Pytorch modules. Internally, this is a double rerouting mechanism (training_step -> forward -> + training_step), allowing us to preserve the function names and signatures. + """ + + def __init__(self, model: torch.nn.Module, *args, **kwargs): + super().__init__(model, *args, **kwargs) + self.show_sync_grad_static_graph_warning = True + + def training_step(self, *args, **kwargs) -> Any: + # Cache the original model.forward() method. + original_forward = self.module.forward + + def wrapped_training_step(*_args, **_kwargs): # noqa: ANN202 + # Unpatch immediately before calling training_step() because itself may want to call the real forward. + self.module.forward = original_forward + # The actual .training_step(). + return self.module.training_step(*_args, **_kwargs) + + # Patch the original_module's forward so we can redirect the arguments back to the real method. + self.module.forward = wrapped_training_step + # Call self, which implicitly calls self.forward() --> model.forward(), which is now model.training_step(). + # Without calling self.forward() or model.forward() explciitly, implicit hooks are also executed. + return self(*args, **kwargs) + + +@contextmanager +def ddp_sync_grad(model, enabled): + r""" + Context manager to enable/disable gradient synchronizations across DDP processes for DDP model. + Modified from: + https://pytorch.org/docs/stable/_modules/torch/nn/parallel/distributed.html#DistributedDataParallel.no_sync + Note that this is incompatible with static_graph=True and will be an no-op if static_graph=True. + + Within this context, gradients will be accumulated on module + variables, which will later be synchronized in the first + forward-backward pass exiting the context. + + .. warning:: + The forward pass should be included inside the context manager, or + else gradients will still be synchronized. + """ + assert isinstance(model, torch.nn.Module) + if isinstance(model, DistributedDataParallel): + old_require_backward_grad_sync = model.require_backward_grad_sync + if model.static_graph and model.require_backward_grad_sync != enabled: + if model.show_sync_grad_static_graph_warning: + log.warning("DDP static_graph=True is incompatible with sync_grad(). Performance will be reduced.") + model.show_sync_grad_static_graph_warning = False + else: + model.require_backward_grad_sync = enabled + try: + yield + finally: + if isinstance(model, DistributedDataParallel): + model.require_backward_grad_sync = old_require_backward_grad_sync + + +def collate_batches(data_batches: list[dict[str, torch.Tensor]]) -> torch.Tensor | dict[str, torch.Tensor]: + """Aggregate the list of data batches from all devices and process the results. + + This is used for gathering validation data batches with cosmos3._src.imaginaire.utils.dataloader.DistributedEvalSampler. + It will return the data/output of the entire validation set in its original index order. The sizes of data_batches + in different ranks may differ by 1 (if dataset size is not evenly divisible), in which case a dummy sample will be + created before calling dis.all_gather(). + + Args: + data_batches (list[dict[str, torch.Tensor]]): List of tensors or (hierarchical) dictionary where + leaf entries are tensors. + + Returns: + data_gather (torch.Tensor | dict[str, torch.Tensor]): tensors or (hierarchical) dictionary where + leaf entries are concatenated tensors. + """ + if isinstance(data_batches[0], torch.Tensor): + # Concatenate the local data batches. + data_concat = torch.cat(data_batches, dim=0) # type: ignore + # Get the largest number of local samples from all ranks to determine whether to dummy-pad on this rank. + max_num_local_samples = torch.tensor(len(data_concat), device="cuda") + dist.all_reduce(max_num_local_samples, op=dist.ReduceOp.MAX) + if len(data_concat) < max_num_local_samples: + assert len(data_concat) + 1 == max_num_local_samples + dummy = torch.empty_like(data_concat[:1]) + data_concat = torch.cat([data_concat, dummy], dim=0) + dummy_count = torch.tensor(1, device="cuda") + else: + dummy_count = torch.tensor(0, device="cuda") + # Get all concatenated batches from all ranks and concatenate again. + dist.all_reduce(dummy_count, op=dist.ReduceOp.SUM) + data_concat = all_gather_tensor(data_concat.contiguous()) + data_collate = torch.stack(data_concat, dim=1).flatten(start_dim=0, end_dim=1) + # Remove the dummy samples. + if dummy_count > 0: + data_collate = data_collate[:-dummy_count] + elif isinstance(data_batches[0], collections.abc.Mapping): + data_collate = dict() + for key in data_batches[0].keys(): + data_collate[key] = collate_batches([data[key] for data in data_batches]) # type: ignore + else: + raise TypeError + return data_collate + + +@torch.no_grad() +def all_gather_tensor(tensor: torch.Tensor) -> list[torch.Tensor]: + """Gather the corresponding tensor from all GPU devices to a list. + + Args: + tensor (torch.Tensor): Pytorch tensor. + + Returns: + tensor_list (list[torch.Tensor]): A list of Pytorch tensors gathered from all GPU devices. + """ + tensor_list = [torch.zeros_like(tensor) for _ in range(get_world_size())] + dist.all_gather(tensor_list, tensor) + return tensor_list + + +def gather_object(payload: Any) -> list[Any] | None: + """Gather the corresponding object from all GPU devices to a rank 0 hosted list. + + Args: + payload: Any pickle-able object. + + Returns: + payload_list (list[Any]) | None: + Rank 0: A list of Pytorch tensors gathered from all RANK process. + Rest : None + """ + rank, world_size = get_rank(), get_world_size() + payload_gathered = [None] * world_size if rank == 0 else None + dist.gather_object(payload, object_gather_list=payload_gathered, dst=0) + return payload_gathered + + +def broadcast(tensor, src, group=None, async_op=False): + world_size = get_world_size() + if world_size < 2: + return tensor + dist.broadcast(tensor, src=src, group=group, async_op=async_op) + + +def dist_reduce_tensor(tensor, rank=0, reduce="mean"): + r"""Reduce to rank 0""" + world_size = get_world_size() + if world_size < 2: + return tensor + with torch.no_grad(): + dist.reduce(tensor, dst=rank) + if get_rank() == rank: + if reduce == "mean": + tensor /= world_size + elif reduce == "sum": + pass + else: + raise NotImplementedError + return tensor + + +def sync_model_states( + model: torch.nn.Module, + process_group: Optional[dist.ProcessGroup] = None, + src: int = 0, + params_and_buffers_to_ignore: Optional[Container[str]] = None, + broadcast_buffers: bool = True, +): + """ + Modify based on DDP source code + Synchronizes the parameters and buffers of a model across different processes in a distributed setting. + + This function ensures that all processes in the specified process group have the same initial parameters and + buffers from the source rank, typically rank 0. It is useful when different processes start with different model + states and a synchronization is required to ensure consistency across all ranks. + + Args: + model (nn.Module): The model whose parameters and buffers are to be synchronized. + process_group (dist.ProcessGroup, optional): The process group for communication. If None, + the default group is used. Defaults to None. + src (int, optional): The source rank from which parameters and buffers will be broadcasted. + Defaults to 0. + params_and_buffers_to_ignore (Optional[Container[str]], optional): A container of parameter and buffer + names to exclude from synchronization. Defaults to None, which means all parameters and buffers are + included. + broadcast_buffers (bool, optional): Whether to broadcast buffers or not. Defaults to True. + + Side Effects: + This function modifies the state of the model in-place to synchronize it with the source rank's model state. + + Raises: + RuntimeError: If the shapes of parameters across processes do not match, a runtime error will be raised. + + Examples: + >>> # downloading duplicated model weights from s3 in each rank and save network bandwidth + >>> # useful and save our time when model weights are huge + >>> if dist.get_rank == 0: + >>> model.load_state_dict(network_bound_weights_download_fn(s3_weights_path)) + >>> dist.barrir() + >>> sync_model_states(model) # sync rank0 weights to other ranks + """ + if not dist.is_available() or not dist.is_initialized(): + return + if process_group is None: + process_group = _get_default_group() + if not params_and_buffers_to_ignore: + params_and_buffers_to_ignore = set() + + log.info( + f"Synchronizing model states from rank {src} to all ranks in process group {get_process_group_ranks(process_group)}." + ) + + # Build tuple of (module, parameter) for all parameters that require grads. + modules_and_parameters = [ + (module, parameter) + for module_name, module in model.named_modules() + for parameter in [ + param + # Note that we access module.named_parameters instead of + # parameters(module). parameters(module) is only needed in the + # single-process multi device case, where it accesses replicated + # parameters through _former_parameters. + for param_name, param in module.named_parameters(recurse=False) + if f"{module_name}.{param_name}" not in params_and_buffers_to_ignore + # if param.requires_grad + # and f"{module_name}.{param_name}" not in params_and_buffers_to_ignore + ] + ] + + # Deduplicate any parameters that might be shared across child modules. + memo = set() + modules_and_parameters = [ + # "p not in memo" is the deduplication check. + # "not memo.add(p)" is always True, and it's only there to cause "add(p)" if needed. + (m, p) + for m, p in modules_and_parameters + if p not in memo and not memo.add(p) # type: ignore[func-returns-value] + ] + + # Build list of parameters. + parameters = [parameter for _, parameter in modules_and_parameters] + if len(parameters) == 0: + return + + _verify_param_shape_across_processes(process_group, parameters) + + _sync_module_states( + module=model, + process_group=process_group, + broadcast_bucket_size=int(250 * 1024 * 1024), + src=src, + params_and_buffers_to_ignore=params_and_buffers_to_ignore, + broadcast_buffers=broadcast_buffers, + ) + + +def all_gather_object(payload: Any) -> list[Any]: + """Gather the corresponding object from all GPU devices to all ranks.""" + world_size = get_world_size() + payload_gathered = [None] * world_size + dist.all_gather_object(payload_gathered, payload) + return payload_gathered # type: ignore[return-value] + + +def broadcast_object(object, *args, **kwargs): + """Broadcast a object to all GPU.""" + if not dist.is_available() or not dist.is_initialized(): + return object + object_list = [object] + dist.broadcast_object_list(object_list, *args, **kwargs) + return object_list[0] + + +def broadcast_object_list(object_list, *args, **kwargs): + """Broadcast a object list to all GPU. (the list is inplace edited)""" + if not dist.is_available() or not dist.is_initialized(): + return None + else: + dist.broadcast_object_list(object_list, *args, **kwargs) + + +def destroy_process_group(): + if not dist.is_available() or not dist.is_initialized(): + return + dist.destroy_process_group() diff --git a/cosmos3/_src/imaginaire/utils/easy_io/__init__.py b/cosmos3/_src/imaginaire/utils/easy_io/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py new file mode 100644 index 00000000..c6c15692 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend +from cosmos3._src.imaginaire.utils.easy_io.backends.http_backend import HTTPBackend +from cosmos3._src.imaginaire.utils.easy_io.backends.local_backend import LocalBackend +from cosmos3._src.imaginaire.utils.easy_io.backends.registry_utils import backends, prefix_to_backends, register_backend + +__all__ = [ + "BaseStorageBackend", + "LocalBackend", + "HTTPBackend", + "register_backend", + "backends", + "prefix_to_backends", +] + +if TRAINING: + from cosmos3._src.imaginaire.utils.easy_io.backends.boto3_backend import Boto3Backend + from cosmos3._src.imaginaire.utils.easy_io.backends.msc_backend import MSCBackend + + __all__ += [ + "Boto3Backend", + "MSCBackend", + ] diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py new file mode 100644 index 00000000..3858402c --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import json +from collections.abc import Generator +from typing import IO, Any, Optional, Union + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.env_parsers.cred_env_parser import CRED_ENVS, CRED_ENVS_DICT + +DEPLOYMENT_ENVS = ["prod", "dev", "stg"] + + +# context manger to open a file or read from env variable +@contextlib.contextmanager +def open_auth(s3_credential_path: Optional[Any], mode: str) -> Generator[Union[None, dict[str, Any], IO]]: + if not s3_credential_path: + log.info(f"No credential file provided {s3_credential_path}.") + yield None + return + + name = s3_credential_path.split("/")[-1].split(".")[0] + if not name: + raise ValueError(f"Could not parse into env var: {s3_credential_path}") + cred_env_name = f"PROD_{name.upper()}" + + if CRED_ENVS.APP_ENV in DEPLOYMENT_ENVS and cred_env_name in CRED_ENVS_DICT: + object_storage_config = get_creds_from_env(cred_env_name) + log.info(f"using ENV vars for {cred_env_name}") + yield object_storage_config + else: + log.info(f"using credential file: {s3_credential_path}") + with open(s3_credential_path, mode) as f: + yield f + + +def get_creds_from_env(cred_env_name: str) -> dict[str, Any]: + try: + object_storage_config = CRED_ENVS_DICT[cred_env_name] + except KeyError: + raise ValueError(f"Could not find {cred_env_name} in CRED_ENVS") + empty_args = {key.upper() for key in object_storage_config if object_storage_config[key] == ""} + if empty_args: + raise ValueError(f"Some required environment variable(s) were not provided for {cred_env_name}", empty_args) + return object_storage_config + + +def json_load_auth(f: Union[None, dict[str, Any], IO]) -> dict[str, Any]: + # None. + if f is None: + return {} + # dict[str, Any]. + elif isinstance(f, dict): + return f + # IO. + else: + return json.load(f) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py new file mode 100644 index 00000000..1c3853b4 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import os.path as osp +from abc import ABCMeta, abstractmethod +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union + + +def mkdir_or_exist(dir_name, mode=0o777): + if dir_name == "": + return + dir_name = osp.expanduser(dir_name) + os.makedirs(dir_name, mode=mode, exist_ok=True) + + +def has_method(obj, method): + return hasattr(obj, method) and callable(getattr(obj, method)) + + +class BaseStorageBackend(metaclass=ABCMeta): + """Abstract class of storage backends.""" + + # a flag to indicate whether the backend can create a symlink for a file + # This attribute will be deprecated in future. + _allow_symlink: bool = False + + @property + def allow_symlink(self) -> bool: + return self._allow_symlink + + @property + def name(self) -> str: + return self.__class__.__name__ + + @abstractmethod + def size(self, filepath: Union[str, Path]) -> int: + pass + + @abstractmethod + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + pass + + @abstractmethod + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + pass + + @abstractmethod + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + pass + + @abstractmethod + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + pass + + @abstractmethod + def exists(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def isdir(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def isfile(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + pass + + @abstractmethod + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + pass + + @abstractmethod + def copyfile(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copytree(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copyfile_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copytree_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + pass + + @abstractmethod + def copytree_to_local(self, src: Union[str, Path], dst: Union[str, Path]) -> Union[str, Path]: + pass + + @abstractmethod + def remove(self, filepath: Union[str, Path]) -> None: + pass + + @abstractmethod + def rmtree(self, dir_path: Union[str, Path]) -> None: + pass + + @abstractmethod + def copy_if_symlink_fails(self, src: Union[str, Path], dst: Union[str, Path]) -> bool: + pass + + @abstractmethod + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + pass + + @abstractmethod + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + pass diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py new file mode 100644 index 00000000..b0b9957e --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py @@ -0,0 +1,862 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import re +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from shutil import SameFileError +from typing import Optional, Union + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend, has_method, mkdir_or_exist +from cosmos3._src.imaginaire.utils.easy_io.backends.boto3_client import Boto3Client + + +class Boto3Backend(BaseStorageBackend): + """boto3 storage backend (for internal usage). + + **Deprecated**. Use the MSC backend instead. + + Boto3Backend supports reading and writing data to multiple clusters. + If the file path contains the cluster name, Boto3Backend will read data + from specified cluster or write data to it. Otherwise, Boto3Backend will + access the default cluster. + + Args: + path_mapping (dict, optional): Path mapping dict from local path to + Boto3 path. When ``path_mapping={'src': 'dst'}``, ``src`` in + ``filepath`` will be replaced by ``dst``. Defaults to None. + s3_credential_path (str, optional): Config path of Boto3 client. Default: None. + `New in version 0.3.3`. + + Examples: + >>> backend = Boto3Backend() + >>> filepath1 = 's3://path/of/file' + >>> filepath2 = 'cluster-name:s3://path/of/file' + >>> backend.get(filepath1) # get data from default cluster + >>> client.get(filepath2) # get data from 'cluster-name' cluster + """ + + def __init__( + self, + s3_credential_path: str = "", + path_mapping: Optional[dict] = None, + ): + self._client = Boto3Client(s3_credential_path=s3_credential_path) + assert isinstance(path_mapping, dict) or path_mapping is None + self.path_mapping = path_mapping + if path_mapping: + for k, v in path_mapping.items(): + log.critical(f"Path mapping: {k} -> {v}", rank0_only=False) + + def _map_path(self, filepath: Union[str, Path]) -> str: + """Map ``filepath`` to a string path whose prefix will be replaced by + :attr:`self.path_mapping`. + + Args: + filepath (str or Path): Path to be mapped. + """ + filepath = str(filepath) + if self.path_mapping is not None: + for k, v in self.path_mapping.items(): + filepath = filepath.replace(k, v, 1) + return filepath + + def _format_path(self, filepath: str) -> str: + """Convert a ``filepath`` to standard format of s3 oss. + + If the ``filepath`` is concatenated by ``os.path.join``, in a Windows + environment, the ``filepath`` will be the format of + 's3://bucket_name\\image.jpg'. By invoking :meth:`_format_path`, the + above ``filepath`` will be converted to 's3://bucket_name/image.jpg'. + + Args: + filepath (str): Path to be formatted. + """ + return re.sub(r"\\+", "/", filepath) + + def _replace_prefix(self, filepath: Union[str, Path]) -> str: + filepath = str(filepath) + return filepath + # return filepath.replace('s3://', 's3://') + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.size(filepath) + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Return bytes read from filepath. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.get(filepath) + b'hello world' + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + value = self._client.get(filepath=filepath, offset=offset, size=size) + return value + + def get_text( + self, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.get_text(filepath) + 'hello world' + """ + return str(self.get(filepath), encoding=encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath``. + + Args: + obj (bytes): Data to be saved. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.put(b'hello world', filepath) + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.put(obj, filepath) + + def fast_put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path], num_processes: int = 32) -> None: + """Write bytes to a given ``filepath`` with multiple processes and async""" + assert num_processes > 1 + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.fast_put(obj, filepath, num_processes=num_processes) + + def put_text( + self, + obj: str, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> None: + """Write text to a given ``filepath``. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to encode the ``obj``. + Defaults to 'utf-8'. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.put_text('hello world', filepath) + """ + self.put(bytes(obj, encoding=encoding), filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.exists(filepath) + True + """ + if not (has_method(self._client, "contains") and has_method(self._client, "isdir")): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `contains` and `isdir` methods, please use a higher" + "version or dev branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.contains(filepath) or self._client.isdir(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/dir' + >>> backend.isdir(filepath) + True + """ + if not has_method(self._client, "isdir"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `isdir` method, please use a higher version or dev" + " branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.isfile(filepath) + True + """ + if not has_method(self._client, "contains"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `contains` method, please use a higher version or " + "dev branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.contains(filepath) + + def join_path( + self, + filepath: Union[str, Path], + *filepaths: Union[str, Path], + ) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result after concatenation. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.join_path(filepath, 'another/path') + 's3://path/of/file/another/path' + >>> backend.join_path(filepath, '/another/path') + 's3://path/of/file/another/path' + """ + filepath = self._format_path(self._map_path(filepath)) + if filepath.endswith("/"): + filepath = filepath[:-1] + formatted_paths = [filepath] + for path in filepaths: + formatted_path = self._format_path(self._map_path(path)) + formatted_paths.append(formatted_path.lstrip("/")) + + return "/".join(formatted_paths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str or Path): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = Boto3Backend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> filepath = 's3://path/of/file' + >>> with backend.get_local_path(filepath) as path: + ... # do something here + """ + assert self.isfile(filepath) + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 's3://path/of/file' + >>> dst = 's3://path/of/file1' + >>> backend.copyfile(src, dst) + 's3://path/of/file1' + + >>> # dst is a directory + >>> dst = 's3://path/of/dir' + >>> backend.copyfile(src, dst) + 's3://path/of/dir/file' + """ + src = self._format_path(self._map_path(src)) + dst = self._format_path(self._map_path(dst)) + if self.isdir(dst): + dst = self.join_path(dst, src.split("/")[-1]) + + if src == dst: + raise SameFileError("src and dst should not be same") + + self.put(self.get(src), dst) + return dst + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + src and dst should have the same prefix. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/dir' + >>> dst = 's3://path/of/dir1' + >>> backend.copytree(src, dst) + 's3://path/of/dir1' + """ + src = self._format_path(self._map_path(src)) + dst = self._format_path(self._map_path(dst)) + + if self.exists(dst): + raise FileExistsError("dst should not exist") + + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + src_path = self.join_path(src, path) + dst_path = self.join_path(dst, path) + self.put(self.get(src_path), dst_path) + + return dst + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Upload a local file src to dst and return the destination file. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 'path/of/your/file' + >>> dst = 's3://path/of/file1' + >>> backend.copyfile_from_local(src, dst) + 's3://path/of/file1' + + >>> # dst is a directory + >>> dst = 's3://path/of/dir' + >>> backend.copyfile_from_local(src, dst) + 's3://path/of/dir/file' + """ + dst = self._format_path(self._map_path(dst)) + if self.isdir(dst): + dst = self.join_path(dst, os.path.basename(src)) + + with open(src, "rb") as f: + self.put(f.read(), dst) + + return dst + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = Boto3Backend() + >>> src = 'path/of/your/dir' + >>> dst = 's3://path/of/dir1' + >>> backend.copytree_from_local(src, dst) + 's3://path/of/dir1' + """ + dst = self._format_path(self._map_path(dst)) + if self.exists(dst): + raise FileExistsError("dst should not exist") + + src = str(src) + + for cur_dir, _, files in os.walk(src): + for f in files: + src_path = os.path.join(cur_dir, f) + dst_path = self.join_path(dst, src_path.replace(src, "")) + self.copyfile_from_local(src_path, dst_path) + + return dst + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 's3://path/of/file' + >>> dst = 'path/of/your/file' + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/file' + + >>> # dst is a directory + >>> dst = 'path/of/your/dir' + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/dir/file' + """ + assert dst_type in ["file", "dir"] + # There is no good way to detect whether dst is a directory or a file, so we make dst_type required + if dst_type == "dir": + basename = os.path.basename(src) + if isinstance(dst, str): + dst = os.path.join(dst, basename) + else: + assert isinstance(dst, Path) + dst = dst / basename + + # Create parent directory if it doesn't exist + parent_dir = os.path.dirname(dst) + os.makedirs(parent_dir, exist_ok=True) + + try: + with open(dst, "wb") as f: + data = self.get(src) + f.write(data) + except Exception as e: + log.error(f"Failed to write file: {e}") + raise + + return dst + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/dir' + >>> dst = 'path/of/your/dir' + >>> backend.copytree_to_local(src, dst) + 'path/of/your/dir' + """ + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + dst_path = os.path.join(dst, path) + mkdir_or_exist(os.path.dirname(dst_path)) + with open(dst_path, "wb") as f: + f.write(self.get(self.join_path(src, path))) + + return dst + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.remove(filepath) + """ + if not has_method(self._client, "delete"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `delete` method, please use a higher version or dev " + "branch instead." + ) + + if not self.exists(filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath): + raise IsADirectoryError("filepath should be a file") + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.delete(filepath) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> backend.rmtree(dir_path) + """ + for path in self.list_dir_or_file(dir_path, list_dir=False, recursive=True): + filepath = self.join_path(dir_path, path) + self.remove(filepath) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + Directly copy src to dst because PetrelBacekend does not support create + a symbolic link. + + Args: + src (str or Path): A file or directory to be copied. + dst (str or Path): Copy a file or directory to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + bool: Return False because Boto3Backend does not support create + a symbolic link. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/file' + >>> dst = 's3://path/of/your/file' + >>> backend.copy_if_symlink_fails(src, dst) + False + >>> src = 's3://path/of/dir' + >>> dst = 's3://path/of/your/dir' + >>> backend.copy_if_symlink_fails(src, dst) + False + """ + if self.isfile(src): + self.copyfile(src, dst) + else: + self.copytree(src, dst) + return False + + def list_dir(self, dir_path: Union[str, Path]): + """List all folders in an S3 bucket with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> backend.list_dir(dir_path) + """ + dir_path = self._map_path(dir_path) + dir_path = self._format_path(dir_path) + dir_path = self._replace_prefix(dir_path) + return self._client.ls_dir(dir_path) + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + Boto3 has no concept of directories but it simulates the directory + hierarchy in the filesystem through public prefixes. In addition, + if the returned path ends with '/', it means the path is a public + prefix which is a logical directory. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + In addition, the returned path of directory will not contains the + suffix '/' which is consistent with other backends. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> # list those files and directories in current directory + >>> for file_path in backend.list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in backend.list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in backend.list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in backend.list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in backend.list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ # noqa: E501 + if not has_method(self._client, "list"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `list` method, please use a higher version or dev" + " branch instead." + ) + + dir_path = self._map_path(dir_path) + dir_path = self._format_path(dir_path) + dir_path = self._replace_prefix(dir_path) + if list_dir and suffix is not None: + raise TypeError("`list_dir` should be False when `suffix` is not None") + + if list_dir and not list_file and not recursive: + raise TypeError( + "Please use `list_dir` instead of `list_dir_or_file` when you only want to list the first level directories." + ) + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + # Boto3's simulated directory hierarchy assumes that directory paths + # should end with `/` + if not dir_path.endswith("/"): + dir_path += "/" + + root = dir_path + + def _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive): + # Keep track of directories we've already yielded to avoid duplicates + yielded_dirs = set() if list_dir else None + + for path in self._client.list(dir_path): + # All paths returned by S3 list are file paths, never directory paths + absolute_path = self.join_path(dir_path, path) + rel_path = absolute_path[len(root) :] + + # If we want directories, extract directory prefixes from file paths + # boto3 client actually never return dir, it only return file paths + if list_dir and "/" in rel_path: + if not recursive: + # Non-recursive: only yield immediate child directory (first level) + first_slash_pos = rel_path.find("/") + immediate_child_dir = rel_path[:first_slash_pos] + + if immediate_child_dir not in yielded_dirs: + yielded_dirs.add(immediate_child_dir) + yield immediate_child_dir + else: + # Recursive: yield all directory levels + path_parts = rel_path.split("/")[:-1] # Exclude filename + current_dir = "" + for part in path_parts: + if current_dir: + current_dir += "/" + part + else: + current_dir = part + + if current_dir not in yielded_dirs: + yielded_dirs.add(current_dir) + yield current_dir + + # Handle file listing + if (suffix is None or rel_path.endswith(suffix)) and list_file: + yield rel_path + + return _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) + + def generate_presigned_url(self, url: str, client_method: str = "get_object", expires_in: int = 3600) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on Boto3 backend. + + Note: + Now only work on Boto3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Default: 'get_object'. + expires_in (int): expires, in seconds. Default: 3600. + + Returns: + str: Generated presigned url. + """ + raise NotImplementedError("generate_presigned_url is not supported in Boto3Backend") + return self._client.generate_presigned_url(url, client_method, expires_in) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py new file mode 100644 index 00000000..becc1a71 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py @@ -0,0 +1,640 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import concurrent.futures +import io +import os +import time +from collections.abc import Generator +from math import ceil +from multiprocessing import shared_memory +from typing import Any, Optional + +import boto3 +import numpy as np +from botocore.config import Config as S3Config +from botocore.exceptions import ClientError + +import cosmos3._src.imaginaire.utils.easy_io.backends.auto_auth as auto +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.env_parsers.cred_env_parser import CRED_ENVS + +try: + # pyrefly: ignore # import-error + import aioboto3 + + # pyrefly: ignore # import-error + import aioboto3.session + + # pyrefly: ignore # import-error + from aiobotocore.config import AioConfig + + # pyrefly: ignore # import-error + from aiobotocore.session import AioSession +except ImportError: + aioboto3 = None + AioSession = None + +MAX_RETRIES = 5 +RETRY_DELAY = 1 # seconds + + +async def upload_single_part_async( + s3: AioSession, bucket: str, key: str, part_number: int, data: bytes, upload_id: str +) -> dict[str, Any]: + """ + Uploads a single part of a file asynchronously to S3. + + Args: + s3 (S3): The S3 client. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + part_number (int): The part number of the upload. + data (bytes): The data to upload. + upload_id (str): The upload ID for the multipart upload. + + Returns: + dict[str, Any]: A dictionary containing the part number and ETag. + """ + for attempt in range(MAX_RETRIES): + try: + response = await s3.upload_part( + Bucket=bucket, Key=key, PartNumber=part_number, UploadId=upload_id, Body=data + ) + return {"PartNumber": part_number, "ETag": response["ETag"]} + except (ClientError, asyncio.TimeoutError, Exception) as e: + log.warning(f"Attempt {attempt + 1} failed for part {part_number}: {str(e)}", rank0_only=False) + if attempt < MAX_RETRIES - 1: + await asyncio.sleep(RETRY_DELAY * (2**attempt)) # Exponential backoff + else: + log.error(f"Failed to upload part {part_number} after {MAX_RETRIES} attempts", rank0_only=False) + raise + + +async def upload_parts_async( + part_size: int, + part_numbers: range, + upload_id: str, + data: bytes, + bucket: str, + key: str, + client_config: dict[str, Any], +) -> list[dict[str, Any]]: + """ + Uploads multiple parts of a file asynchronously to S3. + + Args: + part_size (int): The size of each part in bytes. + part_numbers (range): The range of part numbers to upload. + upload_id (str): The upload ID for the multipart upload. + data (bytes): The data to upload. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + list[dict[str, Any]]: A list of dictionaries containing part numbers and ETags. + """ + session = aioboto3.Session() + config = AioConfig(retries={"max_attempts": 3, "mode": "adaptive"}, connect_timeout=5, read_timeout=10) + start_idx = part_numbers[0] + async with session.client("s3", config=config, **client_config) as s3: + tasks = [] + for part_number in part_numbers: + start = (part_number - start_idx) * part_size + end = min(start + part_size, len(data)) + part_data = data[start:end] + tasks.append(upload_single_part_async(s3, bucket, key, part_number + 1, part_data, upload_id)) + + results = await asyncio.gather(*tasks, return_exceptions=True) + + successful_parts = [] + failed_parts = [] + for part_number, result in enumerate(results, start=start_idx + 1): + if isinstance(result, Exception): + failed_parts.append(part_number) + else: + successful_parts.append(result) + + if failed_parts: + log.error(f"Failed to upload parts: {failed_parts}", rank0_only=False) + raise Exception(f"Failed to upload {len(failed_parts)} parts") + + successful_parts.sort(key=lambda part: part["PartNumber"]) + return successful_parts + + +def upload_parts_to_s3(args: tuple[range, str, int, bytes, str, str, dict[str, Any]]) -> list[dict[str, Any]]: + """ + Uploads parts of a file to S3 using a new event loop. + + Args: + args (tuple[range, str, int, bytes, str, str, dict[str, Any]]): The arguments for uploading parts, including: + part_numbers (range): The range of part numbers to upload. + upload_id (str): The upload ID for the multipart upload. + part_size (int): The size of each part in bytes. + data (bytes): The data to upload. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + list[dict[str, Any]]: A list of dictionaries containing part numbers and ETags. + """ + part_numbers, upload_id, part_size, data, bucket, key, client_config = args + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + parts = loop.run_until_complete( + upload_parts_async(part_size, part_numbers, upload_id, data, bucket, key, client_config) + ) + loop.close() + return parts + + +async def download_single_part_async( + s3, bucket: str, key: str, part_number: int, start: int, end: int, shm_name: str, part_size: int +) -> None: + """ + Downloads a single part of a file asynchronously and writes it to shared memory. + + Args: + s3 (S3): The S3 client. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + part_number (int): The part number. + start (int): The start byte of the part. + end (int): The end byte of the part. + shm_name (str): The name of the shared memory block. + part_size (int): The size of each part in bytes. + """ + for attempt in range(MAX_RETRIES): + try: + range_header = f"bytes={start}-{end}" + response = await s3.get_object(Bucket=bucket, Key=key, Range=range_header) + data = await response["Body"].read() + + shm = shared_memory.SharedMemory(name=shm_name) + offset = part_number * part_size + shm.buf[offset : offset + len(data)] = data + shm.close() + return + except (ClientError, asyncio.TimeoutError, Exception) as e: + log.warning(f"Attempt {attempt + 1} failed for part {part_number}: {str(e)}", rank0_only=False) + if attempt < MAX_RETRIES - 1: + await asyncio.sleep(RETRY_DELAY * (2**attempt)) # Exponential backoff + else: + log.error(f"Failed to download part {part_number} after {MAX_RETRIES} attempts", rank0_only=False) + raise + + +async def download_parts_async( + part_size: int, part_numbers: range, bucket: str, key: str, client_config: dict[str, Any], shm_name: str +) -> None: + """ + Downloads multiple parts of a file asynchronously and writes them to shared memory. + + Args: + part_size (int): The size of each part in bytes. + part_numbers (range): The range of part numbers to download. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + shm_name (str): The name of the shared memory block. + """ + session = aioboto3.Session() + config = AioConfig(retries={"max_attempts": 5, "mode": "adaptive"}, connect_timeout=10, read_timeout=30) + async with session.client("s3", config=config, **client_config) as s3: + tasks = [ + download_single_part_async( + s3, + bucket, + key, + part_number, + part_number * part_size, + (part_number + 1) * part_size - 1, + shm_name, + part_size, + ) + for part_number in part_numbers + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + failed_parts = [part for part, result in zip(part_numbers, results) if isinstance(result, Exception)] + + if failed_parts: + log.error(f"Failed to download parts: {failed_parts}", rank0_only=False) + raise Exception(f"Failed to download {len(failed_parts)} parts") + + +def download_parts_to_s3(args: tuple[range, int, str, str, dict[str, Any], str]) -> bytes: + """ + Downloads parts of a file using a new event loop. + + Args: + args (tuple[range, int, str, str, dict[str, Any]]): The arguments for downloading parts, including: + part_numbers (range): The range of part numbers to download. + part_size (int): The size of each part in bytes. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + bytes: The combined file data from all downloaded parts. + """ + part_numbers, part_size, bucket, key, client_config, shm_name = args + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(download_parts_async(part_size, part_numbers, bucket, key, client_config, shm_name)) + loop.close() + + +class Boto3Client: + """ + This class: + + - Provides higher-level S3 operations. + - Serves as a wrapper around boto3.client in order to make boto3.client serializable. + - It's required to use spawn method of creating DataLoader workers, + which is in turn required to avoid segfaults when using Triton, + e.g. for torch.compile or custom kernels. + """ + + def __init__( + self, + s3_credential_path: str, + max_attempt: int = 3, + ): + self.max_attempt: int = max_attempt + assert s3_credential_path, "s3_credential_path is required" + assert os.path.exists(s3_credential_path) or CRED_ENVS.APP_ENV in [ + "prod", + "dev", + "stg", + ], f"Credential file not found: {s3_credential_path}" + + # Keep track of S3 client constructor parameters so it can be recreated when pickling. + with auto.open_auth(s3_credential_path, "r") as f: + self._s3_cred_info = auto.json_load_auth(f) + self._s3_config = S3Config( + signature_version="s3v4", + s3={"addressing_style": "virtual"}, + response_checksum_validation="when_required", + request_checksum_calculation="when_required", + ) + self._init_client() + self._mc_kv_store = None + + def _init_client(self): + """Initialize the S3 client.""" + self._client = boto3.client("s3", **self._s3_cred_info, config=self._s3_config) + + def __getstate__(self): + state = self.__dict__.copy() + # S3 client isn't pickleable. + del state["_client"] + return state + + def __setstate__(self, state: dict[str, Any]): + self.__dict__.update(state) + self._init_client() + + def size(self, filepath: str) -> int: + filepath = self._check_path(filepath) + + if self._mc_kv_store and self._mc_kv_store.available: + if self._mc_kv_store.has(filepath): + return len(self._mc_kv_store.get(filepath)) + + attempt: int = 0 + while attempt < self.max_attempt: + try: + return self._client.head_object( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + )["ContentLength"] + except ClientError as e: + if e.response["Error"]["Code"] == "404": + raise # Object does not exist. + else: + attempt += 1 + log.error(f"Attempt {attempt} failed for {filepath}: {e}", rank0_only=False) + if attempt >= self.max_attempt: + raise # Re-raise the exception after max attempt + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + attempt += 1 + log.error(f"Attempt {attempt} failed for {filepath}: due to an unexpected error: {e}", rank0_only=False) + if attempt >= self.max_attempt: + raise # Re-raise the exception after max attempt + time.sleep(2) # Wait for 2 seconds before retrying + + raise ConnectionError("Unable to head {} from. {} attempts tried.".format(filepath, attempt)) + + def get(self, filepath: str, offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + raw_filepath = filepath + filepath = self._check_path(filepath) + + read_offset: Optional[int] = None + read_size: Optional[int] = None + byte_range: Optional[str] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=raw_filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + byte_range = f"bytes={read_offset}-{read_offset + read_size - 1}" + + if self._mc_kv_store and self._mc_kv_store.available: + if self._mc_kv_store.has(filepath): + chunk: bytes = self._mc_kv_store.get(filepath) + if read_offset is not None and read_size is not None: + return chunk[read_offset : read_offset + read_size] + else: + return chunk + + attempt = 0 + while attempt < self.max_attempt: + try: + buffer = io.BytesIO() + if byte_range is None: + self._client.download_fileobj( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + Fileobj=buffer, + ) + else: + # The boto S3 Transfer Manager doesn't support ranged reads yet. + # + # https://github.com/boto/boto3/issues/1215 + # https://github.com/boto/s3transfer/issues/248 + resp = self._client.get_object( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + Range=byte_range, + ) + buffer.write(resp["Body"].read()) + buffer.seek(0) + # Only cache full reads. + if byte_range is None: + if self._mc_kv_store and self._mc_kv_store.available: + self._mc_kv_store.put(filepath, buffer.read()) + buffer.seek(0) + + return buffer.read() + except Exception as e: + attempt += 1 + log.error(f"Got an exception: attempt={attempt} - {e} - {filepath}", rank0_only=False) + + raise ConnectionError("Unable to read {} from. {} attempts tried.".format(filepath, attempt)) + + def put(self, obj, filepath): + filepath = self._check_path(filepath) + bucket_name = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + attempt = 0 + while attempt < self.max_attempt: + try: + # If obj is a string path to a local file, use upload_file instead + if isinstance(obj, str) and os.path.isfile(obj): + self._client.upload_file(Filename=obj, Bucket=bucket_name, Key=key) + return + if isinstance(obj, io.BytesIO): + obj.seek(0) + self._client.upload_fileobj(obj, Bucket=bucket_name, Key=key) + return + if isinstance(obj, bytes): + self._client.put_object(Body=obj, Bucket=bucket_name, Key=key) + return + else: + raise ValueError("Unsupported object type for upload") + except ClientError as e: + attempt += 1 + log.error(f"Got an exception: attempt={attempt} - {e} - {filepath}", rank0_only=False) + + raise ConnectionError("Unable to write {} to. {} attempts tried.".format(filepath, attempt)) + + def fast_put(self, obj, filepath, num_processes: int = 32): + assert aioboto3 is not None, "aioboto3 is required for fast_put" + original_filepath = filepath + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + part_size = 16 * 1024 * 1024 # 16 MB part size + + if isinstance(obj, bytes): + data = obj + elif isinstance(obj, str) and os.path.isfile(obj): + with open(obj, "rb") as f: + data = f.read() + elif isinstance(obj, io.BytesIO): + obj.seek(0) + data = obj.read() + else: + raise ValueError("Unsupported object type for upload") + + file_size = len(data) + if file_size <= part_size * num_processes: + return self.put(data, original_filepath) + num_parts = ceil(file_size / part_size) + upload_id = self._client.create_multipart_upload(Bucket=bucket, Key=key)["UploadId"] + + part_numbers = np.array_split(np.arange(num_parts), num_processes) + + with concurrent.futures.ProcessPoolExecutor(max_workers=num_processes) as executor: + args = [] + for i in range(num_processes): + cur_parts = part_numbers[i].tolist() + cur_data = data[cur_parts[0] * part_size : min(cur_parts[-1] * part_size + part_size, file_size)] + args.append((cur_parts, upload_id, part_size, cur_data, bucket, key, self._s3_cred_info)) + results = executor.map(upload_parts_to_s3, args) + parts = [] + for result in results: + parts.extend(result) + + parts = sorted(parts, key=lambda part: part["PartNumber"]) + self._client.complete_multipart_upload( + Bucket=bucket, Key=key, UploadId=upload_id, MultipartUpload={"Parts": parts} + ) + + def contains(self, filepath: str, max_retries=10) -> bool: + """ + Checks if the specified object exists in the S3 bucket with retry logic for errors. + + Args: + filepath (str): The s3 path of the file to check, must start with "s3://". + + Returns: + bool: True if the object exists in the S3 bucket, False otherwise. + + Raises: + ClientError: If an error response other than "404 Not Found" is returned from the S3 service. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + + retries = 0 + while retries < max_retries: + try: + # Try to check if the object exists + self._client.head_object(Bucket=bucket, Key=key) + return True # Object exists + except ClientError as e: + if e.response["Error"]["Code"] == "404": + return False # Object does not exist + else: + retries += 1 + print(f"Attempt {retries} failed with error: {e}") + if retries >= max_retries: + raise # Re-raise the exception if max retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + retries += 1 + print(f"Attempt {retries} failed due to an unexpected error: {e}") + if retries >= max_retries: + raise # Re-raise the exception if max retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + + def isdir(self, filepath: str, max_retries=10) -> bool: + """ + Determines if the specified path corresponds to a directory in S3 with retry logic. + + A directory in S3 is implied if there are any objects stored with the given prefix, + which means this function checks for the existence of any objects at or under the specified path. + + Args: + filepath (str): The s3 path to check, must start with "s3://". + + Returns: + bool: True if the specified path corresponds to a directory in S3, False otherwise. + Directories in S3 are not physical entities but are implied by object keys. + + Raises: + ClientError: An error from the S3 API that isn't related to the absence of the directory + (logged but not raised further). + """ + filepath = self._check_path(filepath) + if not filepath.endswith("/"): + filepath += "/" + + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + + retries = 0 + while retries < max_retries: + try: + # Try to check if any objects exist with the given prefix (i.e., directory in S3) + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter="/", MaxKeys=1) + # Check if any content or prefixes exist under the given path + return "CommonPrefixes" in resp or "Contents" in resp + except ClientError as e: + retries += 1 + log.error(f"Attempt {retries} failed: {e}", rank0_only=False) + if retries >= max_retries: + return False # Return False if maximum retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + retries += 1 + log.error(f"Attempt {retries} failed due to an unexpected error: {e}", rank0_only=False) + if retries >= max_retries: + return False # Return False if maximum retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + + def delete(self, filepath): + filepath = self._check_path(filepath) + self._client.delete_object(Bucket=filepath.split("/")[0], Key="/".join(filepath.split("/")[1:])) + + def ls_dir(self, filepath: str) -> Generator[str, None, None]: + """ + List all folders in an S3 bucket with a given prefix. + + Args: + filepath (str): The S3 path of the folder to list. + + Yields: + str: The keys of the folders in the S3 bucket. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + continuation_token = None + if prefix and not prefix.endswith("/"): + prefix += "/" + + while True: + if continuation_token: + resp = self._client.list_objects_v2( + Bucket=bucket, Prefix=prefix, Delimiter="/", ContinuationToken=continuation_token + ) + else: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter="/") + + if "CommonPrefixes" in resp: + for item in resp["CommonPrefixes"]: + yield item["Prefix"][len(prefix) :] + + # Check if there are more keys to retrieve + if resp.get("IsTruncated"): # If IsTruncated is True, there are more keys + continuation_token = resp.get("NextContinuationToken") + else: + break + + def list(self, filepath: str, exclude_prefix: Optional[str] = None) -> Generator[str, None, None]: + """ + List all keys in an S3 bucket with a given prefix, excluding files that start with + specified prefix. + + Args: + filepath (str): The S3 path of the file to list. + exclude_prefix (str): Files starting with this prefix will be excluded from results. + Defaults to "real". + + Yields: + str: The keys of the files in the S3 bucket that don't start with exclude_prefix. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + + continuation_token = None + + while True: + if continuation_token: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=continuation_token) + else: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix) + + if "Contents" in resp: + for item in resp["Contents"]: + key = item["Key"][len(prefix) :] + # Skip files that start with the excluded prefix + if exclude_prefix is None or not key.startswith(exclude_prefix): + yield key + + # Check if there are more keys to retrieve + if resp.get("IsTruncated"): # If IsTruncated is True, there are more keys + continuation_token = resp.get("NextContinuationToken") + else: + break + + def _check_path(self, filepath: str): + assert filepath.startswith("s3://") + filepath = filepath[5:] + return filepath diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py new file mode 100644 index 00000000..7847c74d --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union +from urllib.request import Request, urlopen + +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend + + +class HTTPBackend(BaseStorageBackend): + """HTTP and HTTPS storage bachend.""" + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = HTTPBackend() + >>> filepath = 'http://path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + request = Request(url=str(filepath), method="HEAD") + with urlopen(request) as response: + if response.status == 200: + return int(response.headers["Content-Length"]) + else: + raise RuntimeError(f"Unexpected response: {response}") + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> backend = HTTPBackend() + >>> backend.get('http://path/of/file') + b'hello world' + """ + request = Request(url=str(filepath), method="GET") + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + request.add_header("Range", f"bytes={read_offset}-{read_offset + read_size - 1}") + with urlopen(request) as response: + if response.status in {200, 206}: + return response.read() + else: + raise RuntimeError(f"Unexpected response: {response}") + + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + """Read text from a given ``filepath``. + + Args: + filepath (str): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = HTTPBackend() + >>> backend.get_text('http://path/of/file') + 'hello world' + """ + return self.get(filepath=filepath).decode(encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + raise NotImplementedError(f"put not supported in {self.name}") + + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + raise NotImplementedError(f"put_text not supported in {self.name}") + + def exists(self, filepath: Union[str, Path]) -> bool: + request = Request(url=str(filepath), method="HEAD") + with urlopen(request) as response: + if response.status == 404: + return False + elif response.status == 200: + return True + else: + raise RuntimeError(f"Unexpected response: {response}") + + def isdir(self, filepath: Union[str, Path]) -> bool: + raise NotImplementedError(f"isdir not supported in {self.name}") + + def isfile(self, filepath: Union[str, Path]) -> bool: + raise NotImplementedError(f"isfile not supported in {self.name}") + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + raise NotImplementedError(f"join_path not supported in {self.name}") + + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = HTTPBackend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> with backend.get_local_path('http://path/of/file') as path: + ... # do something here + """ + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copyfile not supported in {self.name}") + + def copytree(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copytree not supported in {self.name}") + + def copyfile_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copyfile_from_local not supported in {self.name}") + + def copytree_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copytree_from_local not supported in {self.name}") + + def copyfile_to_local(self, src: Union[str, Path], dst: Union[str, Path], dst_type: str) -> Union[str, Path]: + raise NotImplementedError(f"copyfile_to_local not supported in {self.name}") + + def copytree_to_local(self, src: Union[str, Path], dst: Union[str, Path]) -> Union[str, Path]: + raise NotImplementedError(f"copytree_to_local not supported in {self.name}") + + def remove(self, filepath: Union[str, Path]) -> None: + raise NotImplementedError(f"remove not supported in {self.name}") + + def rmtree(self, dir_path: Union[str, Path]) -> None: + raise NotImplementedError(f"rmtree not supported in {self.name}") + + def copy_if_symlink_fails(self, src: Union[str, Path], dst: Union[str, Path]) -> bool: + raise NotImplementedError(f"copy_if_symlink_fails not supported in {self.name}") + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + raise NotImplementedError(f"list_dir not supported in {self.name}") + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + raise NotImplementedError(f"list_dir_or_file not supported in {self.name}") diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py new file mode 100644 index 00000000..91927dcc --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py @@ -0,0 +1,599 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import os.path as osp +import shutil +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union + +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend, mkdir_or_exist + + +class LocalBackend(BaseStorageBackend): + """Raw local storage backend.""" + + _allow_symlink = True + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + return osp.getsize(filepath) + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode. + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.get(filepath) + b'hello world' + """ + read_offset: Optional[int] = None + read_size: Optional[int] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + with open(filepath, "rb") as f: + if read_offset is not None: + f.seek(read_offset) + value = f.read(read_size) + return value + + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.get_text(filepath) + 'hello world' + """ + with open(filepath, encoding=encoding) as f: + text = f.read() + return text + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` will create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.put(b'hello world', filepath) + """ + mkdir_or_exist(osp.dirname(filepath)) + if isinstance(obj, io.BytesIO): + obj.seek(0) + obj = obj.getvalue() + with open(filepath, "wb") as f: + f.write(obj) + + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + """Write text to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` will create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.put_text('hello world', filepath) + """ + mkdir_or_exist(osp.dirname(filepath)) + with open(filepath, "w", encoding=encoding) as f: + f.write(obj) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.exists(filepath) + True + """ + return osp.exists(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/dir' + >>> backend.isdir(filepath) + True + """ + return osp.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.isfile(filepath) + True + """ + return osp.isfile(filepath) + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result of concatenation. + + Examples: + >>> backend = LocalBackend() + >>> filepath1 = '/path/of/dir1' + >>> filepath2 = 'dir2' + >>> filepath3 = 'path/of/file' + >>> backend.join_path(filepath1, filepath2, filepath3) + '/path/of/dir/dir2/path/of/file' + """ + # TODO, if filepath or filepaths are Path, should return Path + return osp.join(filepath, *filepaths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Only for unified API and do nothing. + + Args: + filepath (str or Path): Path to be read data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Examples: + >>> backend = LocalBackend() + >>> with backend.get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + """ + yield filepath + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to '/path1/of/dir/file' + >>> backend.copyfile(src, dst) + '/path1/of/dir/file' + """ + return shutil.copy(src, dst) + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + src and dst should have the same prefix and dst must not already exist. + + TODO: Whether to support dirs_exist_ok parameter. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree(src, dst) + '/path/of/dir2' + """ + return shutil.copytree(src, dst) + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a local file src to dst and return the destination file. Same + as :meth:`copyfile`. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile_from_local(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to + >>> backend.copyfile_from_local(src, dst) + '/path1/of/dir/file' + """ + return self.copyfile(src, dst) + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. Same as + :meth:`copytree`. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree_from_local(src, dst) + '/path/of/dir2' + """ + return self.copytree(src, dst) + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: Optional[str] = None, + ) -> str: + """Copy the file src to local dst and return the destination file. Same + as :meth:`copyfile`. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile_to_local(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to + >>> backend.copyfile_to_local(src, dst) + '/path1/of/dir/file' + """ + return self.copyfile(src, dst) + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree_from_local(src, dst) + '/path/of/dir2' + """ + return self.copytree(src, dst) + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.remove(filepath) + """ + if not self.exists(filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath): + raise IsADirectoryError("filepath should be a file") + + os.remove(filepath) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> dir_path = '/path/of/dir' + >>> backend.rmtree(dir_path) + """ + shutil.rmtree(dir_path) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + If failed to create a symbolic link pointing to src, directly copy src + to dst instead. + + Args: + src (str or Path): Create a symbolic link pointing to src. + dst (str or Path): Create a symbolic link named dst. + + Returns: + bool: Return True if successfully create a symbolic link pointing + to src. Otherwise, return False. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> backend.copy_if_symlink_fails(src, dst) + True + >>> src = '/path/of/dir' + >>> dst = '/path1/of/dir1' + >>> backend.copy_if_symlink_fails(src, dst) + True + """ + try: + os.symlink(src, dst) + return True + except Exception: + if self.isfile(src): + self.copyfile(src, dst) + else: + self.copytree(src, dst) + return False + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + """List all folders in a storage location with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = LocalBackend() + >>> dir_path = 'path/of/dir' + >>> list(backend.list_dir(dir_path)) + ['subdir1/', 'subdir2/'] + """ + for entry in os.scandir(dir_path): + if entry.is_dir(): + yield f"{entry.name}/" + + def list_dir_or_file( + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str or Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix that we are + interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the directory. + Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = LocalBackend() + >>> dir_path = '/path/of/dir' + >>> # list those files and directories in current directory + >>> for file_path in backend.list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in backend.list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in backend.list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in backend.list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in backend.list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ # noqa: E501 + if list_dir and suffix is not None: + raise TypeError("`suffix` should be None when `list_dir` is True") + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + root = dir_path + + def _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive): + for entry in os.scandir(dir_path): + if not entry.name.startswith(".") and entry.is_file(): + rel_path = osp.relpath(entry.path, root) + if (suffix is None or rel_path.endswith(suffix)) and list_file: + yield rel_path + elif osp.isdir(entry.path): + if list_dir: + rel_dir = osp.relpath(entry.path, root) + yield rel_dir + if recursive: + yield from _list_dir_or_file(entry.path, list_dir, list_file, suffix, recursive) + + return _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py new file mode 100644 index 00000000..6d54e6b9 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py @@ -0,0 +1,1075 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import io +import os +import re +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from shutil import SameFileError +from typing import Any, Optional, Union +from urllib.parse import urlparse + +import yaml +from multistorageclient import StorageClient, StorageClientConfig +from multistorageclient.types import Range + +import cosmos3._src.imaginaire.utils.easy_io.backends.auto_auth as auto +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend, mkdir_or_exist + +# {scheme}:// +_URL_PREFIX_REGEX = r"[a-zA-Z0-9+.-]*:\/\/" + + +def _get_telemetry_config_from_msc_secret() -> Optional[dict[str, Any]]: + """Generate MSC telemetry configuration from credentials/msc.secret file if available. + + Reads OpenTelemetry configuration from the ``credentials/msc.secret`` YAML file. + The file should contain an ``msc.opentelemetry`` section with the required fields + (vault_endpoint, vault_namespace, approle_id, approle_secret, mount_point, secret_path). + The endpoint field is optional and defaults to the production OTLP endpoint. + + Returns: + Optional[dict]: OpenTelemetry configuration dictionary if file exists and contains + required fields, None otherwise. + """ + msc_secret_path = Path("credentials/msc.secret") + + if not msc_secret_path.exists(): + log.debug(f"MSC secret file not found at {msc_secret_path}", rank0_only=True) + return None + + try: + with open(msc_secret_path, "r") as f: + msc_config = yaml.safe_load(f) + + if not msc_config or not isinstance(msc_config, dict): + log.warning(f"Invalid MSC secret file format at {msc_secret_path}", rank0_only=True) + return None + + msc_section = msc_config.get("msc", {}) + opentelemetry_section = msc_section.get("opentelemetry", {}) + + required_fields = ( + "vault_endpoint", + "vault_namespace", + "approle_id", + "approle_secret", + "mount_point", + "secret_path", + ) + missing = [f for f in required_fields if not opentelemetry_section.get(f)] + if missing: + log.warning( + f"MSC secret file at {msc_secret_path} missing required fields: {', '.join(missing)}", + rank0_only=True, + ) + return None + + vault_endpoint = opentelemetry_section["vault_endpoint"] + vault_namespace = opentelemetry_section["vault_namespace"] + approle_id = opentelemetry_section["approle_id"] + approle_secret = opentelemetry_section["approle_secret"] + mount_point = opentelemetry_section["mount_point"] + secret_path = opentelemetry_section["secret_path"] + cert_key = opentelemetry_section["cert_key"] + key_key = opentelemetry_section["key_key"] + ca_key = opentelemetry_section["ca_key"] + endpoint = opentelemetry_section["endpoint"] + except Exception as e: + log.warning(f"Failed to load MSC secret file at {msc_secret_path}: {e}", rank0_only=True) + return None + + # Construct OpenTelemetry configuration dictionary. + opentelemetry_config = { + "opentelemetry": { + "metrics": { + "attributes": [ + # All environments. + {"type": "static", "options": {"attributes": {"msc.ppp": "COSMOS", "msc.job": "unknown"}}}, + {"type": "host", "options": {"attributes": {"msc.cluster": "name", "msc.node": "name"}}}, + {"type": "process", "options": {"attributes": {"msc.process": "pid"}}}, + { + "type": "msc_config", + "options": { + "attributes": { + "msc.approle_id": { + "expression": "opentelemetry.metrics.exporter.options.auth.approle_id" + }, + "msc.approle_secret": { + "expression": ( + "hash('sha3-224', opentelemetry.metrics.exporter.options.auth.approle_secret)" + ) + }, + } + }, + }, + # Progressive enhancement for Lepton environments. + # + # https://docs.nvidia.com/dgx-cloud/lepton/features/batch-jobs/predefined-env-vars + { + "type": "environment_variables", + "options": { + "attributes": { + "msc.job": "LEPTON_JOB_NAME", + "msc.job_user": "LEPTON_USERID", + "msc.job_nodes": "LEPTON_JOB_TOTAL_WORKERS", + "msc.cluster": "LEPTON_WORKER_CLUSTER_NAME", + "msc.node": "LEPTON_WORKER_ID", + "msc.node_gpus": "LEPTON_RESOURCE_ACCELERATOR_NUM", + } + }, + }, + # Progressive enhancement for Slurm environments. + # + # https://slurm.schedmd.com/prolog_epilog.html#environment_variables + { + "type": "environment_variables", + "options": { + "attributes": { + "msc.ppp": "SLURM_JOB_ACCOUNT", + "msc.job": "SLURM_JOB_ID", + "msc.job_user": "SLURM_JOB_USER", + "msc.job_nodes": "SLURM_JOB_NUM_NODES", + "msc.job_gpus": "SLURM_GPUS", + "msc.cluster": "SLURM_CLUSTER_NAME", + "msc.node": "SLURMD_NODENAME", + "msc.node_gpus": "SLURM_GPUS_ON_NODE", + "msc.slurm_job_partition": "SLURM_JOB_PARTITION", + } + }, + }, + ], + "reader": { + "options": { + # ≤ 100 Hz collect frequency. + "collect_interval_millis": 10, + "collect_timeout_millis": 100, + # ≤ 1 Hz export frequency. + "export_interval_millis": 1000, + "export_timeout_millis": 500, + } + }, + "exporter": { + "type": "_otlp_mtls_vault", + "options": { + "exporter": {"endpoint": endpoint}, + "auth": { + "vault_endpoint": vault_endpoint, + "vault_namespace": vault_namespace, + "approle_id": approle_id, + "approle_secret": approle_secret, + "mount_point": mount_point, + "secret_path": secret_path, + "cert_key": cert_key, + "key_key": key_key, + "ca_key": ca_key, + }, + }, + }, + }, + } + } + + return opentelemetry_config + + +class MSCBackend(BaseStorageBackend): + """Multi-Storage Client (MSC) backend. + + Uses MSC storage clients instead of MSC shortcuts. + + URL file paths (e.g. 's3://path/of/file') are handled transparently. Using URL file paths + as input will return URL file path outputs when appropriate to match Boto3Backend behavior. + + **If using URL file paths, the storage provider's base path option must be empty!** + + Get/put concurrency can be set for certain providers in the MSC configuration file. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get(filepath) + """ + + _storage_client: StorageClient + _path_mapping: dict[str, str] + + def __init__( + self, + config_path: Optional[str] = "credentials/msc_config.yaml", + profile: Optional[str] = None, + s3_credential_path: Optional[str] = None, + path_mapping: Optional[dict[str, str]] = None, + ): + """Initialize a backend. + + Args: + config_path (str, optional): MSC config path (e.g. ``credentials/msc_config.yaml``). + profile (str, optional): MSC profile from the MSC config to use. + Mutually exclusive with ``s3_credential_path``. + s3_credential_path (str, optional): Legacy Boto3 config path (e.g. ``credentials/s3_training.secret``). + Translated into an MSC profile that's merged with the MSC config at ``config_path`` with: + + - The profile name set to ``s3_credential_path`` verbatim. + - The storage and credentials provider types determined by the file contents. + + Mutually exclusive with ``profile``. + path_mapping (dict, optional): Path mapping dict from src path to dst path. + When ``path_mapping={'src': 'dst'}``, ``src`` in ``filepath`` will be replaced by ``dst``. + Doesn't apply to the local path in ``copy{file,tree}_{from,to}_local`` methods. + """ + if all(_ is None for _ in (profile, s3_credential_path)) or all( + _ is not None for _ in (profile, s3_credential_path) + ): + raise ValueError("Must specify exactly one of profile or s3_credential_path") + + msc_config_dict: dict[str, Any] = {} + + # Use an existing MSC config file as the base MSC config. + if config_path is not None: + config_dict, _ = StorageClientConfig.read_msc_config(config_file_paths=[config_path]) + if config_dict is None: + log.info(f"No MSC config at {config_path}, using empty base MSC config", rank0_only=True) + else: + msc_config_dict = config_dict + + # Create an MSC profile from the legacy Boto3 config. + if s3_credential_path is not None: + with auto.open_auth(s3_credential_path, "r") as unloaded_legacy_boto3_config: + legacy_boto3_config = auto.json_load_auth(unloaded_legacy_boto3_config) + if len(legacy_boto3_config) > 0: + profile = s3_credential_path + + # Merge with any existing profiles. + msc_config_dict["profiles"] = msc_config_dict.get("profiles", {}) + # Merge with the existing profile, replacing `storage_provider` and `credentials_provider` completely. + msc_config_dict["profiles"][profile] = msc_config_dict["profiles"].get(profile, {}) + + storage_provider_type: str = "s3" + parsed_endpoint_url = urlparse(legacy_boto3_config["endpoint_url"]) + # Handle regional SwiftStack endpoints. + if parsed_endpoint_url.hostname.endswith(".s8k.io"): + storage_provider_type = "s8k" + # Handle global and regional GCS endpoints. + elif parsed_endpoint_url.hostname.startswith("storage.") and parsed_endpoint_url.hostname.endswith( + ".googleapis.com" + ): + storage_provider_type = "gcs_s3" + + msc_config_dict["profiles"][profile]["storage_provider"] = { + "type": storage_provider_type, + "options": { + "base_path": "", + "endpoint_url": legacy_boto3_config["endpoint_url"], + "region_name": legacy_boto3_config["region_name"], + }, + } + + if all(_ in legacy_boto3_config for _ in ("aws_access_key_id", "aws_secret_access_key")): + msc_config_dict["profiles"][profile]["credentials_provider"] = { + "type": "S3Credentials", + "options": { + "access_key": legacy_boto3_config["aws_access_key_id"], + "secret_key": legacy_boto3_config["aws_secret_access_key"], + }, + } + else: + raise ValueError("Cannot create profile from empty legacy Boto3 config") + + assert profile is not None, "Failed to resolve MSC profile" + + # Add OpenTelemetry configuration if credentials/msc.secret file is provided. + otel_config = _get_telemetry_config_from_msc_secret() + if otel_config: + msc_config_dict.update(otel_config) + log.debug("MSC Observability is configured from credentials/msc.secret", rank0_only=True) + else: + log.debug( + "MSC Observability is not configured (credentials/msc.secret not found or invalid)", rank0_only=True + ) + + # easy_io needs backend args to be JSON-serializable for backend instance cache keys. + # + # StorageClientConfig isn't, so we need to construct it here instead of receiving one. + self._storage_client = StorageClient( + config=StorageClientConfig.from_dict(config_dict=msc_config_dict, profile=profile) + ) + + assert isinstance(path_mapping, dict) or path_mapping is None + # Make a deep copy of the path mapping to prevent external mutation. + self._path_mapping = {} if path_mapping is None else copy.deepcopy(path_mapping) + for src, dst in self._path_mapping.items(): + log.info(f"Path mapping: {src} -> {dst}", rank0_only=False) + + def _translate_filepath(self, filepath: Union[str, Path], translate_url: bool = True) -> str: + """Translate a `filepath` to a string. + + Paths are of the form 'path/to/file' (path form) or '{protocol}://path/to/file' (URL form). + + Args: + filepath (str): File path to be translated. + translate_url (bool): Strip '{scheme}://' prefixes. Needed for paths passed directly to MSC storage clients. + """ + assert isinstance(filepath, (str, Path)) + + # Change to a POSIX path string. + if isinstance(filepath, str): + # If the ``filepath`` is concatenated by ``os.path.join`` in a Windows + # environment, the ``filepath`` will be the format of 'prefix\file.txt'. + filepath = re.sub(r"\\+", "/", filepath) + elif isinstance(filepath, Path): + # These should only be filesystem paths (e.g. '/path/of/file'). + # URL paths (e.g. ``Path('s3://profile/path/of/file')``) collapse '://' to ':/'. + filepath = filepath.as_posix() + else: + raise ValueError(f"Unhandled filepath type: {type(filepath)}") + + # Remap path. + # + # If there's multiple matching srcs, use the longest src (i.e. the most specific). + longest_src: str = "" + for src in self._path_mapping.keys(): + if filepath.startswith(src) and len(src) > len(longest_src): + longest_src = src + if len(longest_src) > 0: + filepath = filepath.replace(longest_src, self._path_mapping[longest_src], 1) + + # Optionally strip URL prefix then return. + # + # Don't use urlparse in case filepath is an invalid URL. + return re.sub(rf"^{_URL_PREFIX_REGEX}", "", filepath) if translate_url else filepath + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.size(filepath) # file containing "hello world" + 11 + """ + path = self._translate_filepath(filepath=filepath) + return self._storage_client.info(path=path, strict=False).content_length + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Return bytes read from filepath. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get(filepath) + b'hello world' + """ + path = self._translate_filepath(filepath=filepath) + byte_range: Optional[Range] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + byte_range = Range(offset=read_offset, size=read_size) + + if byte_range is None: + buffer = io.BytesIO() + # `StorageClient.read()` defers to `StorageProvider.get_object()` while + # `StorageClient.download_file()` defers to `StorageProvider.download_file()`. + # + # Currently, only `StorageProvider.download_file()` supports parallel downloads + # in some storage providers (e.g. boto S3 transfer manager for S3 storage providers) + # so it's often much faster. + self._storage_client.download_file(remote_path=path, local_path=buffer) + buffer.seek(0) + return buffer.read() + else: + return self._storage_client.read(path=path, byte_range=byte_range) + + def get_text( + self, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get_text(filepath) + 'hello world' + """ + return str(self.get(filepath=filepath), encoding=encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath``. + + Args: + obj (bytes): Data to be saved. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.put(b"hello world", filepath) + """ + path = self._translate_filepath(filepath=filepath) + buffer = io.BytesIO() + if isinstance(obj, bytes): + buffer.write(obj) + buffer.seek(0) + elif isinstance(obj, io.BytesIO): + buffer = obj + else: + raise ValueError(f"Unhandled obj type: {type(obj)}") + # `StorageClient.write()` defers to `StorageProvider.put_object()` while + # `StorageClient.upload_file()` defers to `StorageProvider.upload_file()`. + # + # Currently, only `StorageProvider.upload_file()` supports parallel uploads + # in some storage providers (e.g. boto S3 transfer manager for S3 storage providers) + # so it's often much faster. + self._storage_client.upload_file(remote_path=path, local_path=buffer) + + def put_text( + self, + obj: str, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> None: + """Write text to a given ``filepath``. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to encode the ``obj``. + Defaults to 'utf-8'. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.put_text("hello world", filepath) + """ + self.put(obj=bytes(obj, encoding=encoding), filepath=filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.exists(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + # Include directories and files. + self._storage_client.info(path=path, strict=True) + return True + except FileNotFoundError: + return False + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/dir" # or "s3://path/of/file" + >>> backend.isdir(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + # Include directories and files. + metadata = self._storage_client.info(path=path, strict=True) + return metadata.type == "directory" + except FileNotFoundError: + return False + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.isfile(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + return self._storage_client.is_file(path=path) + except FileNotFoundError: + return False + + def join_path( + self, + filepath: Union[str, Path], + *filepaths: Union[str, Path], + ) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result after concatenation. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.join_path(filepath, "another/path") + 'path/of/file/another/path' # or "s3://path/of/file/another/path" + >>> backend.join_path(filepath, "/another/path") + 'path/of/file/another/path' # or "s3://path/of/file/another/path" + """ + filepath = self._translate_filepath(filepath=filepath, translate_url=False) + if filepath.endswith("/") and not filepath.endswith("://"): + filepath = filepath[:-1] + formatted_paths = [filepath] + for path in filepaths: + formatted_path = self._translate_filepath(filepath=path) + formatted_paths.append(formatted_path.lstrip("/")) + + return "/".join(formatted_paths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str or Path): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = MSCBackend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> with backend.get_local_path(filepath) as path: + ... # do something here + """ + assert self.isfile(filepath=filepath) + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath=filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + If dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/file1" # or "s3://path/of/file1" + >>> backend.copyfile(src, dst) + 'path/of/file1' # or "s3://path/of/file1" + + >>> # dst is a directory + >>> dst = "path/of/dir" # or "s3://path/of/dir" + >>> backend.copyfile(src, dst) + 'path/of/dir/file' # or "s3://path/of/dir/file" + """ + if not self.isfile(filepath=src): + raise FileNotFoundError("src does not exist or is not a file") + if self.isdir(filepath=dst): + dst = self.join_path(dst, self._translate_filepath(filepath=src).split("/")[-1]) + if self._translate_filepath(filepath=src) == self._translate_filepath(filepath=dst): + raise SameFileError("src and dst should not be same") + + self.put(obj=self.get(filepath=src), filepath=dst) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/dir1" # or "s3://path/of/dir1" + >>> backend.copytree(src, dst) + 'path/of/dir1' # or "s3://path/of/dir1" + """ + if not self.isdir(filepath=src): + raise FileNotFoundError("src does not exist or is not a directory") + if self.exists(filepath=dst): + raise FileExistsError("dst should not exist") + + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + src_path = self.join_path(src, path) + dst_path = self.join_path(dst, path) + self.put(obj=self.get(filepath=src_path), filepath=dst_path) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Upload a local file src to dst and return the destination file. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/your/file" + >>> dst = "path/of/file1" # or "s3://path/of/file1" + >>> backend.copyfile_from_local(src, dst) + 'path/of/file1' # or "s3://path/of/file1" + + >>> # dst is a directory + >>> dst = "path/of/dir" + >>> backend.copyfile_from_local(src, dst) + 'path/of/dir/file' # or "s3://path/of/dir/file" + """ + if self.isdir(filepath=dst): + dst = self.join_path(dst, os.path.basename(src)) + + with open(src, "rb") as f: + self.put(obj=f.read(), filepath=dst) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/your/dir" + >>> dst = "path/of/dir1" # or "s3://path/of/dir1" + >>> backend.copytree_from_local(src, dst) + 'path/of/dir1' # or "s3://path/of/dir1" + """ + if self.exists(filepath=dst): + raise FileExistsError("dst should not exist") + + src = str(src) + + for cur_dir, _, files in os.walk(src): + for f in files: + src_path = os.path.join(cur_dir, f) + dst_path = self.join_path(dst, src_path.replace(src, "")) + self.copyfile_from_local(src=src_path, dst=dst_path) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/your/file" + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/file' + + >>> # dst is a directory + >>> dst = "path/of/your/dir" + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/dir/file' + """ + assert dst_type in ["file", "dir"] + # There is no good way to detect whether dst is a directory or a file, so we make dst_type required + if dst_type == "dir": + basename = os.path.basename(self._translate_filepath(filepath=src)) + if isinstance(dst, str): + dst = os.path.join(dst, basename) + else: + assert isinstance(dst, Path) + dst = dst / basename + + # Create parent directory if it doesn't exist + parent_dir = os.path.dirname(dst) + os.makedirs(parent_dir, exist_ok=True) + + try: + with open(dst, "wb") as f: + data = self.get(filepath=src) + f.write(data) + except Exception as e: + log.error(f"Failed to write file: {e}") + raise + + return dst + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + + Returns: + str: The destination directory. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/your/dir" + >>> backend.copytree_to_local(src, dst) + 'path/of/your/dir' + """ + for path in self.list_dir_or_file(dir_path=src, list_dir=False, recursive=True): + dst_path = os.path.join(dst, path) + mkdir_or_exist(os.path.dirname(dst_path)) + with open(dst_path, "wb") as f: + f.write(self.get(filepath=self.join_path(src, path))) + + return dst + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.remove(filepath) + """ + if not self.exists(filepath=filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath=filepath): + raise IsADirectoryError("filepath should be a file") + + self._storage_client.delete(path=self._translate_filepath(filepath=filepath), recursive=False) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> backend.rmtree(dir_path) + """ + self._storage_client.delete(path=self._translate_filepath(filepath=dir_path), recursive=True) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + Directly copy src to dst because MSCBackend does not support creating + a symbolic link. + + Args: + src (str or Path): A file or directory to be copied. + dst (str or Path): Copy a file or directory to dst. + + Returns: + bool: Return False because MSCBackend does not support create + a symbolic link. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/your/file" # or "s3://path/of/your/file" + >>> backend.copy_if_symlink_fails(src, dst) + False + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/your/dir" # or "s3://path/of/your/dir" + >>> backend.copy_if_symlink_fails(src, dst) + False + """ + if self.isfile(filepath=src): + self.copyfile(src=src, dst=dst) + else: + self.copytree(src=src, dst=dst) + return False + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + """List all folders in a storage location with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> list(backend.list_dir(dir_path)) + ["subdir1/", "subdir2/"] + """ + path = self._translate_filepath(filepath=dir_path).removesuffix("/") + "/" + for metadata in self._storage_client.list(path=path, include_directories=True, include_url_prefix=False): + if metadata.type == "directory": + yield metadata.key.removeprefix(path).removesuffix("/") + "/" + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + Most object stores have no concept of directories but it simulates + the directory hierarchy in the filesystem through public prefixes. + In addition, if the returned path ends with '/', it means the path + is a public prefix which is a logical directory. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + In addition, the returned path of directory will not contains the + suffix '/' which is consistent with other backends. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> # list those files and directories in current directory + >>> list(backend.list_dir_or_file(dir_path)) + ["file.txt", "subdir", "subdir/cat.png", "subdir/subsubdir/dog.jpg"] + >>> # only list files + >>> list(backend.list_dir_or_file(dir_path, list_dir=False)) + ["file.txt", "subdir/cat.png", "subdir/subsubdir/dog.jpg"] + >>> # only list directories + >>> list(backend.list_dir_or_file(dir_path, list_file=False)) + ["subdir"] + >>> # only list files ending with specified suffixes + >>> list(backend.list_dir_or_file(dir_path, suffix=".txt")) + ["file.txt"] + >>> # list all files and directory recursively + >>> list(backend.list_dir_or_file(dir_path, recursive=True)) + ["file.txt", "subdir", "subdir/cat.png", "subdir/subsubdir", "subdir/subsubdir/dog.png"] + """ + dir_path = self._translate_filepath(filepath=dir_path).removesuffix("/") + "/" + + if list_dir and suffix is not None: + raise TypeError("`list_dir` should be False when `suffix` is not None") + + if list_dir and not list_file and not recursive: + raise TypeError( + "Please use `list_dir` instead of `list_dir_or_file` " + "when you only want to list the first level directories." + ) + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + yielded_subdir_paths: set[str] = set() + # In the MSC, the `include_directories` option switches between flat and hierarchical for both files and "directories". + # + # In the Boto3Backend, however, the `recursive` option only applies to "directories" (seems like a bug). + # + # Construct directories from file paths to match the Boto3Backend behavior. + # + # If this behavior needs to be fixed, switch to `include_directories=(not recursive)` and adjust metadata processing. + for metadata in self._storage_client.list(path=dir_path, include_directories=False, include_url_prefix=False): + # Only files should be returned with `include_directories=False`, but just in case. + if metadata.type == "file": + rel_path: str = metadata.key.removeprefix(dir_path) + if list_dir: + rel_path_fragments = rel_path.split("/") + if len(rel_path_fragments) > 1: + for i in range(len(rel_path_fragments) - 1 if recursive else 1): + subdir_path = "/".join(rel_path_fragments[: i + 1]) + if subdir_path not in yielded_subdir_paths: + yielded_subdir_paths.add(subdir_path) + yield subdir_path + if list_file: + if suffix is None or rel_path.endswith(suffix): + yield rel_path + + def generate_presigned_url(self, url: str, client_method: str = "get_object", expires_in: int = 3600) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on Boto3 backend. + + Note: + Now only work on Boto3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Default: 'get_object'. + expires_in (int): expires, in seconds. Default: 3600. + + Returns: + str: Generated presigned url. + """ + raise NotImplementedError("generate_presigned_url is not supported in MSCBackend") diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py b/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py new file mode 100644 index 00000000..7ea93250 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Optional, Type, Union + +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.utils.easy_io.backends.base_backend import BaseStorageBackend +from cosmos3._src.imaginaire.utils.easy_io.backends.http_backend import HTTPBackend +from cosmos3._src.imaginaire.utils.easy_io.backends.local_backend import LocalBackend + +backends: dict = {} +prefix_to_backends: dict = {} + + +def _register_backend( + name: str, + backend: Type[BaseStorageBackend], + force: bool = False, + prefixes: Union[str, list, tuple, None] = None, +): + """Register a backend. + + Args: + name (str): The name of the registered backend. + backend (BaseStorageBackend): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + force (bool): Whether to override the backend if the name has already + been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefix + of the registered storage backend. Defaults to None. + """ + global backends, prefix_to_backends + + if not isinstance(name, str): + raise TypeError(f"the backend name should be a string, but got {type(name)}") + + if not inspect.isclass(backend): + raise TypeError(f"backend should be a class, but got {type(backend)}") + if not issubclass(backend, BaseStorageBackend): + raise TypeError(f"backend {backend} is not a subclass of BaseStorageBackend") + + if name in backends and not force: + raise ValueError( + f'{name} is already registered as a storage backend, add "force=True" if you want to override it' + ) + backends[name] = backend + + if prefixes is not None: + if isinstance(prefixes, str): + prefixes = [prefixes] + else: + assert isinstance(prefixes, (list, tuple)) + + for prefix in prefixes: + if prefix in prefix_to_backends and not force: + raise ValueError( + f'{prefix} is already registered as a storage backend, add "force=True" if you want to override it' + ) + + prefix_to_backends[prefix] = backend + + +def register_backend( + name: str, + backend: Optional[Type[BaseStorageBackend]] = None, + force: bool = False, + prefixes: Union[str, list, tuple, None] = None, +): + """Register a backend. + + Args: + name (str): The name of the registered backend. + backend (class, optional): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + When this method is used as a decorator, backend is None. + Defaults to None. + force (bool): Whether to override the backend if the name has already + been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefix + of the registered storage backend. Defaults to None. + + This method can be used as a normal method or a decorator. + + Examples: + + >>> class NewBackend(BaseStorageBackend): + ... def get(self, filepath): + ... return filepath + ... + ... def get_text(self, filepath): + ... return filepath + >>> register_backend('new', NewBackend) + + >>> @register_backend('new') + ... class NewBackend(BaseStorageBackend): + ... def get(self, filepath): + ... return filepath + ... + ... def get_text(self, filepath): + ... return filepath + """ + if backend is not None: + _register_backend(name, backend, force=force, prefixes=prefixes) + return + + def _register(backend_cls): + _register_backend(name, backend_cls, force=force, prefixes=prefixes) + return backend_cls + + return _register + + +register_backend("local", LocalBackend, prefixes="") +register_backend("http", HTTPBackend, prefixes=["http", "https"]) + +if TRAINING: + from cosmos3._src.imaginaire.utils.easy_io.backends.msc_backend import MSCBackend + + # To avoid breaking backward Compatibility, 's3' is also used as a + # prefix for MSCBackend + register_backend("s3", MSCBackend, prefixes=["s3"]) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py b/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py new file mode 100644 index 00000000..6b66d145 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py @@ -0,0 +1,1117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import warnings +from contextlib import contextmanager +from io import BytesIO, StringIO +from pathlib import Path +from typing import IO, Any, Generator, Iterator, Optional, Tuple, Union + +from cosmos3._src.imaginaire.utils.easy_io.backends import backends, prefix_to_backends +from cosmos3._src.imaginaire.utils.easy_io.file_client import FileClient +from cosmos3._src.imaginaire.utils.easy_io.handlers import file_handlers + +backend_instances: dict = {} + + +def is_filepath(filepath): + return isinstance(filepath, (str, Path)) + + +def _parse_uri_prefix(uri: Union[str, Path]) -> str: + """Parse the prefix of uri. + + Args: + uri (str or Path): Uri to be parsed that contains the file prefix. + + Examples: + >>> _parse_uri_prefix('/home/path/of/your/file') + '' + >>> _parse_uri_prefix('s3://path/of/your/file') + 's3' + >>> _parse_uri_prefix('clusterName:s3://path/of/your/file') + 's3' + + Returns: + str: Return the prefix of uri if the uri contains '://'. Otherwise, + return ''. + """ + assert is_filepath(uri) + uri = str(uri) + # if uri does not contains '://', the uri will be handled by + # LocalBackend by default + if "://" not in uri: + return "" + else: + prefix, _ = uri.split("://") + # In the case of Boto3Backend, the prefix may contain the cluster + # name like clusterName:s3://path/of/your/file + if ":" in prefix: + _, prefix = prefix.split(":") + return prefix + + +def _get_file_backend(prefix: str, backend_args: dict): + """Return a file backend based on the prefix or backend_args. + + Args: + prefix (str): Prefix of uri. + backend_args (dict): Arguments to instantiate the corresponding + backend. + """ + # backend name has a higher priority + if "backend" in backend_args: + # backend_args should not be modified + backend_args_bak = backend_args.copy() + backend_name = backend_args_bak.pop("backend") + backend = backends[backend_name](**backend_args_bak) + else: + backend = prefix_to_backends[prefix](**backend_args) + return backend + + +def set_s3_backend( + key: str = "s3:{}", + backend_args: Optional[dict] = None, +): + """register s3 backend. + + Args: + key str: The key to register the s3 backend. Defaults to s3. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + """ + global backend_instances + if backend_args is None: + backend_args = {} + backend = _get_file_backend(key, backend_args) + backend_instances[key] = backend + return backend + + +def get_file_backend( + uri: Union[str, Path, None] = None, + *, + backend_args: Optional[dict] = None, + enable_singleton: bool = False, + backend_key: Optional[str] = None, +): + """Return a file backend based on the prefix of uri or backend_args. + + Args: + uri (str or Path): Uri to be parsed that contains the file prefix. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + enable_singleton (bool): Whether to enable the singleton pattern. + If it is True, the backend created will be reused if the + signature is same with the previous one. Defaults to False. + backend_key: str: The key to register the backend. Defaults to None. + + Returns: + BaseStorageBackend: Instantiated Backend object. + + Examples: + >>> # get file backend based on the prefix of uri + >>> uri = 's3://path/of/your/file' + >>> backend = get_file_backend(uri) + >>> # get file backend based on the backend_args + >>> backend = get_file_backend(backend_args={'backend': 's3'}) + >>> # backend name has a higher priority if 'backend' in backend_args + >>> backend = get_file_backend(uri, backend_args={'backend': 's3'}) + """ + global backend_instances + if backend_key is not None: + if backend_key in backend_instances: + return backend_instances[backend_key] + + if backend_args is None: + backend_args = {} + + if uri is None and "backend" not in backend_args and backend_key is None: + raise ValueError('uri should not be None when "backend" does not exist in backend_args and backend_key is None') + + if uri is not None: + prefix = _parse_uri_prefix(uri) + else: + prefix = "" + + if enable_singleton: + + unique_key = f"{prefix}:{json.dumps(backend_args)}" + if unique_key in backend_instances: + return backend_instances[unique_key] + + backend = _get_file_backend(prefix, backend_args) + backend_instances[unique_key] = backend + if backend_key is not None: + backend_instances[backend_key] = backend + return backend + else: + backend = _get_file_backend(prefix, backend_args) + return backend + + +def size( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> filepath = 'path/of/file' + >>> size(filepath) # file containing 'hello world' + 11 + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.size(filepath) + + +def get( + filepath: Union[str, Path], + offset: Optional[int] = None, + size: Optional[int] = None, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> filepath = '/path/of/file' + >>> get(filepath) + b'hello world' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.get(filepath, offset=offset, size=size) + + +def get_text( + filepath: Union[str, Path], + encoding="utf-8", + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> filepath = '/path/of/file' + >>> get_text(filepath) + 'hello world' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.get_text(filepath, encoding) + + +def put( + obj: bytes, + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Write bytes to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Examples: + >>> filepath = '/path/of/file' + >>> put(b'hello world', filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.put(obj, filepath) + + +def put_text( + obj: str, + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Write text to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str, optional): The encoding format used to open the + ``filepath``. Defaults to 'utf-8'. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Examples: + >>> filepath = '/path/of/file' + >>> put_text('hello world', filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.put_text(obj, filepath) + + +def exists( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> filepath = '/path/of/file' + >>> exists(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.exists(filepath) + + +def isdir( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> filepath = '/path/of/dir' + >>> isdir(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.isdir(filepath) + + +def isfile( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> filepath = '/path/of/file' + >>> isfile(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.isfile(filepath) + + +def join_path( + filepath: Union[str, Path], + *filepaths: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + *filepaths (str or Path): Other paths to be concatenated. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: The result of concatenation. + + Examples: + >>> filepath1 = '/path/of/dir1' + >>> filepath2 = 'dir2' + >>> filepath3 = 'path/of/file' + >>> join_path(filepath1, filepath2, filepath3) + '/path/of/dir/dir2/path/of/file' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.join_path(filepath, *filepaths) + + +@contextmanager +def get_local_path( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Generator[Union[str, Path], None, None]: + """Download data from ``filepath`` and write the data to local path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Note: + If the ``filepath`` is a local path, just return itself and it will + not be released (removed). + + Args: + filepath (str or Path): Path to be read data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Yields: + Iterable[str]: Only yield one path. + + Examples: + >>> with get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + with backend.get_local_path(str(filepath)) as local_path: + yield local_path + + +def copyfile( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError will + be raised. + + Examples: + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> copyfile(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to '/path1/of/dir/file' + >>> copyfile(src, dst) + '/path1/of/dir/file' + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile(src, dst) + + +def copytree( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a directory + named dst and return the destination directory. + + src and dst should have the same prefix and dst must not already exist. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will be + raised. + + Examples: + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> copytree(src, dst) + '/path/of/dir2' + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree(src, dst) + + +def copyfile_from_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy a local file src to dst and return the destination file. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copyfile`. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = 's3://openmmlab/mmengine/file1' + >>> # src will be copied to 's3://openmmlab/mmengine/file1' + >>> copyfile_from_local(src, dst) + s3://openmmlab/mmengine/file1 + + >>> # dst is a directory + >>> dst = 's3://openmmlab/mmengine' + >>> # src will be copied to 's3://openmmlab/mmengine/file'' + >>> copyfile_from_local(src, dst) + 's3://openmmlab/mmengine/file' + """ + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile_from_local(src, dst) + + +def copytree_from_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a directory + named dst and return the destination directory. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copytree`. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> src = '/path/of/dir' + >>> dst = 's3://openmmlab/mmengine/dir' + >>> copyfile_from_local(src, dst) + 's3://openmmlab/mmengine/dir' + """ + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree_from_local(src, dst) + + +def copyfile_to_local( + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copyfile`. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> # dst is a file + >>> src = 's3://openmmlab/mmengine/file' + >>> dst = '/path/of/file' + >>> # src will be copied to '/path/of/file' + >>> copyfile_to_local(src, dst) + '/path/of/file' + + >>> # dst is a directory + >>> dst = '/path/of/dir' + >>> # src will be copied to '/path/of/dir/file' + >>> copyfile_to_local(src, dst) + '/path/of/dir/file' + """ + assert dst_type in ["file", "dir"] + Path(dst).parent.mkdir(parents=True, exist_ok=True) + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile_to_local(src, dst, dst_type=dst_type) + + +def copytree_to_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copytree`. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> src = 's3://openmmlab/mmengine/dir' + >>> dst = '/path/of/dir' + >>> copytree_to_local(src, dst) + '/path/of/dir' + """ + Path(dst).parent.mkdir(parents=True, exist_ok=True) + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree_to_local(src, dst) + + +def remove( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Remove a file. + + Args: + filepath (str, Path): Path to be removed. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> filepath = '/path/of/file' + >>> remove(filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.remove(filepath) + + +def rmtree( + dir_path: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Examples: + >>> dir_path = '/path/of/dir' + >>> rmtree(dir_path) + """ + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.rmtree(dir_path) + + +def copy_if_symlink_fails( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Create a symbolic link pointing to src named dst. + + If failed to create a symbolic link pointing to src, directory copy src to + dst instead. + + Args: + src (str or Path): Create a symbolic link pointing to src. + dst (str or Path): Create a symbolic link named dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + bool: Return True if successfully create a symbolic link pointing to + src. Otherwise, return False. + + Examples: + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> copy_if_symlink_fails(src, dst) + True + >>> src = '/path/of/dir' + >>> dst = '/path1/of/dir1' + >>> copy_if_symlink_fails(src, dst) + True + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copy_if_symlink_fails(src, dst) + + +def list_dir( + dir_path: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +): + """List all folders in an S3 bucket with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> dir_path = '/path/of/dir' + >>> for file_path in list_dir(dir_path): + ... print(file_path) + """ + if not dir_path.endswith("/"): + dir_path += "/" + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + + return backend.list_dir(dir_path) + + +def list_dir_or_file( + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, Tuple[str]]] = None, + recursive: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str or Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix that we are + interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the directory. + Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> dir_path = '/path/of/dir' + >>> for file_path in list_dir_or_file(dir_path): + ... print(file_path) + >>> # list those files and directories in current directory + >>> for file_path in list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + yield from backend.list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) + + +def generate_presigned_url( + url: str, + client_method: str = "get_object", + expires_in: int = 3600, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on s3 backend. + + Note: + Now only work on s3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Defaults to 'get_object'. + expires_in (int): expires, in seconds. Defaults to 3600. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: Generated presigned url. + """ + backend = get_file_backend(url, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.generate_presigned_url(url, client_method, expires_in) + + +def load( + file: Union[str, Path, IO[Any]], + file_format: Optional[str] = None, + file_client_args: Optional[dict] = None, + fast_backend: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, + **kwargs, +): + """Load data from json/yaml/pickle files. + + This method provides a unified api for loading data from serialized files. + + ``load`` supports loading data from serialized files those can be storaged + in different backends. + + Args: + file (str or :obj:`Path` or file-like object): Filename or a file-like + object. + file_format (str, optional): If not specified, the file format will be + inferred from the file extension, otherwise use the specified one. + Currently supported formats include "json", "yaml/yml" and + "pickle/pkl". + file_client_args (dict, optional): Arguments to instantiate a + FileClient. See :class:`mmengine.fileio.FileClient` for details. + Defaults to None. It will be deprecated in future. Please use + ``backend_args`` instead. + fast_backend: bool: Whether to use multiprocess. Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + New in v0.2.0. + + Examples: + >>> load('/path/of/your/file') # file is storaged in disk + >>> load('https://path/of/your/file') # file is storaged in Internet + >>> load('s3://path/of/your/file') # file is storaged in s3 + + Returns: + The content from the file. + """ + if isinstance(file, Path): + file = str(file) + if file_format is None and isinstance(file, str): + file_format = file.split(".")[-1] + # convert file_format to lower case + file_format = file_format.lower() + if file_format not in file_handlers: + raise TypeError(f"Unsupported format: {file_format}") + + if file_client_args is not None: + warnings.warn( + '"file_client_args" will be deprecated in future. Please use "backend_args" instead', + DeprecationWarning, + ) + if backend_args is not None: + raise ValueError('"file_client_args and "backend_args" cannot be set at the same time.') + + handler = file_handlers[file_format] + if isinstance(file, str): + if file_client_args is not None: + file_client = FileClient.infer_client(file_client_args, file) + file_backend = file_client + else: + file_backend = get_file_backend( + file, + backend_args=backend_args, + backend_key=backend_key, + enable_singleton=True, + ) + + if handler.str_like: + with StringIO(file_backend.get_text(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + if fast_backend: + if hasattr(file_backend, "fast_get"): + with BytesIO(file_backend.fast_get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + warnings.warn( + f"fast_backend is not supported by the backend, type {type(file_backend)} fallback to normal get" + ) + with BytesIO(file_backend.get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + with BytesIO(file_backend.get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + elif hasattr(file, "read"): + obj = handler.load_from_fileobj(file, **kwargs) + else: + raise TypeError('"file" must be a filepath str or a file-object') + return obj + + +def dump( + obj: Any, + file: Union[str, Path, IO[Any], None] = None, + file_format: Optional[str] = None, + file_client_args: Optional[dict] = None, + fast_backend: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, + **kwargs, +): + """Dump data to json/yaml/pickle strings or files. + + This method provides a unified api for dumping data as strings or to files, + and also supports custom arguments for each file format. + + ``dump`` supports dumping data as strings or to files which is saved to + different backends. + + Args: + obj (any): The python object to be dumped. + file (str or :obj:`Path` or file-like object, optional): If not + specified, then the object is dumped to a str, otherwise to a file + specified by the filename or file-like object. + file_format (str, optional): Same as :func:`load`. + file_client_args (dict, optional): Arguments to instantiate a + FileClient. See :class:`mmengine.fileio.FileClient` for details. + Defaults to None. It will be deprecated in future. Please use + ``backend_args`` instead. + fast_backend: bool: Whether to use multiprocess. Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + New in v0.2.0. + backend_key: str: The key to register the backend. Defaults to None. + + Examples: + >>> dump('hello world', '/path/of/your/file') # disk + >>> dump('hello world', 's3://path/of/your/file') # ceph or s3 + + Returns: + bool: True for success, False otherwise. + """ + if isinstance(file, Path): + file = str(file) + if file_format is None: + if isinstance(file, str): + file_format = file.split(".")[-1] + elif file is None: + raise ValueError("file_format must be specified since file is None") + # convert file_format to lower case + file_format = file_format.lower() + if file_format not in file_handlers: + raise TypeError(f"Unsupported format: {file_format}") + + if file_client_args is not None: + warnings.warn( + '"file_client_args" will be deprecated in future. Please use "backend_args" instead', + DeprecationWarning, + ) + if backend_args is not None: + raise ValueError('"file_client_args" and "backend_args" cannot be set at the same time.') + + handler = file_handlers[file_format] + if file is None: + return handler.dump_to_str(obj, **kwargs) + elif isinstance(file, str): + if file_client_args is not None: + file_client = FileClient.infer_client(file_client_args, file) + file_backend = file_client + else: + file_backend = get_file_backend( + file, + backend_args=backend_args, + backend_key=backend_key, + enable_singleton=True, + ) + + if handler.str_like: + with StringIO() as f: + handler.dump_to_fileobj(obj, f, **kwargs) + file_backend.put_text(f.getvalue(), file) + else: + with BytesIO() as f: + handler.dump_to_fileobj(obj, f, **kwargs) + if fast_backend: + if hasattr(file_backend, "fast_put"): + file_backend.fast_put(f, file) + else: + warnings.warn("fast_backend is not supported by the backend, fallback to normal put") + file_backend.put(f, file) + else: + file_backend.put(f, file) + elif hasattr(file, "write"): + handler.dump_to_fileobj(obj, file, **kwargs) + else: + raise TypeError('"file" must be a filename str or a file-object') diff --git a/cosmos3/_src/imaginaire/utils/easy_io/file_client.py b/cosmos3/_src/imaginaire/utils/easy_io/file_client.py new file mode 100644 index 00000000..b283d8bf --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/file_client.py @@ -0,0 +1,459 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Generator, Iterator, Optional, Tuple, Union + +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.utils.easy_io.backends import BaseStorageBackend, HTTPBackend, LocalBackend + + +def is_filepath(filepath): + return isinstance(filepath, (str, Path)) + + +class HardDiskBackend(LocalBackend): + """Raw hard disks storage backend.""" + + @property + def name(self): + return self.__class__.__name__ + + +class FileClient: + """A general file client to access files in different backends. + + The client loads a file or text in a specified backend from its path + and returns it as a binary or text file. There are two ways to choose a + backend, the name of backend and the prefix of path. Although both of them + can be used to choose a storage backend, ``backend`` has a higher priority + that is if they are all set, the storage backend will be chosen by the + backend argument. If they are all `None`, the disk backend will be chosen. + Note that It can also register other backend accessor with a given name, + prefixes, and backend class. In addition, We use the singleton pattern to + avoid repeated object creation. If the arguments are the same, the same + object will be returned. + + Warning: + `FileClient` will be deprecated in future. Please use io functions + in https://mmengine.readthedocs.io/en/latest/api/fileio.html#file-io + + Args: + backend (str, optional): The storage backend type. Options are "disk", + "memcached", "lmdb", "http" and "s3". Defaults to None. + prefix (str, optional): The prefix of the registered storage backend. + Options are "s3", "http", "https". Defaults to None. + + Examples: + >>> # only set backend + >>> file_client = FileClient(backend='s3') + >>> # only set prefix + >>> file_client = FileClient(prefix='s3') + >>> # set both backend and prefix but use backend to choose client + >>> file_client = FileClient(backend='s3', prefix='s3') + >>> # if the arguments are the same, the same object is returned + >>> file_client1 = FileClient(backend='s3') + >>> file_client1 is file_client + True + + Attributes: + client (:obj:`BaseStorageBackend`): The backend object. + """ + + _backends = { + "disk": HardDiskBackend, + "http": HTTPBackend, + } + + _prefix_to_backends: dict = { + "http": HTTPBackend, + "https": HTTPBackend, + } + + if TRAINING: + from cosmos3._src.imaginaire.utils.easy_io.backends.msc_backend import MSCBackend + + _backends["s3"] = MSCBackend + _backends["msc"] = MSCBackend + _prefix_to_backends["s3"] = MSCBackend + + _instances: dict = {} + + client: Any + + def __new__(cls, backend=None, prefix=None, **kwargs): + if backend is None and prefix is None: + backend = "disk" + if backend is not None and backend not in cls._backends: + raise ValueError( + f"Backend {backend} is not supported. Currently supported ones are {list(cls._backends.keys())}" + ) + if prefix is not None and prefix not in cls._prefix_to_backends: + raise ValueError( + f"prefix {prefix} is not supported. Currently supported ones are {list(cls._prefix_to_backends.keys())}" + ) + + # concatenate the arguments to a unique key for determining whether + # objects with the same arguments were created + arg_key = f"{backend}:{prefix}" + for key, value in kwargs.items(): + arg_key += f":{key}:{value}" + + # if a backend was overridden, it will create a new object + if arg_key in cls._instances: + _instance = cls._instances[arg_key] + else: + # create a new object and put it to _instance + _instance = super().__new__(cls) + if backend is not None: + _instance.client = cls._backends[backend](**kwargs) + else: + _instance.client = cls._prefix_to_backends[prefix](**kwargs) + + cls._instances[arg_key] = _instance + + return _instance + + @property + def name(self): + return self.client.name + + @property + def allow_symlink(self): + return self.client.allow_symlink + + @staticmethod + def parse_uri_prefix(uri: Union[str, Path]) -> Optional[str]: + """Parse the prefix of a uri. + + Args: + uri (str | Path): Uri to be parsed that contains the file prefix. + + Examples: + >>> FileClient.parse_uri_prefix('s3://path/of/your/file') + 's3' + + Returns: + str | None: Return the prefix of uri if the uri contains '://' else + ``None``. + """ + assert is_filepath(uri) + uri = str(uri) + if "://" not in uri: + return None + else: + prefix, _ = uri.split("://") + # In the case of MSCBackend, the prefix may contains the cluster + # name like clusterName:s3 + if ":" in prefix: + _, prefix = prefix.split(":") + return prefix + + @classmethod + def infer_client( + cls, + file_client_args: Optional[dict] = None, + uri: Optional[Union[str, Path]] = None, + ) -> "FileClient": + """Infer a suitable file client based on the URI and arguments. + + Args: + file_client_args (dict, optional): Arguments to instantiate a + FileClient. Defaults to None. + uri (str | Path, optional): Uri to be parsed that contains the file + prefix. Defaults to None. + + Examples: + >>> uri = 's3://path/of/your/file' + >>> file_client = FileClient.infer_client(uri=uri) + >>> file_client_args = {'backend': 's3'} + >>> file_client = FileClient.infer_client(file_client_args) + + Returns: + FileClient: Instantiated FileClient object. + """ + assert file_client_args is not None or uri is not None + if file_client_args is None: + file_prefix = cls.parse_uri_prefix(uri) # type: ignore + return cls(prefix=file_prefix) + else: + return cls(**file_client_args) + + @classmethod + def _register_backend(cls, name, backend, force=False, prefixes=None): + if not isinstance(name, str): + raise TypeError(f"the backend name should be a string, but got {type(name)}") + if not inspect.isclass(backend): + raise TypeError(f"backend should be a class but got {type(backend)}") + if not issubclass(backend, BaseStorageBackend): + raise TypeError(f"backend {backend} is not a subclass of BaseStorageBackend") + if not force and name in cls._backends: + raise KeyError( + f'{name} is already registered as a storage backend, add "force=True" if you want to override it' + ) + + if name in cls._backends and force: + for arg_key, instance in list(cls._instances.items()): + if isinstance(instance.client, cls._backends[name]): + cls._instances.pop(arg_key) + cls._backends[name] = backend + + if prefixes is not None: + if isinstance(prefixes, str): + prefixes = [prefixes] + else: + assert isinstance(prefixes, (list, tuple)) + for prefix in prefixes: + if prefix not in cls._prefix_to_backends: + cls._prefix_to_backends[prefix] = backend + elif (prefix in cls._prefix_to_backends) and force: + overridden_backend = cls._prefix_to_backends[prefix] + for arg_key, instance in list(cls._instances.items()): + if isinstance(instance.client, overridden_backend): + cls._instances.pop(arg_key) + else: + raise KeyError( + f"{prefix} is already registered as a storage backend," + ' add "force=True" if you want to override it' + ) + + @classmethod + def register_backend(cls, name, backend=None, force=False, prefixes=None): + """Register a backend to FileClient. + + This method can be used as a normal class method or a decorator. + + .. code-block:: python + + class NewBackend(BaseStorageBackend): + + def get(self, filepath): + return filepath + + def get_text(self, filepath): + return filepath + + FileClient.register_backend('new', NewBackend) + + or + + .. code-block:: python + + @FileClient.register_backend('new') + class NewBackend(BaseStorageBackend): + + def get(self, filepath): + return filepath + + def get_text(self, filepath): + return filepath + + Args: + name (str): The name of the registered backend. + backend (class, optional): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + When this method is used as a decorator, backend is None. + Defaults to None. + force (bool, optional): Whether to override the backend if the name + has already been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefixes + of the registered storage backend. Defaults to None. + `New in version 1.3.15.` + """ + if backend is not None: + cls._register_backend(name, backend, force=force, prefixes=prefixes) + return + + def _register(backend_cls): + cls._register_backend(name, backend_cls, force=force, prefixes=prefixes) + return backend_cls + + return _register + + def get(self, filepath: Union[str, Path]) -> Union[bytes, memoryview]: + """Read data from a given ``filepath`` with 'rb' mode. + + Note: + There are two types of return values for ``get``, one is ``bytes`` + and the other is ``memoryview``. The advantage of using memoryview + is that you can avoid copying, and if you want to convert it to + ``bytes``, you can use ``.tobytes()``. + + Args: + filepath (str or Path): Path to read data. + + Returns: + bytes | memoryview: Expected bytes object or a memory view of the + bytes object. + """ + return self.client.get(filepath) + + def get_text(self, filepath: Union[str, Path], encoding="utf-8") -> str: + """Read data from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + """ + return self.client.get_text(filepath, encoding) + + def put(self, obj: bytes, filepath: Union[str, Path]) -> None: + """Write data to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` should create a directory if the directory of ``filepath`` + does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + """ + self.client.put(obj, filepath) + + def put_text(self, obj: str, filepath: Union[str, Path]) -> None: + """Write data to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str, optional): The encoding format used to open the + `filepath`. Defaults to 'utf-8'. + """ + self.client.put_text(obj, filepath) + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str, Path): Path to be removed. + """ + self.client.remove(filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + """ + return self.client.exists(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + """ + return self.client.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + """ + return self.client.isfile(filepath) + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result of concatenation. + """ + return self.client.join_path(filepath, *filepaths) + + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + """Download data from ``filepath`` and write the data to local path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Note: + If the ``filepath`` is a local path, just return itself. + + .. warning:: + ``get_local_path`` is an experimental interface that may change in + the future. + + Args: + filepath (str or Path): Path to be read data. + + Examples: + >>> file_client = FileClient(prefix='s3') + >>> with file_client.get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + + Yields: + Iterable[str]: Only yield one path. + """ + with self.client.get_local_path(str(filepath)) as local_path: + yield local_path + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, Tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + """ + yield from self.client.list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py new file mode 100644 index 00000000..6c135233 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.json_handler import JsonHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.pickle_handler import PickleHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.registry_utils import file_handlers, register_handler +from cosmos3._src.imaginaire.utils.easy_io.handlers.yaml_handler import YamlHandler + +__all__ = [ + "BaseFileHandler", + "JsonHandler", + "PickleHandler", + "YamlHandler", + "register_handler", + "file_handlers", +] diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py new file mode 100644 index 00000000..5e5dcbca --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABCMeta, abstractmethod + + +class BaseFileHandler(metaclass=ABCMeta): + # `str_like` is a flag to indicate whether the type of file object is + # str-like object or bytes-like object. Pickle only processes bytes-like + # objects but json only processes str-like object. If it is str-like + # object, `StringIO` will be used to process the buffer. + str_like = True + + @abstractmethod + def load_from_fileobj(self, file, **kwargs): + pass + + @abstractmethod + def dump_to_fileobj(self, obj, file, **kwargs): + pass + + @abstractmethod + def dump_to_str(self, obj, **kwargs): + pass + + def load_from_path(self, filepath, mode="r", **kwargs): + with open(filepath, mode) as f: + return self.load_from_fileobj(f, **kwargs) + + def dump_to_path(self, obj, filepath, mode="w", **kwargs): + with open(filepath, mode) as f: + self.dump_to_fileobj(obj, f, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py new file mode 100644 index 00000000..269a504d --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class ByteHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file: IO[bytes], **kwargs): + file.seek(0) + # extra all bytes and return + return file.read() + + def dump_to_fileobj( + self, + obj: bytes, + file: IO[bytes], + **kwargs, + ): + # write all bytes to file + file.write(obj) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py new file mode 100644 index 00000000..307a69df --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +from io import StringIO + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class CsvHandler(BaseFileHandler): + def load_from_fileobj(self, file, **kwargs): + del kwargs + reader = csv.reader(file) + return list(reader) + + def dump_to_fileobj(self, obj, file, **kwargs): + del kwargs + writer = csv.writer(file) + if not all(isinstance(row, list) for row in obj): + raise ValueError("Each row must be a list") + writer.writerows(obj) + + def dump_to_str(self, obj, **kwargs): + del kwargs + output = StringIO() + writer = csv.writer(output) + if not all(isinstance(row, list) for row in obj): + raise ValueError("Each row must be a list") + writer.writerows(obj) + return output.getvalue() diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py new file mode 100644 index 00000000..880877b3 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gzip +import pickle +from io import BytesIO +from typing import Any + +from cosmos3._src.imaginaire.utils.easy_io.handlers.pickle_handler import PickleHandler + + +class GzipHandler(PickleHandler): + str_like = False + + def load_from_fileobj(self, file: BytesIO, **kwargs): + with gzip.GzipFile(fileobj=file, mode="rb") as f: + return pickle.load(f) + + def dump_to_fileobj(self, obj: Any, file: BytesIO, **kwargs): + with gzip.GzipFile(fileobj=file, mode="wb") as f: + pickle.dump(obj, f) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py new file mode 100644 index 00000000..2f402d3e --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO, Any, Dict, Tuple + +import imageio +import imageio.v3 as iio_v3 +import numpy as np +import torch + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class ImageioVideoHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj( + self, file: IO[bytes], format: str = "mp4", mode: str = "rgb", **kwargs + ) -> Tuple[np.ndarray, Dict[str, Any]]: + """ + Load video from a file-like object using imageio.v3 with specified format and color mode. + + Parameters: + file (IO[bytes]): A file-like object containing video data. + format (str): Format of the video file (default 'mp4'). + mode (str): Color mode of the video, 'rgb' or 'gray' (default 'rgb'). + + Returns: + tuple: A tuple containing an array of video frames and metadata about the video. + """ + file.seek(0) + + # The plugin argument in v3 replaces the format argument in v2 + plugin = kwargs.pop("plugin", "pyav") + + # Load all frames at once using v3 API + video_frames = iio_v3.imread(file, plugin=plugin, **kwargs) + + # Handle grayscale conversion if needed + if mode == "gray": + import cv2 + + if len(video_frames.shape) == 4: # (frames, height, width, channels) + gray_frames = [] + for frame in video_frames: + gray_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + gray_frame = np.expand_dims(gray_frame, axis=2) # Keep dimensions consistent + gray_frames.append(gray_frame) + video_frames = np.array(gray_frames) + + # Extract metadata + # Note: iio_v3.imread doesn't return metadata directly like v2 did + # We need to extract it separately + file.seek(0) + metadata = self._extract_metadata(file, plugin=plugin) + + return video_frames, metadata + + def _extract_metadata(self, file: IO[bytes], plugin: str = "pyav") -> Dict[str, Any]: + """ + Extract metadata from a video file. + + Parameters: + file (IO[bytes]): File-like object containing video data. + plugin (str): Plugin to use for reading. + + Returns: + dict: Video metadata. + """ + try: + # Create a generator to read frames and metadata + metadata = iio_v3.immeta(file, plugin=plugin) + + # Add some standard fields similar to v2 metadata format + if "fps" not in metadata and "duration" in metadata: + # Read the first frame to get shape information + file.seek(0) + first_frame = iio_v3.imread(file, plugin=plugin, index=0) + metadata["size"] = first_frame.shape[1::-1] # (width, height) + metadata["source_size"] = metadata["size"] + + # Create a consistent metadata structure with v2 + metadata["plugin"] = plugin + if "codec" not in metadata: + metadata["codec"] = "unknown" + if "pix_fmt" not in metadata: + metadata["pix_fmt"] = "unknown" + + # Calculate nframes if possible + if "fps" in metadata and "duration" in metadata: + metadata["nframes"] = int(metadata["fps"] * metadata["duration"]) + else: + metadata["nframes"] = float("inf") + + return metadata + + except Exception as e: + # Fallback to basic metadata + return { + "plugin": plugin, + "nframes": float("inf"), + "codec": "unknown", + "fps": 30.0, # Default values + "duration": 0, + "size": (0, 0), + } + + def dump_to_fileobj( + self, + obj: np.ndarray | torch.Tensor, + file: IO[bytes], + format: str = "mp4", # pylint: disable=redefined-builtin + fps: int = 17, + quality: int = 5, + ffmpeg_params=None, + **kwargs, + ): + """ + Save an array of video frames to a file-like object using imageio. + + Parameters: + obj (Union[np.ndarray, torch.Tensor]): An array of frames to be saved as video. + file (IO[bytes]): A file-like object to which the video data will be written. + format (str): Format of the video file (default 'mp4'). + fps (int): Frames per second of the output video (default 17). + quality (int): Quality of the video (0-10, default 5). + ffmpeg_params (list): Additional parameters to pass to ffmpeg. + + """ + if isinstance(obj, torch.Tensor): + assert obj.dtype == torch.uint8, "Tensor must be of type uint8" + obj = obj.cpu().numpy() + h, w = obj.shape[1:-1] + + # Default ffmpeg params that ensure width and height are set + default_ffmpeg_params = ["-s", f"{w}x{h}"] + + # Use provided ffmpeg_params if any, otherwise use defaults + final_ffmpeg_params = ffmpeg_params if ffmpeg_params is not None else default_ffmpeg_params + + mimsave_kwargs = { + "fps": fps, + "quality": quality, + "macro_block_size": 1, + "ffmpeg_params": final_ffmpeg_params, + "output_params": ["-f", "mp4"], + } + # Update with any other kwargs + mimsave_kwargs.update(kwargs) + log.debug(f"mimsave_kwargs: {mimsave_kwargs}") + + imageio.mimsave(file, obj, format, **mimsave_kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py new file mode 100644 index 00000000..360cf2e1 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import numpy as np + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +def set_default(obj): + """Set default json values for non-serializable values. + + It helps convert ``set``, ``range`` and ``np.ndarray`` data types to list. + It also converts ``np.generic`` (including ``np.int32``, ``np.float32``, + etc.) into plain numbers of plain python built-in types. + """ + if isinstance(obj, (set, range)): + return list(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, np.generic): + return obj.item() + raise TypeError(f"{type(obj)} is unsupported for json dump") + + +class JsonHandler(BaseFileHandler): + def load_from_fileobj(self, file): + return json.load(file) + + def dump_to_fileobj(self, obj, file, **kwargs): + kwargs.setdefault("default", set_default) + json.dump(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("default", set_default) + return json.dumps(obj, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py new file mode 100644 index 00000000..cac37184 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from typing import IO + +import numpy as np + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +def set_default(obj): + """Set default json values for non-serializable values. + + It helps convert ``set``, ``range`` and ``np.ndarray`` data types to list. + It also converts ``np.generic`` (including ``np.int32``, ``np.float32``, + etc.) into plain numbers of plain python built-in types. + """ + if isinstance(obj, (set, range)): + return list(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, np.generic): + return obj.item() + raise TypeError(f"{type(obj)} is unsupported for json dump") + + +class JsonlHandler(BaseFileHandler): + """Handler for JSON lines (JSONL) files.""" + + def load_from_fileobj(self, file: IO[bytes]): + """Load JSON objects from a newline-delimited JSON (JSONL) file object. + + Returns: + A list of Python objects loaded from each JSON line. + """ + data = [] + for line in file: + line = line.strip() + if not line: + continue # skip empty lines if any + data.append(json.loads(line)) + return data + + def dump_to_fileobj(self, obj: IO[bytes], file, **kwargs): + """Dump a list of objects to a newline-delimited JSON (JSONL) file object. + + Args: + obj: A list (or iterable) of objects to dump line by line. + """ + kwargs.setdefault("default", set_default) + for item in obj: + file.write(json.dumps(item, **kwargs) + "\n") + + def dump_to_str(self, obj, **kwargs): + """Dump a list of objects to a newline-delimited JSON (JSONL) string.""" + kwargs.setdefault("default", set_default) + lines = [json.dumps(item, **kwargs) for item in obj] + return "\n".join(lines) + + +if __name__ == "__main__": + from cosmos3._src.imaginaire.utils.easy_io import easy_io + + easy_io.dump([1, 2, 3], "test.jsonl", file_format="jsonl") + print(easy_io.load("test.jsonl")) + easy_io.dump([{"key1": 1, "key2": 2}, {"key1": 3, "key2": 4}], "test.jsonl", file_format="jsonl") + print(easy_io.load("test.jsonl")) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py new file mode 100644 index 00000000..3db6308e --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io import BytesIO +from typing import IO, Any + +import numpy as np + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class NumpyHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file: IO[bytes], **kwargs) -> Any: + """ + Load a NumPy array from a file-like object. + + Parameters: + file (IO[bytes]): The file-like object containing the NumPy array data. + **kwargs: Additional keyword arguments passed to `np.load`. + + Returns: + numpy.ndarray: The loaded NumPy array. + """ + return np.load(file, **kwargs) + + def load_from_path(self, filepath: str, **kwargs) -> Any: + """ + Load a NumPy array from a file path. + + Parameters: + filepath (str): The path to the file to load. + **kwargs: Additional keyword arguments passed to `np.load`. + + Returns: + numpy.ndarray: The loaded NumPy array. + """ + return super().load_from_path(filepath, mode="rb", **kwargs) + + def dump_to_str(self, obj: np.ndarray, **kwargs) -> str: + """ + Serialize a NumPy array to a string in binary format. + + Parameters: + obj (np.ndarray): The NumPy array to serialize. + **kwargs: Additional keyword arguments passed to `np.save`. + + Returns: + str: The serialized NumPy array as a string. + """ + with BytesIO() as f: + np.save(f, obj, **kwargs) + return f.getvalue() + + def dump_to_fileobj(self, obj: np.ndarray, file: IO[bytes], **kwargs): + """ + Dump a NumPy array to a file-like object. + + Parameters: + obj (np.ndarray): The NumPy array to dump. + file (IO[bytes]): The file-like object to which the array is dumped. + **kwargs: Additional keyword arguments passed to `np.save`. + """ + np.save(file, obj, **kwargs) + + def dump_to_path(self, obj: np.ndarray, filepath: str, **kwargs): + """ + Dump a NumPy array to a file path. + + Parameters: + obj (np.ndarray): The NumPy array to dump. + filepath (str): The file path where the array should be saved. + **kwargs: Additional keyword arguments passed to `np.save`. + """ + with open(filepath, "wb") as f: + np.save(f, obj, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py new file mode 100644 index 00000000..8d0b1395 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler # isort:skip + + +class PandasHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return pd.read_csv(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + obj.to_csv(file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError("PandasHandler does not support dumping to str") diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py new file mode 100644 index 00000000..6f49c0e2 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +from io import BytesIO +from typing import Any + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class PickleHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file: BytesIO, **kwargs): + return pickle.load(file, **kwargs) + + def load_from_path(self, filepath, **kwargs): + return super().load_from_path(filepath, mode="rb", **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("protocol", 2) + return pickle.dumps(obj, **kwargs) + + def dump_to_fileobj(self, obj: Any, file: BytesIO, **kwargs): + kwargs.setdefault("protocol", 2) + pickle.dump(obj, file, **kwargs) + + def dump_to_path(self, obj, filepath, **kwargs): + with open(filepath, "wb") as f: + pickle.dump(obj, f, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py new file mode 100644 index 00000000..54a7f753 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO, Optional, Tuple, Union + +import numpy as np + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + +try: + from PIL import Image +except ImportError: + Image = None + + +class PILHandler(BaseFileHandler): + format: str + str_like = False + + def load_from_fileobj( + self, + file: IO[bytes], + fmt: str = "pil", + size: Optional[Union[int, Tuple[int, int]]] = None, + **kwargs, + ): + """ + Load an image from a file-like object and return it in a specified format. + + Args: + file (IO[bytes]): A file-like object containing the image data. + fmt (str): The format to convert the image into. Options are \ + 'numpy', 'np', 'npy', 'type' (all return numpy arrays), \ + 'pil' (returns PIL Image), 'th', 'torch' (returns a torch tensor). + size (Optional[Union[int, Tuple[int, int]]]): The new size of the image as a single integer \ + or a tuple of (width, height). If specified, the image is resized accordingly. + **kwargs: Additional keyword arguments that can be passed to conversion functions. + + Returns: + Image data in the format specified by `fmt`. + + Raises: + IOError: If the image cannot be loaded or processed. + ValueError: If the specified format is unsupported. + """ + try: + img = Image.open(file) + img.load() # Explicitly load the image data + if size is not None: + if isinstance(size, int): + size = ( + size, + size, + ) # create a tuple if only one integer is provided + img = img.resize(size, Image.ANTIALIAS) + + # Return the image in the requested format + if fmt in ["numpy", "np", "npy"]: + return np.array(img, **kwargs) + if fmt == "pil": + return img + if fmt in ["th", "torch"]: + import torch + + # Convert to tensor + img_tensor = torch.from_numpy(np.array(img, **kwargs)) + # Convert image from HxWxC to CxHxW + if img_tensor.ndim == 3: + img_tensor = img_tensor.permute(2, 0, 1) + return img_tensor + raise ValueError( + "Unsupported format. Supported formats are 'numpy', 'np', 'npy', 'pil', 'th', and 'torch'." + ) + except Exception as e: + raise IOError(f"Unable to load image: {e}") from e + + def dump_to_fileobj(self, obj, file: IO[bytes], **kwargs): + if "format" not in kwargs: + kwargs["format"] = self.format + kwargs["format"] = "JPEG" if self.format.lower() == "jpg" else self.format.upper() + obj.save(file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py new file mode 100644 index 00000000..3ef04823 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.byte_handler import ByteHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.csv_handler import CsvHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.gzip_handler import GzipHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.imageio_video_handler import ImageioVideoHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.json_handler import JsonHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.jsonl_handler import JsonlHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.np_handler import NumpyHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.pickle_handler import PickleHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.pil_handler import PILHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.tarfile_handler import TarHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.torch_handler import TorchHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.torchjit_handler import TorchJitHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.txt_handler import TxtHandler +from cosmos3._src.imaginaire.utils.easy_io.handlers.yaml_handler import YamlHandler + +file_handlers = { + "json": JsonHandler(), + "yaml": YamlHandler(), + "yml": YamlHandler(), + "pickle": PickleHandler(), + "pkl": PickleHandler(), + "tar": TarHandler(), + "jit": TorchJitHandler(), + "npy": NumpyHandler(), + "txt": TxtHandler(), + "csv": CsvHandler(), + "gz": GzipHandler(), + "jsonl": JsonlHandler(), + "byte": ByteHandler(), +} + +if TRAINING: + from cosmos3._src.imaginaire.utils.easy_io.handlers.pandas_handler import PandasHandler + + file_handlers["pandas"] = PandasHandler() + +for torch_type in ["pt", "pth", "ckpt"]: + file_handlers[torch_type] = TorchHandler() +for img_type in ["jpg", "jpeg", "png", "bmp", "gif"]: + file_handlers[img_type] = PILHandler() + file_handlers[img_type].format = img_type +try: + from cosmos3._src.imaginaire.utils.easy_io.handlers.trimesh_handler import TrimeshHandler + + for mesh_type in ["ply", "stl", "obj", "glb"]: + file_handlers[mesh_type] = TrimeshHandler() + file_handlers[mesh_type].format = mesh_type +except ImportError: + pass +for video_type in ["mp4", "avi", "mov", "webm", "flv", "wmv"]: + file_handlers[video_type] = ImageioVideoHandler() + + +def _register_handler(handler, file_formats): + """Register a handler for some file extensions. + + Args: + handler (:obj:`BaseFileHandler`): Handler to be registered. + file_formats (str or list[str]): File formats to be handled by this + handler. + """ + if not isinstance(handler, BaseFileHandler): + raise TypeError(f"handler must be a child of BaseFileHandler, not {type(handler)}") + if isinstance(file_formats, str): + file_formats = [file_formats] + if not all([isinstance(item, str) for item in file_formats]): + raise TypeError("file_formats must be a str or a list of str") + for ext in file_formats: + file_handlers[ext] = handler + + +def register_handler(file_formats, **kwargs): + def wrap(cls): + _register_handler(cls(**kwargs), file_formats) + return cls + + return wrap diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py new file mode 100644 index 00000000..973146ad --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tarfile + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class TarHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, mode="r|*", **kwargs): + return tarfile.open(fileobj=file, mode=mode, **kwargs) + + def load_from_path(self, filepath, mode="r|*", **kwargs): + return tarfile.open(filepath, mode=mode, **kwargs) + + def dump_to_fileobj(self, obj, file, mode="w", **kwargs): + with tarfile.open(fileobj=file, mode=mode) as tar: + tar.add(obj, **kwargs) + + def dump_to_path(self, obj, filepath, mode="w", **kwargs): + with tarfile.open(filepath, mode=mode) as tar: + tar.add(obj, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py new file mode 100644 index 00000000..8cd77e53 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import torch +except ImportError: + torch = None + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class TorchHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return torch.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + torch.save(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py new file mode 100644 index 00000000..e5eefe4f --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import torch +except ImportError: + torch = None + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class TorchJitHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return torch.jit.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + torch.jit.save(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py new file mode 100644 index 00000000..0af255a6 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO + +import trimesh + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class TrimeshHandler(BaseFileHandler): + format: str + str_like = False + + def load_from_fileobj(self, file: IO[bytes], **kwargs) -> trimesh.Trimesh: + file = trimesh.load(file_obj=file, file_type=self.format) + return file + + def dump_to_fileobj(self, obj, file: IO[bytes], **kwargs): + obj.export(file_obj=file, file_type=self.format) + return file + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py new file mode 100644 index 00000000..c8d6d740 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler + + +class TxtHandler(BaseFileHandler): + def load_from_fileobj(self, file, **kwargs): + del kwargs + return file.read() + + def dump_to_fileobj(self, obj, file, **kwargs): + del kwargs + if not isinstance(obj, str): + obj = str(obj) + file.write(obj) + + def dump_to_str(self, obj, **kwargs): + del kwargs + if not isinstance(obj, str): + obj = str(obj) + return obj diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py b/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py new file mode 100644 index 00000000..07d6a8fd --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml + +try: + from yaml import CDumper as Dumper # type: ignore + from yaml import CLoader as Loader # type: ignore +except ImportError: + from yaml import Dumper, Loader # type: ignore + +from cosmos3._src.imaginaire.utils.easy_io.handlers.base import BaseFileHandler # isort:skip + + +class YamlHandler(BaseFileHandler): + def load_from_fileobj(self, file, **kwargs): + kwargs.setdefault("Loader", Loader) + return yaml.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + kwargs.setdefault("Dumper", Dumper) + yaml.dump(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("Dumper", Dumper) + return yaml.dump(obj, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/ema.py b/cosmos3/_src/imaginaire/utils/ema.py new file mode 100644 index 00000000..67810acb --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/ema.py @@ -0,0 +1,366 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from contextlib import contextmanager, nullcontext +from typing import TYPE_CHECKING, Any, Generator, List, Optional, Union + +import numpy as np +import torch + +try: + from megatron.core import parallel_state + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + +from cosmos3._src.imaginaire.utils import distributed, log + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.model import ImaginaireModel + + +class FastEmaModelUpdater: + """ + This class is used to update target model~(EMA) given source model~(regular model) and beta. + The method interaface mimic :class:`EMAModelTracker` and :class:`PowerEMATracker`. + Different from two classes, this class does not maintain the EMA model weights as buffers. It expects the user to have two module with same architecture and weights shape. + The class is proposed to work with FSDP model where above two classes are not working as expected. Besides, it is strange to claim model weights as buffers and do unnecessary name changing in :class:`EMAModelTracker` and :class:`PowerEMATracker`. Moeving forward, we should use this class instead of above two classes. + """ + + def __init__(self): + # Flag to indicate whether the cache is taken or not. Useful to avoid cache overwrite + self.is_cached = False + + def update_average(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module, beta: float = 0.9999) -> None: + target_list = [] + source_list = [] + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + assert tgt_params.dtype == torch.float32, ( + f"EMA model only works in FP32 dtype, got {tgt_params.dtype} instead." + ) + target_list.append(tgt_params) + source_list.append(src_params.data) + torch._foreach_mul_(target_list, beta) + torch._foreach_add_(target_list, source_list, alpha=1.0 - beta) + + def copy_to(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module) -> None: + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + tgt_params.data.copy_(src_params.data) + + def cache(self, parameters: Any, is_cpu: bool = False) -> None: + """Save the current parameters for restoring later. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be temporarily stored. + """ + assert self.is_cached is False, "EMA cache is already taken. Did you forget to restore it?" + device = "cpu" if is_cpu else "cuda" + self.collected_params = [param.clone().to(device) for param in parameters] + self.is_cached = True + + def restore(self, parameters: Any) -> None: + """Restore the parameters in self.collected_params. + + Useful to validate the model with EMA parameters without affecting the + original optimization process. Store the parameters before copy_to(). + After validation (or model saving), use this to restore the former parameters. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be updated with the stored parameters. + """ + assert self.is_cached, "EMA cache is not taken yet." + for c_param, param in zip(self.collected_params, parameters, strict=False): + param.data.copy_(c_param.data.type_as(param.data)) + self.collected_params = [] + # Release the cache after we call restore + self.is_cached = False + + +def get_buffer_name(param_name: str, torch_compile_buffer_renaming: bool = False) -> str: + """ + This function creates buffer name used by EMA from parameter's name + + Args: + param_name (str): Model's parameter name + Returns: + buffer_name (str): buffer name to be used for given parameter name + """ + + buffer_name = param_name.replace(".", "-") + + if torch_compile_buffer_renaming: + # torch.compile() adds _orig_mod to state dict names, this way we get original name + buffer_name = buffer_name.replace("_orig_mod-", "") + + return buffer_name + + +class EMAModelTracker(torch.nn.Module): + """This is a class to track the EMA model weights. + + The EMA weights are registered as buffers, which are extractable as state dicts. The names follow those of the + regular weights, except all "." are replaced with "-" (limitation of register_buffer()). This is similar to SDXL's + implementation of EMA. There are no optimizable parameters. + TODO(snah): multi-EMA weights. + + Attributes: + collected_params (list): temporarily stores the regular weights while in EMA mode. + beta (float): EMA decay rate. (default: 0.9999). + torch_compile_buffer_renaming (bool): whether to remove '_orig_mod-' from buffer names when torch.compile is used + """ + + def __init__(self, model: ImaginaireModel, beta: float = 0.9999, torch_compile_buffer_renaming: bool = False): + """Constructor of the EMA model weight tracker. + + Args: + model (ImaginaireModel): The PyTorch model. + beta (float): EMA decay rate. (default: 0.9999). + """ + super().__init__() + self.torch_compile_buffer_renaming: bool = torch_compile_buffer_renaming + if not 0.0 <= beta <= 1.0: + raise ValueError("Decay must be between 0 and 1") + self.beta = beta + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + self.register_buffer(buffer_name, param.clone().detach().data) + self.collected_params = [] + # Flag to indicate whether the cache is taken or not. Useful to avoid cache overwrite + self.is_cached = False + + @torch.no_grad() + def update_average(self, model: ImaginaireModel, iteration: Optional[int] = None) -> None: + del iteration + target_list = [] + source_list = [] + ema_buffers = self.state_dict() + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + buffer = ema_buffers[buffer_name] + assert buffer.dtype == torch.float32, f"EMA model only works in FP32 dtype, got {buffer.dtype} instead." + target_list.append(buffer) + source_list.append(param.data) + torch._foreach_mul_(target_list, self.beta) + torch._foreach_add_(target_list, source_list, alpha=1.0 - self.beta) + + def copy_to(self, model: ImaginaireModel) -> None: + ema_buffers = self.state_dict() + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + buffer = ema_buffers[buffer_name] + param.data.copy_(buffer.data) + + def cache(self, parameters: Any, is_cpu: bool = False) -> None: + """Save the current parameters for restoring later. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be temporarily stored. + """ + assert self.is_cached is False, "EMA cache is already taken. Did you forget to restore it?" + device = "cpu" if is_cpu else "cuda" + self.collected_params = [param.clone().to(device) for param in parameters] + self.is_cached = True + + def restore(self, parameters: Any) -> None: + """Restore the parameters in self.collected_params. + + Useful to validate the model with EMA parameters without affecting the + original optimization process. Store the parameters before copy_to(). + After validation (or model saving), use this to restore the former parameters. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be updated with the stored parameters. + """ + assert self.is_cached, "EMA cache is not taken yet." + for c_param, param in zip(self.collected_params, parameters, strict=False): + param.data.copy_(c_param.data.type_as(param.data)) + self.collected_params = [] + # Release the cache after we call restore + self.is_cached = False + + @classmethod + def initialize_multi_rank_ema( + cls, model: torch.nn.Module, rate: Union[float, List[float]], num: int = 1, enabled: bool = True + ) -> Optional[EMAModelTracker]: + """ + Class method to initialize per rank EMA Model Tracker with different rate. + Each rank will have a different rate based on the given configuration, resulting in different EMA weights. + + Args: + model (torch.nn.Module): The neural network model to be tracked. + rate (Union[float, List[float]]): The decay rate(s) for the EMA. If a list is provided, + it corresponds to rates for different ranks. + num (int, optional): The number of leading ranks to consider for different rates. + Defaults to 1. + enabled (bool, optional): Flag to enable or disable the creation of the tracker. + If False, returns None. Defaults to True. + + Returns: + Optional[EMAModelTracker]: An instance of EMAModelTracker if enabled, otherwise None. + + Example: + >>> model = torch.nn.Linear(10, 2) + >>> tracker = EMAModelTracker.initialize_ema_from_settings(model, rate=[0.1, 0.2], num=2) + >>> print(tracker) + + Notes: + If `rate` is a list and the current rank is less than `num`, the rate for the current rank + is used. If the current rank exceeds `num`, the first rate in the list is used by default. + """ + if not enabled: + return None + if USE_MEGATRON and parallel_state.is_initialized(): + cur_dp_rank = parallel_state.get_data_parallel_rank(with_context_parallel=True) + log.critical(f"using MCore parallel_state for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + log.warning("It should not used together with FSDP!") + else: + cur_dp_rank = distributed.get_rank() + log.critical(f"using torch.distributed for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + rate = rate if isinstance(rate, list) else [rate] + num = min(num, len(rate)) + rate = rate[cur_dp_rank] if cur_dp_rank < num else rate[0] + if cur_dp_rank < num: + print(f"EMAModelTracker: rank {cur_dp_rank}, rate {rate}") + return cls(model, rate) + + +class PowerEMATracker(EMAModelTracker): + def __init__(self, model: ImaginaireModel, s: float = 0.1, torch_compile_buffer_renaming: bool = False): + """Constructor of the EMA model weight tracker. + + Args: + model (ImaginaireModel): The PyTorch model. + s (float): EMA decay rate. See EDM2 paper + torch_compile_buffer_renaming (bool): whether to remove '_orig_mod-' from buffer names when torch.compile is used + """ + super().__init__(model=model, beta=0.0, torch_compile_buffer_renaming=torch_compile_buffer_renaming) + self.exp = np.roots([1, 7, 16 - s**-2, 12 - s**-2]).real.max() + + @torch.no_grad() + def update_average(self, model: ImaginaireModel, iteration: Optional[int] = None) -> None: + if iteration == 0: + beta = 0.0 + else: + i = iteration + 1 + beta = (1 - 1 / i) ** (self.exp + 1) + self.beta = beta + + super().update_average(model, iteration) + + @classmethod + def initialize_multi_rank_ema( + cls, model: torch.nn.Module, rate: float, num: int, enabled: bool = True + ) -> Optional[PowerEMATracker]: + """ + Class method to initialize per rank EMA Model Tracker with different rate. + Each rank will have a different rate based on the given configuration, resulting in different EMA weights. + + Args: + model (torch.nn.Module): The neural network model for which the EMA tracker is being set up. + num (int): The number of ranks for which the rate adjustment is applied. Beyond this, the rate remains unchanged. + rate (float): The base decay rate for the EMA calculation. + enabled (bool, optional): Flag to enable or disable the initialization of the tracker. If False, returns None. + Defaults to True. + + Returns: + Optional[PowerEMATracker]: An instance of PowerEMATracker with adjusted rate if enabled, otherwise None. + + Raises: + None + + Example: + >>> model = torch.nn.Linear(10, 2) + >>> tracker = PowerEMATracker.initialize_multi_rank_ema(model, num=3, rate=0.99) + >>> print(tracker) + + Notes: + The decay rate is modified by dividing it by 2 raised to the power of the rank for each rank less than `num`. + If the rank is greater than or equal to `num`, the base rate is used without modification. This approach + allows higher ranked processes to have a less aggressive decay, potentially reflecting their delayed synchronization + in a distributed training scenario. + """ + if not enabled: + return None + if USE_MEGATRON and parallel_state.is_initialized(): + cur_dp_rank = parallel_state.get_data_parallel_rank(with_context_parallel=True) + log.critical(f"using MCore parallel_state for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + log.warning("It should not used together with FSDP!") + else: + cur_dp_rank = distributed.get_rank() + log.critical(f"using torch.distributed for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + + divider = 2**cur_dp_rank if cur_dp_rank < num else 1 + if cur_dp_rank < num: + print(f"PowerEMATracker: rank {cur_dp_rank}, rate {rate / divider}") + return cls(model, rate / divider) + + +@contextmanager +def ema_scope(model: ImaginaireModel, enabled: bool = False, context: str | None = None) -> Generator[None, None, None]: + """Context manager for switching between regular and EMA model weights. + + This function is a dispatcher that handles two main cases: + 1. If the model has its own `ema_scope` method, it will be used. + This allows models to define custom EMA logic (e.g., for FSDP). + 2. If not, it falls back to a generic mechanism that expects the model + to have a `.ema` attribute containing an EMA tracker object. + + Args: + model (ImaginaireModel): The PyTorch model. + enabled (bool): Whether switching to EMA weights is enabled (default: False). + context (str | None): A logging context string, passed to the model's ema_scope if used. + """ + + def scope_function(): + if enabled: + has_custom_scope = hasattr(model, "ema_scope") and callable(model.ema_scope) + has_generic_ema = hasattr(model, "ema") and isinstance( + model.ema, (FastEmaModelUpdater, EMAModelTracker, PowerEMATracker) + ) + assert has_custom_scope or has_generic_ema + + if has_custom_scope: + return model.ema_scope(context=context) + else: + return ema_scope_generic(model) + else: + return nullcontext() + + with scope_function(): + yield + + +@contextmanager +def ema_scope_generic(model: ImaginaireModel) -> Generator[None, None, None]: + """Generic context manager for switching between regular and EMA model weights. + + Args: + model (ImaginaireModel): The PyTorch model, which must have a `.ema` attribute. + """ + model.ema.cache(model.parameters()) + model.ema.copy_to(model) + + log.info("EMA: switched to EMA weights.") + try: + yield + finally: + model.ema.restore(model.parameters()) + log.info("EMA: restored regular weights.") diff --git a/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py b/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py new file mode 100644 index 00000000..612f45a9 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class EmbeddingConcatStrategy(str, Enum): + FULL_CONCAT = "full_concat" # Concatenate embeddings all layers + MEAN_POOLING = "mean_pooling" # Average pool embeddings all layers + POOL_EVERY_N_LAYERS_AND_CONCAT = "pool_every_n_layers_and_concat" # Pool every n layers and concatenatenate + + def __str__(self) -> str: + return self.value diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py b/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py new file mode 100644 index 00000000..e39041ac --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.utils.env_parsers.env_parser import EnvParser +from cosmos3._src.imaginaire.utils.validator import String + + +class CredentialEnvParser(EnvParser): + APP_ENV = String(default="") + PROD_FT_AWS_CREDS_ACCESS_KEY_ID = String(default="") + PROD_FT_AWS_CREDS_SECRET_ACCESS_KEY = String(default="") + PROD_FT_AWS_CREDS_ENDPOINT_URL = String(default="https://s3.us-west-2.amazonaws.com") + PROD_FT_AWS_CREDS_REGION_NAME = String(default="us-west-2") + + PROD_S3_CHECKPOINT_ACCESS_KEY_ID = String(default="") + PROD_S3_CHECKPOINT_SECRET_ACCESS_KEY = String(default="") + PROD_S3_CHECKPOINT_ENDPOINT_URL = String(default="") + PROD_S3_CHECKPOINT_REGION_NAME = String(default="") + + PROD_GCP_CHECKPOINT_ACCESS_KEY_ID = String(default="") + PROD_GCP_CHECKPOINT_SECRET_ACCESS_KEY = String(default="") + PROD_GCP_CHECKPOINT_ENDPOINT_URL = String(default="") + PROD_GCP_CHECKPOINT_REGION_NAME = String(default="") + + PROD_PDX_BENCHMARK_ACCESS_KEY_ID = String(default="") + PROD_PDX_BENCHMARK_SECRET_ACCESS_KEY = String(default="") + PROD_PDX_BENCHMARK_ENDPOINT_URL = String(default="") + PROD_PDX_BENCHMARK_REGION_NAME = String(default="") + + PROD_TEAM_DIR_ACCESS_KEY_ID = String(default="") + PROD_TEAM_DIR_SECRET_ACCESS_KEY = String(default="") + PROD_TEAM_DIR_ENDPOINT_URL = String(default="") + PROD_TEAM_DIR_REGION_NAME = String(default="") + + PICASSO_AUTH_MODEL_REGISTRY_API_KEY = String(default="") + PICASSO_API_ENDPOINT_URL = String(default="https://invalid") + + +CRED_ENVS = CredentialEnvParser() +CRED_ENVS_DICT = { + "PROD_FT_AWS_CREDS": { + "aws_access_key_id": CRED_ENVS.PROD_FT_AWS_CREDS_ACCESS_KEY_ID, + "aws_secret_access_key": CRED_ENVS.PROD_FT_AWS_CREDS_SECRET_ACCESS_KEY, + "endpoint_url": CRED_ENVS.PROD_FT_AWS_CREDS_ENDPOINT_URL, + "region_name": CRED_ENVS.PROD_FT_AWS_CREDS_REGION_NAME, + }, + "PROD_S3_CHECKPOINT": { + "aws_access_key_id": CRED_ENVS.PROD_S3_CHECKPOINT_ACCESS_KEY_ID, + "aws_secret_access_key": CRED_ENVS.PROD_S3_CHECKPOINT_SECRET_ACCESS_KEY, + "endpoint_url": CRED_ENVS.PROD_S3_CHECKPOINT_ENDPOINT_URL, + "region_name": CRED_ENVS.PROD_S3_CHECKPOINT_REGION_NAME, + }, + "PROD_GCP_CHECKPOINT": { + "aws_access_key_id": CRED_ENVS.PROD_GCP_CHECKPOINT_ACCESS_KEY_ID, + "aws_secret_access_key": CRED_ENVS.PROD_GCP_CHECKPOINT_SECRET_ACCESS_KEY, + "endpoint_url": CRED_ENVS.PROD_GCP_CHECKPOINT_ENDPOINT_URL, + "region_name": CRED_ENVS.PROD_GCP_CHECKPOINT_REGION_NAME, + }, + "PROD_PDX_BENCHMARK": { + "aws_access_key_id": CRED_ENVS.PROD_PDX_BENCHMARK_ACCESS_KEY_ID, + "aws_secret_access_key": CRED_ENVS.PROD_PDX_BENCHMARK_SECRET_ACCESS_KEY, + "endpoint_url": CRED_ENVS.PROD_PDX_BENCHMARK_ENDPOINT_URL, + "region_name": CRED_ENVS.PROD_PDX_BENCHMARK_REGION_NAME, + }, + "PROD_TEAM_DIR": { + "aws_access_key_id": CRED_ENVS.PROD_TEAM_DIR_ACCESS_KEY_ID, + "aws_secret_access_key": CRED_ENVS.PROD_TEAM_DIR_SECRET_ACCESS_KEY, + "endpoint_url": CRED_ENVS.PROD_TEAM_DIR_ENDPOINT_URL, + "region_name": CRED_ENVS.PROD_TEAM_DIR_REGION_NAME, + }, +} diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py b/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py new file mode 100644 index 00000000..fd369dba --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.utils.env_parsers.env_parser import EnvParser +from cosmos3._src.imaginaire.utils.validator import Bool, String + + +class CustomizationEnvParser(EnvParser): + FLEET_FUNCTION = Bool(default=False) + CUSTOMIZATION_TYPE = String(default="") + DEBUG_SKIP_CUSTOMIZATION_DOWNLOAD = Bool(default=False) + FT_AWS_ACCESS_KEY_ID = String(default="") + FT_AWS_SECRET_ACCESS_KEY = String(default="") + FT_AWS_REGION_NAME = String(default="") + FT_AWS_GATEWAY_URL = String(default="") + LAMBDA_STAGE = String(default="prod") + + +CUSTOMIZATION_ENVS = CustomizationEnvParser() diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py b/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py new file mode 100644 index 00000000..36c277f6 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import json +import os + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.validator import JsonDict, Validator + +""" +Base class for parsing environment variables using validators. +Class will go through its list of validators and retrieve values from same named environment variables. +Validators provide: +- default value +- typed parsing +- enforments of mandatory values + +Additionally the environment variables can be passed as single base64 encoded string. + +we cannot enforce that a component isn't directly using the environment variables. +so evaluation of params should throw error to make sure actual env var is correct. +""" + + +class EnvParser: + def __init__(self, b64_str=None): + if b64_str: + log.critical(f"b64_str recieved: {b64_str}") + self.from_b64(b64_str) + else: + self.from_env() + + def from_env(self): + validators = self.get_val_dict() + for key in validators.keys(): + val = os.getenv(key.upper()) + # log.debug(f"getting env var {key.upper()}: {val}") + if val: + setattr(self, key, val) + self.check_mandatory_values() + + def from_json(self, file_name): + with open(file_name, "r") as f: + log.info(f"Reading env params from {file_name}") + dict = json.load(f) + for key, value in dict.items(): + setattr(self, key, value) + self.check_mandatory_values() + + def to_b64(self): + json_str = self.to_json() + # create bytes-like object for b64 encoder + json_str_bytes = json_str.encode() + b64_str = base64.b64encode(json_str_bytes).decode() + + print(b64_str) + return b64_str + + def from_b64(self, b64_str): + json_str = base64.b64decode(b64_str).decode() + dict = json.loads(json_str) + for key, value in dict.items(): + setattr(self, key, value) + self.check_mandatory_values() + + def check_mandatory_values(self): + for key, validator in self.get_val_dict().items(): + if getattr(self, key) is None and validator.default is None: + raise ValueError(f"Missing mandatory env var: {key}") + + @classmethod + def get_val_dict(cls): + log.debug(f"getting val dict of {cls.__name__}") + val_dict = {} + val_dict.update({key: value for key, value in cls.__dict__.items() if isinstance(value, Validator)}) + + return val_dict + + def dump_validators(self): + validators = self.get_val_dict() + for key, value in validators.items(): + log.debug(f"{key}: {value.__get__(self)}") + + def to_json(self, file_name=None): + dict = { + key.upper(): value.__get__(self) + for key, value in EnvParser.__dict__.items() + if isinstance(value, Validator) + } + json_str = json.dumps(dict, indent=4) + print(json_str) + + if file_name: + with open(file_name, "w") as f: + log.info(f"Writing env params to {file_name}") + f.write(json_str) + + return json_str + + def to_string_dict(self): + result = {} + for key, validator in self.get_val_dict().items(): + value = getattr(self, key) + if value is None: + value = validator.default + if isinstance(validator, JsonDict): + value = json.dumps(value) + else: + value = str(value) + result[key] = value + return result + + def __str__(self): + return ", ".join(f"{key}={value}" for key, value in self.__dict__.items()) diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py b/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py new file mode 100644 index 00000000..18000901 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.utils.env_parsers.env_parser import EnvParser +from cosmos3._src.imaginaire.utils.validator import Bool, Int, String + + +class InferenceEnvParser(EnvParser): + MODEL_MODULE = String(default=None) + MODEL_CLASS = String(default=None) + TORCH_HOME = String(default="/config/models/checkpoints") + TRT_ENABLED = Bool(default=False) + PORT = Int(default=8000) + CP_SIZE = Int(default=1) + TP_SIZE = Int(default=1) + FSDP_ENABLED = Bool(default=False) + CUSTOMIZATION_TYPE = String(default="") + NIM_DEPLOYMENT = Bool(default=False) + RUNAI_DEPLOYMENT = Bool(default=False) + BLUR_CUDA = Bool(default=False) + RESIZE_CUDA = Bool(default=False) + + +INFERENCE_ENVS = InferenceEnvParser() + +if __name__ == "__main__": + INFERENCE_ENVS.to_json("env_params.json") + + b64 = INFERENCE_ENVS.to_b64() + + env_params_restored = InferenceEnvParser(b64) + + print(env_params_restored) diff --git a/cosmos3/_src/imaginaire/utils/fsdp_helper.py b/cosmos3/_src/imaginaire/utils/fsdp_helper.py new file mode 100644 index 00000000..47f47ab9 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/fsdp_helper.py @@ -0,0 +1,159 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from contextlib import contextmanager +from functools import partial + +import torch +from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + CheckpointImpl, + apply_activation_checkpointing, + checkpoint_wrapper, +) +from torch.distributed.device_mesh import init_device_mesh +from torch.distributed.fsdp import FullyShardedDataParallel as FSDP +from torch.distributed.fsdp._runtime_utils import ( + _post_forward, + _post_forward_reshard, + _pre_forward, + _pre_forward_unshard, + _root_pre_forward, +) +from torch.distributed.utils import _p_assert + +from cosmos3._src.imaginaire.utils import distributed, log + + +def apply_fsdp_checkpointing(model, list_block_cls): + """apply activation checkpointing to model + returns None as model is updated directly + """ + log.critical("--> applying fdsp activation checkpointing...") + non_reentrant_wrapper = partial( + checkpoint_wrapper, + # offload_to_cpu=False, + checkpoint_impl=CheckpointImpl.NO_REENTRANT, + ) + + def check_fn(submodule): + result = False + for block_cls in list_block_cls: + if isinstance(submodule, block_cls): + result = True + break + return result + + apply_activation_checkpointing(model, checkpoint_wrapper_fn=non_reentrant_wrapper, check_fn=check_fn) + + +@contextmanager +def possible_fsdp_scope( + model: torch.nn.Module, +): + enabled = isinstance(model, FSDP) + if enabled: + assert not torch.is_grad_enabled(), "FSDP context should be entered with grad disabled" + handle = model._handle + args, kwargs = [0], dict(dummy=0) + with torch.autograd.profiler.record_function("FullyShardedDataParallel.possible_fsdp_scope"): + args, kwargs = _root_pre_forward(model, model, args, kwargs) + unused = None + args, kwargs = _pre_forward( + model, + handle, + _pre_forward_unshard, + model._fsdp_wrapped_module, + args, + kwargs, + ) + if handle: + _p_assert( + handle.flat_param.device == model.compute_device, + "Expected `FlatParameter` to be on the compute device " + f"{model.compute_device} but got {handle.flat_param.device}", + ) + try: + yield None + finally: + if enabled: + output = {"output": 1} + _post_forward(model, handle, _post_forward_reshard, model, unused, output) + + +def hsdp_device_mesh(replica_group_size=None, sharding_group_size=None, device=None): + """ + Initializes a device mesh for use with Hybrid Sharding strategy in FSDP (HSDP) training. + + This function requires explicit sizes for replica and sharding groups to accommodate models + whose GPU fit is unknown, providing flexibility in distributed training setups. + + Args: + replica_group_size (int): The size of each replica group. Must be provided to ensure + the model fits within the available resources. + sharding_group_size (int): The size of each sharding group that the model can fit. Must be provided to + ensure the correct distribution of model parameters. + device (str, optional): The device to use (e.g., "cuda:0"). If None, defaults to "cuda" + with the local rank as the device index. + + Returns: + A device mesh object compatible with FSDP. + + Raises: + ValueError: If replica_group_size or sharding_group_size are not provided, or if the + world size is not evenly divisible by the sharding group size. + RuntimeError: If a valid device mesh cannot be created. + + Usage: + If your model fits on 4 GPUS, and you have 3 nodes of 8 GPUs, then: + Sharding_Group_Size = 4 + Replica_Groups_Size = (24 total gpus, 4 per sharding group) = 6 Replica Groups + >>> device_mesh = initialize_device_mesh(replica_group_size, sharding_group_size) + >>> sharded_model = FSDP(model, device_mesh=device_mesh, ...) + """ + + # world_size = int(os.getenv("WORLD_SIZE", "1")) + world_size = distributed.get_world_size() + if sharding_group_size is None: + sharding_group_size = min(world_size, 8) + sharding_group_size = min(sharding_group_size, world_size) + if replica_group_size is None: + replica_group_size = world_size // sharding_group_size + + device = device or "cuda" + + if world_size % sharding_group_size != 0: + raise ValueError( + f"World size {world_size} is not evenly divisible by sharding group size {sharding_group_size}." + ) + + if (world_size // sharding_group_size) % replica_group_size != 0: + raise ValueError( + f"The calculated number of replica groups is not evenly divisible by " + f"replica_group_size {replica_group_size}." + ) + + device_mesh = init_device_mesh( + device, (replica_group_size, sharding_group_size), mesh_dim_names=("replicate", "shard") + ) + if device_mesh is None: + raise RuntimeError("Failed to create a valid device mesh.") + + log.critical( + f"Device mesh initialized with replica group size {replica_group_size} and sharding group size {sharding_group_size}" + ) + + return device_mesh diff --git a/cosmos3/_src/imaginaire/utils/fused_adam.py b/cosmos3/_src/imaginaire/utils/fused_adam.py new file mode 100644 index 00000000..e8131608 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/fused_adam.py @@ -0,0 +1,383 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import transformer_engine as te +import transformer_engine_torch as tex + +from cosmos3._src.imaginaire.utils import distributed, log + + +class FusedAdam(torch.optim.Optimizer): + """Implements Adam algorithm. + + Currently GPU-only. Requires Apex to be installed via + ``pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./``. + + This version of fused Adam implements 2 fusions. + + * Fusion of the Adam update's elementwise operations + * A multi-tensor apply launch that batches the elementwise updates applied to all the model's parameters + into one or a few kernel launches. + + :class:`FusedAdam` may be used as a drop-in replacement for ``torch.optim.AdamW``, + or ``torch.optim.Adam`` with ``adam_w_mode=False``:: + + opt = FusedAdam(model.parameters(), lr = ....) + ... + opt.step() + + .. warning:: + A previous version of :class:`FusedAdam` allowed a number of additional arguments to ``step``. + These additional arguments are now deprecated and unnecessary. + + Adam was been proposed in `Adam: A Method for Stochastic Optimization`_. + + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups. + lr (float, optional): learning rate. (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square. (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability. (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + (default: False) NOT SUPPORTED in FusedAdam! + adam_w_mode (boolean, optional): Apply L2 regularization or weight decay + True for decoupled weight decay(also known as AdamW) (default: True) + capturable (bool, optional): whether to use the version of the optimizer + that can be used with CUDA Graphs. (default: False) + master_weights (bool, optional): whether to maintain FP32 master weights + in the optimizer with FP16 mixed precision training, currently can + only be used with capturable set to True. (default: False) + + .. _Adam - A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__( + self, + params, + lr=1e-3, + bias_correction=True, + betas=(0.9, 0.999), + eps=1e-8, + adam_w_mode=True, + weight_decay=0.0, + amsgrad=False, + capturable=False, + master_weights=False, + ): + if amsgrad: + raise RuntimeError("FusedAdam does not support the AMSGrad variant.") + if master_weights and not capturable: + raise RuntimeError("Master weights is currently only supported with the capturable version.") + # If the optimizer is capturable then LR should be a tensor (on GPU) + log.warning(f"FusedAdam master_weights: {master_weights} capturable: {capturable}") + lr = torch.tensor(lr, dtype=torch.float32) if capturable else lr + defaults = dict(lr=lr, bias_correction=bias_correction, betas=betas, eps=eps, weight_decay=weight_decay) + super(FusedAdam, self).__init__(params, defaults) + self.adam_w_mode = 1 if adam_w_mode else 0 + + self.capturable = capturable + self.master_weights = master_weights + + self.param_groups_master = None + + if capturable: + for idx, group in enumerate(self.param_groups): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + for item in ["lr"]: + if isinstance(group[item], float): + group[item] = torch.tensor(group[item], dtype=torch.float32) + self.param_groups[idx][item] = group[item].to(device=device) + + self._step_supports_amp_scaling = True + + # Skip buffer + self._dummy_overflow_buf = torch.tensor([0], dtype=torch.int, device="cuda") + self.multi_tensor_adam = tex.multi_tensor_adam + self.multi_tensor_adam_capturable = tex.multi_tensor_adam_capturable + self.multi_tensor_adam_capturable_master = tex.multi_tensor_adam_capturable_master + + def step(self, closure=None, grads=None, output_params=None, scale=None, grad_norms=None, grad_scaler=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + + The remaining arguments are deprecated, and are only retained (for the moment) for error-checking purposes. + """ + if any(p is not None for p in [grads, output_params, scale, grad_norms]): + raise RuntimeError( + "FusedAdam has been updated. " + "Simply initialize it identically to torch.optim.Adam, and call step() with no arguments." + ) + loss = None + if closure is not None: + loss = closure() + + if self.param_groups_master is None: + # Create full precision master weights + self.param_groups_master = [] + for i, pg in enumerate(self.param_groups): + param_list = pg["params"] + self.param_groups_master.append( + { + "params": [p.clone().detach().float() if self.master_weights else None for p in param_list], + } + ) + + for group, group_master in zip(self.param_groups, self.param_groups_master): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + bias_correction = 1 if "bias_correction" in group and group["bias_correction"] else 0 + beta1, beta2 = group["betas"] + + # assume same step across group now to simplify things + # per parameter step can be easily support by making it tensor, or pass list into kernel + if "step" in group: + if self.capturable: + group["step"] = ( + group["step"].to(device=device) + if isinstance(group["step"], torch.Tensor) + else torch.tensor(group["step"], dtype=torch.int32, device=device) + ) + group["step"] += (self._dummy_overflow_buf != 1).to(torch.int) + else: + group["step"] += 1 + else: + group["step"] = 1 if not self.capturable else torch.tensor([1], dtype=torch.int, device=device) + + if self.capturable: + group["lr"] = ( + group["lr"].to(device=device) + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32, device=device) + ) + + # create lists for multi-tensor apply + g_16, p_16, m_16, v_16 = [], [], [], [] + g_bf, p_bf, m_bf, v_bf = [], [], [], [] + g_32, p_32, m_32, v_32 = [], [], [], [] + p_16_master = [] + p_32_master = [] + bf16_master = [] + + for p, p_master in zip(group["params"], group_master["params"]): + if p.grad is None: + continue + if p.grad.data.is_sparse: + raise RuntimeError( + "FusedAdam does not support sparse gradients, please consider SparseAdam instead" + ) + + state = self.state[p] + # State initialization + if len(state) == 0: + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like(p.data).float() + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like(p.data).float() + + if p.dtype == torch.float16: + if self.master_weights: + p_16_master.append(p_master.data) + g_16.append(p.grad.data) + p_16.append(p.data) + m_16.append(state["exp_avg"]) + v_16.append(state["exp_avg_sq"]) + elif p.dtype == torch.bfloat16: + if self.master_weights: + bf16_master.append(p_master.data) + g_bf.append(p.grad) + p_bf.append(p) + m_bf.append(state["exp_avg"]) + v_bf.append(state["exp_avg_sq"]) + elif p.dtype == torch.float32: + if self.master_weights: + p_32_master.append(p_master.data) + g_32.append(p.grad.data) + p_32.append(p.data) + m_32.append(state["exp_avg"]) + v_32.append(state["exp_avg_sq"]) + else: + raise RuntimeError("FusedAdam only support fp16 and fp32.") + + # If the optimizer is capturable, then if there's a grad scaler it works + # on the GPU + a different multi_tensor_applier should be called + if self.capturable: + # overflow check of gradients + found_inf = ( + grad_scaler._check_inf_per_device(self)[device] + if grad_scaler is not None + else torch.zeros((1,), device=device) + ) + self._dummy_overflow_buf.copy_(found_inf) + + # get unscale scale factor + scale, inv_scale = None, None + if grad_scaler: + scale = grad_scaler._get_scale_async() + inv_scale = scale.double().reciprocal().float() + else: + scale = torch.ones((1,), device=device, dtype=torch.float32) + inv_scale = torch.ones((1,), device=device, dtype=torch.float32) + + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16, p_16_master] if self.master_weights else [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf, bf16_master] if self.master_weights else [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32, p_32_master] if self.master_weights else [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + else: + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + return loss + + def load_state_dict(self, state_dict): + super().load_state_dict(state_dict) + for group in self.param_groups: + if self.capturable: + group["lr"] = ( + group["lr"].cuda() + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32).cuda() + ) + + if "step" in group: + if self.capturable: + if distributed.get_rank() == 0: + step = ( + group["step"].cuda() + if isinstance(group["step"], torch.Tensor) + else torch.tensor([group["step"]], dtype=torch.int32).cuda() + ) + else: + step = torch.zeros(1, dtype=torch.int32).cuda() + # make it compatible with FSDP optimizer + distributed.broadcast(step, 0) + group["step"] = step + elif isinstance(group["step"], torch.Tensor): + group["step"] = group["step"].item() + for p in group["params"]: + state = self.state[p] + if "exp_avg" in state: + state["exp_avg"] = state["exp_avg"].float() + state["exp_avg_sq"] = state["exp_avg_sq"].float() diff --git a/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py b/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py new file mode 100644 index 00000000..520a83e1 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List + +import torch + + +@torch.jit.script +def fused_nan_to_num(params: List[torch.Tensor]): + for param in params: + torch.nan_to_num(param, nan=0.0, posinf=0.0, neginf=0.0, out=param) diff --git a/cosmos3/_src/imaginaire/utils/graph.py b/cosmos3/_src/imaginaire/utils/graph.py new file mode 100644 index 00000000..6c96deca --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/graph.py @@ -0,0 +1,444 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A rework of make_graphed_callabled function from TransformerEngine so that it works with inference-only.""" + +from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union + +import torch +from torch._C import _graph_pool_handle +from torch.utils._pytree import tree_flatten as _tree_flatten +from torch.utils._pytree import tree_unflatten as _tree_unflatten +from transformer_engine.pytorch.distributed import get_all_rng_states, graph_safe_rng_available +from transformer_engine.pytorch.module.base import TransformerEngineBaseModule + +from cosmos3._src.imaginaire.utils import log + +__all__ = ["create_cuda_graph"] + + +_IS_GRAPH_CAPTURING = False + +_T = TypeVar("_T") +SingleOrTuple = Union[_T, Tuple[_T, ...]] + + +def set_capture_start() -> None: + """Record beginning of `make_graphed_callables`.""" + global _IS_GRAPH_CAPTURING + _IS_GRAPH_CAPTURING = True + + +def set_capture_end() -> None: + """Record end of `make_graphed_callables`.""" + global _IS_GRAPH_CAPTURING + _IS_GRAPH_CAPTURING = False + + +def is_graph_capturing() -> None: + """Return whether within `make_graphed_callables`.""" + return _IS_GRAPH_CAPTURING + + +def graph_pool_handle(): + """ + Returns an opaque token representing the id of a graph memory pool. + """ + return _graph_pool_handle() + + +def _make_graphed_callables( + callables: SingleOrTuple[Callable], + sample_args: SingleOrTuple[Tuple[torch.Tensor, ...]], + num_warmup_iters: int = 3, + sample_kwargs: Optional[SingleOrTuple[Dict[str, Any]]] = None, + pool: Optional[Tuple[int, ...]] = None, +) -> SingleOrTuple[Callable]: + """ + Helper method for `make_graphed_callables` + """ + + if torch.is_autocast_enabled() and torch.is_autocast_cache_enabled(): + raise RuntimeError( + "make_graphed_callables does not support the autocast caching. Please set `cache_enabled=False`." + ) + + # Default is to pass no kwargs to callables + if sample_kwargs is None: + if isinstance(callables, tuple): + sample_kwargs = tuple({} for _ in range(len(sample_args))) + else: + sample_kwargs = {} + + # Canonicalize args as tuples + just_one_callable = False + if not isinstance(callables, tuple): + just_one_callable = True + callables = (callables,) + sample_args = (sample_args,) + sample_kwargs = (sample_kwargs,) + + # Check sizes of args + assert len(sample_args) == len(callables) + assert len(sample_kwargs) == len(callables) + + # Check callables + for c in callables: + if isinstance(c, torch.nn.Module): + assert len(c._backward_hooks) == 0 and len(c._forward_hooks) == 0 and len(c._forward_pre_hooks) == 0, ( + "Modules must not have hooks registered at the time they are passed. " + + "However, registering hooks on modules after passing them " + + "through make_graphed_callables is allowed." + ) + assert all(b.requires_grad is False for b in c.buffers()), ( + "In any :class:`~torch.nn.Module` passed to " + + ":func:`~make_graphed_callables`, only parameters may be trainable. " + + "All buffers must have ``requires_grad=False``." + ) + + # Flatten callable arguments + per_callable_kwargs_keys = [list(kwargs.keys()) for kwargs in sample_kwargs] + flatten_sample_args = [] + for args, kwargs, kwargs_keys in zip(sample_args, sample_kwargs, per_callable_kwargs_keys): + flatten_arg, _ = _tree_flatten(args) + flatten_kwarg, _ = _tree_flatten([kwargs[key] for key in kwargs_keys]) + flatten_sample_args.append(tuple(flatten_arg + flatten_kwarg)) + assert all(isinstance(arg, torch.Tensor) for arg in flatten_arg), ( + "In the beta API, sample_args " + + "for each callable must contain only Tensors. Other types are not allowed." + ) + + # If a callable is an nn.Module, its graph's full input surface is the args the user explicitly + # passes to forward (ie, its sample_args) AND the module's parameter attributes. + per_callable_len_user_args = [len(args) for args in flatten_sample_args] + per_callable_module_params = [tuple(c.parameters()) if isinstance(c, torch.nn.Module) else () for c in callables] + per_callable_static_input_surfaces = [ + flatten_sample_args[i] + per_callable_module_params[i] for i in range(len(callables)) + ] + + fwd_graphs = [torch.cuda.CUDAGraph() for _ in range(len(flatten_sample_args))] + graph_callables = [None for _ in range(len(flatten_sample_args))] + + # For cases with multiple active RNG states, e.g. TP. + if graph_safe_rng_available(): + for _, state in get_all_rng_states().items(): + for fwd_graph in fwd_graphs: + fwd_graph.register_generator_state(state) + + mempool = graph_pool_handle() if pool is None else pool + + # Warmup + # Hopefully prevents cudnn benchmarking and other lazy-initialization cuda work + # from ending up in any captures. + torch.cuda.synchronize() + + # Get warmup func and func_idx. + warmup_func_idx = [] + warmup_func = [] + for func_idx, func in enumerate(callables): + warmup_func_idx.append(func_idx) + warmup_func.append(func) + assert len(warmup_func) == len(sample_args), f"Warmup runs {len(warmup_func)} don't match args {len(sample_args)}." + assert len(warmup_func_idx) == len(set(warmup_func_idx)), ( + f"Warmup runs {len(warmup_func)} but only {len(set(warmup_func_idx))} are unique." + ) + + # Filter the TE modules that cudagraph can access. + visited_te_modules = set() + + def hook_fn(module, inputs, outputs): # pylint: disable=unused-argument + if isinstance(module, TransformerEngineBaseModule): + visited_te_modules.add(module) + + # Run warmup and do the above filtering. + with torch.cuda.stream(torch.cuda.Stream()): + for func_idx, func in zip(warmup_func_idx, warmup_func): + args = sample_args[func_idx] + kwargs = sample_kwargs[func_idx] + for _ in range(num_warmup_iters): + hooks = [] + for module in func.modules(): + hook = module.register_forward_hook(hook_fn) + hooks.append(hook) + outputs, _ = _tree_flatten(func(*args, **kwargs)) + for hook in hooks: + hook.remove() + del outputs + # The following code is added specifically for MCore's special requirements, + # aimed at preventing warmup from altering the control flow. + for module in func.modules(): + if hasattr(module, "is_first_microbatch"): + module.is_first_microbatch = True + torch.cuda.synchronize() + + # All captures here share a mempool. To avoid replays corrupting each other's memory, + # the safest approach is to capture all passes in the same order they'll run: + # Capture forward graphs + per_callable_static_outputs = [] + per_callable_output_unflatten_spec = [] + graph_id = 0 + for func, args, kwargs, fwd_graph in zip(callables, sample_args, sample_kwargs, fwd_graphs): + with torch.cuda.graph(fwd_graph, pool=mempool): + outputs = func(*args, **kwargs) + graph_callables[graph_id] = func + graph_id += 1 + + flatten_outputs, spec = _tree_flatten(outputs) + per_callable_static_outputs.append(tuple(flatten_outputs)) + per_callable_output_unflatten_spec.append(spec) + + def make_graphed_autograd_function( + fwd_graph, + module_params, + kwargs_keys, + len_user_args, + output_unflatten_spec, + static_input_surface, + static_outputs, + ): + class Graphed(torch.autograd.Function): + """Autograd function for graph replay.""" + + @staticmethod + def forward(ctx, *inputs): + # pylint: disable=missing-function-docstring + + # Copy values from new tensors into static tensors + for i in range(len_user_args): + if static_input_surface[i].data_ptr() != inputs[i].data_ptr(): + static_input_surface[i].copy_(inputs[i]) + + # Replay forward graph + fwd_graph.replay() + assert isinstance(static_outputs, tuple) + return tuple(o.detach() for o in static_outputs) + + def functionalized(*user_args, **user_kwargs): + # Check that required kwargs are provided + for key in kwargs_keys: + if key not in user_kwargs: + raise TypeError( + f"Graphed callable was initialized with kwarg {key} ,but it was not provided in graph replay" + ) + + # Runs the autograd function with inputs == all inputs to + # the graph that might require grad (explicit user args + + # module parameters) + # Assumes module params didn't change since capture. + flatten_user_args, _ = _tree_flatten(user_args) + flatten_user_kwargs, _ = _tree_flatten([user_kwargs[key] for key in kwargs_keys]) + func_args = tuple(flatten_user_args) + tuple(flatten_user_kwargs) + module_params + out = Graphed.apply(*func_args) + return _tree_unflatten(out, output_unflatten_spec) + + return functionalized + + # Put together the final graphed callables + ret = [] + for i in range(len(sample_args)): + graphed = make_graphed_autograd_function( + fwd_graphs[i], + per_callable_module_params[i], + per_callable_kwargs_keys[i], + per_callable_len_user_args[i], + per_callable_output_unflatten_spec[i], + per_callable_static_input_surfaces[i], + per_callable_static_outputs[i], + ) + + func = graph_callables[i] + if isinstance(func, torch.nn.Module): + + def make_graphed_forward(func, graph_training_state, graphed, orig_fwd): + def new_fwd(*user_args, **user_kwargs): + # If the module's training-or-eval state matches what we graphed, + # run the graph, otherwise run the original forward method + if func.training == graph_training_state: + return graphed(*user_args, **user_kwargs) + return orig_fwd(*user_args, **user_kwargs) + + return new_fwd + + forward = make_graphed_forward(func, func.training, graphed, func.forward) + ret.append(forward) + else: + ret.append(graphed) + + if just_one_callable: + return ret[0] + + return tuple(ret) + + +def make_graphed_callables_forward( + modules: SingleOrTuple[Callable], + sample_args: SingleOrTuple[Tuple[torch.Tensor, ...]], + num_warmup_iters: int = 3, + sample_kwargs: Optional[SingleOrTuple[Dict[str, Any]]] = None, + pool: Optional[Tuple[int, ...]] = None, +) -> Union[Callable, Tuple[Callable, ...]]: + """ + Make CUDA graph version of Transformer Engine modules + A variation of PyTorch's `make_graphed_callables` utility function. + `original PyTorch implementation `_ + for more documentation. + Graphing parameters + ------------------- + modules: (tuple of) callable + Callable or callables to graph. + sample_args: (tuple of) tuple of torch.Tensor + Positional arguments to callable(s). + num_warmup_iters: int, default = 3 + Number of warmup iterations. + sample_kwargs: (tuple of) dict, optional + Keyword arguments to callable(s) + pool: (tuple of) int, default = `None`, optional + An instance returned from function `torch.cuda.graph_pool_handle` that hints + this graph may share memory with the indicated pool. + """ + set_capture_start() + + # Handle single module. + just_one_callable = False + if not isinstance(modules, tuple): + just_one_callable = True + modules = (modules,) + + forward_funcs = [] + for module in modules: + assert isinstance(module, torch.nn.Module), f"Graphing for {type(module)} is not supported." + forward_funcs.append(module) + + if just_one_callable: + forward_funcs = forward_funcs[0] + else: + forward_funcs = tuple(forward_funcs) + + # Save RNG state. + if graph_safe_rng_available(): + generators = [ + torch.cuda.default_generators[torch.cuda.current_device()], + *get_all_rng_states().values(), + ] + original_rng_states = [state.get_state() for state in generators] + else: + original_rng_states = torch.cuda.get_rng_state() + + graphed_callables = _make_graphed_callables( + forward_funcs, + sample_args, + num_warmup_iters=num_warmup_iters, + sample_kwargs=sample_kwargs, + pool=pool, + ) + + # Ensures warmup does not affect numerics for ops such as dropout. + if graph_safe_rng_available(): + for gen, state in zip(generators, original_rng_states): + gen.set_state(state) + else: + torch.cuda.set_rng_state(original_rng_states) + set_capture_end() + return graphed_callables + + +def create_cuda_graph( + cuda_graphs_storage: dict, + blocks: torch.nn.ModuleList, + tensor_args: list[Any], + tensor_kwargs: dict[str, Any], + extra_key: Optional[str] = None, +) -> str: + def _make_dummy_tensor_like(t: torch.Tensor) -> torch.Tensor: + if t.dtype.is_floating_point: + return torch.randn(t.shape, device=t.device, dtype=t.dtype) + if t.dtype == torch.bool: + return torch.zeros(t.shape, device=t.device, dtype=t.dtype) + if t.dtype in (torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64): + if t.numel() > 0: + low = int(t.min().item()) + high = int(t.max().item()) + if high == low: + high = low + 1 + else: + high = high + 1 + else: + low, high = 0, 1 + return torch.randint(low, high, t.shape, device=t.device, dtype=t.dtype) + # Fallback: use zeros for uncommon dtypes (e.g., complex) to avoid dtype/range pitfalls. + return torch.zeros(t.shape, device=t.device, dtype=t.dtype) + + def _make_dummy_tree(x: Any) -> Any: + flat, spec = _tree_flatten(x) + dummy_flat: list[torch.Tensor] = [] + for leaf in flat: + if not isinstance(leaf, torch.Tensor): + raise TypeError( + f"create_cuda_graph only supports pytrees of torch.Tensor leaves; got leaf type {type(leaf)}" + ) + dummy = _make_dummy_tensor_like(leaf) + dummy.requires_grad = leaf.requires_grad + dummy_flat.append(dummy) + return _tree_unflatten(dummy_flat, spec) + + real_args = [arg for arg in tensor_args if arg is not None] + real_kwargs = {k: v for k, v in tensor_kwargs.items() if v is not None} + + # Shapes key must reflect all tensor leaves (supports tuple/list/dict structures). + flat_tensors: list[torch.Tensor] = [] + for arg in real_args: + flat, _ = _tree_flatten(arg) + for leaf in flat: + if not isinstance(leaf, torch.Tensor): + raise TypeError( + f"create_cuda_graph only supports pytrees of torch.Tensor leaves; got leaf type {type(leaf)}" + ) + flat_tensors.append(leaf) + for _, kwarg in real_kwargs.items(): + flat, _ = _tree_flatten(kwarg) + for leaf in flat: + if not isinstance(leaf, torch.Tensor): + raise TypeError( + f"create_cuda_graph only supports pytrees of torch.Tensor leaves; got leaf type {type(leaf)}" + ) + flat_tensors.append(leaf) + + shapes_key = "_".join(str(shape_component) for t in flat_tensors for shape_component in t.shape) + if extra_key: + shapes_key = f"{shapes_key}_{extra_key}" + if shapes_key not in cuda_graphs_storage: + callables = [] + sample_args = [] + sample_kwargs = [] + for block in blocks: + callables.append(block) + args = [] + kwargs = {} + for arg in real_args: + args.append(_make_dummy_tree(arg)) + for name, kwarg in real_kwargs.items(): + kwargs[name] = _make_dummy_tree(kwarg) + sample_args.append(tuple(args)) + sample_kwargs.append(kwargs) + + log.critical(f"Creating graph for shape {shapes_key}") + cuda_graphs_storage[shapes_key] = make_graphed_callables_forward( + tuple(callables), + tuple(sample_args), + sample_kwargs=tuple(sample_kwargs), + num_warmup_iters=11, + ) + log.critical(f"Created graph for shape {shapes_key}") + return shapes_key diff --git a/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py b/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py new file mode 100644 index 00000000..2e3f5ccd --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class HighSigmaStrategy(str, Enum): + NONE = "none" + UNIFORM80_2000 = "uniform80_2000" + LOGUNIFORM200_100000 = "LOGUNIFORM200_100000" + SHIFT24 = "shift24" + BALANCED_TWO_HEADS_V1 = "balanced_two_heads_v1" + HARDCODED_20steps = "hardcoded_20steps" + + def __str__(self) -> str: + return self.value diff --git a/cosmos3/_src/imaginaire/utils/launch.py b/cosmos3/_src/imaginaire/utils/launch.py new file mode 100644 index 00000000..6e87488e --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/launch.py @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import sys +import time + +import torch +from omegaconf import OmegaConf + +from cosmos3._src.imaginaire.config import Config +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.cluster_env import get_cluster_env +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.imaginaire.utils.env_parsers.cred_env_parser import CRED_ENVS +from cosmos3._src.imaginaire.utils.wandb_util import set_wandb_job_info + +# Global variable to track S3 readiness +S3_READY = False + + +def log_reproducible_setup(config: Config, args: argparse.Namespace) -> None: + """ + Configures the environment for reproducibility of experiments by setting up + S3 backends for storage, logging important job details, and saving configuration and + environment details both locally and on S3. + This function is crucial for ensuring that all aspects of the computational environment are captured and can be + replicated for future runs or analysis. + + Parameters: + config (Config): A configuration object containing all the settings necessary + for the job, including paths and credentials. + args (argparse.Namespace): An argparse namespace containing the command line + arguments passed to the script. This includes configurations + and any overrides specified at runtime. + + Actions: + - Sets up S3 backend for storing user data and other outputs. + - Logs job paths and critical information regarding job execution. + - Saves the job configuration locally only for the main node in a distributed setting. + - Captures and logs command-line execution details. + - Optionally reads git commit and branch information if available and logs them. + - Saves both job environment information and launch details locally and syncs these to S3. + - Supports conditional integration with Weights & Biases (wandb) for experiment tracking. + + Notes: + - The function is designed to run within a distributed environment where certain actions + (like saving configurations) are restricted to the main node (rank 0). + - It uses the 'easy_io' module for interacting with S3, ensuring files are written and + read correctly from the object store. + - It leverages OmegaConf for saving YAML configurations + - git information is read from 'git_commit.txt' and 'git_branch.txt' files if they exist. + - snapshot codebase is saved as 'codebase.zip' if it exists in the current directory. + + Raises: + FileNotFoundError: If specific files like 'git_commit.txt' or 'codebase.zip' are expected + but not found. + IOError: If there are issues in file handling operations, particularly with file + reading/writing. + """ + + run_timestamp = f"{time.strftime('%Y-%m-%d_%H-%M-%S')}" + time_tensor = torch.ByteTensor(bytearray(run_timestamp, "utf-8")).cuda() + distributed.broadcast(time_tensor, 0) + run_timestamp = time_tensor.cpu().numpy().tobytes().decode("utf-8") + + global S3_READY + if os.path.exists(config.checkpoint.save_to_object_store.credentials) or CRED_ENVS.APP_ENV in [ + "prod", + "dev", + "stg", + ]: + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": { + "s3://timestamps_rundir/": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/", + "s3://rundir/": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/", + }, + "s3_credential_path": config.checkpoint.save_to_object_store.credentials, + } + ) + S3_READY = True + else: + log.warning("S3 credentials not found. Skipping easy_io S3 setup.") + + log.warning(f"Job path: {config.job.path}") + job_info = get_cluster_env() + # save cfg to local + if distributed.get_rank() == 0: + job_local_path = config.job.path_local + log.critical(f"Job local path: {job_local_path}") + os.makedirs(config.job.path_local, exist_ok=True) + launch_info = { + "cmd": " ".join(sys.argv), + "args_cfg_path": args.config, + "args_override": args.opts, + } + + job_info["job_local_path"] = str(job_local_path) + job_info["s3"] = f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/" + # optional read git_commit.txt and save git commit id + if os.path.exists("git_commit.txt"): + with open("git_commit.txt", "r") as f: + job_info["commit_id"] = f.read().strip() + log.critical(f"Commit id: {job_info['commit_id']}") + if os.path.exists("git_branch.txt"): + with open("git_branch.txt", "r") as f: + job_info["git_branch"] = f.read().strip() + log.critical(f"git branch: {job_info['git_branch']}") + if os.path.exists("git_diff.txt"): + with open("git_diff.txt", "r") as f: + job_info["git_diff"] = f.read().strip() + log.critical(f"git diff: {job_info['git_diff']}") + + with open(f"{job_local_path}/job_env.yaml", "w") as f: + OmegaConf.save(job_info, f) + with open(f"{job_local_path}/launch_info.yaml", "w") as f: + OmegaConf.save(launch_info, f) + set_wandb_job_info(job_info) + + # by default, we upload run in ngc and slurm + if config.upload_reproducible_setup: + # sync to s3 + if S3_READY: + log.critical( + f"Uploading reproducible setup to s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/" + ) + + config_pkl_save_fp = f"{config.job.path_local}/config.pkl" + easy_io.copyfile_from_local( + config_pkl_save_fp, f"s3://timestamps_rundir/{config_pkl_save_fp.split('/')[-1]}" + ) + config_yaml_save_fp = config_pkl_save_fp.replace(".pkl", ".yaml") + easy_io.copyfile_from_local( + config_yaml_save_fp, f"s3://timestamps_rundir/{config_yaml_save_fp.split('/')[-1]}" + ) + easy_io.copyfile_from_local(f"{job_local_path}/job_env.yaml", "s3://timestamps_rundir/job_env.yaml") + easy_io.copyfile_from_local( + f"{job_local_path}/launch_info.yaml", + "s3://timestamps_rundir/launch_info.yaml", + ) + if os.path.exists("codebase.zip"): + easy_io.copyfile_from_local("codebase.zip", "s3://timestamps_rundir/codebase.zip") + if os.path.exists("code.tar.gz"): + easy_io.copyfile_from_local("code.tar.gz", "s3://timestamps_rundir/code.tar.gz") + if os.path.exists("git_diff.txt"): + easy_io.copyfile_from_local("git_diff.txt", "s3://timestamps_rundir/git_diff.txt") + if easy_io.exists("s3://rundir/job_history.yaml"): + job_history = easy_io.load("s3://rundir/job_history.yaml") + else: + job_history = {} + job_history[len(job_history)] = { + "timestamp": run_timestamp, + "reproduce_dir": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/", + **launch_info, + } + print(job_history) + easy_io.dump(job_history, "s3://rundir/job_history.yaml") + else: + log.warning("S3 credentials not found. Skipping upload of reproducible setup.") + + # save per rank cluster information to s3 + if config.upload_reproducible_setup: + if S3_READY: + easy_io.dump(job_info, f"s3://timestamps_rundir/cluster_env/RANK_{distributed.get_rank():06d}.yaml") diff --git a/cosmos3/_src/imaginaire/utils/log.py b/cosmos3/_src/imaginaire/utils/log.py new file mode 100644 index 00000000..45545d38 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/log.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import os +import sys +from typing import Any + +import torch.distributed as dist +from loguru._logger import Core, Logger + +RANK0_ONLY = True +LEVEL = os.environ.get("LOGURU_LEVEL", "INFO") +RANK = int(os.environ.get("RANK", "0")) + + +def make_new_logger(depth: int = 1) -> Logger: + return Logger( + core=Core(), + exception=None, + depth=depth, + record=False, + lazy=False, + colors=False, + raw=False, + capture=True, + patchers=[], + extra={}, + ) + + +logger = make_new_logger(depth=1) +atexit.register(logger.remove) + + +def _add_relative_path(record: dict[str, Any]) -> None: + try: + start = os.getcwd() + record["extra"]["relative_path"] = os.path.relpath(record["file"].path, start) + except OSError: + # CWD may have been removed (e.g. on some ranks in distributed jobs). + # Fall back to the absolute path so logging still works. + record["extra"]["relative_path"] = f":{record['file'].path}" + + +*options, _, extra = logger._options # type: ignore +logger._options = tuple([*options, [_add_relative_path], extra]) # type: ignore + + +def init_loguru_stdout() -> None: + logger.remove() + datetime_format = get_datetime_format() + machine_format = get_machine_format() + message_format = get_message_format() + logger.add( + sys.stdout, + level=LEVEL, + format=f"{datetime_format}{machine_format}{message_format}", + filter=_rank0_only_filter, + ) + + +def init_loguru_file(path: str) -> None: + datetime_format = get_datetime_format() + machine_format = get_machine_format() + message_format = get_message_format() + logger.add( + path, + encoding="utf8", + level=LEVEL, + format=f"{datetime_format}{machine_format}{message_format}", + rotation="100 MB", + filter=lambda result: _rank0_only_filter(result) or not RANK0_ONLY, + enqueue=True, + ) + + +def get_datetime_format() -> str: + return "[{time:MM-DD HH:mm:ss}|" + + +def get_machine_format() -> str: + node_id = os.environ.get("NGC_ARRAY_INDEX", "0") + num_nodes = int(os.environ.get("NGC_ARRAY_SIZE", "1")) + machine_format = "" + rank = 0 + if dist.is_available(): + if not RANK0_ONLY and dist.is_initialized(): + rank = dist.get_rank() + world_size = dist.get_world_size() + machine_format = ( + f"[Node{node_id:<3}/{num_nodes:<3}][RANK{rank:<5}/{world_size:<5}]" + "[{process.name:<8}]| " + ) + return machine_format + + +def get_message_format() -> str: + message_format = "{level}|{extra[relative_path]}:{line}:{function}] {message}" + return message_format + + +def _rank0_only_filter(record: Any) -> bool: + is_rank0 = record["extra"].get("rank0_only", True) + if RANK == 0 and is_rank0: + return True + if not is_rank0: + record["message"] = f"[RANK {RANK}] " + record["message"] + return not is_rank0 + + +def trace(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).trace(message) + + +def debug(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).debug(message) + + +def info(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).info(message) + + +def success(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).success(message) + + +def warning(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).warning(message) + + +def error(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).error(message) + + +def critical(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).critical(message) + + +def exception(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).exception(message) + + +# Execute at import time. +init_loguru_stdout() diff --git a/cosmos3/_src/imaginaire/utils/misc.py b/cosmos3/_src/imaginaire/utils/misc.py new file mode 100644 index 00000000..48c01e7f --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/misc.py @@ -0,0 +1,689 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +import collections.abc +import functools +import json +import os +import random +from contextlib import ContextDecorator, nullcontext +from dataclasses import fields +from typing import Any, Callable, List, Tuple, TypeVar, Union + +import numpy as np +from loguru import logger as logging + +try: + # pyrefly: ignore # import-error + import straggler +except ImportError: + straggler = None +import termcolor +import torch +from torch.distributed._functional_collectives import AsyncCollectiveTensor +from torch.distributed._tensor.api import DTensor + +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.distributed import all_gather_tensor +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.imaginaire.utils.timer import Timer + + +def requires_grad(model: torch.nn.Module, value: bool = True) -> None: + """Set a model to require gradients or not. + + Args: + model (torch.nn.Module): Neural network model. + value (bool): Whether the network requires gradients or not. + """ + for p in model.parameters(): + p.requires_grad = value + + +def to( + data: Any, + device: str | torch.device | None = None, + dtype: torch.dtype | None = None, + memory_format: torch.memory_format = torch.preserve_format, +) -> Any: + """Recursively cast data into the specified device, dtype, and/or memory_format. + + The input data can be a tensor, a list of tensors, a dict of tensors. + See the documentation for torch.Tensor.to() for details. + + Args: + data (Any): Input data. + device (str | torch.device): GPU device (default: None). + dtype (torch.dtype): data type (default: None). + memory_format (torch.memory_format): memory organization format (default: torch.preserve_format). + + Returns: + data (Any): Data cast to the specified device, dtype, and/or memory_format. + """ + assert device is not None or dtype is not None or memory_format is not None, ( + "at least one of device, dtype, memory_format should be specified" + ) + + if isinstance(data, torch.Tensor): + if ( + memory_format == torch.channels_last + and data.dim() != 4 + or memory_format == torch.channels_last_3d + and data.dim() != 5 + ): + memory_format = torch.preserve_format # do not change the memory format + is_cpu = (isinstance(device, str) and device == "cpu") or ( + isinstance(device, torch.device) and device.type == "cpu" + ) + data = data.to( + device=device, + dtype=dtype, + memory_format=memory_format, + non_blocking=(not is_cpu), + ) + return data + elif isinstance(data, collections.abc.Mapping): + return type(data)({key: to(data[key], device=device, dtype=dtype, memory_format=memory_format) for key in data}) + elif isinstance(data, collections.abc.Sequence) and not isinstance(data, (str, bytes)): + return type(data)([to(elem, device=device, dtype=dtype, memory_format=memory_format) for elem in data]) + else: + return data + + +def serialize(data: Any) -> Any: + """Serialize data by hierarchically traversing through iterables. + + Args: + data (Any): Input data. + + Returns: + data (Any): Serialized data. + """ + if isinstance(data, collections.abc.Mapping): + return type(data)({key: serialize(data[key]) for key in data}) + elif isinstance(data, collections.abc.Sequence) and not isinstance(data, (str, bytes)): + return type(data)([serialize(elem) for elem in data]) + else: + try: + json.dumps(data) + except TypeError: + data = str(data) + return data + + +def print_environ_variables(env_vars: list[str]) -> None: + """Print a specific list of environment variables. + + Args: + env_vars (list[str]): List of specified environment variables. + """ + for env_var in env_vars: + if env_var in os.environ: + log.info(f"Environment variable {Color.green(env_var)}: {Color.yellow(os.environ[env_var])}") + else: + log.warning(f"Environment variable {Color.green(env_var)} not set!") + + +def set_random_seed(seed: int, by_rank: bool = False) -> None: + """Set random seed. This includes random, numpy, Pytorch. + + Args: + seed (int): Random seed. + by_rank (bool): if true, each GPU will use a different random seed. + """ + if by_rank: + seed += distributed.get_rank() + log.info(f"Using random seed {seed}.") + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) # sets seed on the current CPU & all GPUs + + +def arch_invariant_rand( + shape: List[int] | Tuple[int], dtype: torch.dtype, device: str | torch.device, seed: int | None = None +): + """Produce a GPU-architecture-invariant randomized Torch tensor. + + Args: + shape (list or tuple of ints): Output tensor shape. + dtype (torch.dtype): Output tensor type. + device (torch.device): Device holding the output. + seed (int): Optional randomization seed. + + Returns: + tensor (torch.tensor): Randomly-generated tensor. + """ + # Create a random number generator, optionally seeded + rng = np.random.RandomState(seed) + + # Generate random numbers using the generator + random_array = rng.standard_normal(shape).astype(np.float32) # Use standard_normal for normal distribution + + # Convert to torch tensor and return + return torch.from_numpy(random_array).to(dtype=dtype, device=device) + + +def get_data_batch_size(data: dict[str, torch.Tensor] | torch.Tensor) -> int: + """Get the batch size from a data batch, a (possibly hierarchical) dictionary of tensors. + + Args: + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + + Returns: + batch_size (int): Data batch size. + """ + + def _get_batch_size(input_data: Any) -> Union[int, None]: + """ + Helper function that recursively finds a tensor in the input data + (could be a nested dictionary or list of tensors) and returns its batch size. + """ + if isinstance(input_data, torch.Tensor): + return len(input_data) + elif isinstance(input_data, collections.abc.Mapping): + for key, value in input_data.items(): + batch_size = _get_batch_size(value) + if batch_size is not None: + return batch_size + elif isinstance(input_data, (list, tuple)) and len(input_data) > 0: + # Handle list/tuple of tensors (variable-length batches) + # The batch size is the length of the list + # We are verifying if input_data[0] is indeed a tensor. If so, return the length of the list. + if isinstance(input_data[0], torch.Tensor): + return len(input_data) + # Recurse into first element if it's a nested structure + return _get_batch_size(input_data[0]) + return None + + batch_size = _get_batch_size(data) + if not isinstance(batch_size, int): + raise ValueError(f"Batch size ({batch_size}) obtained from invalid data: {data}") + return batch_size + + +def parameters_to_buffer(module: torch.nn.Module, persistent: bool = True): + """Convert parameters in a module to buffers. + Buffers do not have its own gradients and thus not updated by backpropagation. + + Args: + module (torch.nn.Module): a module to convert parameters + persistent (bool): If True, buffers are included in state_dict. + """ + named_params = dict() + + for name, param in module.named_parameters(): + named_params[name] = param + + for name, param in named_params.items(): + module_hierarchy = name.split(".") + submodule_name = ".".join(module_hierarchy[:-1]) + submodule = module.get_submodule(submodule_name) + subname = module_hierarchy[-1] + delattr(submodule, subname) + submodule.register_buffer(subname, param, persistent=persistent) + + return + + +T = TypeVar("T", bound=Callable[..., Any]) + + +class timer(Timer): + """Simple CPU timer for timing the execution of code. + + It can be used as either a context manager or a function decorator. The timing result will be logged upon exit. + + Example: + def func_a(): + time.sleep(1) + with timer("func_a"): + func_a() + + @timer("func_b) + def func_b(): + time.sleep(1) + func_b() + """ + + def __init__(self, context: str, debug: bool = False): + super().__init__( + tag=context, + measure_cpu=True, + measure_cuda=False, + unit="s", + debug=debug, + ) + + +class memory_checker(ContextDecorator): # noqa: N801 + """Simple memory checker for a given block of code. + + It can be used as either a context manager or a function decorator. The memory usage will be logged upon exit. + Example: + def func_a(): + torch.rand([int(1024**2)]).float().cuda() + with memory_checker("func_a"): + func_a() + >>> 0.004GB memory used + + @memory_checker("func_b") + def func_b(): + random_var = torch.rand([int(1024**2)]).cuda() + func_b() + """ + + def __init__(self, context: str, debug: bool = False): + self.context = context + self.debug = debug + + def __enter__(self) -> None: + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + self.initial_memory = torch.cuda.max_memory_allocated() + + def __exit__(self, exc_type, exc_value, traceback) -> None: # noqa: ANN001 + torch.cuda.synchronize() + final_memory = torch.cuda.max_memory_allocated() + message = f"Memory used within {self.context}: {(final_memory - self.initial_memory) / 1024**3:.4f} GB" + if self.debug: + log.debug(message) + else: + log.info(message) + + def __call__(self, func: T) -> T: + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + initial_memory = torch.cuda.max_memory_allocated() + result = func(*args, **kwargs) + torch.cuda.synchronize() + final_memory = torch.cuda.max_memory_allocated() + message = f"Memory used within {self.context}: {(final_memory - initial_memory) / 1024**3:.4f} GB" + if self.debug: + log.debug(message) + else: + log.info(message) + return result + + return wrapper # type: ignore + + +class TrainingTimer: + """Timer for timing the execution of code, aggregating over multiple training iterations. + + It is used as a context manager to measure the execution time of code and store the timing results + for each function. The context managers can be nested. + + Attributes: + results (dict): A dictionary to store timing results for various code. + + Example: + timer = Timer() + for i in range(100): + with timer("func_a"): + func_a() + avg_time = sum(timer.results["func_a"]) / len(timer.results["func_a"]) + print(f"func_a() took {avg_time} seconds.") + """ + + def __init__(self) -> None: + self.results = dict() + self.average_results = dict() + self.timers = [] + self.func_stack = [] + self.reset() + + def reset(self) -> None: + self.results = {key: [] for key in self.results} + + def __enter__(self) -> TrainingTimer: + timer = Timer(measure_cpu=True, measure_cuda=False, debug=True, unit="s") + self.timers.append(timer) + timer.start() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: # noqa: ANN001 + timer = self.timers.pop() + timer.end() + result = timer.get_cpu_time() + key = self.func_stack.pop() + self.results.setdefault(key, []) + self.results[key].append(result) + + def __call__(self, func_name: str) -> TrainingTimer: + self.func_stack.append(func_name) + return self + + def __getattr__(self, func_name: str) -> TrainingTimer: + return self.__call__(func_name) + + def nested(self, func_name: str) -> TrainingTimer: + return self.__call__(func_name) + + def compute_average_results(self) -> dict[str, float]: + results = dict() + for key, value_list in self.results.items(): + results[key] = sum(value_list) / len(value_list) + return results + + +def timeout_handler(timeout_period: float, signum: int, frame: int) -> None: + # What to do when the process gets stuck. For now, we simply end the process. + error_message = f"Timeout error: more than {timeout_period} seconds passed since the last iteration." + if distributed.is_rank0(): + import wandb + + wandb.alert(title="Timeout error!", text=error_message, level=wandb.AlertLevel.ERROR) + raise TimeoutError(error_message) + + +class Color: + """A convenience class to colorize strings in the console. + + Example: + import + print("This is {Color.red('important')}.") + """ + + @staticmethod + def red(x: str) -> str: + return termcolor.colored(str(x), color="red") + + @staticmethod + def green(x: str) -> str: + return termcolor.colored(str(x), color="green") + + @staticmethod + def blue(x: str) -> str: + return termcolor.colored(str(x), color="blue") + + @staticmethod + def cyan(x: str) -> str: + return termcolor.colored(str(x), color="cyan") + + @staticmethod + def yellow(x: str) -> str: + return termcolor.colored(str(x), color="yellow") + + @staticmethod + def magenta(x: str) -> str: + return termcolor.colored(str(x), color="magenta") + + @staticmethod + def grey(x: str) -> str: + return termcolor.colored(str(x), color="grey") + + +class BufferCnt: + """ + Buffer counter which keeps track of the condition when called and returns True when the condition in met "thres" + amount of times, otherwise returns False. + + Example usage: + buf = BufferCnt(thres=3) + for _ in range(5): + if buf(random.random() > 0.5): + print("We got lucky 3 times out of 5.") + + Args: + thres (int): The amount of times the expression needs to be True before returning True. + reset_over_thres (bool): Whether to reset the buffer after returning True. + """ + + def __init__(self, thres=10, reset_over_thres=False): + self._cnt = 0 + self.thres = thres + self.reset_over_thres = reset_over_thres + + def __call__(self, expre, thres=None): + if expre is True: + self._cnt += 1 + else: + self._cnt = 0 + + if thres is None: + thres = self.thres + + if self._cnt >= thres: + if self.reset_over_thres: + self.reset() + return True + + return False + + @property + def cnt(self): + return self._cnt + + def reset(self): + self._cnt = 0 + + +def dataclass_instance_to_dict(dataclass: Any) -> dict: + """Convert a dataclass to a dictionary. + + Args: + dataclass (Any): Dataclass object. + + Returns: + dict: Dictionary representation of the dataclass. + """ + return {f.name: getattr(dataclass, f.name) for f in fields(dataclass)} + + +def get_local_tensor_if_DTensor(tensor: torch.Tensor | DTensor) -> torch.tensor: + if isinstance(tensor, DTensor): + local = tensor.to_local() + # As per PyTorch documentation, if the communication is not finished yet, we need to wait for it to finish + # https://pytorch.org/docs/stable/distributed.tensor.html#torch.distributed.tensor.DTensor.to_local + if isinstance(local, AsyncCollectiveTensor): + return local.wait() + else: + return local + return tensor + + +def set_torch_compile_options(recompile_limit: int = 8, use_duck_shape: bool = True): + """ + Set some of the torch compile config options. The default values of arguments are default config values in PyTorch as of 2.10 version. + The value recompile_limit=32 is useful for Wan Tokenizer encoding compilation, as the standard value of 8 can easily overflow. + The value of use_duck_shape=False is useful for Cosmos3 MoT training to reduce recompilations. + + Args: + recompile_limit (int): Controls the maximum number of cache entries with a guard on same ID_MATCH'd object. + use_duck_shape (bool): This flag changes whether we should use the same symbolic variable to represent input sizes that are the same + """ + try: + # PyTorch >= 2.7 + torch._dynamo.config.recompile_limit = recompile_limit + torch.fx.experimental._config.use_duck_shape = use_duck_shape + except AttributeError: + try: + torch._dynamo.config.cache_size_limit = recompile_limit + torch.fx.experimental._config.use_duck_shape = use_duck_shape + except AttributeError as e: + log.warning("torch.compile is not available due to missing config options.") + raise e + + +class NVTXRangeContext: + """ + Context manager which inserts NVTX range around the current context and optionally calls torch.cuda.synchronize + at the start and the end of the context. + + Args: + name (str): Name of the NVTX range. + enabled (bool): Whether the context manager is enabled. When disabled, it does nothing. Default: True. + synchronize (bool): Whether to call torch.cuda.synchronize() at the start and the end of the context. Default: True. + """ + + def __init__(self, name: str, enabled: bool = True, synchronize: bool = True): + self.name = name + self.enabled = enabled + self.synchronize = synchronize + + def __enter__(self): + if not self.enabled: + return + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push(self.name) + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.enabled: + return + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + +class StragglerDetectorV2: + """StragglerDetectorV2 is a class that allows you to easily integrate "straggler" tool: + https://gitlab-master.nvidia.com/dl/gwe/fault_tolerance_related/straggler/-/tree/cupti?ref_type=heads. + + This tool detects stragglers using low-level CUPTI tool, which can gather kernel execution time with very low overhead. + The execution times are compared across different ranks, as well as to the execution time of the exact same kernels in the past. + This tool can be easily integrated, as it's resilient to any synchronizations, since it captures kernels execution time. + It means that we can wrap the entire forward or backward passes and the stragglers will be identified regardless + of synchronizations happening during the iteration. + + Args: + enabled (bool): Whether the straggler detection is enabled. When disabled, it does nothing. Default: True. + report_freq (int): Generate a report each report_freq iterations that analyzes the GPUs performance. Defaults to 100. + profile_freq (int): Enable the CUPTI profiling each profile_freq iterations. Since the overhead is very low, + the default value is 1. + max_diff (float): Defines the maximum relative difference between the fastest and the slowest rank to determine the slowdown. Defaults to 2.0 + raise_error (bool): Whether to raise error when stragglers are detected enough times. Defaults to True.""" + + def __init__( + self, + enabled: bool = True, + report_freq: int = 100, + profile_freq: int = 1, + max_diff: float = 2.0, + raise_error: bool = True, + save_s3: bool = False, + ): + self.enabled = enabled + self.report_freq = report_freq + self.profile_freq = profile_freq + self.name = self.__class__.__name__ + self.slowdown_count = BufferCnt(thres=10, reset_over_thres=True) + self.max_diff = max_diff + self.raise_error = raise_error + self.save_s3 = save_s3 + + def initialize(self): + if self.enabled: + if not straggler: + + raise RuntimeError( + "Please install straggler package before using StragglerDetectionV2." + "Package can be installed from here: https://gitlab-master.nvidia.com/dl/osiris/straggler" + ) + + straggler.Detector.initialize( + scores_to_compute=["relative_perf_scores", "individual_perf_scores"], + gather_on_rank0=False, # all ranks results will be available on rank 0 + profiling_interval=self.profile_freq, + ) + + def profile_section(self, name: str, section_enabled: bool, profile_cuda: bool = True): + if section_enabled and self.enabled: + return straggler.Detector.detection_section(name, profile_cuda=profile_cuda) + else: + return nullcontext() + + def _aggregate_section_results(self, local_section_summaries): + data = [] + for key in local_section_summaries: + # straggler reports time in ms + data.append(local_section_summaries[key][straggler.Statistic.MAX] / 1000) + return distributed.all_gather_tensor(torch.tensor(data).cuda()) + + def generate_report(self, iteration): + if self.enabled and iteration % self.report_freq == 0: + report = straggler.Detector.generate_report() + gpu_relative_perf_score = report.gpu_relative_perf_scores[distributed.get_rank()] + gpu_relative_perf_score_gather_list = distributed.all_gather_tensor( + torch.tensor([gpu_relative_perf_score]).cuda() + ) + local_section_data = self._aggregate_section_results(report.local_section_summaries) + if distributed.get_rank() == 0: + stragglers = report.identify_stragglers(gpu_rel_threshold=1 / self.max_diff) + wandb_info = { + f"{self.name}/relative_gpu_perf_{rank}": perf[0].item() + for rank, perf in enumerate(gpu_relative_perf_score_gather_list) + } + for key_id, key in enumerate(report.local_section_summaries): + wandb_info.update( + {f"{self.name}/{key}_{rank:03d}": v[key_id].item() for rank, v in enumerate(local_section_data)} + ) + + data_tensor = torch.tensor(gpu_relative_perf_score_gather_list) + slowest_rank_id = torch.argmin(data_tensor) + wandb_info.update( + { + f"slowest_rank/{self.name}_rank": slowest_rank_id.item(), + f"slowest_rank/{self.name}_relative_perf": torch.min(data_tensor).item(), + } + ) + + for key_id, key in enumerate(report.local_section_summaries): + data_tensor = torch.tensor([v[key_id] for v in local_section_data]) + wandb_info.update( + { + f"slowest_rank/slowest_{key}_rank": torch.argmax(data_tensor).item(), + f"slowest_rank/slowest_{key}_time": torch.max(data_tensor).item(), + } + ) + + import wandb + + if wandb.run: + wandb.log(wandb_info, step=iteration) + + import cosmos3._src.imaginaire.utils.launch + + if cosmos3._src.imaginaire.utils.launch.S3_READY and (iteration % (5 * self.report_freq) == 0) and self.save_s3: + easy_io.dump( + wandb_info, + f"s3://rundir/{self.__class__.__name__}/iter_{iteration:09d}.yaml", + ) + easy_io.dump( + report, + f"s3://rundir/{self.__class__.__name__}/report_iter_{iteration:09d}.pkl", + ) + + # Which GPUs are slower than other GPUs, based on the execution time of kernels + relative_stragglers = stragglers["straggler_gpus_relative"] + # Which GPUs are slower than itself in the past, based on the past execution time of kernels. + individual_stragglers = stragglers["straggler_gpus_individual"] + is_slowdown = relative_stragglers or individual_stragglers + if is_slowdown: + hostname = torch.ByteTensor(bytearray(os.uname().nodename, "utf-8")).cuda() + whole_hostname = all_gather_tensor(hostname) + slowest_hostname = whole_hostname[slowest_rank_id].cpu().numpy().tobytes().decode("utf-8") + logging.critical(f"Slowest rank hostname: {slowest_hostname}") + + if self.slowdown_count(is_slowdown) and self.raise_error: + raise RuntimeError( + f"Detected GPU {slowest_rank_id} to be too slow compared to other GPUs." + f" The relative performance of {slowest_rank_id} rank was {report.gpu_relative_perf_scores[slowest_rank_id]}. Terminating the training." + ) diff --git a/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh b/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh new file mode 100755 index 00000000..18738ac2 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +GPU_METRIC=" --gpu-metrics-device=${SLURM_LOCALID} " +NSYSCMD="nsys profile --capture-range cudaProfilerApi --capture-range-end=stop --cuda-memory-usage=true --cudabacktrace=all --python-backtrace=cuda --trace=cuda,nvtx,ucx,mpi,osrt ${GPU_METRIC} --force-overwrite true --output nsys_rank_${SLURM_PROCID}.nsys-rep " +${NSYSCMD} $@ diff --git a/cosmos3/_src/imaginaire/utils/object_store.py b/cosmos3/_src/imaginaire/utils/object_store.py new file mode 100644 index 00000000..45983fb2 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/object_store.py @@ -0,0 +1,417 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import io +import json +import os +import pickle +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Optional +from urllib.parse import urlparse + +import boto3 +import numpy as np +import torch +import yaml +from botocore.config import Config +from PIL import Image + +import cosmos3._src.imaginaire.utils.easy_io.backends.auto_auth as auto +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.easy_io import easy_io + +GLOBAL_S3_CONFIG = Config( + retries={"max_attempts": 20, "mode": "adaptive"}, + connect_timeout=10, + read_timeout=60, + request_checksum_calculation="when_required", + response_checksum_validation="when_required", +) +Image.MAX_IMAGE_PIXELS = None + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import ObjectStoreConfig + + +class ObjectStore: + """This is the interface class for object store, used for interacting with PBSS/AWS (S3). + + **Deprecated**. Use `easy_io` directly instead. + + Attributes: + client (botocore.client.S3): Object store client object. + easy_io_backend: easy_io backend. + bucket (str): Object store bucket name. + """ + + def __init__(self, config_object_storage: ObjectStoreConfig): + + # extracts the easy_io backend instead of the boto3 S3 client. + with auto.open_auth(config_object_storage.credentials, "r") as file: + object_storage_config = auto.json_load_auth(file) + self.client = Boto3Wrapper( + "s3", + **object_storage_config, + ) + self.easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": config_object_storage.credentials, + "path_mapping": None, + } + ) + self.bucket = config_object_storage.bucket + + def _translate_key(self, key: str) -> str: + """Translate an object key to an S3 URL for easy_io. + + Args: + key (str): The key of the object. + + Returns: + str: The object's S3 URL. + """ + return f"s3://{self.bucket}/{key}" + + def load_object( + self, + key: str, + type: str | None = None, + load_func: Callable | None = None, + encoding: str = "UTF-8", + ) -> Any: + """Helper function for loading object from storage. + + Args: + key (str): The key of the object. + type (str): Specified for some common data types. If not provided, `load_func` should be specified. + The predefined types currently supported are: + - "torch": PyTorch model checkpoints, opened with torch.load(). + - "torch.jit": A JIT-compiled TorchScript model, loaded with torch.jit.load(). + - "image": Image objects, opened with PIL.Image.open(). + - "json": JSON files, opened with json.load(). + - "pickle": Picklable objects, opened with pickle.load(). + - "yaml": YAML files, opened with yaml.safe_load(). + - "text": Pure text files. + - "numpy": Numpy arrays, opened with np.load(). + - "bytes": Raw bytes. + load_func (Callable): a custom function for reading the buffer if `type` were not provided. + encoding (str): Text encoding standard (default: "UTF-8"). + + Returns: + object (Any): The downloaded object. + """ + assert type is not None or load_func is not None, "Either type or load_func should be specified." + + buffer = io.BytesIO(self.easy_io_backend.get(filepath=self._translate_key(key=key))) + buffer.seek(0) + + # Read from buffer for common data types. + if type == "torch": + return torch.load(buffer, map_location=lambda storage, loc: storage, weights_only=False) + elif type == "torch.jit": + return torch.jit.load(buffer) + elif type == "image": + image = Image.open(buffer) + image.load() + return image + elif type == "json": + return json.load(buffer) + elif type == "pickle": + return pickle.load(buffer) + elif type == "yaml": + return yaml.safe_load(buffer) + elif type == "text": + return buffer.read().decode(encoding) + elif type == "numpy": + return np.load(buffer, allow_pickle=True) + # Read from buffer as raw bytes. + elif type == "bytes": + return buffer.read() + # Customized load_func should be provided. + else: + return load_func(buffer) + + def save_object( + self, object: Any, key: str, type: str | None = None, save_func: Callable | None = None, encoding: str = "UTF-8" + ) -> None: + """Helper function for saving object to storage. + + Args: + object (Any): The object to upload. + key (str): The key of the object. + type (str): Specified for some common data types. If not provided, `save_func` should be specified. + The predefined types currently supported are: + - "torch": PyTorch model checkpoints, saved with torch.save(). + - "torch.jit": A JIT-compiled TorchScript model, exported with torch.jit.save(). + - "image": Image objects, saved with PIL.Image.save(). + - "json": JSON files, saved with json.dumps(). + - "pickle": Picklable objects, saved with pickle.dump(). + - "yaml": YAML files, saved with yaml.safe_dump(). + - "text": Pure text files. + - "numpy": Numpy arrays, saved with np.save(). + - "bytes": Raw bytes. + save_func (Callable): a custom function for writing the buffer if `type` were not provided. + encoding (str): Text encoding standard (default: "UTF-8"). + """ + assert type is not None or save_func is not None + with io.BytesIO() as buffer: + + # Write to buffer for common data types. + if type == "torch": + torch.save(object, buffer) + elif type == "torch.jit": + torch.jit.save(object, buffer) + elif type == "image": + type = os.path.basename(key).split(".")[-1] + object.save(buffer, format=type) + elif type == "json": + buffer.write(json.dumps(object).encode(encoding)) + elif type == "pickle": + pickle.dump(object, buffer) + elif type == "yaml": + buffer.write(yaml.safe_dump(object).encode(encoding)) + elif type == "text": + buffer.write(object.encode(encoding)) + elif type == "numpy": + np.save(buffer, object) + # Write to buffer as raw bytes. + elif type == "bytes": + buffer.write(bytes(object)) + # Customized save_func should be provided. + else: + save_func(object, buffer) + buffer.seek(0) + self.easy_io_backend.put(obj=buffer, filepath=self._translate_key(key=key)) + + def object_exists(self, key: str) -> bool: + """ + Check whether an object exists in the storage, with retry logic for transient errors. + + Args: + key (str): The key of the object. + + Returns: + bool: True if the object exists, False if not. + """ + return self.easy_io_backend.exists(filepath=self._translate_key(key=key)) + + +class Boto3Wrapper: + """ + This class serves as a wrapper around boto3.client in order to make boto3.client serializable. It's required to use + spawn method of creating DataLoader workers, which is in turn required to avoid segfaults when using Triton, e.g. + for torch.compile or custom kernels. + """ + + def __init__(self, *args, **kwargs): + self._args = args + self._kwargs = kwargs + self.client = None + + def __setstate__(self, state): + self.__dict__ = state + + def __getattr__(self, item): + is_worker = torch.utils.data.get_worker_info() is not None + client = ( + boto3.client(*self._args, **self._kwargs, config=GLOBAL_S3_CONFIG) if self.client is None else self.client + ) + if is_worker: + self.client = client + return getattr(client, item) + + +def sync_s3_dir_to_local( + s3_dir: str, + s3_credential_path: str, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + local_rank_sync: bool = False, +) -> str: + """ + Download an entire directory from S3 to the local cache directory. + + Args: + s3_dir (str): The AWS S3 directory to download. + s3_credential_path (str): The path to the AWS S3 credentials file. + rank_sync (bool, optional): Whether to synchronize download across + ALL distributed workers using `distributed.barrier()`. Defaults to True. + cache_dir (str, optional): The cache folder to sync the S3 directory to. + If None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "~/.cache/imaginaire") will be used. + local_rank_sync (bool, optional): Whether to synchronize download across + workers within the same node using a node-level barrier. This is useful + when the cache directory is not shared across nodes. Defaults to False. + Note: rank_sync and local_rank_sync cannot both be True. + + Returns: + local_dir (str): The path to the local directory. + """ + if local_rank_sync and rank_sync: + raise ValueError("rank_sync and local_rank_sync cannot be True at the same time.") + + if not s3_dir.startswith("s3://"): + # If the directory exists locally, return the local path + assert os.path.exists(s3_dir), f"{s3_dir} is not a S3 path or a local path." + return s3_dir + + # Get local rank for node-level synchronization + local_rank = int(os.getenv("LOCAL_RANK", 0)) if local_rank_sync else None + + easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": s3_credential_path, + "path_mapping": None, + } + ) + + # Parse the S3 URL + parsed_url = urlparse(s3_dir) + obj_prefix = parsed_url.path.lstrip("/") + + # If the local directory is not specified, use the default cache directory + cache_dir = ( + os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire")) + if cache_dir is None + else cache_dir + ) + cache_dir = os.path.expanduser(cache_dir) + Path(cache_dir).mkdir(parents=True, exist_ok=True) + + for obj_suffix in easy_io_backend.list_dir_or_file(dir_path=s3_dir, list_dir=False, list_file=True): + # Create the full path for the destination file, preserving the directory structure + dest_path = os.path.join(cache_dir, obj_prefix, obj_suffix) + + # Ensure the directory exists + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + + # Check if the file already exists + if os.path.exists(dest_path): + continue + else: + s3_obj = f"{s3_dir.removesuffix('/')}/{obj_suffix}" + log.info(f"Downloading {s3_obj} to {dest_path}") + # Download the file + if rank_sync: + # Only rank 0 downloads when using global rank sync + if distributed.get_rank() == 0: + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + elif local_rank_sync: + # Only local rank 0 (first rank on each node) downloads when using local rank sync + if local_rank == 0: + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + else: + # No synchronization - every rank downloads + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + # Synchronize after downloads complete + if rank_sync or local_rank_sync: + distributed.barrier() + + local_dir = os.path.join(cache_dir, obj_prefix) + return local_dir + + +def download_from_s3_with_cache( + s3_path: str, + s3_credential_path: str, + cache_fp: Optional[str] = None, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """download data from S3 with optional caching. + + This function first attempts to load the data from a local cache file. If + the cache file doesn't exist, it downloads the data from S3 to the cache + location. Caching is performed in a rank-aware manner + using `distributed.barrier()` to ensure only one download occurs across + distributed workers (if `rank_sync` is True). + + Args: + s3_path (str): The S3 path of the data to load. + cache_fp (str, optional): The path to the local cache file. If None, + a filename will be generated based on `s3_path` within `cache_dir`. + cache_dir (str, optional): The directory to store the cache file. If + None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "/tmp") will be used. + rank_sync (bool, optional): Whether to synchronize download across + distributed workers using `distributed.barrier()`. Defaults to True. + backend_args (dict, optional): The backend arguments passed to easy_io to construct the backend. + backend_key (str, optional): The backend key passed to easy_io to registry the backend or retrieve the backend if it is already registered. + + Returns: + cache_fp (str): The path to the local cache file. + + Raises: + FileNotFoundError: If the data cannot be found in S3 or the cache. + """ + if not s3_path.startswith("s3://"): + # If the file exists locally, return the local path + assert os.path.exists(s3_path), f"{s3_path} is not a S3 path nor a local path." + return s3_path + + easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": s3_credential_path, + "path_mapping": None, + } + ) + cache_dir = ( + os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire")) + if cache_dir is None + else cache_dir + ) + cache_dir = os.path.expanduser(cache_dir) + if cache_fp is None: + cache_fp = os.path.join(cache_dir, s3_path.replace("s3://", "")) + if not cache_fp.startswith("/"): + cache_fp = os.path.join(cache_dir, cache_fp) + + if rank_sync: + if distributed.get_rank() == 0: + if os.path.exists(cache_fp): + # check the size of cache_fp + if os.path.getsize(cache_fp) < 1: + os.remove(cache_fp) + log.warning(f"Removed empty cache file {cache_fp}.") + + if not os.path.exists(cache_fp): + easy_io_backend.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + log.info(f"Downloaded {s3_path} to {cache_fp}.") + else: + log.info(f"The cache file {cache_fp} already exists.") + distributed.barrier() + else: + if os.path.exists(cache_fp): + # check the size of cache_fp + if os.path.getsize(cache_fp) < 1: + os.remove(cache_fp) + log.warning(f"Removed empty cache file {cache_fp}.") + if not os.path.exists(cache_fp): + easy_io_backend.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + log.info(f"Downloaded {s3_path} to {cache_fp}.") + else: + log.info(f"The cache file {cache_fp} already exists") + return cache_fp diff --git a/cosmos3/_src/imaginaire/utils/optim_instantiate.py b/cosmos3/_src/imaginaire/utils/optim_instantiate.py new file mode 100644 index 00000000..6dde60bc --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/optim_instantiate.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hydra +import torch +from torch import nn + +from cosmos3._src.imaginaire.utils import log + + +def get_regular_param_group(net: nn.Module): + """ + seperate the parameters of the network into two groups: decay and no_decay. + based on nano_gpt codebase. + """ + param_dict = {pn: p for pn, p in net.named_parameters()} + param_dict = {pn: p for pn, p in param_dict.items() if p.requires_grad} + + decay_params = [p for n, p in param_dict.items() if p.dim() >= 2] + nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2] + return decay_params, nodecay_params + + +def get_base_optimizer( + model: nn.Module, + lr: float, + weight_decay: float, + optim_type: str = "adamw", + sharding: bool = False, + **kwargs, +) -> torch.optim.Optimizer: + net_decay_param, net_nodecay_param = get_regular_param_group(model) + + num_decay_params = sum(p.numel() for p in net_decay_param) + num_nodecay_params = sum(p.numel() for p in net_nodecay_param) + net_param_total = num_decay_params + num_nodecay_params + log.critical(f"total num parameters : {net_param_total:,}") + + param_group = [ + { + "params": net_decay_param + net_nodecay_param, + "lr": lr, + "weight_decay": weight_decay, + }, + ] + + if optim_type == "adamw": + opt_cls = torch.optim.AdamW + elif optim_type == "fusedadam": + from cosmos3._src.imaginaire.utils.fused_adam import FusedAdam + + opt_cls = FusedAdam + else: + raise ValueError(f"Unknown optimizer type: {optim_type}") + + return opt_cls(param_group, **kwargs) + + +def get_base_scheduler( + optimizer: torch.optim.Optimizer, + model: nn.Module, + scheduler_config: dict, +): + net_scheduler = hydra.utils.instantiate(scheduler_config) + net_scheduler.model = model + + num_param_groups = len(optimizer.param_groups) + + return torch.optim.lr_scheduler.LambdaLR( + optimizer, + lr_lambda=[ + net_scheduler.schedule, + ] + * num_param_groups, + ) diff --git a/cosmos3/_src/imaginaire/utils/parallel_state_helper.py b/cosmos3/_src/imaginaire/utils/parallel_state_helper.py new file mode 100644 index 00000000..ee27fb22 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/parallel_state_helper.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module contains various helper functions designed to extend the functionality of parallel states within the MCore library. + +MCore is a third-party library that is infrequently updated and may introduce backward compatibility issues in our codebase, such as changes in function signatures or missing / new functions in new versions. + +To mitigate these issues, this module provides stable functions that ensure the cosmos3._src.imaginaire codebase remains compatible with different versions of MCore. +""" + +try: + from megatron.core import parallel_state +except ImportError: + print("Megatron is not installed, is_tp_cp_pp_rank0 functions will not work.") + + +def is_tp_cp_pp_rank0(): + return ( + parallel_state.get_tensor_model_parallel_rank() == 0 + and parallel_state.get_pipeline_model_parallel_rank() == 0 + and parallel_state.get_context_parallel_rank() == 0 + ) diff --git a/cosmos3/_src/imaginaire/utils/primitives.py b/cosmos3/_src/imaginaire/utils/primitives.py new file mode 100644 index 00000000..2b85de49 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/primitives.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def is_primitive(value): + return isinstance(value, (int, float, str, bool, type(None))) + + +def convert_to_primitive(value): + if isinstance(value, (list, tuple)): + return [convert_to_primitive(v) for v in value if is_primitive(v) or isinstance(v, (list, dict))] + elif isinstance(value, dict): + return {k: convert_to_primitive(v) for k, v in value.items() if is_primitive(v) or isinstance(v, (list, dict))} + elif is_primitive(value): + return value + else: + return "non-primitive" # Skip non-primitive types diff --git a/cosmos3/_src/imaginaire/utils/profiling.py b/cosmos3/_src/imaginaire/utils/profiling.py new file mode 100644 index 00000000..569caa45 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/profiling.py @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import os +import time + +import torch + +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.easy_io import easy_io + +# (qsh 2024-11-23) credits +# https://github.com/pytorch/torchtitan/blob/main/torchtitan/profiling.py + +# how much memory allocation/free ops to record in memory snapshots +MEMORY_SNAPSHOT_MAX_ENTRIES = 100000 + + +@contextlib.contextmanager +def maybe_enable_profiling(config, *, global_step: int = 0): + # get user defined profiler settings + enable_profiling = config.trainer.profiling.enable_profiling + profile_freq = config.trainer.profiling.profile_freq + + if enable_profiling: + trace_dir = os.path.join(config.job.path_local, "torch_trace") + if distributed.get_rank() == 0: + os.makedirs(trace_dir, exist_ok=True) + + rank = distributed.get_rank() + + def trace_handler(prof): + curr_trace_dir_name = "iteration_" + str(prof.step_num) + curr_trace_dir = os.path.join(trace_dir, curr_trace_dir_name) + if not os.path.exists(curr_trace_dir): + os.makedirs(curr_trace_dir, exist_ok=True) + + log.info(f"Dumping traces at step {prof.step_num}") + begin = time.monotonic() + if rank in config.trainer.profiling.target_ranks: + prof.export_chrome_trace(f"{curr_trace_dir}/rank{rank}_trace.json.gz") + log.info(f"Finished dumping traces in {time.monotonic() - begin:.2f} seconds") + + log.info(f"Profiling active. Traces will be saved at {trace_dir}") + + if not os.path.exists(trace_dir): + os.makedirs(trace_dir, exist_ok=True) + + warmup, active = config.trainer.profiling.profile_warmup, 1 + wait = profile_freq - (active + warmup) + assert wait >= 0, "profile_freq must be greater than or equal to warmup + active" + + with torch.profiler.profile( + activities=[ + torch.profiler.ProfilerActivity.CPU, + torch.profiler.ProfilerActivity.CUDA, + ], + schedule=torch.profiler.schedule(wait=wait, warmup=warmup, active=active), + on_trace_ready=trace_handler, + record_shapes=config.trainer.profiling.record_shape, + profile_memory=config.trainer.profiling.profile_memory, + with_stack=config.trainer.profiling.with_stack, + with_modules=config.trainer.profiling.with_modules, + ) as torch_profiler: + torch_profiler.step_num = global_step + yield torch_profiler + else: + torch_profiler = contextlib.nullcontext() + yield None + + +@contextlib.contextmanager +def maybe_enable_memory_snapshot(config, *, global_step: int = 0): + enable_snapshot = config.trainer.profiling.enable_memory_snapshot + if enable_snapshot: + if config.trainer.profiling.save_s3: + snapshot_dir = "s3://rundir" + else: + snapshot_dir = os.path.join(config.job.path_local, "memory_snapshot") + if distributed.get_rank() == 0: + os.makedirs(snapshot_dir, exist_ok=True) + + rank = torch.distributed.get_rank() + + class MemoryProfiler: + def __init__(self, step_num: int, freq: int): + torch.cuda.memory._record_memory_history(max_entries=MEMORY_SNAPSHOT_MAX_ENTRIES) + # when resume training, we start from the last step + self.step_num = step_num + self.freq = freq + + def step(self, exit_ctx: bool = False): + self.step_num += 1 + if not exit_ctx and self.step_num % self.freq != 0: + return + if not exit_ctx: + curr_step = self.step_num + dir_name = f"iteration_{curr_step}" + else: + # dump as iteration_0_exit if OOM at iter 1 + curr_step = self.step_num - 1 + dir_name = f"iteration_{curr_step}_exit" + curr_snapshot_dir = os.path.join(snapshot_dir, dir_name) + if not config.trainer.profiling.save_s3 and not os.path.exists(curr_snapshot_dir): + os.makedirs(curr_snapshot_dir, exist_ok=True) + log.info(f"Dumping memory snapshot at step {curr_step}") + begin = time.monotonic() + + if rank in config.trainer.profiling.target_ranks: + easy_io.dump( + torch.cuda.memory._snapshot(), + f"{curr_snapshot_dir}/rank{rank}_memory_snapshot.pickle", + ) + log.info(f"Finished dumping memory snapshot in {time.monotonic() - begin:.2f} seconds") + + log.info(f"Memory profiler active. Snapshot will be saved at {snapshot_dir}") + profiler = MemoryProfiler(global_step, config.trainer.profiling.profile_freq) + try: + yield profiler + except torch.cuda.OutOfMemoryError as e: + profiler.step(exit_ctx=True) + else: + yield None + + +@contextlib.contextmanager +def maybe_enable_nsys_profiling(config, *, global_step: int = 0): + """Context manager for Nsight Systems profiling via cudaProfilerStart/Stop. + + Usage: launch training with + nsys profile --capture-range=cudaProfilerApi --capture-range-end=stop python ... + and set trainer.profiling.enable_nsys=true, profile_freq=. + + Reuses the torch-profile flags (profile_freq, target_ranks, profile_warmup). + The profiler is started `profile_warmup` iterations before the target and + stopped right after it. + """ + enable_nsys = config.trainer.profiling.enable_nsys + if not enable_nsys: + yield None + return + + rank = distributed.get_rank() + target_ranks = config.trainer.profiling.target_ranks + freq = config.trainer.profiling.profile_freq + warmup = config.trainer.profiling.profile_warmup + + active_iter = freq - 1 # profile_freq=5001 profiles iter 5000 + start_iter = max(0, active_iter - warmup) + + class NsysProfiler: + def __init__(self, step_num: int): + self.step_num = step_num + self._profiling = False + + def step(self): + self.step_num += 1 + if rank not in target_ranks: + return + if self.step_num == start_iter and not self._profiling: + log.info(f"[Nsys] Starting CUDA profiler at iter {self.step_num} (active iter: {active_iter})") + torch.cuda.cudart().cudaProfilerStart() + self._profiling = True + if self.step_num == active_iter + 1 and self._profiling: + torch.cuda.cudart().cudaProfilerStop() + self._profiling = False + log.info(f"[Nsys] Stopped CUDA profiler at iter {self.step_num}") + + log.info(f"[Nsys] Profiling enabled. Will capture iter {start_iter}-{active_iter} on ranks {target_ranks}") + profiler = NsysProfiler(global_step) + try: + yield profiler + finally: + if profiler._profiling: + torch.cuda.cudart().cudaProfilerStop() diff --git a/cosmos3/_src/imaginaire/utils/progress_bar.py b/cosmos3/_src/imaginaire/utils/progress_bar.py new file mode 100644 index 00000000..3b0d6ec7 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/progress_bar.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Progress bar wrapper that gets automatically disabled when in a Timer region, or any other context +where we'd want to disable progress bars, including when TQDM is not present, or when user sets +DISABLE_TQDM=1. We can eventually add a simple ascii progress bar as fallback for missing +dependencies. +""" + +import os + +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.timer import in_timer_region + +try: + import tqdm as _tqdm # noqa: F401 + + HAS_TQDM = True +except ImportError: + HAS_TQDM = False +except Exception as e: + HAS_TQDM = True + + +def _tqdm_wrapper(*args, **kwargs): + if HAS_TQDM: + import tqdm + + return tqdm.tqdm(*args, **kwargs) + + raise ImportError("TQDM is not installed. Please install it and try again.") + + +def progress_bar(fn, desc=None, total=None, force_display: bool = False): + """ + Progress bars a great, but they're not for everybody, certainly not for everywhere. + They must be guarded against: + * We're benchmarking performance (with Timer) + * If tqdm / other progress bars aren't available, skip instead of failing. + * If multi-process / GPU, only one (usually rank 0) must display it, just like prints. + * If the user just doesn't want progress bars (toggle via environment variables. + + This function consideres all of those cases + """ + + disable_tqdm = os.environ.get("DISABLE_TQDM", "0") == "1" + is_in_timer_region = in_timer_region() + is_rank0 = True + + # Wide-scope try/except on determining rank, in case distributed context is uninitialized in a + # single-process program. If exception occurs, it's better to just assume single-process. + try: + is_rank0 = distributed.get_rank() == 0 + except Exception as e: + pass + + if not force_display and (not is_rank0 or is_in_timer_region or disable_tqdm): + return fn + + return _tqdm_wrapper(fn, desc=desc, total=total) + + +__all__ = ["progress_bar"] diff --git a/cosmos3/_src/imaginaire/utils/registry.py b/cosmos3/_src/imaginaire/utils/registry.py new file mode 100644 index 00000000..6f27c1a0 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/registry.py @@ -0,0 +1,160 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utilities for managing registries. +Credit: https://gitlab.com/qsh.zh/jam/-/blob/master/jammy/utils/registry.py with MIT License +""" + +import collections + +__all__ = [ + "Registry", + "DefaultRegistry", + "RegistryGroup", + "CallbackRegistry", +] + + +class Registry: + __FALLBACK_KEY__ = "__fallback__" + + _registry = None + + def __init__(self): + self._init_registry() + + def _init_registry(self): + self._registry = {} + + @property + def fallback(self): + return self._registry.get(self.__FALLBACK_KEY__, None) + + def set_fallback(self, value): + self._registry[self.__FALLBACK_KEY__] = value + return self + + def register(self, entry, value): + self._registry[entry] = value + return self + + def unregister(self, entry): + return self._registry.pop(entry, None) + + def has(self, entry): + return entry in self._registry + + def lookup(self, entry, fallback=True, default=None): + if fallback: + fallback_value = self._registry.get(self.__FALLBACK_KEY__, default) + else: + fallback_value = default + return self._registry.get(entry, fallback_value) + + def keys(self): + return list(self._registry.keys()) + + def items(self): + return list(self._registry.items()) + + +class DefaultRegistry(Registry): + __base_class__ = dict + + def _init_registry(self): + base_class = type(self).__base_class__ + self._registry = collections.defaultdict(base_class) + + def lookup(self, entry, fallback=False, default=None): + assert fallback is False and default is None + return self._registry[entry] + + def __getitem__(self, item): + return self.lookup(item) + + +class RegistryGroup: + __base_class__ = Registry + + def __init__(self): + self._init_registry_group() + + def _init_registry_group(self): + base_class = type(self).__base_class__ + self._registries = collections.defaultdict(base_class) + + def __getitem__(self, item): + return self._registries[item] + + def register(self, registry_name, entry, value, **kwargs): + return self._registries[registry_name].register(entry, value, **kwargs) + + def lookup(self, registry_name, entry, fallback=True, default=None): + return self._registries[registry_name].lookup(entry, fallback=fallback, default=default) + + +class CallbackRegistry(Registry): + """ + A callable manager utils. + + If there exists a super callback, it will block all callbacks. + A super callback will receive the called name as its first argument. + + Then the dispatcher will try to call the callback by name. + If such name does not exists, a fallback callback will be called. + + The fallback callback will also receive the called name as its first argument. + + Examples: + + >>> registry = CallbackRegistry() + >>> callback_func = print + >>> registry.register('name', callback_func) # register a callback. + >>> registry.dispatch('name', 'arg1', 'arg2', kwarg1='kwarg1') # dispatch. + """ + + def __init__(self): + super().__init__() + self._super_callback = None + + @property + def super_callback(self): + return self._super_callback + + def set_super_callback(self, callback): + self._super_callback = callback + return self + + @property + def fallback_callback(self): + return self.fallback + + def set_fallback_callback(self, callback): + return self.set_fallback(callback) + + def dispatch(self, name, *args, **kwargs): + if self._super_callback is not None: + return self._super_callback(self, name, *args, **kwargs) + return self.dispatch_direct(name, *args) + + def dispatch_direct(self, name, *args, **kwargs): + """Dispatch by name, ignoring the super callback.""" + callback = self.lookup(name, fallback=False) + if callback is None: + if self.fallback_callback is None: + raise ValueError('Unknown callback entry: "{}".'.format(name)) + return self.fallback_callback(name, *args, **kwargs) + return callback(*args, **kwargs) diff --git a/cosmos3/_src/imaginaire/utils/replace_bg_color.py b/cosmos3/_src/imaginaire/utils/replace_bg_color.py new file mode 100644 index 00000000..b48a810f --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/replace_bg_color.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import re + +import numpy as np +from PIL import Image + +_IMG_EXTENSIONS = "jpg jpeg png ppm pgm pbm pnm".split() + + +def lin2srgb(lin): + """Convert sRGB values to physically linear ones. The transformation is + uniform in RGB, so *srgb* can be of any shape. + + *srgb* values should range between 0 and 1, inclusively. + + """ + gamma = 1.055 * lin ** (1.0 / 2.4) - 0.055 + scale = 12.92 * lin + return np.where(lin > 0.0031308, gamma, scale) + + +def srgb2lin(srgb): + """Convert sRGB values to physically linear ones. The transformation is + uniform in RGB, so *srgb* can be of any shape. + + *srgb* values should range between 0 and 1, inclusively. + + """ + gamma = ((srgb + 0.055) / 1.055) ** 2.4 + scale = srgb / 12.92 + return np.where(srgb > 0.04045, gamma, scale) + + +def replace_bg_color_u8(fg: np.array, fg_mask: np.array, bg_color_old: list, bg_color_new: list): + r"""Given an image with background, as well as the foreground mask and old background color, + Replace the old background color with the new one. + Assuming everything is in uint8 + Args: + fg [..., 3] np.array + fg_mask[..., 1] np.array: 0 -> full background; 255 -> full foreground. + bg_color_old [3] RGB 0-255: Old background. + bg_color_new [3] RGB 0-255: New background + """ + assert fg.dtype == np.uint8 and fg_mask.dtype == np.uint8 + fg_mask = fg_mask.astype(np.float32) / 255.0 + fg = fg.astype(np.float32) / 255.0 + bg_color_old = np.array(bg_color_old, dtype=np.float32) / 255.0 + bg_color_new = np.array(bg_color_new, dtype=np.float32) / 255.0 + bg_mask = 1.0 - fg_mask + result = srgb2lin(fg) + bg_mask * (srgb2lin(bg_color_new) - srgb2lin(bg_color_old)) + result = lin2srgb(result) + result = np.clip((result * 255.0).round(), 0, 255).astype(np.uint8) + return result + + +def replace_bg_color_pil(fg_pil: Image.Image, fg_mask_pil: Image.Image, bg_color_old: list, bg_color_new: list): + fg = np.array(fg_pil) + fg_mask = np.array(fg_mask_pil) + if fg_mask.ndim == 2: + fg_mask = fg_mask[..., None] + else: + fg_mask = fg_mask[..., :1] + result = replace_bg_color_u8(fg, fg_mask, bg_color_old, bg_color_new) + return Image.fromarray(result) + + +def pil_loader_with_mask(key, data, background_color_new=None, background_color_old=[255, 255, 255], mask=None): + r""" + Function to load an image. + If the image is corrupt, it returns a black image. + Args: + key: Image key. + data: Image data stream. + """ + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _IMG_EXTENSIONS: + return None + + with io.BytesIO(data) as stream: + img = Image.open(stream) + img = img.convert("RGB") + if background_color_new is not None: + assert mask is not None + with io.BytesIO(mask) as stream: + mask = Image.open(stream) + mask.load() + mask = mask.convert("L") + img = replace_bg_color_pil(img, mask, background_color_old, background_color_new) + return img + + +def pil_loader(key, data, type="RGB"): + r""" + Function to load an image. + If the image is corrupt, it returns a black image. + Args: + key: Image key. + data: Image data stream. + """ + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _IMG_EXTENSIONS: + return None + + with io.BytesIO(data) as stream: + img = Image.open(stream) + img.load() + img = img.convert(type) + + return img diff --git a/cosmos3/_src/imaginaire/utils/s3_utils.py b/cosmos3/_src/imaginaire/utils/s3_utils.py new file mode 100644 index 00000000..e46eff16 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/s3_utils.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Any, Optional + +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +def download_from_s3_with_cache( + s3_path: str, + cache_fp: Optional[str] = None, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """download data from S3 with optional caching. + + This function first attempts to load the data from a local cache file. If + the cache file doesn't exist, it downloads the data from S3 to the cache + location. Caching is performed in a rank-aware manner + using `distributed.barrier()` to ensure only one download occurs across + distributed workers (if `rank_sync` is True). + + Args: + s3_path (str): The S3 path of the data to load. + cache_fp (str, optional): The path to the local cache file. If None, + a filename will be generated based on `s3_path` within `cache_dir`. + cache_dir (str, optional): The directory to store the cache file. If + None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "/tmp") will be used. + rank_sync (bool, optional): Whether to synchronize download across + distributed workers using `distributed.barrier()`. Defaults to True. + backend_args (dict, optional): The backend arguments passed to easy_io to construct the backend. + backend_key (str, optional): The backend key passed to easy_io to registry the backend or retrieve the backend if it is already registered. + + Returns: + cache_fp (str): The path to the local cache file. + + Raises: + FileNotFoundError: If the data cannot be found in S3 or the cache. + """ + cache_dir = os.environ.get("TORCH_HOME") if cache_dir is None else cache_dir + cache_dir = ( + os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire")) + if cache_dir is None + else cache_dir + ) + cache_dir = os.path.expanduser(cache_dir) + if cache_fp is None: + cache_fp = os.path.join(cache_dir, s3_path.replace("s3://", "")) + if not cache_fp.startswith("/"): + cache_fp = os.path.join(cache_dir, cache_fp) + + if distributed.get_rank() == 0: + if os.path.exists(cache_fp): + # check the size of cache_fp + if os.path.getsize(cache_fp) < 1: + os.remove(cache_fp) + log.warning(f"Removed empty cache file {cache_fp}.") + + if rank_sync: + if not os.path.exists(cache_fp): + log.critical(f"Local cache {cache_fp} Not exist! Downloading {s3_path} to {cache_fp}.") + log.info(f"backend_args: {backend_args}") + log.info(f"backend_key: {backend_key}") + + easy_io.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + log.info(f"Downloaded {s3_path} to {cache_fp}.") + else: + log.info(f"Local cache {cache_fp} already exist! {s3_path} -> {cache_fp}.") + + distributed.barrier() + else: + if not os.path.exists(cache_fp): + easy_io.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + + log.info(f"Downloaded {s3_path} to {cache_fp}.") + return cache_fp + + +def load_from_s3_with_cache( + s3_path: str, + cache_fp: Optional[str] = None, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, + easy_io_kwargs: Optional[dict] = None, +) -> Any: + """Loads data from S3 with optional caching. + + This function first attempts to load the data from a local cache file. If + the cache file doesn't exist, it downloads the data from S3 to the cache + location and then loads it. Caching is performed in a rank-aware manner + using `distributed.barrier()` to ensure only one download occurs across + distributed workers (if `rank_sync` is True). + + Args: + s3_path (str): The S3 path of the data to load. + cache_fp (str, optional): The path to the local cache file. If None, + a filename will be generated based on `s3_path` within `cache_dir`. + cache_dir (str, optional): The directory to store the cache file. If + None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "/tmp") will be used. + rank_sync (bool, optional): Whether to synchronize download across + distributed workers using `distributed.barrier()`. Defaults to True. + backend_args (dict, optional): The backend arguments passed to easy_io to construct the backend. + backend_key (str, optional): The backend key passed to easy_io to registry the backend or retrieve the backend if it is already registered. + + Returns: + Any: The loaded data from the S3 path or cache file. + + Raises: + FileNotFoundError: If the data cannot be found in S3 or the cache. + """ + cache_fp = download_from_s3_with_cache(s3_path, cache_fp, cache_dir, rank_sync, backend_args, backend_key) + + if easy_io_kwargs is None: + easy_io_kwargs = {} + return easy_io.load(cache_fp, **easy_io_kwargs) diff --git a/cosmos3/_src/imaginaire/utils/scheduler.py b/cosmos3/_src/imaginaire/utils/scheduler.py new file mode 100644 index 00000000..e45eb96a --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/scheduler.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import List + +import torch + + +class WarmupLambdaLR(torch.optim.lr_scheduler.LambdaLR): + def __init__(self, optimizer, warmup, last_epoch=-1, verbose=False): + # Define the lambda function based on the warmup period + self.warmup = warmup + + def lr_lambda(epoch): + # Increase lr linearly for the first 'warmup' epochs + if epoch < warmup: + return float(epoch + 1) / warmup + # After 'warmup' epochs, keep lr constant + return 1.0 + + # Initialize the parent class with the generated lr_lambda + super(WarmupLambdaLR, self).__init__(optimizer, lr_lambda, last_epoch) + + +# cosine lr decay scheduler with warmup from https://github.com/karpathy/nanoGPT/blob/master/train.py#L228 +class WarmupCosineLR(torch.optim.lr_scheduler.LRScheduler): + def __init__( + self, + optimizer: torch.optim.Optimizer, + warmup_iters: int, + lr_decay_iters: int, + min_lr: float, + last_epoch: int = -1, + ): + self.warmup_iters = warmup_iters + self.lr_decay_iters = lr_decay_iters + self.min_lr = min_lr + super().__init__(optimizer, last_epoch) + + def get_lr(self) -> List[float]: + # 1) linear warmup for warmup_iters steps + if self.last_epoch < self.warmup_iters: + return [base_lr * self.last_epoch / self.warmup_iters for base_lr in self.base_lrs] + # 2) if it > lr_decay_iters, return min learning rate + if self.last_epoch > self.lr_decay_iters: + return [self.min_lr for _ in self.base_lrs] + # 3) in between, use cosine decay down to min learning rate + decay_ratio = (self.last_epoch - self.warmup_iters) / (self.lr_decay_iters - self.warmup_iters) + assert 0 <= decay_ratio <= 1 + coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # coeff ranges 0..1 + return [self.min_lr + coeff * (base_lr - self.min_lr) for base_lr in self.base_lrs] diff --git a/cosmos3/_src/imaginaire/utils/submit_job_helper.py b/cosmos3/_src/imaginaire/utils/submit_job_helper.py new file mode 100644 index 00000000..5619aa66 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/submit_job_helper.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import os.path as osp + +import git +from loguru import logger as logging + + +def is_git(path): + try: + _ = git.Repo(path, search_parent_directories=True).git_dir + return True + except git.exc.InvalidGitRepositoryError: + return False + + +def git_rootdir(path=""): + if is_git(os.getcwd()): + git_repo = git.Repo(os.getcwd(), search_parent_directories=True) + root = git_repo.git.rev_parse("--show-toplevel") + return osp.join(root, path) + logging.info("not a git repo") + return osp.join(os.getcwd(), path) diff --git a/cosmos3/_src/imaginaire/utils/timer.py b/cosmos3/_src/imaginaire/utils/timer.py new file mode 100644 index 00000000..f351b1d2 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/timer.py @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Timer: helps measure CPU and CUDA times easily and reliably. +""" + +import time +from contextlib import ContextDecorator +from contextvars import ContextVar +from functools import wraps +from typing import Callable + +import torch + +from cosmos3._src.imaginaire.utils import log + +_timer_active = ContextVar("_timer_active", default=False) + + +def in_timer_region() -> bool: + return _timer_active.get() + + +def _autoformat_time_us(time_us: float) -> str: + """ + Automatically format time in nanoseconds. + """ + if time_us >= 1e6: + time_s = time_us * 1e-6 + return f"{time_s:.2f} s" + + if time_us >= 1e3: + time_ms = time_us * 1e-3 + return f"{time_ms:.2f} ms" + + return f"{time_us:.2f} us" + + +def format_time_str(time_us: float, unit: str | None = None) -> str: + """ + Automatically format time in nanoseconds either automatically or based on + desired unit. + """ + if unit is None: + return _autoformat_time_us(time_us) + + if unit == "us": + return f"{time_us:.2f} us" + + if unit == "ms": + return f"{time_us * 1e-3:.2f} ms" + + if unit == "s": + return f"{time_us * 1e-6:.2f} s" + + raise NotImplementedError(f"Time unit {unit} is not supported.") + + +def format_time(time_us: float, unit: str) -> float: + """ + Format time in nanoseconds based on desired unit. + """ + + if unit == "us": + return time_us + + if unit == "ms": + return time_us * 1e-3 + + if unit == "s": + return time_us * 1e-6 + + raise NotImplementedError(f"Time unit {unit} is not supported.") + + +class Timer(ContextDecorator): + """ + Reliable CPU and CUDA Timer. + + Args: + tag (str | None): Optional tag used in logs/prints. + + measure_cpu (bool): Whether to measure CPU time (using `time`). Default: `True`. + + measure_cuda (bool): Whether to measure CUDA time (using CUDA events). Default: `True`. + + unit (str | None): Optional time unit. Must be either "s" (seconds), "ms" (microseconds), + "us" (nanoseconds), or None (format automatically based on value). + + debug (bool): Whether to log results in debug mode instead of info. Default is False. + + Examples: + ```python + with Timer(measure_cpu=True, measure_cuda=True, unit="ms"): + model(x) + ``` + + ```python + @Timer(measure_cpu=True, measure_cuda=True, unit="ms") + def func(x): + return model(x) + ``` + """ + + def __init__( + self, + tag: str | None = None, + measure_cpu: bool = True, + measure_cuda: bool = True, + unit: str | None = None, + debug: bool = False, + ): + self.measure_cpu = measure_cpu + self.measure_cuda = measure_cuda + + self.measured = False + self.cpu_time_us = 0 + self.cuda_time_us = 0 + + self.busy = False + self.cpu_time_start = None + self.cuda_start_event = None + self.cuda_end_event = None + self.cuda_stream = None + + self.tag = "unknown" if tag is None else tag + self.unit = unit + if self.unit is not None and self.unit not in ["s", "ms", "us"]: + raise NotImplementedError(f"Time unit {self.unit} is not supported.") + + self.debug = debug + + def _log(self, msg: str): + if self.debug: + log.debug(msg) + else: + log.info(msg) + + def __enter__(self): + self.token = _timer_active.set(True) + self.start() + + def __exit__(self, exc_type, exc_value, traceback): + self.end() + self.report() + _timer_active.reset(self.token) + + def __call__(self, func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + self.start() + result = func(*args, **kwargs) + self.end() + self.report() + return result + + return wrapper # type: ignore + + def report(self): + """ + Reports measurements. + """ + if self.measure_cpu and self.measure_cuda: + self._log(f"Time spent on {self.tag}: CPU: {self.get_cpu_time_str()}, CUDA: {self.get_cuda_time_str()}") + elif self.measure_cpu: + self._log(f"Time spent on {self.tag}: {self.get_cpu_time_str()}") + elif self.measure_cuda: + self._log(f"CUDA time spent on {self.tag}: {self.get_cuda_time_str()}") + else: + raise NotImplementedError() + + def get_cpu_time(self) -> float: + """ + Returns CPU time measurement. + """ + if not self.measure_cpu: + raise RuntimeError(f"CPU timer is disabled ({self.measure_cpu=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + if self.unit is None: + raise RuntimeError("No unit was specified. Please use get_cpu_time_str() instead.") + + assert self.unit is not None + return format_time(self.cpu_time_us, unit=self.unit) + + def get_cuda_time(self) -> float: + """ + Returns CUDA time measurement. + """ + if not self.measure_cuda: + raise RuntimeError(f"CUDA timer is disabled ({self.measure_cuda=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + if self.unit is None: + raise RuntimeError("No unit was specified. Please use get_cuda_time_str() instead.") + + assert self.unit is not None + return format_time(self.cuda_time_us, unit=self.unit) + + def get_cpu_time_str(self) -> str: + """ + Returns CPU time measurement in string format. + """ + if not self.measure_cpu: + raise RuntimeError(f"CPU timer is disabled ({self.measure_cpu=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + return format_time_str(self.cpu_time_us, unit=self.unit) + + def get_cuda_time_str(self) -> str: + """ + Returns CUDA time measurement in string format. + """ + if not self.measure_cuda: + raise RuntimeError(f"CUDA timer is disabled ({self.measure_cuda=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + return format_time_str(self.cuda_time_us, unit=self.unit) + + def reset(self): + """ + Resets recorded measurements + """ + self.measured = False + self.cpu_time_us = 0 + self.cuda_time_us = 0 + + def start(self, cuda_device: torch.device | None = None, cuda_stream: torch.cuda.Stream | None = None): + """ + Start time measurements. + + Args: + cuda_device (torch.device | None): CUDA device. Will use default CUDA device if not indicated. + + cuda_stream (torch.cuda.Stream | None): CUDA stream to use for CUDA time measurement. + Will use default stream for current CUDA device if not indicated. + """ + if self.busy: + raise RuntimeError("Already called Timer.start() once!") + + self.busy = True + + if self.measure_cuda: + self.cuda_stream = cuda_stream if cuda_stream is not None else torch.cuda.current_stream(cuda_device) + self.cuda_stream.synchronize() + + if self.measure_cpu: + self.cpu_time_start = time.time() + + if self.measure_cuda: + self.cuda_start_event = torch.cuda.Event(enable_timing=True) + self.cuda_end_event = torch.cuda.Event(enable_timing=True) + self.cuda_stream.record_event(self.cuda_start_event) + + def end(self): + """ + Ends time measurements. + + NOTE: must be done on the same CUDA device and stream as start(). + """ + if not self.busy: + raise RuntimeError("Timer.start() must be called exactly once before end()!") + + if self.measure_cuda: + self.cuda_stream.record_event(self.cuda_end_event) + self.cuda_end_event.synchronize() + + if self.measure_cpu: + self.cpu_time_end = time.time() + self.cpu_time_us = (self.cpu_time_end - self.cpu_time_start) * 1e6 + + if self.measure_cuda: + self.cuda_time_us = self.cuda_start_event.elapsed_time(self.cuda_end_event) * 1e3 + + self.busy = False + self.measured = True diff --git a/cosmos3/_src/imaginaire/utils/tone_curve.py b/cosmos3/_src/imaginaire/utils/tone_curve.py new file mode 100644 index 00000000..2f394bca --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/tone_curve.py @@ -0,0 +1,197 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Literal + +import numpy as np +from PIL import Image + + +def lin2srgb(lin): + """Convert sRGB values to physically linear ones. The transformation is + uniform in RGB, so *srgb* can be of any shape. + + *srgb* values should range between 0 and 1, inclusively. + + """ + gamma = 1.055 * lin ** (1.0 / 2.4) - 0.055 + scale = 12.92 * lin + return np.where(lin > 0.0031308, gamma, scale) + + +def srgb2lin(srgb): + """Convert sRGB values to physically linear ones. The transformation is + uniform in RGB, so *srgb* can be of any shape. + + *srgb* values should range between 0 and 1, inclusively. + + """ + gamma = ((srgb + 0.055) / 1.055) ** 2.4 + scale = srgb / 12.92 + return np.where(srgb > 0.04045, gamma, scale) + + +def commerce_tonemap(color): + startCompression = 0.8 - 0.04 + desaturation = 0.15 + + x = np.min(color, axis=-1, keepdims=True) + offset = np.where(x < 0.08, x - 6.25 * x * x, 0.04) + color -= offset + peak = np.max(color, axis=-1, keepdims=True) + uncompressed = color + + d = 1.0 - startCompression + newPeak = 1.0 - d * d / (peak + d - startCompression) + with np.errstate(divide="ignore", invalid="ignore"): # Avoid error print + color = color * (newPeak / peak) + + g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0) + + compressed = color * (1 - g) + newPeak * g + + return np.where(peak < startCompression, uncompressed, compressed) + + +# https://github.com/RenderKit/oidn/blob/master/training/color.py + + +# Computes the luminance of an RGB color +def luminance(r, g, b): + return 0.212671 * r + 0.715160 * g + 0.072169 * b + + +# Computes an autoexposure value for a NumPy image +def autoexposure(image, mask, key=0.18): + maxBinSize = 16 # downsampling amount + eps = 1e-8 + + image = image * mask + # Compute the luminance of each pixel + r = image[..., 0] + g = image[..., 1] + b = image[..., 2] + L = luminance(r, g, b) + + # Center crop if the image size is not whole multiple of maxBinSize + crop_H = L.shape[0] // maxBinSize * maxBinSize + pad_top = round((L.shape[0] - crop_H) / 2) + crop_W = L.shape[1] // maxBinSize * maxBinSize + pad_left = round((L.shape[1] - crop_W) / 2) + L = L[pad_top : pad_top + crop_H, pad_left : pad_left + crop_W] + mask = mask[pad_top : pad_top + crop_H, pad_left : pad_left + crop_W] + + # Downsample the image to minimize sensitivity to noise + H = L.shape[0] # original height + W = L.shape[1] # original width + L = L.reshape(H // maxBinSize, maxBinSize, W // maxBinSize, maxBinSize) + L = np.mean(L, axis=(1, 3)) + mask = mask.reshape(H // maxBinSize, maxBinSize, W // maxBinSize, maxBinSize) + mask = np.mean(mask, axis=(1, 3)) + with np.errstate(divide="ignore", invalid="ignore"): # Avoid error print + L /= mask + L = L[mask > eps] + + # Keep only values greater than epsilon + L = L[L > eps] + if L.size == 0: + return 1.0 + + # Compute the exposure value + return float(key / np.exp2(np.log2(L).mean())) + + +# Default values changed to identity transformation, aka do nothing. +def apply_tone_curve( + imgs: list[Image.Image], + input_mapping: Literal["log", "straight"] = "log", + output_mapping: Literal["commerce", "straight", "log"] = "commerce", + exposure_bias: float = 1.5, + auto: bool = True, + ae_pregain: float = 1.0, + ae_key: float = 0.18, + ae_strength_below: float = 1.0, + ae_strength_above: float = 1.0, +) -> tuple[list[Image.Image], float]: + r"""Adjust the exposure of a list of images together. + For cam_v1 data, use input_mapping="log" + For cam_v2 data, use input_mapping="straight" + Some of the previous models are trained with output_mapping="commerce". This is a very forgiving curve. + But to match the style of PixelSquid, use output_mapping="straight" + See https://docs.google.com/document/d/1z08rWvWzqd_tNPlh7_D4aIkdaLAerSSagXK4pQlQxCk/edit for detail + + Args: + imgs: list of PIL images + + Returns: + ret: list of PIL images with exposure adjusted + """ + num_imgs = len(imgs) + img = np.concatenate([np.asarray(x) for x in imgs], axis=0).astype(np.float32) / 255.0 + mask = img[..., 3:4].astype(np.float32) # H,W,1 + img = img[..., :3] # Remove alpha + + img = srgb2lin(img) + + if input_mapping == "log": + img = np.exp(img) - 1 + elif input_mapping == "straight": + pass + else: + raise NotImplementedError(f"Unknown input_mapping: {input_mapping}") + + if auto: + img *= ae_pregain + exposure = autoexposure(img, mask, key=ae_key) + log_exposure = math.log2(exposure) + if log_exposure <= 0: + log_exposure *= ae_strength_below + else: + log_exposure *= ae_strength_above + exposure = 2.0**log_exposure + else: + exposure = 1.0 + exposure *= exposure_bias + + img = img * exposure + + if output_mapping == "commerce": + img = commerce_tonemap(img) + elif output_mapping == "log": + img = np.log(img + 1) + elif output_mapping == "straight": + pass + else: + raise NotImplementedError(f"Unknown output_mapping: {output_mapping}") + + img = lin2srgb(img) + img = np.concatenate([img, mask], axis=-1) + img = np.clip((img * 255.0).round(), 0, 255).astype(np.uint8) + return [Image.fromarray(x) for x in np.split(img, num_imgs, axis=0)], exposure + + +def apply_exposure(img: Image, exposure: float) -> Image: + r"""Apply exposure adjustment to a PIL image. + Args: + img: a PIL image, RGB or RGBA + exposure: exposure value + Returns: + img: PIL image with exposure adjusted + """ + img = np.asarray(img).astype(np.float32) / 255.0 + img[..., :3] = lin2srgb(srgb2lin(img[..., :3]) * exposure) + img = np.clip((img * 255.0).round(), 0, 255).astype(np.uint8) + return Image.fromarray(img) diff --git a/cosmos3/_src/imaginaire/utils/training.py b/cosmos3/_src/imaginaire/utils/training.py new file mode 100644 index 00000000..2644a057 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/training.py @@ -0,0 +1,171 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch + +from cosmos3._src.imaginaire.functional.batch_ops import batch_mul +from cosmos3._src.imaginaire.utils import log + + +def random_dropout(embeddings, drop_rate): + r""" + Function to perform random dropout for embeddings. + When we drop embeddings, we zero them out. + Args: + embeddings (tensor): Input embeddings + drop_rate (float): Rate of dropping the embedding. + """ + num_samples = embeddings.shape[0] + # Create a shape (num_samples, 1, 1, 1, 1, ...) depending on embeddings dim. + # This is done to ensure we can broadcast the zero_flag to the embeddings. + # embeddings.ndim is 3 for images, and 4 for videos, and the corresponding + # shapes are (num_samples, 1, 1) and (num_samples, 1, 1, 1) respectively. + tensor_shape = (num_samples,) + tuple([1] * (embeddings.ndim - 1)) + zero_flag = torch.ones(tensor_shape).to(embeddings.dtype) * (1 - drop_rate) + zero_flag = torch.bernoulli(zero_flag).to(embeddings.device) + embeddings = embeddings * zero_flag + return embeddings + + +def random_embed_replace(src_embed, tgt_embed, drop_rate, position=0): + r""" + Function to perform random embedding replacement. + With probability given by drop rate, we replace src_embed by tgt_embed + Args: + src_embed (tensor): Src embeddings + tgt_embed (tensor): Tgt embeddings + drop_rate (float): Rate of replacing the embedding. + position (int): Starting position to replace the sequence + """ + for i in range(src_embed.shape[0]): + coin_flip = torch.rand(1).item() + if coin_flip < drop_rate: + src_embed[i][position:] = tgt_embed + + return src_embed + + +def to255_round_uint8_append(vis_images, total_vis_images): + r""" + Map pixel values of vis_images to [0 255] and quantize them to 256 bins + """ + if vis_images is not None: + vis_images = ((vis_images + 1) / 2).clamp_(0, 1).mul_(255).round_().type(torch.uint8) + total_vis_images.append(vis_images) + return vis_images + + +def no_round_append(vis_images, total_vis_images): + r""" + Append the images as is without type casting + """ + if vis_images is not None: + total_vis_images.append(vis_images) + return vis_images + + +def sample_sigma_and_xt( + sde, + target_data: torch.Tensor, + data_batch: dict = None, + use_same_noise_multiview: bool = False, + use_low_noise_first_view: bool = False, +): + """Sample pertubation noise levels and generate noisy observations.""" + # Sample pertubation noise levels + tensor_kwargs = {"device": "cuda", "dtype": target_data.dtype} + t = sde.sample_t(batch_size=target_data.size()[0]).to(**tensor_kwargs) # check precision and memory_format later + if data_batch is not None and data_batch.get("num_view", None) is not None: + if use_same_noise_multiview and not use_low_noise_first_view: + t_shape = t.shape + t = t.view(-1, int(data_batch["num_view"].view(-1)[0].item())) + t[:, 1:] = t[:, 0:1] + t = t.view(t_shape) + elif use_low_noise_first_view and not use_same_noise_multiview: + t_shape = t.shape + t = t.view(-1, int(data_batch["num_view"].view(-1)[0].item())) + t[:, 0] = 0.02 + t = t.view(t_shape) + elif use_low_noise_first_view and use_same_noise_multiview: + t_shape = t.shape + t = t.view(-1, int(data_batch["num_view"].view(-1)[0].item())) + t[:, 0] = 0.02 + t[:, 2:] = t[:, 1:2] + t = t.view(t_shape) + # Generate an N(0,1) noise map. + epsilon = torch.randn_like(target_data, **tensor_kwargs) + # Get the mean and stand deviation of the marginal probability distribution. + mean, std = sde.marginal_prob(target_data, t) + # Generate noisy observations + xt = mean + batch_mul(std, epsilon) # corrupted data + + data_batch = {} + data_batch["t"] = t # between model.sde.eps to 1 + data_batch["epsilon"] = epsilon # Standard normal noise map + data_batch["mean"] = mean # mean of the marginal distribution + data_batch["std"] = std # std deviation of the marginal distribution + data_batch["xt"] = xt # corrupted data + data_batch["target"] = target_data + return data_batch + + +def form_loss_mask( + data_batch: dict, + x_shape: tuple, + dtype: torch.dtype, + device: torch.device, + loss_masking_cfg: dict = {"human_body_mask": 2, "human_face_mask": 4, "human_hand_mask": 4, "padding_mask": 0}, +) -> torch.Tensor: + r""" + Function to form a combined mask given several loss masks. + If there are overlapping region between multiple masks, we assign the max value to the + overlapping region. For the unmasked regions, we assign a value of 1. + However, if there is a mask specifying zero value, we zero it out. + Zeroing is crucial for padded loss. + Copied from i3, kaal.py form_loss_mask function. + zero_mask: mask out some region by setting them as zero + For example, + mask1: [0, 1, 1, 1, 0, 0], weight: 2 + mask2: [1, 0, 1, 0, 0, 0], weight: 4 + mask3: [0, 1, 0, 0, 0, 0], weight: 0 + + Final loss mask: [4, 0, 4, 2, 1, 1] + """ + loss_mask = torch.ones(x_shape, dtype=dtype, device=device) + zero_mask = torch.ones(x_shape, dtype=dtype, device=device) + + for key in loss_masking_cfg: + if key not in data_batch: + if loss_masking_cfg[key] > 0: + log.warning(f"You set {key} to have larger loss, but there is no such mask data") + continue + # Repeat mask along channel's dimension. ndim=4 for images. + repeat_dims = (1, 3) + tuple([1] * (data_batch[key].ndim - 2)) + mask_key = torch.tile(data_batch[key], dims=repeat_dims) + weight_key = loss_masking_cfg[key] + + assert weight_key >= 0, "Current support only for weight >= 0" + + if key == "zero_mask": + zero_mask = zero_mask * mask_key + elif weight_key == 0: + zero_mask = zero_mask * (1 - mask_key) + else: + no_mask_region = (mask_key == 0).float() + loss_mask = mask_key * weight_key + no_mask_region * loss_mask + # loss_mask = torch.max(loss_mask_new, loss_mask) + + loss_mask_final = loss_mask * zero_mask + return loss_mask_final diff --git a/cosmos3/_src/imaginaire/utils/validator.py b/cosmos3/_src/imaginaire/utils/validator.py new file mode 100644 index 00000000..b61d1220 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/validator.py @@ -0,0 +1,512 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import binascii +import itertools +import json +import os +from abc import ABC, abstractmethod +from io import BytesIO +from typing import Any + +# Sentinel value to indicate that no default was explicitly set by the user +# we want to mimic usage of function parameters: if no default is provided, the parameter is mandatory +_UNSET = object() + + +# from https://docs.python.org/3/howto/descriptor.html#validator-class + + +# validators can be customized to very specific needs, e.g. see HumanAttributes below +class Validator(ABC): + def __init__(self, default=_UNSET, hidden=False): + self.default = default + self.hidden = hidden + + # set name is called when the validator is created as class variable + # name is the name of the variable in the owner class, so here we create the name for the backing variable + def __set_name__(self, owner, name): + self.private_name = "_" + name + + def __get__(self, obj, objtype=None): + value = getattr(obj, self.private_name, self.default) + if value is _UNSET: + # If we reach here, it means a mandatory parameter was accessed without being set + attr_name = getattr(self, "private_name", "unknown").lstrip("_") + raise ValueError( + f"Parameter '{attr_name}' is mandatory but has not been set. " + f"No default value was provided and no value was assigned." + ) + return value + + def __set__(self, obj, value): + value = self.validate(value) + setattr(obj, self.private_name, value) + + @abstractmethod + def validate(self, value): + pass + + def json(self): + pass + + +class Bool(Validator): + def __init__(self, default=_UNSET, hidden=False, tooltip=None): + super().__init__(default, hidden) + self.default = default + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if isinstance(value, int): + value = value != 0 + elif isinstance(value, str): + value = value.lower() + if value in ["true", "1"]: + value = True + elif value in ["false", "0"]: + value = False + else: + raise ValueError(f"Expected {value!r} to be one of ['True', 'False', '1', '0']") + elif not isinstance(value, bool): + raise TypeError(f"Expected {value!r} to be an bool") + + return value + + def get_range_iterator(self): + return [True, False] + + def __repr__(self) -> str: + return f"Bool({self.private_name=} {self.default=} {self.hidden=})" + + def json(self): + return { + "type": bool.__name__, + "default": self.default, + "tooltip": self.tooltip, + } + + +class Int(Validator): + def __init__(self, default=_UNSET, min=None, max=None, step=1, hidden=False, tooltip=None): + self.min = min + self.max = max + self.default = default + self.step = step + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if isinstance(value, str): + value = int(value) + elif not isinstance(value, int): + raise TypeError(f"Expected {value!r} to be an int") + + if self.min is not None and value < self.min: + raise ValueError(f"Expected {value!r} to be at least {self.min!r}") + if self.max is not None and value > self.max: + raise ValueError(f"Expected {value!r} to be no more than {self.max!r}") + return value + + def get_range_iterator(self): + if self.default is _UNSET: + default_val = 0 + else: + default_val = int(self.default) if isinstance(self.default, (int, float, str)) else 0 + iter_min = self.min if self.min is not None else default_val + iter_max = self.max if self.max is not None else (default_val + 100) + return itertools.takewhile(lambda x: x <= iter_max, itertools.count(iter_min, self.step)) + + def __repr__(self) -> str: + return f"Int({self.private_name=} {self.default=}, {self.min=}, {self.max=} {self.hidden=})" + + def json(self): + return { + "type": int.__name__, + "default": self.default, + "min": self.min, + "max": self.max, + "step": self.step, + "tooltip": self.tooltip, + } + + +class Float(Validator): + def __init__(self, default=_UNSET, min=None, max=None, step=0.5, hidden=False, tooltip=None): + self.min = min + self.max = max + self.default = default + self.step = step + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if isinstance(value, str) or isinstance(value, int): + value = float(value) + elif not isinstance(value, float): + raise TypeError(f"Expected {value!r} to be float") + + if self.min is not None and value < self.min: + raise ValueError(f"Expected {value!r} to be at least {self.min!r}") + if self.max is not None and value > self.max: + raise ValueError(f"Expected {value!r} to be no more than {self.max!r}") + return value + + def get_range_iterator(self): + if self.default is _UNSET: + default_val = 0.0 + else: + default_val = float(self.default) if isinstance(self.default, (int, float, str)) else 0.0 + iter_min = self.min if self.min is not None else default_val + iter_max = self.max if self.max is not None else (default_val + 100.0) + return itertools.takewhile(lambda x: x <= iter_max, itertools.count(iter_min, self.step)) + + def __repr__(self) -> str: + return f"Float({self.private_name=} {self.default=}, {self.min=}, {self.max=} {self.hidden=})" + + def json(self): + return { + "type": float.__name__, + "default": self.default, + "min": self.min, + "max": self.max, + "step": self.step, + "tooltip": self.tooltip, + } + + +class String(Validator): + def __init__(self, default=_UNSET, min=None, max=None, predicate=None, hidden=False, tooltip=None): + self.min = min + self.max = max + self.predicate = predicate + self.default = default + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if value is None: + return value # Allow None as a valid value to be compatible with existing code + # this breaks strict typing, so do this only for strings + if not isinstance(value, str): + raise TypeError(f"Expected {value!r} to be an str or None") + if self.min is not None and len(value) < self.min: + raise ValueError(f"Expected {value!r} to be no smaller than {self.min!r}") + if self.max is not None and len(value) > self.max: + raise ValueError(f"Expected {value!r} to be no bigger than {self.max!r}") + if self.predicate is not None and not self.predicate(value): + raise ValueError(f"Expected {self.predicate} to be true for {value!r}") + return value + + def get_range_iterator(self): + return iter([self.default]) + + def __repr__(self) -> str: + return f"String({self.private_name=} {self.default=}, {self.min=}, {self.max=} {self.hidden=})" + + def json(self): + return { + "type": str.__name__, + "default": self.default, + "tooltip": self.tooltip, + } + + +class Path(Validator): + def __init__(self, default=_UNSET, hidden=False, tooltip=None): + self.default = default + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if value is None: + return value + if not isinstance(value, str): + raise TypeError(f"{self.private_name} validator: Expected {value!r} to be an str") + if not os.path.exists(value): + raise ValueError(f"{self.private_name} validator: Expected {value!r} to be a valid path") + + return value + + def get_range_iterator(self): + return iter([self.default]) + + def __repr__(self) -> str: + return f"String({self.private_name=} {self.default=}, {self.hidden=})" + + +class InputImage(Validator): + def __init__( + self, default=_UNSET, hidden=False, tooltip=None, supported_formats=["jpeg", "jpg", "png", "bmp", "gif"] + ): + self.default = default + self.hidden = hidden + self.tooltip = tooltip + self.supported_formats = supported_formats + + def validate(self, value): + ext = os.path.splitext(value)[1].lower() + + if ext not in self.supported_formats: + raise ValueError(f"Unsupported image format: {ext}") + + if not isinstance(value, str): + raise TypeError(f"Expected {value!r} to be an str") + if not os.path.exists(value): + raise ValueError(f"Expected {value!r} to be a valid path") + return value + + def get_range_iterator(self): + return iter([self.default]) + + def __repr__(self) -> str: + return f"String({self.private_name=} {self.default=} {self.hidden=})" + + def json(self): + return { + "type": InputImage.__name__, + "default": self.default, + "values": self.supported_formats, + "tooltip": self.tooltip, + } + + +class JsonDict(Validator): + """ + JSON stringified version of a python dict. + Example: '{"ema_customization_iter.pt": "ema_customization_iter.pt"}' + """ + + def __init__(self, default=_UNSET, hidden=False): + self.default = default + self.hidden = hidden + + def validate(self, value): + if not value: + return {} + try: + dict = json.loads(value) + return dict + except json.JSONDecodeError as e: + raise ValueError(f"Expected {value!r} to be json stringified dict. Error: {str(e)}") + + def __repr__(self) -> str: + return f"Dict({self.default=} {self.hidden=})" + + +class Dict(Validator): + """ + Python dict. + Example: {'key': 'value'} + + This allows a single level of parameter nesting, but not a full nested dict. + For now we validate the individual keys here and store the dict as is. + Alternatively, we could have a validator that gets/sets another ValidatorParams class. + """ + + def __init__(self, default=_UNSET, hidden=False): + self.default = default + self.hidden = hidden + + def validate(self, value): + if not isinstance(value, dict): + raise TypeError(f"Expected {value!r} to be an dict") + return value + + def __repr__(self) -> str: + value = getattr(self, self.private_name, self.default) + + return f"Dict({self.private_name=} {self.default=} {self.hidden=} value={json.dumps(value, indent=4)})" + + +class OneOf(Validator): + def __init__(self, default=_UNSET, options=None, type_cast=None, hidden=False, tooltip=None): + self.options = set(options) if options is not None else set() + self.default = default + self.type_cast = type_cast # Cast the value to this type before checking if it's in options + self.tooltip = tooltip + self.hidden = hidden + + def validate(self, value): + if self.type_cast: + try: + value = self.type_cast(value) + except ValueError: + raise ValueError(f"Expected {value!r} to be castable to {self.type_cast!r}") + + if value not in self.options: + raise ValueError(f"Expected {value!r} to be one of {self.options!r}") + + return value + + def get_range_iterator(self): + return self.options + + def __repr__(self) -> str: + return f"OneOf({self.private_name=} {self.options=} {self.hidden=})" + + def json(self): + return { + "type": OneOf.__name__, + "default": self.default, + "values": list(self.options), + "tooltip": self.tooltip, + } + + +class MultipleOf(Validator): + def __init__(self, default=_UNSET, multiple_of: int = 1, type_cast=None, hidden=False, tooltip=None): + if type(multiple_of) is not int: + raise ValueError(f"Expected {multiple_of!r} to be an int") + self.multiple_of = multiple_of + self.default = default + self.type_cast = type_cast + + # if a parameter is hidden then probe() can't expose the param + # and the param can't be set anymore + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value): + if self.type_cast: + try: + value = self.type_cast(value) + except ValueError: + raise ValueError(f"Expected {value!r} to be castable to {self.type_cast!r}") + + if value % self.multiple_of != 0: + raise ValueError(f"Expected {value!r} to be a multiple of {self.multiple_of!r}") + + return value + + def get_range_iterator(self): + return itertools.count(0, self.multiple_of) + + def __repr__(self) -> str: + return f"MultipleOf({self.private_name=} {self.multiple_of=} {self.hidden=})" + + def json(self): + return { + "type": MultipleOf.__name__, + "default": self.default, + "multiple_of": self.multiple_of, + "tooltip": self.tooltip, + } + + +class HumanAttributes(Validator): + def __init__(self, default=_UNSET, hidden=False, tooltip=None): + self.default = default + self.hidden = hidden + self.tooltip = tooltip + + # hard code the options for now + # we extend this to init parameter as needed + valid_attributes = { + "emotion": ["angry", "contemptful", "disgusted", "fearful", "happy", "neutral", "sad", "surprised"], + "race": ["asian", "indian", "black", "white", "middle eastern", "latino hispanic"], + "gender": ["male", "female"], + "age group": [ + "young", + "teen", + "adult early twenties", + "adult late twenties", + "adult early thirties", + "adult late thirties", + "adult middle aged", + "older adult", + ], + } + + def get_range_iterator(self): + # create a list of all possible combinations + l1 = self.valid_attributes["emotion"] + l2 = self.valid_attributes["race"] + l3 = self.valid_attributes["gender"] + l4 = self.valid_attributes["age group"] + all_combinations = list(itertools.product(l1, l2, l3, l4)) + return iter(all_combinations) + + def validate(self, value): + human_attributes = value.lower() + if human_attributes not in ["none", "random"]: + # In this case, we need for custom attribute string + + attr_string = human_attributes + for attr_key in ["emotion", "race", "gender", "age group"]: + attr_detected = False + for attr_label in self.valid_attributes[attr_key]: + if attr_string.startswith(attr_label): + attr_string = attr_string[len(attr_label) + 1 :] # noqa: E203 + attr_detected = True + break + + if attr_detected is False: + raise ValueError(f"Expected {value!r} to be one of {self.valid_attributes!r}") + + return value + + def __repr__(self) -> str: + return f"HumanAttributes({self.private_name=} {self.hidden=})" + + def json(self): + return { + "type": HumanAttributes.__name__, + "default": self.default, + "values": self.valid_attributes, + "tooltip": self.tooltip, + } + + +class BytesIOType(Validator): + """ + Validator class for BytesIO. Valid inputs are either: + - bytes + - objects of class BytesIO + - str which can be successfully decoded into BytesIO + """ + + def __init__(self, default=_UNSET, hidden=False, tooltip=None): + self.default = default + self.hidden = hidden + self.tooltip = tooltip + + def validate(self, value: Any) -> BytesIO: + if isinstance(value, str): + try: + # Decode the Base64 string + decoded_bytes = base64.b64decode(value) + # Create a BytesIO stream from the decoded bytes + return BytesIO(decoded_bytes) + except (binascii.Error, ValueError) as e: + raise ValueError(f"Invalid Base64 encoded string: {e}") + elif isinstance(value, bytes): + return BytesIO(value) + elif isinstance(value, BytesIO): + return value + else: + raise TypeError(f"Expected {value!r} to be a Base64 encoded string, bytes, or BytesIO") + + def __repr__(self) -> str: + return f"BytesIOValidator({self.default=}, {self.hidden=})" + + def json(self): + return { + "type": BytesIO.__name__, + "default": self.default, + "tooltip": self.tooltip, + } diff --git a/cosmos3/_src/imaginaire/utils/validator_params.py b/cosmos3/_src/imaginaire/utils/validator_params.py new file mode 100644 index 00000000..abdfbd58 --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/validator_params.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import pprint +import shlex + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.validator import _UNSET, Validator + +""" +Base class for all model parameter classes. + +The primary purpose is to fully validate any input parameter including type, range, etc. +By using custom validators, we can additionally validate complex parameters such as images, text, etc. +Additioally, the class can parse command line arguments into a dictionary of parameters +and create a model parameter class from a dictionary of parameters. + +if default of a validator is _UNSET, the parameter is mandatory and must be provided by the user. +Hence validators without explicit defaults require user input. +""" + + +class ValidatorParams: + """ + factory method to create a model params class from a given api and a dictionary of args + in comparison to createFromCmd, the server can first parse and modify some args, + finally use this factory method to create the model params + """ + + @classmethod + def create(cls, kwargs): + instance = cls() + log.info(f"creating model params class={cls}") + instance.from_kwargs(kwargs) + + val_dict = cls.get_val_dict() + + for key, validator in val_dict.items(): + # Check if validator has no user-provided default (_UNSET) and no value was assigned + if validator.default is _UNSET: + value = getattr(instance, key, _UNSET) + if value is _UNSET: + raise ValueError( + f"mandatory parameter {key} is missing - no default provided and no value assigned by user" + ) + + return instance + + """ + factory method to create a model params class from a command string + """ + + @classmethod + def createFromCmd(cls, cmd: str) -> object: + kwargs = cls.parse(cmd) + return cls.create(kwargs) + + def from_kwargs(self, kwargs): + # most attributes of this class are validators, + # but dervied class could add non-validators + # or some validators might be hidden + # therefore only allow exposed params to be set + for key, value in kwargs.items(): + if key in self.get_exposed_params(): + setattr(self, key, value) + else: + raise ValueError(f"unknown parameter {key} in command line") + + def to_kwargs(self) -> dict: + """for a given config return a dictionary of all the parameters and their values""" + param_names = self.get_exposed_params() + return {key: getattr(self, key) for key in param_names} + + @classmethod + def validate_kwargs(cls, kwargs) -> dict: + """validate a dictionary of args and return the validated dictionary""" + instance = cls.create(kwargs) + return instance.to_kwargs() + + @staticmethod + def parse(cmd: str) -> dict: + """parse a command string into an api command (e.g. text2image) and a dictionary of args""" + args = {} + pairs = shlex.split(cmd) + + for arg in pairs: + key, value = arg.split("=", 1) # Split only on the first '=' + value = value.strip().strip("'") + key = key.strip("--") + args[key.strip()] = value + + log.debug(f"parsed cmd-line: {args}") + return args + + @classmethod + def probe(cls) -> list[str]: + params = cls.get_exposed_params() + log.info(f"exposed params for {cls}: {params}") + return params + + """ + extened version of probe will query from each validator extended information. + This will include default parameters, min, max, step, etc. + """ + + @classmethod + def probe_ex(cls) -> dict: + validator_dict = cls.get_val_dict() + + parameter_info = {key: value.json() for key, value in validator_dict.items() if not value.hidden} + log.info(f"exposed params for {cls}: {json.dumps(parameter_info, indent=4)}") + return parameter_info + + # a model parameter class can also have non exposed parameters: + # we can hide parameters as needed from public API (compare to former exposed_params list in yaml configs in imaginaire3) + # class can also have non-validator attributes + @classmethod + def get_exposed_params(cls) -> list[str]: + # log.debug(f"getting exposed params of {cls.__name__}") + + # the exposed params are repeatedly used for parsing so we cache them + # note that we are caching the exposed params per class in the class hierarchy! + # each class has its own set of exposed params. + # instances of the class will have the same set of exposed params + if "_exposed_params" not in cls.__dict__: + # log.debug(f"creating cache exposed params of {cls.__name__}") + validator_dict = cls.get_val_dict() + + # if a parameter is hidden then probe() can't expose the param + # and the param can't be set anymore + cls._exposed_params = [key for key, value in validator_dict.items() if not value.hidden] + return cls._exposed_params + + def exposed_params_dict(self): + keys = self.get_exposed_params() + out_dict = {key: getattr(self, key) for key in keys} + return out_dict + + """ + returns a dictionary of all validators in the class hierarchy, e.g. for a string validator: + + prompt_validator = String() + + so prompt_validator is the instance of the String validator. the dictionary will be: + + {'prompt_validator': prompt_validator} + """ + + @classmethod + def get_val_dict(cls) -> dict[str, Validator]: + # log.debug(f"getting val dict of {cls.__name__}") + val_dict = {} + if cls is not ValidatorParams: + val_dict.update(cls.__bases__[0].get_val_dict()) + + val_dict.update({key: value for key, value in cls.__dict__.items() if isinstance(value, Validator)}) + + return val_dict + + @classmethod + def debug_print(cls): + pp = pprint.PrettyPrinter(indent=4) + print(f"*********** validator dict for {cls.__name__} ***********") + val_dict = cls.get_val_dict() + pp.pprint(val_dict) + + def __str__(self): + return ", ".join(f"{key}={value}" for key, value in self.__dict__.items()) + + def __repr__(self): + return ", ".join(f"{key}={value}" for key, value in self.__dict__.items()) diff --git a/cosmos3/_src/imaginaire/utils/wandb_util.py b/cosmos3/_src/imaginaire/utils/wandb_util.py new file mode 100644 index 00000000..fd681a8a --- /dev/null +++ b/cosmos3/_src/imaginaire/utils/wandb_util.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +import attrs +import wandb +import wandb.util +from omegaconf import DictConfig + +from cosmos3._src.imaginaire.lazy_config.lazy import LazyConfig +from cosmos3._src.imaginaire.utils import distributed, log, object_store +from cosmos3._src.imaginaire.utils.easy_io import easy_io + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import CheckpointConfig, Config, JobConfig + from cosmos3._src.imaginaire.model import ImaginaireModel + +JOB_INFO = {} + + +def set_wandb_job_info(job_info: dict) -> None: + """Set the job info for the W&B logger. + + Args: + job_info (dict): The job info. + """ + JOB_INFO.update(job_info) + + +@distributed.rank0_only +def init_wandb(config: Config, model: ImaginaireModel) -> None: + """Initialize Weights & Biases (wandb) logger. + + Args: + config (Config): The config object for the Imaginaire codebase. + model (ImaginaireModel): The PyTorch model. + """ + if isinstance(config.job, DictConfig): + from cosmos3._src.imaginaire.config import JobConfig + + config_job = JobConfig(**config.job) + else: + config_job = config.job + config_checkpoint = config.checkpoint + # Try to fetch the W&B job ID for resuming training. + wandb_id = _read_wandb_id(config_job, config_checkpoint) + if wandb_id is None: + # Generate a new W&B job ID. + wandb_id = wandb.util.generate_id() + _write_wandb_id(config_job, config_checkpoint, wandb_id=wandb_id) + log.info(f"Generating new wandb ID: {wandb_id}") + else: + log.info(f"Resuming with existing wandb ID: {wandb_id}") + # refactor config so that wandb better understands it + local_safe_yaml_fp = LazyConfig.save_yaml(config, os.path.join(config_job.path_local, "config.yaml")) + if os.path.exists(local_safe_yaml_fp): + config_resolved = easy_io.load(local_safe_yaml_fp) + else: + config_resolved = attrs.asdict(config) + # Initialize the wandb library. If we attempt to resume an existing run + # but the current user does not have permission to update that run + # (common when re-using an ID created by someone else), fall back to + # creating a fresh run ID and re-initializing. + try: + wandb.init( + force=True, + id=wandb_id, + project=config_job.project, + group=config_job.group, + name=config_job.name, + config=config_resolved, + dir=config_job.path_local, + resume="allow", + mode=config_job.wandb_mode, + ) + except Exception as e: + # Detect common permission / upload errors from wandb and recover + msg = str(e) + if ( + "member role does not have Update Run permission" in msg + or "Error uploading run" in msg + or "returned error 403" in msg + ): + log.warning("W&B run exists but current user lacks update permission; starting a new run instead.") + # Generate and persist a new wandb id, then create a fresh run. + wandb_id = wandb.util.generate_id() + _write_wandb_id(config_job, config_checkpoint, wandb_id=wandb_id) + wandb.init( + force=True, + id=wandb_id, + project=config_job.project, + group=config_job.group, + name=config_job.name, + config=config_resolved, + dir=config_job.path_local, + mode=config_job.wandb_mode, + ) + elif "returned error 401" in msg or "user is not logged in" in msg: + log.warning("W&B authentication failed (401); falling back to offline mode. Error: %s", msg) + wandb.init( + force=True, + id=wandb_id, + project=config_job.project, + group=config_job.group, + name=config_job.name, + config=config_resolved, + dir=config_job.path_local, + mode="offline", + ) + else: + raise + + if wandb.run: + wandb.run.config.update({f"JOB_INFO/{k}": v for k, v in JOB_INFO.items()}, allow_val_change=True) + + +def _read_wandb_id(config_job: JobConfig, config_checkpoint: CheckpointConfig) -> str | None: + """Read the W&B job ID. If it doesn't exist, return None. + + Args: + config_wandb (JobConfig): The config object for the W&B logger. + config_checkpoint (CheckpointConfig): The config object for the checkpointer. + + Returns: + wandb_id (str | None): W&B job ID. + """ + wandb_id = None + if config_checkpoint.load_from_object_store.enabled: + object_store_loader = object_store.ObjectStore(config_checkpoint.load_from_object_store) + wandb_id_path = f"{config_job.path}/wandb_id.txt" + if object_store_loader.object_exists(key=wandb_id_path): + wandb_id = object_store_loader.load_object(key=wandb_id_path, type="text").strip() + else: + wandb_id_path = f"{config_job.path_local}/wandb_id.txt" + if os.path.isfile(wandb_id_path): + wandb_id = open(wandb_id_path).read().strip() + return wandb_id + + +def _write_wandb_id(config_job: JobConfig, config_checkpoint: CheckpointConfig, wandb_id: str) -> None: + """Write the generated W&B job ID. + + Args: + config_wandb (JobConfig): The config object for the W&B logger. + config_checkpoint (CheckpointConfig): The config object for the checkpointer. + wandb_id (str): The W&B job ID. + """ + content = f"{wandb_id}\n" + if config_checkpoint.save_to_object_store.enabled: + object_store_saver = object_store.ObjectStore(config_checkpoint.save_to_object_store) + wandb_id_path = f"{config_job.path}/wandb_id.txt" + object_store_saver.save_object(content, key=wandb_id_path, type="text") + else: + wandb_id_path = f"{config_job.path_local}/wandb_id.txt" + with open(wandb_id_path, "w") as file: + file.write(content) diff --git a/cosmos3/_src/imaginaire/visualize/__init__.py b/cosmos3/_src/imaginaire/visualize/__init__.py new file mode 100644 index 00000000..f5f9b5f8 --- /dev/null +++ b/cosmos3/_src/imaginaire/visualize/__init__.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.visualize.img import save_batch_img, show_batch_img # noqa: F401 diff --git a/cosmos3/_src/imaginaire/visualize/img.py b/cosmos3/_src/imaginaire/visualize/img.py new file mode 100644 index 00000000..b5cf296c --- /dev/null +++ b/cosmos3/_src/imaginaire/visualize/img.py @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""image visualization utilities. + +based on https://gitlab.com/qsh.zh/jam/-/blob/master/jamviz/img.py MIT License +""" + +import os +from typing import Union + +import numpy as np +import torch +from einops import rearrange +from PIL import Image +from torchvision.utils import make_grid + +__all__ = [ + "show_batch_img", + "save_batch_img", +] + + +def _reshape_viz_batch_img(img_data: torch.Tensor | np.ndarray, shape: int | str = 7) -> tuple: + """ + Reshapes a batch of images for visualization, organizing them into a grid format. + + Args: + img_data (torch.Tensor | np.ndarray): The image data to be reshaped, can be either a PyTorch tensor or a NumPy array. + shape (int | str, optional): Defines the layout of the grid. If an integer is provided, it specifies both the number of rows and columns. If a string is provided in the format 'nrowxncol', it parses to individual row and column numbers. Defaults to 7. + + Returns: + tuple: A tuple containing: + img (np.ndarray | torch.Tensor): The image data arranged in grid format. + nrow (int): Number of rows in the grid. + ncol (int): Number of columns in the grid. + + Raises: + RuntimeError: If the shape parameter is neither an int nor a string, or if it's a string that doesn't contain 'x'. + + Example: + >>> tensor_images = torch.rand(64, 3, 28, 28) # Example tensor of 64 images + >>> img_grid, rows, cols = _reshape_viz_batch_img(tensor_images, '8x8') + >>> img_grid.shape + (224, 224, 3) + """ + if isinstance(shape, int): + nrow, ncol = shape, shape + elif isinstance(shape, str): + if "x" not in shape: + nrow, ncol = int(shape), int(shape) + else: + shape = shape.split("x") + nrow, ncol = int(shape[0]), int(shape[1]) + else: + raise RuntimeError(f"shape {shape} not support") + if isinstance(img_data, torch.Tensor): + assert img_data.shape[1] in [1, 3] + grid_img = make_grid(img_data[: nrow * ncol].detach().cpu(), ncol) + img = grid_img.permute(1, 2, 0) + elif isinstance(img_data, np.ndarray): + if img_data.shape[1] in [1, 3]: + img = rearrange(img_data[: nrow * ncol], "(b t) c h w -> (b h) (t w) c", b=nrow) + else: + img = rearrange(img_data[: nrow * ncol], "(b t) h w c -> (b h) (t w) c", b=nrow) + return img, nrow, ncol + + +def show_batch_img( + img_data: torch.Tensor | np.ndarray, + shape: int | str = 7, + grid: int = 3, + is_n1p1: bool = False, + auto_n1p1: bool = True, +) -> None: + """ + Displays a batch of images using matplotlib after arranging them into a specified grid layout. + + Args: + img_data (torch.Tensor | np.ndarray): The image data to be displayed. + shape (int | str, optional): The grid shape to organize the images. Defaults to 7. + grid (int, optional): Scaling factor for each image in the grid, affecting the overall size of the displayed figure. Defaults to 3. + is_n1p1 (bool, optional): Whether to normalize the images from [-1, 1] to [0, 1] for visualization. Defaults to False. + auto_n1p1 (bool, optional): If true, automatically adjusts images from [-1, 1] to [0, 1] based on minimum pixel value detection. Defaults to True. + + Returns: + None: This function does not return anything but displays the image grid using matplotlib. + + Example: + >>> tensor_images = torch.rand(64, 3, 28, 28) # Example tensor of 64 images + >>> show_batch_img(tensor_images, '8x8') + """ + import matplotlib.pyplot as plt + + if is_n1p1: + img_data = (img_data + 1) / 2 + else: + if auto_n1p1: + if isinstance(img_data, torch.Tensor): + if img_data.min().item() < -0.5: + img_data = (img_data + 1) / 2 + elif isinstance(img_data, np.ndarray): + if np.min(img_data) < -0.5: + img_data = (img_data + 1) / 2 + img, nrow, ncol = _reshape_viz_batch_img(img_data, shape) + plt.figure(figsize=(ncol * grid, nrow * grid)) + plt.axis("off") + plt.imshow(img) + + +def save_batch_img(fpath: str, img_data: Union[torch.Tensor, np.ndarray], shape: Union[int, str] = 7) -> None: + """ + Saves a batch of images to a file after arranging them into a grid format. Handles both PyTorch tensors and NumPy arrays as input. + + Args: + fpath (str): File path where the image will be saved. + img_data (Union[torch.Tensor, np.ndarray]): The image data to be saved. Can be a PyTorch tensor or a NumPy array. + shape (Union[int, str], optional): The grid shape to organize the images. Can be an integer specifying equal number of rows and columns, or a string specifying 'nrowxncol'. Defaults to 7. + + Returns: + None: This function does not return anything but saves the image to the specified file path. + + Raises: + RuntimeError: If the input shape is neither an integer nor a string, or it does not include 'x' when provided as a string. + + Example: + >>> tensor_images = torch.rand(64, 3, 28, 28) # Example tensor of 64 images + >>> save_batch_img('path/to/save/image.png', tensor_images, '8x8') + # This saves the image grid to 'path/to/save/image.png' + """ + img, _, _ = _reshape_viz_batch_img(img_data, shape) + if isinstance(img, np.ndarray): + img = torch.from_numpy(img) + ndarr = img.mul(255).add_(0.5).clamp_(0, 255).to("cpu", torch.uint8).numpy() + im = Image.fromarray(ndarr) + os.makedirs(os.path.dirname(fpath), exist_ok=True) + im.save(fpath) diff --git a/cosmos3/_src/imaginaire/visualize/video.py b/cosmos3/_src/imaginaire/visualize/video.py new file mode 100644 index 00000000..20b8f403 --- /dev/null +++ b/cosmos3/_src/imaginaire/visualize/video.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO, Any, Union + +import numpy as np +import torch +from einops import rearrange +from PIL import Image as PILImage +from torch import Tensor + +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +def save_video(grid, video_name, fps=30): + import cv2 + import ffmpegcv + + grid = (grid * 255).astype(np.uint8) + grid = np.transpose(grid, (1, 2, 3, 0)) + with ffmpegcv.VideoWriter(video_name, "h264", fps) as writer: + for frame in grid: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + + writer.write(frame) + + +def save_img_or_video( + sample: Tensor, # [C,T,H,W] in [0,1] range + save_fp_wo_ext: Union[str, IO[Any]], + fps: int = 24, + quality=None, + ffmpeg_params=None, + **kwargs, +) -> None: + """ + Save a tensor as an image or video file based on shape + + Args: + sample (Tensor): Input tensor with shape (C, T, H, W) in [0, 1] range. + save_fp_wo_ext (Union[str, IO[Any]]): File path without extension or file-like object. + fps (int): Frames per second for video. Default is 24. + """ + assert sample.ndim == 4, "Only support 4D tensor" + assert isinstance(save_fp_wo_ext, str) or hasattr(save_fp_wo_ext, "write"), ( + "save_fp_wo_ext must be a string or file-like object" + ) + + if torch.is_floating_point(sample): + sample = sample.clamp(0, 1) + else: + assert sample.dtype == torch.uint8, "Only support uint8 tensor" + sample = sample.float().div(255) + + if ffmpeg_params is not None: + kwargs["ffmpeg_params"] = ffmpeg_params + + if sample.shape[1] == 1: + save_obj = PILImage.fromarray( + rearrange((sample.cpu().float().numpy() * 255), "c 1 h w -> h w c").astype(np.uint8), + mode="RGB", + ) + ext = ".jpg" if isinstance(save_fp_wo_ext, str) else "" + easy_io.dump( + save_obj, + f"{save_fp_wo_ext}{ext}" if isinstance(save_fp_wo_ext, str) else save_fp_wo_ext, + file_format="jpg", + format="JPEG", + quality=85 if quality is None else quality, + **kwargs, + ) + else: + if quality is not None: + kwargs["quality"] = quality + save_obj = rearrange((sample.cpu().float().numpy() * 255), "c t h w -> t h w c").astype(np.uint8) + ext = ".mp4" if isinstance(save_fp_wo_ext, str) else "" + easy_io.dump( + save_obj, + f"{save_fp_wo_ext}{ext}" if isinstance(save_fp_wo_ext, str) else save_fp_wo_ext, + file_format="mp4", + format="mp4", + fps=fps, + **kwargs, + ) diff --git a/cosmos3/_src/vfm/__init__.py b/cosmos3/_src/vfm/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/callbacks/__init__.py b/cosmos3/_src/vfm/callbacks/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/callbacks/compile_tokenizer.py b/cosmos3/_src/vfm/callbacks/compile_tokenizer.py new file mode 100644 index 00000000..70f8860f --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/compile_tokenizer.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Training callback that defers AOT compilation of the VAE tokenizer. + +The actual compilation logic lives in +:meth:`~projects.cosmos3.vfm.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface.compile_encode`. +This module provides a :class:`CompileTokenizer` callback that invokes it +at the right point during training (after ``compile_after_iterations`` +steps, to avoid NCCL timeouts during CUDA/cuDNN warm-up). + +Typical config usage +-------------------- +.. code-block:: python + + CompileTokenizer( + enabled=True, + compile_after_iterations=3, + warmup_resolutions=["256", "480", "720"], + ) +""" + +from collections.abc import Sequence + +import torch + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + + +class CompileTokenizer(Callback): + """Training callback that defers AOT compilation of the VAE tokenizer. + + Hooks into ``on_training_step_start``. On the + ``compile_after_iterations``-th step it calls + ``Wan2pt2VAEInterface.compile_encode`` to compile and load all chunk + variants. Every subsequent step is a no-op. + """ + + def __init__( + self, + enabled: bool = False, + compile_after_iterations: int = 3, + warmup_resolutions: Sequence[str] | None = None, + ): + """ + Args: + enabled: Master switch. When ``False`` the callback is a + complete no-op and no compilation occurs. + compile_after_iterations: How many training steps to skip + before triggering compilation. The default (3) lets CUDA + context setup and Transformer compilation finish first. + warmup_resolutions: Resolution keys (e.g. ``["256", "480", "720"]``) + to AOT-compile. Should include every resolution used in + training. Must be a non-empty list when *enabled* is ``True``. + """ + super().__init__() + self.enabled: bool = enabled + self.compile_after_iterations: int = compile_after_iterations + self.skip_counter: int = 0 + self.warmup_resolutions: Sequence[str] | None = warmup_resolutions + + if self.enabled: + if self.warmup_resolutions is None: + raise ValueError("warmup_resolutions must be provided when enabled, got None") + if len(self.warmup_resolutions) == 0: + raise ValueError("warmup_resolutions must be a non-empty list when enabled, got an empty list") + + def on_training_step_start( + self, model: OmniMoTModel, data_batch: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + """Called at the start of every training step. + + On the ``compile_after_iterations``-th call, triggers AOT compilation + via ``tokenizer.compile_encode``. + + Args: + model: The OmniMoTModel whose ``tokenizer_vision_gen`` will be compiled. + data_batch: Current training batch (unused, required by Callback API). + iteration: Current training iteration (unused; we track our own counter + via ``skip_counter`` because this callback may be registered after + iteration 0). + """ + if not self.enabled: + return + + tokenizer = model.tokenizer_vision_gen + + if isinstance(tokenizer, torch.jit.ScriptModule): + log.critical( + f"The Tokenizer model {type(tokenizer)} is a JIT model, " + "which is not compilable. The Tokenizer will not be compiled.", + rank0_only=False, + ) + self.enabled = False + return + + if self.skip_counter == self.compile_after_iterations: + if self.warmup_resolutions is not None: + tokenizer.compile_encode( + self.warmup_resolutions, + output_dir=self.config.job.path_local, + ) + + self.skip_counter += 1 diff --git a/cosmos3/_src/vfm/callbacks/dataloader_state.py b/cosmos3/_src/vfm/callbacks/dataloader_state.py new file mode 100644 index 00000000..d1909b2e --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/dataloader_state.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +import torch + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import Callback + + +@dataclass +class NoReplaceShardlistState: + epoch: int = 0 + index: int = 0 + + +class DataLoaderStateCallback(Callback): + checkpoint_component: str = "dataloader" + + def __init__( + self, + distributor_type: str | None = None, + ) -> None: + super().__init__() + self.distributor_type = distributor_type + self.config: Any = None + self.state: dict[int, NoReplaceShardlistState] = {} + self.verbose = True + + def _update_state_from_batch(self, data_batch: dict[str, torch.Tensor]) -> None: + worker_ids = data_batch["sample_worker_id"].tolist() # [B] + epochs = data_batch["sample_epoch"].tolist() # [B] + indices = data_batch["sample_index"].tolist() # [B] + for worker_id, epoch, index in zip(worker_ids, epochs, indices, strict=True): + if worker_id not in self.state: + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + + elif self.state[worker_id].epoch < epoch or ( + self.state[worker_id].index < index and self.state[worker_id].epoch == epoch + ): + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + + def on_training_step_batch_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.distributor_type == "no_replace": + self._update_state_from_batch(data_batch) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.distributor_type == "no_replace": + if self.verbose: + if iteration % self.config.trainer.logging_iter == 0: + msg = "\n" + for wid, state in self.state.items(): + msg += f"worker {wid}: epoch={state.epoch}, index={state.index}\n" + log.info(msg) + + def has_checkpoint_state(self) -> bool: + return self.distributor_type == "no_replace" + + def state_dict(self) -> dict[int, dict[str, int]]: + if self.distributor_type != "no_replace": + return {} + + state_dict: dict[int, dict[str, int]] = {} + for worker_id, per_worker_state in self.state.items(): + state_dict[worker_id] = {"epoch": per_worker_state.epoch, "index": per_worker_state.index} + log.info( + f"Saved dataloader state for worker {worker_id}: " + f"epoch={per_worker_state.epoch}, index={per_worker_state.index}" + ) + return state_dict + + def load_state_dict(self, state_dict: dict[int, dict[str, int]]) -> None: + if self.distributor_type != "no_replace": + return + + if not state_dict: + log.info("No dataloader state found in checkpoint") + return + + self.state = {} + for worker_id, per_worker_state in state_dict.items(): + epoch = per_worker_state["epoch"] + index = per_worker_state["index"] + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + os.environ[f"NSL_STATE_WORKER_{worker_id}_EPOCH"] = str(epoch) + os.environ[f"NSL_STATE_WORKER_{worker_id}_INDEX"] = str(index) + log.info(f"Loaded no replace dataloader state for worker {worker_id}: epoch={epoch}, index={index}") + + def on_save_checkpoint(self, model: ImaginaireModel, state_dict: dict[str, Any]) -> None: + if self.distributor_type == "no_replace" and "dataloader" not in state_dict: + state_dict["dataloader"] = self.state_dict() + + def on_load_checkpoint(self, model: ImaginaireModel, state_dict: dict[str, Any]) -> None: + if "dataloader" in state_dict: + self.load_state_dict(state_dict["dataloader"]) diff --git a/cosmos3/_src/vfm/callbacks/dataloading_monitor.py b/cosmos3/_src/vfm/callbacks/dataloading_monitor.py new file mode 100644 index 00000000..77e3a1b4 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/dataloading_monitor.py @@ -0,0 +1,538 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from collections import defaultdict + +import psutil +import torch +import torch.distributed as dist +import wandb + +from cosmos3._src.imaginaire.datasets.webdataset.utils.stream import ( + ENABLE_STREAM_WANDB, + WATCHDOG_ENABLED, + collect_throughput_ipc_stats, +) +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.datasets.joint_dataloader import _PackingMetrics + +_AGG_COUNT, _AGG_SUM, _AGG_MIN, _AGG_MAX = 0, 1, 2, 3 +_AGG_COLS = 4 + + +class DetailedDataLoadingSpeedMonitor(Callback): + def __init__( + self, + every_n: int, + step_size: int = 1, + save_s3: bool = False, + ): + self.every_n = every_n + self.step_size = step_size + self.should_run = False + self.start_dataloading_time = None + self.dataloading_time = None + self.name = self.__class__.__name__ + self.save_s3 = save_s3 + self.time_delta_list = [] + self.memory_consumption_list = [] + self.memory_consumption_percentage_list = [] + self._pending_time_delta = None + self.dataloading_time_per_dataset = {} + self._worker_batch_times = [] + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times: dict[str, list[float]] = defaultdict(list) + self._worker_times_by_ds_wid: dict[tuple[str, int], list[float]] = defaultdict(list) + self._dataset_scalar_stats: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats: dict[str, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list)) + + def on_before_dataloading(self, iteration: int = 0) -> None: + # We want to run it one iteration before on_training_step_start should_run is set to True. + global_step = iteration // self.step_size + self.should_run = (global_step + 1) % self.every_n == 0 + self.start_dataloading_time = time.time() + + def on_after_dataloading(self, iteration: int = 0) -> None: + self._pending_time_delta = time.time() - self.start_dataloading_time + self.time_delta_list.append(self._pending_time_delta) + memory = psutil.virtual_memory() + self.memory_consumption_list.append(memory.used / (1024**3)) + self.memory_consumption_percentage_list.append(memory.percent) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + dataset_name = data_batch.get("dataset_name", ["default"])[0] + if self._pending_time_delta is not None: + if dataset_name not in self.dataloading_time_per_dataset: + self.dataloading_time_per_dataset[dataset_name] = [] + self.dataloading_time_per_dataset[dataset_name].append(self._pending_time_delta) + self._pending_time_delta = None + + for batch_key, _, agg_type in _PackingMetrics.STATS_SPEC: + if batch_key not in data_batch: + continue + val = int(data_batch[batch_key]) + if agg_type == "scalar": + self._dataset_scalar_stats[batch_key][dataset_name] += val + else: + self._dataset_list_stats[batch_key][dataset_name].append(val) + + if "_worker_batch_time" in data_batch: + bt = float(data_batch["_worker_batch_time"]) + self._worker_batch_times.append(bt) + wid = int(data_batch.get("_worker_id", 0)) + self._worker_times_by_ds_wid[(dataset_name, wid)].append(bt) + if "_worker_aug_time" in data_batch: + self._worker_aug_times.append(float(data_batch["_worker_aug_time"])) + if "_worker_io_time" in data_batch: + self._worker_io_times.append(float(data_batch["_worker_io_time"])) + if "_worker_aug_step_times" in data_batch: + for step_name, t in data_batch["_worker_aug_step_times"].items(): + self._worker_aug_step_times[step_name].append(float(t)) + + if self.should_run: + # Convert list to tensor on GPU for gathering + local_times = torch.tensor(self.time_delta_list).cuda() # [num_iters] + local_memory_consumption = torch.tensor(self.memory_consumption_list).cuda() # [num_iters] + local_memory_consumption_percentage = torch.tensor( + self.memory_consumption_percentage_list + ).cuda() # [num_iters] + iteration_count = len(self.time_delta_list) + self.time_delta_list = [] # Reset the list + self.memory_consumption_list = [] + self.memory_consumption_percentage_list = [] + + # Gather all times from all ranks + # Each tensor in the list has shape (num_iterations,) + gathered_times_list = distributed.all_gather_tensor(local_times) # list of [num_iters], len=world_size + + # Stack to get shape (world_size, num_iterations) + all_times = torch.stack(gathered_times_list) # [world_size,num_iters] + + # Calculate per-rank statistics + # dim=1 is across iterations + rank_means = torch.mean(all_times, dim=1) # [world_size] + rank_maxes = torch.max(all_times, dim=1).values # [world_size] + + wandb_info = {f"{self.name}_mean/dataloading_{k:03d}": v.item() for k, v in enumerate(rank_means)} + wandb_info.update({f"{self.name}_max/dataloading_{k:03d}": v.item() for k, v in enumerate(rank_maxes)}) + + gathered_memory_consumption = distributed.all_gather_tensor( + local_memory_consumption + ) # list of [num_iters], len=world_size + gathered_memory_consumption_percentage = distributed.all_gather_tensor( + local_memory_consumption_percentage + ) # list of [num_iters], len=world_size + + wandb_info.update( + { + f"{self.name}_mean/memory_consumption_gb_{k:03d}": v.mean().item() + for k, v in enumerate(gathered_memory_consumption) + } + ) + wandb_info.update( + { + f"{self.name}_mean/memory_consumption_percentage_{k:03d}": v.mean().item() + for k, v in enumerate(gathered_memory_consumption_percentage) + } + ) + + wandb_info[f"{self.name}_mean/memory_consumption_gb_mean"] = ( + torch.stack(gathered_memory_consumption).mean().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_mean/memory_consumption_percentage_mean"] = ( + torch.stack(gathered_memory_consumption_percentage).mean().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_max/memory_consumption_gb_max"] = ( + torch.stack(gathered_memory_consumption).max().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_max/memory_consumption_percentage_max"] = ( + torch.stack(gathered_memory_consumption_percentage).max().item() # [world_size,num_iters] + ) + + # Identify slowest rank based on mean time + slowest_dataloading_rank_id = torch.argmax(rank_means) # [] + max_dataloading = torch.max(rank_means) # [] + + # Calculate sum of max times across all iterations (new metric) + # Max across ranks for each iteration (dim=0) + max_per_iter = torch.max(all_times, dim=0).values # [num_iters] + sum_of_max_times = torch.sum(max_per_iter).item() / iteration_count + + wandb_info.update( + { + "slowest_rank/slowest_dataloading_rank": slowest_dataloading_rank_id.item(), + "slowest_rank/slowest_dataloading_time": max_dataloading.item(), + "slowest_rank/sum_of_max_dataloading_time_per_iteration": sum_of_max_times, + } + ) + + # 1. Gather and log stream throughput and watchdog reconnect stats for `stream_throughput` metrics + self._gather_and_log_stream_throughput(wandb_info) + + # Only all_gather_object to get name indices (dataset names, aug-step names, worker-balance keys) across all ranks + # Later methods 2-4 use efficient all_gather_tensor to gather tensor data, then compute statistics and log metrics + ds_index, aug_index, dswid_index = self._discover_name_indices() + + # 2.Gather and log per-dataset dataloading wait times for `dl_wait_time_per_dataset` metrics + self._gather_and_log_per_dataset_time(wandb_info, ds_index) + + # 3. Gather and log per-dataset sampling stats for `dl_packing_stats` metrics + self._gather_and_log_packing_stats(wandb_info, ds_index) + + # 4. Gather and log worker timing metrics for `dl_worker_batch_time`, `dl_worker_balance_per_dataset`, `dl_worker_augmentation` metrics + self._gather_and_log_worker_timing(wandb_info, dswid_index, aug_index) + + if wandb.run: + wandb.log(wandb_info, step=iteration) + + if self.save_s3 and distributed.is_rank0(): + easy_io.dump( + wandb_info, + f"s3://rundir/{self.name}/iter_{iteration:09d}.yaml", + ) + + def _discover_name_indices( + self, + ) -> tuple[dict[str, int], dict[str, int], dict[str, int]]: + """Discover the global union of dataset, aug-step, and worker-balance names. + + Performs a single ``all_gather_object`` call to exchange short string + lists across all ranks and returns deterministic index mappings. + + Returns: + ds_index: ``{dataset_name: col_idx}`` for per-dataset tensors. + aug_index: ``{step_name: col_idx}`` for augmentation step tensors. + dswid_index: ``{"ds|wid": col_idx}`` for worker-balance tensors. + """ + local_ds_names: set[str] = set(self.dataloading_time_per_dataset.keys()) + for key_dict in self._dataset_scalar_stats.values(): + local_ds_names.update(key_dict.keys()) + for key_dict in self._dataset_list_stats.values(): + local_ds_names.update(key_dict.keys()) + + local_names = { + "datasets": sorted(local_ds_names), + "aug_steps": sorted(self._worker_aug_step_times.keys()), + "ds_wid": sorted(f"{ds}|{wid}" for ds, wid in self._worker_times_by_ds_wid.keys()), + } + all_names: list[dict] = [{} for _ in range(dist.get_world_size())] # len=world_size + dist.all_gather_object(all_names, local_names) + + union_ds = sorted({n for r in all_names for n in r.get("datasets", [])}) + ds_index = {name: i for i, name in enumerate(union_ds)} + + union_aug = sorted({n for r in all_names for n in r.get("aug_steps", [])}) + aug_index = {name: i for i, name in enumerate(union_aug)} + + union_dswid = sorted({n for r in all_names for n in r.get("ds_wid", [])}) + dswid_index = {name: i for i, name in enumerate(union_dswid)} + + return ds_index, aug_index, dswid_index + + def _gather_and_log_per_dataset_time(self, wandb_info: dict, ds_index: dict[str, int]) -> None: + """Gather per-dataset dataloading wait times via ``all_gather_tensor``.""" + N = len(ds_index) + if N == 0: + self.dataloading_time_per_dataset = {} + return + + local_ds_time = torch.full((N,), float("nan"), dtype=torch.float64).cuda() # [num_datasets] + for ds, times in self.dataloading_time_per_dataset.items(): + if ds in ds_index: + local_ds_time[ds_index[ds]] = sum(times) / len(times) + + all_ds_time = self._gather_list_stats(local_ds_time) # [world_size, num_datasets] + for ds, i in ds_index.items(): + col = all_ds_time[:, i] # [world_size] + valid = col[~col.isnan()] # [<=world_size] + if len(valid) > 0: + wandb_info[f"dl_wait_time_per_dataset/{ds}_mean"] = valid.mean().item() + wandb_info[f"dl_wait_time_per_dataset/{ds}_max"] = valid.max().item() + + self.dataloading_time_per_dataset = {} + + def _gather_and_log_packing_stats(self, wandb_info: dict, ds_index: dict[str, int]) -> None: + """Gather packing diagnostics via ``all_gather_tensor``, driven by ``_PackingMetrics.STATS_SPEC``.""" + _STATS = "dl_packing_stats" + N = len(ds_index) + if N == 0: + self._dataset_scalar_stats = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats = defaultdict(lambda: defaultdict(list)) + return + + for batch_key, wandb_suffix, _ in _PackingMetrics.STATS_SPEC: + if batch_key == "_num_tokens": + # Token fraction: gather per-rank token sums, compute each dataset's share of total + local_v = torch.zeros(N, dtype=torch.float64).cuda() # [num_datasets] + for ds, i in ds_index.items(): + local_v[i] = self._dataset_scalar_stats.get(batch_key, {}).get(ds, 0) + all_v = self._gather_list_stats(local_v) # [world_size, num_datasets] + global_tokens = all_v.sum(dim=0) # [num_datasets] + total = global_tokens.sum().item() + for ds, i in ds_index.items(): + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}"] = global_tokens[i].item() / total if total > 0 else 0.0 + + elif batch_key == "_dropped_count": + # Dropped samples: gather per-rank counts, report global total per dataset + local_v = torch.zeros(N, dtype=torch.float64).cuda() # [num_datasets] + for ds, i in ds_index.items(): + local_v[i] = self._dataset_scalar_stats.get(batch_key, {}).get(ds, 0) + all_v = self._gather_list_stats(local_v) # [world_size, num_datasets] + for ds, i in ds_index.items(): + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_total"] = int(all_v[:, i].sum().item()) + + else: + # Per-batch distributions (_num_samples, _from_buffer, _from_workers, _buffer_size). + # Each rank packs [count, sum, min, max]; reduce to weighted global mean/min/max. + local_t = torch.full( + (N, _AGG_COLS), float("nan"), dtype=torch.float64 + ).cuda() # [num_datasets, _AGG_COLS] + for ds, i in ds_index.items(): + vals = self._dataset_list_stats.get(batch_key, {}).get(ds, []) + if vals: + local_t[i] = torch.tensor([len(vals), sum(vals), min(vals), max(vals)], dtype=torch.float64) + all_t = self._gather_list_stats(local_t) # [world_size, num_datasets, _AGG_COLS] + for ds, i in ds_index.items(): + result = self._reduce_agg_column(all_t[:, i, :]) + if result: + mean_val, min_val, max_val = result + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_mean"] = mean_val + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_min"] = min_val + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_max"] = max_val + + self._dataset_scalar_stats = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats = defaultdict(lambda: defaultdict(list)) + + def _gather_and_log_stream_throughput(self, wandb_info: dict) -> None: + """Gather stream throughput and watchdog reconnect stats via IPC files.""" + if not ENABLE_STREAM_WANDB: + return + + tp_keys = ["MBps"] + if WATCHDOG_ENABLED: + tp_keys.append("watchdog_reconnects") + tp_stats = collect_throughput_ipc_stats() + local_tp = torch.tensor([tp_stats.get(k, 0.0) for k in tp_keys]).cuda() # [num_metrics] + gathered_tp = distributed.all_gather_tensor(local_tp) # list of [num_metrics], len=world_size + all_tp = torch.stack(gathered_tp) # [world_size, num_metrics] + + for ki, k in enumerate(tp_keys): + col = all_tp[:, ki] # [world_size] + wandb_info[f"stream_throughput/{k}_mean"] = col.mean().item() + wandb_info[f"stream_throughput/{k}_min"] = col.min().item() + wandb_info[f"stream_throughput/{k}_max"] = col.max().item() + if k == "watchdog_reconnects": + wandb_info[f"stream_throughput/{k}_sum"] = col.sum().item() + + mbps_col = all_tp[:, 0] # [world_size] + slowest_throughput_rank = mbps_col.argmin().item() + wandb_info["slowest_rank/slowest_stream_throughput_rank"] = slowest_throughput_rank + + @staticmethod + def _gather_list_stats(local: torch.Tensor) -> torch.Tensor: + """all_gather_tensor + stack, returning [world_size, *local.shape].""" + return torch.stack(distributed.all_gather_tensor(local)) + + @staticmethod + def _reduce_agg_column(col: torch.Tensor) -> tuple[float, float, float] | None: + """From a [world_size, _AGG_COLS] slice, return (mean, min, max) or None if empty. + + Each row is [count, sum, min, max] from one rank. Rows with NaN count + are ranks that had no data for this key. + + Used for metrics where each rank accumulates a variable-length list of + values (e.g. samples_per_batch, buffer_size, per-aug-step times) and we + need a correct weighted global mean rather than a simple average of + per-rank means. The sum/count columns make this possible. + + Callers: ``_gather_and_log_packing_stats`` (list-type metrics) and + ``_gather_and_log_worker_timing`` (per-aug-step breakdown). + """ + valid = col[~col[:, _AGG_COUNT].isnan()] + if len(valid) == 0: + return None + total_count = valid[:, _AGG_COUNT].sum().item() + total_sum = valid[:, _AGG_SUM].sum().item() + if total_count == 0: + return None + return ( + total_sum / total_count, + valid[:, _AGG_MIN].min().item(), + valid[:, _AGG_MAX].max().item(), + ) + + def _gather_and_log_worker_timing( + self, wandb_info: dict, dswid_index: dict[str, int], aug_index: dict[str, int] + ) -> None: + """Gather worker timing from all ranks and log percentile metrics. + + All metrics here are worker-side measurements — time spent inside + DataLoader worker processes producing batches. This is different from + DetailedDataLoadingSpeedMonitor or dl_wait_time_per_dataset/ metrics which measure main-process wall-clock time, + This can help identify if the bottleneck is in the dataloader worker processes or in the main process, + for example waiting for a packed output batch from the JointDataLoader + + Logged metrics: + Section 1 – dl_worker_batch_time/ + Every individual batch time from every worker from every rank, all + thrown into one pool. One data point = one batch produced by one + worker at one step. Computes p50/p90/p99/max/mean of that pool. + Answers: What is the tail latency to produce a batch? + + Section 2 – dl_worker_balance_per_dataset/ + First computes each worker's average batch time over the logging + window. One data point = one worker's mean over several batches. + Then gathers these per-worker averages across all ranks, grouped by + dataset. Computes mean/std/min/max of those averages. + Answers: Are some workers consistently slower than others within + each sub-dataloader? + + Section 3 – dl_worker_augmentation/ + Unified augmentation profiling. Contains: + - total_aug_mean|min|max – total augmentation time per batch + - total_io_mean|min|max – I/O time per batch (batch_time minus aug_time) + - aug_fraction_mean, io_fraction_mean – what fraction of batch time is spent in augmentation vs I/O + - aug_steps/{StepName}_mean|min|max – per-augmentor-step breakdown + (e.g. VideoParsingWithFullFrames for video decode, + TextTokenizerTransform for text tokenization). + All use mean/min/max globally across all ranks. + Answers: Is the bottleneck in augmentations or downloads, and + which augmentor step dominates? + + Note: dl_packing_stats/ is logged from on_training_step_end (not here). + It reports token_fraction, samples_per_batch, from_buffer, from_workers, buffer_size, and dropped_total per dataset — useful for tuning num_workers/batch_size/prefetch per dataloader. + """ + if not self._worker_batch_times: + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times = defaultdict(list) + self._worker_times_by_ds_wid = defaultdict(list) + return + + _PERCENTILES = [0.50, 0.90, 0.99] + _PNAMES = ["p50", "p90", "p99"] + + # Gather raw batch times across all ranks + local_bt = torch.tensor(self._worker_batch_times, dtype=torch.float32).cuda() # [num_batches_local] + gathered_bt = distributed.all_gather_tensor(local_bt) # list of [num_batches_local], len=world_size + all_bt = torch.cat(gathered_bt) # [num_batches_all_ranks] + + # Section 1: global batch time percentiles + _BATCH_PREFIX = "dl_worker_batch_time" + for pval, pname in zip(_PERCENTILES, _PNAMES): + wandb_info[f"{_BATCH_PREFIX}/{pname}"] = all_bt.quantile(pval).item() + wandb_info[f"{_BATCH_PREFIX}/max"] = all_bt.max().item() + wandb_info[f"{_BATCH_PREFIX}/mean"] = all_bt.mean().item() + + # Section 2: per-dataloader worker balance + # Each rank fills its (dataset, worker_id) slots with that worker's + # mean batch time; NaN marks absent slots. After all_gather we group + # by dataset and compute cross-rank statistics. + + _BALANCE_PREFIX = "dl_worker_balance_per_dataset" + if dswid_index: + N_dswid = len(dswid_index) + local_pw = torch.full((N_dswid,), float("nan"), dtype=torch.float64).cuda() # [num_ds_worker_pairs] + for (ds_name, wid), ts in self._worker_times_by_ds_wid.items(): + key = f"{ds_name}|{wid}" + if key in dswid_index: + local_pw[dswid_index[key]] = sum(ts) / len(ts) + + all_pw = self._gather_list_stats(local_pw) # [world_size, num_ds_worker_pairs] + + # Pass 1: collect all valid per-worker means, grouped by dataset + ds_worker_vals: dict[str, list[float]] = defaultdict(list) + for key, idx in dswid_index.items(): + ds_name = key.rsplit("|", 1)[0] + col = all_pw[:, idx] # [world_size] + valid = col[~col.isnan()] # [<=world_size] + ds_worker_vals[ds_name].extend(valid.tolist()) + + # Pass 2: log per-dataset worker balance statistics + for ds_name in sorted(ds_worker_vals): + pw_means = ds_worker_vals[ds_name] + if not pw_means: + continue + pw_t = torch.tensor(pw_means, dtype=torch.float32).cuda() # [num_workers_for_ds] + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_mean"] = pw_t.mean().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_std"] = pw_t.std().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_min"] = pw_t.min().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_max"] = pw_t.max().item() + + # Section 3: augmentation profiling (total aug/io + per-step breakdown) + _AUG_PREFIX = "dl_worker_augmentation" + + if self._worker_aug_times: + local_aug = torch.tensor(self._worker_aug_times, dtype=torch.float32).cuda() # [num_batches_local] + all_aug = torch.cat(distributed.all_gather_tensor(local_aug)) # [num_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/total_aug_mean"] = all_aug.mean().item() + wandb_info[f"{_AUG_PREFIX}/total_aug_min"] = all_aug.min().item() + wandb_info[f"{_AUG_PREFIX}/total_aug_max"] = all_aug.max().item() + + if self._worker_io_times: + local_io = torch.tensor(self._worker_io_times, dtype=torch.float32).cuda() # [num_batches_local] + all_io = torch.cat(distributed.all_gather_tensor(local_io)) # [num_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/total_io_mean"] = all_io.mean().item() + wandb_info[f"{_AUG_PREFIX}/total_io_min"] = all_io.min().item() + wandb_info[f"{_AUG_PREFIX}/total_io_max"] = all_io.max().item() + + if self._worker_aug_times and self._worker_batch_times: + aug_fracs = [ + a / b for a, b in zip(self._worker_aug_times, self._worker_batch_times) if b > 0 + ] # [num_valid_batches_local] + if aug_fracs: + local_fracs = torch.tensor(aug_fracs, dtype=torch.float32).cuda() # [num_valid_batches_local] + all_fracs = torch.cat(distributed.all_gather_tensor(local_fracs)) # [num_valid_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/aug_fraction_mean"] = all_fracs.mean().item() + wandb_info[f"{_AUG_PREFIX}/io_fraction_mean"] = 1.0 - all_fracs.mean().item() + + # Per-augmentor-step breakdown (converted to all_gather_tensor) + if aug_index: + N_aug = len(aug_index) + local_aug_steps = torch.full( + (N_aug, _AGG_COLS), float("nan"), dtype=torch.float64 + ).cuda() # [num_aug_steps, _AGG_COLS] + for step_name, ts in self._worker_aug_step_times.items(): + if step_name in aug_index and ts: + local_aug_steps[aug_index[step_name]] = torch.tensor( + [len(ts), sum(ts), min(ts), max(ts)], dtype=torch.float64 + ) + + all_aug_steps = self._gather_list_stats(local_aug_steps) # [world_size, num_aug_steps, _AGG_COLS] + for step_name, idx in aug_index.items(): + result = self._reduce_agg_column(all_aug_steps[:, idx, :]) + if result: + mean_val, min_val, max_val = result + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_mean"] = mean_val + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_min"] = min_val + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_max"] = max_val + + self._worker_batch_times = [] + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times = defaultdict(list) + self._worker_times_by_ds_wid = defaultdict(list) diff --git a/cosmos3/_src/vfm/callbacks/device_monitor.py b/cosmos3/_src/vfm/callbacks/device_monitor.py new file mode 100644 index 00000000..80365485 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/device_monitor.py @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Any, Dict, List, Tuple + +import pandas as pd +import psutil +import pynvml +import torch +import wandb + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +def log_prof_data( + data_list: List[Dict[str, Any]], + iteration: int, +) -> Tuple[pd.DataFrame]: + # Create a table to log data with rank information + columns = ["iteration", "rank"] + list(data_list[0].keys()) + data = [] + + # Initialize dictionaries to store min and max values for each metric + min_values = {key: float("inf") for key in columns[2:]} + max_values = {key: float("-inf") for key in columns[2:]} + sum_values = {key: 0.0 for key in columns[2:]} + + count = 0 + + for _rank, prof_data in enumerate(data_list): + row = [iteration, _rank] + [prof_data[key] for key in columns[2:]] + data.append(row) + count += 1 + + # Update min, max, and sum values + for key in columns[2:]: + min_values[key] = min(min_values[key], prof_data[key]) + max_values[key] = max(max_values[key], prof_data[key]) + sum_values[key] += prof_data[key] + + # Calculate average values + avg_values = {key: sum_values[key] / count for key in columns[2:]} + + df = pd.DataFrame(data, columns=columns) + summary_df = pd.DataFrame({"Avg": avg_values, "Max": max_values, "Min": min_values}) + + if wandb.run: + # Log the table + table = wandb.Table(dataframe=df) + wandb.log({"DeviceMonitor/prof_data": table}, step=iteration) + + # Log summary statistics + summary = {} + for key in columns[2:]: + summary[f"DeviceMonitor/min_{key}"] = min_values[key] + summary[f"DeviceMonitor/max_{key}"] = max_values[key] + summary[f"DeviceMonitor/avg_{key}"] = avg_values[key] + + wandb.log(summary, step=iteration) + return df, summary_df + + +class DeviceMonitor(EveryN): + """ + A callback to monitor device (CPU/GPU) usage and log it at regular intervals. + + Args: + every_n (int, optional): The frequency at which the callback is invoked. Defaults to 200. + step_size (int, optional): The step size for the callback. Defaults to 1. + save_s3 (bool, optional): Whether to save the monitoring data to S3. Defaults to False. + """ + + def __init__( + self, + every_n: int = 200, + step_size: int = 1, + save_s3: bool = False, + upload_every_n_mul: int = 1, + log_memory_detail: bool = True, + ): + super().__init__(every_n=every_n, step_size=step_size) + self.name = self.__class__.__name__ + self.save_s3 = save_s3 + self.s3_save_fp = f"s3://rundir/{self.name}" + self.upload_every_n = upload_every_n_mul * every_n + + self.log_memory_detail = log_memory_detail + + def on_train_start(self, model, iteration=0): + torch.cuda.reset_peak_memory_stats() + self.world_size = distributed.get_world_size() + self.rank = distributed.get_rank() + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/{self.name}" + if self.rank == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"{self.name} callback: local_dir: {self.local_dir}") + + local_rank = int(os.getenv("LOCAL_RANK", 0)) + self.handle = pynvml.nvmlDeviceGetHandleByIndex(local_rank) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + cur_process = psutil.Process(os.getpid()) + # cur_process.children(recursive=True) can crash if the dataloader is constantly creating and destroying processes (e.g. calling FFmpeg). + try: + cpu_memory_usage = sum(p.memory_info().rss for p in [cur_process] + cur_process.children(recursive=True)) + except Exception as e: # e.g. psutil.NoSuchProcess + log.warning(f"Failed to get CPU memory usage with error {e}") + cpu_memory_usage = 0 + cpu_mem_gb = cpu_memory_usage / (1024**3) + + peak_gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3) + peak_gpu_mem_reserved_gb = torch.cuda.max_memory_reserved() / (1024**3) + temp = torch.cuda.temperature() + try: + power = torch.cuda.power_draw() + except Exception as e: + log.warning(f"Failed to get power draw with error {e}") + power = 0 + util = torch.cuda.utilization() + clock = torch.cuda.clock_rate() + + memory_info = pynvml.nvmlDeviceGetMemoryInfo(self.handle) + nvml_used_gpu_mem_gb = memory_info.used / (1024**3) + nvml_free_gpu_mem_gb = memory_info.free / (1024**3) + + prof_data = { + "cpu_mem_gb": cpu_mem_gb, + "peak_gpu_mem_gb": peak_gpu_mem_gb, + "peak_gpu_mem_reserved_gb": peak_gpu_mem_reserved_gb, + "nvml_used_gpu_mem_gb": nvml_used_gpu_mem_gb, + "nvml_free_gpu_mem_gb": nvml_free_gpu_mem_gb, + "temp": temp, + "power": power, + "util": util, + "clock": clock, + } + + data_list = [prof_data] * self.world_size + # this is blocking by default + if self.world_size > 1: + torch.distributed.all_gather_object(data_list, prof_data) + torch.distributed.barrier() + + df, summary_df = log_prof_data(data_list, iteration) + if self.save_s3 and self.rank == 0: + global_step = iteration // self.step_size + should_run = global_step % self.upload_every_n == 0 + if should_run: + df.to_csv(os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), index=False) + summary_df.to_csv(os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), index=True) + easy_io.copyfile_from_local( + os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), + os.path.join(self.s3_save_fp, f"prof_data_{iteration:09d}.csv"), + ) + easy_io.copyfile_from_local( + os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), + os.path.join(self.s3_save_fp, f"summary_{iteration:09d}.csv"), + ) + if self.rank == 0: + log.info(f"{self.name} Stats:\n{summary_df.to_string()}") + if self.log_memory_detail: + memory_stats = torch.cuda.memory_stats() + if wandb.run: + wandb_memory_info = {f"mem/{key}": memory_stats[key] for key in memory_stats.keys()} + wandb.log(wandb_memory_info, step=iteration) + if self.save_s3: + global_step = iteration // self.step_size + should_run = global_step % self.upload_every_n == 0 + if should_run: + easy_io.dump( + memory_stats, + os.path.join(self.s3_save_fp, f"memory_stats_{iteration:09d}.yaml"), + ) + + torch.cuda.reset_peak_memory_stats() diff --git a/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py b/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py new file mode 100644 index 00000000..77cd4556 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py @@ -0,0 +1,429 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Callback for sampling and visualizing joint audio-video generation. + +Extends the video sampling callback to also handle sound: +- Generates video and audio samples via model.generate_samples_from_batch() +- Logs video frames as image grids to WandB (same as EveryNDrawSample) +- Logs audio as WandB Audio objects +- Optionally creates combined video+audio MP4 files via ffmpeg +""" + +import os +import subprocess +from contextlib import nullcontext +from functools import partial + +import torch +import torch.distributed as dist +import torchvision +import wandb +from einops import rearrange + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log, misc +from cosmos3._src.vfm.callbacks.every_n_draw_sample import pad_images_and_cat, resize_image +from cosmos3._src.vfm.utils.data_utils import slice_data_batch + + +class EveryNDrawAudioVideoSample(EveryN): + """Callback for sampling and visualizing joint audio-video generation. + + Samples from the model and logs both video frames and audio to WandB. + + Args: + every_n: Frequency at which the callback is invoked + step_size: Step size for the callback (default: 1) + n_viz_sample: Number of samples to visualize in WandB (default: 3) + num_sampling_step: Number of ODE integration steps (default: 35) + guidance: List of guidance scales to try (default: [1.0, 3.0, 7.0]) + save_s3: Whether to save to S3 (default: False) + is_ema: Whether to use EMA model (default: False) + video_fps: FPS for video visualization (default: 24) + """ + + def __init__( + self, + every_n: int, + step_size: int = 1, + n_viz_sample: int = 3, + num_sampling_step: int = 35, + guidance: list[float] | None = None, + save_s3: bool = False, + is_ema: bool = False, + video_fps: int = 24, + generation_mode: str = "t2vs", + run_at_start: bool = False, + ): + super().__init__(every_n, step_size, run_at_start=run_at_start) + self.n_viz_sample = n_viz_sample + self.save_s3 = save_s3 + self.name = self.__class__.__name__ + self.is_ema = is_ema + self.guidance = guidance if guidance is not None else [1.0, 3.0, 7.0] + self.num_sampling_step = num_sampling_step + self.rank = distributed.get_rank() + self.video_fps = video_fps + self.generation_mode = generation_mode + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/{self.name}" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"Callback: local_dir: {self.local_dir}") + + self.data_parallel_id = self.rank + + # Check if model has sound tokenizer + self.has_sound = hasattr(model, "tokenizer_sound_gen") and model.tokenizer_sound_gen is not None + if self.has_sound: + self.audio_sample_rate = model.tokenizer_sound_gen.sample_rate + log.info( + f"[{self.name}] Audio-video callback initialized: " + f"audio_sample_rate={self.audio_sample_rate}, is_ema={self.is_ema}" + ) + else: + self.audio_sample_rate = 48000 + log.warning(f"[{self.name}] Model does not have tokenizer_sound_gen, audio sampling disabled.") + + @torch.no_grad() + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + if self.is_ema: + if not model.config.ema.enabled: + return + context = partial(model.ema_scope, "every_n_av_sampling") + else: + context = nullcontext + + tag = "ema" if self.is_ema else "reg" + + with context(): + sample_results = self.sample(trainer, model, data_batch, output_batch, loss, iteration) + dist.barrier() + + if wandb.run and self.rank == 0: + info = {"trainer/global_step": iteration} + + # Log video grid + if sample_results.get("video_grid_path"): + info[f"{self.name}/{tag}_video"] = wandb.Image( + sample_results["video_grid_path"], + caption=f"iter={iteration}, guidance={self.guidance}", + ) + + # Log video+audio MP4s + for i, (mp4_path, caption) in enumerate(sample_results.get("video_audio_samples", [])): + if os.path.exists(mp4_path): + info[f"{self.name}/{tag}_video_audio_{i}"] = wandb.Video( + mp4_path, fps=self.video_fps, caption=caption + ) + + # Log standalone audio + for i, (audio_path, caption) in enumerate(sample_results.get("audio_samples", [])): + if os.path.exists(audio_path): + info[f"{self.name}/{tag}_audio_{i}"] = wandb.Audio( + audio_path, sample_rate=self.audio_sample_rate, caption=caption + ) + + # Log captions + if sample_results.get("captions"): + captions_text = "\n".join([f"{i}: {c}" for i, c in enumerate(sample_results["captions"])]) + info[f"{self.name}/{tag}_captions"] = wandb.Html(f"
{captions_text}
") + + wandb.log(info, step=iteration) + + torch.cuda.empty_cache() + + @misc.timer("EveryNDrawAudioVideoSample: sample") + def sample(self, trainer, model, data_batch, output_batch, loss, iteration): + """Generate audio-video samples and save results. + + Mode-aware behavior: + - t2vs/ti2sv: Decode both video and sound from model output + - tv2s: Use raw conditioning video for visualization, only decode sound + - ts2v: Only decode video, skip sound visualization (conditioned) + """ + data_batch = slice_data_batch(data_batch, start=0, limit=self.n_viz_sample) + + tag = "ema" if self.is_ema else "reg" + mode = self.generation_mode + results = {} + + # Get conditioning data + gen_data_clean = model.get_data_and_condition(data_batch) + raw_data = gen_data_clean.raw_state_vision + x0 = gen_data_clean.x0_tokens_vision + batch_size = len(x0) + + # Get captions for logging + captions = data_batch.get(model.input_caption_key, [""] * batch_size) + if isinstance(captions, torch.Tensor): + captions = ["[tensor]"] * batch_size + results["captions"] = captions[: self.n_viz_sample] + + # Determine what to decode based on mode + # tv2s: Video is conditioning input (use raw_data), only sound is generated + # ts2v: Sound is conditioning input, only video is generated + # t2vs/ti2sv: Both are generated + decode_video = mode not in ("tv2s",) # Skip video decode when video is conditioning + decode_sound = mode not in ("ts2v",) # Skip sound decode when sound is conditioning + + video_samples_all = [] + audio_samples_all = [] + + max_w = max(image.shape[-1] for image in raw_data) + max_h = max(image.shape[-2] for image in raw_data) + t_crop = min(image.shape[-3] for image in raw_data) + + for guidance in self.guidance: + sample_output = model.generate_samples_from_batch( + data_batch, + guidance=guidance, + n_sample=self.n_viz_sample, + num_steps=self.num_sampling_step, + seed=list(range(iteration, iteration + self.n_viz_sample)), + ) + + # Video handling based on mode — decode one at a time and move to CPU immediately + if decode_video: + sample_vision = sample_output.get("vision", []) + if sample_vision and hasattr(model, "decode"): + decoded_cpu = [] + for i in range(len(sample_vision)): + dec = model.decode(sample_vision[i]).float().cpu() + decoded_cpu.append(dec) + video_samples_all.append(pad_images_and_cat(decoded_cpu, max_w, max_h, t_crop)) + del decoded_cpu + + # Sound handling based on mode — decode one at a time and move to CPU immediately + if decode_sound: + sound_latents = sample_output.get("sound", []) + if sound_latents and self.has_sound: + audio_cpu = [] + for s in sound_latents: + dec = model.decode_sound(s).float().cpu() + audio_cpu.append(dec) + audio_samples_all.append(audio_cpu) + del audio_cpu + + # Free all GPU memory from this guidance iteration before next one + del sample_output + torch.cuda.empty_cache() + + # For tv2s: Use raw conditioning video instead of decoded (avoids wasteful VAE round-trip) + if mode == "tv2s": + conditioning_video = ( + pad_images_and_cat(raw_data[: min(self.n_viz_sample, batch_size)], max_w, max_h, t_crop).float().cpu() + ) + # Use conditioning video for all guidance scales (same video, different audio) + video_for_mp4 = conditioning_video + else: + video_for_mp4 = None # Will use per-guidance decoded video below + + # Add ground truth video for comparison (skip for tv2s where video isn't generated) + if decode_video: + video_samples_all.append( + pad_images_and_cat(raw_data[: min(self.n_viz_sample, batch_size)], max_w, max_h, t_crop).float().cpu() + ) + + # Save video grid (skip for tv2s — video evaluation should be done separately) + if video_samples_all and decode_video: + video_grid_path = self._save_video_grid(video_samples_all, batch_size, tag, iteration) + if video_grid_path: + results["video_grid_path"] = video_grid_path + + # Save audio samples and video+audio MP4s + if audio_samples_all and self.rank == 0: + audio_paths = [] + video_audio_paths = [] + + # Get conditioning FPS for video playback + conditioning_fps = data_batch.get("conditioning_fps", None) + if conditioning_fps is not None and isinstance(conditioning_fps, (torch.Tensor, list)): + video_write_fps = float( + conditioning_fps[0].item() if isinstance(conditioning_fps, torch.Tensor) else conditioning_fps[0] + ) + else: + video_write_fps = self.video_fps + + for g_idx, audio_batch in enumerate(audio_samples_all): + # Determine which video to pair with audio for MP4 + if video_for_mp4 is not None: + # tv2s: Use conditioning video for all guidance scales + video_batch = video_for_mp4 + elif g_idx < len(video_samples_all) - 1: + # t2vs/ti2sv: Use decoded video from this guidance scale (exclude GT at end) + video_batch = video_samples_all[g_idx] + else: + video_batch = None + + for sample_idx in range(min(self.n_viz_sample, len(audio_batch))): + audio_waveform = audio_batch[sample_idx] # [C,N_samples] + + # Save standalone audio + audio_path = self._save_audio(audio_waveform, tag, iteration, g_idx, sample_idx) + if audio_path: + caption = f"mode={mode}, guidance={self.guidance[g_idx]}, sample={sample_idx}" + if sample_idx < len(captions): + caption += f", caption: {captions[sample_idx][:100]}" + audio_paths.append((audio_path, caption)) + + # Create video+audio MP4 + if video_batch is not None and sample_idx < video_batch.shape[0]: + video_tensor = video_batch[sample_idx] # [C,T,H,W] + mp4_path = self._save_video_with_audio( + video_tensor, + audio_waveform, + tag, + iteration, + g_idx, + sample_idx, + fps=video_write_fps, + ) + if mp4_path: + video_audio_paths.append((mp4_path, caption)) + + results["audio_samples"] = audio_paths + results["video_audio_samples"] = video_audio_paths + + return results + + def _save_video_grid( + self, video_samples: list[torch.Tensor], batch_size: int, tag: str, iteration: int + ) -> str | None: + """Save video samples as image grid for WandB.""" + if self.rank != 0 or not wandb.run: + return None + + to_show = (1.0 + torch.stack(video_samples, dim=0).clamp(-1, 1)) / 2.0 # [N_rows,B,C,T,H,W] range [0,1] + n_viz_sample = min(self.n_viz_sample, batch_size) + is_single_frame = to_show.shape[3] == 1 + + file_base_fp = f"{tag}_AV_Video_Iter{iteration:09d}.jpg" + local_path = f"{self.local_dir}/{file_base_fp}" + + if is_single_frame: + to_show = rearrange( + to_show[:, :n_viz_sample], "n b c t h w -> t c (n h) (b w)" + ) # [1,C,N_rows*H,B*W] (t=1) + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + torchvision.utils.save_image(resize_image(image_grid, 1024), local_path) + else: + to_show = to_show[:, :n_viz_sample] # [N_rows,B,C,T,H,W] + _T = to_show.shape[3] + three_frames_list = [0, _T // 2, _T - 1] + to_show = to_show[:, :, :, three_frames_list] # [N_rows,B,C,3,H,W] + to_show = rearrange(to_show, "n b c t h w -> 1 c (n h) (b t w)") # [1,C,N_rows*H,B*3*W] (t=3) + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + torchvision.utils.save_image(resize_image(image_grid, 1024), local_path) + + return local_path + + def _save_audio( + self, audio_waveform: torch.Tensor, tag: str, iteration: int, guidance_idx: int, sample_idx: int + ) -> str | None: + """Save audio waveform as WAV file.""" + if self.rank != 0: + return None + try: + import soundfile as sf + + file_name = f"{tag}_Audio_Iter{iteration:09d}_g{guidance_idx}_s{sample_idx}.wav" + local_path = f"{self.local_dir}/{file_name}" + + audio_np = audio_waveform.clamp(-1, 1).numpy() + if audio_np.ndim == 2: + audio_np = audio_np.T # [C, N] → [N, C] for soundfile + + sf.write(local_path, audio_np, self.audio_sample_rate) + return local_path + except Exception as e: + log.warning(f"Failed to save audio: {e}", rank0_only=False) + return None + + def _save_video_with_audio( + self, + video_tensor: torch.Tensor, + audio_tensor: torch.Tensor, + tag: str, + iteration: int, + guidance_idx: int, + sample_idx: int, + fps: float | None = None, + ) -> str | None: + """Create MP4 video with audio using ffmpeg.""" + video_fps = fps if fps is not None else self.video_fps + if self.rank != 0: + return None + try: + import soundfile as sf + + file_base = f"{tag}_VideoAudio_Iter{iteration:09d}_g{guidance_idx}_s{sample_idx}" + mp4_path = f"{self.local_dir}/{file_base}.mp4" + temp_video_path = f"{self.local_dir}/{file_base}_temp.mp4" + temp_audio_path = f"{self.local_dir}/{file_base}_temp.wav" + + # Save video frames as temp MP4 + video_frames = video_tensor.permute(1, 0, 2, 3) # [T,C,H,W] + video_frames = (video_frames.clamp(-1, 1) + 1) / 2 # [T,C,H,W] range [0,1] + video_frames = (video_frames * 255).to(torch.uint8) # [T,C,H,W] + + torchvision.io.write_video( + temp_video_path, + video_frames.permute(0, 2, 3, 1).cpu(), # [T,H,W,C] + fps=video_fps, + video_codec="libx264", + ) + + # Save audio as temp WAV + audio_np = audio_tensor.clamp(-1, 1).numpy() + if audio_np.ndim == 2: + audio_np = audio_np.T + sf.write(temp_audio_path, audio_np, self.audio_sample_rate) + + # Combine with ffmpeg + cmd = [ + "ffmpeg", + "-y", + "-i", + temp_video_path, + "-i", + temp_audio_path, + "-c:v", + "libx264", + "-c:a", + "aac", + "-shortest", + "-loglevel", + "error", + mp4_path, + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + log.warning(f"ffmpeg failed: {result.stderr}") + return None + + # Cleanup + for f in [temp_video_path, temp_audio_path]: + if os.path.exists(f): + os.remove(f) + + return mp4_path + except Exception as e: + log.warning(f"Failed to create video with audio: {e}", rank0_only=False) + return None diff --git a/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py b/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py new file mode 100644 index 00000000..1863a915 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py @@ -0,0 +1,438 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import os +from contextlib import nullcontext +from functools import partial +from typing import List, Optional + +import numpy as np +import torch +import torch.distributed as dist +import torch.nn.functional as F +import torchvision +import torchvision.transforms.functional as torchvision_F +import wandb +from einops import rearrange + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log, misc +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.imaginaire.visualize.video import save_img_or_video +from cosmos3._src.vfm.utils.data_utils import slice_data_batch + + +def resize_image(image: torch.Tensor, size: int = 1024) -> torch.Tensor: + """ + Resize the image to the given size. This is done so that wandb can display the image correctly. + """ + _, h, w = image.shape + ratio = size / max(h, w) + new_h, new_w = int(ratio * h), int(ratio * w) + return torchvision_F.resize(image, (new_h, new_w)) + + +def is_primitive(value): + return isinstance(value, (int, float, str, bool, type(None))) + + +def convert_to_primitive(value): + if isinstance(value, (list, tuple)): + return [convert_to_primitive(v) for v in value if is_primitive(v) or isinstance(v, (list, dict))] + elif isinstance(value, dict): + return {k: convert_to_primitive(v) for k, v in value.items() if is_primitive(v) or isinstance(v, (list, dict))} + elif is_primitive(value): + return value + else: + return "non-primitive" # Skip non-primitive types + + +def pad_images_and_cat(images: List[torch.Tensor], max_w: int, max_h: int, t_crop: int = 1) -> torch.Tensor: + """ + Pad images to a common size and concatenate them along the batch dimension. + + This function is needed because different samples in a batch can have different resolutions. + To create a unified visualization grid, all images must be padded to the same dimensions. + Images are center-padded to preserve their visual content in the middle. + + Args: + images: List of image/video tensors with shape [B, C, T, H, W]. + max_w: Target width to pad all images to. + max_h: Target height to pad all images to. + t_crop: Number of temporal frames to keep for videos. If > 1 and the image + has more than 1 frame, only the first t_crop frames are retained. + + Returns: + Concatenated tensor of padded images with shape [total_B, C, T, max_h, max_w]. + """ + padded_images = [] + for image in images: + # Pad the image to the center + padding_h = (max_h - image.shape[-2]) // 2 + padding_w = (max_w - image.shape[-1]) // 2 + padded_image = torch.nn.functional.pad( + image, (padding_w, max_w - image.shape[-1] - padding_w, padding_h, max_h - image.shape[-2] - padding_h) + ) # [B,C,T,max_h,max_w] + # Handle video case + if image.shape[2] > 1 and t_crop > 1: + padded_image = padded_image[:, :, 0:t_crop, :, :] + + padded_images.append(padded_image) + return torch.cat(padded_images, dim=0) # [total_B,C,T,max_h,max_w] (total_B = sum of batch dims) + + +class EveryNDrawSample(EveryN): + """ + This callback sample condition inputs from training data, run inference and save the results to wandb and s3. + + Args: + every_n (int): The frequency at which the callback is invoked. + step_size (int, optional): The step size for the callback. Defaults to 1. + n_viz_sample (int, optional): for each batch, min(n_viz_sample, batch_size) samples will be saved to wandb. Defaults to 3. + n_sample_to_save (int, optional): number of samples to save. The actual number of samples to save is min(n_sample_to_save, data parallel instances). Defaults to 128. + num_sampling_step (int, optional): number of sampling steps. Defaults to 35. + guidance (List[float], optional): guidance scale. Defaults to [0.0, 3.0, 7.0]. + do_x0_prediction (bool, optional): whether to do x0 prediction. Defaults to True. + n_sigmas_for_x0_prediction (int, optional): number of sigmas to use for x0 prediction. Defaults to 4. + save_s3 (bool, optional): whether to save to s3. Defaults to False. + is_ema (bool, optional): whether the callback is run for ema model. Defaults to False. + use_negative_prompt (bool, optional): whether to use negative prompt. Defaults to False. + fps (int, optional): frames per second when saving the video. Defaults to 16. + """ + + def __init__( + self, + every_n: int, + step_size: int = 1, + n_viz_sample: int = 2, + n_sample_to_save: int = 128, + num_sampling_step: int = 35, + guidance: List[float] = [0.0, 3.0, 7.0], + do_x0_prediction: bool = True, + n_sigmas_for_x0_prediction: int = 4, + save_s3: bool = False, + save_local: bool = False, + is_ema: bool = False, + use_negative_prompt: bool = False, + prompt_type: str = "t5_xxl", + fps: int = 16, + run_at_start: bool = False, + ): + # s3: # files: min(n_sample_to_save, data instance) # per file: min(batch_size, n_viz_sample) + # wandb: 1 file, # per file: min(batch_size, n_viz_sample) + super().__init__(every_n, step_size, run_at_start=run_at_start) + + self.n_viz_sample = n_viz_sample + self.n_sample_to_save = n_sample_to_save + self.save_s3 = save_s3 + self.save_local = save_local + self.do_x0_prediction = do_x0_prediction + self.n_sigmas_for_x0_prediction = n_sigmas_for_x0_prediction + self.name = self.__class__.__name__ + self.is_ema = is_ema + self.use_negative_prompt = use_negative_prompt + self.prompt_type = prompt_type + self.guidance = guidance + self.num_sampling_step = num_sampling_step + self.rank = distributed.get_rank() + self.fps = fps + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/{self.name}" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"Callback: local_dir: {self.local_dir}") + + self.data_parallel_id = self.rank + + @misc.timer("EveryNDrawSample: x0") + @torch.no_grad() + def x0_pred(self, trainer, model, data_batch, output_batch, loss, iteration): + tag = "ema" if self.is_ema else "reg" + + log.debug("starting data and condition model", rank0_only=False) + + + data_clean = model.get_data_and_condition(data_batch) + raw_data = data_clean.raw_state_vision + x0 = data_clean.x0_tokens_vision + + # Handle model parallelism if available (legacy models) + if hasattr(model, "broadcast_split_for_model_parallelsim"): + _, condition, x0, _ = model.broadcast_split_for_model_parallelsim(None, None, x0, None) + + log.debug("done data and condition model", rank0_only=False) + batch_size = len(x0) + sigmas = np.exp( + np.linspace( + math.log(model.sde.sigma_min), math.log(model.sde.sigma_max), self.n_sigmas_for_x0_prediction + 1 + )[1:] + ) + + to_show = [] + generator = torch.Generator(device="cuda") + generator.manual_seed(0) + random_noise = torch.randn(*x0.shape, generator=generator, **model.tensor_kwargs) # same shape as x0 + _ones = torch.ones(batch_size, **model.tensor_kwargs) # [B] + mse_loss_list = [] + for _, sigma in enumerate(sigmas): + x_sigma = sigma * random_noise + x0 + log.debug(f"starting denoising {sigma}", rank0_only=False) + sample = model.denoise(x_sigma, None).x0 + log.debug(f"done denoising {sigma}", rank0_only=False) + mse_loss = distributed.dist_reduce_tensor(F.mse_loss(sample, x0)) + mse_loss_list.append(mse_loss) + + if hasattr(model, "decode"): + sample = model.decode(sample) + to_show.append(sample.float().cpu()) + to_show.append( + raw_data.float().cpu(), + ) + + base_fp_wo_ext = f"{tag}_ReplicateID{self.data_parallel_id:04d}_x0_Iter{iteration:09d}" + + local_path = self.run_save(to_show, batch_size, base_fp_wo_ext) + return local_path, torch.tensor(mse_loss_list).cuda(), sigmas # [N_sigmas] + + @torch.no_grad() + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + if self.is_ema: + if not model.config.ema.enabled: + return + context = partial(model.ema_scope, "every_n_sampling") + else: + context = nullcontext + + tag = "ema" if self.is_ema else "reg" + sample_counter = getattr(trainer, "sample_counter", iteration) + batch_info = { + "data": { + k: convert_to_primitive(v) + for k, v in data_batch.items() + if is_primitive(v) or isinstance(v, (list, dict)) + }, + "sample_counter": sample_counter, + "iteration": iteration, + } + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + easy_io.dump( + batch_info, + f"s3://rundir/{self.name}/Iter{iteration:09d}/BatchInfo_ReplicateID{self.data_parallel_id:04d}_Iter{iteration:09d}.json", + ) + + log.debug("entering, every_n_impl", rank0_only=False) + with context(): + if self.do_x0_prediction: + log.debug("entering, x0_pred", rank0_only=False) + x0_img_fp, mse_loss, sigmas = self.x0_pred( + trainer, + model, + data_batch, + output_batch, + loss, + iteration, + ) + log.debug("done, x0_pred", rank0_only=False) + if self.save_s3 and self.rank == 0: + easy_io.dump( + { + "mse_loss": mse_loss.tolist(), + "sigmas": sigmas.tolist(), + "iteration": iteration, + }, + f"s3://rundir/{self.name}/{tag}_MSE_Iter{iteration:09d}.json", + ) + + log.debug("entering, sample", rank0_only=False) + sample_img_fp = self.sample( + trainer, + model, + data_batch, + output_batch, + loss, + iteration, + ) + log.debug("done, sample", rank0_only=False) + + log.debug("waiting for all ranks to finish", rank0_only=False) + dist.barrier() + if wandb.run: + sample_counter = getattr(trainer, "sample_counter", iteration) + data_type = "image" if model.is_image_batch(data_batch) else "video" + tag += f"_{data_type}" + info = { + "trainer/global_step": iteration, + "sample_counter": sample_counter, + } + if self.do_x0_prediction: + info[f"{self.name}/{tag}_x0"] = wandb.Image(x0_img_fp, caption=f"{sample_counter}") + # convert mse_loss to a dict + mse_loss = mse_loss.tolist() + info.update({f"x0_pred_mse_{tag}/Sigma{sigmas[i]:0.5f}": mse_loss[i] for i in range(len(mse_loss))}) + + info[f"{self.name}/{tag}_sample"] = wandb.Image(sample_img_fp, caption=f"{sample_counter}") + wandb.log( + info, + step=iteration, + ) + torch.cuda.empty_cache() + + @misc.timer("EveryNDrawSample: sample") + def sample(self, trainer, model, data_batch, output_batch, loss, iteration): + data_batch = slice_data_batch(data_batch, start=0, limit=self.n_viz_sample) + + tag = "ema" if self.is_ema else "reg" + + # Obtain text embeddings online + text_encoder_config = getattr(model.config, "text_encoder_config", None) + if text_encoder_config is not None and text_encoder_config.compute_online: + text_embeddings = model.text_encoder.compute_text_embeddings_online(data_batch, model.input_caption_key) + data_batch["t5_text_embeddings"] = text_embeddings + data_batch["t5_text_mask"] = torch.ones( + text_embeddings.shape[0], text_embeddings.shape[1], device="cuda" + ) # [B,N_tokens] (all tokens valid) + + data_clean = model.get_data_and_condition(data_batch) + raw_data = data_clean.raw_state_vision + x0 = data_clean.x0_tokens_vision + + # determine the number of visualized samples + n_viz_sample = min(self.n_viz_sample, data_clean.batch_size) + + # Check if this is a multi-item vision batch (image editing) + num_items = data_clean.num_vision_items_per_sample + is_multi_item = num_items is not None + + if is_multi_item: + # Image editing: raw_data is flat [src1, tgt1, src2, tgt2, ...]. + # Split into per-sample condition (source) and GT target images. + condition_images: list[torch.Tensor] = [] + gt_target_images: list[torch.Tensor] = [] + vis_offset = 0 + for sample_idx in range(data_clean.batch_size): + n_vis = num_items[sample_idx] + # First item(s) are condition, last item is generation target + + # but we need to support multiple conditions per sample in the future. Current code + # can handle this without throwing an error. + condition_images.append(raw_data[vis_offset]) # source image (1, C, 1, H, W) + gt_target_images.append(raw_data[vis_offset + n_vis - 1]) # target image (1, C, 1, H, W) + vis_offset += n_vis + + # Use target images for max_w/max_h/t_crop (generated samples match target size) + max_w = max(img.shape[-1] for img in gt_target_images) + max_h = max(img.shape[-2] for img in gt_target_images) + t_crop = min(img.shape[-3] for img in gt_target_images) + else: + max_w = max(image.shape[-1] for image in raw_data) + max_h = max(image.shape[-2] for image in raw_data) + t_crop = min(image.shape[-3] for image in raw_data) + + to_show = [] + + # Row 0 (image editing only): condition (source) images + if is_multi_item: + to_show.append(pad_images_and_cat(condition_images[:n_viz_sample], max_w, max_h, t_crop).float().cpu()) + + for guidance in self.guidance: + sample = model.generate_samples_from_batch( + data_batch, + guidance=guidance, + n_sample=n_viz_sample, + num_steps=self.num_sampling_step, + has_negative_prompt=True if self.use_negative_prompt else False, + seed=list(range(iteration, iteration + n_viz_sample)), + ) + sample_vision = sample["vision"] + assert hasattr(model, "decode") + sample_vision_decoded = [model.decode(sample_vision_i) for sample_vision_i in sample_vision] + assert len(sample_vision_decoded) == n_viz_sample + to_show.append(pad_images_and_cat(sample_vision_decoded, max_w, max_h, t_crop).float().cpu()) + + # Last row: ground truth + if is_multi_item: + # Image editing: show GT target images (not the flat raw_data which mixes src + tgt) + assert len(gt_target_images) == n_viz_sample + to_show.append(pad_images_and_cat(gt_target_images, max_w, max_h, t_crop).float().cpu()) + else: + assert len(raw_data) == n_viz_sample + to_show.append(pad_images_and_cat(raw_data, max_w, max_h, t_crop).float().cpu()) + + base_fp_wo_ext = f"{tag}_ReplicateID{self.data_parallel_id:04d}_Sample_Iter{iteration:09d}" + base_fp_wo_ext = f"Iter{iteration:09d}/{base_fp_wo_ext}" + + batch_size = data_clean.batch_size + local_path = self.run_save(to_show, batch_size, base_fp_wo_ext) + return local_path + + def run_save(self, to_show, batch_size, base_fp_wo_ext) -> Optional[str]: + to_show = (1.0 + torch.stack(to_show, dim=0).clamp(-1, 1)) / 2.0 # [N_rows,B,C,T,H,W] range [0,1] + is_single_frame = to_show.shape[3] == 1 + n_viz_sample = min(self.n_viz_sample, batch_size) + to_show = to_show[:, :n_viz_sample] + + # ! we only save first n_sample_to_save video! + video_grid = rearrange(to_show, "n b c t h w -> c t (n h) (b w)") # [C,T,N_rows*H,B*W] + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + save_img_or_video( + video_grid, + f"s3://rundir/{self.name}/{base_fp_wo_ext}", + fps=self.fps, + ) + if self.save_local and self.data_parallel_id < self.n_sample_to_save: + local_video_path = f"{self.local_dir}/{base_fp_wo_ext}" + os.makedirs(os.path.dirname(local_video_path), exist_ok=True) + save_img_or_video(video_grid, local_video_path, fps=self.fps) + + file_base_fp = f"{base_fp_wo_ext}_resize.jpg" + local_path = f"{self.local_dir}/{file_base_fp}" + + if self.rank == 0 and wandb.run: + if is_single_frame: # image case + to_show = rearrange( + to_show[:, :n_viz_sample], + "n b c t h w -> t c (n h) (b w)", + ) # [1,C,N_rows*H,B*W] (t=1 for images) + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + # resize so that wandb can handle it + os.makedirs(os.path.dirname(local_path), exist_ok=True) + torchvision.utils.save_image(resize_image(image_grid, 1024), local_path, nrow=1, scale_each=True) + else: + to_show = to_show[:, :n_viz_sample] # [N_rows,B,C,T,H,W] + + # resize 3 frames frames so that we can display them on wandb + _T = to_show.shape[3] + three_frames_list = [0, _T // 2, _T - 1] + to_show = to_show[:, :, :, three_frames_list] # [N_rows,B,C,3,H,W] (3 sampled frames) + log_image_size = 1024 + to_show = rearrange( + to_show, + "n b c t h w -> 1 c (n h) (b t w)", + ) # [1,C,N_rows*H,B*3*W] (t=3 sampled frames) + + os.makedirs(os.path.dirname(local_path), exist_ok=True) + # resize so that wandb can handle it + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + os.makedirs(os.path.dirname(local_path), exist_ok=True) + torchvision.utils.save_image( + resize_image(image_grid, log_image_size), local_path, nrow=1, scale_each=True + ) + + return local_path + return None diff --git a/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py b/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py new file mode 100644 index 00000000..24d1a3b2 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py @@ -0,0 +1,333 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from contextlib import nullcontext +from functools import partial + +import torch +import torch.distributed as dist +import torchvision +import wandb +from einops import rearrange, repeat + +from cosmos3._src.imaginaire.utils import log, misc +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.imaginaire.visualize.video import save_img_or_video +from cosmos3._src.vfm.callbacks.every_n_draw_sample import ( + EveryNDrawSample, + convert_to_primitive, + is_primitive, + pad_images_and_cat, + resize_image, +) +from cosmos3._src.vfm.utils.data_utils import slice_data_batch + + +class EveryNDurationFPSDrawSample(EveryNDrawSample): + """ + Callback to visualize samples with specific Duration/FPS metadata control. + It performs two types of generation: + 1. Standard generation (using the batch as-is). + 2. "Consistent" generation: Rewrites the duration/FPS metadata in the caption + to match the actual video FPS and generated frame count, then generates again. + The "Consistent" results are logged as individual windows per sample to show + the exact caption used. + """ + + @misc.timer("EveryNDurationFPSDrawSample: sample") + def sample(self, trainer, model, data_batch, output_batch, loss, iteration): + data_batch = slice_data_batch(data_batch, start=0, limit=self.n_viz_sample) + + tag = "ema" if self.is_ema else "reg" + results = {} + + # Obtain text embeddings online + text_encoder_config = getattr(model.config, "text_encoder_config", None) + if text_encoder_config is not None and text_encoder_config.compute_online: + text_embeddings = model.text_encoder.compute_text_embeddings_online(data_batch, model.input_caption_key) + data_batch["t5_text_embeddings"] = text_embeddings + data_batch["t5_text_mask"] = torch.ones( + text_embeddings.shape[0], text_embeddings.shape[1], device="cuda" + ) # [B,N_tokens] (all tokens valid) + + data_clean = model.get_data_and_condition(data_batch) + raw_data = data_clean.raw_state_vision + x0 = data_clean.x0_tokens_vision + + # Setup negative prompts if needed + if self.use_negative_prompt: + batch_size = len(x0) + if self.negative_prompt_data["t5_text_embeddings"].shape != data_batch["t5_text_embeddings"].shape: + data_batch["neg_t5_text_embeddings"] = misc.to( + repeat( + self.negative_prompt_data["t5_text_embeddings"], + "... -> b ...", + b=batch_size, + ), # [B,N_tokens,D] + **model.tensor_kwargs, + ) + else: + data_batch["neg_t5_text_embeddings"] = misc.to( + self.negative_prompt_data["t5_text_embeddings"], + **model.tensor_kwargs, + ) + data_batch["neg_t5_text_mask"] = data_batch["t5_text_mask"] + + # Compute max dimensions for padding (supports variable shapes) + max_w = max(image.shape[-1] for image in raw_data) + max_h = max(image.shape[-2] for image in raw_data) + t_crop = min(image.shape[-3] for image in raw_data) + + # Helper to run generation for a specific data batch configuration + def _generate_and_save(batch_to_use, suffix="", split_batch=False, save_fps=None): + to_show = [] + for guidance in self.guidance: + sample = model.generate_samples_from_batch( + batch_to_use, + guidance=guidance, + n_sample=self.n_viz_sample, + num_steps=self.num_sampling_step, + has_negative_prompt=True if self.use_negative_prompt else False, + seed=list(range(iteration, iteration + self.n_viz_sample)), + ) + sample_vision = sample["vision"] + if hasattr(model, "decode"): + sample_vision_decoded = [model.decode(sample_vision[i]) for i in range(len(sample_vision))] + else: + sample_vision_decoded = sample_vision + to_show.append(pad_images_and_cat(sample_vision_decoded, max_w, max_h, t_crop).float().cpu()) + + to_show.append( + pad_images_and_cat(raw_data[: len(sample_vision_decoded)], max_w, max_h, t_crop).float().cpu() + ) + + base_fp_wo_ext = f"{tag}_ReplicateID{self.data_parallel_id:04d}_Sample_Iter{iteration:09d}{suffix}" + batch_size = len(x0) + + if split_batch: + # When splitting, run_save_split returns keys like "_0", "_1" + # We need to prepend the suffix (e.g. "_consistent") to these keys + # so they become "_consistent_0", "_consistent_1" + split_results = self.run_save_split(to_show, batch_size, base_fp_wo_ext, save_fps=save_fps) + return {f"{suffix}{k}": v for k, v in split_results.items()} + else: + return self.run_save(to_show, batch_size, base_fp_wo_ext) + + # 1. Standard generation + results[""] = _generate_and_save(data_batch) + + # 2. "Consistent Duration/FPS" Variation + is_video = not model.is_image_batch(data_batch) + input_caption_key = getattr(model, "input_caption_key", "ai_caption") + + if is_video and input_caption_key in data_batch and "conditioning_fps" in data_batch: + original_captions = data_batch[input_caption_key] + + batch_copy = data_batch.copy() + fps_values = data_batch["conditioning_fps"] + + new_captions = [] + for i, cap in enumerate(original_captions): + new_captions.append(cap) + + batch_copy[input_caption_key] = new_captions + + # Re-compute embeddings + if text_encoder_config is not None and text_encoder_config.compute_online: + text_embeddings = model.text_encoder.compute_text_embeddings_online(batch_copy, input_caption_key) + batch_copy["t5_text_embeddings"] = text_embeddings + batch_copy["t5_text_mask"] = torch.ones( + text_embeddings.shape[0], text_embeddings.shape[1], device="cuda" + ) # [B,N_tokens] (all tokens valid) + if "neg_t5_text_embeddings" in batch_copy: + pass + + # Pass captions back so we can log them + batch_result = _generate_and_save(batch_copy, suffix="_consistent", split_batch=True, save_fps=fps_values) + # Attach captions to the result dictionary for logging + batch_result["__captions__"] = new_captions + results.update(batch_result) + + return results + + def run_save_split(self, to_show, batch_size, base_fp_wo_ext, save_fps=None) -> dict: + """ + Similar to run_save but splits the batch into individual images. + """ + to_show = (1.0 + torch.stack(to_show, dim=0).clamp(-1, 1)) / 2.0 # [N_rows,B,C,T,H,W] range [0,1] + n_viz_sample = min(self.n_viz_sample, batch_size) + + # We assume video here since we checked is_video + to_show_full = to_show[:, :n_viz_sample] # [N_rows,B,C,T,H,W] - Keep full video for S3 saving + _T = to_show_full.shape[3] + + # Save individual FULL videos to S3 if enabled (before 3-frame reduction) + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + for i in range(n_viz_sample): + # Extract individual FULL video from batch + individual_video = to_show_full[:, i : i + 1] # [n, 1, c, T, h, w] - FULL video + + # Get FPS for this specific batch item + item_fps = self.fps # fallback + if save_fps is not None: + if isinstance(save_fps, torch.Tensor): + item_fps = save_fps[i].item() if save_fps.ndim > 0 else save_fps.item() + elif isinstance(save_fps, (list, tuple)): + item_fps = save_fps[i] + else: + item_fps = float(save_fps) + + # Save individual FULL video to S3 + save_img_or_video( + rearrange(individual_video, "n b c t h w -> c t (n h) (b w)"), # [C,T,N_rows*H,W] + f"s3://rundir/{self.name}/{base_fp_wo_ext}_{i}", + fps=item_fps, + ) + + # NOW reduce to 3 frames for WandB visualization only + three_frames_list = [0, _T // 2, _T - 1] + to_show_3frames = to_show_full[:, :, :, three_frames_list] # [N_rows,B,C,3,H,W] + log_image_size = 1024 + + # Save individual FULL videos to S3 if enabled (before 3-frame reduction) + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + for i in range(n_viz_sample): + # Extract individual FULL video from batch + individual_video = to_show_full[:, i : i + 1] # [N_rows,1,C,T,H,W] + + # Get FPS for this specific batch item + item_fps = self.fps # fallback + if save_fps is not None: + if isinstance(save_fps, torch.Tensor): + item_fps = save_fps[i].item() if save_fps.ndim > 0 else save_fps.item() + elif isinstance(save_fps, (list, tuple)): + item_fps = save_fps[i] + else: + item_fps = float(save_fps) + + # Save individual FULL video to S3 + save_img_or_video( + rearrange(individual_video, "n b c t h w -> c t (n h) (b w)"), # [C,T,N_rows*H,W] + f"s3://rundir/{self.name}/{base_fp_wo_ext}_{i}", + fps=item_fps, + ) + + # NOW reduce to 3 frames for WandB visualization only + three_frames_list = [0, _T // 2, _T - 1] + to_show_3frames = to_show_full[:, :, :, three_frames_list] # [N_rows,B,C,3,H,W] + log_image_size = 1024 + + paths = {} + for i in range(n_viz_sample): + sample_data = to_show_3frames[:, i : i + 1] # [N_rows,1,C,3,H,W] + sample_grid_data = rearrange(sample_data, "n b c t h w -> 1 c (n h) (b t w)") # [1,C,N_rows*H,3*W] (t=3) + + sample_path = f"{self.local_dir}/{base_fp_wo_ext}_{i}_resize.jpg" + if self.rank == 0: + image_grid = torchvision.utils.make_grid(sample_grid_data, nrow=1, padding=0, normalize=False) + torchvision.utils.save_image( + resize_image(image_grid, log_image_size), sample_path, nrow=1, scale_each=True + ) + paths[f"_{i}"] = sample_path + return paths + + @torch.no_grad() + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + if self.is_ema: + if not model.config.ema.enabled: + return + context = partial(model.ema_scope, "every_n_sampling") + else: + context = nullcontext + + tag = "ema" if self.is_ema else "reg" + sample_counter = getattr(trainer, "sample_counter", iteration) + # Log batch info logic from base class... + batch_info = { + "data": { + k: convert_to_primitive(v) + for k, v in data_batch.items() + if is_primitive(v) or isinstance(v, (list, dict)) + }, + "sample_counter": sample_counter, + "iteration": iteration, + } + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + easy_io.dump( + batch_info, + f"s3://rundir/{self.name}/BatchInfo_ReplicateID{self.data_parallel_id:04d}_Iter{iteration:09d}.json", + ) + + log.debug("entering, every_n_impl", rank0_only=False) + with context(): + # Skipping x0_pred for brevity in this specialized callback + log.debug("entering, sample", rank0_only=False) + sample_img_paths = self.sample( + trainer, + model, + data_batch, + output_batch, + loss, + iteration, + ) + log.debug("done, sample", rank0_only=False) + dist.barrier() + + if wandb.run: + data_type = "image" if model.is_image_batch(data_batch) else "video" + tag += f"_{data_type}" + info = { + "trainer/global_step": iteration, + "sample_counter": sample_counter, + } + # Handle dictionary of paths + if isinstance(sample_img_paths, dict): + # Retrieve captions if available + consistent_captions = sample_img_paths.get("__captions__", []) + + # Log standard (key "") + if "" in sample_img_paths: + info[f"{self.name}/{tag}_sample"] = wandb.Image(sample_img_paths[""], caption=f"{sample_counter}") + + # Log consistent variations (keys "_consistent_0", etc) + for suffix, path in sample_img_paths.items(): + if suffix == "" or suffix == "__captions__": + continue + + caption_text = f"{sample_counter}{suffix}" + + if "_consistent_" in suffix: + try: + idx = int(suffix.split("_")[-1]) + if idx < len(consistent_captions): + full_caption = consistent_captions[idx] + # Extract duration and FPS values and prepend for WandB display + duration_match = re.search(r"(\d+\.?\d*)\s+seconds?", full_caption) + fps_match = re.search(r"(\d+\.?\d*)\s+FPS", full_caption, re.IGNORECASE) + + if duration_match and fps_match: + duration = duration_match.group(1) + fps = fps_match.group(1) + caption_text = f"(Dur: {duration}s, FPS: {fps}fps) {full_caption}" + else: + caption_text = full_caption # No metadata found, use as-is + except Exception as e: + log.warning(f"Failed to parse suffix '{suffix}' for caption lookup: {e}") + + info[f"{self.name}/{tag}_sample{suffix}"] = wandb.Image(path, caption=caption_text) + + wandb.log(info, step=iteration) + torch.cuda.empty_cache() diff --git a/cosmos3/_src/vfm/callbacks/expert_heatmap.py b/cosmos3/_src/vfm/callbacks/expert_heatmap.py new file mode 100644 index 00000000..e30d022d --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/expert_heatmap.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import matplotlib.pyplot as plt +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def compute_expert_heatmap(vfm: torch.nn.Module) -> dict[str, torch.Tensor]: + """ + Compute the heatmap for the MoE blocks in the language model. + + The heatmap is a dictionary with keys set to ["und", "gen"] and values set to + a tensor of shape (num_layers, num_experts). + + Each element of the tensor is the average number of tokens routed to each expert for a + given layer. The sum of the elements in each row should be equal to the average number + of experts per token for the MoE model (config.num_experts_per_tok). + + For dense models, the heatmap is an empty dictionary. + """ + with torch.no_grad(): + num_layers = len(vfm.language_model.model.layers) + + example_dtensor = vfm.language_model.model.layers[0].self_attn.q_proj.weight + if isinstance(example_dtensor, DTensor): + assert hasattr(example_dtensor, "device_mesh") + device_mesh = example_dtensor.device_mesh + else: + device_mesh = None + + expert_heatmaps = {} + for tower in ["und", "gen"]: + expert_heatmaps_per_layer = [] + + for layer_idx in range(num_layers): + layer_module = vfm.language_model.model.layers[layer_idx] + mlp_module = layer_module.mlp if tower == "und" else layer_module.mlp_moe_gen + if isinstance(mlp_module, Qwen3VLMoeTextSparseMoeBlock): + # This is accumulated across all iterations. + total_tokens_per_expert = mlp_module.get_total_tokens_per_expert() + total_tokens = mlp_module.get_total_tokens() + + # Compute the average across all ranks. + assert device_mesh is not None, "MoE models require multiple GPUs." + total_tokens_per_expert = DTensor.from_local( + total_tokens_per_expert, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + total_tokens = DTensor.from_local( + total_tokens, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + mean_tokens_per_expert = total_tokens_per_expert.float() / total_tokens.float() # [num_experts] + expert_heatmaps_per_layer.append(mean_tokens_per_expert) + + if len(expert_heatmaps_per_layer) > 0: + expert_heatmaps[tower] = torch.stack(expert_heatmaps_per_layer, dim=0) # [num_layers,num_experts] + + return expert_heatmaps + + +class ExpertHeatmap(EveryN): + """ + Plots the expert heatmap for the MoE blocks in the language model. + + Args: + every_n (int): Number of iterations to log the expert heatmap. + """ + + def __init__(self, every_n: int = 1000): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + expert_heatmaps = compute_expert_heatmap(model.net) + + if distributed.is_rank0() and wandb.run: + for tower, heatmap in expert_heatmaps.items(): + fig, ax = plt.subplots() + im = ax.imshow(heatmap.cpu().numpy()) + ax.set_xlabel("Experts") + ax.set_ylabel("Layers") + plt.colorbar(im, ax=ax) + wandb.log( + { + f"expert_heatmap/{tower}": fig, + }, + step=iteration, + ) + plt.close(fig) diff --git a/cosmos3/_src/vfm/callbacks/generation.py b/cosmos3/_src/vfm/callbacks/generation.py new file mode 100644 index 00000000..5e045b95 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/generation.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import glob +import os +from pathlib import Path + +import einops +import numpy as np +import torch +import torchvision +import wandb +from PIL import Image + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed, log + + +class Generation(EveryN): + def __init__( + self, + every_n: int = 500, + num_vis: int = 10, + ): + r""" + This callback enables us to perform full generation from class indices. + The generated images are saved to s3. + + Args: + every_n (int): Call this callback every_n steps + num_vis (int): Number of visualizations to save + """ + super().__init__(every_n) + self.num_vis = num_vis + + def on_train_start(self, model: torch.nn.Module, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/generation" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"Callback: local_dir: {self.local_dir}") + + @torch.inference_mode() + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: torch.nn.Module, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if not hasattr(model, "run_pipe_for_data_batch"): + log.warning("Model does not have run_pipe_for_data_batch method, skipping generation") + return + if model.config.train_mllm_only: + log.warning("Skipping generation in MLLM only mode") + return + assert ( + len(data_batch["diffusion_media_input"].shape) == 5 and data_batch["diffusion_media_input"].shape[1] == 3 + ), ( + f"`diffusion_media_input` must have the shape of (bs, 3, T, H, W), current shape is {data_batch['diffusion_media_input'].shape}" + ) + + log.info(f"Generating video for iteration {iteration}, data_batch keys: {data_batch.keys()}") + video = model.run_pipe_for_data_batch(data_batch) + + input_video = data_batch["diffusion_media_input"] # [B,3,T,H,W] + + log.info(f"Video list length: {len(video)}") + rank = distributed.get_rank() + output_path = os.path.join(self.local_dir, f"iter_{iteration}") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + B, _, T, height, width = input_video.shape + gt_image = einops.rearrange(input_video, "B C T H W -> (B T) C H W") # [BT,3,H,W] + gt_image = ((gt_image + 1) / 2).clamp(0, 1).float() # [BT,3,H,W], range: 0-1 + gt_grid = torchvision.utils.make_grid(gt_image, nrow=B * T, padding=0).cpu() # [3,H,W*BT] + + video = torch.stack( + [torch.from_numpy(np.array(image.resize((width, height))) / 255.0) for image in video], dim=0 + ) # [BT,H,W,3] + video = einops.rearrange(video, "BT H W C -> BT C H W") # [BT,3,H,W] + video_grid = torchvision.utils.make_grid(video, nrow=B * T, padding=0).cpu() # [3,H,W*BT] + if video_grid.shape[2] < gt_grid.shape[2]: + # the output from sampling function is less than the ground truth, so we need to pad the video_grid on the left + pad_width = gt_grid.shape[2] - video_grid.shape[2] + video_grid = torch.nn.functional.pad(video_grid, (pad_width, 0)) # [3,H,W*BT] + video_grid[:, :, :pad_width] = gt_grid[ + :, :, :pad_width + ] # Pad the generated grid with the ground truth images + + log.info(f"video_grid: {video_grid.shape}, gt_grid: {gt_grid.shape}") + display_image = torch.stack([video_grid, gt_grid], dim=0) # [2,3,H,W*BT] + display_image = torchvision.utils.make_grid( + display_image, nrow=1, padding=2, pad_value=1.0 + ) # [3,H_total,W_total] + + log.info( + f"Generated image: {video[0].shape} -> {video_grid.shape}, gt_image: {gt_image[0].shape} -> {gt_grid.shape} | display_image: {display_image.shape}" + ) + if rank <= self.num_vis: + display_image = einops.rearrange(display_image.numpy(), "C H W -> H W C") * 255.0 # [H,W,3] + display_image = display_image.astype(np.uint8) + display_image = Image.fromarray(display_image) + current_width, current_height = display_image.size + # reduce the image size to half + display_image = display_image.resize((current_width // 2, current_height // 2)) + display_image.save(output_path + f"_rank_{rank}.jpg") + caption_list = data_batch["raw_captions"][0] + with open(output_path + f"_rank_{rank}.txt", "w") as f: + f.write("top: generation, bottom: ground truth. Left to right: condition, generation\n") + f.write(caption_list) + + # barrier + distributed.barrier() + if rank == 0 and wandb.run is not None: + file_list = ( + sorted(glob.glob(output_path + "*.jpg"))[: self.num_vis] + + sorted(glob.glob(output_path + "*.mp4"))[: self.num_vis] + ) + caption_file_list = [file.replace(".jpg", ".txt").replace(".mp4", ".txt") for file in file_list] + caption_list = [Path(caption_file).read_text() for caption_file in caption_file_list] + wandb.log( + { + "vis/generation": [ + wandb.Image(file, caption=caption) for file, caption in zip(file_list, caption_list) + ] + }, + step=iteration, + ) diff --git a/cosmos3/_src/vfm/callbacks/grad_clip.py b/cosmos3/_src/vfm/callbacks/grad_clip.py new file mode 100644 index 00000000..4276fd66 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/grad_clip.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import List, Tuple + +import torch +import wandb + +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + + +@torch.jit.script +def _fused_nan_to_num(params: List[torch.Tensor]): + for param in params: + torch.nan_to_num(param, nan=0.0, posinf=0.0, neginf=0.0, out=param) + + +@dataclass +class _MagnitudeRecord: + state: float = 0 + iter_count: int = 0 + + def reset(self) -> None: + self.state = 0 + self.iter_count = 0 + + def update(self, cur_state: torch.Tensor) -> None: + self.state += cur_state + self.iter_count += 1 + + def get_stat(self) -> Tuple[float, float]: + if self.iter_count > 0: + avg_state = self.state / self.iter_count + avg_state = avg_state.item() + else: + avg_state = 0 + self.reset() + return avg_state + + +class GradClip(Callback): + """ + This callback is used to clip the gradient norm of the model. + It also logs the average gradient norm of the model to wandb. + """ + + def __init__(self, clip_norm=1.0, force_finite: bool = True): + self.clip_norm = clip_norm + self.force_finite = force_finite + + self.img_mag_log = _MagnitudeRecord() + self.video_mag_log = _MagnitudeRecord() + self._cur_state = None + + def on_training_step_start( + self, model: OmniMoTModel, data_batch: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + if model.is_image_batch(data_batch): + self._cur_state = self.img_mag_log + else: + self._cur_state = self.video_mag_log + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + del optimizer, scheduler + if isinstance(model_ddp, distributed.DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + params = [] + + if self.force_finite: + for param in model.parameters(): + if param.grad is not None: + params.append(param.grad) # [*param_shape] + _fused_nan_to_num(params) + + total_norm = model.clip_grad_norm_(self.clip_norm) # [] + + self._cur_state.update(total_norm) + if iteration % self.config.trainer.logging_iter == 0: + avg_img_mag, avg_video_mag = self.img_mag_log.get_stat(), self.video_mag_log.get_stat() + if wandb.run: + wandb.log( + { + "clip_grad_norm/image": avg_img_mag, + "clip_grad_norm/video": avg_video_mag, + "iteration": iteration, + }, + step=iteration, + ) diff --git a/cosmos3/_src/vfm/callbacks/heart_beat.py b/cosmos3/_src/vfm/callbacks/heart_beat.py new file mode 100644 index 00000000..e39a0970 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/heart_beat.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from datetime import datetime + +import pytz +import torch + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +class HeartBeat(EveryN): + """ + A callback that logs a heartbeat message at regular intervals to indicate that the training process is still running. + + Args: + every_n (int): The frequency at which the callback is invoked. + step_size (int, optional): The step size for the callback. Defaults to 1. + update_interval_in_minute (int, optional): The interval in minutes for logging the heartbeat. Defaults to 20 minutes. + save_s3 (bool, optional): Whether to save the heartbeat information to S3. Defaults to False. + """ + + def __init__(self, every_n: int, step_size: int = 1, update_interval_in_minute: int = 20, save_s3: bool = False): + super().__init__(every_n=every_n, step_size=step_size) + self.name = self.__class__.__name__ + self.update_interval_in_minute = update_interval_in_minute + self.save_s3 = save_s3 + self.pst = pytz.timezone("America/Los_Angeles") + self.is_hitted = False + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.time = time.time() + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}_start.yaml") + easy_io.dump(info, f"s3://timestamps_rundir/{self.name}_start.yaml") + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if not self.is_hitted: + self.is_hitted = True + if distributed.get_rank() == 0: + self.report(iteration) + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + @distributed.rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if time.time() - self.time > 60 * self.update_interval_in_minute: + self.report(iteration) + + def report(self, iteration: int = 0): + self.time = time.time() + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}.yaml") + + @distributed.rank0_only + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}_end.yaml") + easy_io.dump(info, f"s3://timestamps_rundir/{self.name}_end.yaml") diff --git a/cosmos3/_src/vfm/callbacks/hf_export.py b/cosmos3/_src/vfm/callbacks/hf_export.py new file mode 100644 index 00000000..96ff278b --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/hf_export.py @@ -0,0 +1,471 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""HFExportCallback: export VLM DCP checkpoints to HuggingFace safetensors format. + +Design notes +------------ +- Hooks into ``on_save_checkpoint`` (called by DistributedCheckpointer.save() before I/O). +- All ranks participate in the weight-gather phase (DTensor.full_tensor() all-gathers). +- Rank 0 accumulates CPU tensors, writes shards, and uploads — other ranks exit early. +- File I/O and upload run in a background thread on rank 0 to avoid blocking training. +- Worker exceptions are stored in ``_worker_exception`` and re-raised on the next + checkpoint or at train end, so failures are never silently swallowed. +- Controlled entirely via ``config.checkpoint.hf_export`` (HFExportConfig). + +Phase 2+ note +------------- +Weight parameters are iterated via ``model.model._model.named_parameters()`` where +``model.model`` is the ``HFModel`` wrapper and ``model.model._model`` is the underlying +HuggingFace transformer. Parameter names are already HF-native — no weight_mapper +remapping is required. +""" + +import json +import os +import shutil +import threading +from typing import Any + +import torch + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.distributed import is_rank0 + +try: + from safetensors.torch import save_file as _safetensors_save_file +except ImportError: + _safetensors_save_file = None + +try: + from transformers import AutoTokenizer, GenerationConfig +except ImportError: + AutoTokenizer = None + GenerationConfig = None + +# Map string dtype names (as stored in TrainConfig.param_dtype) to torch dtypes. +_DTYPE_MAP: dict[str, torch.dtype] = { + "float32": torch.float32, + "float16": torch.float16, + "bfloat16": torch.bfloat16, + "float64": torch.float64, +} + + +def _upload_folder_to_s3(local_folder: str, bucket: str, s3_prefix: str, credential_path: str) -> None: + """Upload every file under *local_folder* to ``s3://{bucket}/{s3_prefix}/...``. + + Uses the i4 ``easy_io`` S3 backend (Boto3Backend), which reads credentials from + *credential_path*. Files are uploaded as streaming transfers via boto3's + ``upload_file()`` — the full shard is never loaded into memory. + """ + from cosmos3._src.imaginaire.utils.easy_io import easy_io + + backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": credential_path, + "path_mapping": None, + } + ) + for root, _, files in os.walk(local_folder): + for fname in sorted(files): + local_path = os.path.join(root, fname) + rel = os.path.relpath(local_path, local_folder) + s3_path = f"s3://{bucket}/{s3_prefix}/{rel}" + # Pass the local path string so Boto3Backend uses upload_file() — + # a streaming transfer that avoids reading the whole shard into memory. + backend.put(local_path, s3_path) + log.info(f"[HFExportCallback] Uploaded {local_path} → {s3_path}") + + +class HFExportCallback(Callback): + """Export VLM weights to HuggingFace-compatible safetensors after each DCP checkpoint. + + Enabled / configured via ``config.checkpoint.hf_export`` (HFExportConfig). Disabled + by default — add this callback and set ``hf_export.enabled = True`` to activate. + + Exports written to:: + + {job.path_local}/hf_exports/iter_{iteration:09d}/ + 00000.safetensors + ... + model.safetensors.index.json + config.json + tokenizer.json (if tokenizer can be loaded from model_name_or_path) + + Optionally uploads to: + - S3 (``hf_export.upload_to_object_store``) + - HuggingFace Hub (``hf_export.hf_repo_id``) + + Args: + dtype: Export weight dtype (e.g. ``"bfloat16"``). Use + ``"${model.config.train.param_dtype}"`` in the Hydra callback config to inherit from + the training precision. + """ + + # HuggingFace convention: max 4 GB per shard file. + _MAX_SHARD_BYTES: int = 4 * 1024**3 + + def __init__(self, dtype: str = "bfloat16") -> None: + self._export_dtype: torch.dtype | None = _DTYPE_MAP.get(dtype) + self._current_iteration: int = 0 + self._export_thread: threading.Thread | None = None + # Stores any exception raised inside the background worker so it can be + # re-raised on the main thread at the next checkpoint or train end. + self._worker_exception: BaseException | None = None + + # ------------------------------------------------------------------ + # Callback hooks + # ------------------------------------------------------------------ + + def on_save_checkpoint_start(self, model: Any, iteration: int = 0) -> None: + self._current_iteration = iteration + + def on_save_checkpoint(self, model: Any, state_dict: dict[str, Any]) -> None: + hf_cfg = self.config.checkpoint.hf_export + if not hf_cfg.enabled: + return + + iteration = self._current_iteration + if iteration % hf_cfg.export_every_n != 0: + return + + # Deferred import to avoid circular dependency at module load time. + from cosmos3._src.vfm.models.vlm_model import VLMModel + + if not isinstance(model, VLMModel): + # The legacy vlm/train.py path passes model_parts: list[nn.Module] (raw HF + # models without the VLMModel attribute structure). HF export requires the + # VLMModel wrapper, which is only available via the unified scripts/train.py path. + if isinstance(model, list): + log.warning( + "[HFExportCallback] Received model_parts (list) instead of VLMModel. " + "HF export requires the unified training path (scripts/train.py). Skipping." + ) + else: + log.warning( + "[HFExportCallback] model is not VLMModel (got %s); skipping HF export.", + type(model).__name__, + ) + return + + if _safetensors_save_file is None: + raise ImportError("safetensors is required for HFExportCallback. Install it with: pip install safetensors") + + output_dir = os.path.join(self.config.job.path_local, "hf_exports", f"iter_{iteration:09d}") + + # ---------------------------------------------------------------- + # Phase 1 (all ranks): gather sharded parameters into CPU chunks. + # full_tensor() is a collective operation — all ranks must participate. + # ---------------------------------------------------------------- + cpu_chunks, manifest, total_size = self._gather_weights(model) + + # ---------------------------------------------------------------- + # Phase 2 (rank 0, background thread): file I/O + optional upload. + # ---------------------------------------------------------------- + if not is_rank0(): + return + + # Block on any still-running export from the previous checkpoint and + # propagate any worker exception before starting a new export. + if self._export_thread is not None and self._export_thread.is_alive(): + log.warning( + "[HFExportCallback] Previous export thread still running; waiting before starting export for iter %d.", + iteration, + ) + self._export_thread.join() + + if self._worker_exception is not None: + exc = self._worker_exception + self._worker_exception = None + raise RuntimeError(f"[HFExportCallback] Previous export failed with: {exc}") from exc + + self._export_thread = threading.Thread( + target=self._save_and_upload, + args=(cpu_chunks, manifest, total_size, model.hf_config, model.model_name_or_path, output_dir, iteration), + daemon=True, + ) + self._export_thread.start() + + def on_train_end(self, model: Any, iteration: int = 0) -> None: + """Wait for the final export thread so the process does not exit prematurely.""" + if self._export_thread is not None and self._export_thread.is_alive(): + log.info("[HFExportCallback] Waiting for export thread to finish...") + self._export_thread.join() + log.info("[HFExportCallback] Export thread done.") + + if self._worker_exception is not None: + exc = self._worker_exception + self._worker_exception = None + raise RuntimeError(f"[HFExportCallback] Export thread failed with: {exc}") from exc + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _gather_weights(self, model: Any) -> tuple[list[dict[str, torch.Tensor]], dict[str, str], int]: + """Iterate model parameters, all-gather DTensor shards, and build CPU chunks. + + Must be called on **all ranks**. Only rank 0 populates the returned + ``cpu_chunks`` and ``manifest``; other ranks return empty structures but still + participate in the distributed all-gathers. + + Returns: + cpu_chunks: List of ``{weight_name: cpu_tensor}`` dicts, one per shard file. + manifest: Mapping of ``weight_name → shard_filename``. + total_size: Total byte count of all exported tensors (for the index JSON). + """ + cpu_chunks: list[dict[str, torch.Tensor]] = [] + manifest: dict[str, str] = {} + current_chunk: dict[str, torch.Tensor] = {} + current_chunk_bytes: int = 0 + total_size: int = 0 + file_idx: int = 0 + + for name, param in model.model._model.named_parameters(): + # Phase 2+: HFModel initialises _model via AutoModelForImageTextToText / + # AutoModelForCausalLM, so parameter names are HF-native and match the + # safetensors checkpoint keys loaded by _load_vlm_weights(). + # + # MoE note: Qwen3VLMoeTextExpertsGroupedMm stores expert weights in HF-native + # grouped layout — gate_up_proj: [E, H, 2F], down_proj: [E, F, H] — matching + # the checkpoint format exactly. No transposition or per-expert fan-out is + # needed. (The legacy Phase 0 path stored tensors in a transposed internal + # format [E, 2F, H] under the name "gate_and_up_projs" and required + # weight_mapper.policy_map_local_key_for_export_tensor() to transpose back on + # export. Phase 2 uses HFModel and has no such internal reformat.) + # + # torch.compile and gradient-checkpointing wrappers inject prefixes into + # named_parameters() output. Strip them so exported keys are HF-native, + # matching what HFModel._load_vlm_weights() does for the in-memory state dict. + name = name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", "") + + # Gather across FSDP / TP / CP ranks (collective — all ranks must call). + if isinstance(param, torch.distributed.tensor.DTensor): + param = param.full_tensor() + param = param.detach() + if self._export_dtype is not None: + param = param.to(dtype=self._export_dtype) + + tensor_bytes = param.element_size() * param.numel() + + # Flush the current chunk when the shard size limit would be exceeded. + # current_chunk_bytes is tracked on ALL ranks so shard boundaries are + # consistent (the shard_name written into manifest must agree everywhere). + if current_chunk_bytes + tensor_bytes > self._MAX_SHARD_BYTES and current_chunk_bytes > 0: + if is_rank0(): + cpu_chunks.append(current_chunk) + current_chunk = {} + file_idx += 1 + current_chunk_bytes = 0 + + shard_name = f"{file_idx:05d}.safetensors" + if is_rank0(): + current_chunk[name] = param.cpu() + manifest[name] = shard_name + total_size += tensor_bytes + current_chunk_bytes += tensor_bytes + + # Flush the final (possibly partial) chunk. + if current_chunk_bytes > 0 and is_rank0() and current_chunk: + cpu_chunks.append(current_chunk) + + return cpu_chunks, manifest, total_size + + def _save_and_upload( + self, + cpu_chunks: list[dict[str, torch.Tensor]], + manifest: dict[str, str], + total_size: int, + hf_config: Any, + model_name_or_path: str, + output_dir: str, + iteration: int, + ) -> None: + """Write safetensors shards, HF config, tokenizer; upload to S3 / HF Hub. + + Runs on rank 0 inside a background thread. Any exception is stored in + ``self._worker_exception`` so the main thread can re-raise it. + """ + try: + self._do_save_and_upload( + cpu_chunks, manifest, total_size, hf_config, model_name_or_path, output_dir, iteration + ) + except Exception as exc: + log.error( + "[HFExportCallback] Export worker for iter %d raised an exception: %s", + iteration, + exc, + exc_info=True, + ) + self._worker_exception = exc + + def _do_save_and_upload( + self, + cpu_chunks: list[dict[str, torch.Tensor]], + manifest: dict[str, str], + total_size: int, + hf_config: Any, + model_name_or_path: str, + output_dir: str, + iteration: int, + ) -> None: + """Core export logic (called from the background thread via ``_save_and_upload``). + + Error handling is tiered: + - Steps 1-4 (shards, index JSON, HF config, source-model file copy): any exception + propagates to the outer ``_save_and_upload`` wrapper so the main thread is notified. + A failed file copy leaves the checkpoint unusable for trust_remote_code models, so + it is treated as a hard failure like the shard writes. + - Steps 5-7 (tokenizer, generation_config, S3 upload, HF Hub upload): failures are + treated as soft warnings. The tokenizer and generation config are best-effort; upload + failures do not invalidate the local safetensors export, so an outage must not abort + training. + """ + hf_cfg = self.config.checkpoint.hf_export + os.makedirs(output_dir, exist_ok=True) + log.info(f"[HFExportCallback] Writing iter {iteration} export to {output_dir}") + + # 1. Safetensors shards — one file per chunk (ordered by file_idx). + # Each chunk is cleared after writing so its tensors can be GC'd + # incrementally rather than being held until the whole loop completes. + for i in range(len(cpu_chunks)): + chunk = cpu_chunks[i] + shard_path = os.path.join(output_dir, f"{i:05d}.safetensors") + _safetensors_save_file(chunk, shard_path) + log.info(f"[HFExportCallback] Wrote {shard_path}") + cpu_chunks[i] = {} # release tensor references for GC + + # 2. model.safetensors.index.json + # total_size is pre-computed in _gather_weights to avoid needing chunks here. + index_json = {"metadata": {"total_size": total_size}, "weight_map": manifest} + index_path = os.path.join(output_dir, "model.safetensors.index.json") + with open(index_path, "w") as fh: + json.dump(index_json, fh, indent=4) + + # 3. HuggingFace model config. + hf_config.save_pretrained(output_dir) + + # 4. Copy missing .py/.json files for trust_remote_code models. + # Only applicable when model_name_or_path is a local directory. + # The full directory layout is preserved so nested packages referenced by + # auto_map are included (mirroring convert_checkpoint.py's copytree approach). + # Files already present in the export dir (e.g., config.json written by + # hf_config.save_pretrained) are never overwritten. + # HARD failure: a broken copy leaves the checkpoint unloadable, so any I/O error + # propagates to the background-worker wrapper (same as shard writes). + if model_name_or_path and os.path.isdir(model_name_or_path): + real_src = os.path.realpath(model_name_or_path) + real_out = os.path.realpath(output_dir) + copied = [] + for root, dirs, files in os.walk(real_src): + real_root = os.path.realpath(root) + # Prune any subtree that is, leads to, or is inside output_dir. + # This prevents recursing into previously written export dirs when + # output_dir (or a parent of it) lives inside model_name_or_path. + dirs[:] = [ + d + for d in dirs + if not ( + (p := os.path.realpath(os.path.join(real_root, d))) == real_out + or p.startswith(real_out + os.sep) + or real_out.startswith(p + os.sep) + ) + ] + if real_root == real_out or real_root.startswith(real_out + os.sep): + continue + rel_dir = os.path.relpath(real_root, real_src) + for fname in files: + if not (fname.endswith(".py") or fname.endswith(".json")): + continue + src = os.path.join(real_root, fname) + dst_dir = output_dir if rel_dir == "." else os.path.join(output_dir, rel_dir) + dst = os.path.join(dst_dir, fname) + if not os.path.exists(dst): + os.makedirs(dst_dir, exist_ok=True) + shutil.copy2(src, dst) + copied.append(os.path.join(rel_dir, fname) if rel_dir != "." else fname) + if copied: + log.info(f"[HFExportCallback] Copied missing files from source model: {copied}") + + # 5. Tokenizer (best-effort — may fail for custom / gated models). + if AutoTokenizer is not None and model_name_or_path: + try: + tok = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True) + tok.save_pretrained(output_dir) + except Exception as exc: + log.warning(f"[HFExportCallback] Tokenizer save skipped: {exc}") + + # 6. Generation config (best-effort — not all models expose one). + if GenerationConfig is not None and model_name_or_path: + try: + gen_cfg = GenerationConfig.from_pretrained(model_name_or_path, trust_remote_code=True) + gen_cfg.save_pretrained(output_dir) + except Exception as exc: + log.warning(f"[HFExportCallback] generation_config save skipped: {exc}") + + # 7. S3 upload — soft failure: local export is intact regardless of upload outcome. + obj_store = hf_cfg.upload_to_object_store + if obj_store.enabled: + s3_prefix = f"{self.config.job.path}/hf_exports/iter_{iteration:09d}" + try: + _upload_folder_to_s3(output_dir, obj_store.bucket, s3_prefix, obj_store.credentials) + log.info(f"[HFExportCallback] S3 upload done: s3://{obj_store.bucket}/{s3_prefix}") + except Exception as exc: + # Intentionally soft: an upload outage must not crash training. + log.warning(f"[HFExportCallback] S3 upload failed (local export intact): {exc}") + + # 8. HuggingFace Hub upload — soft failure: see comment above. + if hf_cfg.hf_repo_id: + self._upload_to_hf_hub(output_dir, hf_cfg.hf_repo_id) + + log.info(f"[HFExportCallback] Export complete for iter {iteration}.") + + @staticmethod + def _upload_to_hf_hub(output_dir: str, repo_id: str, max_retries: int = 3) -> None: + try: + from huggingface_hub import HfApi + except ImportError: + log.warning("[HFExportCallback] huggingface_hub not installed; skipping HF Hub upload.") + return + + api = HfApi() + for attempt in range(1, max_retries + 1): + try: + api.create_repo(repo_id=repo_id, exist_ok=True) + break + except Exception as exc: + log.warning(f"[HFExportCallback] create_repo attempt {attempt}/{max_retries} failed: {exc}") + if attempt == max_retries: + log.warning( + f"[HFExportCallback] Could not create HF Hub repo '{repo_id}' after " + f"{max_retries} attempts; skipping upload." + ) + return + + for attempt in range(1, max_retries + 1): + try: + api.upload_folder( + folder_path=output_dir, + repo_id=repo_id, + commit_message=f"Upload checkpoint from {os.path.basename(output_dir)}", + ) + log.info(f"[HFExportCallback] Uploaded to HF Hub: {repo_id}") + return + except Exception as exc: + log.warning(f"[HFExportCallback] HF Hub upload attempt {attempt}/{max_retries} failed: {exc}") + + log.warning(f"[HFExportCallback] All {max_retries} HF Hub upload attempts failed for {repo_id}.") diff --git a/cosmos3/_src/vfm/callbacks/iter_speed.py b/cosmos3/_src/vfm/callbacks/iter_speed.py new file mode 100644 index 00000000..5d36ec19 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/iter_speed.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import torch +import wandb +from torch import Tensor + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.distributed import rank0_only +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +class IterSpeed(EveryN): + """ + Args: + hit_thres (int): Number of iterations to wait before logging. + save_s3 (bool): Whether to save to S3. + save_s3_every_log_n (int): Save to S3 every n log iterations, which means save_s3_every_log_n n * every_n global iterations. + """ + + def __init__(self, *args, hit_thres: int = 5, save_s3: bool = True, save_s3_every_log_n: int = 10, **kwargs): + super().__init__(*args, **kwargs) + self.time = None + self.hit_counter = 0 + self.hit_thres = hit_thres + self.save_s3 = save_s3 + self.save_s3_every_log_n = save_s3_every_log_n + self.name = self.__class__.__name__ + self.last_hit_time = time.time() + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.hit_counter < self.hit_thres: + log.info( + f"Iteration {iteration}: " + f"Hit counter: {self.hit_counter + 1}/{self.hit_thres} | " + f"Loss: {loss.detach().item():.4f} | " + f"Time: {time.time() - self.last_hit_time:.2f}s", + rank0_only=False, + ) + self.hit_counter += 1 + self.last_hit_time = time.time() + #! useful for large scale training and avoid oom crash in the first two iterations!!! + torch.cuda.synchronize() + return + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, Tensor], + output_batch: dict[str, Tensor], + loss: Tensor, + iteration: int, + ) -> None: + if self.time is None: + self.time = time.time() + return + cur_time = time.time() + iter_speed = (cur_time - self.time) / self.every_n / self.step_size + + log.info( + f"{iteration} : iter_speed {iter_speed:.2f} seconds per iteration | Loss: {loss.detach().item():.4f}", + rank0_only=False, + ) + + is_image_batch = model.is_image_batch(data_batch) + per_sample_batch_counter = dict() + if is_image_batch: + image_batch_size = len(data_batch[model.input_image_key]) + per_sample_batch_counter["image_batch_size"] = image_batch_size + else: + video_batch_size = len(data_batch[model.input_video_key]) + per_sample_batch_counter["video_batch_size"] = video_batch_size + + if wandb.run: + sample_counter = getattr(trainer, "sample_counter", iteration) + wandb.log( + { + "timer/iter_speed": iter_speed, + "sample_counter": sample_counter, + } + | per_sample_batch_counter, + step=iteration, + ) + self.time = cur_time + if self.save_s3: + if iteration % (self.save_s3_every_log_n * self.every_n) == 0: + easy_io.dump( + { + "iter_speed": iter_speed, + "iteration": iteration, + }, + f"s3://rundir/{self.name}/iter_{iteration:09d}.yaml", + ) diff --git a/cosmos3/_src/vfm/callbacks/load_pretrained.py b/cosmos3/_src/vfm/callbacks/load_pretrained.py new file mode 100644 index 00000000..d140088d --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/load_pretrained.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils.callback import Callback + + +class LoadPretrained(Callback): + def __init__(self): + r""" + This callback enables us to load pretrained model weights if needed. + Model weights are initialized from safetensors if not loaded already from DCP checkpoint. + """ + super().__init__() + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + model.load_pretrained_model_if_needed() diff --git a/cosmos3/_src/vfm/callbacks/low_precision.py b/cosmos3/_src/vfm/callbacks/low_precision.py new file mode 100644 index 00000000..0a0438fc --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/low_precision.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from typing import Union + +import torch + +from cosmos3._src.imaginaire.config import Config +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import LowPrecisionCallback as BaseLowPrecisionCallback +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + + +class LowPrecisionCallback(BaseLowPrecisionCallback): + """ + Config with non-primitive type makes it difficult to override the option. + The callback gets precision from model.precision instead. + It also auto disabled when using fp32. + """ + + def __init__(self, config: Config, trainer: ImaginaireTrainer, update_iter: int): + self.config = config + self.trainer = trainer + self.update_iter = update_iter + + def on_train_start(self, model: Union[OmniMoTModel, list[OmniMoTModel]], iteration: int = 0) -> None: + if not isinstance(model, list): + model = [model] + for model_part in model: + if model_part.precision == torch.float32: + log.critical("Using fp32, should disable master weights.") + self.update_iter = sys.maxsize + else: + assert model_part.precision in [ + torch.bfloat16, + torch.float16, + torch.half, + ], "LowPrecisionCallback must use a low precision dtype." + self.precision_type = model_part.precision diff --git a/cosmos3/_src/vfm/callbacks/mfu.py b/cosmos3/_src/vfm/callbacks/mfu.py new file mode 100644 index 00000000..71a5a1be --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/mfu.py @@ -0,0 +1,318 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MFU (Model FLOPs Utilization) callback for OmniMoT training. + +Computes and logs MFU metrics for specified hardware targets (e.g. H100, GB200) +by calculating the actual training FLOPs per step and comparing against +theoretical peak throughput. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from decimal import Decimal + +import torch +import wandb + +from cosmos3._src.imaginaire.attention.utils import is_blackwell_dc +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.flops import ( + OmniMoTModelDescriptor, + compute_omni_mot_flops_per_batch, + compute_wan_vae_encoder_flops, + get_omni_mot_model_descriptor, +) +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.distributed import rank0_only + + +@dataclass +class HardwareTarget: + """Specification of a hardware target for MFU computation. + + Attributes: + name: Human-readable name (used as W&B tag, e.g. "H100"). + peak_tflops: Theoretical peak throughput in TFLOPS (e.g. 989 for H100 BF16). + """ + + name: str + peak_tflops: float + + +# Pre-defined hardware targets +H100 = HardwareTarget(name="H100", peak_tflops=989.0) +GB200 = HardwareTarget(name="GB200", peak_tflops=2250.0) + + +class MFUCallback(EveryN): + """Callback that computes and logs Model FLOPs Utilization (MFU) to W&B. + + MFU is defined as: + MFU = achieved_tflops_per_gpu / peak_tflops_per_gpu + + where achieved_tflops_per_gpu is computed from the model's theoretical + training FLOPs (forward + backward) divided by the measured wall-clock + time per step. + + This callback accumulates per-step FLOPs between logging intervals and + reports the average MFU over that window. + + Args: + backwardpass_ratio: Ratio of backward-to-forward FLOPs (default 2.0). + hit_thres: Number of warm-up iterations before logging begins. + include_vae_encoder: If True (default), include the Wan 2.2 VAE encoder + forward-pass FLOPs in the per-step total. The VAE is frozen during + training so only forward FLOPs are counted. + include_padding: If True, include FLOPs spent on padding tokens (the + causal split appended by sequence-packing finalize()). Gives a + ``total GPU FLOPs`` view instead of ``useful FLOPs`` only. + grad_accum_iter: Number of gradient accumulation steps per optimizer + update (default 1). When > 1, ``on_training_step_end`` is called + once per optimizer step but the wall-clock time covers all + micro-batches, so per-step FLOPs are multiplied by this count. + """ + + def __init__( + self, + *args, + backwardpass_ratio: float = 2.0, + hit_thres: int = 5, + include_vae_encoder: bool = True, + include_padding: bool = True, + grad_accum_iter: int = 1, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.hardware_target = GB200 if is_blackwell_dc() else H100 + self.backwardpass_ratio = backwardpass_ratio + self.hit_thres = hit_thres + self.include_vae_encoder = include_vae_encoder + self.include_padding = include_padding + self.grad_accum_iter = grad_accum_iter + + # Lazily initialised from model config on first call + self._model_descriptor: OmniMoTModelDescriptor | None = None + self._freeze_und: bool = False + self._vision_gen: bool = True + self._action_gen: bool = False + self._sound_gen: bool = False + self._world_size: int = 1 + self._use_activation_checkpointing: bool = False + + # Accumulation state between every_n windows + self._accumulated_flops = Decimal(0) + self._accumulated_flops_vae = Decimal(0) + self._steps_in_window: int = 0 + self._window_start_time: float | None = None + + # Warm-up counter + self._hit_counter: int = 0 + + # ------------------------------------------------------------------ # + # Lazy initialisation from model + # ------------------------------------------------------------------ # + + def _ensure_initialised(self, model: ImaginaireModel) -> None: + """Build the ``OmniMoTModelDescriptor`` from the live model config.""" + if self._model_descriptor is not None: + return + + # Access VLM config from the language model inside the network + vlm_cfg = model.net.language_model.config # type: ignore[attr-defined] + net_cfg = model.net.config # type: ignore[attr-defined] + + self._freeze_und = getattr(vlm_cfg, "freeze_und", False) + self._vision_gen = getattr(net_cfg, "vision_gen", True) + self._action_gen = getattr(net_cfg, "action_gen", False) + self._sound_gen = getattr(net_cfg, "sound_gen", False) + + # Read activation checkpointing from the model's parallelism config + model_cfg = getattr(model, "config", None) + parallelism_cfg = getattr(model_cfg, "parallelism", None) + self._use_activation_checkpointing = getattr(parallelism_cfg, "use_activation_checkpointing", False) + + # MoE fields (may not exist for dense-only configs) + use_moe = getattr(vlm_cfg, "use_moe", False) + num_experts = getattr(vlm_cfg, "num_experts", 0) + num_experts_per_tok = getattr(vlm_cfg, "num_experts_per_tok", 0) + moe_intermediate_size = getattr(vlm_cfg, "moe_intermediate_size", 0) + decoder_sparse_step = getattr(vlm_cfg, "decoder_sparse_step", 1) + mlp_only_layers = list(getattr(vlm_cfg, "mlp_only_layers", [])) + + self._model_descriptor = get_omni_mot_model_descriptor( + hidden_size=vlm_cfg.hidden_size, + num_hidden_layers=vlm_cfg.num_hidden_layers, + num_attention_heads=vlm_cfg.num_attention_heads, + num_key_value_heads=vlm_cfg.num_key_value_heads, + head_dim=getattr(vlm_cfg, "head_dim", None), + intermediate_size=vlm_cfg.intermediate_size, + vocab_size=vlm_cfg.vocab_size, + use_moe=use_moe, + num_experts=num_experts, + num_experts_per_tok=num_experts_per_tok, + moe_intermediate_size=moe_intermediate_size, + decoder_sparse_step=decoder_sparse_step, + mlp_only_layers=mlp_only_layers, + latent_patch_size=getattr(net_cfg, "latent_patch_size", 2), + latent_channel_size=getattr(net_cfg, "latent_channel_size", 48), + action_dim=getattr(net_cfg, "action_dim", 32), + sound_dim=getattr(net_cfg, "sound_dim", 64), + frequency_embedding_size=getattr(net_cfg, "frequency_embedding_size", 256), + predict_text_tokens=getattr(net_cfg, "predict_text_tokens", False), + ) + + self._world_size = torch.distributed.get_world_size() if torch.distributed.is_initialized() else 1 + + # ------------------------------------------------------------------ # + # Per-step accumulation + # ------------------------------------------------------------------ # + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # Warm-up: skip first few iterations (compilation, allocation, etc.) + if self._hit_counter < self.hit_thres: + self._hit_counter += 1 + return + + self._ensure_initialised(model) + + # Start the timing window on the first post-warmup step + if self._window_start_time is None: + self._window_start_time = time.monotonic() + + # Extract per-modality token counts from output_batch + und_token_length = output_batch.get("und_token_length") + if und_token_length is None: + return + + und_tokens = int(und_token_length) + vision_tokens = int(output_batch.get("vision_token_length", 0)) + action_tokens = int(output_batch.get("action_token_length", 0)) + sound_tokens = int(output_batch.get("sound_token_length", 0)) + + # Per-split attention metadata for packed sequences + split_lens: list[int] | None = output_batch.get("split_lens") + attn_modes_list: list[str] | None = output_batch.get("attn_modes") + + # Compute FLOPs for this per-device micro-batch. + # B = 1 because token counts are already summed across all samples in + # the packed sequence on this device. + assert self._model_descriptor is not None + step_flops = compute_omni_mot_flops_per_batch( + cfg=self._model_descriptor, + B=1, + text_tokens=und_tokens, + vision_tokens=vision_tokens, + action_tokens=action_tokens, + sound_tokens=sound_tokens, + freeze_und=self._freeze_und, + vision_gen=self._vision_gen, + action_gen=self._action_gen, + sound_gen=self._sound_gen, + backwardpass_ratio=self.backwardpass_ratio, + split_lens=split_lens, + attn_modes=attn_modes_list, + include_padding=self.include_padding, + use_activation_checkpointing=self._use_activation_checkpointing, + ) + + # VAE encoder forward-pass FLOPs (frozen, no backward). + if self.include_vae_encoder: + vae_pixel_shapes = output_batch.get("vae_pixel_shapes") + if vae_pixel_shapes: + for pT, pH, pW in vae_pixel_shapes: + vae_flops = compute_wan_vae_encoder_flops(B=1, T=pT, H=pH, W=pW) + self._accumulated_flops_vae += vae_flops + step_flops += vae_flops + + # When gradient accumulation is used, on_training_step_end is called + # once per optimizer step (not per micro-batch). Multiply by the + # accumulation count so the FLOPs cover all micro-batches in the step. + # For VAE with gradient accumulation we assume all micro-batches have the same FLOP count + if self.grad_accum_iter > 1: + step_flops *= self.grad_accum_iter + + self._accumulated_flops += step_flops + self._steps_in_window += 1 + + # Delegate to EveryN for the periodic reporting logic + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + # ------------------------------------------------------------------ # + # Periodic reporting + # ------------------------------------------------------------------ # + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if self._window_start_time is None or self._steps_in_window == 0: + return + + elapsed = time.monotonic() - self._window_start_time + if elapsed <= 0: + return + + if self._accumulated_flops <= 0: + log.warning( + f"Number of calculated FLOPs must be more than 0, got {self._accumulated_flops} at iteration {iteration} for {self._steps_in_window} steps." + ) + + # Achieved TFLOPS *per GPU* over the window + # accumulated_flops is the total per-device FLOPs over all steps in window + achieved_tflops_per_gpu = float(self._accumulated_flops) / elapsed / 1e12 + + avg_flops_per_step = float(self._accumulated_flops) / self._steps_in_window + avg_time_per_step = elapsed / self._steps_in_window + + log_info: dict[str, float] = { + "mfu/achieved_tflops_per_gpu": achieved_tflops_per_gpu, + "mfu/avg_flops_per_step": avg_flops_per_step, + "mfu/avg_time_per_step_s": avg_time_per_step, + "mfu/steps_in_window": float(self._steps_in_window), + "mfu/vae_flops_percentage": float(self._accumulated_flops_vae / self._accumulated_flops) * 100.0, + } + + mfu = ( + achieved_tflops_per_gpu / self.hardware_target.peak_tflops if self.hardware_target.peak_tflops > 0 else 0.0 + ) + log_info[f"mfu/{self.hardware_target.name}"] = mfu + + # W&B log + if wandb.run is not None: + wandb.log(log_info, step=iteration) + + # Reset accumulation window + self._accumulated_flops = Decimal(0) + self._accumulated_flops_vae = Decimal(0) + self._steps_in_window = 0 + self._window_start_time = time.monotonic() diff --git a/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py b/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py new file mode 100644 index 00000000..5954c059 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py @@ -0,0 +1,203 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +""" +MoE Specialization Callback +============================ +Monitors whether MoE experts are developing distinct, stable roles over training. +A well-trained MoE should have experts that specialize — each processing a different +kind of input — rather than a few generalist experts doing everything while the rest +idle. + + Expert Co-activation Rate + ------------------------- + If two experts frequently fire together on the same token (both in the top-K + selected), they are likely learning redundant representations. Ideally experts + specialize on non-overlapping token types, so co-activation should stay close + to the chance baseline of K/N (e.g. 8/128 ≈ 0.0625 for the 235B model). + + For each layer and each unique expert pair (i, j), we compute: + CoAct(i, j) = N_{i,j} / N_i + where N_{i,j} = number of tokens where both i and j were selected, and N_i = + total tokens routed to expert i. We then summarize across all pairs as max and + mean. A rising mean_coact, especially well above the chance baseline, signals + that the router is collapsing onto a small correlated cluster of experts. + +Buffer ownership +---------------- + coactivation_counts is reset here (in compute_moe_coactivation_metrics). + Per-expert token counts are derived from coactivation_counts itself + (row_sum + col_sum) / (K-1), so this callback is fully independent of + ExpertHeatmap's reset cycle for total_tokens_per_expert. +""" + +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def _get_device_mesh(vfm: torch.nn.Module): + weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight + return weight.device_mesh if isinstance(weight, DTensor) else None + + +def _allreduce_dtensor(t: torch.Tensor, device_mesh) -> torch.Tensor: + """Sum-reduce a local tensor across all FSDP ranks and return the global tensor.""" + return DTensor.from_local( + t, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + +def compute_moe_coactivation_metrics(vfm: torch.nn.Module) -> dict[str, dict]: + """ + Compute per-layer Expert Co-activation metrics for both towers. + + For each unique expert pair (i < j) in the upper triangle of the N×N + coactivation matrix, computes: + CoAct(i, j) = N_{i,j} / N_i + where N_{i,j} is the count of tokens where both i and j were in the top-K, + and N_i is the total token count for expert i (the row expert, i.e. the + lower-indexed expert in the pair). + + N_i is derived directly from the co-activation matrix rather than from + the shared total_tokens_per_expert buffer, so this metric is independent + of ExpertHeatmap's reset cycle. Each token routed to expert i contributes + to (K-1) co-activation pairs, so N_i = (row_sum_i + col_sum_i) / (K-1). + + High co-activation relative to the chance baseline (K/N) indicates that + certain expert pairs are systematically selected together — a sign of + redundancy rather than specialization. + + Returns a dict: tower -> { + "layer_indices": list[int] — actual model layer positions + "max_coact": Tensor[num_moe_layers] — worst pair per layer + "mean_coact": Tensor[num_moe_layers] — average over all pairs + "chance_baseline": float — K/N, same for all layers (reference) + } + """ + with torch.no_grad(): + device_mesh = _get_device_mesh(vfm) + if device_mesh is None: + return {} + + results: dict[str, dict] = {} + for tower in ["und", "gen"]: + layer_indices, max_coacts, mean_coacts, chance_baselines = [], [], [], [] + + num_layers = len(vfm.language_model.model.layers) + for layer_idx in range(num_layers): + layer = vfm.language_model.model.layers[layer_idx] + mlp = layer.mlp if tower == "und" else getattr(layer, "mlp_moe_gen", None) + if not isinstance(mlp, Qwen3VLMoeTextSparseMoeBlock): + continue + + coact_counts = _allreduce_dtensor(mlp.get_coactivation_counts(reset=True), device_mesh) # [N, N] + + n = mlp.num_experts + k = mlp.top_k + + # Derive per-expert token counts directly from the co-activation + # matrix so we don't depend on ExpertHeatmap's reset cycle. + # Each token that routes to expert i contributes (K-1) entries + # across row i and column i of the upper-triangle matrix. + tokens_per_expert = (coact_counts.sum(dim=1) + coact_counts.sum(dim=0)).float() / (k - 1) + + mask = torch.triu(torch.ones(n, n, dtype=torch.bool, device=coact_counts.device), diagonal=1) + # CoAct(i, j) = N_{i,j} / N_i — normalise by how often expert i fires overall. + denom = tokens_per_expert.unsqueeze(1).clamp(min=1) # [N, 1] + coact_rates = (coact_counts.float() / denom)[mask] # [N*(N-1)/2] + + layer_indices.append(layer_idx) + max_coacts.append(coact_rates.max()) + mean_coacts.append(coact_rates.mean()) + # Chance baseline = probability two randomly-chosen top-K slots land on the + # same pair under uniform routing = K/N. Constant across layers and steps, + # logged once per tower as a reference line. + chance_baselines.append(k / n) + + if layer_indices: + results[tower] = { + "layer_indices": layer_indices, + "max_coact": torch.stack(max_coacts), + "mean_coact": torch.stack(mean_coacts), + "chance_baseline": chance_baselines[0], # same value for all layers + } + + return results + + +class MoESpecializationCallback(EveryN): + """ + Logs per-layer MoE specialization metrics to W&B every N training steps. + + What it captures + ---------------- + Whether MoE experts are developing distinct routing identities: + + Expert Co-activation (logged every N steps) + - mean_coact / max_coact per layer: how often expert pairs fire together + relative to the chance_baseline (K/N). Values well above the baseline + suggest the router is selecting a redundant cluster of experts rather + than a diverse set. + + W&B layout + ---------- + moe_specialization/coact_chance_baseline/ — flat reference (K/N) + moe_specialization/max_coact//layer_NNN|mean|max + moe_specialization/mean_coact//layer_NNN|mean|max + + Args: + every_n (int): Logging interval in training steps. + """ + + def __init__(self, every_n: int = 100): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + vfm = model.net + + coact_results = compute_moe_coactivation_metrics(vfm) + + if not (distributed.is_rank0() and wandb.run): + return + + log_dict: dict[str, float] = {} + + for tower, tower_metrics in coact_results.items(): + layer_indices = tower_metrics.pop("layer_indices") + chance_baseline = tower_metrics.pop("chance_baseline") + log_dict[f"moe_specialization/coact_chance_baseline/{tower}"] = chance_baseline + for metric_name, values in tower_metrics.items(): + for layer_idx, val in zip(layer_indices, values): + log_dict[f"moe_specialization/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item() + log_dict[f"moe_specialization/{metric_name}/{tower}/mean"] = values.mean().item() + log_dict[f"moe_specialization/{metric_name}/{tower}/max"] = values.max().item() + + wandb.log(log_dict, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/moe_stability_callback.py b/cosmos3/_src/vfm/callbacks/moe_stability_callback.py new file mode 100644 index 00000000..8b482acf --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/moe_stability_callback.py @@ -0,0 +1,203 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +""" +MoE Stability Callback +====================== +Monitors whether the MoE router is staying healthy over the course of training. +A healthy router distributes tokens reasonably evenly, keeps all experts alive, +and remains uncertain enough (high entropy) that it is still learning to route. + +Three metrics are tracked per layer, per tower (und / gen): + + Dead Expert Rate + ---------------- + Fraction of experts receiving fewer than 10% of their fair-share of tokens + (i.e. load fraction f_i < 0.1 / N). A dead expert has been effectively shut + out by the router — it gets no gradient signal and its capacity is wasted. + Ideal = 0. A rising dead-expert rate in the gen tower during early training + is a common failure mode. + + Load Imbalance Factor (LIF) + --------------------------- + N * max(f_i), where f_i is the fraction of tokens routed to expert i. + Measures how much the busiest expert is overloaded relative to uniform. + LIF = 1.0 is perfect balance; <= 1.3 is healthy; > 3.0 indicates severe + collapse onto a small set of experts. This is the same quantity watched by + the load-balancing loss, but measured empirically rather than from the loss + objective. + + Router Entropy (normalized) + --------------------------- + Mean per-token Shannon entropy of the full routing distribution, divided by + log(N) to put it on a [0, 1] scale. H = 1 means the router is maximally + uncertain (uniform over all experts); H = 0 means it always picks the same + expert. Early in training entropy is high; we want it to stay reasonably + high (> ~0.7) so the router continues to explore. A sudden drop signals + routing collapse. + +Buffer ownership +---------------- + This callback is fully self-contained: it reads and resets its own dedicated + buffers (stability_tokens_per_expert, stability_total_tokens, sum_token_entropy). + It does not depend on ExpertHeatmap's reset cycle. +""" + +import math + +# Fraction of uniform fair-share below which an expert is considered "dead" (e.g. 0.1 → < 10% of K/N). +DEAD_EXPERT_THRESHOLD_MULTIPLIER = 0.1 + +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def compute_moe_stability_metrics(vfm: torch.nn.Module) -> dict[str, dict]: + """ + Compute per-layer MoE stability metrics for both towers. + + Iterates over all model layers, skipping any that do not use + Qwen3VLMoeTextSparseMoeBlock (e.g. dense layers when decoder_sparse_step > 1). + Actual model layer indices are preserved so W&B keys (layer_000, layer_042, ...) + always refer to the correct transformer layer regardless of MoE sparsity pattern. + + Returns a dict: tower -> { + "layer_indices": list[int] — actual model layer positions + "dead_expert_rate": Tensor[num_moe_layers] + "lif": Tensor[num_moe_layers] + "router_entropy_normalized":Tensor[num_moe_layers] + } + """ + with torch.no_grad(): + num_layers = len(vfm.language_model.model.layers) + + example_weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight + device_mesh = example_weight.device_mesh if isinstance(example_weight, DTensor) else None + + if device_mesh is None: + return {} + + def _allreduce(t: torch.Tensor) -> torch.Tensor: + return DTensor.from_local( + t, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + results: dict[str, dict] = {} + for tower in ["und", "gen"]: + layer_indices, dead_rates, lifs, entropies = [], [], [], [] + + for layer_idx in range(num_layers): + layer_module = vfm.language_model.model.layers[layer_idx] + # "und" tower uses layer.mlp; "gen" tower uses layer.mlp_moe_gen. + # Both attributes exist on every layer (set in unified_mot.py), but only + # layers where (layer_idx+1) % decoder_sparse_step == 0 are MoE blocks. + mlp_module = layer_module.mlp if tower == "und" else getattr(layer_module, "mlp_moe_gen", None) + if not isinstance(mlp_module, Qwen3VLMoeTextSparseMoeBlock): + continue + + total_tokens_per_expert = _allreduce(mlp_module.get_stability_tokens_per_expert(reset=True)) + total_tokens = _allreduce(mlp_module.get_stability_total_tokens(reset=True)) + sum_token_entropy = _allreduce(mlp_module.get_sum_token_entropy(reset=True)) + + n = mlp_module.num_experts + total = total_tokens.float().clamp(min=1) + f_i = total_tokens_per_expert.float() / total # [N] load fraction per expert + + k = mlp_module.top_k + + layer_indices.append(layer_idx) + # Uniform fair share per expert is K/N. "Dead" = below 10% of that. + dead_rates.append((f_i < DEAD_EXPERT_THRESHOLD_MULTIPLIER * k / n).float().mean()) + # LIF = max(f_i) * N / K. Interpretation: + # 1.0 = perfectly balanced (every expert gets its fair share) + # 2.0 = busiest expert handles 2x its fair share + # >3.0 = severe imbalance, consider tuning load-balancing loss + lifs.append(f_i.max() * n / k) + # Mean per-token entropy, normalized to [0, 1] by log(N). + # squeeze() collapses the [1] buffer shape to a 0-d scalar. + entropies.append((sum_token_entropy.float() / total / math.log(n)).squeeze()) + + if layer_indices: + results[tower] = { + "layer_indices": layer_indices, + "dead_expert_rate": torch.stack(dead_rates), + "lif": torch.stack(lifs), + "router_entropy_normalized": torch.stack(entropies), + } + + return results + + +class MoEStabilityCallback(EveryN): + """ + Logs per-layer MoE stability metrics to W&B every N training steps. + + What it captures + ---------------- + Whether the MoE router remains in a healthy, balanced state over training. + The three metrics collectively answer: are all experts still being used + (dead_expert_rate), is load spread evenly (lif), and is the router still + making uncertain, exploratory decisions (router_entropy_normalized)? + + W&B layout + ---------- + For each metric and each tower, two kinds of series are logged: + - moe_stability///layer_NNN — per model layer time series + - moe_stability///mean|max — summary across all MoE layers + + Typical healthy ranges: + dead_expert_rate → 0 (any sustained non-zero value is a concern) + lif → <= 1.3 (alarm at > 3.0) + router_entropy_normalized → > 0.7 (collapse if it drops sharply) + + Args: + every_n (int): Logging interval in training steps. + """ + + def __init__(self, every_n: int = 100): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + metrics = compute_moe_stability_metrics(model.net) + + if not (distributed.is_rank0() and wandb.run): + return + + log_dict: dict[str, float] = {} + for tower, tower_metrics in metrics.items(): + layer_indices = tower_metrics.pop("layer_indices") + for metric_name, values in tower_metrics.items(): + for layer_idx, val in zip(layer_indices, values): + log_dict[f"moe_stability/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item() + log_dict[f"moe_stability/{metric_name}/{tower}/mean"] = values.mean().item() + log_dict[f"moe_stability/{metric_name}/{tower}/max"] = values.max().item() + + wandb.log(log_dict, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/norm_monitor.py b/cosmos3/_src/vfm/callbacks/norm_monitor.py new file mode 100644 index 00000000..429496ef --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/norm_monitor.py @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Optional, Union + +import torch +import torch.distributed as dist +import wandb +from torch import nn +from torch.distributed.tensor import DTensor + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log, misc +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.distributed import DistributedDataParallel +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.datasets.sequence_packing import get_gen_seq + +try: + from apex.contrib.layer_norm import FastLayerNorm +except ImportError: + FastLayerNorm = None + + +class NormMonitor(Callback): + def __init__( + self, + every_n: Optional[int] = None, + step_size: int = 1, + layer_norm_only: bool = False, + model_key: Optional[str] = None, + log_stat_wandb: bool = False, + save_s3: bool = False, + track_activations: bool = False, + ): + """Monitor and log parameter/gradient/activation norms during training. + + Args: + every_n: Log statistics every N global steps. If None, logging is disabled. + step_size: Number of micro-steps per global step (for gradient accumulation). + layer_norm_only: If True, only track LayerNorm and Embedding parameters. + If False, track all parameters. + model_key: Attribute name to access the model (e.g., "diffusion_model"). + If None, use the model directly. + log_stat_wandb: If True, log per-parameter statistics to wandb. + If False, only log aggregate norms. + save_s3: If True, save statistics to S3 bucket. + track_activations: If True, track activation norms + and gradients of activations at each transformer block. If set to False, only + weight norms and weight gradient norms will be tracked. + """ + self.every_n = every_n + self.step_size = step_size + self.model_key = model_key + self.layer_norm_only = layer_norm_only + self.log_stat_wandb = log_stat_wandb + self.save_s3 = save_s3 + self.track_activations = track_activations + self.name = self.__class__.__name__ + + # Storage for activation statistics (populated by hooks) + self._activation_stats: dict[str, dict[str, torch.Tensor]] = {} + self._activation_grad_stats: dict[str, dict[str, torch.Tensor]] = {} + self._hooks: list[torch.utils.hooks.RemovableHandle] = [] + self._should_record = False + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/norm_monitor" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"{self.__class__.__name__} callback: local_dir: {self.local_dir}") + + # Register activation hooks if enabled + if self.track_activations: + self._register_activation_hooks(model) + + def _register_activation_hooks(self, model: ImaginaireModel) -> None: + """Register forward and backward hooks on transformer blocks to capture activation statistics. + + Hooks are registered at the block level (on model.model.layers children) rather than + on individual modules inside blocks. This is compatible with torch.compile since + compile is applied per-block, and hooks on the outer block fire outside the compiled graph. + """ + if self.model_key is not None: + model = getattr(model, self.model_key) + + # Get the transformer layers - hooks are registered on each block + if not hasattr(model.net.language_model.model, "layers"): + log.warning( + f"{self.__class__.__name__}: Could not find model.net.language_model.model.layers. " + "Activation tracking requires model structure with model.net.language_model.model.layers." + ) + return + + layers = model.net.language_model.model.layers + + for layer_id, block in layers.named_children(): + block_name = f"blocks.{layer_id}" + + # Forward hook to capture activation norms (block output) + # Also registers a tensor hook for gradient tracking + def make_forward_hook(name: str): + def forward_hook( + mod: nn.Module, inp: tuple[torch.Tensor, ...], out: torch.Tensor | tuple[torch.Tensor, ...] + ) -> None: + if not self._should_record: + return + # We track activation norms of only generation sequences. + activation = get_gen_seq(out[0]) + + # Certain algorithms do more than one pass through the model. + # (E.g. teacher forcing). We merge stats in that case. + new_stats = self._compute_l2_stats(activation) + existing = self._activation_stats.get(name) + if existing is not None: + existing["sq_sum"] += new_stats["sq_sum"] + existing["max"] = torch.max(existing["max"], new_stats["max"]) + else: + self._activation_stats[name] = new_stats + + # Register tensor hook for gradient tracking. + # This works with activation checkpointing (unlike module backward hooks). + def make_tensor_grad_hook(hook_name: str): + def tensor_grad_hook(grad: torch.Tensor | None) -> None: + # The block may get gradients internally via attention, + # even if the output is unused. + if grad is None: + return + + # If there is more than one pass through the model + # (e.g. teacher forcing), then merge the stats. + new_stats = self._compute_l2_stats(grad) + existing = self._activation_grad_stats.get(hook_name) + if existing is not None: + existing["sq_sum"] += new_stats["sq_sum"] + existing["max"] = torch.max(existing["max"], new_stats["max"]) + else: + self._activation_grad_stats[hook_name] = new_stats + + return tensor_grad_hook + + if activation.requires_grad: + activation.register_hook(make_tensor_grad_hook(name)) + + return forward_hook + + forward_handle = block.register_forward_hook(make_forward_hook(block_name)) + self._hooks.append(forward_handle) + + if distributed.is_rank0(): + num_blocks = len(list(layers.named_children())) + log.info(f"{self.__class__.__name__}: Registered activation hooks on {num_blocks} transformer blocks") + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + """Clean up hooks when training ends.""" + for hook in self._hooks: + hook.remove() + self._hooks.clear() + + def on_before_forward( + self, + iteration: int = 0, + ) -> None: + """Enable activation recording before forward pass if this iteration should be logged.""" + if not self.track_activations: + return + global_step = iteration // self.step_size + should_run = global_step % self.every_n == 0 + self._should_record = should_run + if should_run: + # Clear previous activation stats + self._activation_stats.clear() + self._activation_grad_stats.clear() + + def on_before_optimizer_step( + self, + model_ddp: Union[DistributedDataParallel, ImaginaireModel], + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + global_step = iteration // self.step_size + should_run = global_step % self.every_n == 0 + if not should_run: + return + + if isinstance(model_ddp, DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + if self.model_key is not None: + model = getattr(model, self.model_key) + + self._compute_and_log_stats(model, iteration) + + # Disable recording after logging + self._should_record = False + + def _get_named_parameters(self, model: nn.Module) -> dict[str, nn.Parameter]: + """Get named parameters, optionally filtered to layer norm only.""" + named_parameters = {} + if self.layer_norm_only: + ln_modules = (nn.LayerNorm, nn.Embedding) + if FastLayerNorm is not None: + ln_modules += (FastLayerNorm,) + for mn, m in model.named_modules(): + if isinstance(m, ln_modules): + for pn, p in m.named_parameters(): + fpn = f"{mn}.{pn}" if mn else pn + named_parameters[fpn] = p + else: + named_parameters = dict(model.named_parameters()) + return named_parameters + + def _should_track_param(self, param_name: str) -> bool: + """Check if parameter should be tracked based on naming conventions.""" + # Track only generation tower params, exclude EMA params + return "moe_gen" in param_name and "net_ema" not in param_name + + def _compute_l2_stats(self, tensor: torch.Tensor, detach: bool = True) -> dict[str, torch.Tensor]: + """Compute statistics (squared sum and max) for a tensor. + + Args: + tensor: Input tensor to compute statistics for. + detach: If True, detach the tensor before computing stats. + + Returns: + Dictionary with "sq_sum" (squared sum for L2 norm) and "max" (absolute max). + """ + data = tensor.detach() if detach else tensor + if isinstance(data, DTensor): + data = data.to_local() + + return { + "sq_sum": (data.float() ** 2).sum(), + "max": data.abs().max(), + } + + @misc.timer("norm_monitor") + def _compute_and_log_stats(self, model: nn.Module, iteration: int = 0) -> None: + """FSDP-efficient implementation using local shards + all_reduce. + + Instead of gathering full parameters with summon_full_params (expensive), + we compute local statistics on each rank's shard and use all_reduce to + aggregate them across all ranks. + """ + named_parameters = self._get_named_parameters(model) + + # Accumulators for local shard statistics (squared sum for L2 norm) + local_param_sq_sum = torch.tensor(0.0, device="cuda", dtype=torch.float32) + local_grad_sq_sum = torch.tensor(0.0, device="cuda", dtype=torch.float32) + + # Per-parameter stats: {param_name: [local_sq_sum, local_max]} + per_param_stats: dict[str, dict[str, torch.Tensor]] = {} + per_grad_stats: dict[str, dict[str, torch.Tensor]] = {} + + for param_name, param in named_parameters.items(): + if not self._should_track_param(param_name): + continue + + # Compute local statistics on this rank's shard + per_param_stats[param_name] = self._compute_l2_stats(param) + local_param_sq_sum += per_param_stats[param_name]["sq_sum"] + + if param.grad is not None: + per_grad_stats[param_name] = self._compute_l2_stats(param.grad, detach=False) + local_grad_sq_sum += per_grad_stats[param_name]["sq_sum"] + + # All-reduce to aggregate statistics across all FSDP ranks + dist.all_reduce(local_param_sq_sum, op=dist.ReduceOp.SUM) + dist.all_reduce(local_grad_sq_sum, op=dist.ReduceOp.SUM) + + # All-reduce per-parameter stats + for param_name, stats_dict in per_param_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + for param_name, stats_dict in per_grad_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + # All-reduce activation stats (activations are replicated, so reduce across all ranks for consistency) + for module_name, stats_dict in self._activation_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + for module_name, stats_dict in self._activation_grad_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + # Only rank 0 logs the results + if distributed.is_rank0(): + important_info = { + "trainer/global_step": iteration, + "sample_counter": getattr(self.trainer, "sample_counter", iteration), + "total_param_l2_norm": local_param_sq_sum.sqrt().item(), + } + if local_grad_sq_sum > 0: + important_info["total_grad_l2_norm"] = local_grad_sq_sum.sqrt().item() + + stats = {} + for param_name, stats_dict in per_param_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/weight_norm/{param_name}"] = l2_norm.item() + stats[f"stats/weight_max/{param_name}"] = stats_dict["max"].item() + + for param_name, stats_dict in per_grad_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/grad_norm/{param_name}"] = l2_norm.item() + stats[f"stats/grad_max/{param_name}"] = stats_dict["max"].item() + + # Add activation stats + for module_name, stats_dict in self._activation_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/act_norm/{module_name}"] = l2_norm.item() + stats[f"stats/act_max/{module_name}"] = stats_dict["max"].item() + + for module_name, stats_dict in self._activation_grad_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/act_grad_norm/{module_name}"] = l2_norm.item() + stats[f"stats/act_grad_max/{module_name}"] = stats_dict["max"].item() + + if wandb.run is not None: + if self.log_stat_wandb: + wandb.log({**stats, **important_info}, step=iteration) + else: + wandb.log(important_info, step=iteration) + + if self.save_s3: + easy_io.dump({**stats, **important_info}, f"s3://rundir/{self.name}/stats_{iteration:09d}.pt") diff --git a/cosmos3/_src/vfm/callbacks/ofu.py b/cosmos3/_src/vfm/callbacks/ofu.py new file mode 100644 index 00000000..f18c9127 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/ofu.py @@ -0,0 +1,293 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OFU (Operational FLOPs Utilization) callback for OmniMoT training. + +Computes and logs OFU metrics by launching ``nvidia-smi dmon`` as a background +subprocess and parsing the Tensor Core activity (mmaact) and processor clock +(pclk) columns. OFU is defined as:: + + OFU = mmaact * (pclk / max_pclk) + +where ``max_pclk`` is the max boost clock for the detected hardware (e.g. +1980 MHz for H100, 2062 MHz for GB200). The result is in the 0-100 range. +""" + +from __future__ import annotations + +import subprocess +import threading +from collections import defaultdict +from dataclasses import dataclass + +import torch +import wandb + +from cosmos3._src.imaginaire.attention.utils import is_blackwell_dc +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.distributed import is_rank0, rank0_only + + +@dataclass +class HardwareTarget: + """Hardware-specific constants for OFU normalisation. + + Attributes: + name: Human-readable name (used as W&B tag, e.g. "H100"). + max_pclk_mhz: Max boost SM clock in MHz used to normalise OFU. + """ + + name: str + max_pclk_mhz: float + + +# Pre-defined hardware targets +H100 = HardwareTarget(name="H100", max_pclk_mhz=1980.0) +GB200 = HardwareTarget(name="GB200", max_pclk_mhz=2062.0) + + +class OFUCallback(EveryN): + """Callback that computes and logs Operational FLOPs Utilization (OFU) to W&B. + + OFU = mmaact * (pclk / max_pclk), where mmaact is the MMA activity + percentage and pclk is the current processor clock from ``nvidia-smi dmon``. + ``max_pclk`` is determined from the detected hardware (H100 or GB200). + The result is in the 0-100 range. + + The callback launches ``nvidia-smi dmon`` as a background subprocess on + ``on_train_start`` and a daemon thread continuously reads its output. + At every logging interval, accumulated samples are consumed, averaged per GPU + and overall, and logged to W&B under ``ofu/{hardware_name}``. + + Args: + hit_thres: Number of warm-up training iterations to skip before logging. + """ + + def __init__( + self, + *args, + hit_thres: int = 5, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.hardware_target = GB200 if is_blackwell_dc() else H100 + self.hit_thres = hit_thres + + # Subprocess state + self._process: subprocess.Popen | None = None + self._reader_thread: threading.Thread | None = None + self._stop_event = threading.Event() + + # Buffered samples protected by a lock: list of (gpu_idx, mmaact, pclk) + self._lock = threading.Lock() + self._samples: list[tuple[int, float, float]] = [] + + # Column indices parsed from the header (set by _reader_loop) + self._col_gpu: int | None = None + self._col_mmaact: int | None = None + self._col_pclk: int | None = None + + # Warm-up counter + self._hit_counter: int = 0 + + # ------------------------------------------------------------------ # + # Background reader + # ------------------------------------------------------------------ # + + def _parse_header(self, line: str) -> bool: + """Parse a dmon header line to locate column indices. + + Called on every ``#`` line because nvidia-smi dmon reprints the header + every few seconds. Returns True if ``gpu``, ``mmaact``, and ``pclk`` + columns are all found; warns only when the column-names line (identified + by the presence of ``gpu``) lacks a required column. Silently ignores + the units line (``# Idx W C ...``) which does not contain ``gpu``. + """ + cols = line.lstrip("#").strip().split() + col_map = {name.lower(): idx for idx, name in enumerate(cols)} + gpu_idx = col_map.get("gpu") + mmaact_idx = col_map.get("mmaact") + pclk_idx = col_map.get("pclk") + if gpu_idx is not None and mmaact_idx is not None and pclk_idx is not None: + if self._col_mmaact is None: + log.info(f"OFUCallback: found mmaact at column {mmaact_idx}, pclk at column {pclk_idx}") + self._col_gpu = gpu_idx + self._col_mmaact = mmaact_idx + self._col_pclk = pclk_idx + return True + if gpu_idx is not None: + missing = [name for name, idx in [("mmaact", mmaact_idx), ("pclk", pclk_idx)] if idx is None] + log.warning( + f"OFUCallback: column(s) {missing} not found in nvidia-smi dmon header: {cols}. " + "OFU metrics will not be available." + ) + return False + + def _reader_loop(self) -> None: + """Background thread that reads nvidia-smi dmon output line-by-line.""" + assert self._process is not None and self._process.stdout is not None + + for line in self._process.stdout: + if self._stop_event.is_set(): + break + line = line.strip() + if not line: + continue + + # Header lines repeat every few seconds — always re-parse so that a + # missed or failed first parse is recovered on the next occurrence. + if line.startswith("#"): + self._parse_header(line) + continue + + # Skip data lines until we have column indices + if self._col_gpu is None or self._col_mmaact is None or self._col_pclk is None: + continue + + parts = line.split() + try: + gpu_idx = int(parts[self._col_gpu]) + mmaact = float(parts[self._col_mmaact]) + pclk = float(parts[self._col_pclk]) + except (ValueError, IndexError): + continue + + with self._lock: + self._samples.append((gpu_idx, mmaact, pclk)) + + # ------------------------------------------------------------------ # + # Lifecycle hooks + # ------------------------------------------------------------------ # + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + if not is_rank0(): + return + + try: + # --gpm-metrics 5 means that we access Tensor Activity under mmaact column. + # -d 5 means that we sample the data every 5 seconds. + cmd = ["nvidia-smi", "dmon", "--gpm-metrics", "5", "-d", "5"] + self._process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, # line-buffered + ) + self._reader_thread = threading.Thread(target=self._reader_loop, daemon=True) + self._reader_thread.start() + log.info(f"OFUCallback: launched nvidia-smi dmon --gpm-metrics 5") + except FileNotFoundError: + log.warning("OFUCallback: nvidia-smi not found, OFU metrics will not be available") + except Exception as e: + log.warning(f"OFUCallback: failed to launch nvidia-smi dmon: {e}") + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + if not is_rank0(): + return + self._stop_event.set() + if self._process is not None: + try: + self._process.terminate() + self._process.wait(timeout=5) + except ProcessLookupError: + pass # already exited + except subprocess.TimeoutExpired: + self._process.kill() + self._process = None + if self._reader_thread is not None: + self._reader_thread.join(timeout=5) + self._reader_thread = None + + # ------------------------------------------------------------------ # + # Per-step gating + # ------------------------------------------------------------------ # + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # All ranks must enter super().on_training_step_end() so they reach the + # distributed barrier inside EveryN. Only rank 0 has samples to clear. + if self._hit_counter < self.hit_thres: + self._hit_counter += 1 + if self._hit_counter == self.hit_thres: + # Discard samples collected during warm-up (compilation, allocation, etc.) + with self._lock: + self._samples.clear() + return + # Delegate to EveryN for the periodic reporting logic + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + # ------------------------------------------------------------------ # + # Periodic reporting + # ------------------------------------------------------------------ # + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if self._process is None: + return + + # Drain buffered samples + with self._lock: + samples = list(self._samples) + self._samples.clear() + + if not samples: + log.warning( + f"OFUCallback: no nvidia-smi samples collected at iteration {iteration}. " + "Check that the dmon subprocess launched and that the mmaact column is present." + ) + return + + # Compute per-GPU OFU: mmaact * (pclk / max_pclk) + max_pclk = self.hardware_target.max_pclk_mhz + gpu_ofu: dict[int, list[float]] = defaultdict(list) + gpu_mmaact: dict[int, list[float]] = defaultdict(list) + gpu_pclk: dict[int, list[float]] = defaultdict(list) + for gpu_idx, mmaact, pclk in samples: + gpu_ofu[gpu_idx].append(mmaact * (pclk / max_pclk)) + gpu_mmaact[gpu_idx].append(mmaact) + gpu_pclk[gpu_idx].append(pclk) + + # Overall averages across all GPUs and samples + all_ofu = [v for vals in gpu_ofu.values() for v in vals] + all_mmaact = [v for vals in gpu_mmaact.values() for v in vals] + all_pclk = [v for vals in gpu_pclk.values() for v in vals] + + log_info: dict[str, float] = { + f"ofu/{self.hardware_target.name}": sum(all_ofu) / len(all_ofu), + "ofu/mmaact": sum(all_mmaact) / len(all_mmaact), + "ofu/avg_pclk_mhz": sum(all_pclk) / len(all_pclk), + "ofu/num_samples": float(len(samples)), + } + + if wandb.run is not None: + wandb.log(log_info, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/param_count.py b/cosmos3/_src/vfm/callbacks/param_count.py new file mode 100644 index 00000000..cc4a1263 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/param_count.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +import torch.nn as nn + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.count_params import count_params +from cosmos3._src.imaginaire.utils.distributed import rank0_only +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +class ParamCount(Callback): + def __init__( + self, + save_s3: bool = False, + ): + self.save_s3 = save_s3 + self.name = self.__class__.__name__ + + @rank0_only + def on_train_start(self, model: Union[ImaginaireModel, list[nn.Module]], iteration: int = 0) -> None: + if isinstance(model, list): + num_param = sum([count_params(m) for m in model]) + else: + num_param = count_params(model) + + log.info(f"Total number of parameters on current rank: {num_param}", rank0_only=False) + info = { + "num_parameters": num_param, + } + + if self.save_s3: + rank = distributed.get_rank() + easy_io.dump(info, f"s3://rundir/{self.name}_{rank}.yaml") diff --git a/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py b/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py new file mode 100644 index 00000000..f61ecdec --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import wandb + +import cosmos3._src.vfm.datasets.sequence_packing as sequence_packing +from cosmos3._src.imaginaire.callbacks.every_n import EveryN +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer + + +class SequencePackingPadding(EveryN): + """ + Callback that saves lengths to which und and gen sequences are padded. This information will be used + to compute FLOPs done during training. + + Args: + every_n (int): Frequency with which callback is run during training. + """ + + def __init__(self, every_n: int = 500): + super().__init__(every_n=every_n, step_size=1, barrier_after_run=False, run_at_start=True) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if wandb.run: + log_dict = { + "SequencePackingPadding/max_causal_len_image_batch": sequence_packing.MAX_CAUSAL_LEN_IMAGE_BATCH, + "SequencePackingPadding/max_full_len_image_batch": sequence_packing.MAX_FULL_LEN_IMAGE_BATCH, + "SequencePackingPadding/max_causal_len_video_batch": sequence_packing.MAX_CAUSAL_LEN_VIDEO_BATCH, + "SequencePackingPadding/max_full_len_video_batch": sequence_packing.MAX_FULL_LEN_VIDEO_BATCH, + } + modality = "video" + if "is_image_batch" in output_batch: + modality = "image" if output_batch["is_image_batch"] else "video" + if "und_token_length" in output_batch: + log_dict[f"SequencePackingPadding/und_token_length_{modality}"] = output_batch["und_token_length"] + if "gen_token_length" in output_batch: + log_dict[f"SequencePackingPadding/gen_token_length_{modality}"] = output_batch["gen_token_length"] + if "action_token_length" in output_batch: + log_dict[f"SequencePackingPadding/action_token_length"] = output_batch["action_token_length"] + if "sound_token_length" in output_batch: + log_dict[f"SequencePackingPadding/sound_token_length"] = output_batch["sound_token_length"] + if "vision_token_length" in output_batch: + log_dict[f"SequencePackingPadding/vision_token_length"] = output_batch["vision_token_length"] + + wandb.log( + log_dict, + step=iteration, + ) diff --git a/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py b/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py new file mode 100644 index 00000000..482282fe --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py @@ -0,0 +1,356 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Optional + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import torch +import torch.distributed as dist +import wandb + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, misc +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +def _get_quantile_bins(n=10) -> np.ndarray: + """Get predefined bins based on logarithmically spaced values""" + points = torch.linspace(0, 1, n + 1) + return points.numpy() + + +@dataclass +class _SigmaLossCache: + """A fixed-size queue for caching sigma and loss tensors. + + Stores sigma/loss pairs on CPU. + When the total number of elements exceeds queue_size, the oldest entries + are automatically removed to maintain the size limit. + + Args: + queue_size: Maximum number of elements to store in the cache. + """ + + def __init__(self, queue_size: int = 2000): + self.queue_size = queue_size + self.reset() + + def reset(self): + self.sigma_list: list[torch.Tensor] = [] + self.loss_list: list[torch.Tensor] = [] + self._total_elements: int = 0 + + def add(self, sigma: torch.Tensor, loss: torch.Tensor): + # Convert to bf16 and store on CPU + sigma_cpu = sigma.detach().cpu().to(torch.bfloat16) + loss_cpu = loss.detach().cpu().to(torch.bfloat16) + + self.sigma_list.append(sigma_cpu) + self.loss_list.append(loss_cpu) + self._total_elements += sigma_cpu.numel() + + # Remove oldest elements if queue exceeds max size + while self._total_elements > self.queue_size and len(self.sigma_list) > 1: + removed_sigma = self.sigma_list.pop(0) + self.loss_list.pop(0) + self._total_elements -= removed_sigma.numel() + + def get_arrays(self) -> tuple[torch.Tensor, torch.Tensor]: + if not self.sigma_list: + return torch.tensor([], dtype=torch.bfloat16), torch.tensor([], dtype=torch.bfloat16) + + sigma_arr = torch.cat(self.sigma_list, dim=0) # [N_total] (concatenated across cached batches) + loss_arr = torch.cat(self.loss_list, dim=0) # [N_total] + + return sigma_arr, loss_arr + + +class SigmaLossAnalysis(Callback): + """Analyze the relationship between sigma (noise level) and flow matching loss. + + This callback tracks per-instance flow matching losses at different sigma values + during training. It maintains separate caches for image and video batches, + periodically aggregates statistics across all distributed ranks, and logs + the results to wandb. + + The analysis helps understand how well the model learns to denoise at different + noise levels, which is useful for diagnosing training dynamics in flow matching + models. + + Args: + every_n: Log statistics every N iterations. + every_n_viz: Create visualization plots every N iterations (must be multiple of every_n). + save_s3: If True, save raw data to S3 for offline analysis. + """ + + def __init__( + self, + every_n: int = 1, + every_n_viz: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.save_s3 = save_s3 + self.every_n = every_n + assert every_n_viz % every_n == 0, "every_n_viz must be a multiple of every_n in sigma_loss_analysis callback" + self.every_n_viz = every_n_viz + self.name = self.__class__.__name__ + + self.image_cache = _SigmaLossCache(queue_size=2000) + self.video_cache = _SigmaLossCache(queue_size=2000) + + def _create_analysis_plots( + self, + sigma_arr: torch.Tensor, + loss_arr: torch.Tensor, # [N] # [N] + ) -> Optional[wandb.Image]: + if len(sigma_arr) == 0: + return None + + # Convert to numpy for plotting + sigma_np = sigma_arr.cpu().float().numpy() + loss_np = loss_arr.cpu().float().numpy() + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) + + # Get predefined bins based on logarithmically spaced values + sigma_bins = _get_quantile_bins(10) + + # y_tick_min, y_tick_max = 0, 1.0 + y_tick_min, y_tick_max = 0, 1.0 + # 2D histogram with exponential sigma bins and fixed [0,1] loss range + loss_bins = np.linspace(y_tick_min, y_tick_max, 20) + + counts, xedges, yedges = np.histogram2d(sigma_np, loss_np, bins=(sigma_bins, loss_bins)) + if counts.max() < 0.1: + return None + + # Plot heatmap with exponential scale colormap + im = ax1.imshow( + counts.T, + origin="lower", + aspect="auto", + extent=[sigma_bins[0], sigma_bins[-1], y_tick_min, y_tick_max], + norm=matplotlib.colors.LogNorm(vmin=1, vmax=counts.max()), + ) + plt.colorbar(im, ax=ax1) + + # Set fixed loss ticks from 0 to 1 + yticks = np.linspace(y_tick_min, y_tick_max, 6) + ax1.set_yticks(yticks) + ax1.set_yticklabels([f"{y:.1f}" for y in yticks]) + + ax1.set_xlabel("Sigma") + ax1.set_ylabel("Loss") + title = "Sigma vs Loss Distribution" + ax1.set_title(title) + + # Sigma histogram with loss statistics per bin + hist_counts, _ = np.histogram(sigma_np, bins=sigma_bins) + bin_indices = np.digitize(sigma_np, sigma_bins) - 1 + + # Calculate statistics per bin + n_bins = len(sigma_bins) - 1 + means = np.zeros(n_bins) + stds = np.zeros(n_bins) + for i in range(n_bins): + bin_mask = bin_indices == i + if bin_mask.any(): + means[i] = loss_np[bin_mask].mean() + stds[i] = loss_np[bin_mask].std() + else: + means[i] = np.nan + stds[i] = np.nan + + # Plot histogram + bin_centers = (sigma_bins[:-1] + sigma_bins[1:]) / 2 + ax2.bar(bin_centers, hist_counts, width=np.diff(sigma_bins), alpha=0.3, align="center") + + # Plot loss statistics on twin axis + ax2_twin = ax2.twinx() + valid_mask = ~np.isnan(means) + ax2_twin.errorbar( + bin_centers[valid_mask], means[valid_mask], yerr=stds[valid_mask], color="red", fmt="o-", alpha=0.5 + ) + + ax2.set_xlabel("Sigma (Log Scale)") + ax2.set_ylabel("Count") + ax2_twin.set_ylabel("Loss (mean ± std)") + title = "Sigma Distribution with Loss Statistics" + ax2.set_title(title) + + # Add grid for better readability + ax1.grid(True, alpha=0.3) + ax2.grid(True, alpha=0.3) + + # Create log-scale labels + sigma_labels = [f"{val:.1e}" for val in sigma_bins] + ax1.set_xticks(sigma_bins[1:-1]) # Skip boundary bins + ax1.set_xticklabels(sigma_labels[1:-1], rotation=45) + ax1.set_xscale("linear") + ax2.set_xticks(sigma_bins[1:-1]) + ax2.set_xticklabels(sigma_labels[1:-1], rotation=45) + ax2.set_xscale("linear") + + plt.tight_layout() + fig_img = wandb.Image(fig) + plt.close(fig) + + return fig_img + + def _process_stats(self, sigma: torch.Tensor, loss: torch.Tensor) -> dict: + """Calculate summary statistics for sigma and loss distributions. + + Args: + sigma: Tensor of sigma (noise level) values. + loss: Tensor of corresponding loss values. + + Returns: + Dictionary containing: + - sigma_log_mean: Mean of log(sigma). Log-space is used since sigma spans + multiple orders of magnitude, a standard practice on flow matching / EDM models. + - sigma_log_std: Standard deviation of log(sigma). + - loss_mean: Average loss across all samples. + - loss_std: Standard deviation of loss, measuring spread. + - loss_min: Minimum loss value observed. + - loss_max: Maximum loss value observed. + - loss_median: Median (50th percentile) loss, robust to outliers. + - loss_q1: First quartile (25th percentile) of loss. + - loss_q3: Third quartile (75th percentile) of loss. + """ + return { + "sigma_log_mean": float(sigma.log().mean()), + "sigma_log_std": float(sigma.log().std()), + "loss_mean": float(loss.mean()), + "loss_std": float(loss.std()), + "loss_min": float(loss.min()), + "loss_max": float(loss.max()), + "loss_median": float(loss.median()), + "loss_q1": float(torch.quantile(loss.float(), 0.25)), + "loss_q3": float(torch.quantile(loss.float(), 0.75)), + } + + def _gather_and_save(self, cache: _SigmaLossCache, iteration: int, prefix: str, log_viz: bool = True) -> dict: + info = {} + + # Gather data from all ranks + local_sigma, local_loss = cache.get_arrays() + world_size = dist.get_world_size() + + if world_size > 1: + # Gather sizes first + local_size = torch.tensor([len(local_sigma)], dtype=torch.long, device="cuda") # [1] + sizes = [torch.zeros_like(local_size) for _ in range(world_size)] + dist.all_gather(sizes, local_size) + sizes = [s.item() for s in sizes] + + # Gather data + max_size = max(sizes) + if max_size > 0: + # Move to GPU for gathering + padded_sigma = torch.zeros(max_size, dtype=torch.bfloat16, device="cuda") # [max_size] + padded_loss = torch.zeros(max_size, dtype=torch.bfloat16, device="cuda") # [max_size] + + if len(local_sigma) > 0: + padded_sigma[: len(local_sigma)] = local_sigma.cuda() + padded_loss[: len(local_loss)] = local_loss.cuda() + + all_sigma = [torch.zeros_like(padded_sigma) for _ in range(world_size)] + all_loss = [torch.zeros_like(padded_loss) for _ in range(world_size)] + + dist.all_gather(all_sigma, padded_sigma) + dist.all_gather(all_loss, padded_loss) + + if distributed.is_rank0(): + # Combine data from all ranks + valid_sigma = [] + valid_loss = [] + for sigma, loss, size in zip(all_sigma, all_loss, sizes): + if size > 0: + valid_sigma.append(sigma[:size]) + valid_loss.append(loss[:size]) + + if valid_sigma: + sigma_arr = torch.cat(valid_sigma) # [N_total] (across all ranks) + loss_arr = torch.cat(valid_loss) # [N_total] + + # Overall statistics + info[f"{prefix}/total_samples"] = sigma_arr.shape[0] + + # Calculate statistics + stats = self._process_stats(sigma_arr, loss_arr) + info.update({f"{prefix}/{k}": v for k, v in stats.items()}) + + # Create visualization + if log_viz: + fig_img = self._create_analysis_plots(sigma_arr, loss_arr) + print(fig_img) + if fig_img is not None: + info[f"{prefix}/distribution_plot"] = fig_img + + if self.save_s3: + save_data = { + "sigma": sigma_arr.cpu(), + "loss": loss_arr.cpu(), + "stats": {k: v for k, v in info.items() if not isinstance(v, wandb.Image)}, + } + easy_io.dump( + save_data, + f"s3://rundir/{self.name}/{prefix}_Iter{iteration:09d}.pkl", + ) + + cache.reset() + return info + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ): + sigma = output_batch["sigma"] + fm_loss_vision_per_instance = output_batch["flow_matching_loss_vision_per_instance"] + + # sigma is [B] (base), [B,1] (TF), or [B,T_max] (DF); reduce to [B] for logging + assert sigma.ndim <= 2, f"Sigma should be [B] or [B,T_max], got shape {sigma.shape}" + if sigma.ndim == 2: + sigma = sigma.mean(dim=-1) # [B] (reduced from [B,T_max] or [B,1]) + + if model.is_image_batch(data_batch): + self.image_cache.add(sigma, fm_loss_vision_per_instance) + else: + self.video_cache.add(sigma, fm_loss_vision_per_instance) + + if iteration % self.every_n == 0: + info = {} + + with misc.timer("sigma_loss_analysis"): + log_viz = iteration % self.every_n_viz == 0 + # Process image data + if len(self.image_cache.sigma_list) > 0: + info.update(self._gather_and_save(self.image_cache, iteration, "sigma_loss_image", log_viz=log_viz)) + + # Process video data + if len(self.video_cache.sigma_list) > 0: + info.update(self._gather_and_save(self.video_cache, iteration, "sigma_loss_video", log_viz=log_viz)) + + if distributed.is_rank0() and info and wandb.run: + wandb.log(info, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/skip_nan_step.py b/cosmos3/_src/vfm/callbacks/skip_nan_step.py new file mode 100644 index 00000000..799c181f --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/skip_nan_step.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.distributed as dist + +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.callback import Callback + + +class SkipNaNStep(Callback): + """Skip the optimizer step only when ALL ranks produce NaN/Inf loss. + + When only some ranks produce NaN, the existing GradClip callback's + nan_to_num handling is sufficient (NaN gradients become 0, valid + gradients from clean ranks are still used). This callback only + intervenes when every rank has NaN, meaning no useful gradient + signal exists. + + The all-reduce ensures all ranks agree on skip/no-skip, preventing + NCCL desync. + + Args: + max_consecutive_nan: Abort training after this many consecutive + all-rank-NaN optimizer steps. Set to 0 to disable the limit. + """ + + def __init__(self, max_consecutive_nan: int = 100) -> None: + super().__init__() + self.max_consecutive_nan = max_consecutive_nan + self._nan_detected = False + self._consecutive_nan_count = 0 + + def on_before_backward( + self, + model_ddp: distributed.DistributedDataParallel, + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss).any() or torch.isinf(loss).any(): + self._nan_detected = True + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + nan_flag = torch.tensor([1.0 if self._nan_detected else 0.0], device="cuda") + dist.all_reduce(nan_flag, op=dist.ReduceOp.SUM) + nan_rank_count = int(nan_flag.item()) + world_size = dist.get_world_size() + + if nan_rank_count > 0 and nan_rank_count < world_size: + self._consecutive_nan_count = 0 + + elif nan_rank_count == world_size: + if isinstance(model_ddp, distributed.DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + for param in model.parameters(): + if param.grad is not None: + param.grad.zero_() + + self._consecutive_nan_count += 1 + log.warning( + f"ALL ranks NaN/Inf at iteration {iteration}, skipping optimizer step " + f"(consecutive: {self._consecutive_nan_count})", + ) + + if self.max_consecutive_nan > 0 and self._consecutive_nan_count >= self.max_consecutive_nan: + raise RuntimeError( + f"Training unstable: all-rank NaN/Inf loss for {self._consecutive_nan_count} " + f"consecutive optimizer steps at iteration {iteration}. Aborting.", + ) + else: + self._consecutive_nan_count = 0 + + self._nan_detected = False diff --git a/cosmos3/_src/vfm/callbacks/training_stats.py b/cosmos3/_src/vfm/callbacks/training_stats.py new file mode 100644 index 00000000..ba54b861 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/training_stats.py @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.distributed as dist +import wandb + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.vfm.callbacks.wandb_log import _LossRecord +from cosmos3._src.vfm.datasets.action.domain_utils import EMBODIMENT_TO_DOMAIN_ID + +# Build inverse mapping: domain_id -> embodiment_type. First occurrence wins when multiple embodiment names share the +# same domain id. +DOMAIN_ID_TO_EMBODIMENT: dict[int, str] = {} +for _k, _v in EMBODIMENT_TO_DOMAIN_ID.items(): + DOMAIN_ID_TO_EMBODIMENT.setdefault(_v, _k) + + +class TrainingStatsCallback(Callback): + """Callback for tracking and logging training mode and embodiment statistics to wandb.""" + + def __init__(self, log_freq: int = 100): + super().__init__() + self.log_freq = log_freq + self._mode_counts: dict[str, int] = {} + self._mode_total_count: int = 0 + self._embodiment_counts: dict[str, int] = {} + self._embodiment_total_count: int = 0 + self._per_embodiment_loss: dict[str, _LossRecord] = {} + self._per_embodiment_sub_loss: dict[str, dict[str, _LossRecord]] = {} + + def _accumulate_mode_counts(self, data_batch: dict[str, torch.Tensor]) -> None: + modes = data_batch.get("mode", None) + if modes is None: + return + + if isinstance(modes, str): + modes_list = [modes] + elif isinstance(modes, (list, tuple)): + modes_list = [str(m) for m in modes] + elif isinstance(modes, torch.Tensor): + # Defensive: support cases where mode might be encoded numerically. + modes_list = [str(m) for m in modes.detach().cpu().tolist()] + else: + modes_list = [str(modes)] + + for mode in modes_list: + self._mode_total_count += 1 + self._mode_counts[mode] = self._mode_counts.get(mode, 0) + 1 + + def _accumulate_embodiment_counts(self, data_batch: dict[str, torch.Tensor]) -> None: + domain_ids = data_batch.get("domain_id", None) + if domain_ids is None: + return + + if isinstance(domain_ids, int): + domain_id_list = [domain_ids] + elif isinstance(domain_ids, (list, tuple)): + domain_id_list = [int(d) for d in domain_ids if d is not None] + elif isinstance(domain_ids, torch.Tensor): + # Flatten to handle any shape (scalar, 1D, or 2D with trailing dim) + domain_id_list = [int(d) for d in domain_ids.detach().cpu().flatten().tolist()] + else: + domain_id_list = [int(domain_ids)] + + for domain_id in domain_id_list: + embodiment = DOMAIN_ID_TO_EMBODIMENT.get(domain_id, f"unknown_{domain_id}") + self._embodiment_total_count += 1 + self._embodiment_counts[embodiment] = self._embodiment_counts.get(embodiment, 0) + 1 + + def _gather_global_mode_counts(self) -> tuple[int, dict[str, int]]: + """ + Returns (global_total, global_mode_counts) aggregated across all ranks. + """ + local: dict[str, int] = dict(self._mode_counts) + local["__total__"] = int(self._mode_total_count) + + if dist.is_available() and dist.is_initialized(): + world_size = int(dist.get_world_size()) + gathered: list[dict[str, int] | None] = [None for _ in range(world_size)] + dist.all_gather_object(gathered, local) + else: + gathered = [local] + + global_total = 0 + global_counts: dict[str, int] = {} + for item in gathered: + if not item: + continue + global_total += int(item.get("__total__", 0)) + for k, v in item.items(): + if k == "__total__": + continue + global_counts[k] = global_counts.get(k, 0) + int(v) + return global_total, global_counts + + def _gather_global_embodiment_counts(self) -> tuple[int, dict[str, int]]: + """ + Returns (global_total, global_embodiment_counts) aggregated across all ranks. + """ + local: dict[str, int] = dict(self._embodiment_counts) + local["__total__"] = int(self._embodiment_total_count) + + if dist.is_available() and dist.is_initialized(): + world_size = int(dist.get_world_size()) + gathered: list[dict[str, int] | None] = [None for _ in range(world_size)] + dist.all_gather_object(gathered, local) + else: + gathered = [local] + + global_total = 0 + global_counts: dict[str, int] = {} + for item in gathered: + if not item: + continue + global_total += int(item.get("__total__", 0)) + for k, v in item.items(): + if k == "__total__": + continue + global_counts[k] = global_counts.get(k, 0) + int(v) + return global_total, global_counts + + def _build_mode_log_dict( + self, *, log_prefix: str, global_total: int, global_counts: dict[str, int] + ) -> dict[str, float]: + info: dict[str, float] = {} + + denom = float(global_total) if global_total > 0 else 0.0 + for mode in sorted(global_counts.keys()): + count = float(global_counts.get(mode, 0)) + pct = (100.0 * count / denom) if denom > 0 else 0.0 + info[f"{log_prefix}_stats_mode/{mode}"] = pct + + return info + + def _build_embodiment_log_dict( + self, *, log_prefix: str, global_total: int, global_counts: dict[str, int] + ) -> dict[str, float]: + info: dict[str, float] = {} + + denom = float(global_total) if global_total > 0 else 0.0 + for embodiment in sorted(global_counts.keys()): + count = float(global_counts.get(embodiment, 0)) + pct = (100.0 * count / denom) if denom > 0 else 0.0 + info[f"{log_prefix}_stats_embodiment/{embodiment}"] = pct + + return info + + def _get_batch_embodiment(self, data_batch: dict[str, torch.Tensor]) -> str | None: + """Extract the embodiment name from the first non-None sample's domain_id.""" + domain_ids = data_batch.get("domain_id", None) + if domain_ids is None: + return None + if isinstance(domain_ids, torch.Tensor): + if domain_ids.numel() == 0: + return None + domain_id = int(domain_ids.flatten()[0].item()) + elif isinstance(domain_ids, (list, tuple)): + first = next((d for d in domain_ids if d is not None), None) + if first is None: + return None + domain_id = int(first) + else: + domain_id = int(domain_ids) + return DOMAIN_ID_TO_EMBODIMENT.get(domain_id, f"unknown_{domain_id}") + + def _accumulate_per_embodiment_loss( + self, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + ) -> None: + embodiment = self._get_batch_embodiment(data_batch) + if embodiment is None: + return + + if embodiment not in self._per_embodiment_loss: + self._per_embodiment_loss[embodiment] = _LossRecord() + self._per_embodiment_loss[embodiment].loss += loss.detach().float() + self._per_embodiment_loss[embodiment].iter_count += 1 + + if embodiment not in self._per_embodiment_sub_loss: + self._per_embodiment_sub_loss[embodiment] = {} + for key in output_batch: + if "loss" in key and "per_instance" not in key: + if key not in self._per_embodiment_sub_loss[embodiment]: + self._per_embodiment_sub_loss[embodiment][key] = _LossRecord() + self._per_embodiment_sub_loss[embodiment][key].loss += output_batch[key].detach().float() + self._per_embodiment_sub_loss[embodiment][key].iter_count += 1 + + def _compute_per_embodiment_loss_stats(self, log_prefix: str) -> dict[str, float]: + """Compute per-embodiment loss averages across all ranks. + + All ranks must call this method (contains collective operations). + Returns the log dict (only meaningful on rank 0). + """ + dist_available = dist.is_available() and dist.is_initialized() + world_size = int(dist.get_world_size()) if dist_available else 1 + + # Step 1: gather union of embodiment names across ranks + local_embodiments = sorted(self._per_embodiment_loss.keys()) + if dist_available: + all_embodiments: list[list[str] | None] = [None for _ in range(world_size)] + dist.all_gather_object(all_embodiments, local_embodiments) + else: + all_embodiments = [local_embodiments] + union_embodiments = sorted({e for el in all_embodiments for e in el}) + + # Step 2: gather union of sub-loss keys across ranks + local_sub_keys = sorted({k for d in self._per_embodiment_sub_loss.values() for k in d}) + if dist_available: + all_sub_keys: list[list[str] | None] = [None for _ in range(world_size)] + dist.all_gather_object(all_sub_keys, local_sub_keys) + else: + all_sub_keys = [local_sub_keys] + union_sub_keys = sorted({k for kl in all_sub_keys for k in kl}) + + # Step 3: insert NaN dummy _LossRecord for missing embodiment/key combos + for emb in union_embodiments: + if emb not in self._per_embodiment_loss: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") + dummy.iter_count += 1 + self._per_embodiment_loss[emb] = dummy + if emb not in self._per_embodiment_sub_loss: + self._per_embodiment_sub_loss[emb] = {} + for key in union_sub_keys: + if key not in self._per_embodiment_sub_loss[emb]: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") + dummy.iter_count += 1 + self._per_embodiment_sub_loss[emb][key] = dummy + + # Step 4: compute distributed averages (all ranks participate in all_reduce) + log_dict: dict[str, float] = {} + for emb in union_embodiments: + avg, valid = self._per_embodiment_loss[emb].get_stat(return_valid_mask_sum=True) + if valid > 0: + log_dict[f"{log_prefix}_stats_loss/{emb}"] = avg + + for emb in union_embodiments: + for key in union_sub_keys: + avg, valid = self._per_embodiment_sub_loss[emb][key].get_stat(return_valid_mask_sum=True) + if valid > 0: + log_dict[f"{log_prefix}_stats_loss_detail/{emb}_{key}"] = avg + + # Step 5: reset accumulators + self._per_embodiment_loss = {} + self._per_embodiment_sub_loss = {} + + return log_dict + + @torch.no_grad() + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self._accumulate_mode_counts(data_batch) + self._accumulate_embodiment_counts(data_batch) + self._accumulate_per_embodiment_loss(data_batch, output_batch, loss) + + if iteration % self.log_freq != 0: + return + + # All ranks must participate in collective operations below. + mode_total, mode_counts = self._gather_global_mode_counts() + embodiment_total, embodiment_counts = self._gather_global_embodiment_counts() + per_embodiment_loss_dict = self._compute_per_embodiment_loss_stats(log_prefix="train") + + if not distributed.is_rank0(): + return + + if wandb.run is None: + return + + log_dict: dict[str, float] = {} + log_dict.update( + self._build_mode_log_dict(log_prefix="train", global_total=mode_total, global_counts=mode_counts) + ) + log_dict.update( + self._build_embodiment_log_dict( + log_prefix="train", global_total=embodiment_total, global_counts=embodiment_counts + ) + ) + log_dict.update(per_embodiment_loss_dict) + + wandb.log({k: float(v) for k, v in log_dict.items()}, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py b/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py new file mode 100644 index 00000000..1220eeb8 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import List, Tuple, Union + +import torch +import wandb + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.vfm.utils.vlm.optimizer import OptimizersContainer +from projects.cosmos3.vlm.utils.distributed import gradient_norm_clipping + + +@torch.jit.script +def _fused_nan_to_num(params: List[torch.Tensor]): + # Note this will raise error: + # for param in params: + # torch.nan_to_num(param, nan=0.0, posinf=0.0, neginf=0.0, out=param) ~~~~~~~~~~~~~~~~ <--- HERE + # RuntimeError: nan_to_num(): functions with out=... arguments don't support automatic differentiation, but one of the arguments requires grad. + for param in params: + torch.nan_to_num(param, nan=0.0, posinf=0.0, neginf=0.0, out=param) + + +@dataclass +class _MagnitudeRecord: + state: float = 0 + iter_count: int = 0 + + def reset(self) -> None: + self.state = 0 + self.iter_count = 0 + + def update(self, cur_state: torch.Tensor) -> None: + self.state += cur_state + self.iter_count += 1 + + def get_stat(self) -> Tuple[float, float]: + if self.iter_count > 0: + avg_state = self.state / self.iter_count + avg_state = avg_state.item() + else: + avg_state = 0 + self.reset() + return avg_state + + +class GradClip(Callback): + def __init__(self, clip_norm=1.0, force_finite: bool = False): + self.clip_norm = clip_norm + self.force_finite = force_finite + # Single global-norm tracker. Earlier versions partitioned by DeviceMesh + # and clipped each bucket independently — that changed the relative + # rescale between dense and EP-split MoE params, distorting the update + # direction. gradient_norm_clipping() now combines per-mesh norms into + # one scalar and applies a uniform rescale across every bucket. + self._global_norm_log = _MagnitudeRecord() + + def on_before_optimizer_step( + self, + model_ddp: Union[torch.nn.Module, list[torch.nn.Module]], + optimizer: OptimizersContainer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + del scheduler + model_parts = model_ddp + if not isinstance(model_parts, list): + model_parts = [model_parts] + + all_params: List[torch.Tensor] = [] + for model_part in model_parts: + for param in model_part.parameters(): + if param.grad is not None: + all_params.append(param) + + if self.force_finite: + _fused_nan_to_num([p.grad for p in all_params]) + + total_norm = gradient_norm_clipping( + all_params, + self.clip_norm, + foreach=True, + ) + self._global_norm_log.update(total_norm) + + if iteration % self.config.trainer.logging_iter == 0 and wandb.run: + avg_norm = self._global_norm_log.get_stat() + log.info(f"clip_grad_norm/global: {avg_norm}") + wandb.log({"iteration": iteration, "clip_grad_norm/global": avg_norm}, step=iteration) + + if (iteration == 1 or iteration % (self.config.trainer.logging_iter * 10) == 0) and wandb.run: + # Log learning rate + unique_lr = {} + for optim_per_model, model_part_name in zip(optimizer.optimizers, optimizer.model_part_names): + for param_group in optim_per_model[0].param_groups: + unique_lr[f"optim/lr_{model_part_name}"] = param_group["lr"] + + log.info(f"learning_rate: {unique_lr}") + wandb.log(unique_lr, step=iteration) diff --git a/cosmos3/_src/vfm/callbacks/wandb_log.py b/cosmos3/_src/vfm/callbacks/wandb_log.py new file mode 100644 index 00000000..1e6f62a7 --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/wandb_log.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Copied from projects/cosmos/reason1/callbacks/wandb_log.py remove loss_per_token related code +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +@dataclass +class _LossRecord: + loss: torch.Tensor | float = 0 + iter_count: int = 0 + name: str | None = None + + def reset(self) -> None: + self.loss = 0 + self.iter_count = 0 + + def get_stat(self, return_valid_mask_sum: bool = False) -> Tuple[float, float]: + if self.iter_count == 0: + self.loss = torch.tensor([float("nan")], device="cuda") + self.iter_count = 1 + self.loss = self.loss.mean() + msg_str = f"{self.name}: sum_loss={self.loss.item()}/iter_count={self.iter_count}=" + avg_loss_tensor = self.loss / self.iter_count + # Create a mask (1 if valid, 0 if NaN or Inf) + valid_mask = torch.tensor([torch.isfinite(avg_loss_tensor).float()], device="cuda") + msg_str += f"avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}, " + + # Replace NaN/Inf with 0 to avoid affecting sum + avg_loss_tensor = torch.where( + torch.isfinite(avg_loss_tensor), avg_loss_tensor, torch.tensor([0.0], device="cuda") + ) + + # Reduce across all ranks + dist.all_reduce(avg_loss_tensor, op=dist.ReduceOp.SUM) # Sum of valid losses + dist.all_reduce(valid_mask, op=dist.ReduceOp.SUM) # Count of valid losses + msg_str += f" | all_reduce: avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}" + # Compute final average, avoiding division by zero + if valid_mask.item() > 0: + final_avg_loss = (avg_loss_tensor / valid_mask).item() + valid_mask_sum = valid_mask.item() + else: + final_avg_loss = 0.0 # Default to zero if all values were invalid + valid_mask_sum = 0 + + avg_loss = final_avg_loss + msg_str += f" | final: avg_loss={final_avg_loss}" + if self.name is not None: + log.debug(msg_str, rank0_only=False) + self.reset() + if return_valid_mask_sum: + return avg_loss, valid_mask_sum + else: + return avg_loss + + +class WandbCallback(Callback): + def __init__( + self, + logging_iter_multipler: int = 1, + save_logging_iter_multipler: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.final_loss_log = _LossRecord() + self.final_loss_log_per_dataset = {} + self.final_all_loss_log = {} + self.logging_iter_multipler = logging_iter_multipler + self.save_logging_iter_multipler = save_logging_iter_multipler + assert self.logging_iter_multipler > 0, "logging_iter_multipler should be greater than 0" + self.save_s3 = save_s3 + self.wandb_extra_tag = f"@{logging_iter_multipler}" if logging_iter_multipler > 1 else "" + self.name = "wandb_loss_log" + self.wandb_extra_tag + self.unstable_count = torch.zeros(1, device="cuda") + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss) or torch.isinf(loss): + log.critical( + f"Unstable loss {loss} at iteration {iteration}", + rank0_only=False, + ) + self.unstable_count += 1 + + # All elements within a batch have the same dataset name (image or video) + dataset_name = data_batch.get("dataset_name", "default")[0] + + if dataset_name not in self.final_loss_log_per_dataset: + self.final_loss_log_per_dataset[dataset_name] = _LossRecord() + self.final_loss_log_per_dataset[dataset_name].name = dataset_name + self.final_loss_log_per_dataset[dataset_name].loss += loss.detach().float() + self.final_loss_log_per_dataset[dataset_name].iter_count += 1 + + self.final_loss_log.loss += loss.detach().float() + self.final_loss_log.iter_count += 1 + + for key in output_batch.keys(): + # Curve can be plotted only on aggregated loss, not per-instance loss + if "loss" in key and "per_instance" not in key: + if key not in self.final_all_loss_log: + self.final_all_loss_log[key] = _LossRecord() + self.final_all_loss_log[key].loss += output_batch[key].detach().float() + self.final_all_loss_log[key].iter_count += 1 + + if iteration % (self.config.trainer.logging_iter * self.logging_iter_multipler) == 0: + avg_final_loss = self.final_loss_log.get_stat() + + avg_final_all_loss = {} + for key in self.final_all_loss_log.keys(): + avg_final_all_loss[key] = self.final_all_loss_log[key].get_stat() + + # Step 1: Gather all dataset names across ranks + local_dataset_names = list(self.final_loss_log_per_dataset.keys()) + all_dataset_names = [None for _ in range(dist.get_world_size())] + dist.all_gather_object(all_dataset_names, local_dataset_names) + + # Step 2: Create the union of all dataset names + union_dataset_names = set() + for names in all_dataset_names: + union_dataset_names.update(names) + # Step 3: For any missing dataset name, add dummy _LossRecord with NaN loss + union_dataset_names = sorted(list(union_dataset_names)) # This is very important! + for dataset_name in union_dataset_names: + if dataset_name not in self.final_loss_log_per_dataset: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") # Will be masked out + dummy.iter_count += 1 + self.final_loss_log_per_dataset[dataset_name] = dummy + + avg_final_loss_per_dataset = {} + for dataset_name in union_dataset_names: + avg_loss, valid_mask_sum = self.final_loss_log_per_dataset[dataset_name].get_stat( + return_valid_mask_sum=True + ) + if valid_mask_sum > 0: + avg_final_loss_per_dataset[dataset_name] = avg_loss + + dist.all_reduce(self.unstable_count, op=dist.ReduceOp.SUM) + + if distributed.is_rank0() and wandb.run is not None: + info = {} + info.update( + { + f"train{self.wandb_extra_tag}/loss": avg_final_loss, + f"train{self.wandb_extra_tag}/unstable_count": self.unstable_count.item(), + "iteration": iteration, + } + ) + for key, loss in avg_final_all_loss.items(): + info.update( + { + f"train{self.wandb_extra_tag}_detail/{key}": loss, + } + ) + for dataset_name, loss in avg_final_loss_per_dataset.items(): + tag = "" + if "per_seq" in dataset_name: + tag = "_per_seq" + dataset_name = dataset_name.replace("per_seq/", "") + info.update( + { + f"train{self.wandb_extra_tag}_per_data{tag}/{dataset_name}": loss, + } + ) + if self.save_s3: + if ( + iteration + % ( + self.config.trainer.logging_iter + * self.logging_iter_multipler + * self.save_logging_iter_multipler + ) + == 0 + ): + easy_io.dump( + info, + f"s3://rundir/{self.name}/Train_Iter{iteration:09d}.json", + ) + + if wandb: + wandb.log(info, step=iteration) + + # reset unstable count + self.unstable_count.zero_() + self.final_loss_log_per_dataset = {} diff --git a/cosmos3/_src/vfm/callbacks/wandb_log_eval.py b/cosmos3/_src/vfm/callbacks/wandb_log_eval.py new file mode 100644 index 00000000..739b8a7f --- /dev/null +++ b/cosmos3/_src/vfm/callbacks/wandb_log_eval.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.callback import Callback +from cosmos3._src.imaginaire.utils.easy_io import easy_io + + +@dataclass +class _LossRecord: + loss: float = 0 + iter_count: int = 0 + + def reset(self) -> None: + self.loss = 0 + self.iter_count = 0 + + def get_stat(self) -> Tuple[float, float]: + if self.iter_count > 0: + avg_loss_tensor = self.loss / self.iter_count + # Create a mask (1 if valid, 0 if NaN or Inf) + valid_mask = torch.tensor([torch.isfinite(avg_loss_tensor).float()], device="cuda") + + # Replace NaN/Inf with 0 to avoid affecting sum + avg_loss_tensor = torch.where( + torch.isfinite(avg_loss_tensor), avg_loss_tensor, torch.tensor([0.0], device="cuda") + ) + + # Reduce across all ranks + dist.all_reduce(avg_loss_tensor, op=dist.ReduceOp.SUM) # Sum of valid losses + dist.all_reduce(valid_mask, op=dist.ReduceOp.SUM) # Count of valid losses + + # Compute final average, avoiding division by zero + if valid_mask.item() > 0: + final_avg_loss = (avg_loss_tensor / valid_mask).item() + else: + final_avg_loss = 0.0 # Default to zero if all values were invalid + + avg_loss = final_avg_loss + else: + avg_loss = 0 + self.reset() + return avg_loss + + +class WandbCallback(Callback): + def __init__( + self, + save_s3: bool = False, + ) -> None: + super().__init__() + self.final_loss_log = _LossRecord() + self.final_loss_log_per_dataset = {} + + self.save_s3 = save_s3 + self.wandb_extra_tag = "" + self.name = "wandb_loss_val_log" + self.unstable_count = torch.zeros(1, device="cuda") + self.url_key_list = [] + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss) or torch.isinf(loss): + log.critical( + f"Unstable val loss {loss} at iteration {iteration}", + rank0_only=False, + ) + self.unstable_count += 1 + + dataset_name = data_batch.get("dataset_name", "default") + + # Handle case where dataset_name gets batched into a list + if isinstance(dataset_name, list): + + assert len(dataset_name) == 1, "dataset_name should be a list of 1" + dataset_name = dataset_name[0] + + if dataset_name not in self.final_loss_log_per_dataset: + self.final_loss_log_per_dataset[dataset_name] = _LossRecord() + + self.final_loss_log_per_dataset[dataset_name].loss += loss.detach().float() + self.final_loss_log_per_dataset[dataset_name].iter_count += 1 + self.final_loss_log.loss += loss.detach().float() + self.final_loss_log.iter_count += 1 + + self.url_key_list.append(f"{data_batch.get('__url__', [''])[0]}, {data_batch.get('__key__', [''])[0]}") + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + avg_final_loss = self.final_loss_log.get_stat() + + log.info(f"avg_final_loss: {avg_final_loss}") + dist.all_reduce(self.unstable_count, op=dist.ReduceOp.SUM) + # gather url and key list from all ranks + url_key_list = [None] * dist.get_world_size() + dist.all_gather_object(url_key_list, self.url_key_list) + url_key_list = [item for sublist in url_key_list for item in sublist] + + unique_url_key_list = list(set(url_key_list)) + if distributed.is_rank0(): + info = {} + log.info( + f"[val] number of unique url and key: {len(unique_url_key_list)} / {len(url_key_list)}; avg_final_loss: {avg_final_loss}" + ) + info.update( + { + f"val{self.wandb_extra_tag}/loss": avg_final_loss, + f"val{self.wandb_extra_tag}/unstable_count": self.unstable_count.item(), + "iteration": iteration, + f"val{self.wandb_extra_tag}/num_unique_url_key": len(unique_url_key_list), + f"val{self.wandb_extra_tag}/total_url_key": len(url_key_list), + } + ) + if self.save_s3: + easy_io.dump( + info, + f"s3://rundir/{self.name}/Val_Iter{iteration:09d}.json", + ) + + if wandb.run is not None: + wandb.log(info, step=iteration) + + # reset unstable count + self.unstable_count.zero_() + self.url_key_list = [] diff --git a/cosmos3/_src/vfm/checkpointer/__init__.py b/cosmos3/_src/vfm/checkpointer/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/checkpointer/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/checkpointer/dcp.py b/cosmos3/_src/vfm/checkpointer/dcp.py new file mode 100644 index 00000000..4f49ac6b --- /dev/null +++ b/cosmos3/_src/vfm/checkpointer/dcp.py @@ -0,0 +1,861 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Distributed checkpoint (DCP) directory structure and storage backends. + +The checkpointer saves model state in a sharded format across multiple processes: + +self.save_dirname/ +├── iter_000000005/ # Checkpoint at iteration 5 +│ ├── model/ # Model state shards +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ ├── optim/ # Optimizer state shards +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ ├── scheduler/ # Learning rate scheduler state +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ └── trainer/ # Additional training state +│ ├── __0_0.distcp # Shard 0 from rank 0 +│ └── __1_0.distcp # Shard 1 from rank 1 +│ └── dataloader/ # Optional per-rank dataloader state +│ ├── rank_0.pkl +│ └── rank_1.pkl +└── latest_checkpoint.txt # Points to most recent checkpoint folder, e.g. iter_000000005 + +Storage backends: +- Local filesystem: + self.save_dirname = "{config_job.path_local}/checkpoints" + +- S3 object store: + self.save_dirname = "s3://{bucket}/{config_job.path}/checkpoints" + where bucket = self.config_checkpoint.save_to_object_store.bucket + +The sharded format enables efficient distributed saving/loading by: +1. Parallelizing I/O across processes +2. Reducing memory usage per process +3. Supporting both local and cloud storage backends +""" + +import enum +import multiprocessing +import os +import re +import time +from multiprocessing import get_context +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.distributed as dist +import torch.distributed.checkpoint as dcp +from torch import nn +from torch.distributed.checkpoint.filesystem import FileSystemReader, FileSystemWriter +from torch.distributed.checkpoint.metadata import ( + STATE_DICT_TYPE, + Metadata, + StorageMeta, +) +from torch.distributed.checkpoint.state_dict import ( + StateDictOptions, + get_model_state_dict, + get_optimizer_state_dict, + set_model_state_dict, + set_optimizer_state_dict, +) +from torch.distributed.checkpoint.stateful import Stateful + +from cosmos3._src.imaginaire.checkpointer.base import AbstractCheckpointer +from cosmos3._src.imaginaire.checkpointer.s3_filesystem import S3StorageReader, S3StorageWriter +from cosmos3._src.imaginaire.config import CheckpointConfig, JobConfig +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import callback, distributed, log, misc +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.utils.rand_state import get_rand_state_dict, set_rand_state_dict + + +class ModelWrapper(Stateful): + """ + Wrapper for model state dict handling. Strips away the _orig_mod. prefix + among other things from the state dict keys. + """ + + def __init__(self, model: nn.Module) -> None: + self.model = model + + def state_dict(self) -> dict[str, Any]: + return get_model_state_dict(self.model) + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + set_model_state_dict( + self.model, + model_state_dict=state_dict, + options=StateDictOptions(strict=True), + ) + + +class OptimizerWrapper(Stateful): + def __init__( + self, + model: nn.Module, + optim: torch.optim.Optimizer | List[torch.optim.Optimizer] | Stateful, + ) -> None: + self.model = model + self.optim_stateful = ( + optim if isinstance(optim, Stateful) and not isinstance(optim, torch.optim.Optimizer) else None + ) + self.optim = [optim] if isinstance(optim, torch.optim.Optimizer) else optim + + def state_dict(self) -> Dict[str, Any]: + if self.optim_stateful is not None: + return self.optim_stateful.state_dict() + return get_optimizer_state_dict( + model=self.model, + optimizers=self.optim, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + + def load_state_dict(self, state_dict: Dict[str, Any]) -> None: + if self.optim_stateful is not None: + self.optim_stateful.load_state_dict(state_dict) + return + set_optimizer_state_dict( + model=self.model, + optimizers=self.optim, + optim_state_dict=state_dict, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + + +class DataloaderWrapper: + def __init__( + self, + callbacks: Optional[callback.CallBackGroup], + ) -> None: + self.stateful = self._resolve_stateful(callbacks) + + @staticmethod + def _resolve_stateful( + callbacks: Optional[callback.CallBackGroup], + ) -> Any | None: + if callbacks is None: + return None + + for current_callback in getattr(callbacks, "_callbacks", []): + if getattr(current_callback, "checkpoint_component", None) != "dataloader": + continue + return current_callback if current_callback.has_checkpoint_state() else None + return None + + def has_state(self) -> bool: + return self.stateful is not None + + def state_dict(self) -> dict[Any, Any]: + if self.stateful is None: + return {} + return self.stateful.state_dict() + + def load_state_dict(self, state_dict: dict[Any, Any]) -> None: + if self.stateful is None: + if state_dict: + log.warning("Dataloader checkpoint state exists, but no dataloader state handler is registered.") + return + self.stateful.load_state_dict(state_dict) + + +class AsyncMode(str, enum.Enum): + DISABLED = "disabled" + ASYNC_WITH_PINNED_MEM = "async_with_pinned_mem" + + +class Terminate: + pass + + +class SaveDone: + def __init__(self, iteration: int, elapsed_time: float, succeeded: bool): + self.iteration = iteration + self.elapsed_time = elapsed_time + self.succeeded = succeeded + + def __str__(self): + return f"SaveDone(iteration={self.iteration}, elapsed_time={self.elapsed_time}, succeeded={self.succeeded})" + + +def save_checkpoint_in_background( + receiver_queue: multiprocessing.Queue, + sender_queue: multiprocessing.Queue, + config_checkpoint: CheckpointConfig, + config_job: JobConfig, +) -> None: + """ + Handles model checkpoint saving in a separate background process using PyTorch's distributed functionality. + This function runs in a dedicated process to avoid blocking the main training loop. + + Args: + receiver_queue: Queue to receive state dictionaries and commands from the main process + sender_queue: Queue to send completion signals back to the main process + config_checkpoint: Configuration settings for checkpoint saving behavior + config_job: Configuration settings for the training job + + Flow: + 1. Initializes distributed processing environment + 2. Continuously waits for state dictionaries to save + 3. Saves checkpoints asynchronously + 4. Signals completion back to main process + 5. Terminates when receiving a Terminate signal + + Raises: + AssertionError: If received object is neither Terminate signal nor valid state dict tuple + + Note: + - Uses a different port than the main process to avoid conflicts + - Disables TorchElastic agent store for checkpoint operations + - Automatically cleans up distributed process group on exit + """ + # Configure distributed environment + os.environ["MASTER_PORT"] = str(int(os.environ["MASTER_PORT"]) + 2) + os.environ["TORCHELASTIC_USE_AGENT_STORE"] = "False" + + # Set up GPU device and distributed processing + torch.cuda.set_device(int(os.environ["LOCAL_RANK"])) + if dist.is_initialized(): + dist.destroy_process_group() + dist.init_process_group(backend="gloo") + + # Initialize checkpointing mechanism + checkpoint_handler = DistributedCheckpointer( + config_checkpoint=config_checkpoint, + config_job=config_job, + callbacks=None, + disable_async=True, + ) + + while True: + log.info(f"Checkpoint background process is ready for next task, waiting for new state_dict") + received_data = receiver_queue.get() + log.info(f"Checkpoint background process received new state_dict") + + if isinstance(received_data, Terminate): + log.info(f"Checkpoint background process received termination signal, closing sender queue") + break + + assert isinstance(received_data, tuple), "Received data must be a tuple of (state_dict, checkpoint_path)" + state_dict, checkpoint_path = received_data + + # Save checkpoint and measure time taken. + start_time = time.monotonic() + iteration = state_dict["trainer"][0]["iteration"] + succeeded = False + + try: + log.info(f"Saving checkpoint to {checkpoint_path}") + checkpoint_handler.save_state_dict_worker(state_dict, checkpoint_path) + succeeded = True + except Exception as e: + log.error(f"Error saving checkpoint to {checkpoint_path}: {e}") + # continue because if the thread exits, the main thread keeps on adding to the queue + finally: + elapsed_time = time.monotonic() - start_time + log.info( + f"Checkpoint save completed in background process. " + f"Time taken: {elapsed_time:.2f} seconds, iteration: {iteration}, " + f"status: {'SUCCESS' if succeeded else 'FAILURE'}" + ) + sender_queue.put(SaveDone(iteration, elapsed_time, succeeded)) + + log.info("Cleaning up: destroying distributed process group") + dist.destroy_process_group() + + +def _replace_keys_with_ema_keys(state_dict: STATE_DICT_TYPE) -> STATE_DICT_TYPE: + """ + Renames model parameters from "net." to "net_ema.". + """ + if not all(k.startswith("net.") for k in state_dict.keys()): + raise ValueError("State dict must start with net. keys when load_ema_to_reg is True") + return {k.replace("net.", "net_ema."): v for k, v in state_dict.items()} + + +class CustomLoadPlanner(dcp.DefaultLoadPlanner): + """ + CustomLoadPlanner that supports ignoring keys during checkpoint load. + This is useful when the checkpoint is saved with a different component + architecture, e.g. different RoPE embeddings than the current model. + """ + + def __init__( + self, + flatten_state_dict: bool = True, + flatten_sharded_tensors: bool = True, + allow_partial_load: bool = False, + keys_to_skip_loading: List[str] = [], + load_ema_to_reg: bool = False, + ) -> None: + super().__init__( + flatten_state_dict=flatten_state_dict, + flatten_sharded_tensors=flatten_sharded_tensors, + allow_partial_load=allow_partial_load, + ) + self.keys_to_skip_loading = keys_to_skip_loading + self.load_ema_to_reg = load_ema_to_reg + if len(keys_to_skip_loading) > 0: + log.info(f"Skipping loading of keys that match the following patterns: {keys_to_skip_loading}") + + def set_up_planner( + self, + state_dict: STATE_DICT_TYPE, + metadata: Metadata | None = None, + is_coordinator: bool = False, + ) -> None: + state_dict = self._skip_keys_if_found(state_dict) + + if self.load_ema_to_reg: + state_dict = _replace_keys_with_ema_keys(state_dict) + + super().set_up_planner( + state_dict=state_dict, + metadata=metadata, + is_coordinator=is_coordinator, + ) + + def _skip_keys_if_found( + self, + state_dict: STATE_DICT_TYPE, + ) -> Dict[str, Any]: + """ + While loading the checkpoint, skip the weight loading for the keys + that contain any element of `self.keys_to_skip_loading` as a substring. + """ + if len(self.keys_to_skip_loading) == 0: + return state_dict + + new_state_dict = {} + for fqn, obj in state_dict.items(): + if any(skip_key in fqn for skip_key in self.keys_to_skip_loading): + log.warning(f"Skipping loading of key: {fqn}") + continue + new_state_dict[fqn] = obj + return new_state_dict + + +class CustomSavePlanner(dcp.DefaultSavePlanner): + """ + Custom save planner that enables an override for cache_plans_key when + caching of save plans is enabled. Caching of save plans reduces checkpointing + time by reusing the same save plan across checkpoints. This reduces the + checkpointing time by ~60% (benchmarked using the 235B-A22B Qwen3-VL model + on 64 GB200 nodes). + """ + + def __init__( + self, + flatten_state_dict: bool = True, + flatten_sharded_tensors: bool = True, + dedup_save_to_lowest_rank: bool = False, + save_reg_to_ema: bool = False, + enable_plan_caching: bool = False, + cache_plans_key: str | None = None, + ) -> None: + super().__init__( + flatten_state_dict=flatten_state_dict, + flatten_sharded_tensors=flatten_sharded_tensors, + dedup_save_to_lowest_rank=dedup_save_to_lowest_rank, + enable_plan_caching=enable_plan_caching, + ) + if cache_plans_key is not None: + self._cached_plans_key = cache_plans_key + + self.save_reg_to_ema = save_reg_to_ema + + def set_up_planner( + self, + state_dict: STATE_DICT_TYPE, + storage_meta: StorageMeta | None = None, + is_coordinator: bool = False, + ) -> None: + if self.save_reg_to_ema: + state_dict = _replace_keys_with_ema_keys(state_dict) + + super().set_up_planner( + state_dict=state_dict, + storage_meta=storage_meta, + is_coordinator=is_coordinator, + ) + + +class DistributedCheckpointer(AbstractCheckpointer): + CHECKPOINT_KEYS = ["model", "optim", "scheduler", "trainer", "dataloader"] + + def __init__( + self, + config_checkpoint: CheckpointConfig, + config_job: JobConfig, + callbacks: Optional[callback.CallBackGroup] = None, + disable_async: bool = False, + ): + super().__init__(config_checkpoint, config_job, callbacks) + self.config_checkpoint = config_checkpoint + if config_checkpoint.dcp_async_mode_enabled and not disable_async: + self.async_mode = AsyncMode.ASYNC_WITH_PINNED_MEM + else: + self.async_mode = AsyncMode.DISABLED + + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + ctx = get_context("spawn") + self.mp_queue_send = ctx.Queue() + self.mp_queue_recv = ctx.Queue() + self.mp = ctx.Process( + target=save_checkpoint_in_background, + args=( + self.mp_queue_send, + self.mp_queue_recv, + config_checkpoint, + config_job, + ), + daemon=True, + ) + self.mp.start() + self.cpu_offload_state_dict = None + self.staging_ckpt_file = None + self.staging_stream = torch.cuda.Stream() + self.checkpoint_in_progress = False + + def keys_to_resume_during_load(self) -> tuple[set[str], str | None, bool | None]: + """ + Determines the keys to resume from the checkpoint and the checkpoint path. + If the checkpoint is the latest checkpoint of the same model, then it is a + normal resume. If the checkpoint is a different model's checkpoint, then it is + a warm start. + + Args: + None + + Returns: + resume_keys: The keys to resume from the checkpoint. + checkpoint_path: The path to the checkpoint. If the checkpoint is a different + warm_start: Whether to warm start the training from a different model's checkpoint. + If the checkpoint is a different model's checkpoint, then this is True. + If the checkpoint is the latest checkpoint of the same model, then this is False. + """ + latest_checkpoint_file = self._read_latest_checkpoint_file() + + resume_keys = [] + warm_start = None + + if latest_checkpoint_file is not None: + # 1. Resume training from the latest checkpoint of the same model. + warm_start = False + checkpoint_path = os.path.join(self.load_dirname, latest_checkpoint_file) + resume_keys.extend(self.CHECKPOINT_KEYS) + + else: + if self.load_path and not str(self.load_path).endswith(".pt"): + # 2. Warm Start: Resume training from a different model's checkpoint + # specified by `load_path`. + warm_start = True + checkpoint_path = self.load_path + + if self.load_s3_backend_key: + checkpoint_path = f"s3://{self.config_checkpoint.load_from_object_store.bucket}/{checkpoint_path}" + + # If the path doesn't end with specific checkpoint, read the latest + # checkpoint file to determine the most recent checkpoint iteration. + if not re.search(r"/checkpoints/iter_\d{9}/?$", checkpoint_path): + old_ckpt_path = checkpoint_path + latest_ckpt_path = os.path.join(checkpoint_path, "checkpoints/latest_checkpoint.txt") + + # If the latest checkpoint file exists, use it to determine the + # checkpoint path. Otherwise, use the original path. + if easy_io.exists(latest_ckpt_path, backend_key=self.load_s3_backend_key): + checkpoint_file = easy_io.load( + latest_ckpt_path, backend_key=self.load_s3_backend_key + ).strip() + checkpoint_path = f"{checkpoint_path}/checkpoints/{checkpoint_file}" + else: + log.warning( + f"Latest checkpoint file {latest_ckpt_path} not found, load from {old_ckpt_path}" + ) + checkpoint_path = old_ckpt_path + + if self.load_training_state: + resume_keys.extend(self.CHECKPOINT_KEYS) + else: + resume_keys.append("model") + if self.only_load_scheduler_state: + resume_keys.append("scheduler") + else: + checkpoint_path = None + + if len(self.keys_not_to_resume) > 0: + for key in self.keys_not_to_resume: + assert key in self.CHECKPOINT_KEYS, f"Invalid key to resume: {key} not in {self.CHECKPOINT_KEYS}" + resume_keys = [key for key in resume_keys if key not in self.keys_not_to_resume] + + return set(resume_keys), checkpoint_path, warm_start + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + if self.callbacks is not None: + self.callbacks.on_load_checkpoint_start(model) + + resume_keys, checkpoint_path, warm_start = self.keys_to_resume_during_load() + resume_keys = sorted(resume_keys) + log.critical(f"Resuming ckpt {checkpoint_path} with keys: {resume_keys}") + + iteration = 0 + + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + dataloader_wrapper = DataloaderWrapper(self.callbacks) + _state_dict: dict[str, Any] = {} + for key in resume_keys: + dist.barrier() + + cur_key_ckpt_full_path = os.path.join(checkpoint_path, key) + log.critical(f"Start loading checkpoint from {cur_key_ckpt_full_path}") + + storage_reader = self.get_storage_reader(cur_key_ckpt_full_path) + strict_resume = self.config_checkpoint.strict_resume + + # Note that we only allow skipping loading of keys during warm start. If the checkpoint is + # the latest checkpoint of the same model, then we don't need to skip any keys. + keys_to_skip_loading = self.config_checkpoint.keys_to_skip_loading if warm_start else [] + + load_planner = CustomLoadPlanner( + allow_partial_load=not strict_resume, + keys_to_skip_loading=keys_to_skip_loading, + ) + + if key == "model": + log.info("- Loading the model...") + _model_wrapper = ModelWrapper(model) + _state_dict = _model_wrapper.state_dict() + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + if self.config_checkpoint.load_ema_to_reg: + # The model has both net.* and net_ema.* submodules, so _state_dict + # contains both sets of keys after dcp.load(). Copy EMA weights into + # regular model weights so we can resume from EMA and reset EMA. + for sd_key in list(_state_dict.keys()): + if sd_key.startswith("net."): + key_ema = "net_ema." + sd_key.removeprefix("net.") + assert key_ema in _state_dict, ( + f"EMA key {key_ema} not found in state_dict. " + "Ensure the model has net_ema submodule." + ) + _state_dict[sd_key] = _state_dict[key_ema] + results = _model_wrapper.load_state_dict(_state_dict) + if results is not None: + if len(results.missing_keys) > 0: + raise ValueError(f"Missing keys (not found in checkpoint): {results.missing_keys}") + if len(results.unexpected_keys) > 0: + raise ValueError( + f"Unexpected keys (found in checkpoint but not in model): {results.unexpected_keys}" + ) + + elif key == "optim": + log.info("- Loading the optimizer...") + _optim_wrapper = OptimizerWrapper(model, optimizer) + _state_dict = _optim_wrapper.state_dict() + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + _optim_wrapper.load_state_dict(_state_dict) + + elif key == "scheduler": + log.info("- Loading the scheduler...") + _state_dict = scheduler.state_dict() + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + scheduler.load_state_dict(_state_dict) + + elif key == "trainer": + log.info("- Loading the trainer...") + + # Use rank-specific key for RNG state to support correct per-rank restoration + rng_key = f"rng_state_{dist.get_rank()}" + current_rng_state = get_rand_state_dict() + _state_dict = { + "grad_scaler": grad_scaler.state_dict(), + "iteration": iteration, + } + # Check if rng_key exists in checkpoint metadata to avoid failure with strict_resume=True + metadata = storage_reader.read_metadata() + rng_key_exists = any( + k.startswith(f"{rng_key}.") or k == rng_key for k in metadata.state_dict_metadata.keys() + ) + if rng_key_exists: + _state_dict[rng_key] = current_rng_state + + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + grad_scaler.load_state_dict(_state_dict["grad_scaler"]) + iteration = _state_dict["iteration"] + set_rand_state_dict(_state_dict.get(rng_key, current_rng_state)) + elif key == "dataloader": + if not easy_io.exists(cur_key_ckpt_full_path, backend_key=self.load_s3_backend_key): + log.info( + f"Checkpoint {cur_key_ckpt_full_path} does not exist, skip loading dataloader.", + rank0_only=False, + ) + continue + + rank = dist.get_rank() + dataloader_pkl_path = os.path.join(cur_key_ckpt_full_path, f"rank_{rank}.pkl") + if not easy_io.exists(dataloader_pkl_path, backend_key=self.load_s3_backend_key): + log.info(f"No dataloader checkpoint found at {dataloader_pkl_path}", rank0_only=False) + continue + + log.info(f"- Loading the dataloader {cur_key_ckpt_full_path}...", rank0_only=False) + _state_dict = easy_io.load( + dataloader_pkl_path, + file_format="pkl", + backend_key=self.load_s3_backend_key, + ) + dataloader_wrapper.load_state_dict(_state_dict) + else: + raise ValueError(f"Invalid key: {key}. not support to resume.") + + if self.callbacks is not None and resume_keys: + self.callbacks.on_load_checkpoint(model, state_dict=_state_dict) + log.info(f"Loaded checkpoint from {checkpoint_path} in iteration {iteration}") + + else: + log.info("Training from scratch.") + + torch.cuda.empty_cache() + + if self.callbacks is not None: + self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path) + return iteration + + def _checkpoint_async_with_pinned_memory( + self, checkpoint_file: str, state_dict: Dict[str, Tuple[Any, str]] + ) -> None: + assert self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM, "Async mode must be AsyncMode.ASYNC_WITH_PINNED_MEM" + + from torch.distributed._state_dict_utils import _copy_state_dict, _create_cpu_state_dict + + if self.cpu_offload_state_dict is None: + log.info(f"Preparing the CPU memory for staging") + self.cpu_offload_state_dict = _create_cpu_state_dict(state_dict, pin_memory=True, share_memory=True) + + log.info(f"Staging the state_dict in CPU memory") + with torch.cuda.stream(self.staging_stream): + self.cpu_offload_state_dict = _copy_state_dict( + state_dict, + self.cpu_offload_state_dict, + non_blocking=True, + ) + self.staging_ckpt_file = checkpoint_file + + self.staging_stream.synchronize() + log.info(f"Staging the state_dict in CPU memory completed") + + self.mp_queue_send.put_nowait((self.cpu_offload_state_dict, self.staging_ckpt_file)) + self.checkpoint_in_progress = True + log.info(f"Submitted checkpoint to background process") + + def _wait_for_previous_async_checkpoint(self) -> None: + """ + Gets the results of previously submitted checkpoints. + Pass them to callbacks if checkpoint succeeded. + """ + assert self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM, "Async mode must be AsyncMode.ASYNC_WITH_PINNED_MEM" + + if not self.checkpoint_in_progress: + return + + success = False + try: + log.info(f"Waiting for checkpoint save result") + + # Note that we set a timeout of 1 hour to avoid blocking the main process + # indefinitely. Gloo and NCCL timeouts are ~30 minutes, so this timeout + # should typically be sufficient. + save_done: SaveDone = self.mp_queue_recv.get(timeout=3600) + + log.info(f"Received checkpoint save result: {save_done}") + + if self.callbacks is not None and save_done.succeeded: + self.callbacks.on_save_checkpoint_success( + iteration=save_done.iteration, elapsed_time=save_done.elapsed_time + ) + self.checkpoint_in_progress = False + success = save_done.succeeded + + except Exception as e: + log.error(f"Error waiting for checkpoint save result: {e}") + + if not success: + # Terminate training execution upon a failed checkpoint save attempt. + # A failure at this stage typically indicates a non-recoverable system error. + # Continuing execution would result in subsequent persistent failures and + # unnecessary waste of GPU resources. + raise RuntimeError("Previous checkpoint save failed. Exiting...") + + def get_storage_writer(self, checkpoint_path: str) -> Union[S3StorageWriter, FileSystemWriter]: + if self.save_to_object_store: + return S3StorageWriter( + credential_path=self.config_checkpoint.save_to_object_store.credentials, + path=checkpoint_path, + enable_gcs_patch_in_boto3=self.config_checkpoint.enable_gcs_patch_in_boto3, + ) + return FileSystemWriter(path=checkpoint_path) + + def get_storage_reader(self, checkpoint_path: str) -> Union[S3StorageReader, FileSystemReader]: + if self.load_from_object_store: + return S3StorageReader( + credential_path=self.config_checkpoint.load_from_object_store.credentials, + path=checkpoint_path, + enable_gcs_patch_in_boto3=self.config_checkpoint.enable_gcs_patch_in_boto3, + ) + return FileSystemReader(checkpoint_path) + + def _save_as_pkl(self, obj: Any, output_dir: str) -> None: + """Save per-rank Python checkpoint state such as no-replace dataloader progress.""" + rank = dist.get_rank() + path = os.path.join(output_dir, f"rank_{rank}.pkl") + easy_io.dump( + obj, + path, + file_format="pkl", + backend_key=self.save_s3_backend_key, + ) + log.info(f"Saved state to {path}") + + def save_state_dict_worker(self, to_save_dict: Dict[str, Tuple[Any, str]], checkpoint_file: str) -> None: + for key, (v, full_checkpoint_path) in to_save_dict.items(): + if key == "dataloader": + self._save_as_pkl(v, full_checkpoint_path) + else: + storage_writer = self.get_storage_writer(full_checkpoint_path) + # Note that it is ok to create a new CustomSavePlanner object + # for each checkpoint save since the save plans are cached in a + # class dictionary. + save_planner = CustomSavePlanner( + dedup_save_to_lowest_rank=True, + enable_plan_caching=True, + cache_plans_key=f"custom_planner_{key}", + ) + dcp.save( + v, + storage_writer=storage_writer, + planner=save_planner, + ) + + if distributed.is_rank0(): + log.info(f"Saving last checkpoint file {checkpoint_file}") + self._write_latest_checkpoint_file(checkpoint_file) + + log.info(f"Saved checkpoint to {os.path.join(self.save_dirname, checkpoint_file)}") + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + self._wait_for_previous_async_checkpoint() + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_start(model, iteration) + + checkpoint_file = f"iter_{iteration:09}" + + # Use rank-specific key for RNG state to ensure each rank saves its own state + rng_key = f"rng_state_{dist.get_rank()}" + + to_save_dict = { + "model": ModelWrapper(model).state_dict(), + "optim": OptimizerWrapper(model, optimizer).state_dict(), + "scheduler": scheduler.state_dict(), + "trainer": { + "grad_scaler": grad_scaler.state_dict(), + "iteration": iteration, + rng_key: get_rand_state_dict(), + }, + } + dataloader_wrapper = DataloaderWrapper(self.callbacks) + if dataloader_wrapper.has_state(): + to_save_dict["dataloader"] = dataloader_wrapper.state_dict() + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint(model, state_dict=to_save_dict) + + for k in to_save_dict.keys(): + output_dirname = os.path.join(self.save_dirname, f"iter_{iteration:09}/{k}") + to_save_dict[k] = (to_save_dict[k], output_dirname) + + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + dataloader_entry = to_save_dict.pop("dataloader", None) + if dataloader_entry is not None: + dataloader_state, dataloader_save_dir = dataloader_entry + self._save_as_pkl(dataloader_state, dataloader_save_dir) + self._checkpoint_async_with_pinned_memory(checkpoint_file, to_save_dict) + else: + start_time = time.monotonic() + self.save_state_dict_worker(to_save_dict, checkpoint_file) + elapsed_time = time.monotonic() - start_time + log.info(f"Checkpoint save completed: Time taken: {elapsed_time:.2f} seconds") + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_success(iteration=iteration, elapsed_time=elapsed_time) + + # This measures exposed (synchronous) checkpoint time, on_save_checkpoint_success() + # is instead called to measure the entire duration for asynchronous checkpoint for the async case too. + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration) + + def finalize(self) -> None: + super().finalize() + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + if self.mp and self.mp.is_alive(): + # Wait for the previous checkpoint to complete. + self._wait_for_previous_async_checkpoint() + + self.mp_queue_send.put(Terminate()) + self.mp.join() diff --git a/cosmos3/_src/vfm/conditioner.py b/cosmos3/_src/vfm/conditioner.py new file mode 100644 index 00000000..f812b723 --- /dev/null +++ b/cosmos3/_src/vfm/conditioner.py @@ -0,0 +1,578 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import copy +from abc import ABC, abstractmethod +from collections import defaultdict +from contextlib import nullcontext +from dataclasses import dataclass, fields +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union + +import omegaconf +import torch +import torch.nn as nn +from torch.distributed import ProcessGroup + +from cosmos3._src.imaginaire.functional.batch_ops import batch_mul +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.count_params import count_params +from cosmos3._src.imaginaire.utils.disabled_train import disabled_train +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.utils.context_parallel import broadcast + +T = TypeVar("T", bound="BaseCondition") + + +class DataType(str, Enum): + IMAGE = "image" + VIDEO = "video" + MIX = "mix" + + def __str__(self) -> str: + return self.value + + +def broadcast_condition(condition: BaseCondition, process_group: Optional[ProcessGroup] = None) -> BaseCondition: + """ + Broadcast the condition from the minimum rank in the specified group(s). + """ + if condition.is_broadcasted: + return condition + + kwargs = condition.to_dict(skip_underscore=False) + for key, value in kwargs.items(): + if value is not None: + kwargs[key] = broadcast(value, process_group) + kwargs["_is_broadcasted"] = True + return type(condition)(**kwargs) + + +@dataclass(frozen=True) +class BaseCondition(ABC): + """ + Attributes: + _is_broadcasted: Flag indicating if parallel broadcast splitting + has been performed. This is an internal implementation detail. + """ + + _is_broadcasted: bool = False + + def to_dict(self, skip_underscore: bool = True) -> Dict[str, Any]: + """Converts the condition to a dictionary. + + Returns: + Dictionary containing the condition's fields and values. + """ + # return {f.name: getattr(self, f.name) for f in fields(self) if not f.name.startswith("_")} + return {f.name: getattr(self, f.name) for f in fields(self) if not (f.name.startswith("_") and skip_underscore)} + + @property + def is_broadcasted(self) -> bool: + return self._is_broadcasted + + def broadcast(self, process_group: torch.distributed.ProcessGroup) -> BaseCondition: + """Broadcasts and splits the condition across the checkpoint parallelism group. + For most condition, such as Text2WorldCondition, we do not need split. + + Args: + process_group: The process group for broadcast and split + + Returns: + A new BaseCondition instance with the broadcasted and split condition. + """ + if self.is_broadcasted: + return self + return broadcast_condition(self, process_group) + + +@dataclass(frozen=True) +class Text2WorldCondition(BaseCondition): + crossattn_emb: Optional[torch.Tensor] = None + data_type: DataType = DataType.VIDEO + fps: Optional[torch.Tensor] = None + + def edit_data_type(self, data_type: DataType) -> Text2WorldCondition: + """Edit the data type of the condition. + + Args: + data_type: The new data type. + + Returns: + A new Text2WorldCondition instance with the new data type. + """ + kwargs = self.to_dict(skip_underscore=False) + kwargs["data_type"] = data_type + return type(self)(**kwargs) + + @property + def is_video(self) -> bool: + return self.data_type == DataType.VIDEO + + +@dataclass(frozen=True) +class GR00TV1Img2VidCondition(Text2WorldCondition): + gt_first_frame: Optional[torch.Tensor] = None + use_image_condition: bool = False + condition_video_input_mask: Optional[torch.Tensor] = None + + def edit_video_condition( + self, + x0, + process_group: Optional[ProcessGroup] = None, # x0: [B,C,T,H,W] + ) -> GR00TV1Img2VidCondition: + """Edit the video condition to include the video mask information. + + Args: + x0: The first frame of the video. + + Returns: + A new GR00TV1Img2VidCondition instance with the video mask information. + """ + pg_size = 1 if process_group is None else process_group.size() + kwargs = self.to_dict(skip_underscore=False) + B, _, T, H, W = x0.shape + condition_video_input_mask = torch.zeros((B, 1, T, H, W), dtype=x0.dtype, device=x0.device) # [B,1,T,H,W] + if pg_size == 1 or process_group.rank() == 0: + kwargs["gt_first_frame"] = x0[:, :, 0].detach() # [B,C,H,W] + condition_video_input_mask[:, :, 0] += 1 + kwargs["condition_video_input_mask"] = condition_video_input_mask + return type(self)(**kwargs) + + +class AbstractEmbModel(nn.Module): + def __init__(self): + super().__init__() + + self._is_trainable = None + self._dropout_rate = None + self._input_key = None + + self._return_dict = False + + @property + def is_trainable(self) -> bool: + return self._is_trainable + + @property + def dropout_rate(self) -> Union[float, torch.Tensor]: + return self._dropout_rate + + @property + def input_key(self) -> str: + return self._input_key + + @property + def is_return_dict(self) -> bool: + return self._return_dict + + @is_trainable.setter + def is_trainable(self, value: bool): + self._is_trainable = value + + @dropout_rate.setter + def dropout_rate(self, value: Union[float, torch.Tensor]): + self._dropout_rate = value + + @input_key.setter + def input_key(self, value: str): + self._input_key = value + + @is_return_dict.setter + def is_return_dict(self, value: bool): + self._return_dict = value + + @is_trainable.deleter + def is_trainable(self): + del self._is_trainable + + @dropout_rate.deleter + def dropout_rate(self): + del self._dropout_rate + + @input_key.deleter + def input_key(self): + del self._input_key + + @is_return_dict.deleter + def is_return_dict(self): + del self._return_dict + + def random_dropout_input( + self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None + ) -> torch.Tensor: + del key + dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate + return batch_mul( + torch.bernoulli((1.0 - dropout_rate) * torch.ones(in_tensor.shape[0])).type_as(in_tensor), # [B] + in_tensor, + ) # [B,N_text,hidden_size] + + def details(self) -> str: + return "" + + def summary(self) -> str: + input_key = self.input_key if self.input_key is not None else getattr(self, "input_keys", None) + return ( + f"{self.__class__.__name__} \n\tinput key: {input_key}" + f"\n\tParam count: {count_params(self, False)} \n\tTrainable: {self.is_trainable}" + f"\n\tDropout rate: {self.dropout_rate}" + f"\n\t{self.details()}" + ) + + +class TextAttr(AbstractEmbModel): + def __init__( + self, + input_key: List[str], + dropout_rate: Optional[float] = 0.0, + use_empty_string: bool = False, + empty_string_embeddings_path: str = "s3://bucket/predict2_assets/reason1_empty_string_embeddings.pt", + credential_path: str = "credentials/s3_training.secret", + ): + super().__init__() + self._input_key = input_key + self._dropout_rate = dropout_rate + # if True, will use empty string embeddings + # otherwise use zero tensor embeddings + self.use_empty_string = use_empty_string + self._empty_string_embeddings_cache = None + self.empty_string_embeddings_path = empty_string_embeddings_path + self.credential_path = credential_path + + def forward(self, token: torch.Tensor): + return {"crossattn_emb": token} + + def _get_empty_string_embeddings(self) -> torch.Tensor: + """Lazy load and cache empty string embeddings.""" + if self._empty_string_embeddings_cache is None: + self._empty_string_embeddings_cache = easy_io.load( + self.empty_string_embeddings_path, + backend_args={"backend": "s3", "s3_credential_path": self.credential_path}, + ) + return self._empty_string_embeddings_cache + + def random_dropout_input( + self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None + ) -> torch.Tensor: + if key is not None and "mask" in key: + return in_tensor + if not self.use_empty_string: + return super().random_dropout_input(in_tensor, dropout_rate, key) + B = in_tensor.shape[0] + dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate + empty_string_embeddings = self._get_empty_string_embeddings() + empty_string_embeddings = empty_string_embeddings.expand(in_tensor.shape).to( + dtype=in_tensor.dtype, device=in_tensor.device + ) # [B,N_text,hidden_size] + + keep_mask = torch.bernoulli((1.0 - dropout_rate) * torch.ones(B, device=in_tensor.device)).type_as( + in_tensor + ) # [B] + keep_mask = keep_mask.view(B, *[1] * (in_tensor.dim() - 1)) # [B,1,...] broadcastable shape + return keep_mask * in_tensor + (1.0 - keep_mask) * empty_string_embeddings # [B,N_text,hidden_size] + + def details(self) -> str: + return "Output key: [crossattn_emb]" + + +class TextAttrEmptyStringDrop(AbstractEmbModel): + def __init__(self, input_key: List[str], dropout_rate: Optional[float] = 0.0): + super().__init__() + self._input_key = input_key + self._dropout_rate = dropout_rate + self.empty_prompt_data = None + + def forward(self, token: torch.Tensor): + return {"crossattn_emb": token} + + def random_dropout_input( + self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None + ) -> torch.Tensor: + if key is not None and "mask" in key: + return in_tensor + del key + if self.empty_prompt_data is None: + self.empty_prompt_data = easy_io.load( + "s3://bucket/edify_video/v4/validation/item_dataset/negative_prompt/empty_string_umt5.pt", + backend_args={"backend": "s3", "s3_credential_path": "credentials/s3_training.secret"}, + ) + dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate + + B = in_tensor.shape[0] # batch size + # Create dropout mask: 1 -> keep in_tensor, 0 -> use empty_prompt_data + keep_mask = torch.bernoulli((1.0 - dropout_rate) * torch.ones(B, device=in_tensor.device)).type_as( + in_tensor + ) # [B] + keep_mask = keep_mask.view(B, *[1] * (in_tensor.dim() - 1)) # [B,1,...] broadcastable shape + # Prepare empty_prompt_data with correct shape, dtype, and device + empty_prompt = self.empty_prompt_data.to(dtype=in_tensor.dtype, device=in_tensor.device) + # Repeat empty_prompt along batch dimension if needed + if empty_prompt.shape[0] != B: + if empty_prompt.shape[0] == 1: + empty_prompt = empty_prompt.expand(B, *empty_prompt.shape[1:]) # [B,N_text,hidden_size] + else: + raise ValueError( + f"empty_prompt_data batch size {empty_prompt.shape[0]} does not match in_tensor batch size {B}" + ) + + # Mix using the dropout mask + return keep_mask * in_tensor + (1.0 - keep_mask) * empty_prompt # [B,N_text,hidden_size] + + def details(self) -> str: + return "Output key: [crossattn_emb]" + + +class ReMapkey(AbstractEmbModel): + def __init__( + self, + input_key: str, + output_key: Optional[str] = None, + dropout_rate: Optional[float] = 0.0, + dtype: Optional[str] = None, + ): + super().__init__() + self.output_key = output_key + self.dtype = { + None: None, + "float": torch.float32, + "bfloat16": torch.bfloat16, + "half": torch.float16, + "float16": torch.float16, + "int": torch.int32, + "long": torch.int64, + }[dtype] + self._input_key = input_key + self._output_key = output_key + self._dropout_rate = dropout_rate + + def forward(self, element: torch.Tensor) -> Dict[str, torch.Tensor]: + key = self.output_key if self.output_key else self.input_key + if isinstance(element, torch.Tensor): + element = element.to(dtype=self.dtype) + return {key: element} + + def details(self) -> str: + key = self.output_key if self.output_key else self.input_key + return f"Output key: {key} \n\tDtype: {self.dtype}" + + +class BooleanFlag(AbstractEmbModel): + def __init__(self, input_key: str, output_key: Optional[str] = None, dropout_rate: Optional[float] = 0.0): + super().__init__() + self._input_key = input_key + self._dropout_rate = dropout_rate + self.output_key = output_key + + def forward(self, *args, **kwargs) -> Dict[str, torch.Tensor]: + del args, kwargs + key = self.output_key if self.output_key else self.input_key + return {key: self.flag} + + def random_dropout_input( + self, in_tensor: torch.Tensor, dropout_rate: Optional[float] = None, key: Optional[str] = None + ) -> torch.Tensor: + del key + dropout_rate = dropout_rate if dropout_rate is not None else self.dropout_rate + self.flag = torch.bernoulli((1.0 - dropout_rate) * torch.ones(1)).bool().to(device=in_tensor.device) # [1] + return in_tensor + + def details(self) -> str: + key = self.output_key if self.output_key else self.input_key + return f"Output key: {key} \n\t This is a boolean flag" + + +class GeneralConditioner(nn.Module, ABC): + """ + An abstract module designed to handle various embedding models with conditional and unconditional configurations. + This abstract base class initializes and manages a collection of embedders that can dynamically adjust + their dropout rates based on conditioning. + + Attributes: + KEY2DIM (dict): A mapping from output keys to dimensions used for concatenation. + embedders (nn.ModuleDict): A dictionary containing all embedded models initialized and configured + based on the provided configurations. + + Parameters: + emb_models (Union[List, Any]): A dictionary where keys are embedder names and values are configurations + for initializing the embedders. + + Example: + See Edify4ConditionerConfig + """ + + KEY2DIM = {"crossattn_emb": 1} + + def __init__(self, **emb_models: Union[List, Any]): + super().__init__() + self.embedders = nn.ModuleDict() + for n, (emb_name, emb_config) in enumerate(emb_models.items()): + embedder = instantiate(emb_config) + assert isinstance(embedder, AbstractEmbModel), ( + f"embedder model {embedder.__class__.__name__} has to inherit from AbstractEmbModel" + ) + embedder.is_trainable = getattr(emb_config, "is_trainable", True) + embedder.dropout_rate = getattr(emb_config, "dropout_rate", 0.0) + if not embedder.is_trainable: + embedder.train = disabled_train + for param in embedder.parameters(): + param.requires_grad = False + embedder.eval() + + log.info(f"Initialized embedder #{n}-{emb_name}: \n {embedder.summary()}") + self.embedders[emb_name] = embedder + + @abstractmethod + def forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> Any: + """Should be implemented in subclasses to handle conditon datatype""" + raise NotImplementedError + + def _forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> Dict: + """ + Processes the input batch through all configured embedders, applying conditional dropout rates if specified. + Output tensors for each key are concatenated along the dimensions specified in KEY2DIM. + + Parameters: + batch (Dict): The input data batch to process. + override_dropout_rate (Optional[Dict[str, float]]): Optional dictionary to override default dropout rates + per embedder key. + + Returns: + Dict: A dictionary of output tensors concatenated by specified dimensions. + + Note: + In case the network code is sensitive to the order of concatenation, you can either control the order via \ + config file or make sure the embedders return a unique key for each output. + """ + output = defaultdict(list) + if override_dropout_rate is None: + override_dropout_rate = {} + + # make sure emb_name in override_dropout_rate is valid + for emb_name in override_dropout_rate.keys(): + assert emb_name in self.embedders, f"invalid name found {emb_name}" + + for emb_name, embedder in self.embedders.items(): + embedding_context = nullcontext if embedder.is_trainable else torch.no_grad + with embedding_context(): + if isinstance(embedder.input_key, str): + emb_out = embedder( + embedder.random_dropout_input( + batch[embedder.input_key], override_dropout_rate.get(emb_name, None) + ) + ) + elif isinstance(embedder.input_key, (list, omegaconf.listconfig.ListConfig)): + emb_out = embedder( + *[ + embedder.random_dropout_input(batch.get(k), override_dropout_rate.get(emb_name, None), k) + for k in embedder.input_key + ] + ) + else: + raise KeyError( + f"Embedder '{embedder.__class__.__name__}' requires an 'input_key' attribute to be defined as either a string or list of strings" + ) + for k, v in emb_out.items(): + output[k].append(v) + # Concatenate the outputs; crossattn_emb is concatenated along dim=1: [B,N_text,hidden_size] + return {k: torch.cat(v, dim=self.KEY2DIM.get(k, -1)) for k, v in output.items()} + + def get_condition_uncondition( + self, + data_batch: Dict, + ) -> Tuple[Any, Any]: + """ + Processes the provided data batch to generate two sets of outputs: conditioned and unconditioned. This method + manipulates the dropout rates of embedders to simulate two scenarios — one where all conditions are applied + (conditioned), and one where they are removed or reduced to the minimum (unconditioned). + + This method first sets the dropout rates to zero for the conditioned scenario to fully apply the embedders' effects. + For the unconditioned scenario, it sets the dropout rates to 1 (or to 0 if the initial unconditional dropout rate + is insignificant) to minimize the embedders' influences, simulating an unconditioned generation. + + Parameters: + data_batch (Dict): The input data batch that contains all necessary information for embedding processing. The + data is expected to match the required format and keys expected by the embedders. + + Returns: + Tuple[Any, Any]: A tuple containing two condition: + - The first one contains the outputs with all embedders fully applied (conditioned outputs). + - The second one contains the outputs with embedders minimized or not applied (unconditioned outputs). + """ + cond_dropout_rates, dropout_rates = {}, {} + for emb_name, embedder in self.embedders.items(): + cond_dropout_rates[emb_name] = 0.0 + dropout_rates[emb_name] = 1.0 if embedder.dropout_rate > 1e-4 else 0.0 + + condition: Any = self(data_batch, override_dropout_rate=cond_dropout_rates) + un_condition: Any = self(data_batch, override_dropout_rate=dropout_rates) + return condition, un_condition + + def get_condition_with_negative_prompt( + self, + data_batch: Dict, + ) -> Tuple[Any, Any]: + """ + Similar functionality as get_condition_uncondition + But use negative prompts for unconditon + """ + cond_dropout_rates, uncond_dropout_rates = {}, {} + for emb_name, embedder in self.embedders.items(): + cond_dropout_rates[emb_name] = 0.0 + if isinstance(embedder, TextAttr): + uncond_dropout_rates[emb_name] = 0.0 + else: + uncond_dropout_rates[emb_name] = 1.0 if embedder.dropout_rate > 1e-4 else 0.0 + + data_batch_neg_prompt = copy.deepcopy(data_batch) + if "neg_t5_text_embeddings" in data_batch_neg_prompt: + if isinstance(data_batch_neg_prompt["neg_t5_text_embeddings"], torch.Tensor): + data_batch_neg_prompt["t5_text_embeddings"] = data_batch_neg_prompt["neg_t5_text_embeddings"] + + condition: Any = self(data_batch, override_dropout_rate=cond_dropout_rates) + un_condition: Any = self(data_batch_neg_prompt, override_dropout_rate=uncond_dropout_rates) + + return condition, un_condition + + +class VideoConditioner(GeneralConditioner): + def forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> Text2WorldCondition: + output = super()._forward(batch, override_dropout_rate) + return Text2WorldCondition(**output) + + +class GR00TV1Img2VidConditioner(GeneralConditioner): + def forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> GR00TV1Img2VidCondition: + output = super()._forward(batch, override_dropout_rate) + return GR00TV1Img2VidCondition(**output) diff --git a/cosmos3/_src/vfm/configs/__init__.py b/cosmos3/_src/vfm/configs/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/__init__.py b/cosmos3/_src/vfm/configs/base/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/config.py b/cosmos3/_src/vfm/configs/base/config.py new file mode 100644 index 00000000..456d7c02 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/config.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, List + +import attrs +from omegaconf import OmegaConf + +from cosmos3._src.imaginaire import config +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer as Trainer +from cosmos3._src.imaginaire.utils.config_helper import import_all_modules_from_package +from cosmos3._src.vfm.configs.base.defaults.model_config import ModelConfig + + +@attrs.define(slots=False) +class DataSetting: + """Configuration for data. + + Attributes: + qwen_max_video_token_length: Maximum video token length. + qwen_target_fps: Target fps for video sampling. + text_chat_order: Order of text items in user messages. + """ + + qwen_max_video_token_length: int = 8192 + + +@attrs.define(slots=False) +class Config(config.Config): + data_setting: DataSetting = attrs.field(factory=DataSetting) + defaults: List[Any] = attrs.field( + factory=lambda: [ + "_self_", + {"model": "mot_fsdp"}, + {"data_train": None}, + {"data_val": None}, + {"optimizer": "adamw"}, + {"scheduler": "warmup_cosine_lr"}, + {"checkpoint": "s3"}, + {"callbacks": ["basic", "optimization", "job_monitor", "generation"]}, + {"ema": "power"}, + {"tokenizer": "wan2pt2_tokenizer"}, + {"sound_tokenizer": None}, # Optional: for audio-video generation + {"cluster": "gcp_iad_gb200"}, + {"vlm_config": None}, + {"ckpt_type": "dcp"}, + {"experiment": None}, + ] + ) + + def validate(self) -> None: + super().validate() + self._dispatch_model_config_validate() + + def _dispatch_model_config_validate(self) -> None: + """Run model-family validation on the composed model.config. + + validate() runs before instantiate(), so self.model.config is a + DictConfig wrapping the structured schema rather than the attrs class. + DictConfig surfaces fields but not methods, so to drive the typed + isinstance dispatch the schema must first be materialized via + OmegaConf.to_object. + """ + materialized = OmegaConf.to_object(self.model.config) + if isinstance(materialized, ModelConfig): + materialized.validate(self) + + +def make_config() -> Config: + c = Config( + model=None, + optimizer=None, + scheduler=None, + dataloader_train=None, + dataloader_val=None, + ) + + # Specifying values through instances of attrs + c.job.project = "cosmos3_vfm" + c.job.group = "debug" + c.job.name = "delete_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + c.trainer.type = Trainer + c.trainer.straggler_detection.enabled = False + c.trainer.max_iter = 400_000 + c.trainer.logging_iter = 20 + c.trainer.validation_iter = 100 + c.trainer.run_validation = False + c.trainer.callbacks = None + + c.upload_reproducible_setup = False + + from cosmos3._src.vfm.configs.base.defaults.callbacks import register_callbacks + from cosmos3._src.vfm.configs.base.defaults.checkpointer import register_checkpoint, register_ckpt_type + from cosmos3._src.vfm.configs.base.defaults.cluster import register_cluster + from cosmos3._src.vfm.configs.base.defaults.ema import register_ema + + # from cosmos3._src.vfm.configs.base.defaults.data import register_data + from cosmos3._src.vfm.configs.base.defaults.model import register_model + from cosmos3._src.vfm.configs.base.defaults.optimizer import register_optimizer, register_scheduler + from cosmos3._src.vfm.configs.base.defaults.tokenizer import register_sound_tokenizer, register_tokenizer + from cosmos3._src.vfm.configs.base.defaults.vlm import register_vlm + + # Call this function to register config groups for advanced overriding. the order follows the default config groups + # register_data() + register_model() + register_checkpoint() + register_ckpt_type() + register_optimizer() + register_scheduler() + register_callbacks() + register_tokenizer() + register_sound_tokenizer() + register_ema() + register_cluster() + register_vlm() + + # experiment config are defined in the experiment folder + # call import_all_modules_from_package to register them + import_all_modules_from_package("cosmos3._src.vfm.configs.base.experiment", reload=True) + return c diff --git a/cosmos3/_src/vfm/configs/base/defaults/__init__.py b/cosmos3/_src/vfm/configs/base/defaults/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/defaults/callbacks.py b/cosmos3/_src/vfm/configs/base/defaults/callbacks.py new file mode 100644 index 00000000..74568cd3 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/callbacks.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dataloader config options.""" + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.callbacks.manual_gc import ManualGarbageCollection +from cosmos3._src.imaginaire.lazy_config import PLACEHOLDER +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.utils.callback import WandBCallback +from cosmos3._src.vfm.callbacks.compile_tokenizer import CompileTokenizer +from cosmos3._src.vfm.callbacks.dataloading_monitor import DetailedDataLoadingSpeedMonitor +from cosmos3._src.vfm.callbacks.device_monitor import DeviceMonitor +from cosmos3._src.vfm.callbacks.every_n_draw_sample import EveryNDrawSample +from cosmos3._src.vfm.callbacks.expert_heatmap import ExpertHeatmap +from cosmos3._src.vfm.callbacks.grad_clip import GradClip +from cosmos3._src.vfm.callbacks.heart_beat import HeartBeat +from cosmos3._src.vfm.callbacks.iter_speed import IterSpeed +from cosmos3._src.vfm.callbacks.load_pretrained import LoadPretrained +from cosmos3._src.vfm.callbacks.low_precision import LowPrecisionCallback +from cosmos3._src.vfm.callbacks.mfu import MFUCallback +from cosmos3._src.vfm.callbacks.moe_specialization_callback import MoESpecializationCallback +from cosmos3._src.vfm.callbacks.moe_stability_callback import MoEStabilityCallback +from cosmos3._src.vfm.callbacks.norm_monitor import NormMonitor +from cosmos3._src.vfm.callbacks.ofu import OFUCallback +from cosmos3._src.vfm.callbacks.param_count import ParamCount +from cosmos3._src.vfm.callbacks.sequence_packing_padding import SequencePackingPadding +from cosmos3._src.vfm.callbacks.sigma_loss_analysis import SigmaLossAnalysis +from cosmos3._src.vfm.callbacks.skip_nan_step import SkipNaNStep +from cosmos3._src.vfm.callbacks.training_stats import TrainingStatsCallback +from cosmos3._src.vfm.callbacks.wandb_log import WandbCallback as WandBCallbackMultiplier +from cosmos3._src.vfm.callbacks.wandb_log_eval import WandbCallback as WandBCallbackEval + +BASIC_CALLBACKS = dict( + iter_speed=L(IterSpeed)( # does not use model or optimizer + every_n="${trainer.logging_iter}", + save_s3="${upload_reproducible_setup}", + save_s3_every_log_n=500, + hit_thres=50, + ), + manual_gc=L(ManualGarbageCollection)(every_n=5), # does not use model or optimizer + wandb=L(WandBCallback)(), + wandb_2x=L(WandBCallbackMultiplier)( + logging_iter_multipler=2, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + param_count=L(ParamCount)( # use model + save_s3="${upload_reproducible_setup}", + ), + dataloader_speed=L(DetailedDataLoadingSpeedMonitor)( + every_n=100, + save_s3="${upload_reproducible_setup}", + ), + wandb_val=L(WandBCallbackEval)( + save_s3="${upload_reproducible_setup}", + ), + moe_stability=L(MoEStabilityCallback)(every_n=250), + moe_specialization=L(MoESpecializationCallback)(every_n=250), + expert_heatmap=L(ExpertHeatmap)(), + load_pretrained=L(LoadPretrained)(), + compile_tokenizer=L(CompileTokenizer)(enabled=False, compile_after_iterations=3), + norm_monitor=L(NormMonitor)( + every_n=5000, + log_stat_wandb=True, + save_s3="${upload_reproducible_setup}", + track_activations=True, + ), + sigma_loss_analysis=L(SigmaLossAnalysis)( + every_n=5000, + every_n_viz=5000, + save_s3="${upload_reproducible_setup}", + ), + sequence_packing_padding=L(SequencePackingPadding)(every_n="${trainer.logging_iter}"), + mfu=L(MFUCallback)(every_n="${trainer.logging_iter}", grad_accum_iter="${trainer.grad_accum_iter}"), + ofu=L(OFUCallback)(every_n="${trainer.logging_iter}"), +) + +JOB_MONITOR_CALLBACKS = dict( + heart_beat=L(HeartBeat)( + every_n=200, + update_interval_in_minute=20, + save_s3="${upload_reproducible_setup}", + ), + device_monitor=L(DeviceMonitor)( + every_n=200, + save_s3="${upload_reproducible_setup}", + upload_every_n_mul=5, + ), +) + +OPTIMIZATION_CALLBACKS = dict( + skip_nan_step=L(SkipNaNStep)(max_consecutive_nan=100), + grad_clip=L(GradClip)(clip_norm=1.0), # use model, not supported yet + low_precision=L(LowPrecisionCallback)(update_iter=1, config=PLACEHOLDER, trainer=PLACEHOLDER), # use model +) + +VIZ_ONLINE_SAMPLING_CALLBACKS = dict( + every_n_sample_reg=L(EveryNDrawSample)( + every_n=5000, + save_s3=True, + do_x0_prediction=False, + ), + every_n_sample_ema=L(EveryNDrawSample)( + every_n=5000, + is_ema=True, + save_s3=True, + do_x0_prediction=False, + ), +) + + +def register_callbacks(): + cs = ConfigStore.instance() + cs.store(group="callbacks", package="trainer.callbacks", name="basic", node=BASIC_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="job_monitor", node=JOB_MONITOR_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="optimization", node=OPTIMIZATION_CALLBACKS) + # Online sampling generation callback + cs.store( + group="callbacks", package="trainer.callbacks", name="viz_online_sampling", node=VIZ_ONLINE_SAMPLING_CALLBACKS + ) + # Register "generation" as alias for "viz_online_sampling" (expected by base config.py defaults) + cs.store(group="callbacks", package="trainer.callbacks", name="generation", node=VIZ_ONLINE_SAMPLING_CALLBACKS) + + TRAINING_STATS_CALLBACKS = dict( + training_stats=L(TrainingStatsCallback)( + log_freq=100, + ) + ) + cs.store(group="callbacks", package="trainer.callbacks", name="training_stats", node=TRAINING_STATS_CALLBACKS) diff --git a/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py b/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py new file mode 100644 index 00000000..f4e77296 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Copied from https://gitlab-master.nvidia.com/dir/imaginaire4/-/blob/d0921eb675d1251e73c4b19acdd78e6ad936ae3b/projects/cosmos/reason2/configs/base/defaults/checkpointer.py without changes +""" + +from typing import Dict + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire import config +from cosmos3._src.imaginaire.checkpointer.dummy import Checkpointer as DummyCheckpointer +from cosmos3._src.imaginaire.config import CheckpointConfig +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.checkpointer.dcp import DistributedCheckpointer + +local_object_store = config.ObjectStoreConfig( + enabled=False, +) + +pdx_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/pdx_vfm_checkpoint.secret", + bucket="checkpoints", +) + +s3_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training.secret", + bucket="bucket", +) + +s3_eu_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training_eu.secret", + bucket="bucket", +) + +gcp_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/gcp_checkpoint.secret", + bucket="bucket", +) + +neb_eu_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/neb_eu.secret", + bucket="nv-01-10206-checkpoint-experiments", +) + +CHECKPOINT_LOCAL = CheckpointConfig( + save_to_object_store=local_object_store, + load_from_object_store=local_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_PDX = CheckpointConfig( + save_to_object_store=pdx_object_store, + load_from_object_store=pdx_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_S3 = CheckpointConfig( + save_to_object_store=s3_object_store, + load_from_object_store=s3_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_S3_EU = CheckpointConfig( + save_to_object_store=s3_eu_object_store, + load_from_object_store=s3_eu_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_GCP = CheckpointConfig( + save_to_object_store=gcp_object_store, + save_iter=1000, + load_from_object_store=gcp_object_store, + load_path="", + load_training_state=False, + strict_resume=True, + enable_gcs_patch_in_boto3=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_NEB_EU = CheckpointConfig( + save_to_object_store=neb_eu_object_store, + load_from_object_store=neb_eu_object_store, + save_iter=2000, + broadcast_via_filesystem=True, +) + + +def register_checkpoint(): + cs = ConfigStore.instance() + cs.store(group="checkpoint", package="checkpoint", name="local", node=CHECKPOINT_LOCAL) + cs.store(group="checkpoint", package="checkpoint", name="pdx", node=CHECKPOINT_PDX) + cs.store(group="checkpoint", package="checkpoint", name="s3", node=CHECKPOINT_S3) + cs.store(group="checkpoint", package="checkpoint", name="s3_eu", node=CHECKPOINT_S3_EU) + cs.store(group="checkpoint", package="checkpoint", name="gcp", node=CHECKPOINT_GCP) + cs.store(group="checkpoint", package="checkpoint", name="neb_eu", node=CHECKPOINT_NEB_EU) + + +DUMMY_CHECKPOINTER: Dict[str, str] = L(DummyCheckpointer)() +DISTRIBUTED_CHECKPOINTER: Dict[str, str] = L(DistributedCheckpointer)() + + +def register_ckpt_type(): + cs = ConfigStore.instance() + cs.store(group="ckpt_type", package="checkpoint.type", name="dummy", node=DUMMY_CHECKPOINTER) + cs.store(group="ckpt_type", package="checkpoint.type", name="dcp", node=DISTRIBUTED_CHECKPOINTER) diff --git a/cosmos3/_src/vfm/configs/base/defaults/cluster.py b/cosmos3/_src/vfm/configs/base/defaults/cluster.py new file mode 100644 index 00000000..bb017174 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/cluster.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import attrs +from hydra.core.config_store import ConfigStore + + +@attrs.define(slots=False) +class ClusterConfig: + """ + Config for the cluster specific information. + Everything cluster specific should be here. + """ + + object_store_bucket_data: str + object_store_bucket_checkpoint: str + object_store_bucket_pretrained: str + + object_store_credential_data: str + object_store_credential_checkpoint: str + object_store_credential_pretrained: str + + +AWSIADH100Config: ClusterConfig = ClusterConfig( + object_store_bucket_data="", + object_store_bucket_checkpoint="bucket", + object_store_bucket_pretrained="bucket", + object_store_credential_data="credentials/s3_training.secret", + object_store_credential_checkpoint="credentials/s3_checkpoint.secret", + object_store_credential_pretrained="credentials/s3_checkpoint.secret", +) + +GCPIADGB200Config: ClusterConfig = ClusterConfig( + object_store_bucket_data="", + object_store_bucket_checkpoint="bucket", + object_store_bucket_pretrained="bucket", + object_store_credential_data="credentials/gcp_checkpoint.secret", + object_store_credential_checkpoint="credentials/gcp_training.secret", + object_store_credential_pretrained="credentials/gcp_training.secret", +) + + +def register_cluster(): + cs = ConfigStore.instance() + cs.store(group="cluster", package="job.cluster", name="aws_iad_h100", node=AWSIADH100Config) + cs.store(group="cluster", package="job.cluster", name="gcp_iad_gb200", node=GCPIADGB200Config) diff --git a/cosmos3/_src/vfm/configs/base/defaults/conditioner.py b/cosmos3/_src/vfm/configs/base/defaults/conditioner.py new file mode 100644 index 00000000..69f70e71 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/conditioner.py @@ -0,0 +1,194 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from dataclasses import dataclass +from typing import Dict, Optional + +import torch +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.vfm.conditioner import BooleanFlag, GeneralConditioner, ReMapkey, Text2WorldCondition +from cosmos3._src.vfm.utils.context_parallel import broadcast_split_tensor + + +@dataclass(frozen=True) +class Video2WorldCondition(Text2WorldCondition): + use_video_condition: bool = False + # the following two attributes are used to set the video condition; during training, inference + gt_frames: Optional[torch.Tensor] = None + condition_video_input_mask: Optional[torch.Tensor] = None + + def set_video_condition( + self, + gt_frames: torch.Tensor, + random_min_num_conditional_frames: int, + random_max_num_conditional_frames: int, + num_conditional_frames: Optional[int] = None, + conditional_frames_probs: Optional[Dict[int, float]] = None, + ) -> "Video2WorldCondition": + """ + Sets the video conditioning frames for video-to-video generation. + + This method creates a conditioning mask for the input video frames that determines + which frames will be used as context frames for generating new frames. The method + handles both image batches (T=1) and video batches (T>1) differently. + + Args: + gt_frames: A tensor of ground truth frames with shape [B, C, T, H, W], where: + B = batch size + C = number of channels + T = number of frames + H = height + W = width + + random_min_num_conditional_frames: Minimum number of frames to use for conditioning + when randomly selecting a number of conditioning frames. + + random_max_num_conditional_frames: Maximum number of frames to use for conditioning + when randomly selecting a number of conditioning frames. + + num_conditional_frames: Optional; If provided, all examples in the batch will use + exactly this many frames for conditioning. If None, a random number of frames + between random_min_num_conditional_frames and random_max_num_conditional_frames + will be selected for each example in the batch. + + conditional_frames_probs: Optional; Dictionary mapping number of frames to probabilities. + If provided, overrides the random_min/max_num_conditional_frames with weighted sampling. + Example: {0: 0.5, 1: 0.25, 2: 0.25} for 50% chance of 0 frames, 25% for 1, 25% for 2. + + Returns: + A new Video2WorldCondition object with the gt_frames and conditioning mask set. + The conditioning mask (condition_video_input_mask) is a binary tensor + of shape [B, 1, T, H, W] where 1 indicates frames used for conditioning and 0 + indicates frames to be generated. + + Notes: + - For image batches (T=1), no conditioning frames are used (num_conditional_frames_B = 0). + - For video batches: + - If num_conditional_frames is provided, all examples use that fixed number of frames. + - Otherwise, each example randomly uses between random_min_num_conditional_frames and + random_max_num_conditional_frames frames. + - The mask marks the first N frames as conditioning frames (set to 1) for each example. + """ + kwargs = self.to_dict(skip_underscore=False) + kwargs["gt_frames"] = gt_frames + + # condition_video_input_mask + B, _, T, H, W = gt_frames.shape + condition_video_input_mask = torch.zeros(B, 1, T, H, W, dtype=gt_frames.dtype, device=gt_frames.device) + if T == 1: # handle image batch + num_conditional_frames_B = torch.zeros(B, dtype=torch.int32) + else: # handle video batch + if num_conditional_frames is not None: + num_conditional_frames_B = torch.ones(B, dtype=torch.int32) * num_conditional_frames + elif conditional_frames_probs is not None: + # Use weighted sampling based on provided probabilities + frames_options = list(conditional_frames_probs.keys()) + weights = list(conditional_frames_probs.values()) + num_conditional_frames_B = torch.tensor( + random.choices(frames_options, weights=weights, k=B), dtype=torch.int32 + ) + else: + num_conditional_frames_B = torch.randint( + random_min_num_conditional_frames, random_max_num_conditional_frames + 1, size=(B,) + ) + for idx in range(B): + condition_video_input_mask[idx, :, : num_conditional_frames_B[idx], :, :] += 1 + + kwargs["condition_video_input_mask"] = condition_video_input_mask + return type(self)(**kwargs) + + def edit_for_inference( + self, is_cfg_conditional: bool = True, num_conditional_frames: int = 1 + ) -> "Video2WorldCondition": + _condition = self.set_video_condition( + gt_frames=self.gt_frames, + random_min_num_conditional_frames=0, + random_max_num_conditional_frames=0, + num_conditional_frames=num_conditional_frames, + ) + if not is_cfg_conditional: + # Do not use classifier free guidance on conditional frames. + # YB found that it leads to worse results. + _condition.use_video_condition.fill_(True) + return _condition + + def broadcast(self, process_group: torch.distributed.ProcessGroup) -> "Video2WorldCondition": + if self.is_broadcasted: + return self + # extra efforts + gt_frames = self.gt_frames + condition_video_input_mask = self.condition_video_input_mask + kwargs = self.to_dict(skip_underscore=False) + kwargs["gt_frames"] = None + kwargs["condition_video_input_mask"] = None + new_condition = Text2WorldCondition.broadcast( + type(self)(**kwargs), + process_group, + ) + + kwargs = new_condition.to_dict(skip_underscore=False) + _, _, T, _, _ = gt_frames.shape + if process_group is not None: + if T > 1 and process_group.size() > 1: + gt_frames = broadcast_split_tensor(gt_frames, seq_dim=2, process_group=process_group) + condition_video_input_mask = broadcast_split_tensor( + condition_video_input_mask, seq_dim=2, process_group=process_group + ) + kwargs["gt_frames"] = gt_frames + kwargs["condition_video_input_mask"] = condition_video_input_mask + return type(self)(**kwargs) + + +class Video2WorldConditioner(GeneralConditioner): + def forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> Video2WorldCondition: + output = super()._forward(batch, override_dropout_rate) + return Video2WorldCondition(**output) + + +_SHARED_CONFIG = dict( + fps=L(ReMapkey)( + input_key="fps", + output_key="fps", + dropout_rate=0.0, + dtype=None, + ), + use_video_condition=L(BooleanFlag)( + input_key="fps", + output_key="use_video_condition", + dropout_rate=0.2, + ), +) + +VideoPredictionConditioner: LazyDict = L(Video2WorldConditioner)( + **_SHARED_CONFIG, +) + + +def register_conditioner(): + cs = ConfigStore.instance() + cs.store( + group="conditioner", + package="model.config.conditioner", + name="video_prediction_conditioner", + node=VideoPredictionConditioner, + ) diff --git a/cosmos3/_src/vfm/configs/base/defaults/ema.py b/cosmos3/_src/vfm/configs/base/defaults/ema.py new file mode 100644 index 00000000..2ea0b65d --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/ema.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import attrs +from hydra.core.config_store import ConfigStore + + +@attrs.define(slots=False) +class EMAConfig: + """ + Config for the EMA. + """ + + enabled: bool = True + rate: float = 0.1 + iteration_shift: int = 0 + + +PowerEMAConfig: EMAConfig = EMAConfig( + enabled=True, + rate=0.10, + iteration_shift=0, +) + + +def register_ema(): + cs = ConfigStore.instance() + cs.store(group="ema", package="model.config.ema", name="power", node=PowerEMAConfig) diff --git a/cosmos3/_src/vfm/configs/base/defaults/model.py b/cosmos3/_src/vfm/configs/base/defaults/model.py new file mode 100644 index 00000000..3d2ed22f --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/model.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.configs.base.defaults.model_config import ( + OmniMoTModelConfig, + ParallelismConfig, +) +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + +MOT_DDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="ddp", + ), + model=L(OmniMoTModel)( + config=OmniMoTModelConfig(), + _recursive_=False, + ), +) + + +MOT_FSDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="fsdp", + ), + model=L(OmniMoTModel)( + config=OmniMoTModelConfig( + parallelism=ParallelismConfig( + data_parallel_shard_degree=8, + ), + ), + _recursive_=False, + ), +) + + +def register_model(): + cs = ConfigStore.instance() + cs.store(group="model", package="_global_", name="mot_ddp", node=MOT_DDP_CONFIG) + cs.store(group="model", package="_global_", name="mot_fsdp", node=MOT_FSDP_CONFIG) diff --git a/cosmos3/_src/vfm/configs/base/defaults/model_config.py b/cosmos3/_src/vfm/configs/base/defaults/model_config.py new file mode 100644 index 00000000..585512c9 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/model_config.py @@ -0,0 +1,389 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Any, Literal + +import attrs + +from cosmos3._src.imaginaire.config import Config +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.vfm.configs.base.defaults.ema import EMAConfig +from cosmos3._src.vfm.configs.base.defaults.vlm import VLMConfig +from cosmos3._src.vfm.configs.base.vlm.defaults.training import PolicyConfig, TrainConfig + + +@attrs.define(slots=False) +class ModelConfig: + """Typed base for project model configs. + + Subclasses override validate to add family-specific checks. The receiver is + a fresh attrs copy from OmegaConf.to_object, so mutations to self are + discarded; write through root_config for any propagating side effects. + + The ema field is required (disabled by default) so trainer/callback reads + stay as model.config.ema.enabled across every family; subclasses that opt in + (e.g. OmniMoTModelConfig) override with their own EMAConfig. + """ + + ema: EMAConfig = EMAConfig(enabled=False) + + def validate(self, root_config: Config) -> None: + return + + +@attrs.define(slots=False) +class DiffusionExpertConfig: + # This determines the range of timesteps before the fourier feature embedding is applied. + timestep_range: float = 1.0 + # Whether to load the generation pathway weights from pretrained LLM/VLM weights. + load_weights_from_pretrained: bool = True + + patch_spatial: int = 2 + max_vae_latent_side_after_patchify: int = ( + 20 # Max dimension (h or w) of the VAE latent after patchification (320/(8*2)) + ) + # Position embedding type for vision tokens: + # - "3d_rope": Additive 3D RoPE embeddings (VideoRopePosition3DEmb) + 1D position IDs for attention + # - "flattened_sin_cos": Additive flattened sin/cos embeddings + 1D position IDs for attention + # - "unified_3d_mrope": No additive embedding + 3D position IDs for Qwen3VL-style mRoPE attention + position_embedding_type: str = "3d_rope" + # When finetuning from lower resolution to higher resolution, the spatial resolution of videos increase. + # So, we need to adjust the position embedding. + # We use NTK based RoPE extrapolation to adjust the position embedding. + # Reference: (https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/) + # Design adapted from Cosmos2.5 (https://arxiv.org/pdf/2511.00062) + # extrapolation_ratio here is how the base of the RoPE is scaled + # b' = b * extrapolation_ratio^(dim / (dim - 2)) + rope_h_extrapolation_ratio: float = 1.0 + rope_w_extrapolation_ratio: float = 1.0 + rope_t_extrapolation_ratio: float = 1.0 + enable_fps_modulation: bool = False + base_fps: int = 24 + # For unified_3d_mrope: whether spatial (H, W) indices reset to 0 for each vision segment + unified_3d_mrope_reset_spatial_ids: bool = True + # Setting the temporal gap on the boundary of the different modalities, default is 0, using a value greater than 0 will add an additional offset on the accumulated temporal offset. + unified_3d_mrope_temporal_modality_margin: int = 0 + + +@attrs.define(slots=False) +class ParallelismConfig: + # Activation checkpointing is used to reduce the memory usage of the model. + # The outputs of each layer are checkpointed, the intermediate results are not saved. + use_activation_checkpointing: bool = False + + # Torch compile is used to compile the model for faster training. + use_torch_compile: bool = False + + # Whether to use CUDA graphs for faster inference. This option does not work during training. + use_cuda_graphs: bool = False + + # Whether the entire Cosmos3 VFM network is compiled, or only a specific region is compiled. + # Use "language" to compile only individual layers in the MOT model. + # Use "all" to compile the the MOT model, as well as encode/decode functions. + compiled_region: Literal["all", "language"] = "language" + + # Whether torch.compile should generate symbolic-shape (dynamic) kernels + # (maps to ``torch.compile(dynamic=...)``). Defaults to True for training, + # which sees varying shapes across batches (sequence length, CP sharding, ...); + # specializing would recompile continuously. See ParallelismOverrides in + # packages/cosmos3/cosmos3/common/args.py for the inference-side rationale + # (where dynamic=False is preferred for stable AR shapes). + compile_dynamic: bool = True + + # Enable autotuning for pointwise/reduction Triton kernels (e.g. RMSNorm). + # Explores 6 candidate configs instead of the default 1, improving kernel performance + # at the cost of longer first-iteration compilation time. + max_autotune_pointwise: bool = False + + # Enable coordinate descent tuning after autotuning. Starts from the best autotuned + # config and explores nearby configs by adjusting one parameter at a time. + # Requires max_autotune_pointwise=True to have effect on reduction kernels. + coordinate_descent_tuning: bool = False + + # Whether to enable inference mode. + enable_inference_mode: bool = False + + # Number of ranks for sharding the model weights. + data_parallel_shard_degree: int = 1 + + # Number of ranks for context parallelism. + context_parallel_shard_degree: int = 1 + + # Number of ranks for CFG parallelism. + cfg_parallel_shard_degree: int = 1 + + # Precision for the model. + precision: str = "bfloat16" + + +@attrs.define(slots=False) +class LBLConfig: + # For load balancing loss computation. + # - "local": Use the fraction of tokens routed to each expert only for the local rank. + # - "global": Use the fraction of tokens routed to each expert across all ranks. + method: str = "local" + + # Coefficients for the load balancing loss. + # - "und": Coefficient for the load balancing loss for the "und" pathway. + # - "gen": Coefficient for the load balancing loss for the "gen" pathway. + coeff_und: float | None = None + coeff_gen: float | None = None + + +@attrs.define(slots=False) +class RectifiedFlowTrainingConfig: + shift: Any = 5 # Training time shift. If dict, maps resolution (str) to shift value (int) + use_dynamic_shift: bool = False # Whether to use dynamic shifting + train_time_image_distribution: str = "logitnormal" # Training time distribution for images + train_time_video_distribution: str = "logitnormal" # Training time distribution for videos + train_time_action_distribution: str = "logitnormal" # Training time distribution for actions + train_time_sound_distribution: str = "logitnormal" # Training time distribution for sound + train_time_weight: str = "uniform" # Training time weight + loss_scale: float = 1.0 # Loss scale + image_loss_scale: float | None = None # If set, overrides loss_scale for images + sound_loss_scale: float | None = None # If set, overrides loss_scale for sound + use_high_sigma_strategy: bool = False # Whether to use high sigma strategy + high_sigma_ratio: float = 0.05 # Ratio of using high sigmas + high_sigma_timesteps_min: int = 995 # Minimum timestep for high sigma + high_sigma_timesteps_max: int = 1000 # Maximum timestep for high sigma + use_discrete_rf: bool = False # Whether to use discrete formulation of rectified flow + + # user: please adjust this value according to loss_scale to balance the action loss with the video loss. + # default is 10.0 to align with previous training settings. + action_loss_weight: float = 10.0 + + # Independent noise schedule for action. When False (default), action shares the sigma + # sampled from the vision RF on every step — legacy behavior. When True, action samples + # its own sigma from `rectified_flow_action` using `shift_action` and + # `use_high_sigma_strategy_action`. Action always uses a shared scalar sigma per sample + # ([B,1]), independent of vision's DF mode. If action opts in to the high-sigma strategy, + # it reuses the global ratio / min / max. + independent_action_schedule: bool = False + shift_action: int | None = None # must be int; None → inherit `shift` (which must also be int) + use_high_sigma_strategy_action: bool = False + + # When True, per-instance flow-matching loss is normalized by the count of + # active (noisy) elements rather than all elements — preserves sum/active_count + # semantics so conditioning-heavy samples (e.g. I2V, forward_dynamics, diffusion + # forcing, AR rollout teacher-forcing) contribute gradient on par with K=0 + # samples. With .mean() the gradient of a K-conditioned sample is scaled by + # (T-K)/T, which undertrains the attend-to-clean-history dynamics. Kept + # False by default to preserve legacy loss magnitudes; enable for AR/DF training. + normalize_loss_by_active: bool = False + + +@attrs.define(slots=False) +class RectifiedFlowInferenceConfig: + scheduler_type: str = "unipc" # Scheduler type + num_train_timesteps: int = 1000 + shift: int = 1 + use_dynamic_shifting: bool = False + + +@attrs.define(slots=False) +class FixedStepSamplerConfig: + """Config for the fixed-step sampler used by distilled models. + + Uses a fixed sigma schedule instead of a smooth multi-step solver. + + Mirrors the constructor args of ``FixedStepSampler``. + """ + + # Discrete noise-level schedule (descending, excluding the final 0.0 step). + # Convention: exclude the final 0.0 step — FixedStepSampler appends it automatically. + # Values must be descending. Using 0.999 instead of 1.0 avoids numeric edge cases at sigma=1. + t_list: list[float] = [0.999, 0.75, 0.5, 0.25] + # Integrator type: "ode" (deterministic Euler) or "sde" (stochastic re-noising at each step). + sample_type: str = "ode" + + +# Don't have any defaults and init only in config file. +@attrs.define(slots=False) +class OmniMoTModelConfig(ModelConfig): + """ + Config for Omni MoT model. + """ + + tokenizer: LazyDict = None + net: LazyDict = None + ema: EMAConfig = EMAConfig() + parallelism: ParallelismConfig = ParallelismConfig() + + # Rectified flow configs + rectified_flow_training_config: RectifiedFlowTrainingConfig = RectifiedFlowTrainingConfig() + rectified_flow_inference_config: RectifiedFlowInferenceConfig = RectifiedFlowInferenceConfig() + + # Optional fixed-step sampler for distilled models (None for base models). + fixed_step_sampler_config: FixedStepSamplerConfig | None = None + + # Model configs + vlm_config: VLMConfig = VLMConfig() + diffusion_expert_config: DiffusionExpertConfig = DiffusionExpertConfig() + # Training data keys + input_video_key: str = "video" + input_image_key: str = "images" # key to fetch input image from data_batch + input_caption_key: str = "ai_caption" # Key used to fetch input captions + + # State and sequence shapes + state_ch: int = 16 # for latent model, ref to the latent channel number + state_t: int = 8 # for latent model, ref to the latent number of frames + latent_downsample_factor: int = 8 + resolution: str = "512" + max_num_tokens_after_packing: int = 13312 # Final num tokens after sequence packing + + # Attention implementation for joint understanding + generation + # Note "two_way" and "three_way" disallow and remove "End-of-Vision" or other text token in the generation tower. + # "three_way" must only be used when introducing sparsity + joint_attn_implementation: str = ( + "two_way" # "two_way", "three_way" or "flex" (NOTICE: We are planning to remove "flex" soon) + ) + + # Per-layer NATTEN parameters + # Must use "three_way" attention if used. + # If None, all attention layers remain dense. + # If not None, must be a list exactly the size of number of layers, and each layer can be either + # None (dense) or a dictionary, with at least 'kernel_size' or 'kernel_size_float' keys + # specifying sparsity. NATTEN parameters 'dilation' and 'stride' may also be specified either as + # static integers, or as floating point values that will be mapped to their domain during + # runtime. Integer parameters should never be mixed with floating point ones. + # + # Floating point parameters are highly recommended, unless the use case will have a fixed token + # layout (input resolution). + # + # Examples: + # Interleaved sliding window layers, "GPT-OSS"-style, with static window size: + # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size": (8, 8)}] + # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices + # will use a static sliding window size of 8x8. + # + # Interleaved sliding window layers, "GPT-OSS"-style, with input-dependent window size: + # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size_float": (0.5, 0.5)}] + # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices + # will use a dynamic window size that is 50% of the input along each of the two dimensions. + # + # Interleaved sliding window and dilated layers, "DiNAT"-style: + # natten_parameter_list = [ + # { + # "kernel_size_float": (0.5, 0.5), + # "dilation_float": (1.0, 1.0), + # } if layer_idx % 2 != 0 else { + # "kernel_size_float": (0.5, 0.5), + # } + # ] + # All layers will use a dynamic window size that is 50% of the input along each of the two + # dimensions. Layers with odd indices will also dilate to the maximum level possible. + # + natten_parameter_list: list | None = None + + # Temporal causality for training autoregressive video generation models. + # When enabled, applies temporal causal attention to generation supertokens. + # Each supertoken is num_action_tokens_per_supertoken action tokens followed + # by H*W vision tokens; the value is stamped onto the packed sequence by the + # temporal-causal packer and read by attention/KV-cache code unchanged. + # Only supports image2video modes (with or without actions). + # Requires joint_attn_implementation="three_way". + video_temporal_causal: bool = False + # "none": standard joint denoising (shared σ, no clean context) + # "teacher_forcing": all frames noised with shared σ; clean history via cross-attention + # "diffusion_forcing": each latent frame gets independent σ ~ Uniform[0,1] + # "teacher_forcing_dcm": replayed teacher-forcing discrete-time consistency distillation + causal_training_strategy: Literal["none", "teacher_forcing", "diffusion_forcing", "teacher_forcing_dcm"] = "none" + + # Load balancing loss config. + lbl: LBLConfig = LBLConfig() + + # vision configs + vision_gen: bool = True # whether to use vision related parameters and condition/generate vision tokens + + # action configs + action_gen: bool = False # whether to use action related parameters and condition/generate action tokens + max_action_dim: int = 32 # maximum dimension of the action space, we need to pad the data to this dimension. + num_embodiment_domains: int = 32 # number of domains for the domain-aware linear layer + + # sound configs + sound_gen: bool = False # whether to use sound related parameters and condition/generate sound tokens + sound_tokenizer: LazyDict | None = None # Sound tokenizer config (e.g., AVAE) + sound_dim: int | None = None # Sound latent channel size (e.g., 64 for AVAE 48kHz) + sound_latent_fps: int = 25 # Sound tokenizer's latent rate (e.g., 48kHz / 1920 hop = 25 Hz) + + log_enc_time_every_n: int = 100 # Frequency of logging encoding time to W&B + + def validate(self, root_config: Config) -> None: + """Skip pretrained loading if a training checkpoint exists. + + Mutates root_config.model.config.* directly because the receiver self + is a fresh attrs copy from OmegaConf.to_object and its writes would be + dropped. + """ + from cosmos3._src.imaginaire.utils import log + from cosmos3._src.vfm.checkpointer.dcp import DistributedCheckpointer + + # There are three cases to consider: + # 1. Model is being trained from scratch (using weights from Hugging Face). + # (both _read_latest_checkpoint_file() and load_path are None). + # In this case, we should load the understanding pathway weights from HF weights, + # Additionally, we must copy the understanding pathway weights to the generation + # pathway. + # + # 2. Model is being trained from a previous checkpoint. + # (_read_latest_checkpoint_file() is not None and load_path can be None or not). + # In this case, the model weights have been already loaded from DCP checkpoint + # (checkpointer/dcp.py). We must skip both loading understanding pathway weights, + # and copying the understanding pathway weights to the generation pathway. + + # 3. Model is being warm-started from a load_path (but no previous checkpoint exists). + # (_read_latest_checkpoint_file() is None and load_path is not None). + # In this case, the model weights have been already loaded from DCP checkpoint + # due to load_path being specified (checkpointer/dcp.py). However, we must still + # load the understanding weights from HF weights (since the understanding model + # may be moved from Qwen3-VL to Cosmos-Reason2 for example). We should not copy + # the understanding pathway weights to the generation pathway (since the generation + # pathway has already been pretrained using the previous model weights, for example, + # the Qwen3-VL weights). But the understanding weights are always kept unchanged. + + if not self.vlm_config.load_pretrained and not self.diffusion_expert_config.load_weights_from_pretrained: + # Neither if branch below is taken; no need to create checkpointer. + return + + checkpointer = DistributedCheckpointer( + root_config.checkpoint, root_config.job, callbacks=None, disable_async=True + ) + + if self.vlm_config.load_pretrained: + if checkpointer._read_latest_checkpoint_file() is not None: + log.info( + "Checkpoint found: disabling pretrained model loading to avoid double loading. " + "Model weights will be loaded from checkpoint instead of safetensors." + ) + root_config.model.config.vlm_config.load_pretrained = False + + if self.diffusion_expert_config.load_weights_from_pretrained: + if checkpointer.load_path is not None: + log.info( + "Load path found: disabling pretrained model loading for generation pathway. " + "Generation pathway weights will be loaded from load_path instead of safetensors." + ) + root_config.model.config.diffusion_expert_config.load_weights_from_pretrained = False + + +@attrs.define(slots=False) +class VLMModelConfig(ModelConfig): + """ + Config for VLM model. + """ + + policy: PolicyConfig = PolicyConfig() + train: TrainConfig = TrainConfig() diff --git a/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py b/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py new file mode 100644 index 00000000..76475082 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Hydra ConfigStore registration for multiview dataloaders. + +Registers named dataloader configs that can be referenced via Hydra overrides +(e.g. ``{override /data_train: video_control_mads_multiview_0823_gcs_720p_10fps_93frames_7views}``) +or used as templates for inline ``L(get_multiview_video_loader)(...)`` in +experiment configs. + +Two naming conventions: + + **Transfer** (with control signal): + ``video_control_{dataset}_{store}_{res}_{fps}_{frames}_{views}`` + + **Predict** (no control signal): + ``video_{dataset}_{store}_{res}_{fps}_{frames}_{views}`` +""" + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.datasets.multiview.multiview_data_source import ( + DEFAULT_CAMERAS, + INDEX_TO_CAMERA_MAPPING, + TRANSFER_CAPTION_KEY_MAPPING, + TRANSFER_CONTROL_KEY_MAPPING, + TRANSFER_VIDEO_KEY_MAPPING, +) +from cosmos3._src.vfm.datasets.multiview.multiview_dataset import ( + MultiviewAugmentationConfig, + get_multiview_video_loader, +) + +# --------------------------------------------------------------------------- +# Camera view subsets +# --------------------------------------------------------------------------- + +CAMERA_VIEW_CONFIGS: dict[str, tuple[str, ...]] = { + "7views": DEFAULT_CAMERAS, + "1view_front": ("camera_front_wide_120fov",), + "4views": ( + "camera_front_wide_120fov", + "camera_cross_right_120fov", + "camera_rear_tele_30fov", + "camera_cross_left_120fov", + ), +} + +# --------------------------------------------------------------------------- +# Grid dimensions +# --------------------------------------------------------------------------- + +_TRANSFER_DATASETS = ["mads_multiview_0823"] +_OBJECT_STORES = ["gcs"] + +_RESOLUTIONS: list[tuple[str, tuple[int, int]]] = [ + ("720p", (720, 1280)), +] + +_FPS: list[tuple[str, int]] = [ + ("10fps", 1), # MADS transfer data is already at 10 fps +] + +_NUM_VIDEO_FRAMES: list[tuple[str, int]] = [ + ("29frames", 29), + ("61frames", 61), + ("93frames", 93), +] + + +def register_multiview_dataloaders() -> None: + """Register all multiview dataloader configs with Hydra ConfigStore.""" + + cs = ConfigStore.instance() + + # ----- Transfer dataloaders (with control signals) ----- + for dataset in _TRANSFER_DATASETS: + for object_store in _OBJECT_STORES: + for resolution_str, resolution_hw in _RESOLUTIONS: + for fps_str, downsample_factor in _FPS: + for num_frames_str, num_frames in _NUM_VIDEO_FRAMES: + for views_str, camera_keys in CAMERA_VIEW_CONFIGS.items(): + name = ( + f"video_control_{dataset}_{object_store}_{resolution_str}_" + f"{fps_str}_{num_frames_str}_{views_str}" + ) + cs.store( + group="data_train", + package="dataloader_train", + name=name, + node=L(get_multiview_video_loader)( + dataset_name=dataset, + is_train=True, + augmentation_config=L(MultiviewAugmentationConfig)( + resolution_hw=resolution_hw, + fps_downsample_factor=downsample_factor, + num_video_frames=num_frames, + camera_keys=camera_keys, + camera_video_key_mapping=TRANSFER_VIDEO_KEY_MAPPING, + camera_caption_key_mapping=TRANSFER_CAPTION_KEY_MAPPING, + camera_control_key_mapping=TRANSFER_CONTROL_KEY_MAPPING, + position_to_camera_mapping=INDEX_TO_CAMERA_MAPPING, + single_caption_camera_name="camera_front_wide_120fov", + ), + ), + ) + + # ----- Predict dataloaders (no control signals, for future use) ----- + # These use named keys (video_camera_front_wide_120fov, etc.) and need + # different datasets (e.g. alpamayo_dec2024) with 30 fps native data. + # Uncomment and add predict datasets to the catalog when needed. + # + # _PREDICT_DATASETS = ["alpamayo_dec2024"] + # _PREDICT_FPS = [("10fps", 3), ("15fps", 2)] # 30 fps native → downsample + # for dataset in _PREDICT_DATASETS: + # for object_store in _OBJECT_STORES: + # for resolution_str, resolution_hw in _RESOLUTIONS: + # for fps_str, downsample_factor in _PREDICT_FPS: + # for num_frames_str, num_frames in _NUM_VIDEO_FRAMES: + # for views_str, camera_keys in CAMERA_VIEW_CONFIGS.items(): + # name = ( + # f"video_{dataset}_{object_store}_{resolution_str}_" + # f"{fps_str}_{num_frames_str}_{views_str}" + # ) + # cs.store( + # group="data_train", + # package="dataloader_train", + # name=name, + # node=L(get_multiview_video_loader)( + # dataset_name=dataset, + # is_train=True, + # augmentation_config=L(MultiviewAugmentationConfig)( + # resolution_hw=resolution_hw, + # fps_downsample_factor=downsample_factor, + # num_video_frames=num_frames, + # camera_keys=camera_keys, + # camera_video_key_mapping=PREDICT_VIDEO_KEY_MAPPING, + # camera_caption_key_mapping=PREDICT_CAPTION_KEY_MAPPING, + # camera_control_key_mapping=None, + # position_to_camera_mapping=None, + # single_caption_camera_name=None, + # ), + # ), + # ) + + +# Auto-register on import +register_multiview_dataloaders() diff --git a/cosmos3/_src/vfm/configs/base/defaults/optimizer.py b/cosmos3/_src/vfm/configs/base/defaults/optimizer.py new file mode 100644 index 00000000..6dedd47b --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/optimizer.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Copied from https://gitlab-master.nvidia.com/dir/imaginaire4/-/blob/d0921eb675d1251e73c4b19acdd78e6ad936ae3b/projects/cosmos/reason2/configs/base/defaults/optimizer.py without changes +""" + +from cosmos3._src.imaginaire.configs.lr_scheduler import LambdaLinearSchedulerConfig +from cosmos3._src.imaginaire.functional.lr_scheduler import LambdaWarmUpCosineScheduler +from cosmos3._src.imaginaire.lazy_config import PLACEHOLDER +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.utils.config_helper import ConfigStore +from cosmos3._src.vfm.utils.optimizer import build_optimizer + +optimizer_kwargs = dict( + # Learning rate for the optimizer. + lr=1e-4, + # Weight decay for the optimizer. + weight_decay=0.1, + # Beta1 and beta2 for the optimizer. + betas=[0.9, 0.99], + # Epsilon for the optimizer. + eps=1e-8, + # Whether to use fuse updates to all parameters. + fused=True, + # Keys to select for the optimizer. + keys_to_select=[], + # Per-key LR multipliers. Maps parameter name patterns to LR multipliers. + # E.g. {"sound2llm": 5.0, "llm2sound": 5.0} gives those params 5x the base LR. + lr_multipliers={}, + # Whether to disable weight decay for one-dimensional params such as norm weights and biases. + # Default is False to preserve historical optimizer behavior. + disable_weight_decay_for_1d_params=False, +) + + +def register_optimizer(): + cs = ConfigStore.instance() + cs.store( + group="optimizer", + package="optimizer", + name="fusedadamw", + node=L(build_optimizer)( + model=PLACEHOLDER, + optimizer_type="FusedAdam", + **optimizer_kwargs, + ), + ) + cs.store( + group="optimizer", + package="optimizer", + name="adamw", + node=L(build_optimizer)( + model=PLACEHOLDER, + optimizer_type="AdamW", + **optimizer_kwargs, + ), + ) + + +def register_scheduler(): + cs = ConfigStore.instance() + cs.store(group="scheduler", package="scheduler", name="lambdalinear", node=LambdaLinearSchedulerConfig) + # Cosine scheduler that works with any optimizer (including fusedadamw) + cs.store( + group="scheduler", + package="scheduler", + name="lambdacosine", + node=L(LambdaWarmUpCosineScheduler)( + warm_up_steps=[2000], + f_min=[0.0], + f_max=[1.0], + f_start=[0.0], + cycle_lengths=[100000], + verbosity_interval=0, + ), + ) diff --git a/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py b/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py new file mode 100644 index 00000000..a0f1bffe --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import PLACEHOLDER, LazyDict +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.tokenizers.audio.avae import AVAEInterface +from cosmos3._src.vfm.tokenizers.dc_ae.dc_ae_4x32x32 import DCAE4x32x32Interface +from cosmos3._src.vfm.tokenizers.flux_vae_8x8 import FluxVAEInterface +from cosmos3._src.vfm.tokenizers.uniae.noncausal_4x16x16 import UniAEVAEInterface +from cosmos3._src.vfm.tokenizers.wan2pt1_vae_4x8x8 import Wan2pt1VAEInterface +from cosmos3._src.vfm.tokenizers.wan2pt2_vae_4x16x16 import Wan2pt2VAEInterface + +PRETRAINED_TOKENIZER_WAN2PT1_VAE_PTH = "pretrained/tokenizers/video/wan2pt1/Wan2.1_VAE.pth" +PRETRAINED_TOKENIZER_WAN2PT2_VAE_PTH = "pretrained/tokenizers/video/wan2pt2/Wan2.2_VAE.pth" +PRETRAINED_TOKENIZER_FLUX_VAE_PTH = "pretrained/tokenizers/image/flux/ae.safetensors" + +# UniAE checkpoint paths +PRETRAINED_TOKENIZER_UNIAE_4X16X16_C48_T8TO24_64TO512P_FPS_ALL_ENCODER_NONCAUSAL_DECODER_NONCAUSAL_NOGAN_BEST_S1_VAE_PTH = "pretrained/tokenizers/video/cosmos/uniae4x16x16_c48_t8to24_64to512p_fps_all_encoder_noncausal_decoder_noncausal_nogan_best_s1.pt" + +# DCAE checkpoint paths +PRETRAINED_TOKENIZER_DCAE_PTH = "pretrained/tokenizers/video/cosmos/dc-ae-v-1.0-f32t4c64-cosmos-encoder-causal-decoder-chunk-causal-4-frame-120-pad-7-no-gan.pt" +PRETRAINED_TOKENIZER_DCAE_4X32X32_C64_T120_256P_FPS_ALL_ENCODER_CAUSAL_DECODER_CHUNKCAUSAL4_NOGAN_COSMOS_PAD_7_V0PT2_PTH = "pretrained/tokenizers/video/cosmos/dcae4x32x32_c64_t120_256p_fps_all_encoder_causal_decoder_chunk_causal_4_nogan_cosmos_pad_7_v0.2.pt" + +# AVAE (Audio VAE) checkpoint paths +PRETRAINED_TOKENIZER_AVAE_PTH = "pretrained/tokenizers/audio/avae/model_unwrap.ckpt" +PRETRAINED_TOKENIZER_AVAE_44K_NONCAUSAL = "pretrained/tokenizers/audio/avae/avae_44k_noncausal_21hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_44K_CAUSAL = "pretrained/tokenizers/audio/avae/avae_44k_causal_21hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_48K_25HZ = "pretrained/tokenizers/audio/avae/avae_48k_noncausal_25hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_48K_6HZ = "pretrained/tokenizers/audio/avae/avae_48k_noncausal_6hz_64ch.ckpt" + + +# Flux tokenizer config +FluxVAEConfig: LazyDict = L(FluxVAEInterface)( + # This is the flux image tokenizer. + # We use it for bagel inference. + # We do not use it for Cosmos3. + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_FLUX_VAE_PTH, + chunk_duration=1, + spatial_compression_factor=8, + temporal_compression_factor=1, +) + +Wan2pt1VAEConfig: LazyDict = L(Wan2pt1VAEInterface)( + # 4x8x8 tokenizer + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_WAN2PT1_VAE_PTH, + spatial_compression_factor=8, + temporal_compression_factor=4, +) + +Wan2pt2VAEConfig: LazyDict = L(Wan2pt2VAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_WAN2PT2_VAE_PTH, + spatial_compression_factor=16, + temporal_compression_factor=4, +) + +DCAE4x32x32Config: LazyDict = L(DCAE4x32x32Interface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_DCAE_PTH, + spatial_compression_factor=32, + temporal_compression_factor=4, +) + +DCAE4x32x32C64T120_256pFpsAllEncoderCausalDecoderChunkCausal4NoganCosmosPad7V0pt2Config: LazyDict = L( + DCAE4x32x32Interface +)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_DCAE_4X32X32_C64_T120_256P_FPS_ALL_ENCODER_CAUSAL_DECODER_CHUNKCAUSAL4_NOGAN_COSMOS_PAD_7_V0PT2_PTH, + model_name="dcae4x32x32_c64_t120_256p_fps_all_encoder_causal_decoder_chunk_causal_4_nogan_cosmos_pad_7_v0.2", + spatial_compression_factor=32, + temporal_compression_factor=4, +) + +UniAE4x16x16C48T8to24_64to512pFpsAllEncoderNoncausalDecoderNoncausalNoganBestS1Config: LazyDict = L(UniAEVAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_UNIAE_4X16X16_C48_T8TO24_64TO512P_FPS_ALL_ENCODER_NONCAUSAL_DECODER_NONCAUSAL_NOGAN_BEST_S1_VAE_PTH, + spatial_compression_factor=16, + temporal_compression_factor=4, +) + +# ============================================================================= +# AVAE (Audio VAE) Tokenizer Configs +# ============================================================================= + +# Legacy config with tanh companding (non-commercial use only) +# Latent rate: 44100 / 2048 = 21.53Hz +AVAETokenizerConfig: LazyDict = L(AVAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + avae_path=PRETRAINED_TOKENIZER_AVAE_PTH, + sample_rate=44100, + audio_channels=2, + io_channels=64, + hop_size=2048, + normalization_type="tanh", + tanh_input_scale=1.0, + tanh_output_scale=3.0, +) + + +# 44.1kHz Non-causal (PRIMARY - used for V2A/T2A training) +# Latent rate: 44100 / 2048 = 21.53Hz +AVAE_44k_NoncausalConfig: LazyDict = L(AVAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + avae_path=PRETRAINED_TOKENIZER_AVAE_44K_NONCAUSAL, + sample_rate=44100, + audio_channels=2, + io_channels=64, + hop_size=2048, + normalize_latents=True, + tanh_input_scale=1.5, + tanh_output_scale=3.5, +) + +# 48kHz 25Hz (higher quality audio) +# Latent rate: 48000 / 1920 = 25Hz +AVAE_48k_25hzConfig: LazyDict = L(AVAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + avae_path=PRETRAINED_TOKENIZER_AVAE_48K_25HZ, + sample_rate=48000, + audio_channels=2, + io_channels=64, + hop_size=1920, + normalize_latents=True, + tanh_input_scale=1.5, + tanh_output_scale=3.5, +) + + +def register_tokenizer(): + cs = ConfigStore.instance() + + # Wan2pt1 and Wan2pt2 tokenizers + cs.store(group="tokenizer", package="model.config.tokenizer", name="wan2pt1_tokenizer", node=Wan2pt1VAEConfig) + cs.store(group="tokenizer", package="model.config.tokenizer", name="wan2pt2_tokenizer", node=Wan2pt2VAEConfig) + # UniAE tokenizer + cs.store( + group="tokenizer", + package="model.config.tokenizer", + name="uniae_4x16x16_c48_t8to24_64to512p_fps_all_encoder_noncausal_decoder_noncausal_nogan_best_s1_tokenizer", + node=UniAE4x16x16C48T8to24_64to512pFpsAllEncoderNoncausalDecoderNoncausalNoganBestS1Config, + ) + # Flux tokenizer + cs.store(group="tokenizer", package="model.config.tokenizer", name="flux_tokenizer", node=FluxVAEConfig) + # DC AE 4x32x32 tokenizer + cs.store( + group="tokenizer", + package="model.config.tokenizer", + name="dc_ae_4x32x32_tokenizer", + node=DCAE4x32x32Config, + ) + cs.store( + group="tokenizer", + package="model.config.tokenizer", + name="dc_ae_4x32x32_c64_t120_256p_fps_all_encoder_causal_decoder_chunk_causal_4_nogan_cosmos_pad_7_v0.2_tokenizer", + node=DCAE4x32x32C64T120_256pFpsAllEncoderCausalDecoderChunkCausal4NoganCosmosPad7V0pt2Config, + ) + + +def register_sound_tokenizer(): + """Register sound tokenizers in Hydra ConfigStore under model.config.sound_tokenizer.""" + cs = ConfigStore.instance() + cs.store( + group="sound_tokenizer", package="model.config.sound_tokenizer", name="avae_48k_25hz", node=AVAE_48k_25hzConfig + ) + cs.store( + group="sound_tokenizer", + package="model.config.sound_tokenizer", + name="avae_44k_noncausal", + node=AVAE_44k_NoncausalConfig, + ) + cs.store( + group="sound_tokenizer", package="model.config.sound_tokenizer", name="avae_tokenizer", node=AVAETokenizerConfig + ) diff --git a/cosmos3/_src/vfm/configs/base/defaults/unittest.py b/cosmos3/_src/vfm/configs/base/defaults/unittest.py new file mode 100644 index 00000000..ed2b5333 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/unittest.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import attrs + +# from cosmos3._src.vfm.configs.base.defaults.cluster import GCPIADGB200Config + +# We are hardcoding the unittest assets in this file. + +# CLUSTER_CONFIG = GCPIADGB200Config + +# add codeowner for cosmos3/_src/vfm/tokenizers + + +@attrs.define(slots=False) +class SwfitStackPDXrConfig: + """ + Config for the cluster specific information. + Everything cluster specific should be here. + """ + + object_store_bucket_data: str + object_store_credential_data: str + + +UNITTEST_CONFIG = SwfitStackPDXrConfig( + object_store_bucket_data="unittest", + object_store_credential_data="credentials/pdx_dir.secret", +) + +TOKENIZER_RECONSTRUCTION_VIDEO_PATH = "tokenizer/video/panda70m_test_0000039_00000.mp4" +AVAE_RECONSTRUCTION_AUDIO_PATH = "tokenizer/audio/test_audio.wav" diff --git a/cosmos3/_src/vfm/configs/base/defaults/vlm.py b/cosmos3/_src/vfm/configs/base/defaults/vlm.py new file mode 100644 index 00000000..7a73cfcd --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/defaults/vlm.py @@ -0,0 +1,977 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Configs for VLM / LLM models + +import json +import os + +import attrs +import torch.distributed as dist +from transformers import PreTrainedTokenizerFast + +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.imaginaire.lazy_config import instantiate as lazy_instantiate +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.config_helper import ConfigStore +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.models.mot.unified_mot import ( + Nemotron3DenseVLTextConfig, + Nemotron3DenseVLTextForCausalLM, + Qwen3VLMoeTextConfig, + Qwen3VLMoeTextForCausalLM, + Qwen3VLTextConfig, + Qwen3VLTextForCausalLM, +) +from cosmos3._src.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer + + +def create_vlm_config(base_config: LazyDict, **overrides): + vlm_config = lazy_instantiate(base_config) + for key, value in overrides.items(): + setattr(vlm_config, key, value) + return vlm_config + + +def get_rank_safe() -> int: + if dist.is_available() and dist.is_initialized(): + return dist.get_rank() + return 0 # default to rank 0 when not in distributed mode + + +################################################################################ +# Download tokenizer files from s3 +# Download to ~/.cache/imaginaire4/tokenizer_files/{model_name} and then load from there. +def download_tokenizer_files(model_name: str, config_variant: str) -> str: + if config_variant == "hf": + return model_name + + if config_variant == "s3": + ckpt_bucket = "bucket" + credentials = "credentials/s3_checkpoint.secret" + elif config_variant == "gcp": + ckpt_bucket = "bucket" + credentials = "credentials/gcp_checkpoint.secret" + else: + raise ValueError(f"Invalid config variant: {config_variant}") + + model_path = f"s3://{ckpt_bucket}/cosmos3/pretrained/huggingface/{model_name}" + if not INTERNAL: + from cosmos3._src.imaginaire.utils.checkpoint_db import download_checkpoint_v2 + + model_path = download_checkpoint_v2(model_path) + if "://" not in model_path: + return model_path + + imaginaire_cache_dir = os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire4")) + destination_dir = os.path.join(imaginaire_cache_dir, f"tokenizer_files/{model_name}/rank_{get_rank_safe()}") + s3_backend_args = { + "backend": "s3", + "s3_credential_path": credentials, + } + + extensions = ["json", "txt", "jinja"] + for extension in extensions: + for file_path in easy_io.list_dir_or_file( + model_path, + list_dir=False, + list_file=True, + suffix=extension, + recursive=False, + backend_args=s3_backend_args, + ): + full_path = easy_io.join_path(model_path, file_path, backend_args=s3_backend_args) + local_path = f"{destination_dir}/{file_path}" + if os.path.exists(local_path): + log.debug(f"Skipping already downloaded tokenizer file: {local_path}") + continue + log.info(f"Downloading tokenizer file: {full_path} to {local_path}, cwd: {os.getcwd()}") + # Download the file + file_data = easy_io.get(full_path, backend_args=s3_backend_args) + easy_io.put(file_data, local_path) + return destination_dir + + +def create_qwen2_tokenizer_with_download(pretrained_model_name: str, config_variant: str): + destination_dir = download_tokenizer_files(pretrained_model_name, config_variant) + return Qwen2Tokenizer.from_pretrained(destination_dir) + + +@attrs.define(slots=False) +class VLMConfig: + # Name of the huggingface model + model_name: str = "" + + # Langugage model class to instantiate + model_instance: LazyDict | None = None + + # Tokenizer class to instantiate + tokenizer: LazyDict | None = None + + # Path to the checkpoint + checkpoint_path: str = "" + + # Path to the credential file + credential_path: str = "" # Path to the credential file + + # Whether to enable GCS patch in boto3 for DCP loading from GCS + enable_gcs_patch_in_boto3: bool = False + + # Whether to load the pretrained LLM / VLM + load_pretrained: bool = True + + # Layer module to use. We override the decoder layer in huggingface model with this class. + # This is needed as we need to initialize MoT layers. + layer_module: str = "Qwen2MoTDecoderLayer" + + # Whether to use QK normalization for text expert + qk_norm_for_text: bool = False + + # Whether to use QK normalization for diffusion expert + qk_norm_for_diffusion: bool = True # Whether to use QK normalization for diffusion expert + + # If True, use the same word embedding matrices for input and outut embedding layers. + tie_word_embeddings: bool = False + + # Whether to prepend a system prompt during text tokenization. + # Checkpoints trained with system prompt enabled require this to be True at inference time. + use_system_prompt: bool = False + + # If set, forces safetensors weight remapping ("qwen3" vs "nemotron_3_dense_vl"/"nemotron_3_llm"). None = auto-detect. + vlm_checkpoint_format: str | None = None + + +def create_nemotron_tokenizer_with_download(pretrained_model_name: str, config_variant: str) -> PreTrainedTokenizerFast: + """Load Nemotron Fast BPE tokenizer, downloading files from S3/GCP if needed.""" + destination_dir = download_tokenizer_files(pretrained_model_name, config_variant) + return PreTrainedTokenizerFast.from_pretrained(destination_dir, trust_remote_code=True) + + +def _patch_nemotron_llm_tokenizer_vision_tokens(destination_dir: str) -> None: + """Remap reserved placeholder tokens to vision special tokens in-place. + + The Nemotron LLM tokenizer reserves ```` / ```` + at IDs 20/21 -- the same slots the VLM tokenizer uses for + ``<|vision_start|>`` / ``<|vision_end|>``. Renaming them here keeps + every vision-token ID inside the original vocab_size (131072) so no + embedding-layer resize is needed during FSDP training. + """ + remap = {"": "<|vision_start|>", "": "<|vision_end|>"} + + tokenizer_json_path = os.path.join(destination_dir, "tokenizer.json") + if os.path.exists(tokenizer_json_path): + with open(tokenizer_json_path) as f: + data = json.load(f) + for entry in data.get("added_tokens", []): + if entry["content"] in remap: + entry["content"] = remap[entry["content"]] + vocab = data.get("model", {}).get("vocab", {}) + for old_name, new_name in remap.items(): + if old_name in vocab: + vocab[new_name] = vocab.pop(old_name) + with open(tokenizer_json_path, "w") as f: + json.dump(data, f) + + tokenizer_config_path = os.path.join(destination_dir, "tokenizer_config.json") + if os.path.exists(tokenizer_config_path): + with open(tokenizer_config_path) as f: + tc_data = json.load(f) + for entry in tc_data.get("added_tokens_decoder", {}).values(): + if entry.get("content") in remap: + entry["content"] = remap[entry["content"]] + with open(tokenizer_config_path, "w") as f: + json.dump(tc_data, f) + + +def create_nemotron_llm_tokenizer_with_download( + pretrained_model_name: str, config_variant: str +) -> PreTrainedTokenizerFast: + """Load Nemotron pure-LLM tokenizer with vision token slots pre-mapped. + + Unlike the VLM tokenizer which already contains ``<|vision_start|>`` + and ``<|vision_end|>``, the LLM tokenizer has generic placeholders at + those IDs. This function renames them so that ``add_special_tokens`` + resolves them to IDs 20/21 (within vocab_size) instead of appending + new entries beyond the embedding table. + """ + destination_dir = download_tokenizer_files(pretrained_model_name, config_variant) + _patch_nemotron_llm_tokenizer_vision_tokens(destination_dir) + return PreTrainedTokenizerFast.from_pretrained(destination_dir, trust_remote_code=True) + + +# Configs for LLM models +Qwen3MoT_LLM_0p6b_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-0.6B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(Qwen2Tokenizer.from_pretrained)( + pretrained_model_name_or_path="Qwen/Qwen3-0.6B", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-0.6B/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3MoT_LLM_0p6b_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-0.6B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-0.6B", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-0.6B/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, +) + +Nemotron3_LLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/NVIDIA-Nemotron-3-2B-BF16", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(create_nemotron_llm_tokenizer_with_download)( + pretrained_model_name="Nemotron/NVIDIA-Nemotron-3-2B-BF16", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Nemotron/NVIDIA-Nemotron-3-2B-BF16/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_llm", +) + +# Configs for VL instruct models + +# Config for Qwen3VL 30B A3B Instruct model +# Qwen3VLMoE uses Qwen2Tokenizer +Qwen3VLMoT_VLM_30b_a3b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-30B-A3B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + + +Qwen3VLMoT_VLM_30b_a3b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-30B-A3B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_30b_a3b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-30B-A3B-Private", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-30B-A3B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 235B A22B Instruct model +# Qwen3VLMoE uses Qwen2Tokenizer +Qwen3VLMoT_VLM_235b_a22b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-235B-A22B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-235B-A22B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-235B-A22B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + + +# Config for Qwen3VL 2B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_2b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-2B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_2b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-2B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Qwen3VLMoT_VLM_2b_Instruct_HF_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="hf", + ), + load_pretrained=True, +) + +Nemotron3DenseVL_VLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Nemotron-3-Dense-VL-2B-BF16-Alignment", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(create_nemotron_tokenizer_with_download)( + pretrained_model_name="Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_dense_vl", +) + +Cosmos3Reasoner_Nemotron_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-2B-Private", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(create_nemotron_tokenizer_with_download)( + pretrained_model_name="Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/nvidia/Cosmos3-Reasoner-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_dense_vl", +) + +CosmosReason2_VLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-2B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-2B/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-2B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-2B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 4B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_4b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-4B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-4B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-4B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_4b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-4B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-4B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-4B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 8B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_8b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-8B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-8B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_8b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-8B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_8b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-8B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-8B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_8b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-8B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-8B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 32B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_32b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-32B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-32B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-32B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_32b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-32B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-32B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_32b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-32B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-32B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_32b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-32B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://bucket/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-32B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + + +def register_vlm(): + cs = ConfigStore.instance() + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_mot_0p6b", + node=Qwen3MoT_LLM_0p6b_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_mot_0p6b_gcp", + node=Qwen3MoT_LLM_0p6b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="nemotron_3_llm_2b_gcp", + node=Nemotron3_LLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_30b_a3b_instruct", + node=Qwen3VLMoT_VLM_30b_a3b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_30b_a3b_instruct_gcp", + node=Qwen3VLMoT_VLM_30b_a3b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_235b_a22b_instruct_gcp", + node=Qwen3VLMoT_VLM_235b_a22b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct", + node=Qwen3VLMoT_VLM_2b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct_gcp", + node=Qwen3VLMoT_VLM_2b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct_hf", + node=Qwen3VLMoT_VLM_2b_Instruct_HF_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="nemotron_3_dense_vl_2b_gcp", + node=Nemotron3DenseVL_VLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_nemotron_vlm_2b_private_gcp", + node=Cosmos3Reasoner_Nemotron_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_2b_gcp", + node=CosmosReason2_VLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_2b_private_gcp", + node=CosmosReason2_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_2b_private_gcp", + node=Cosmos3Reasoner_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_8b_private_gcp", + node=CosmosReason2_VLM_8b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_8b_private_gcp", + node=Cosmos3Reasoner_VLM_8b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_32b_private_gcp", + node=CosmosReason2_VLM_32b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_32b_private_gcp", + node=Cosmos3Reasoner_VLM_32b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_30b_a3b_private_gcp", + node=CosmosReason2_VLM_30b_a3b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_4b_instruct", + node=Qwen3VLMoT_VLM_4b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_4b_instruct_gcp", + node=Qwen3VLMoT_VLM_4b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_8b_instruct", + node=Qwen3VLMoT_VLM_8b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_8b_instruct_gcp", + node=Qwen3VLMoT_VLM_8b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_32b_instruct", + node=Qwen3VLMoT_VLM_32b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_32b_instruct_gcp", + node=Qwen3VLMoT_VLM_32b_Instruct_GCP_Config, + ) diff --git a/cosmos3/_src/vfm/configs/base/vlm/__init__.py b/cosmos3/_src/vfm/configs/base/vlm/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/vlm/config.py b/cosmos3/_src/vfm/configs/base/vlm/config.py new file mode 100644 index 00000000..2aa451f9 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/config.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.config_helper import import_all_modules_from_package +from cosmos3._src.vfm.configs.base.vlm.defaults.callbacks import register_callbacks +from cosmos3._src.vfm.configs.base.vlm.defaults.checkpointer import register_checkpoint, register_ckpt_type +from cosmos3._src.vfm.configs.base.vlm.defaults.config import Config +from cosmos3._src.vfm.configs.base.vlm.defaults.dataloader import register_data_debug +from cosmos3._src.vfm.configs.base.vlm.defaults.dataloader_weighted_url import ( + register_data_recipe, + register_data_weighted_url, + register_data_weighted_url_with_text, +) +from cosmos3._src.vfm.configs.base.vlm.defaults.model import register_model +from cosmos3._src.vfm.configs.base.vlm.defaults.optimizer import register_optimizer, register_scheduler +from cosmos3._src.vfm.configs.base.vlm.defaults.vlm_policy import register_vlm_policy + + +def make_config() -> Config: + c = Config( + model=None, + optimizer=None, + scheduler=None, + dataloader_train=None, + dataloader_val=None, + ) + + # Specifying values through instances of attrs + c.job.project = "cosmos_reason2" + c.job.group = "debug" + c.job.name = "delete_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + # Unified path: ImaginaireTrainer drives both VLM and VFM. + c.trainer.type = ImaginaireTrainer + c.trainer.straggler_detection.enabled = False + c.trainer.max_iter = 400_000 + c.trainer.logging_iter = 20 + c.trainer.validation_iter = 100 + c.trainer.run_validation = False + c.trainer.callbacks = None + c.trainer.cudnn.benchmark = False + c.upload_reproducible_setup = True + + # Call this function to register config groups for advanced overriding. the order follows the default config groups + register_model() + register_vlm_policy() + # Register dataloader configs + register_data_weighted_url() + register_data_recipe() + register_data_weighted_url_with_text() + register_data_debug() + log.info("Registering optimizer, scheduler, checkpoint, ckpt type, and callbacks") + register_optimizer() + register_scheduler() + register_checkpoint() + register_ckpt_type() + register_callbacks() + import_all_modules_from_package("cosmos3._src.vfm.configs.base.vlm.experiment", reload=True) + return c diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py new file mode 100644 index 00000000..7fe35e15 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dataloader config options. +Based on projects/cosmos/ar/v1/configs/registry.py +""" + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.callbacks.manual_gc import ManualGarbageCollection +from cosmos3._src.imaginaire.lazy_config import PLACEHOLDER +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.utils.callback import WandBCallback +from cosmos3._src.vfm.callbacks.dataloader_state import DataLoaderStateCallback +from cosmos3._src.vfm.callbacks.dataloading_monitor import DetailedDataLoadingSpeedMonitor +from cosmos3._src.vfm.callbacks.hf_export import HFExportCallback +from cosmos3._src.vfm.callbacks.vlm.grad_clip import GradClip +from cosmos3._src.vfm.configs.base.defaults.callbacks import JOB_MONITOR_CALLBACKS +from projects.cosmos3.vlm.callbacks.data_stats import DataStatsCallback +from projects.cosmos3.vlm.callbacks.iter_speed import IterSpeed +from projects.cosmos3.vlm.callbacks.log_tensor_shape import LogTensorShapeCallback +from projects.cosmos3.vlm.callbacks.low_precision import LowPrecisionCallback +from projects.cosmos3.vlm.callbacks.param_count import ParamCount +from projects.cosmos3.vlm.callbacks.wandb_log import WandbCallback as WandBCallbackMultiplier +from projects.cosmos3.vlm.callbacks.wandb_log_eval import WandbCallback as WandBCallbackEval +from projects.cosmos3.vlm.callbacks.wandb_log_simgple import WandbCallback as WandBCallbackMultiplierSimple +from projects.cosmos3.vlm.callbacks.wandb_vis import VisualizationLoggingCallback + +# from cosmos3._src.imaginaire.utils.callback import NVTXCallback + + +def register_callbacks(): + cs = ConfigStore.instance() + BASIC_CALLBACKS = dict( + iter_speed=L(IterSpeed)( # does not use model or optimizer + every_n="${trainer.logging_iter}", + save_s3="${upload_reproducible_setup}", + save_s3_every_log_n=500, + hit_thres=50, + ), + manual_gc=L(ManualGarbageCollection)(every_n=5), # does not use model or optimizer + wandb=L(WandBCallback)(), + param_count=L(ParamCount)( # use model + save_s3="${upload_reproducible_setup}", + ), + dataloader_speed=L(DetailedDataLoadingSpeedMonitor)( + every_n=100, + save_s3="${upload_reproducible_setup}", + ), + grad_clip=L(GradClip)(clip_norm=1.0), # use model + low_precision=L(LowPrecisionCallback)( + update_iter=1, + config=PLACEHOLDER, + trainer=PLACEHOLDER, + param_torch_dtype="${model.config.policy.param_torch_dtype}", + ), # use model + + # nvtx=L(NVTXCallback)(synchronize=True), + ) + + PER_DATASET_PERN_CALLBACKS = dict( + wandb_10x=L(WandBCallbackMultiplier)( + logging_iter_multipler=10, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_2x=L(WandBCallbackMultiplier)( + logging_iter_multipler=2, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + data_stats=L(DataStatsCallback)( + logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_val=L(WandBCallbackEval)( + save_s3="${upload_reproducible_setup}", + ), + ) + + SIMPLE_LOG_CALLBACKS = dict( + wandb_10x=L(WandBCallbackMultiplierSimple)( + logging_iter_multipler=10, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_2x=L(WandBCallbackMultiplierSimple)( + logging_iter_multipler=2, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + log_tensor_shape=L(LogTensorShapeCallback)(num_log=10), + dataloader_state=L(DataLoaderStateCallback)( + distributor_type="${data_setting.distributor_type}", + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="basic_vlm", node=BASIC_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="per_dataset", node=PER_DATASET_PERN_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="simple_log", node=SIMPLE_LOG_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="job_monitor", node=JOB_MONITOR_CALLBACKS) + + DATA_VIS_CALLBACKS_QWEN = dict( + wandb_vis=L(VisualizationLoggingCallback)( + every_n=500, + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="data_vis_qwen", node=DATA_VIS_CALLBACKS_QWEN) + + HF_EXPORT_CALLBACKS = dict( + hf_export=L(HFExportCallback)( + dtype="${model.config.train.param_dtype}", + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="hf_export", node=HF_EXPORT_CALLBACKS) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py new file mode 100644 index 00000000..1285c583 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire import config +from cosmos3._src.imaginaire.checkpointer.dummy import Checkpointer as DummyCheckpointer +from cosmos3._src.imaginaire.config import CheckpointConfig +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.checkpointer.dcp import DistributedCheckpointer + +local_object_store = config.ObjectStoreConfig( + enabled=False, +) + +pdx_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/pdx_vfm_checkpoint.secret", + bucket="checkpoints", +) + +s3_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training.secret", + bucket="bucket", +) + +s3_eu_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training_eu.secret", + bucket="bucket", +) + +CHECKPOINT_LOCAL = CheckpointConfig( + save_to_object_store=local_object_store, + load_from_object_store=local_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_PDX = CheckpointConfig( + save_to_object_store=pdx_object_store, + load_from_object_store=pdx_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_S3 = CheckpointConfig( + save_to_object_store=s3_object_store, + load_from_object_store=s3_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + + +def register_checkpoint() -> None: + cs = ConfigStore.instance() + cs.store(group="checkpoint", package="checkpoint", name="local", node=CHECKPOINT_LOCAL) + cs.store(group="checkpoint", package="checkpoint", name="pdx", node=CHECKPOINT_PDX) + cs.store(group="checkpoint", package="checkpoint", name="s3", node=CHECKPOINT_S3) + + +DUMMY_CHECKPOINTER: Dict[str, str] = L(DummyCheckpointer)() +DISTRIBUTED_CHECKPOINTER: Dict[str, str] = L(DistributedCheckpointer)() + + +def register_ckpt_type() -> None: + cs = ConfigStore.instance() + cs.store(group="ckpt_type", package="checkpoint.type", name="dummy", node=DUMMY_CHECKPOINTER) + cs.store(group="ckpt_type", package="checkpoint.type", name="dcp", node=DISTRIBUTED_CHECKPOINTER) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py new file mode 100644 index 00000000..387382e5 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, List, Literal + +import attrs + +from cosmos3._src.imaginaire import config +from cosmos3._src.vfm.configs.base.vlm.defaults.training import PolicyConfig, TrainConfig + + +@attrs.define(slots=False) +class DataSetting: + """Configuration for data. + + Attributes: + qwen_max_video_token_length: Maximum video token length. + qwen_target_fps: Target fps for video sampling. + text_chat_order: Order of text items in user messages. + distributor_type: "with_replace" (WeightedShardlistBasic) or "no_replace" (NoReplaceShardlistBasic). + distributor_seed: Seed for the distributor. + """ + + qwen_max_video_token_length: int = 8192 + qwen_max_image_token_length: int = 8192 + qwen_target_fps: float = 4.0 + text_chat_order: Literal["text_end", "text_start", "random"] = "text_end" + temporal_localization_output_format: Literal[ + "dense_video_caption", "temporal_localization", "temporal_caption", "random" + ] = "random" + temporal_localization_fps: float = 1.0 + # For packed dataset + max_batch_size: int = 1 + max_tokens: int = 16000 + distributor_type: Literal["with_replace", "no_replace"] = "with_replace" + distributor_seed: int = 1993 + webdataset_detshuffle: bool = False + num_data_workers: int = 8 + data_prefetch_factor: int = 1 + val_split_ratio: float = 0.0 + + +@attrs.define(slots=False) +class Config(config.Config): + train: TrainConfig = TrainConfig() + policy: PolicyConfig = PolicyConfig() + data_setting: DataSetting = DataSetting() + defaults: List[Any] = attrs.field( + factory=lambda: [ + "_self_", + {"model": "vlm_fsdp"}, + {"vlm_policy": None}, + {"data_train": None}, + {"data_val": None}, + {"optimizer": "fusedadamw"}, + {"scheduler": "warmup_cosine_lr"}, + {"checkpoint": "s3"}, + {"ckpt_type": "dcp"}, + {"callbacks": ["basic_vlm"]}, + {"experiment": None}, + ] + ) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py new file mode 100644 index 00000000..06fb1c94 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from torch.utils.data import DataLoader + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.utils.config_helper import ConfigStore +from cosmos3._src.vfm.processors import build_processor +from projects.cosmos3.vlm.datasets.collate_fn import custom_collate +from projects.cosmos3.vlm.datasets.debug_data_qwen import DebugQwenDataset +from projects.cosmos3.vlm.datasets.dummy_data_qwen import DummyQwenDataset + + +# Debug dataset +def create_debug_dataloader_config_qwen( + num_images, loss_on_completion_only: bool = True, use_dummy_image: bool = False +): + return L(DataLoader)( + dataset=L(DebugQwenDataset)( + tokenizer=L(build_processor)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", + ), + num_images=num_images, + seq_len="${model.config.policy.model_max_length}", + image_token_len="${model.config.policy.qwen_max_video_token_length}", + # use_dummy_image=use_dummy_image, + ), + num_workers=8, + prefetch_factor=4, + batch_size=1, + sampler=None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ) + + +def create_dummy_dataloader_config_qwen(): + return L(DataLoader)( + dataset=L(DummyQwenDataset)( + tokenizer=L(build_processor)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", + ), + num_visual_tokens="${model.config.policy.qwen_max_video_token_length}", + total_tokens="${model.config.policy.model_max_length}", + batch_size="${dataloader_train.batch_size}", + ), + num_workers=8, + prefetch_factor=4, + batch_size=1, + sampler=None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ) + + +def register_data_debug(): + cs = ConfigStore.instance() + for split in ["train", "val"]: + cs.store( + group=f"data_{split}", + package=f"dataloader_{split}", + name="debug_image_data_qwen", # This data is from pixtral model output, expected to have low loss ~1.4 + node=create_debug_dataloader_config_qwen(1), + ) + cs.store( + group=f"data_{split}", + package=f"dataloader_{split}", + name="dummy_image_data_qwen", + node=create_dummy_dataloader_config_qwen(), + ) + + +def register_data(): + register_data_debug() diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py new file mode 100644 index 00000000..c2c54cbd --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py @@ -0,0 +1,570 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.datasets.vlm.joint_dataset_dynamic_batch_webloader import ( + JointDatasetDynamicBatchingWebLoader, +) +from cosmos3._src.vfm.processors import build_processor +from projects.cosmos3.vlm.datasets.augmentors.bytes_to_media import BytesToMedia +from projects.cosmos3.vlm.datasets.augmentors.filter_output_key import FilterOutputKey +from projects.cosmos3.vlm.datasets.augmentors.filter_seq_length import FilterSeqLength +from projects.cosmos3.vlm.datasets.augmentors.floating_number_format import FloatingNumberFormat +from projects.cosmos3.vlm.datasets.augmentors.format_describe_anything import FormatDescribeAnything +from projects.cosmos3.vlm.datasets.augmentors.prompt_format import PromptFormat +from projects.cosmos3.vlm.datasets.augmentors.shuffle_text_media_order import ShuffleTextMediaOrder +from projects.cosmos3.vlm.datasets.augmentors.timestamp import TimeStamp +from projects.cosmos3.vlm.datasets.augmentors.timestamp_with_subject_tracking import ( + TimeStampWithSubjectTracking, +) +from projects.cosmos3.vlm.datasets.augmentors.timestamp_without_augment_message import ( + TimeStampWithoutAugmentMessage, +) +from projects.cosmos3.vlm.datasets.augmentors.timestamp_without_end_time import TimeStampWithoutEndTime +from projects.cosmos3.vlm.datasets.augmentors.tokenize_data import TokenizeData +from projects.cosmos3.vlm.datasets.collate_fn import custom_collate +from projects.cosmos3.vlm.datasets.dataset_provider_sft import get_vlm_dataset +from projects.cosmos3.vlm.datasets.distributor_with_weight import ( + NoReplaceShardlistBasic, + WeightedShardlistBasic, +) +from projects.cosmos3.vlm.datasets.joint_dataloader import IterativeJointDataLoader + + +def create_distributor_config( + distributor_type: str, + data_weight_dict: dict, + url_to_category_fn, + shuffle: bool = True, + split_by_node: bool = False, + split_by_worker: bool = False, + resume_flag: bool = False, + verbose: bool = True, + is_infinite_loader: bool = True, + seed: int = 1993, + subsample_config: dict | None = None, + split: str = "train", +): + """ + Return a LazyCall to the distributor class based on distributor_type. + + Args: + distributor_type: "with_replace" -> WeightedShardlistBasic, "no_replace" -> NoReplaceShardlistBasic + data_weight_dict: category -> weight (or repetitions) mapping + url_to_category_fn: maps URL path to category key + split: "train" or "val" (used by NoReplaceShardlistBasic) + Other args: passed to the distributor constructor. + + Returns: + L(WeightedShardlistBasic)(...) or L(NoReplaceShardlistBasic)(...) — a LazyCall to the distributor class. + """ + common = dict( + data_weight_dict=data_weight_dict, + url_to_category_fn=url_to_category_fn, + shuffle=shuffle, + split_by_node=split_by_node, + split_by_worker=split_by_worker, + resume_flag=resume_flag, + verbose=verbose, + is_infinite_loader=is_infinite_loader, + subsample_config=subsample_config, + split=split, + ) + if distributor_type == "with_replace": + return L(WeightedShardlistBasic)(**common) + if distributor_type == "no_replace": + return L(NoReplaceShardlistBasic)(seed=seed, **common) + + raise ValueError(f"distributor_type must be in ['with_replace', 'no_replace'], got {distributor_type!r}") + + +def create_data_augmentor_config(): + return { + "bytes_to_media": L(BytesToMedia)( + input_key="media", + output_key="media", + min_fps_thres=2, + max_fps_thres=60, + target_fps="${data_setting.qwen_target_fps}", # type: ignore + max_video_token_length="${data_setting.qwen_max_video_token_length}", # type: ignore + processor=processor, + is_input_pickle_byptes=False, # If True, it means the input "media" is pickled bytes that needs to be unpickled first; if False, it means the input "media" is raw bytes that can be directly decoded to image/video. Set to False for most cases, and only set to True for some special datasets where media is stored as pickled bytes. + ), # takes "videos" and output "videos" + "prompt_format": L(PromptFormat)( # takes text_keys and output "conversation" + input_keys=["texts"], + text_chat_order="${data_setting.text_chat_order}", + ), + "shuffle_text_media_order": L(ShuffleTextMediaOrder)(), + # ============================ + # TL data augmentation + # ============================ + "timestamp": L(TimeStamp)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="temporal_localization", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "av_reasoning_localization_20250627", + "tl_activitynet_20250630", + "tl_agibot_fisheye_20250630", + "tl_2dvlm_20250627", + "tl_2dvlm_20251121", + "tl_youcook2_20250716", + "tl_yt_cctv_warehouse_20250724", + ], + processor=processor, + ), + "TL_recaption": L(TimeStamp)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="caption", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_2dvlm_recaption_20251121", + "tl_2dvlm_recaption_20250627", + ], + processor=processor, + ), + # Special augmentors: + # timestamp_without_end_time: nexar data does not contain end time + # timestamp_with_subject_trackig: plm data has subject id + mask, and it's video data + # format_describe_anything: dam data has subject id + mask + category label, and it's image data (does not need timestampt) + # timestamp_without_augment_message: rft tl data require timestamp augmentation to video, but keep original text + "timestamp_without_end_time": L(TimeStampWithoutEndTime)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="temporal_localization", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_nexar_20250708", + "mimicgen_temporal_localization", + ], + processor=processor, + ), + "timestamp_with_subject_trackig": L(TimeStampWithSubjectTracking)( + input_key="media", + output_format="temporal_location_subject", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_plm_sav_20250714", + ], + processor=processor, + ), + "floating_number_format": L(FloatingNumberFormat)( + input_key="conversation", + decimal_places=2, + urls_needs_format=[ + "3d_grounding_av", + ], + ), + "format_describe_anything": L(FormatDescribeAnything)( + input_key="media", + urls_needs_timestamp=[ + "describe-anything-dataset", + ], + ), + "timestamp_without_augment_message": L(TimeStampWithoutAugmentMessage)( + input_key="media", + output_format="${data_setting.temporal_localization_output_format}", + urls_needs_timestamp=[ + "rl_distill_tl_0729", + ], + processor=processor, + ), + # ============================ + # End of TL data augmentation + # ============================ + "tokenize_data": L(TokenizeData)( + processor=processor, + max_video_token_length="${data_setting.qwen_max_video_token_length}", + max_image_token_length="${data_setting.qwen_max_image_token_length}", + add_system_prompt_if_missing=True, + text_only=False, + ), + "filter_output_keys": L(FilterOutputKey)( + text_only=False, + ), + "filter_seq_length": L(FilterSeqLength)( + max_token_length="${data_setting.max_tokens}", + processor=processor, + ), + } + + +processor = L(build_processor)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", +) + + +def get_vlm_dataset_from_module( + data_module: str, + split: str = "train", + distributor_split: str = "train", + object_store: str = "s3", + augmentor_config: dict | None = None, + distributor_type: str = "with_replace", + distributor_seed: int = 1993, + buffer_size: int = 2, + detshuffle: bool = False, +): + """Resolve data module at instantiation time instead of config registration time. + + This defers importlib.import_module to when the config is actually used (training time), + avoiding the ~10+ minute startup penalty from eagerly importing all registered dataset + modules during Hydra config store population. + """ + data_weight_attr = data_module.split(".")[-1] + module_path = ".".join(data_module.split(".")[:-1]) + data_weight_module = importlib.import_module(module_path) + + full_datainfo = data_weight_module.DATAINFO + data_weight_dict = getattr(data_weight_module, data_weight_attr) + url_to_category = data_weight_module.url_to_category + subsample_config = getattr(data_weight_module, "subsample_config", None) + + distributor_config = create_distributor_config( + distributor_type=distributor_type, + data_weight_dict=data_weight_dict, + url_to_category_fn=url_to_category, + split=distributor_split, + seed=distributor_seed, + subsample_config=subsample_config, + ) + + return get_vlm_dataset( + full_datainfo=full_datainfo, + url_to_category_fn=url_to_category, + buffer_size=buffer_size, + object_store=object_store, + data_weight_dict=data_weight_dict, + split=split, + augmentor_config=augmentor_config, + distributor_config=distributor_config, + detshuffle=detshuffle, + ) + + +def create_dataloader_config( + data_module: str, + split: str = "train", + distributor_split: str = "train", + object_store: str = "s3", +): + """Create a lazy dataloader config that defers dataset module import to instantiation time. + + Args: + data_module: Full dotted path to the data weight dict, e.g. + "projects.cosmos3.vlm.datasets.data_sources_nanov2.data_weight.stage_1.data_weight_repeat". + The module should export DATAINFO, url_to_category, and the named weight dict. + split: Dataset split ("train" or "val"). + distributor_split: Distributor split for train/val sharding. + object_store: Object store backend ("s3", "s3_vlmdb", "pdx", "neb_eu"). + + Returns: + L(get_vlm_dataset_from_module): a LazyCall that resolves the module at instantiation time. + """ + return L(get_vlm_dataset_from_module)( + data_module=data_module, + split=split, + distributor_split=distributor_split, + object_store=object_store, + augmentor_config=create_data_augmentor_config(), + distributor_type="${data_setting.distributor_type}", + distributor_seed="${data_setting.distributor_seed}", + detshuffle="${data_setting.webdataset_detshuffle}", + ) + + +def register_data_weighted_url(): + cs = ConfigStore.instance() + # This will register dataset: + # reason1_v01_understanding_only_pdx + # reason1_v01_understanding_only_s3 + # reason1_v01_understanding_only_neb_eu + # eagle_v01_sft_no_text_only_pdx + # eagle_v01_sft_no_text_only_s3 + # eagle_v01_sft_no_text_only_neb_eu + # eagle_v02_grounding_2d_pdx + # eagle_v02_grounding_2d_s3 + # eagle_v02_grounding_2d_neb_eu + # eagle_v03_grounding_2d_v1_2_pdx + # eagle_v03_grounding_2d_v1_2_s3 + # eagle_v03_grounding_2d_v1_2_neb_eu + # eagle_v04_sft_no_text_only_no_grounding_2d_pdx + # eagle_v04_sft_no_text_only_no_grounding_2d_s3 + # eagle_v04_sft_no_text_only_no_grounding_2d_neb_eu + # joint_v01_cr1_understanding_eagle_sft_pdx + # joint_v01_cr1_understanding_eagle_sft_s3 + # joint_v01_cr1_understanding_eagle_sft_neb_eu + for dataset_id, data_module in { + "01": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.understanding_only.data_weight_default", # 01_reason1_understanding_only_default_s3 + "02": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full.data_weight_default", # 02_eagle_sft_full_default_s3 + "03": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.grounding_2d_v1_1.data_weight_default", # 03_eagle_grounding_2d_v1_1_default_s3 + "04": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.grounding_2d_v1_2.data_weight_default", # 04_eagle_grounding_2d_v1_2_default_s3 + "05": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_full_5v5", # 05_joint_cr1_eagle_sft_full_5v5_s3 + "06": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_2d_5v5", # 06_joint_cr1_eagle_sft_2d_5v5_s3 + "07": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.pretrain.data_weight_default", # 07_eagle_pretrain_default_s3 + "08": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_default", # 08_eagle_sft_full_mul_repeat_default_s3 + "09": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_debug", # 09_eagle_sft_full_mul_repeat_debug_s3 + "10": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_full_5v5_mul_repeat", # 10_joint_cr1_eagle_sft_full_5v5_mul_repeat_s3 + "11": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_single", # 11_eagle_sft_full_mul_repeat_single_s3 + "12": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.understanding_only.data_weight_default", # 12_reason1_understanding_only_default_s3 + "13": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1.data_weight_mix_5v5", # 13_reason1p0_1p1_mix_5v5_s3 + "14": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason1_2.data_weight_mix_5v5v5", # 14_joint_reason1_2_mix_5v5v5_s3 + "15": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1.data_weight_debug_tl", # 15_reason1_reason1p0_1p1_debug_tl_s3 + "16": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason1_2.data_weight_mix_all_zero", # 16_joint_reason1_2_mix_all_zero_s3 + "17": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_only_vatex_subset", # 17_eagle_sft_full_mul_repeat_only_vatex_subset_s3 + "18": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason1_2.data_weight_understand", # 18_joint_reason1_2_data_weight_understand + "19": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_mix_5v5", # 19_reason1_reason1p0_1p1_721_mix_5v5_s3 + "20": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_debug_2dvlm", # 20_reason1_reason1p0_1p1_721_debug_2dvlm_s3 + "21": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_debug_av", # 21_reason1_reason1p0_1p1_721_debug_av_s3 + "22": "projects.cosmos3.vlm.datasets.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_no_rft", # 22_reason1_reason1p0_1p1_721_no_rft_s3 + "23": "projects.cosmos3.vlm.datasets.data_sources_reason2.data_weight.reason2.data_weight_all_zero", # 23_reason2_reason2_all_zero_s3 + # 24: Reason 2 data count 5% of total + "24": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason1p0_1p1_2_721.data_weight_mix_475v475v005", # 24_joint_reason1p0_1p1_2_721_mix_475v475v005_s3 + "25": "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.grounding_2d_v1_1.data_weight_no_robospatial", # 25_eagle_grounding_2d_v1_1_no_robospatial_s3 + # via exploration + "26": "projects.cosmos3.vlm.datasets.data_sources_via.data_weight.default.data_weight_spatial_suc_only", # 26_via_default_spatial_suc_only_s3 + "27": "projects.cosmos3.vlm.datasets.data_sources_via.data_weight.default.data_weight_spatial_suc_only_round4", # 27_via_default_spatial_suc_only_round4_s3 + "28": "projects.cosmos3.vlm.datasets.data_sources_via.data_weight.default.data_weight_90_suc_only_round2", # + "29": "projects.cosmos3.vlm.datasets.data_sources_via.data_weight.default.data_weight_all_zeros", # 29_via_default_all_zeros_s3 + # reason2 release + "54": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2_release.data_weight_joint", # 54_joint_reason2_release_joint_s3 + "55": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2_release.data_weight_with_recaption", # 55_joint_reason2_release_joint_with_recaption_s3 + "56": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2_release.data_weight_with_recaption_wo_human", # 56_joint_reason2_release_joint_with_recaption_wo_human_s3 + "57": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2p0_2p1.data_weight_joint", # 57_joint_reason2p0_2p1_joint_s3 + "101": "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2_release.data_weight_debug_recaption", # 101_joint_reason2_release_debug_recaption_s3 + # # taxonomy distillation + # "100": "projects.cosmos3.vlm.datasets.data_sources_taxonomy_distill.data_weight.taxonomy_distill.data_weight_default", # 100_taxonomy_distill_taxonomy_distill_default_s3 + # # interleave document scoring distillation + # "102": "projects.cosmos3.vlm.datasets.data_sources_interleave_scoring.data_weight.interleave_scoring.data_weight_default", # 102_interleave_scoring_interleave_scoring_default_s3 + # video taxonomy distillation + "103": "projects.cosmos3.vlm.datasets.data_sources_video_taxonomy.data_weight.video_taxonomy.data_weight_default", # 103_video_taxonomy_video_taxonomy_default_s3 + # nanov2 pre/post-training + "200": "projects.cosmos3.vlm.datasets.data_sources_nanov2.data_weight.stage_1_0218_34m_uniform_pretrain.data_weight_repeat", # 200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb + "201": "projects.cosmos3.vlm.datasets.data_sources_nanov2.data_weight.stage_1_0218_34m_uniform_posttrain.data_weight_repeat", # 201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb + # Data ablation configs (below is a dummy example, do not uncomment) + "202": "projects.cosmos3.vlm.datasets.data_sources_nanov2.data_weight.new_category_data_mixture.data_weight_repeat", # 202_nanov2_new_category_data_mixture_repeat_s3_vlmdb + }.items(): + data_source_name = data_module.split("data_sources_")[-1].split(".")[0] + dataset_file_name = data_module.split(".")[-2] + data_weight_name = data_module.split("data_weight_")[-1] + for distributor_split in ["train", "val"]: + for object_store in ["pdx", "s3", "s3_vlmdb", "neb_eu"]: + dataset_name = f"{dataset_id}_{data_source_name}_{dataset_file_name}_{data_weight_name}_{object_store}" + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=dataset_name, + node=L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers="${data_setting.num_data_workers}" if distributor_split == "train" else 0, + sampler=None, + prefetch_factor="${data_setting.data_prefetch_factor}" + if distributor_split == "train" + else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + ) + + +def register_data_weighted_url_with_text(): + cs = ConfigStore.instance() + + # This will register dataset: + + for dataset_id, data_modules in { + "m01": { + "with_visual": ( + 5, + "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_default", + ), + "text_only": ( + 1, + "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_text_only", + ), + }, # m01_visual_5_mix_text_1__eagle_sft_full_mul_repeat_default_s3 + "m02": { + "with_visual": ( + 5, + "projects.cosmos3.vlm.datasets.data_sources_joint.data_weight.reason2p0_2p1.data_weight_joint", + ), + "text_only": ( + 1, + "projects.cosmos3.vlm.datasets.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_text_only", + ), + }, # m02_visual_5_mix_text_1__joint_reason2p0_2p1_joint_s3 + }.items(): + ratio_with_visual, data_module_with_visual = data_modules["with_visual"] + ratio_text_only, data_module_text_only = data_modules["text_only"] + data_source_name = data_module_with_visual.split("data_sources_")[-1].split(".")[0] + dataset_file_name = data_module_with_visual.split(".")[-2] + data_weight_name = data_module_with_visual.split("data_weight_")[-1] + + for distributor_split in ["train", "val"]: + for object_store in ["pdx", "s3", "neb_eu"]: + dataset_name = f"{dataset_id}_visual_{ratio_with_visual}_mix_text_{ratio_text_only}__{data_source_name}_{dataset_file_name}_{data_weight_name}_{object_store}" + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=dataset_name, + node=L(IterativeJointDataLoader)( + dataloaders={ + "with_visual": { + "ratio": ratio_with_visual, + "dataloader": L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module_with_visual, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers="${data_setting.num_data_workers}" + if distributor_split == "train" + else 0, + sampler=None, + prefetch_factor="${data_setting.data_prefetch_factor}" + if distributor_split == "train" + else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + }, + "text_only": { + "ratio": ratio_text_only, + "dataloader": L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module_text_only, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers=2, + sampler=None, + prefetch_factor=1, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + }, + } + ), + ) + + +def register_data_recipe(): + from projects.cosmos3.vlm.datasets.recipe_dataloader import VLMRecipeDataLoader + + cs = ConfigStore.instance() + # This will register recipe-based dataloaders using VLMRecipeDataLoader. + # Recipe names and storage types are stored in the PostgreSQL recipe database. + # Registered configs: + # cosmos_reason2_s3_vlmdb + for recipe_name, storage_type in [ + ("cosmos_reason2_s3_vlmdb", "s3_vlmdb"), # cosmos_reason2_s3_vlmdb_recipe + ( + "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb", + "s3_vlmdb", + ), # nemotron_nanov2_stage_1_0218_34m_uniform_s3_vlmdb__v1_recipe + ( + "nemotron_nanov2_stage_1_0218_34m_uniform_posttrain_s3_vlmdb", + "s3_vlmdb", + ), # nemotron_nanov2_stage_1_0218_34m_uniform_posttrain_s3_vlmdb__v1_recipe + ]: + config_name = f"{recipe_name.replace('/', '__')}_recipe" + for distributor_split in ["train", "val"]: + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=config_name, + node=L(VLMRecipeDataLoader)( + recipe_name=recipe_name, + storage_type=storage_type, + model_name_or_path="${model.config.policy.model_name_or_path}", + max_tokens="${data_setting.max_tokens}", + max_batch_size="${data_setting.max_batch_size}", + pool_size=16, + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + augmentor_config=create_data_augmentor_config(), + distributor_type="${data_setting.distributor_type}", + detshuffle="${data_setting.webdataset_detshuffle}", + split="train", # use train split of the dataset + distributor_split=distributor_split, # split training dataset into train and val splits + val_split_ratio="${data_setting.val_split_ratio}", + distributor_seed="${data_setting.distributor_seed}", + num_workers="${data_setting.num_data_workers}" if distributor_split == "train" else 0, + prefetch_factor="${data_setting.data_prefetch_factor}" if distributor_split == "train" else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + ) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py new file mode 100644 index 00000000..2fae36e5 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.vfm.configs.base.defaults.model_config import VLMModelConfig +from cosmos3._src.vfm.configs.base.vlm.defaults.training import TrainConfig +from cosmos3._src.vfm.models.vlm_model import VLMModel + +VLM_FSDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="fsdp", + ), + model=L(VLMModel)( + config=VLMModelConfig( + train=TrainConfig(param_dtype="bfloat16"), + ), + checkpoint="${checkpoint}", + ), +) + + +def register_model(): + cs = ConfigStore.instance() + cs.store(group="model", package="_global_", name="vlm_fsdp", node=VLM_FSDP_CONFIG) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py new file mode 100644 index 00000000..4f77bec6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3._src.imaginaire.lazy_config import PLACEHOLDER +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.utils.config_helper import ConfigStore +from cosmos3._src.vfm.utils.vlm.optimizer import OptimizerConfig, build_lr_schedulers, build_optimizers + + +def register_optimizer(): + cs = ConfigStore.instance() + cs.store( + group="optimizer", + package="optimizer", + name="fusedadamw", + node=L(build_optimizers)( + model_parts=PLACEHOLDER, + model_part_names=PLACEHOLDER, + config=L(OptimizerConfig)( + name="FusedAdam", + ), + ), + ) + cs.store( + group="optimizer", + package="optimizer", + name="adamw", + node=L(build_optimizers)( + model_parts=PLACEHOLDER, + model_part_names=PLACEHOLDER, + config=L(OptimizerConfig)( + name="AdamW", + ), + ), + ) + + +def register_scheduler(): + cs = ConfigStore.instance() + cs.store( + group="scheduler", + package="scheduler", + name="warmup_cosine_lr", + node=L(build_lr_schedulers)( + optimizers=PLACEHOLDER, + name="warmup_cosine_lr", + warmup_iters=1000, + lr_decay_iters="${trainer.max_iter}", + lr="${optimizer.config.lr}", + init_lr="${optimizer.config.init_lr}", + end_lr="${optimizer.config.end_lr}", + ), + ) diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py new file mode 100644 index 00000000..7d613bad --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from dataclasses import MISSING, field +from typing import Union + +import attrs +import torch + +from cosmos3._src.imaginaire.config import make_freezable + + +def skip_ui_field(*, default=MISSING, default_factory=MISSING, **kwargs): + metadata = kwargs.pop("metadata", {}) + metadata["skip_ui"] = True + if default_factory is not MISSING: + return field(default_factory=default_factory, metadata=metadata, **kwargs) + elif default is not MISSING: + return field(default=default, metadata=metadata, **kwargs) + else: + raise ValueError("Must provide either default or default_factory.") + + +@make_freezable +@attrs.define(slots=False) +class TrainPolicyConfig: + mini_batch: int = 1 + type: str = "sft" + + +@make_freezable +@attrs.define(slots=False) +class FP8: + enable_fp8: bool = False + + +@make_freezable +@attrs.define(slots=False) +class TrainConfig: + # Whether to use async tensor parallelism + async_tp_enabled: bool = False + # Whether to use torch.compile + compile: bool = False + + # The data type for parameters and activations + param_dtype: str = "bfloat16" + master_dtype: str = "float32" + + # The data type for reduction in FSDP + fsdp_reduce_dtype: str = "float32" + + # Whether to offload the model to CPU if using FSDP + fsdp_offload: bool = False + + # Reshard the param after forward pass in FSDP + fsdp_reshard_after_forward: str = "default" + + # The batch size for training per iteration in one replica, this is the local batch size for each gradient accumulation step + train_batch_per_replica: int = 1 + + # The interval of train step for synchronizing weights between replicas. + sync_weight_interval: int = 1 + + # Train policy + train_policy: TrainPolicyConfig = TrainPolicyConfig() + fp8: FP8 = FP8() + deterministic: bool = True + + def __post_init__(self): + self.ckpt.__post_init__() + if self.async_tp_enabled and not self.compile: + raise ValueError("Async tensor parallelism requires torch.compile to be enabled") + + def key_values(self): + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + + @property + def param_torch_dtype(self): + return { + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float32": torch.float32, + }[self.param_dtype] + + @property + def master_torch_dtype(self): + return { + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float32": torch.float32, + }[self.master_dtype] + + @property + def fsdp_reduce_torch_dtype(self): + return {"float32": torch.float32}[self.fsdp_reduce_dtype] + + +@make_freezable +@attrs.define(slots=False) +class ParallelismConfig: + # Number of initial replicas to be created + n_init_replicas: int = 1 + # Tensor parallelism size + tp_size: int = 1 + # Context parallelism size + cp_size: int = 1 + # Expert parallelism size + ep_size: int = 1 + # Data Parallelism size in sharded mode + dp_shard_size: int = -1 + # Pipeline parallelism size + pp_size: int = 1 + # Pipeline parallelism dynamic shape + pp_dynamic_shape: bool = False + # Pipeline parallelism micro batch size + pp_micro_batch_size: int = 1 + # Data Parallelism size in replicate mode + dp_replicate_size: int = 1 + # The method to rotate kv shards during context parallelism + cp_rotate_method: str = "allgather" + + @property + def world_size(self): + world_size = os.environ.get("WORLD_SIZE", 1) + return int(world_size) + + @property + def local_world_size(self): + local_world_size = os.environ.get("LOCAL_WORLD_SIZE", 1) + return int(local_world_size) + + +# Why we does not make this freezable? +# Because we need to path the cache model dir as model_name_or_path to the cosmos-rl model to use the +# model weights downloaded from s3. If cosmos-rl support reading model from s3 directly, we can make it freezable. +@attrs.define(slots=False) +class PolicyConfig: + # Parallelism configuration + parallelism: ParallelismConfig = ParallelismConfig() + # The model name or path, compatible with huggingface model name or local path + model_name_or_path: str = "Qwen/Qwen2.5-VL-3B-Instruct" + # The maximum length for training, longer than this will be ignored for training stability + model_max_length: int = 16000 + # Whether to use gradient checkpointing + model_gradient_checkpointing: bool = True + # The maximum length for video tokens, only applied to qwen model + qwen_max_video_token_length: int = 8000 + param_torch_dtype: str = "bfloat16" + + # Pretrain weights (Optional) + pretrain_weights_path_vlm: str = "" + pretrain_weights_path_llm: str = "" + pretrain_weights_path_vit: str = "" + pretrain_weights_cred: str = "credentials/s3_training.secret" + + # Extra model config + lora: Union[str, None] = None + enable_liger_kernel: bool = False + trainable_map: Union[str, None] = None + monkey_patch_for_text_only_data: bool = False diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py b/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py new file mode 100644 index 00000000..1944c9f5 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.vfm.configs.base.vlm.defaults.training import PolicyConfig + +# Each entry replaces cfg.model.config.policy via package="model.config.policy". +# Sibling to the VFM vlm_config group at +# cosmos3/_src/vfm/configs/base/defaults/vlm.py: that group binds +# VLMConfig SKUs onto OmniMoTModelConfig.vlm_config; this group binds +# PolicyConfig SKUs onto VLMModelConfig.policy. The two schemas are kept +# separate today because the loader contracts diverge (VFM uses a +# registry-label + LazyDict model_instance with MoTDecoderLayer +# substitution; VLM uses a literal HF cache path fed to from_pretrained). +# Convergence onto a single SKU schema is tracked as L6 in +# config_unification_plan.v10.md. + +qwen2_5_vl_7b = PolicyConfig(model_name_or_path="Qwen/Qwen2.5-VL-7B-Instruct") + +eagle_er_1p7b = PolicyConfig( + model_name_or_path="eagle_er_qwen3_1p7b_siglip_400m", + model_max_length=16000, +) + +internvl3_5_1b = PolicyConfig( + model_name_or_path="OpenGVLab/InternVL3_5-1B-HF", + model_max_length=16000, # 40960 is the max length by default. +) + +internvl3_5_2b = PolicyConfig( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF", + model_max_length=16000, # 40960 is the max length by default. +) + +qwen3_vl_2b = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Init") + +qwen3_vl_30b_a3b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-30B-A3B-Instruct") + +qwen3_vl_30b_a3b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-30B-A3B-Thinking") + +qwen3_vl_235b_a22b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-235B-A22B-Thinking") + +qwen3_vl_8b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-8B-Thinking") + +qwen3_vl_8b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-8B-Instruct") + +qwen3_vl_2b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Instruct") + +qwen3_vl_2b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Thinking") + +qwen3_vl_4b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-4B-Instruct") + +qwen3_vl_4b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-4B-Thinking") + +qwen3_vl_32b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-32B-Instruct") + +nemotron_nano_12b_v2_vl_bf16 = PolicyConfig(model_name_or_path="nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16") + + +def register_vlm_policy(): + cs = ConfigStore.instance() + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen2_5_vl_7b", + node=qwen2_5_vl_7b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="eagle_er_1p7b", + node=eagle_er_1p7b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="internvl3_5_1b", + node=internvl3_5_1b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="internvl3_5_2b", + node=internvl3_5_2b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b", + node=qwen3_vl_2b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_30b_a3b_instruct", + node=qwen3_vl_30b_a3b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_30b_a3b_thinking", + node=qwen3_vl_30b_a3b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_235b_a22b_thinking", + node=qwen3_vl_235b_a22b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_8b_thinking", + node=qwen3_vl_8b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_8b_instruct", + node=qwen3_vl_8b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b_instruct", + node=qwen3_vl_2b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b_thinking", + node=qwen3_vl_2b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_4b_instruct", + node=qwen3_vl_4b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_4b_thinking", + node=qwen3_vl_4b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_32b_instruct", + node=qwen3_vl_32b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="nemotron_nano_12b_v2_vl_bf16", + node=nemotron_nano_12b_v2_vl_bf16, + ) diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py b/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py b/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py new file mode 100644 index 00000000..b906615b --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py @@ -0,0 +1,105 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA CORPORATION +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- +"""Phase 2 VFM VLMModel smoke test experiments. + +These configs exercise the Phase 2 HFModel/FSDP2 path end-to-end on 4 GPUs. +They are NOT training runs — max_iter=10 is intentionally minimal. + +Usage: + torchrun --nproc_per_node=4 --master_port=12341 -m scripts.train \\ + --config=cosmos3/_src/vfm/configs/base/vlm/config.py \\ + -- experiment=pre_exp012_000_phase2_vlm_smoke_4gpu +""" + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyDict + +cs = ConfigStore.instance() + +# --------------------------------------------------------------------------- +# Smoke test: 4-GPU FSDP2 with qwen3_vl_2b, debug data, 10 iterations. +# Exercises: HFModel meta-init → parallelize() → forward → CE loss → backward +# Requires: trainable_params (Phase 2 mandatory), dp_shard_size=4 +# --------------------------------------------------------------------------- +pre_exp012_000_phase2_vlm_smoke_4gpu_8b = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /data_val": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="phase2_smoke", + ), + trainer=dict( + max_iter=10, + run_validation=False, + logging_iter=1, + ), + optimizer=dict( + config=dict( + # Phase 2 REQUIRED: trainable_params regex list. + # ".*" trains all parameters — appropriate for smoke test. + trainable_params=[".*"], + lr=1e-5, + fused=True, + ), + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=4, + dp_replicate_size=-1, + ), + ), + ), + ), + # dataloader_train=dict( + # enable_flop_based_batching=True, + # target_runtime_seconds=3.0, # Used when max_batch_size > 1 + # ), + checkpoint=dict( + # Don't save checkpoints during smoke test + save_iter=100000, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + webdataset_detshuffle=True, + ), + ) +) + + +for _item in [ + pre_exp012_000_phase2_vlm_smoke_4gpu_8b, +]: + experiment_name = [name.lower() for name, value in globals().items() if value is _item][0] + if "job" not in _item: + _item["job"] = dict(name=experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}") + else: + _item["job"]["name"] = experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + cs.store(group="experiment", package="_global_", name=experiment_name, node=_item) diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py b/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py new file mode 100644 index 00000000..b49627f8 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py @@ -0,0 +1,854 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos3._src.imaginaire.lazy_config import LazyDict + +cs = ConfigStore.instance() + +""" +Bundled VLM training experiments registered under the ``experiment`` group. + +Usage: +torchrun --nproc_per_node=8 --master_port=12341 -m scripts.train \ + --config=cosmos3/_src/vfm/configs/base/vlm/config.py \ + -- experiment=pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader +""" + + +pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "eagle_er_1p7b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + max_iter=32000, + ), + checkpoint=dict(save_iter=5000), + ) +) + +pre_exp010_010_internvl3_5_1b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_1b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + ), + checkpoint=dict(save_iter=2000), + ) +) + +pre_exp010_020_internvl3_5_2b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + max_iter=32000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_000_qwen3_vl_2b +""" +pre_exp011_000_qwen3_vl_2b = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + checkpoint=dict(save_iter=2000), + ) +) + + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_020_qwen3_vl_2b_vit2k8k +""" +pre_exp011_020_qwen3_vl_2b_vit2k8k = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 +""" +pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + max_batch_size=8, + max_tokens=16000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 +""" +pre_exp011_040_qwen3_vl_2b_vit2k8k_mbs8_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=8, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + checkpoint=dict(save_iter=2000), + ) +) + +pre_exp011_041_qwen3_vl_2b_vit2k8k_mbs8_flop3s_mix_text_only = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=8, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + dataloaders=dict( + with_visual=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + text_only=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + ) + ), + checkpoint=dict(save_iter=2000), + ) +) + + +pre_exp011_100_qwen3_vl_2b_align = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + optimizer=dict( + config=dict( + freeze_vision_encoder=True, + freeze_llm=True, + lr=1e-5, + init_lr=1e-7, + end_lr=5e-6, + ), + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +# reinit the llm and/or projector weights for internvl3_5_2b +pre_exp011_300_internvl3_5_2b_reinit_align = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + optimizer=dict( + config=dict( + freeze_vision_encoder=True, + freeze_llm=True, + lr=1e-5, + init_lr=1e-7, + end_lr=5e-6, + ), + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + model=dict( + config=dict( + policy=dict( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF-ReinitLLMProj", + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + + +# reinit the llm and/or projector weights for internvl3_5_2b +pre_exp011_400_internvl3_5_2b_reinit_e2e = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + model=dict( + config=dict( + policy=dict( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF-ReinitLLMProj", + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp014_000_qwen3_vl_8b_instruct_vit2k8k_mbs8_flop3s +- reduce lr to 2e-6 cause this is SFT +""" +pre_exp015_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "dummy_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only data_train=m02_visual_5_mix_text_1__joint_reason2p0_2p1_joint_s3 +""" +pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=10000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + dataloaders=dict( + with_visual=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + text_only=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + ) + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp014_000_qwen3_vl_8b_instruct_vit2k8k_mbs8_flop3s +- reduce lr to 2e-6 cause this is SFT +""" +pre_exp016_000_qwen3_vl_8b_thinking_vit2k8k_mbs1_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_thinking"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp017_000_nemotron_vl_12b_mbs1 +""" +pre_exp017_000_nemotron_vl_12b_mbs1 = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "51_joint_reason1p0_1p1_2_1126_1126_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "nemotron_nano_12b_v2_vl_bf16"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain +""" +pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb"}, + {"override /data_val": "200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=60000, + run_validation=True, + validation_iter=40, + max_val_iter=1, + ), + optimizer=dict( + config=dict( + lr=2e-5, + fused=True, + weight_decay=0.05, + betas=[0.9, 0.999], + freeze_vision_encoder=True, + freeze_mm_projector=True, + ), + ), + scheduler=dict( + warmup_iters=6000, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + monkey_patch_for_text_only_data=True, + pretrain_weights_path_llm="Qwen/Qwen3-8B", + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict( + save_iter=2000, + ), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain +""" +pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb"}, + {"override /data_val": "201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + run_validation=True, + validation_iter=40, + max_val_iter=1, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + weight_decay=0.05, + betas=[0.9, 0.999], + freeze_vision_encoder=True, + freeze_mm_projector=True, + ), + ), + scheduler=dict( + warmup_iters=800, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1996, + val_split_ratio=0.05, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + dp_shard_size=8, + dp_replicate_size=-1, + ), + monkey_patch_for_text_only_data=True, + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict( + save_iter=1000, + load_path="cosmos_reason2/pre_exp015/pre_exp015_288_34m_v1352_repeat_uniform_1_epoch_n64/checkpoints/iter_000060000/", + ), + ) +) + + +pre_exp020_001_qwen3_vl_30b_a3b_instruct_ep = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /data_val": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_30b_a3b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict(group="debug"), + trainer=dict( + max_iter=8000, + logging_iter=1, + ), + optimizer=dict(config=dict(lr=5e-5, fused=True)), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + webdataset_detshuffle=True, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + tp_size=1, + ep_size=2, + dp_shard_size=2, + dp_replicate_size=1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + + +for _item in [ + pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp010_010_internvl3_5_1b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp010_020_internvl3_5_2b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp011_000_qwen3_vl_2b, + pre_exp011_020_qwen3_vl_2b_vit2k8k, + pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8, + pre_exp011_040_qwen3_vl_2b_vit2k8k_mbs8_flop3s, + pre_exp011_041_qwen3_vl_2b_vit2k8k_mbs8_flop3s_mix_text_only, + pre_exp011_100_qwen3_vl_2b_align, + pre_exp011_300_internvl3_5_2b_reinit_align, + pre_exp011_400_internvl3_5_2b_reinit_e2e, + pre_exp015_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s, + pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only, + pre_exp016_000_qwen3_vl_8b_thinking_vit2k8k_mbs1_flop3s, + pre_exp017_000_nemotron_vl_12b_mbs1, + pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain, + pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain, + pre_exp020_001_qwen3_vl_30b_a3b_instruct_ep, +]: + experiment_name = [name.lower() for name, value in globals().items() if value is _item][0] + if "job" not in _item: + _item["job"] = dict(name=experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}") + else: + _item["job"]["name"] = experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + cs.store(group="experiment", package="_global_", name=experiment_name, node=_item) diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py b/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py new file mode 100644 index 00000000..676fd475 --- /dev/null +++ b/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass, field +from typing import Dict, List + + +@dataclass +class Experiment: + job_exp: str + nnode: int + command_args: List[str] + job_name: str = None + init_command: str = "" + job_group: str = None + extra_env_vars: Dict[str, str] = field(default_factory=dict) diff --git a/cosmos3/_src/vfm/datasets/__init__.py b/cosmos3/_src/vfm/datasets/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/datasets/action/action_normalization.py b/cosmos3/_src/vfm/datasets/action/action_normalization.py new file mode 100644 index 00000000..1d8defed --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/action_normalization.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Action normalization helpers.""" + +import json +from pathlib import Path + +import numpy as np +import torch + +from cosmos3._src.imaginaire.utils import log + + +def load_action_stats(stats_path: str, stats_key: str = "global") -> dict[str, np.ndarray]: + """Load pre-computed action normalization stats from a JSON file.""" + path = Path(stats_path) + if not path.exists(): + raise FileNotFoundError(f"Action normalization stats not found at {stats_path}.") + log.info(f"Loading action normalization stats from {stats_path}") + with path.open("r") as f: + raw = json.load(f) + if stats_key in raw: + raw = raw[stats_key] + if not isinstance(raw, dict): + raise TypeError(f"Action normalization stats block {stats_key!r} in {stats_path} must be a dict.") + elif stats_key != "global": + raise KeyError(f"Action normalization stats block {stats_key!r} not found in {stats_path}.") + stat_keys = {"mean", "std", "min", "max", "q01", "q99"} + return {k: np.array(v, dtype=np.float32) for k, v in raw.items() if k in stat_keys} + + +def normalize_action( + action: torch.Tensor, + method: str, + stats: dict[str, torch.Tensor], +) -> torch.Tensor: + """Normalize action tensor (all dimensions including gripper).""" + if method == "quantile": + q01, q99 = stats["q01"], stats["q99"] + denom = (q99 - q01).clamp(min=1e-8) + return (2.0 * (action - q01) / denom - 1.0).clamp(-1.0, 1.0) + if method == "meanstd": + return (action - stats["mean"]) / stats["std"].clamp(min=1e-8) + if method == "minmax": + lo, hi = stats["min"], stats["max"] + denom = (hi - lo).clamp(min=1e-8) + return (2.0 * (action - lo) / denom - 1.0).clamp(-1.0, 1.0) + raise ValueError(f"Unknown normalization method: {method!r}") diff --git a/cosmos3/_src/vfm/datasets/action/action_spec.py b/cosmos3/_src/vfm/datasets/action/action_spec.py new file mode 100644 index 00000000..dfc6811f --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/action_spec.py @@ -0,0 +1,247 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Action-vector specification: per-dim type label + idle thresholds. + +Single concept: every column of an action vector has a :class:`DimType` label. +Idle detection iterates by type and applies the matching algorithm: + + POS → ‖action[pos_idx]‖ per arm < eps_t + ROT → distance(rot, identity) per group < eps_r + GRIPPER → max |Δgripper| < eps_g (frame 0 idle by convention) + JOINT → max |Δjoint| < joint_threshold (frame 0 idle) + RESERVED → ignored + +An :class:`ActionSpec` is just ``names`` + ``types`` + ``rotation_format``. +Build one declaratively via :func:`build_action_spec` from DSL components:: + + build_action_spec(Pos(), Rot("rot6d"), Gripper()) # 10D single arm + build_action_spec(Pos(), Rot("rot6d")) # 9D no gripper + build_action_spec(Joint(n=14, label="arm"), # 30D joint-space + Joint(n=14, label="end"), + Joint(n=2, label="gripper")) + build_action_spec(Pos(prefix="left"), Rot("rot6d", "left"), Gripper(prefix="left"), + Pos(prefix="right"), Rot("rot6d", "right"), Gripper(prefix="right")) + +Naming convention: + Default ``pos_x``, ``rot_0``, ``gripper``, ``arm_0`` ... + With ``prefix="left"`` (idempotent on trailing ``_``): ``left_pos_x`` ... +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import ClassVar + +from cosmos3._src.vfm.datasets.action.pose_utils import ( + RotationConvention, + _identity_rotation_vector, +) + + +class DimType(str, Enum): + """Per-column action-dim category (drives idle detection).""" + + POS = "pos" + ROT = "rot" + GRIPPER = "gripper" + JOINT = "joint" + RESERVED = "reserved" + + +@dataclass(frozen=True, slots=True) +class ActionSpec: + """Structural description of an action vector: names + per-dim types. + + All ROT dims share a single ``rotation_format``; mixed formats in one spec + are not supported (raise at build time). + + This struct contains no detection thresholds — those are passed at call + time to :func:`compute_idle_frames` so each dataset can tune them + independently of layout. + """ + + names: list[str] + types: list[DimType] + rotation_format: RotationConvention = "rot6d" + + @property + def dim(self) -> int: + return len(self.names) + + +# --------------------------------------------------------------------------- +# DSL components +# --------------------------------------------------------------------------- + + +def _join_prefix(prefix: str, name: str) -> str: + """Join ``prefix`` and ``name`` with a single ``_``; idempotent on trailing ``_``.""" + return name if not prefix else f"{prefix.rstrip('_')}_{name}" + + +@dataclass(frozen=True) +class Pos: + """Translation block. + + Default 3D (``pos_x``, ``pos_y``, ``pos_z``). For planar tasks (e.g. PushT) + use ``Pos(dim=2)`` → ``pos_x``, ``pos_y``. ``dim >= 4`` falls back to + indexed names ``pos_0``, ``pos_1``, ... + """ + + dim: int = 3 + prefix: str = "" + type: ClassVar[DimType] = DimType.POS + + def names(self) -> list[str]: + if self.dim <= 3: + return [_join_prefix(self.prefix, f"pos_{c}") for c in "xyz"[: self.dim]] + return [_join_prefix(self.prefix, f"pos_{i}") for i in range(self.dim)] + + +@dataclass(frozen=True) +class Rot: + """Rotation block; ``format`` selects the encoding. + + Supported formats and per-dim names: + + - ``rot6d`` → 6 dims, ``rot_0`` ... ``rot_5`` (identity ``[1,0,0,0,1,0]``) + - ``rot9d`` → 9 dims, ``rot_0`` ... ``rot_8`` (identity ``[1,0,0,0,1,0,0,0,1]``) + - ``euler_xyz`` → 3 dims, ``roll``, ``pitch``, ``yaw`` (identity ``[0,0,0]``) + - ``axisangle`` → 3 dims, ``axang_x/y/z`` (identity ``[0,0,0]``) + - ``quat_xyzw`` / ``quat_wxyz`` → 4 dims, ``quat_x/y/z/w`` in declared order + """ + + format: RotationConvention = "rot6d" + prefix: str = "" + type: ClassVar[DimType] = DimType.ROT + + @property + def rotation_format(self) -> RotationConvention: + return self.format + + @property + def dim(self) -> int: + return _identity_rotation_vector(self.format).shape[0] + + def names(self) -> list[str]: + if self.format == "euler_xyz": + return [_join_prefix(self.prefix, c) for c in ("roll", "pitch", "yaw")] + if self.format == "axisangle": + return [_join_prefix(self.prefix, f"axang_{c}") for c in "xyz"] + if self.format.startswith("quat_"): + order = self.format.split("_", 1)[1] # "xyzw" or "wxyz" + return [_join_prefix(self.prefix, f"quat_{c}") for c in order] + return [_join_prefix(self.prefix, f"rot_{i}") for i in range(self.dim)] + + +@dataclass(frozen=True) +class Gripper: + """1D gripper command (binary 0/1 or continuous). Detected by frame-diff.""" + + prefix: str = "" + type: ClassVar[DimType] = DimType.GRIPPER + + @property + def dim(self) -> int: + return 1 + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, "gripper")] + + +@dataclass(frozen=True) +class Joint: + """``n`` joint commands. Detected by frame-diff against ``joint_threshold``.""" + + n: int = 0 + label: str = "joint" + prefix: str = "" + type: ClassVar[DimType] = DimType.JOINT + + @property + def dim(self) -> int: + return self.n + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, f"{self.label}_{i}") for i in range(self.n)] + + +@dataclass(frozen=True) +class Reserved: + """``n`` dims counted in ``action_dim`` but ignored by idle detection.""" + + n: int = 0 + label: str = "reserved" + prefix: str = "" + type: ClassVar[DimType] = DimType.RESERVED + + @property + def dim(self) -> int: + return self.n + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, f"{self.label}_{i}") for i in range(self.n)] + + +# --------------------------------------------------------------------------- +# Builder +# --------------------------------------------------------------------------- + + +# Type alias for any DSL component. Not a runtime check — only annotation hint. +Component = Pos | Rot | Gripper | Joint | Reserved + + +def build_action_spec(*components: Component) -> ActionSpec: + """Compose ``components`` into an :class:`ActionSpec`. + + Each component contributes its ``names()`` and replicates its ``type`` for + every column it occupies. The first ROT component's ``rotation_format`` + is captured for the whole spec; mixing formats raises ``ValueError``. + """ + names: list[str] = [] + types: list[DimType] = [] + rotation_format: RotationConvention | None = None + + for c in components: + names.extend(c.names()) + types.extend([c.type] * c.dim) + if c.type == DimType.ROT: + fmt = c.rotation_format # type: ignore[union-attr] + if rotation_format is None: + rotation_format = fmt + elif rotation_format != fmt: + raise ValueError(f"Mixed rotation_format in one ActionSpec: {rotation_format!r} vs {fmt!r}") + + return ActionSpec( + names=names, + types=types, + rotation_format=rotation_format or "rot6d", + ) + + +__all__ = [ + "ActionSpec", + "Component", + "DimType", + "Gripper", + "Joint", + "Pos", + "Reserved", + "Rot", + "build_action_spec", +] diff --git a/cosmos3/_src/vfm/datasets/action/dataloaders.py b/cosmos3/_src/vfm/datasets/action/dataloaders.py new file mode 100644 index 00000000..0daa4a53 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/dataloaders.py @@ -0,0 +1,160 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import random +import time +from typing import Callable, Iterator + +import numpy as np +import torch +import torch.utils.data + +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.vfm.datasets.action.unified_dataset import ActionUnifiedIterableDataset +from cosmos3._src.vfm.datasets.joint_dataloader import custom_collate_fn + + +def _action_worker_init_fn( + worker_id: int, seed: int = 42, use_deterministic_seed: bool = True, rank: int = 0, world_size: int = 1 +) -> None: + if use_deterministic_seed: + worker_seed = seed + rank * 9999 + worker_id + else: + worker_seed = int(time.time() * 1000) % (2**32) + rank * 9999 + worker_id + random.seed(worker_seed) + np.random.seed(worker_seed % (2**32)) + torch.manual_seed(worker_seed) + + info = torch.utils.data.get_worker_info() + assert info is not None + dataset = info.dataset + if isinstance(dataset, ActionUnifiedIterableDataset): + dataset.assign_worker(worker_id, info.num_workers, rank, world_size) + + +def create_action_worker_init_fn(seed: int = 42, use_deterministic_seed: bool = True) -> Callable[[int], None]: + """Create a worker_init_fn for Action training with ``ActionUnifiedIterableDataset``. + + Seeds RNGs first, then calls ``dataset.assign_worker()`` to set up + rank-level dataset assignment and worker-level shard distribution. + + Passed to ``DataLoader`` (or ``InfiniteDataLoader``) as the + ``worker_init_fn`` parameter. Only called when ``num_workers > 0``. + + Args: + seed: Base seed for deterministic worker seeding. Ignored when + ``use_deterministic_seed=False`` (time-based seed used instead). + use_deterministic_seed: If True, use the provided seed for reproducible + RNG initialization. If False, derive a time-based seed so that + each resume sees different data. This is preferred for large-scale + runs that resume frequently, and when ``in_order=False`` already + makes iteration order non-deterministic. + + Returns: + A ``worker_init_fn`` suitable for ``torch.utils.data.DataLoader``. + """ + try: + rank = distributed.get_rank() + world_size = distributed.get_world_size() + except RuntimeError: + rank = 0 + world_size = 1 + + return functools.partial( + _action_worker_init_fn, + seed=seed, + use_deterministic_seed=use_deterministic_seed, + rank=rank, + world_size=world_size, + ) + + +class InfiniteDataLoader(torch.utils.data.DataLoader): + """A dataloader that yields forever with proper seeding for reproducibility. + + All Action datasets are ``IterableDataset`` instances (map-style datasets + are automatically wrapped by :class:`~.transforms.MapToIterableAdapter`). + The loader catches ``StopIteration`` and restarts the iterator so that + iteration never ends. + """ + + def __init__( + self, + *args, + seed: int = 42, + use_deterministic_seed: bool = True, + **kwargs, + ) -> None: + """Initialize InfiniteDataLoader. + + Args: + *args: Positional arguments passed to parent DataLoader. + seed: Random seed for reproducible worker initialization. + Default is 42 for reproducibility. + use_deterministic_seed: If True, use the provided seed for reproducible + RNG initialization. If False, derive a time-based seed so that + each resume sees different data. This is preferred for large-scale + runs that resume frequently, and when ``in_order=False`` already + makes iteration order non-deterministic. + **kwargs: Keyword arguments passed to parent DataLoader. + """ + kwargs.pop("shuffle", None) + kwargs["shuffle"] = False + + # Default to ``custom_collate_fn`` so that variable-length per-sample + # tensors (e.g. ``text_token_ids``) and multi-item keys (``video``, + # ``action``, ...) are returned as lists rather than stacked by + # PyTorch's ``default_collate``. + if kwargs.get("collate_fn") is None: + kwargs["collate_fn"] = custom_collate_fn + + if "worker_init_fn" not in kwargs or kwargs["worker_init_fn"] is None: + kwargs["worker_init_fn"] = create_action_worker_init_fn(seed, use_deterministic_seed=use_deterministic_seed) + + num_workers = kwargs.get("num_workers", 0) + if num_workers == 0: + try: + rank = distributed.get_rank() + except RuntimeError: + rank = 0 + if use_deterministic_seed: + rank_seed = seed + rank * 9999 + else: + rank_seed = int(time.time() * 1000) % (2**32) + rank * 9999 + random.seed(rank_seed) + np.random.seed(rank_seed % (2**32)) + torch.manual_seed(rank_seed) + + super().__init__(*args, **kwargs) + self._stream_iterator: Iterator | None = None + + def __len__(self) -> int: + # Delegate to DataLoader which calls len(self.dataset). + # Raises TypeError if the underlying dataset has no __len__. + return super().__len__() + + def __iter__(self) -> Iterator: + """Yield batches forever.""" + while True: + if self._stream_iterator is None: + self._stream_iterator = super().__iter__() + try: + yield next(self._stream_iterator) # type: ignore[arg-type] + except StopIteration: + self._stream_iterator = super().__iter__() + yield next(self._stream_iterator) # type: ignore[arg-type] diff --git a/cosmos3/_src/vfm/datasets/action/domain_utils.py b/cosmos3/_src/vfm/datasets/action/domain_utils.py new file mode 100644 index 00000000..85dda5e7 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/domain_utils.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Domain ID helpers for cross-embodiment action datasets.""" + +EMBODIMENT_TO_DOMAIN_ID: dict[str, int] = { + "no_action": 0, + "av": 1, + "camera_pose": 2, + "hand_pose": 3, + "pusht": 4, + "libero": 5, + "umi": 6, + "bridge_orig_lerobot": 7, + "droid_lerobot": 8, + "robomind-franka": 8, # Both Droid and RoboMIND-Franka are using robotiq and franka + "embodiment_b": 9, + "robomind-franka-dual": 12, + "robomind-ur": 13, + "agibotworld": 15, + "embodiment_c_gripper": 15, + "embodiment_c_gripper_ext": 15, + "fractal": 20, +} + + +def get_domain_id(embodiment_type: str) -> int: + """Get the domain ID for a given embodiment type.""" + key = embodiment_type.lower().strip() + if key not in EMBODIMENT_TO_DOMAIN_ID: + raise KeyError( + f"Unknown embodiment type: {embodiment_type!r}. " + f"Available embodiments: {sorted(EMBODIMENT_TO_DOMAIN_ID.keys())}" + ) + return EMBODIMENT_TO_DOMAIN_ID[key] diff --git a/cosmos3/_src/vfm/datasets/action/json_formatter.py b/cosmos3/_src/vfm/datasets/action/json_formatter.py new file mode 100644 index 00000000..847c3f81 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/json_formatter.py @@ -0,0 +1,306 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import math + +import torch + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.action.viewpoint_utils import DEFAULT_VIEWPOINT_TEMPLATES +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + + +def _should_append_idle_frame_info(mode: object) -> bool: + """Return whether idle-frame prompt metadata should be surfaced.""" + return mode != "inverse_dynamics" + + +class ActionPromptJsonFormatter: + """Format action prompts into a structured JSON-compatible dictionary. + + JSON fields are emitted in this order: ``cinematography``, ``actions``, + ``duration``, ``fps``, ``resolution``, then ``aspect_ratio``. Like video JSON + prompts, ``cinematography`` is a dictionary, duration is truncated to an + integer-second string such as ``"2s"``, and aspect ratio is stored as a + comma-separated string such as ``"16,9"``. If ``data_dict["mode"]`` is + ``"inverse_dynamics"``, idle-frame metadata is omitted from the prompt. + """ + + def __init__( + self, + caption_key: str = "ai_caption", + viewpoint_key: str = "viewpoint", + video_key: str = "video", + fps_key: str = "conditioning_fps", + image_size_key: str = "image_size", + idle_frames_key: str = "idle_frames", + total_frames_key: str = "idle_frames_total", + action_key: str = "action", + viewpoint_templates: dict[str, str] | None = None, + ) -> None: + self.caption_key: str = caption_key + self.viewpoint_key: str = viewpoint_key + self.video_key: str = video_key + self.fps_key: str = fps_key + self.image_size_key: str = image_size_key + self.idle_frames_key: str = idle_frames_key + self.total_frames_key: str = total_frames_key + self.action_key: str = action_key + self.viewpoint_templates: dict[str, str] = ( + viewpoint_templates if viewpoint_templates is not None else DEFAULT_VIEWPOINT_TEMPLATES + ) + + def __call__(self, data_dict: dict) -> dict: + """Replace the caption with the action JSON prompt structure.""" + additional_view_description = data_dict.pop("additional_view_description", None) + caption = data_dict.get(self.caption_key) + if not isinstance(caption, str) or caption == "": + return data_dict + + height, width = self._get_resolution(data_dict) + fps = self._get_scalar_float(data_dict.get(self.fps_key), self.fps_key) + if fps <= 0: + raise ValueError(f"ActionPromptJsonFormatter: '{self.fps_key}' must be positive, got {fps}") + + video = data_dict.get(self.video_key) + if not isinstance(video, torch.Tensor) or video.ndim < 2: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.video_key}' to be a video tensor with shape " + f"(C, T, H, W), got {type(video).__name__}" + ) + duration_seconds = video.shape[1] / fps + duration = self._truncate_seconds(duration_seconds) + action_end_time = self._round_time_seconds(duration_seconds) + + prompt = { + "cinematography": { + "framing": self._get_viewpoint_caption(data_dict, additional_view_description), + }, + "actions": [ + { + "time": f"0:00-{self._format_time_mss(action_end_time)}", + "description": self._ensure_sentence(caption), + "idle_frame": self._get_idle_frame_info(data_dict), + } + ], + "duration": f"{duration}s", + "fps": float(fps), + "resolution": {"H": height, "W": width}, + "aspect_ratio": self._get_aspect_ratio(width, height), + } + cleaned_prompt = self._drop_empty_fields(prompt) + self._raise_if_empty_fields(cleaned_prompt) + data_dict[self.caption_key] = cleaned_prompt + return data_dict + + def _truncate_seconds(self, seconds: float) -> int: + """Truncate duration to integer seconds, matching video JSON-caption augmentors.""" + if seconds < 0 or not math.isfinite(seconds): + return 0 + return int(seconds) + + def _round_time_seconds(self, seconds: float) -> int: + """Round an action timestamp to integer seconds, matching video captioning.""" + if seconds < 0 or not math.isfinite(seconds): + return 0 + return round(seconds) + + def _format_time_mss(self, seconds: int) -> str: + """Format integer seconds as M:SS for JSON prompt time ranges.""" + minutes, remaining_seconds = divmod(seconds, 60) + return f"{minutes}:{remaining_seconds:02d}" + + def _get_aspect_ratio(self, width: int, height: int) -> str: + """Return the canonical width,height aspect ratio string when known.""" + for aspect_ratio_sizes in VIDEO_RES_SIZE_INFO.values(): + for aspect_ratio, (candidate_w, candidate_h) in aspect_ratio_sizes.items(): + if width == candidate_w and height == candidate_h: + return aspect_ratio + + divisor = math.gcd(width, height) + if divisor == 0: + raise ValueError( + f"ActionPromptJsonFormatter: width and height must be non-zero, got width={width}, height={height}." + ) + return f"{width // divisor},{height // divisor}" + + def _get_viewpoint_caption(self, data_dict: dict, additional_view_description: object | None) -> str | None: + """Resolve the viewpoint text used in the ``cinematography`` field.""" + viewpoint = data_dict.get(self.viewpoint_key) + template = self.viewpoint_templates.get(viewpoint) if isinstance(viewpoint, str) else None + + if template is None: + if viewpoint is not None: + log.warning( + f"ActionPromptJsonFormatter: unrecognized viewpoint {viewpoint!r}. " + f"Known viewpoints: {sorted(self.viewpoint_templates.keys())}. " + f"Using additional view description when available.", + rank0_only=False, + ) + return self._get_optional_text(additional_view_description) + + if additional_view_description: + separator = " " if template.endswith(".") else ". " + template = template + separator + str(additional_view_description).rstrip() + return template + + def _get_resolution(self, data_dict: dict) -> tuple[int, int]: + """Resolve ``(height, width)`` from the post-padding image size.""" + image_size = data_dict.get(self.image_size_key) + if image_size is None: + raise ValueError(f"ActionPromptJsonFormatter: missing '{self.image_size_key}' in data_dict.") + + if isinstance(image_size, torch.Tensor): + if image_size.numel() < 2: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.image_size_key}' to contain at least " + f"height and width, got shape {tuple(image_size.shape)}" + ) + return int(image_size[0].item()), int(image_size[1].item()) + + try: + return int(image_size[0]), int(image_size[1]) + except (TypeError, ValueError, IndexError) as e: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.image_size_key}' to contain height and width." + ) from e + + def _get_scalar_float(self, value: object, key: str) -> float: + """Parse a required scalar float from a tensor or Python value.""" + if value is None: + raise ValueError(f"ActionPromptJsonFormatter: missing '{key}' in data_dict.") + + if isinstance(value, torch.Tensor): + if value.numel() != 1: + raise ValueError( + f"ActionPromptJsonFormatter: expected scalar tensor at '{key}', got shape {tuple(value.shape)}" + ) + return float(value.item()) + + if isinstance(value, (str, int, float)): + try: + return float(value) + except ValueError as e: + raise ValueError( + f"ActionPromptJsonFormatter: expected scalar float-compatible value at '{key}'." + ) from e + raise ValueError(f"ActionPromptJsonFormatter: expected scalar float-compatible value at '{key}'.") + + def _get_optional_scalar_int(self, value: object, key: str) -> int | None: + """Parse an optional scalar integer metadata value.""" + if value is None: + return None + + if isinstance(value, torch.Tensor): + if value.numel() != 1: + log.warning( + f"ActionPromptJsonFormatter: expected scalar tensor at '{key}', got shape " + f"{tuple(value.shape)}. Skipping.", + rank0_only=False, + ) + return None + return int(value.item()) + + if isinstance(value, (str, int, float)): + try: + return int(value) + except ValueError: + pass + log.warning( + f"ActionPromptJsonFormatter: expected integer-compatible value at " + f"'{key}', got {type(value).__name__}. Skipping.", + rank0_only=False, + ) + return None + + def _get_total_frames(self, data_dict: dict) -> int | None: + """Resolve the total action-frame count for idle-frame text.""" + total_frames = self._get_optional_scalar_int(data_dict.get(self.total_frames_key), self.total_frames_key) + if total_frames is not None: + return total_frames + + action = data_dict.get(self.action_key) + if isinstance(action, torch.Tensor): + if action.ndim == 0: + log.warning( + f"ActionPromptJsonFormatter: expected action tensor at " + f"'{self.action_key}' to have a frame dimension. Skipping total frames.", + rank0_only=False, + ) + return None + return int(action.shape[0]) + + try: + return len(action) if action is not None else None + except TypeError: + return None + + def _get_idle_frame_info(self, data_dict: dict) -> str | None: + """Build the idle-frame string for the action object.""" + if not _should_append_idle_frame_info(data_dict.get("mode")): + return None + + idle_frames = self._get_optional_scalar_int(data_dict.get(self.idle_frames_key), self.idle_frames_key) + total_frames = self._get_total_frames(data_dict) + + if idle_frames is not None and total_frames is not None: + return f"{idle_frames} out of {total_frames}." + if idle_frames is not None: + return f"{idle_frames}." + return None + + def _ensure_sentence(self, text: str) -> str: + """Return text with terminal sentence punctuation.""" + text = text.strip() + if text.endswith((".", "!", "?")): + return text + return f"{text}." + + def _get_optional_text(self, value: object) -> str | None: + """Return stripped text, leaving empty optional text for the final prune pass.""" + if value is None: + return None + text = str(value).rstrip() + return text if text else None + + def _drop_empty_fields(self, value: object) -> object: + """Recursively remove empty strings, dictionaries, lists, and ``None`` values.""" + if isinstance(value, dict): + return { + key: cleaned + for key, item in value.items() + if not self._is_empty(cleaned := self._drop_empty_fields(item)) + } + if isinstance(value, list): + return [cleaned for item in value if not self._is_empty(cleaned := self._drop_empty_fields(item))] + return value + + def _is_empty(self, value: object) -> bool: + """Return whether a JSON field should be dropped.""" + return value is None or value == "" or value == [] or value == {} + + def _raise_if_empty_fields(self, value: object, path: str = "prompt") -> None: + """Validate that no empty JSON fields remain after pruning.""" + if self._is_empty(value): + raise ValueError(f"ActionPromptJsonFormatter: empty field remains at {path}.") + + if isinstance(value, dict): + for key, item in value.items(): + self._raise_if_empty_fields(item, f"{path}.{key}") + elif isinstance(value, list): + for index, item in enumerate(value): + self._raise_if_empty_fields(item, f"{path}[{index}]") diff --git a/cosmos3/_src/vfm/datasets/action/libero_dataset.py b/cosmos3/_src/vfm/datasets/action/libero_dataset.py new file mode 100644 index 00000000..58c294fb --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/libero_dataset.py @@ -0,0 +1,623 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LIBERO dataset for training from local storage, supporting multiple dataset roots.""" + +import random +from pathlib import Path +from typing import Literal + +import torch +import torchvision.transforms.functional as F +from lerobot.datasets.lerobot_dataset import LeRobotDataset +from torch.utils.data import Dataset + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.action.action_normalization import ( + load_action_stats, + normalize_action, +) +from cosmos3._src.vfm.datasets.action.action_spec import ( + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos3._src.vfm.datasets.action.domain_utils import get_domain_id +from cosmos3._src.vfm.datasets.action.libero_pose_utils import ( + libero_action_dim, + libero_rotation_format, +) +from cosmos3._src.vfm.datasets.action.pose_utils import ( + compute_idle_frames, + convert_rotation, +) + +LIBERO_ROOTS: list[str] = [ + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_10_no_noops_1.0.0_lerobot_aligned", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_90_no_noops_lerobot_shuffled", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_object_no_noops_1.0.0_lerobot_aligned", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_spatial_no_noops_1.0.0_lerobot", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_goal_no_noops_1.0.0_lerobot", +] + + +class LIBERODataset(Dataset): + """ + A Dataset wrapper for LeRobot LIBERO dataset(s) designed for training from local storage. + + This dataset: + - Loads data from local storage using LeRobotDataset + - Supports multiple dataset roots that are concatenated into one dataset + - Supports configurable camera modes (image, wrist_image, or concat_view) + - Filters episodes for train/val split + - Filters frames at episode boundaries (to avoid padding issues with delta timestamps) + - Uses task descriptions from meta/tasks.parquet for ai_caption + """ + + _NORMALIZERS_DIR = Path(__file__).parent / "normalizers" + + def __init__( + self, + repo_id: str | list[str] = "lerobot/libero_90", + root: str | list[str] | None = LIBERO_ROOTS, + image_size: int = 256, + chunk_length: int = 16, # must be divisible by 4 + fps: int = 10, # IMPORTANT! LIBERO is at 20fps. If using frame_wise_relative in policy mode, we have to match the fps. + mode: str = "policy", + video_backend: str | None = "torchcodec", + download_videos: bool = False, + force_cache_sync: bool = False, + tolerance_s: float = 1e-4, + split: str = "train", + val_ratio: float = 0.01, + seed: int = 0, + # Camera configuration + camera_mode: str = "image", # 'image', 'wrist_image', or 'concat_view' + # Action configuration + action_space: str = "frame_wise_relative", # "absolute" or "relative" or "frame_wise_relative" + # rotation_space + rotation_space: Literal["9d", "6d", "3d"] = "3d", + # Native simulator frame or shared OpenCV-style EE frame used by midtraining. + pose_coordinate_frame: Literal["native", "opencv"] = "native", + # domain-aware configuration + embodiment_type: str = "libero", + action_normalization: Literal["quantile", "quantile_rot", "meanstd", "minmax"] | None = None, + action_stats_path: str | None = None, + skip_video_loading: bool = False, + ): + super().__init__() + self._embodiment_type = embodiment_type + self.domain_id = get_domain_id(embodiment_type) + self.image_size = image_size + self.chunk_length = chunk_length + assert self.chunk_length % 4 == 0, "chunk_length must be divisible by 4" + self.fps = fps + self.mode = mode + self.split = split.lower().strip() + self.val_ratio = val_ratio + self.seed = seed + self.camera_mode = camera_mode.lower().strip() + self.action_space = action_space + self.action_normalization = action_normalization + self.rotation_space = rotation_space.lower().strip() + self.pose_coordinate_frame = pose_coordinate_frame + self._pose_convention = self.action_space + self._rotation_format = libero_rotation_format(self.rotation_space) + # When True, skip video decoding entirely: drop image keys from + # delta_timestamps so LeRobot never touches the mp4, and return + # ``video=None`` in __getitem__. Must be set at construction time + # because LeRobotDataset is eagerly built in __init__. + self._skip_video_loading = bool(skip_video_loading) + + # Load action normalization stats. ``action_min`` / ``action_range`` are + # retained for older LIBERO eval code that knows how to invert a + # range-style [-1, 1] normalization. + self._norm_stats: dict[str, torch.Tensor] | None = None + self.action_min: torch.Tensor | None = None + self.action_max: torch.Tensor | None = None + self.action_range: torch.Tensor | None = None + if self.action_normalization is not None: + stats_path = self._resolve_action_stats_path(action_stats_path) + stats_key = "global_raw" if self.action_normalization == "quantile_rot" else "global" + raw_stats = load_action_stats(str(stats_path), stats_key=stats_key) + self._norm_stats = {} + for key, value in raw_stats.items(): + self._norm_stats[key] = torch.from_numpy(value).float() # [D] + self._set_range_denormalization_stats() + log.info( + f"Loaded LIBERO action stats from {stats_path} with action_normalization={self.action_normalization}" + ) + + # Validate camera mode + if self.camera_mode not in {"image", "wrist_image", "concat_view"}: + raise ValueError(f"Unsupported camera_mode={camera_mode!r}. Use 'image', 'wrist_image', or 'concat_view'.") + + # Validate split + if self.split not in {"train", "val", "valid", "validation", "eval", "test", "full"}: + raise ValueError(f"Unsupported {split=}. Use train/val/full.") + + # Build delta timestamps based on camera mode + dt = 1.0 / self.fps + + if self.fps != 20: + log.warning( + f"LIBERO is at 20fps. If using frame_wise_relative for policy mode training, we have to match the fps. fps={self.fps}" + ) + + # Determine which image keys to use + if self.camera_mode == "image": + self.image_keys = ["observation.images.image"] + elif self.camera_mode == "wrist_image": + self.image_keys = ["observation.images.wrist_image"] + else: # concat_view + self.image_keys = ["observation.images.image", "observation.images.wrist_image"] + + # Build delta_timestamps for all keys (same convention as PushT: 0 to chunk_length) + self.delta_timestamps: dict[str, list[float]] = {} + if not self._skip_video_loading: + for key in self.image_keys: + self.delta_timestamps[key] = [i * dt for i in range(0, chunk_length + 1)] + self.delta_timestamps["observation.state"] = [i * dt for i in range(0, chunk_length + 1)] + self.delta_timestamps["action"] = [i * dt for i in range(0, chunk_length + 1)] + + # Normalize repo_id and root to lists + repo_id_list: list[str] = [repo_id] if isinstance(repo_id, str) else list(repo_id) + root_list: list[str | None] + if root is None: + root_list = [None for _ in repo_id_list] + elif isinstance(root, str): + root_list = [root] + else: + root_list = [r for r in root] + + if len(repo_id_list) != len(root_list): + raise ValueError( + f"Length mismatch: repo_id has {len(repo_id_list)} items, root has {len(root_list)} items." + ) + + # Load all datasets + self.datasets: list[LeRobotDataset] = [] + self.tasks_dfs: list = [] # Store tasks DataFrames for each dataset + for rid, r in zip(repo_id_list, root_list): + dataset = LeRobotDataset( + repo_id=rid, + root=r, + delta_timestamps=self.delta_timestamps, # type: ignore + tolerance_s=tolerance_s, + force_cache_sync=force_cache_sync, + download_videos=download_videos, + video_backend=video_backend, + episodes=None, # Load full dataset, filter later + ) + self.datasets.append(dataset) + self.tasks_dfs.append(dataset.meta.tasks) + + # Build index mapping: list of (dataset_idx, local_idx) for valid frames + self.index_map: list[tuple[int, int, int]] = [] # (dataset_idx, local_idx, episode_idx) + self._episode_boundaries: list[dict[int, tuple[int, int]]] = [] + self._episode_splits: list[tuple[set[int], set[int]]] = [] + + total_episodes = 0 + total_frames = 0 + for ds_idx, dataset in enumerate(self.datasets): + # Compute episode splits for this dataset + train_eps, val_eps = self._compute_episode_splits_for_dataset(dataset) + self._episode_splits.append((train_eps, val_eps)) + + # Get episodes for current split + split_episodes = self._get_split_episodes_for_dataset(ds_idx) + + # Build episode boundaries + boundaries = self._build_episode_boundaries_for_dataset(dataset) + self._episode_boundaries.append(boundaries) + + # Filter indices + indices = self._filter_indices_for_dataset(ds_idx, dataset, split_episodes, boundaries) + self.index_map.extend(indices) + + total_episodes += dataset.num_episodes + total_frames += len(dataset) + + log.info( + f"Loaded LIBERO dataset with {len(repo_id_list)} source(s) split={self.split!r} " + f"camera_mode={self.camera_mode!r} " + f"total_episodes={total_episodes} " + f"total_frames={total_frames} " + f"valid_indices={len(self.index_map)}" + ) + + def _compute_episode_splits_for_dataset(self, dataset: LeRobotDataset) -> tuple[set[int], set[int]]: + """Compute train/val episode splits deterministically for a single dataset.""" + total_episodes = int(dataset.meta.total_episodes) + + if not (0.0 < self.val_ratio < 1.0): + raise ValueError(f"{self.val_ratio=} must be in (0, 1).") + + n_val = max(1, int(round(total_episodes * self.val_ratio))) + # val_eps = set(range(n_val)) + # train_eps = set(range(n_val, total_episodes)) + + # Yihuai: Randomly select validation episodes instead of the first n_val episodes (otherwise task will be repeated) + rng = random.Random(self.seed) # To ensure validation episodes are the same on all ranks + val_eps = set(rng.sample(range(total_episodes), n_val)) + train_eps = set(range(total_episodes)) - val_eps + + log.info(f"train_eps={train_eps}, val_eps={val_eps}") + + return train_eps, val_eps + + def _get_split_episodes_for_dataset(self, ds_idx: int) -> set[int]: + """Get the episode set for the current split for a specific dataset.""" + train_eps, val_eps = self._episode_splits[ds_idx] + if self.split in {"val", "valid", "validation", "eval", "test"}: + return val_eps + elif self.split == "train": + return train_eps + else: # full + return train_eps | val_eps + + def _build_episode_boundaries_for_dataset(self, dataset: LeRobotDataset) -> dict[int, tuple[int, int]]: + """Build a dict of episode_index -> (start_frame, end_frame) for a single dataset.""" + boundaries: dict[int, tuple[int, int]] = {} + for ep in dataset.meta.episodes: + ep_idx = int(ep["episode_index"]) # type: ignore[index] + start = int(ep["dataset_from_index"]) # type: ignore[index] + end = int(ep["dataset_to_index"]) # type: ignore[index] + boundaries[ep_idx] = (start, end) + return boundaries + + def _filter_indices_for_dataset( + self, + ds_idx: int, + dataset: LeRobotDataset, + split_episodes: set[int], + boundaries: dict[int, tuple[int, int]], + ) -> list[tuple[int, int, int]]: + """Filter valid indices for a single dataset, returning (dataset_idx, local_idx, episode_idx).""" + index_map: list[tuple[int, int, int]] = [] + all_meta = list(dataset.meta.episodes) + + for ep_idx in split_episodes: + if ep_idx >= len(all_meta): + continue + ep = all_meta[ep_idx] + + ep_start = int(ep["dataset_from_index"]) # type: ignore[index] + ep_end = int(ep["dataset_to_index"]) # type: ignore[index] + + # Valid range: [start, end - chunk_length - 1] inclusive + # We drop chunk_length frames at end to ensure we can query up to delta=chunk_length. + start = ep_start + end = ep_end - self.chunk_length - 1 + + if end >= start: + for local_idx in range(start, end + 1): + index_map.append((ds_idx, local_idx, ep_idx)) + + return index_map + + def __len__(self) -> int: + return len(self.index_map) + + def _get_task_description(self, ds_idx: int, item: dict) -> str: + """Get task description for the current item from meta/tasks.parquet. + + The tasks.parquet has task descriptions as the DataFrame index (row labels) + and task_index as an integer column. We look up by task_index and return + the corresponding index name (the actual task description string). + """ + task_idx = item.get("task_index") + if task_idx is not None: + if isinstance(task_idx, torch.Tensor): + task_idx = task_idx.item() + task_idx = int(task_idx) + tasks_df = self.tasks_dfs[ds_idx] + if task_idx in tasks_df["task_index"].values: + row = tasks_df[tasks_df["task_index"] == task_idx].iloc[0] + # The task description is the index name (row label), not a column value + return str(row.name) + raise ValueError(f"Task index {task_idx} not found in tasks.parquet for dataset {ds_idx}") + + def _compute_anchored_actions( + self, + state_raw: torch.Tensor, + action_raw: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Compute anchored relative actions (batched). + + Converts frame-wise relative actions to anchored relative actions where each + action[t] represents the target pose (after applying action[t] to state[t]) + expressed in state 0's local coordinate frame. + + Mathematical formulation: + 1. Compute target in world frame (LIBERO convention): + - p_{t+1} = p_t + delta_p[t] (position addition in world frame) + - R_{t+1} = R_delta[t] @ R_t (rotation composition, delta first) + 2. Compute anchored (left-multiply by T_0^{-1}): + - anchored_pos[t] = R_0^T @ (p_{t+1} - p_0) + - anchored_rot[t] = R_0^T @ R_{t+1} + + Args: + state_raw: State tensor of shape (T+1, 8): [x, y, z, ax, ay, az, grip1, grip2] + where (ax, ay, az) is axis-angle rotation. + action_raw: Action tensor of shape (T+1, 7): [dx, dy, dz, dax, day, daz, grip] + where (dax, day, daz) is axis-angle rotation delta. + + Returns: + anchored_translation: (T, 3) - position in state_0's local frame + anchored_rotation_9d: (T, 9) - rotation relative to state_0 as flattened 3x3 matrix + gripper: (T, 1) - original gripper commands (unchanged) + """ + # Extract positions and rotations from states + p_states = state_raw[:, :3] # [T+1,3] + rotvec_states = state_raw[:, 3:6] # [T+1,3] - axis-angle + + # Extract deltas from actions (use first T actions) + delta_p = action_raw[:-1, :3] # [T,3] + delta_rotvec = action_raw[:-1, 3:6] # [T,3] - axis-angle delta + gripper = action_raw[:-1, 6:7] # [T,1] + + # Convert all axis-angle to rotation matrices (batched) + R_states = convert_rotation(rotvec_states, input_format="axisangle", output_format="matrix") # [T+1,3,3] + R_deltas = convert_rotation(delta_rotvec, input_format="axisangle", output_format="matrix") # [T,3,3] + + # Initial pose (state 0) + p_0 = p_states[0] # [3] + R_0 = R_states[0] # [3,3] + R_0_T = R_0.T # [3,3] - transpose for inverse rotation + + # Current states for t = 0..T-1 + p_t = p_states[:-1] # [T,3] + R_t = R_states[:-1] # [T,3,3] + + # Step 1: Compute target poses in world frame (LIBERO convention) + # p_target = p_t + delta_p + p_target = p_t + delta_p # [T,3] + + # R_target = R_delta @ R_t (batched matrix multiply) + R_target = torch.bmm(R_deltas, R_t) # [T,3,3] + + # Step 2: Compute anchored (in state_0's local frame) + # anchored_p = R_0^T @ (p_target - p_0) + displacement = p_target - p_0 # [T,3] + anchored_p = (R_0_T @ displacement.T).T # [T,3] + + # anchored_R = R_0^T @ R_target (batched) + R_0_T_expanded = R_0_T.unsqueeze(0).expand(R_target.shape[0], -1, -1) # [T,3,3] + anchored_R = torch.bmm(R_0_T_expanded, R_target) # [T,3,3] + + return anchored_p, anchored_R, gripper + + def _convert_rotation_to_repr(self, rotation_matrix: torch.Tensor) -> torch.Tensor: + """Convert rotation matrix to the desired representation. + + Args: + rotation_matrix: Rotation matrices of shape (T, 3, 3). + + Returns: + Rotation in the configured ``rotation_space`` format. + """ + return convert_rotation(rotation_matrix, "matrix", libero_rotation_format(self.rotation_space)) + + def _normalizer_filename(self) -> str: + rotation_suffix = { + "3d": "3d", + "6d": "rot6d", + "9d": "rot9d", + }.get(self.rotation_space) + if rotation_suffix is None: + raise ValueError(f"Unsupported rotation_space={self.rotation_space!r}.") + action_space = self.action_space.replace("-", "_") + return f"{self._embodiment_type}_{action_space}_{rotation_suffix}.json" + + def _resolve_action_stats_path(self, action_stats_path: str | None) -> Path: + if action_stats_path is None: + stats_path = self._NORMALIZERS_DIR / self._normalizer_filename() + if stats_path.exists(): + return stats_path + raise FileNotFoundError( + f"Could not find bundled LIBERO action stats at {stats_path}. " + "Pass action_stats_path explicitly or regenerate stats with compute_action_stats.py." + ) + + stats_path = Path(action_stats_path) + if stats_path.is_absolute(): + if stats_path.exists(): + return stats_path + raise FileNotFoundError(f"Could not find action_stats_path={action_stats_path!r}.") + + module_dir = Path(__file__).resolve().parent + candidates: list[Path] = [] + for parent in module_dir.parents: + candidates.append(parent / stats_path) + candidates.append(self._NORMALIZERS_DIR / stats_path.name) + candidates.append(module_dir / stats_path.name) + for candidate in candidates: + if candidate.exists(): + return candidate + raise FileNotFoundError( + f"Could not resolve action_stats_path={action_stats_path!r}; tried: {[str(c) for c in candidates]}" + ) + + def _set_range_denormalization_stats(self) -> None: + if self._norm_stats is None: + return + + if self.action_normalization == "minmax": + lo_key, hi_key = "min", "max" + elif self.action_normalization in ("quantile", "quantile_rot"): + lo_key, hi_key = "q01", "q99" + else: + return + + if lo_key not in self._norm_stats or hi_key not in self._norm_stats: + raise ValueError( + f"Action stats for {self.action_normalization!r} normalization require " + f"{lo_key!r} and {hi_key!r} entries." + ) + self.action_min = self._norm_stats[lo_key] # [D] + self.action_max = self._norm_stats[hi_key] # [D] + action_range = self.action_max - self.action_min # [D] + self.action_range = torch.clamp(action_range, min=1e-6) # [D] + + def __getitem__(self, idx: int, _retry_count: int = 0) -> dict[str, torch.Tensor | str]: + """Get a single item from the dataset.""" + max_retries = 10 + ds_idx, local_idx, ep_idx = self.index_map[idx] + dataset = self.datasets[ds_idx] + try: + item = dataset[local_idx] + except Exception as e: + log.warning( + f"Error loading item (retry {_retry_count}/{max_retries}): idx={idx}, ds_idx={ds_idx}, " + f"local_idx={local_idx}, ep_idx={ep_idx}, repo_id={dataset.meta.repo_id}, error={e}" + ) + if _retry_count >= max_retries: + raise RuntimeError(f"Failed to load data after {max_retries} retries") from e + new_idx = random.randint(0, len(self) - 1) + return self.__getitem__(new_idx, _retry_count + 1) + + if self.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy", "image2video"]) + else: + mode = self.mode + + # Get task description for ai_caption + task_description = self._get_task_description(ds_idx, item) + + # Process video based on camera mode (skipped entirely when + # skip_video_loading=True; image keys are also absent from + # delta_timestamps so LeRobot never decoded them). + video: torch.Tensor | None + if self._skip_video_loading: + video = None + else: + if self.camera_mode == "concat_view": + # Load both cameras and concatenate horizontally + video_1: torch.Tensor = item["observation.images.image"] + video_2: torch.Tensor = item["observation.images.wrist_image"] + + # Resize each if needed + if video_1.shape[-1] != self.image_size or video_1.shape[-2] != self.image_size: + video_1 = F.resize(video_1, [self.image_size, self.image_size]) + if video_2.shape[-1] != self.image_size or video_2.shape[-2] != self.image_size: + video_2 = F.resize(video_2, [self.image_size, self.image_size]) + + # Concatenate along width dimension (last dim for TCHW) + video_tchw = torch.cat([video_1, video_2], dim=-1) # (T, C, H, W*2) + else: + # Single camera mode + image_key = self.image_keys[0] + video_tchw = item[image_key] + + # Resize if needed + if video_tchw.shape[-1] != self.image_size or video_tchw.shape[-2] != self.image_size: + video_tchw = F.resize(video_tchw, [self.image_size, self.image_size]) + + # Convert to uint8 and transpose to (C, T, H, W) + video = (video_tchw * 255).clamp(0, 255).to(torch.uint8).permute(1, 0, 2, 3) + + # Action (raw): LIBERO actions are 7D (6 DoF + gripper) + action_raw: torch.Tensor = item["action"] + # State (raw): LIBERO state is 8D (6 DoF + 2 gripper states) + state_raw: torch.Tensor = item["observation.state"] + + # Action: (T+1, D) -> (T, D) + # Take all but last action + # LIBERO action format: [x, y, z, ax, ay, az, gripper] (7D) where (ax,ay,az) is axis-angle + + if self.action_space == "relative": + # Compute anchored relative actions + # Returns: translation (T, 3), rotation_matrix (T, 3, 3), gripper (T, 1) + translation, rotation_matrix, gripper = self._compute_anchored_actions(state_raw, action_raw.clone()) + elif self.action_space == "frame_wise_relative": + action = action_raw[:-1].clone() # [T,7] + translation = action[:, :3] # [T,3] + rotation_rotvec = action[:, 3:6] # [T,3] + gripper = action[:, 6:] # [T,1] + rotation_matrix = convert_rotation( + rotation_rotvec, input_format="axisangle", output_format="matrix" + ) # [T,3,3] + else: + raise ValueError(f"Unsupported action space: {self.action_space}") + + rotation = self._convert_rotation_to_repr(rotation_matrix) # [T,rot_dim] + action = torch.cat([translation, rotation, gripper], dim=-1) # [T,action_dim] + + # Compute idle_frames from the raw (un-normalized) action, only when the + # action layout has correct per-frame idle semantics (frame_wise_relative + # ⇔ backward_framewise). The other action_spaces ("relative", + # "absolute") encode per-frame motion differently and would not give + # meaningful idle counts under the same threshold check. + idle_frames: torch.Tensor | None = None + if self.action_space == "frame_wise_relative": + try: + spec = build_action_spec(Pos(), Rot(libero_rotation_format(self.rotation_space)), Gripper()) + n = compute_idle_frames(action, spec) + idle_frames = torch.tensor(n, dtype=torch.long) + except (ValueError, TypeError): + idle_frames = None + + if self.action_normalization is not None and self._norm_stats is not None and self.action_min is not None: + if action.shape[-1] != self.action_min.shape[0]: + raise ValueError( + f"Action dimension {action.shape[-1]} does not match stats dimension " + f"{self.action_min.shape[0]}. Recompute stats for the current " + f"rotation_space={self.rotation_space!r} and action_space={self.action_space!r}." + ) + method = "quantile" if self.action_normalization == "quantile_rot" else self.action_normalization + action = normalize_action(action, method, self._norm_stats) # [T,D] + + # Index + key = torch.tensor([local_idx], dtype=torch.long) + + if self.camera_mode == "image": + viewpoint = "third_person_view" + elif self.camera_mode == "wrist_image": + viewpoint = "wrist_view" + else: + viewpoint = "concat_view" + + result: dict[str, torch.Tensor | str] = { + "source_repo_id": dataset.meta.repo_id, + "video": video, + "action": action, + "action_raw": action_raw, + "conditioning_fps": torch.tensor(self.fps, dtype=torch.long), + "prompt": task_description, + "ai_caption": task_description, + "mode": mode, + "state": state_raw, + "action_space": self.action_space, + "rotation_space": self.rotation_space, + "pose_coordinate_frame": self.pose_coordinate_frame, + "__key__": key, + "domain_id": torch.tensor(self.domain_id, dtype=torch.long), + "viewpoint": viewpoint, + } + if idle_frames is not None: + result["idle_frames"] = idle_frames + + if self.camera_mode == "concat_view" and not self._skip_video_loading: + result["additional_view_description"] = ( + "The left half shows the third-person view; the right half shows the wrist-mounted camera." + ) + + return result + + @property + def action_dim(self) -> int: + return libero_action_dim(self.rotation_space) diff --git a/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py b/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py new file mode 100644 index 00000000..a4207755 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Small LIBERO pose helpers shared by training and closed-loop eval.""" + +from __future__ import annotations + +import numpy as np +import torch + +from cosmos3._src.vfm.datasets.action.pose_utils import ( + RotationConvention, + build_abs_pose_from_components, +) + +# Same local-frame post-rotation pattern used by DROID/Bridge/Fractal: +# R_opencv = R_native @ *_TO_OPENCV. +LIBERO_TO_OPENCV: np.ndarray = np.array( + [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + dtype=np.float32, +) + +LIBERO_ROTATION_FORMATS: dict[str, RotationConvention] = { + "3d": "axisangle", + "6d": "rot6d", + "9d": "rot9d", +} +LIBERO_ACTION_DIMS: dict[str, int] = {"3d": 7, "6d": 10, "9d": 13} + + +def libero_rotation_format(rotation_space: str) -> RotationConvention: + """Return the shared ``pose_utils`` rotation format for a LIBERO setting.""" + rotation_format = LIBERO_ROTATION_FORMATS.get(rotation_space) + if rotation_format is None: + raise ValueError(f"Unsupported rotation_space={rotation_space!r}. Use 3d/6d/9d.") + return rotation_format + + +def libero_action_dim(rotation_space: str) -> int: + """Return ``[xyz, rotation, gripper]`` action width for LIBERO.""" + action_dim = LIBERO_ACTION_DIMS.get(rotation_space) + if action_dim is None: + raise ValueError(f"Unsupported rotation_space={rotation_space!r}. Use 3d/6d/9d.") + return action_dim + + +def libero_rotation_space_from_action_dim(action_dim: int) -> str: + """Infer LIBERO rotation space from unpadded action width.""" + for rotation_space, dim in LIBERO_ACTION_DIMS.items(): + if dim == action_dim: + return rotation_space + raise ValueError(f"Unable to infer rotation_space from action_dim={action_dim}.") + + +def build_libero_abs_pose(state_raw: torch.Tensor | np.ndarray, *, to_opencv: bool) -> np.ndarray: + """Build absolute LIBERO EE poses from state rows. + + ``state_raw`` is ``[x,y,z,axisangle(3),gripper(2)]``. When requested, the + local EE frame is post-rotated into the shared OpenCV-style action frame. + """ + if isinstance(state_raw, torch.Tensor): + state_np = state_raw.detach().cpu().numpy().astype(np.float32, copy=False) + else: + state_np = np.asarray(state_raw, dtype=np.float32) + + poses_abs = build_abs_pose_from_components(state_np[:, :3], state_np[:, 3:6], "axisangle") + if to_opencv: + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ LIBERO_TO_OPENCV + return poses_abs diff --git a/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json b/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json new file mode 100644 index 00000000..6cde6705 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json @@ -0,0 +1,37 @@ +{ + "metadata": { + "embodiment_type": "libero", + "pose_convention": "frame_wise_relative", + "pose_coordinate_frame": "native", + "rotation_format": "6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": null, + "dataset_name": "libero", + "dataset_class": "LIBERODataset", + "dataset_root": ["outputs/libero_datasets/libero_10", "outputs/libero_datasets/libero_object", "outputs/libero_datasets/libero_spatial", "outputs/libero_datasets/libero_goal"], + "_comment": "Dataset paths are placeholders; the statistics values are independent of local dataset location.", + "split": "train", + "num_samples_stats": 10000, + "reservoir_size": 50000, + "max_samples": 10000, + "sampling_seed": 42 + }, + "global": { + "mean": [ 0.050704, 0.097407, -0.094833, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.476725], + "std": [ 0.333621, 0.387175, 0.457140, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.499460], + "min": [-0.937500, -0.937500, -0.937500, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.937500, 0.937500, 0.937500, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.723214, -0.808929, -0.937500, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.937500, 0.870536, 0.937500, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [ 0.050704, 0.097407, -0.094833, 0.994873, -0.004579, -0.004288, 0.004389, 0.996104, 0.001109, 0.476725], + "std": [ 0.333621, 0.387175, 0.457140, 0.010807, 0.077802, 0.063386, 0.078571, 0.009994, 0.038504, 0.499460], + "min": [-0.937500, -0.937500, -0.937500, 0.902028, -0.356085, -0.367416, -0.370434, 0.921907, -0.255000, 0.000000], + "max": [ 0.937500, 0.937500, 0.937500, 1.000000, 0.368853, 0.341214, 0.356395, 1.000000, 0.348251, 1.000000], + "q01": [-0.723214, -0.808929, -0.937500, 0.934955, -0.223431, -0.189878, -0.334735, 0.938516, -0.107736, 0.000000], + "q99": [ 0.937500, 0.870536, 0.937500, 1.000000, 0.331000, 0.163153, 0.226216, 1.000000, 0.127158, 1.000000] + } +} diff --git a/cosmos3/_src/vfm/datasets/action/pose_utils.py b/cosmos3/_src/vfm/datasets/action/pose_utils.py new file mode 100644 index 00000000..491db4ce --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/pose_utils.py @@ -0,0 +1,759 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rotation and pose utilities for action datasets. + +This module centralizes three related responsibilities used across the action +dataset stack: + +1. Converting rotations between the conventions used by the datasets and the + action model (`euler_xyz`, quaternion, axis-angle, rot6d, rot9d, matrix). +2. Building absolute homogeneous poses of shape ``(T, 4, 4)`` from per-frame + translation and rotation components. +3. Converting trajectories between absolute-pose form and the relative-pose + action vectors consumed by the datasets. + + The relative-pose action vectors always follow the shared layout + ``[translation(3), rotation(...)]``. The rotation block is encoded with the + requested rotation output convention, and `convert_rotation()` is the + canonical public entrypoint for representation conversion. +""" + +import math +from typing import Literal + +import numpy as np +import torch +from scipy.spatial.transform import Rotation as R + +PoseConvention = Literal["absolute", "backward_anchored", "backward_framewise"] +RotationConvention = Literal["matrix", "euler_xyz", "quat_xyzw", "quat_wxyz", "rot6d", "axisangle", "rot9d"] + + +def _to_numpy_float32(array: torch.Tensor | np.ndarray) -> np.ndarray: + """Convert an input array to a NumPy ``float32`` array. + + Args: + array: A torch tensor or NumPy array with arbitrary leading dimensions. + + Returns: + A NumPy array with dtype ``float32``. Torch tensors are moved to CPU + before conversion. NumPy inputs are converted with ``copy=False`` + semantics when possible. + + Raises: + ValueError: If a torch tensor with ``requires_grad=True`` is passed. + These utilities are non-differentiable; callers must explicitly + detach tensors before conversion. + """ + if isinstance(array, torch.Tensor): + if array.requires_grad: + raise ValueError( + "pose_utils conversion is non-differentiable; call `.detach()` " + "explicitly before passing tensors with requires_grad=True" + ) + return array.cpu().numpy().astype(np.float32, copy=False) + return np.asarray(array, dtype=np.float32) + + +def _normalize_rotation_matrices(rot_matrices: np.ndarray) -> np.ndarray: + """Project approximate matrices onto valid rotation matrices. + + This helper uses an SVD-based projection onto ``SO(3)``. It is mainly used + when decoding rotations from network-like representations such as rot6d or rot9d + where the input may not already be perfectly orthonormal. + + Args: + rot_matrices: Array of shape ``(..., 3, 3)`` containing one or more + approximate rotation matrices. + + Returns: + Array of shape ``(..., 3, 3)`` whose trailing matrices are proper + rotation matrices with determinant ``+1``. + + Raises: + ValueError: If the input does not have trailing shape ``(3, 3)``. + """ + matrices = np.asarray(rot_matrices, dtype=np.float32) + if matrices.ndim < 2 or matrices.shape[-2:] != (3, 3): + raise ValueError(f"Rotation matrices must have shape (..., 3, 3), got {matrices.shape}") + + original_shape = matrices.shape[:-2] + matrices_flat = matrices.reshape(-1, 3, 3) + + # Batched SVD projection to SO(3). + U, _, Vt = np.linalg.svd(matrices_flat) + normalized = U @ Vt + + # Ensure determinant is +1 (proper rotations, no reflections). + det = np.linalg.det(normalized) + reflection_mask = det < 0 + if np.any(reflection_mask): + U_reflect = U.copy() + U_reflect[reflection_mask, :, -1] *= -1 + normalized[reflection_mask] = U_reflect[reflection_mask] @ Vt[reflection_mask] + + return normalized.astype(np.float32, copy=False).reshape(*original_shape, 3, 3) + + +def convert_rotation( + rotation: torch.Tensor | np.ndarray, + input_format: RotationConvention, + output_format: RotationConvention, + normalize_matrix: bool = False, +) -> torch.Tensor | np.ndarray: + """Convert rotations between the conventions used by action datasets. + + The function first maps the input representation to rotation matrices and + then emits the requested output convention. It is the single conversion seam + used by the public pose helpers so that all code paths share the same + convention handling. + + Supported input conventions: + - ``matrix``: rotation matrices with shape ``(..., 3, 3)`` + - ``euler_xyz``: Euler xyz angles in radians with shape ``(..., 3)`` + - ``quat_xyzw``: quaternions in SciPy's xyzw order with shape ``(..., 4)`` + - ``quat_wxyz``: quaternions in wxyz order with shape ``(..., 4)`` + - ``rot6d``: column-based 6D representation with shape ``(..., 6)`` + - ``rot9d``: flattened rotation matrices with shape ``(..., 9)`` + - ``axisangle``: axis-angle vectors with shape ``(..., 3)`` + + Supported output conventions: + - ``matrix`` + - ``euler_xyz`` + - ``quat_xyzw`` + - ``quat_wxyz`` + - ``rot6d`` + - ``axisangle`` + - ``rot9d`` + + Args: + rotation: Input rotations in the representation specified by + ``input_format``. + input_format: Convention used by ``rotation``. + output_format: Convention to return. + normalize_matrix: Whether to project intermediate matrices to a valid + rotation before returning. This is most useful when decoding from + approximate ``rot6d``/``rot9d`` inputs or non-unit quaternions. + + Returns: + Rotations with the same leading shape as the input, expressed in the + requested output convention. Torch inputs return torch outputs on the + same device with the same dtype; NumPy inputs return NumPy arrays. + + Raises: + ValueError: If the input shape is incompatible with ``input_format`` or + if either format is unsupported. + """ + input_is_tensor = isinstance(rotation, torch.Tensor) + input_dtype = rotation.dtype if input_is_tensor else None + input_device = rotation.device if input_is_tensor else None + rotation_np = _to_numpy_float32(rotation) + + if input_format == "matrix": + if rotation_np.ndim < 2 or rotation_np.shape[-2:] != (3, 3): + raise ValueError(f"matrix rotation must have shape (..., 3, 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-2] + matrices_flat = rotation_np.reshape(-1, 3, 3) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "euler_xyz": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 3: + raise ValueError(f"{input_format} rotation must have shape (..., 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = R.from_euler("xyz", rotation_np.reshape(-1, 3), degrees=False).as_matrix().astype(np.float32) + elif input_format in ("quat_xyzw", "quat_wxyz"): + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 4: + raise ValueError(f"{input_format} rotation must have shape (..., 4), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + quaternions = rotation_np.reshape(-1, 4) + if input_format == "quat_wxyz": + quaternions = quaternions[:, [1, 2, 3, 0]] + norms = np.linalg.norm(quaternions, axis=-1) + if np.any(norms < 1e-8): + raise ValueError(f"Found zero-norm quaternion(s) (min norm={norms.min():.2e}).") + if normalize_matrix: + quaternions = quaternions / norms[:, None] + matrices_flat = R.from_quat(quaternions).as_matrix().astype(np.float32) + elif input_format == "rot6d": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 6: + raise ValueError(f"{input_format} rotation must have shape (..., 6), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + rot6d_flat = rotation_np.reshape(-1, 6) + col0 = rot6d_flat[:, :3] + col1 = rot6d_flat[:, 3:] + col2 = np.cross(col0, col1, axis=-1) + matrices_flat = np.stack((col0, col1, col2), axis=-1).astype(np.float32) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "rot9d": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 9: + raise ValueError(f"rot9d rotation must have shape (..., 9), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = rotation_np.reshape(-1, 3, 3) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "axisangle": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 3: + raise ValueError(f"axisangle rotation must have shape (..., 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = R.from_rotvec(rotation_np.reshape(-1, 3)).as_matrix().astype(np.float32) + else: + raise ValueError(f"Unsupported input_format: {input_format!r}") + + if output_format == "matrix": + converted = matrices_flat.reshape(*original_shape, 3, 3).astype(np.float32) + elif output_format == "rot9d": + converted = matrices_flat.reshape(-1, 9) + elif output_format == "rot6d": + converted = matrices_flat[:, :, :2].transpose(0, 2, 1).reshape(-1, 6) + elif output_format == "quat_xyzw": + converted = R.from_matrix(matrices_flat).as_quat().astype(np.float32) + elif output_format == "quat_wxyz": + converted = R.from_matrix(matrices_flat).as_quat().astype(np.float32) + converted = converted[:, [3, 0, 1, 2]] + elif output_format == "euler_xyz": + converted = R.from_matrix(matrices_flat).as_euler("xyz", degrees=False).astype(np.float32) + elif output_format == "axisangle": + converted = R.from_matrix(matrices_flat).as_rotvec().astype(np.float32) + else: + raise ValueError(f"Unsupported output_format: {output_format!r}") + + if output_format != "matrix": + converted = converted.reshape(*original_shape, converted.shape[-1]) + + if input_is_tensor: + return torch.from_numpy(np.ascontiguousarray(converted)).to(dtype=input_dtype, device=input_device) + return converted + + +# ----------------------------------------------------------------------------- +# Absolute pose construction +# ----------------------------------------------------------------------------- + + +def build_abs_pose_from_components( + xyz: torch.Tensor | np.ndarray, + rotation: torch.Tensor | np.ndarray, + rotation_input_format: Literal["euler_xyz", "quat_xyzw", "quat_wxyz", "axisangle"], + translation_scale: float | None = None, +) -> np.ndarray: + """Build absolute homogeneous poses from per-frame translation and rotation. + + This is the canonical helper for turning dataset-provided pose components + into a sequence of rigid transforms. Each output pose is a homogeneous + transform whose top-left ``3 x 3`` block stores rotation and whose last + column stores translation. + + Args: + xyz: Per-frame translations with shape ``(T, 3)``. + rotation: Per-frame rotations with shape ``(T, 3)`` for ``euler_xyz`` + and ``axisangle``, or ``(T, 4)`` for quaternion conventions. + rotation_input_format: Convention used by ``rotation``. Supported values + are ``euler_xyz``, ``quat_xyzw``, ``quat_wxyz``, and ``axisangle``. + translation_scale: Optional factor used to divide translations before + inserting them into the output poses. This is useful when upstream + data stores translations in a scaled unit. + + Returns: + Absolute poses with shape ``(T, 4, 4)`` and dtype ``float32``. + + Raises: + ValueError: If the translation and rotation arrays have incompatible + lengths or unsupported shapes, or if ``translation_scale`` is zero. + """ + xyz_np = _to_numpy_float32(xyz) + rotation_np = _to_numpy_float32(rotation) + + if xyz_np.ndim != 2 or xyz_np.shape[1] != 3: + raise ValueError(f"xyz must have shape (T, 3), got {xyz_np.shape}") + if rotation_np.ndim != 2: + raise ValueError(f"rotation must be 2D, got {rotation_np.shape}") + if rotation_np.shape[0] != xyz_np.shape[0]: + raise ValueError( + f"xyz and rotation must have the same length, got {xyz_np.shape[0]} and {rotation_np.shape[0]}" + ) + + rot_mats = np.asarray( + convert_rotation(rotation_np, input_format=rotation_input_format, output_format="matrix"), + dtype=np.float32, + ) + + if translation_scale is not None: + if translation_scale == 0: + raise ValueError("translation_scale must be non-zero") + xyz_np = xyz_np / float(translation_scale) + + poses_abs = np.eye(4, dtype=np.float32)[None].repeat(xyz_np.shape[0], axis=0) + poses_abs[:, :3, :3] = rot_mats.astype(np.float32) + poses_abs[:, :3, 3] = xyz_np + return poses_abs + + +# ----------------------------------------------------------------------------- +# Relative pose conversions +# ----------------------------------------------------------------------------- + + +def _delta_transform_to_pose_vector( + delta_T: np.ndarray, + rotation_output_format: RotationConvention, + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Encode a relative transform as an action vector. + + The shared action-vector layout is always ``[translation(3), rotation(...)]``. + The translation block is multiplied by ``translation_scale`` before concatenation, + and the rotation block is multiplied by ``rotation_scale``. + + Args: + delta_T: Relative transform of shape ``(4, 4)``. + rotation_output_format: Concrete convention used for the output rotation + block. + translation_scale: Scalar multiplier applied to the translation block. + rotation_scale: Scalar multiplier applied to the rotation block. Used to + match the loss scale of the rotation block to the translation block. + The decoder must divide by the same factor before reconstructing the + rotation matrix. + + Returns: + A ``float32`` action vector whose first three values are translation and + whose remaining values are the rotation in ``rotation_output_format``. + """ + delta_np = np.asarray(delta_T, dtype=np.float32) + if delta_np.shape != (4, 4): + raise ValueError(f"delta_T must have shape (4, 4), got {delta_np.shape}") + + translation = delta_np[:3, 3] * translation_scale + rotation = np.asarray( + convert_rotation(delta_np[:3, :3], input_format="matrix", output_format=rotation_output_format), + dtype=np.float32, + ) + rotation = rotation * rotation_scale + return np.concatenate([translation, rotation]).astype(np.float32) + + +def _pose_vector_to_delta_transform( + pose_vector: np.ndarray, + rotation_input_format: RotationConvention, + translation_scale: float, + normalize_rotation: bool, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Decode an action vector back into a relative homogeneous transform. + + This is the inverse of `_delta_transform_to_pose_vector()` when the same + rotation convention and scale are used. + + Args: + pose_vector: Relative-pose action vector with layout + ``[translation(3), rotation(...)]``. + rotation_input_format: Concrete convention used by the rotation block. + translation_scale: Scalar used to undo the translation scaling applied during + encoding. + normalize_rotation: Whether to project the decoded rotation to a valid + matrix before assembling the transform. + rotation_scale: Scalar used to undo the rotation scaling applied during + encoding. Must match the value used by + `_delta_transform_to_pose_vector()`. + + Returns: + A relative homogeneous transform with shape ``(4, 4)`` and dtype + ``float32``. + """ + pose_vector_np = np.asarray(pose_vector, dtype=np.float32) + rotation_block = pose_vector_np[3:] / rotation_scale + + rotation_matrix = np.asarray( + convert_rotation( + rotation_block, + input_format=rotation_input_format, + output_format="matrix", + normalize_matrix=normalize_rotation, + ), + dtype=np.float32, + ) + + delta_T = np.eye(4, dtype=np.float32) + delta_T[:3, 3] = pose_vector_np[:3] / translation_scale + delta_T[:3, :3] = rotation_matrix + return delta_T + + +def _get_relative_delta_transform( + poses_abs: np.ndarray, + inv_poses_abs: np.ndarray, + frame_idx: int, + pose_convention: PoseConvention, +) -> np.ndarray: + """Compute one relative transform from an absolute-pose trajectory. + + Args: + poses_abs: Absolute poses of shape ``(T, 4, 4)``. + inv_poses_abs: Precomputed inverses of ``poses_abs`` with the same shape. + frame_idx: Index of the step to encode, in ``[0, T - 2]``. + pose_convention: Pose convention controlling which two poses + define the delta and whether it is framewise or anchored. + + Returns: + The relative transform ``delta_T`` with shape ``(4, 4)`` for the + requested step and convention. + """ + if pose_convention == "backward_framewise": + return inv_poses_abs[frame_idx] @ poses_abs[frame_idx + 1] + if pose_convention == "backward_anchored": + return inv_poses_abs[0] @ poses_abs[frame_idx + 1] + raise ValueError( + f"Unsupported pose_convention={pose_convention!r}. Expected one of: backward_framewise, backward_anchored." + ) + + +def _apply_relative_delta_transform( + current_pose: np.ndarray, + initial_pose: np.ndarray, + delta_T: np.ndarray, + pose_convention: PoseConvention, +) -> np.ndarray: + """Recover the next absolute pose from a decoded relative transform. + + Args: + current_pose: The current reconstructed pose for framewise modes. + initial_pose: The anchor pose used by anchored modes. + delta_T: Relative transform for the current step. + pose_convention: Pose convention controlling how ``delta_T`` + should be composed back into an absolute pose. + + Returns: + The next absolute pose with shape ``(4, 4)``. + """ + if pose_convention == "backward_framewise": + return current_pose @ delta_T + if pose_convention == "backward_anchored": + return initial_pose @ delta_T + raise ValueError( + f"Unsupported pose_convention={pose_convention!r}. Expected one of: backward_framewise, backward_anchored." + ) + + +def pose_abs_to_rel( + poses_abs: np.ndarray, + rotation_format: RotationConvention = "rot9d", + pose_convention: PoseConvention = "backward_framewise", + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Convert an absolute-pose trajectory into relative-pose action vectors. + + Args: + poses_abs: Absolute poses with shape ``(T, 4, 4)``. These are typically + object-in-world or camera-to-world transforms. + rotation_format: Rotation convention used for the output rotation block. + Supported values are ``rot9d``, ``rot6d``, ``quat_xyzw``, and + ``euler_xyz``. + pose_convention: Pose convention: + - ``backward_framewise``: ``delta_T = T_i^{-1} @ T_{i+1}`` + - ``backward_anchored``: ``delta_T = T_0^{-1} @ T_{i+1}`` + translation_scale: Scalar multiplier applied to the translation block of each + encoded action vector. + rotation_scale: Scalar multiplier applied to the rotation block of each + encoded action vector. Use this to match the loss scale of rotation + and translation. `pose_rel_to_abs()` must be called with the same + value to invert the scaling. + + Returns: + An array of shape ``(T - 1, D)`` where ``D = 3 + rotation_dim``. + + Raises: + AssertionError: If fewer than two absolute poses are provided. + """ + num_frames = len(poses_abs) + assert num_frames > 1, "At least 2 frames are required to compute relative poses" + + # Compute inverse poses + inv_poses_abs = np.linalg.inv(poses_abs) + + poses_rel = [] + # We produce num_frames - 1 relative poses + for i in range(num_frames - 1): + delta_T = _get_relative_delta_transform(poses_abs, inv_poses_abs, i, pose_convention) + poses_rel.append( + _delta_transform_to_pose_vector( + delta_T, + rotation_output_format=rotation_format, + translation_scale=translation_scale, + rotation_scale=rotation_scale, + ) + ) + + return np.stack(poses_rel).astype(np.float32) # [T-1,D] + + +def pose_rel_to_abs( + poses_rel: np.ndarray, + rotation_format: RotationConvention = "rot9d", + pose_convention: PoseConvention = "backward_framewise", + initial_pose: np.ndarray | None = None, + normalize_rotation: bool = True, + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Reconstruct an absolute-pose trajectory from relative-pose action vectors. + + Args: + poses_rel: Relative-pose action vectors with shape ``(T - 1, D)`` and + layout ``[translation(3), rotation(...)]``. + rotation_format: Convention used by the rotation block of ``poses_rel``. + pose_convention: Pose convention used when the vectors were + encoded. This must match the convention passed to `pose_abs_to_rel()`. + initial_pose: Absolute pose for the first frame. If ``None``, the + identity transform is used. + normalize_rotation: Whether to project decoded rotations onto ``SO(3)`` + before composing them back into the trajectory. + translation_scale: Scalar used to undo the translation scaling applied during + `pose_abs_to_rel()`. + rotation_scale: Scalar used to undo the rotation scaling applied during + `pose_abs_to_rel()`. Must match the value passed there. + + Returns: + Absolute poses with shape ``(T, 4, 4)`` where ``T = len(poses_rel) + 1``. + """ + if initial_pose is None: + initial_pose = np.eye(4) + + poses_abs = [initial_pose] + current_pose = initial_pose + + num_poses_rel = poses_rel.shape[0] + + for i in range(num_poses_rel): + delta_T = _pose_vector_to_delta_transform( + poses_rel[i], + rotation_input_format=rotation_format, + translation_scale=translation_scale, + normalize_rotation=normalize_rotation, + rotation_scale=rotation_scale, + ) + next_pose = _apply_relative_delta_transform(current_pose, initial_pose, delta_T, pose_convention) + + poses_abs.append(next_pose) + current_pose = next_pose + + return np.stack(poses_abs) # [T,4,4] + + +# ----------------------------------------------------------------------------- +# Idle-frame detection +# ----------------------------------------------------------------------------- + + +def _identity_rotation_vector(rotation_format: RotationConvention) -> np.ndarray: + """Return the identity-rotation vector for a given rotation convention. + + Used by :func:`compute_idle_frames` to test whether a rotation block is + close to "no rotation" in its current encoding. + """ + if rotation_format in ("matrix", "rot9d"): + return np.array([1, 0, 0, 0, 1, 0, 0, 0, 1], dtype=np.float32) + if rotation_format == "rot6d": + return np.array([1, 0, 0, 0, 1, 0], dtype=np.float32) + if rotation_format == "quat_xyzw": + return np.array([0, 0, 0, 1], dtype=np.float32) + if rotation_format == "quat_wxyz": + return np.array([1, 0, 0, 0], dtype=np.float32) + if rotation_format in ("euler_xyz", "axisangle"): + return np.array([0, 0, 0], dtype=np.float32) + raise ValueError(f"Unsupported rotation_format={rotation_format!r}") + + +def _rotation_angle_per_arm(rotations: np.ndarray, rotation_format: str) -> np.ndarray: + """Geodesic angle (rad) from identity for each arm at each frame. + + ``rotations`` has shape ``(T, n_arms, n_per_arm)``; the returned array has + shape ``(T, n_arms)``. The angle is rotation-format aware so a fixed + ``eps_r`` threshold has consistent geometric meaning across formats: + + - ``rot6d`` → reconstruct ``trace(R)`` in closed form from the two stored + columns ``a, b`` (already unit-orthogonal as they came from a valid + rotation matrix). The third column is ``a × b``, so + ``trace(R) = a[0] + b[1] + a[0]·b[1] - a[1]·b[0]``. + ``angle = arccos(clip((trace - 1) / 2, -1, 1))``. + - ``rot9d`` → reshape to ``(..., 3, 3)`` and use + ``trace(R) = R[0,0] + R[1,1] + R[2,2]``. + - ``quat_xyzw`` / ``quat_wxyz`` → ``angle = 2 · arccos(|q_w|)``; the + absolute value handles the double cover (``q`` and ``-q`` represent the + same rotation). + - ``axisangle`` → the magnitude of the axis-angle vector *is* the angle. + - ``euler_xyz`` → no closed-form angle; use ``‖euler‖`` as a conservative + upper bound (exact for single-axis rotations, an overestimate for + composed ones — fine for idle detection where small angles are the + regime of interest). + """ + if rotation_format == "rot6d": + a = rotations[..., :3] + b = rotations[..., 3:6] + trace = a[..., 0] + b[..., 1] + a[..., 0] * b[..., 1] - a[..., 1] * b[..., 0] + return np.arccos(np.clip((trace - 1.0) / 2.0, -1.0, 1.0)) + if rotation_format == "rot9d": + mat = rotations.reshape(*rotations.shape[:-1], 3, 3) + trace = mat[..., 0, 0] + mat[..., 1, 1] + mat[..., 2, 2] + return np.arccos(np.clip((trace - 1.0) / 2.0, -1.0, 1.0)) + if rotation_format in ("quat_xyzw", "quat_wxyz"): + qw = rotations[..., 3] if rotation_format == "quat_xyzw" else rotations[..., 0] + return 2.0 * np.arccos(np.clip(np.abs(qw), 0.0, 1.0)) + if rotation_format == "axisangle": + return np.linalg.norm(rotations, axis=-1) + if rotation_format == "euler_xyz": + # Exact for single-axis rotations, overestimate for composed ones — + # safe for idle thresholds since overestimation can only mark a frame + # as non-idle, never spuriously idle. + return np.linalg.norm(rotations, axis=-1) + raise ValueError(f"Unsupported rotation_format={rotation_format!r}") + + +def _consecutive_streaks(idle: np.ndarray, min_streak: int) -> np.ndarray: + """Zero out idle bits not belonging to a run of ``>= min_streak`` Trues. + + Pure-numpy two-pointer scan. ``min_streak <= 1`` is a no-op (returns the + input mask unchanged). + """ + if min_streak <= 1: + return idle + out = np.zeros_like(idle) + n = len(idle) + i = 0 + while i < n: + if not idle[i]: + i += 1 + continue + j = i + while j < n and idle[j]: + j += 1 + if j - i >= min_streak: + out[i:j] = True + i = j + return out + + +def compute_idle_frames( + action_raw: torch.Tensor | np.ndarray, + spec: "ActionSpec", # noqa: F821 — forward ref, real import is in action_spec.py + *, + eps_t: float = 1e-3, + eps_r: float = math.radians(5.0), + eps_g: float = 1e-2, + joint_threshold: float = 5e-4, + min_streak: int = 3, +) -> int: + """Count idle frames in a raw (un-normalized) action chunk. + + Idle detection runs per-DimType (driven by ``spec.types``); a frame is + *raw-idle* iff every relevant type group is idle on that frame, and + counts toward the final tally only if it belongs to a run of at least + ``min_streak`` consecutive raw-idle frames. The streak filter rejects + isolated low-motion frames (instantaneous slowdowns) which carry weak + physical meaning and add noise to the IdleFrames training signal. + + DimType branches: + + - ``POS`` → combined ``‖action[pos_idx]‖`` (L2 across all POS dims) + < ``eps_t``. For single-arm specs (3 dims) this is the standard ``‖t‖`` + check; for multi-arm specs the combined norm is slightly stricter than + a per-arm check. + - ``ROT`` → per-arm geodesic rotation angle (rad) from identity + < ``eps_r``. The angle is computed in a rotation-format aware way (see + :func:`_rotation_angle_per_arm`) so the threshold has consistent + geometric meaning regardless of the encoding. + - ``GRIPPER`` → ``max |action[t] - action[t-1]| < eps_g``. ``np.diff`` + with ``prepend=action[0]`` makes step 0 ``|0|`` (treated as "no change"); + with the streak filter this can no longer create a spurious single-frame + idle event. + - ``JOINT`` → same frame-diff scheme as gripper with + ``joint_threshold`` (rad / step). + - ``RESERVED`` → ignored. + + Defaults (in the units of the un-normalized action): + + - ``eps_t = 1e-3`` → 1 mm per-frame translation + - ``eps_r = 5°`` → 5° per-frame rotation (geodesic angle) + - ``eps_g = 1e-2`` → 1 % gripper command change + - ``joint_threshold = 5e-4`` → ~0.03° / step joint angle change + - ``min_streak = 3`` → require a run of >= 3 consecutive idle frames + + The input must be **un-normalized** so the identity transform sits at + known coordinates (translation ≈ 0, rotation ≈ identity). The action + vector is also assumed to be encoded in a per-step / framewise convention + (e.g. ``backward_framewise``); anchored conventions (``backward_anchored``) + accumulate over the chunk and would silently break the POS/ROT idle + checks. Callers (e.g. the LeRobot base class) gate on pose convention + before calling this function. + """ + if isinstance(action_raw, torch.Tensor): + action = action_raw.detach().cpu().numpy().astype(np.float32, copy=False) + else: + action = np.asarray(action_raw, dtype=np.float32) + + if action.ndim != 2: + raise ValueError(f"action_raw must be 2-D (T, D); got shape {action.shape}") + num_frames, action_dim = action.shape + if num_frames == 0: + return 0 + if action_dim != len(spec.types): + raise ValueError(f"action_dim={action_dim} does not match spec.dim={len(spec.types)}") + + # Import locally to avoid a circular import at module load time + # (action_spec.py imports RotationConvention from this file). + from cosmos3._src.vfm.datasets.action.action_spec import DimType + + pos_idx = [i for i, t in enumerate(spec.types) if t == DimType.POS] + rot_idx = [i for i, t in enumerate(spec.types) if t == DimType.ROT] + grip_idx = [i for i, t in enumerate(spec.types) if t == DimType.GRIPPER] + joint_idx = [i for i, t in enumerate(spec.types) if t == DimType.JOINT] + + idle = np.ones(num_frames, dtype=bool) + + # POS: combined L2 norm across all translation dims. + if pos_idx: + idle &= np.linalg.norm(action[:, pos_idx], axis=1) < eps_t + + # ROT: per-arm geodesic angle (rad). + if rot_idx: + rot_id = _identity_rotation_vector(spec.rotation_format) + n_per_arm = rot_id.shape[0] + if len(rot_idx) % n_per_arm != 0: + raise ValueError( + f"ROT dims ({len(rot_idx)}) not a multiple of " + f"rotation_format={spec.rotation_format!r} dim ({n_per_arm})" + ) + rotations = action[:, rot_idx].reshape(num_frames, -1, n_per_arm) + angles = _rotation_angle_per_arm(rotations, spec.rotation_format) # (T, n_arms) + idle &= angles.max(axis=1) < eps_r + + # GRIPPER: max |Δgripper| across all gripper dims; step 0's diff is 0. + if grip_idx: + gripper = action[:, grip_idx] + diff = np.abs(np.diff(gripper, axis=0, prepend=gripper[:1])) + idle &= diff.max(axis=1) < eps_g + + # JOINT: same frame-diff scheme with joint_threshold. + if joint_idx: + joints = action[:, joint_idx] + diff = np.abs(np.diff(joints, axis=0, prepend=joints[:1])) + idle &= diff.max(axis=1) < joint_threshold + + if min_streak > 1: + idle = _consecutive_streaks(idle, min_streak) + + return int(idle.sum()) diff --git a/cosmos3/_src/vfm/datasets/action/transforms.py b/cosmos3/_src/vfm/datasets/action/transforms.py new file mode 100644 index 00000000..c812865d --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/transforms.py @@ -0,0 +1,681 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dataset transform wrappers for the Action project. + +This module provides the ``ActionTransformPipeline`` and spatial padding utilities. + +The reflection padding snaps each sample to the closest predefined resolution from +``VIDEO_RES_SIZE_INFO`` (matching VFM's approach), guaranteeing a bounded set of +output shapes that are all multiples of 16. + +See :func:`~.unified_dataset.wrap_dataset` for the convenience factory that +combines datasets with transforms, and :class:`~.unified_dataset.MapToIterableAdapter` +for the map-to-iterable wrapper. +""" + +from __future__ import annotations + +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.action.json_formatter import ActionPromptJsonFormatter +from cosmos3._src.vfm.datasets.action.viewpoint_utils import ViewpointTextInfo +from cosmos3._src.vfm.datasets.augmentors.duration_fps_text_timestamps import DurationFPSTextTimeStamps +from cosmos3._src.vfm.datasets.augmentors.idle_frames_text_info import IdleFramesTextInfo +from cosmos3._src.vfm.datasets.augmentors.resolution_text_info import ResolutionTextInfo +from cosmos3._src.vfm.datasets.augmentors.text_tokenizer import TextTokenizerTransform +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution + + +def _should_append_idle_frame_info(mode: object) -> bool: + """Return whether idle-frame prompt metadata should be surfaced.""" + return mode != "inverse_dynamics" + + +def pad_action_to_max_dim(action: torch.Tensor, max_action_dim: int) -> torch.Tensor: + """Pad action tensor to max_action_dim along the last dimension. + + Args: + action: Action tensor of shape (T, D) where D is the current action dimension. + max_action_dim: Target action dimension to pad to. + + Returns: + Padded action tensor of shape (T, max_action_dim). + """ + if action.shape[-1] > max_action_dim: + raise ValueError(f"Action dimension {action.shape[-1]} is greater than max_action_dim {max_action_dim}") + elif action.shape[-1] == max_action_dim: + return action + else: + padding_size = max_action_dim - action.shape[-1] + zero_padding = torch.zeros( + *action.shape[:-1], padding_size, dtype=action.dtype, device=action.device + ) # [T,padding_size] + return torch.cat([action, zero_padding], dim=-1) # [T,max_action_dim] + + +def find_closest_target_size(h: int, w: int, resolution: str | int) -> tuple[int, int]: + """Find the closest predefined target size for a given input resolution. + + Looks up ``VIDEO_RES_SIZE_INFO[resolution]`` and selects the aspect ratio + whose ``H/W`` ratio is closest to the input ``h/w``. + + Args: + h: Input height in pixels. + w: Input width in pixels. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``). + + Returns: + ``(target_w, target_h)`` from the predefined table. + + Raises: + ValueError: If *resolution* is not a key in ``VIDEO_RES_SIZE_INFO``. + """ + if isinstance(resolution, int): + resolution = str(resolution) + if resolution not in VIDEO_RES_SIZE_INFO: + raise ValueError( + f"Resolution '{resolution}' not found in VIDEO_RES_SIZE_INFO. Available: {list(VIDEO_RES_SIZE_INFO.keys())}" + ) + + candidates = VIDEO_RES_SIZE_INFO[resolution] + input_ratio = h / w + + best_key: str | None = None + best_diff = float("inf") + for aspect_key, (cand_w, cand_h) in candidates.items(): + cand_ratio = cand_h / cand_w + diff = abs(input_ratio - cand_ratio) + if diff < best_diff: + best_diff = diff + best_key = aspect_key + + assert best_key is not None + target_w, target_h = candidates[best_key] + return target_w, target_h + + +def reflection_pad_to_target( + data_dict: dict, + keys: list[str], + keep_aspect_ratio: bool, + target_w: int, + target_h: int, +) -> dict: + """Resize (aspect-preserving) and reflection-pad tensors to exact target size. + + For each key in *keys*, the tensor is: + + 1. Resized so its spatial dimensions fit within ``(target_h, target_w)`` + while preserving the aspect ratio (matching VFM's + ``ResizeLargestSideAspectPreserving``). + 2. Reflection-padded (or edge-padded when the padding exceeds the spatial + dimension) to reach exactly ``(target_h, target_w)`` (matching VFM's + ``ReflectionPadding``). + + After processing, the following entries are added to *data_dict*: + + - ``"image_size"``: ``torch.Tensor`` of shape ``(4,)`` containing + ``[target_h, target_w, orig_h_resized, orig_w_resized]`` where + ``target_h/w`` is the padded canvas size and ``orig_h/w_resized`` + is the original spatial size after aspect-preserving resize (i.e. + the content region before padding). After ``default_collate`` + this becomes ``(B, 4)``; the ``IterativeJointDataLoader`` then + splits it into per-sample ``(1, 4)`` tensors so the model can + index as ``data_batch["image_size"][i][0][0]``. + + Args: + data_dict: The sample dictionary (mutated in-place). + keys: Data-dict keys whose tensors should be resized and padded. + Tensors must have shape ``(C, H, W)`` or ``(C, T, H, W)``. + keep_aspect_ratio: Whether to keep the aspect ratio of the input tensor. + target_w: Target width in pixels. + target_h: Target height in pixels. + + Returns: + The mutated *data_dict*. + """ + orig_h_resized: int = 0 + orig_w_resized: int = 0 + + for key in keys: + if key not in data_dict: + continue + tensor = data_dict[key] + if not isinstance(tensor, torch.Tensor): + continue + + # Extract spatial dims + if tensor.ndim == 3: + orig_h, orig_w = tensor.shape[-2:] + elif tensor.ndim == 4: + orig_h, orig_w = tensor.shape[-2:] + else: + raise ValueError(f"Unexpected tensor ndim={tensor.ndim} for key '{key}', expected 3 or 4") + + # Step 1: aspect-preserving resize to fit within (target_h, target_w) + if keep_aspect_ratio: + # Prevent upscaling the video by setting the upper bound of scaling_ratio to 1.0. + scaling_ratio = min(target_w / orig_w, target_h / orig_h, 1.0) + orig_h_resized = int(scaling_ratio * orig_h + 0.5) + orig_w_resized = int(scaling_ratio * orig_w + 0.5) + assert orig_h_resized <= target_h and orig_w_resized <= target_w, ( + f"Resize error: orig ({orig_h}, {orig_w}) target ({target_h}, {target_w}) " + f"computed ({orig_h_resized}, {orig_w_resized})" + ) + else: + orig_h_resized = target_h + orig_w_resized = target_w + + if orig_h_resized != orig_h or orig_w_resized != orig_w: + tensor = transforms_F.resize( + tensor, + size=[orig_h_resized, orig_w_resized], + interpolation=transforms_F.InterpolationMode.BICUBIC, + antialias=True, + ) + + # Step 2: padding to exact target size (bottom and right only) + if orig_w_resized != target_w or orig_h_resized != target_h: + padding_right = target_w - orig_w_resized + padding_bottom = target_h - orig_h_resized + padding = [0, 0, padding_right, padding_bottom] + + if padding_right >= orig_w_resized or padding_bottom >= orig_h_resized: + tensor = transforms_F.pad(tensor, padding, padding_mode="edge") + else: + tensor = transforms_F.pad(tensor, padding, padding_mode="reflect") + + data_dict[key] = tensor + + # image_size: shape (4,) — [target_h, target_w, orig_h_resized, orig_w_resized]. + # Matches VFM's item_dataset convention. default_collate stacks to (B, 4); + # IterativeJointDataLoader._get_next_sample slices to (1, 4) per sample so + # the model can index [i][0][0]. + data_dict["image_size"] = torch.tensor( + [target_h, target_w, orig_h_resized, orig_w_resized], dtype=torch.float + ) # [4] + + return data_dict + + +def remove_reflection_padding( + tensor: torch.Tensor, + image_size: torch.Tensor, +) -> torch.Tensor: + """Remove reflection padding added by :func:`reflection_pad_to_target`. + + Content is at top-left; crops to ``(orig_h_resized, orig_w_resized)``. + + Args: + tensor: Tensor whose last two dimensions are the padded spatial dims. + Supports any leading dimensions, e.g. ``(C, T, H, W)`` or + ``(C, H, W)``. + image_size: 1-D tensor of shape ``(4,)`` containing + ``[target_h, target_w, orig_h_resized, orig_w_resized]`` where + ``orig_h/w_resized`` is the original spatial size after + aspect-preserving resize (i.e. the content region before + padding) — the same convention stored by + :func:`reflection_pad_to_target` and VFM's + ``ReflectionPadding``. + + Returns: + Cropped tensor of shape ``(..., orig_h_resized, orig_w_resized)``. + """ + target_h = int(image_size[0].item()) + target_w = int(image_size[1].item()) + orig_h_resized = int(image_size[2].item()) + orig_w_resized = int(image_size[3].item()) + + if orig_h_resized == target_h and orig_w_resized == target_w: + return tensor + + return tensor[..., :orig_h_resized, :orig_w_resized].contiguous() + + +def build_sequence_plan_from_mode( + mode: str, + video_length: int, + action_length: int, + has_text: bool = True, + video_temporal_downsample: int = 4, + num_history_actions: int = 0, +) -> SequencePlan: + """Build a SequencePlan based on the training mode. + + This function determines whether action should be included and computes the + appropriate condition frame indexes for vision and action based on the mode. + + Args: + mode: Training mode. One of: + - "image2video": Image-to-video generation (no action) + - "forward_dynamics": Predict video given first frame and all actions + - "inverse_dynamics": Predict actions given all video frames + - "policy": Predict both actions and video given first frame + video_length: Number of video frames (including the conditioning frame). + action_length: Number of action steps (typically video_length - 1). + has_text: Whether text conditioning is available. Defaults to True. + video_temporal_downsample: Temporal downsampling factor of the video + tokenizer. Used to compute condition frame indexes for inverse + dynamics mode. Defaults to 4. + + Returns: + SequencePlan instance with appropriate settings. + Use ``sequence_plan.has_action`` to check if action should be included. + + Raises: + ValueError: If mode is not one of the supported modes. + + Example: + >>> sequence_plan = build_sequence_plan_from_mode( + ... mode="policy", + ... video_length=5, + ... action_length=4, + ... ) + >>> sequence_plan.has_action + True + >>> sequence_plan.as_dict() + {'has_text': True, 'has_vision': True, 'has_action': True, + 'condition_frame_indexes_vision': [0], 'condition_frame_indexes_action': []} + """ + valid_modes = ["image2video", "forward_dynamics", "inverse_dynamics", "policy"] + if mode not in valid_modes: + raise ValueError(f"Invalid mode: {mode!r}. Must be one of {valid_modes}") + + # Determine if action should be included based on mode + # image2video mode: no action (pure image-to-video generation) + # forward_dynamics, inverse_dynamics, policy: action is needed + has_action = mode != "image2video" + + # Determine condition frame indexes based on mode + # image2video/forward_dynamics/policy: first frame is clean (conditioning) + # inverse_dynamics: all frames are provided as context + if mode in ["image2video", "forward_dynamics", "policy"]: + condition_frame_indexes_vision = [0] + elif mode == "inverse_dynamics": + # All frames are observed for inverse dynamics + condition_frame_indexes_vision = list(range(0, (video_length - 1) // video_temporal_downsample + 1)) + else: + condition_frame_indexes_vision = [] + + # For action conditioning indexes: + # forward_dynamics: all action steps are clean (conditioning) + # inverse_dynamics/policy: action is supervised (predicted) + # History frames (prepended) are always conditioning. + base_action_length = action_length - num_history_actions + if mode == "forward_dynamics": + condition_frame_indexes_action = list(range(action_length)) + + # This currently assumes that the action length is the same as the video length - 1 + # and if action length is the same as the video length, then the first action is the conditioning action + elif base_action_length == video_length - 1: + condition_frame_indexes_action = list(range(num_history_actions)) + elif base_action_length == video_length: + condition_frame_indexes_action = list(range(num_history_actions + 1)) + + if base_action_length == video_length - 1: + action_start_frame_offset = 1 - num_history_actions + if base_action_length == video_length: + action_start_frame_offset = -num_history_actions + + return SequencePlan( + has_text=has_text, + has_vision=True, + has_action=has_action, + condition_frame_indexes_vision=condition_frame_indexes_vision, + condition_frame_indexes_action=condition_frame_indexes_action, + action_start_frame_offset=action_start_frame_offset, + ) + + +class VideoResize: + """Resize and reflection-pad video-aligned tensors for a single sample. + + Resolution is supplied at call time. When ``resolution`` is ``None``, the + tier is auto-detected from the sample's ``"video"`` spatial dimensions. + + Args: + pad_keys: Data-dict keys whose values should be resized and padded. + Pass an empty list to disable padding entirely. Defaults to + ``["video"]``. + keep_aspect_ratio: Whether to resize aspect-preservingly to the closest + predefined target size before padding. Defaults to ``True``. + log_prefix: Prefix used in debug logging. + """ + + def __init__( + self, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + log_prefix: str = "VideoResize", + ) -> None: + self.pad_keys = pad_keys if pad_keys is not None else ["video"] + self.keep_aspect_ratio = keep_aspect_ratio + self.log_prefix = log_prefix + + def __call__(self, data_dict: dict, resolution: str | int | None) -> dict: + """Resize and pad a sample in-place. + + Args: + data_dict: Sample dictionary containing a ``"video"`` entry. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, + ``"720"``). When ``None``, auto-detected from video dimensions. + + Returns: + The same dictionary, mutated in-place with padded tensors and an + ``"image_size"`` entry. + """ + video = data_dict.get("video") + assert isinstance(video, torch.Tensor), "video is required for reflection padding" + h, w = video.shape[-2:] + + if resolution is None: + resolution = get_vision_data_resolution((h, w)) + + self._log_shapes(data_dict, when="before") + + if self.keep_aspect_ratio: + target_w, target_h = find_closest_target_size(h, w, resolution) + else: + target_w = int(resolution) + target_h = int(resolution) + reflection_pad_to_target(data_dict, self.pad_keys, self.keep_aspect_ratio, target_w, target_h) + + self._log_shapes(data_dict, when="after ") + return data_dict + + def _log_shapes(self, data_dict: dict, when: str) -> None: + """Log tensor shapes for the configured pad keys.""" + for key in self.pad_keys: + val = data_dict.get(key) + if isinstance(val, torch.Tensor): + log.debug(f"{self.log_prefix}: {when} padding '{key}' shape = {tuple(val.shape)}") + + +class ActionTransformPipeline: + """A composable transform pipeline that chains ``VideoResize``, text + tokenization, and automatic sequence plan construction. + + Reflection padding snaps each sample to the closest predefined aspect + ratio from ``VIDEO_RES_SIZE_INFO[resolution]``, resizes + (aspect-preserving) to fit within the target, then reflection-pads to + the exact target size. This guarantees a bounded set of output shapes + (5 per resolution tier), all multiples of 16. Resolution is supplied + at call time via the required ``resolution`` argument to ``__call__``; + when ``resolution`` is ``None``, the tier is auto-detected from the + video's spatial dimensions via ``get_vision_data_resolution``. + + Text tokenization is enabled when ``tokenizer_config`` is provided. + + When the data dictionary contains a ``"mode"`` key, the pipeline automatically + builds a ``SequencePlan`` via :func:`build_sequence_plan_from_mode` and attaches + it as ``data_dict["sequence_plan"]``. For modes where action is not needed + (e.g. ``"image2video"``), the ``"action"`` and ``"domain_id"`` keys are set to + ``None``. + + Args: + pad_keys: Data-dict keys whose values should be resized and padded. Pass + an empty list to disable padding entirely. Defaults to ``["video"]``. + tokenizer_config: A lazy-instantiable config dict for the VLM tokenizer. When + ``None``, text tokenization is skipped. Defaults to ``None``. + cfg_dropout_rate: Probability of replacing the caption with an empty string for + classifier-free guidance. Only used when text tokenization is enabled. + Defaults to ``0.0``. + caption_key: The data-dict key that contains the input caption string. + Defaults to ``"ai_caption"``. + text_token_key: The data-dict key where tokenized text IDs will be stored. + Defaults to ``"text_token_ids"``. + video_temporal_downsample: Temporal downsampling factor of the video tokenizer. + Used when building a ``SequencePlan`` for ``"inverse_dynamics"`` mode. + Defaults to 4. + max_action_dim: Target action dimension to pad to. The ``"action"`` tensor + in every sample is padded along its last dimension via + :func:`pad_action_to_max_dim`. Defaults to 32. + action_channel_masking: When ``True`` (default), the original action + dimension is stored in ``"raw_action_dim"`` so that the model masks + loss/noise/velocity on zero-padded action channels. When ``False``, + ``"raw_action_dim"`` is set to ``None`` and the model treats all + ``max_action_dim`` channels equally (original main-branch behavior). + append_viewpoint_info: Whether to append viewpoint type metadata to the + caption (via ``ViewpointTextInfo`` augmentor). Requires that + samples contain a ``"viewpoint"`` key. Defaults to ``True``. + append_duration_fps_timestamps: Whether to append duration and FPS metadata to the + caption (matching VFM's ``DurationFPSTextTimeStamps`` augmentor). + Defaults to ``True``. + append_resolution_info: Whether to append resolution metadata to the + caption (matching VFM's ``ResolutionTextInfo`` augmentor). + Defaults to ``True``. + append_idle_frames: Whether to append the idle-frame count out of the + total action frames to the caption (Pi0.7-style metadata, via + ``IdleFramesTextInfo`` augmentor). The dataset is responsible for + populating ``data_dict["idle_frames"]``; samples without it are + silently skipped. Idle-frame text is skipped only for + ``"inverse_dynamics"`` mode. Defaults to ``False`` so existing + experiments are unaffected. + idle_frames_dropout: Per-field dropout rate for the idle-frame segment. + With this probability the augmentor leaves the caption unchanged + (matching Pi0.7's ~5% per-component dropout). Independent of the + global ``cfg_dropout_rate``, which empties the whole caption. + Defaults to 0.05. + format_prompt_as_json: Whether to replace the plain text prompt with a + structured JSON-compatible dictionary before tokenization. When + enabled, legacy string metadata appenders are skipped and the JSON + formatter owns viewpoint, action, resolution, duration, FPS, and + idle-frame fields. Defaults to ``False``. + """ + + def __init__( + self, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + tokenizer_config: dict | None = None, + cfg_dropout_rate: float = 0.0, + caption_key: str = "ai_caption", + text_token_key: str = "text_token_ids", + video_temporal_downsample: int = 4, + max_action_dim: int = 32, + action_channel_masking: bool = True, + append_viewpoint_info: bool = True, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + append_idle_frames: bool = False, + idle_frames_dropout: float = 0.05, + format_prompt_as_json: bool = False, + ) -> None: + self.caption_key: str = caption_key + self.video_temporal_downsample: int = video_temporal_downsample + self.max_action_dim: int = max_action_dim + self.action_channel_masking: bool = action_channel_masking + + # --- Spatial resize/padding stage (resolution supplied at call time) --- + self.video_resize: VideoResize = VideoResize( + pad_keys=pad_keys, + keep_aspect_ratio=keep_aspect_ratio, + log_prefix="ActionTransformPipeline", + ) + self.pad_keys: list[str] = self.video_resize.pad_keys + self.keep_aspect_ratio: bool = self.video_resize.keep_aspect_ratio + + self.prompt_json_formatter: ActionPromptJsonFormatter | None = None + if format_prompt_as_json: + self.prompt_json_formatter = ActionPromptJsonFormatter(caption_key=caption_key) + + # --- Viewpoint text augmentor (runs after ai_caption, before duration/FPS) --- + self.viewpoint_augmentor: ViewpointTextInfo | None = None + if append_viewpoint_info and self.prompt_json_formatter is None: + self.viewpoint_augmentor = ViewpointTextInfo( + input_keys=[caption_key, "viewpoint"], + output_keys=[caption_key], + args={"caption_key": caption_key, "viewpoint_key": "viewpoint", "enabled": True}, + ) + + # --- Duration/FPS text augmentor (runs before tokenization) --- + self.duration_fps_augmentor: DurationFPSTextTimeStamps | None = None + if append_duration_fps_timestamps and self.prompt_json_formatter is None: + self.duration_fps_augmentor = DurationFPSTextTimeStamps( + input_keys=[caption_key, "video", "conditioning_fps"], + output_keys=[caption_key], + args={"caption_key": caption_key, "video_key": "video", "fps_key": "conditioning_fps"}, + ) + + # --- Resolution text augmentor (runs before tokenization) --- + self.resolution_info_augmentor: ResolutionTextInfo | None = None + if append_resolution_info and self.prompt_json_formatter is None: + self.resolution_info_augmentor = ResolutionTextInfo( + input_keys=[caption_key, "video", "image_size"], + output_keys=[caption_key], + args={"caption_key": caption_key, "video_key": "video", "enabled": True}, + ) + + # --- IdleFrames text augmentor (Pi0.7-style episode metadata) --- + # Runs after resolution info, before tokenization. Per-field dropout is + # independent from the tokenizer's global cfg_dropout_rate. + self.idle_frames_augmentor: IdleFramesTextInfo | None = None + if append_idle_frames and self.prompt_json_formatter is None: + self.idle_frames_augmentor = IdleFramesTextInfo( + input_keys=[caption_key, "idle_frames", "action"], + output_keys=[caption_key], + args={ + "caption_key": caption_key, + "idle_frames_key": "idle_frames", + "action_key": "action", + "dropout_rate": idle_frames_dropout, + "enabled": True, + }, + ) + + # --- Text tokenizer augmentor --- + self.text_tokenizer: TextTokenizerTransform | None = None + if tokenizer_config is not None: + self.text_tokenizer = TextTokenizerTransform( + input_keys=[caption_key], + output_keys=[text_token_key], + args={ + "tokenizer_config": tokenizer_config, + "cfg_dropout_rate": cfg_dropout_rate, + }, + ) + + def __call__(self, data_dict: dict, resolution: str | None) -> dict: + """Apply the transform pipeline to a single data dictionary. + + Resolution is required at call time and is the only source of truth + for this sample. When ``resolution`` is ``None``, the tier is + auto-detected from the video's spatial dimensions. + + The pipeline runs in order: + + 1. Resize + reflection-pad spatial dimensions to the closest + predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``. + 2. Format the caption as a structured JSON prompt (if enabled). + 3. Otherwise, append viewpoint type metadata to caption (if enabled). + 4. Append duration/FPS metadata to caption (if enabled). + 5. Append resolution metadata to caption (if enabled). + 6. Append idle-frame metadata (Pi0.7-style) to caption unless the + sample is in inverse dynamics mode (if enabled). + 7. Tokenize caption text (if enabled). + 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present). + 9. If action is needed by the plan, pad ``"action"`` to ``max_action_dim``. + 10. Otherwise, nullify ``"action"`` and ``"domain_id"`` (e.g. in + ``"image2video"`` mode). + + Args: + data_dict: A sample dictionary as returned by a Action dataset. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``) + for this sample. When ``None``, auto-detected from video dimensions. + + Returns: + The same dictionary, mutated in-place with padded tensors, + ``image_size``, tokenized text IDs, and a + ``"sequence_plan"`` entry added. + """ + mode = data_dict.get("mode") + assert mode is not None, "mode is required" + + # 1. Resize + reflection-pad spatial dimensions to the closest predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``. + data_dict = self.video_resize(data_dict, resolution) + + # 2. Format the caption as structured JSON when requested; otherwise run the legacy string appenders. + if self.prompt_json_formatter is not None: + data_dict = self.prompt_json_formatter(data_dict) + else: + # 3. Append viewpoint type metadata to caption (if enabled). + if self.viewpoint_augmentor is not None: + result = self.viewpoint_augmentor(data_dict) + if result is not None: + data_dict = result + + # 4. Append duration/FPS metadata to caption (if enabled). + if self.duration_fps_augmentor is not None: + result = self.duration_fps_augmentor(data_dict) + if result is not None: + data_dict = result + + # 5. Append resolution metadata to caption (if enabled). + if self.resolution_info_augmentor is not None: + result = self.resolution_info_augmentor(data_dict) + if result is not None: + data_dict = result + + # 6. Append idle-frame metadata to caption (if enabled for this mode). + if self.idle_frames_augmentor is not None and _should_append_idle_frame_info(mode): + result = self.idle_frames_augmentor(data_dict) + if result is not None: + data_dict = result + + # 7. Tokenize caption text (if enabled). + if self.text_tokenizer is not None: + data_dict = self.text_tokenizer(data_dict) + + # 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present). + video = data_dict.get("video") + action = data_dict.get("action") + assert video is not None, "video is required" + video_length = video.shape[1] # [C,T,H,W] -> T + action_length = action.shape[0] if isinstance(action, torch.Tensor) else max(video_length - 1, 0) + + # Prepend history action frames (ground-truth conditioning) if present. + history_action = data_dict.pop("history_action", None) + num_history_actions = 0 + if history_action is not None and isinstance(action, torch.Tensor): + num_history_actions = history_action.shape[0] + action = torch.cat([history_action, action], dim=0) + action_length += num_history_actions + + sequence_plan = build_sequence_plan_from_mode( + mode=mode, + video_length=video_length, + action_length=action_length, + video_temporal_downsample=self.video_temporal_downsample, + num_history_actions=num_history_actions, + ) + data_dict["sequence_plan"] = sequence_plan + + if sequence_plan.has_action: + assert isinstance(action, torch.Tensor), "action tensor is required when sequence plan has action" + data_dict["raw_action_dim"] = torch.tensor(action.shape[1]) if self.action_channel_masking else None + data_dict["action"] = pad_action_to_max_dim(action, self.max_action_dim) + else: + # Nullify action-related fields when action is not needed so the + # collate function can simply stack all non-None actions. + data_dict["raw_action_dim"] = None + data_dict["action"] = None + data_dict["domain_id"] = None + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/action/unified_dataset.py b/cosmos3/_src/vfm/datasets/action/unified_dataset.py new file mode 100644 index 00000000..ca960e72 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/unified_dataset.py @@ -0,0 +1,594 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unified iterable dataset for Action multi-embodiment robot data. + +``ActionUnifiedIterableDataset`` is the Layer 2 component of the Action data loading +pipeline. It wraps *all* Action datasets into a single ``IterableDataset`` and +handles: + +- **Rank-level dataset assignment** (Hare-Niemeyer proportional allocation) +- **Worker-level shard distribution** (round-robin within a dataset family) +- **Per-sample transforms** via :class:`~.transforms.ActionTransformPipeline` +- **Weighted random fallback** when worker assignment is not active + +See ``docs/dataloader.md`` for the full design document. +""" + +from __future__ import annotations + +import gc +import random +import warnings +from collections.abc import Iterator, Mapping, Sequence +from typing import Any + +from torch.utils.data import Dataset, IterableDataset + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.action.transforms import ActionTransformPipeline +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + +_iterable_dataset_len_warning_suppressed = False + + +def _suppress_iterable_dataset_len_warning() -> None: + """Register a one-time filter for PyTorch's IterableDataset len() warning. + + The inner datasets may not implement ``__len__``, so the wrapper reports + ``len()=0``. PyTorch's iterator then warns on every ``__next__`` when + samples are fetched. This filter suppresses that warning. + """ + global _iterable_dataset_len_warning_suppressed + if _iterable_dataset_len_warning_suppressed: + return + _iterable_dataset_len_warning_suppressed = True + warnings.filterwarnings( + "ignore", + message="Length of IterableDataset.*was reported to be 0", + category=UserWarning, + module="torch.utils.data.dataloader", + ) + + +# --------------------------------------------------------------------------- +# Worker-side periodic garbage collection +# --------------------------------------------------------------------------- +# DataLoader workers running IterableDatasets are long-lived forked processes. +# Complex sample dictionaries (nested dicts, tensors, Arrow references) can +# create circular-reference chains that Python's reference counting alone +# cannot free. The generational GC *does* collect them eventually, but its +# default thresholds are too conservative for high-throughput data loading, +# causing RSS to grow monotonically until the node OOMs. +# +# Calling ``gc.collect()`` periodically inside the worker iteration loop +# eliminates the leak with negligible overhead (<1 ms per call vs ~6 s +# iteration time). +# +_GC_INTERVAL: int = 10 + + +def _maybe_gc(interval: int, count: int) -> int: + """Increment *count* and run ``gc.collect()`` every *interval* samples.""" + if interval <= 0: + return count + count += 1 + if count % interval == 0: + gc.collect() + return count + + +class ActionUnifiedIterableDataset(IterableDataset): + """Single IterableDataset wrapping all Action datasets. + + Handles worker-to-dataset assignment, shard distribution, and transforms. + + Args: + datasets: List of dicts, each with keys ``"name"`` (str identifier), + ``"dataset"`` (the dataset instance), and ``"ratio"`` (float + sampling weight). + transform: Transform pipeline applied to every yielded sample. + shard_across_workers: When ``True``, ranks are assigned to + dataset families via Hare-Niemeyer and workers get round-robin + shards. When ``False`` (default), every worker loads all + datasets and iterates with weighted random selection. + """ + + def __init__( + self, + datasets: list[dict[str, Any]], + transform: ActionTransformPipeline, + shard_across_workers: bool = False, + ) -> None: + super().__init__() + self._datasets = datasets + self._transform = transform + self._shard_across_workers = shard_across_workers + + # Set per-worker by assign_worker; None means not yet assigned. + self._dataset: Any | None = None + self._resolution: str | None = None # resolution for single-dataset path + self._sources_initialized = False + + # Backward compat: expose ``self.dataset`` pointing to the first + # inner dataset and ``self.transform`` exposing the pipeline + # (mirrors old TransformedIterableDataset interface). + self.dataset = datasets[0]["dataset"] if datasets else None + self.transform = transform + + # -- source initialization ------------------------------------------------ + + def _ensure_sources_registered(self) -> None: + if self._sources_initialized: + return + self._sources_initialized = True + for entry in self._datasets: + ds = entry["dataset"] + shard_roots = getattr(ds, "_all_shard_roots", []) + if shard_roots and hasattr(ds, "_register_sources"): + ds._register_sources() + + # -- backward-compat helpers ----------------------------------------------- + + def __len__(self) -> int: # type: ignore[override] + total = 0 + for entry in self._datasets: + ds = entry["dataset"] + try: + total += len(ds) # type: ignore[arg-type] + except TypeError: + pass + return total + + def __getattr__(self, name: str) -> Any: + """Forward attribute lookups to the first inner dataset.""" + if name.startswith("_") or not self._datasets: + raise AttributeError(name) + return getattr(self._datasets[0]["dataset"], name) + + # -- Hare-Niemeyer rank allocation ----------------------------------------- + + @staticmethod + def _compute_rank_ranges( + datasets: list[dict[str, Any]], + world_size: int, + ) -> list[tuple[int, int]]: + """Hare-Niemeyer allocation of ranks to datasets. + + Guarantees at least 1 rank per dataset, distributes the rest + proportionally. Returns a list of ``(start_rank, end_rank)`` ranges. + + Raises: + ValueError: If ``world_size < len(datasets)``. + """ + n_ds = len(datasets) + if world_size < n_ds: + raise ValueError(f"world_size ({world_size}) must be >= number of datasets ({n_ds})") + ratios = [d["ratio"] for d in datasets] + total = sum(ratios) + + # Hare-Niemeyer (largest-remainder) method: + # 1. Give every dataset a guaranteed minimum of 1 rank. + # 2. Distribute the leftover ranks proportionally to each dataset's + # ratio. Take the floor of each fractional allocation, then award + # the still-unassigned ranks one-by-one to datasets with the + # largest fractional remainders. + # Example: world_size=8, ratios=[3, 1] (2 datasets) + # remaining = 8 - 2 = 6 + # fractional = [6*3/4, 6*1/4] = [4.5, 1.5] + # floors = [4, 1], remainders = [0.5, 0.5], leftover = 1 + # award 1 extra to first dataset -> floors = [5, 1] + # counts = [1+5, 1+1] = [6, 2] + counts = [1] * n_ds + remaining = world_size - n_ds + if remaining > 0: + fractional = [remaining * r / total for r in ratios] + floors = [int(f) for f in fractional] + remainders = [f - fl for f, fl in zip(fractional, floors)] + leftover = remaining - sum(floors) + for idx in sorted(range(n_ds), key=lambda j: -remainders[j])[:leftover]: + floors[idx] += 1 + counts = [1 + f for f in floors] + + # Convert per-dataset counts into contiguous rank intervals. + # Example continued: counts=[6, 2] -> ranges=[(0,6), (6,8)] + # ranks 0..5 serve dataset 0, ranks 6..7 serve dataset 1. + ranges: list[tuple[int, int]] = [] + cursor = 0 + for c in counts: + ranges.append((cursor, cursor + c)) + cursor += c + return ranges + + # -- worker assignment ----------------------------------------------------- + + def assign_worker( + self, + worker_id: int, + num_workers: int, + rank: int, + world_size: int, + ) -> None: + """Assign this worker to a dataset family and distribute shards. + + Called by the DataLoader's ``worker_init_fn`` (via + :func:`~.dataloaders.create_action_worker_init_fn`) -- not by the + dataset itself. + + Two-level assignment: + + 1. **Rank -> dataset family** (Hare-Niemeyer over *world_size* + ranks). Every rank is fully dedicated to one family. + 2. **Workers -> shards** (round-robin within the family's worker + pool). ``family_worker_id = rank_within_family * num_workers + + worker_id``. + + When ``shard_across_workers=False``: no assignment is performed. + Every worker loads all datasets and ``__iter__`` uses weighted + random selection. + """ + self._sources_initialized = True + if not self._shard_across_workers: + for entry in self._datasets: + ds = entry["dataset"] + shard_roots = getattr(ds, "_all_shard_roots", []) + if shard_roots and hasattr(ds, "_register_sources"): + ds._register_sources() + return + + rank_ranges = self._compute_rank_ranges(self._datasets, world_size) + + # Step 1: which dataset family does this rank belong to? + # ``rank_ranges`` is a list of (start_rank, end_rank) intervals -- one + # per dataset family -- produced by ``_compute_rank_ranges()`` above + # using Hare-Niemeyer allocation. The intervals are contiguous and + # non-overlapping, covering [0, world_size), so every rank belongs to + # exactly one family. + # + # We scan through the intervals to find the one containing this rank, + # then derive two values: + # - rank_within_family: this rank's 0-based position inside its + # family (used in Step 2 to build a globally unique worker id). + # - num_family_ranks: total number of ranks assigned to this family + # (used in Step 2 to compute the family's worker pool size). + # + # ``self._dataset`` is set to the matched family's dataset object so + # that ``__iter__`` only yields samples from this one dataset. + # + # Example with world_size=8, ratios=[3,1] -> ranges=[(0,6), (6,8)]: + # rank 3 -> family 0, rank_within_family=3, num_family_ranks=6 + # rank 6 -> family 1, rank_within_family=0, num_family_ranks=2 + num_family_ranks = 1 + rank_within_family = 0 + for i, (start_rank, end_rank) in enumerate(rank_ranges): + if start_rank <= rank < end_rank: + entry = self._datasets[i] + self._dataset = entry["dataset"] + self._resolution = entry["resolution"] + rank_within_family = rank - start_rank + num_family_ranks = end_rank - start_rank + break + + # Step 2: distribute shards across workers within the family. + # Each rank spawns ``num_workers`` DataLoader workers (set by the + # DataLoader's ``num_workers`` arg). So the family's total worker + # pool is ``num_family_ranks * num_workers``. + # + # We flatten the 2D index (rank_within_family, worker_id) into a + # single linear ``family_worker_id`` so every worker in the family + # gets a globally unique id within that family: + # family_worker_id = rank_within_family * num_workers + worker_id + # + # Example: family has 3 ranks, each rank spawns 2 workers -> 6 total: + # rank_within_family=0: worker_id 0 -> fwid 0, worker_id 1 -> fwid 1 + # rank_within_family=1: worker_id 0 -> fwid 2, worker_id 1 -> fwid 3 + # rank_within_family=2: worker_id 0 -> fwid 4, worker_id 1 -> fwid 5 + # + # This linear id is then used for round-robin shard assignment below. + family_total_workers = num_family_ranks * num_workers + family_worker_id = rank_within_family * num_workers + worker_id + + # Round-robin assignment: worker k gets shards k, k+stride, k+2*stride, ... + # This ensures shards are evenly spread across the family's workers. + # + # When family_total_workers > num_shards, some workers get an empty + # list from range() (any worker with family_worker_id >= num_shards, + # since start >= stop). The ``if not my_shards`` guard catches this + # and falls back to ``family_worker_id % num_shards``, wrapping the + # worker around to an existing shard so it shares rather than idles. + # + # Example: AgiBotWorld with 190 shards and 256 family workers: + # Workers 0-189 -> each gets 1 unique shard via range() + # Workers 190-255 -> empty range, fallback to family_worker_id % 190, + # sharing a shard with an earlier worker. + # + # Multiple workers reading the same shard is fine because each worker + # has a different RNG seed (``seed + rank * 9999 + worker_id``), so + # they produce different sample orderings from the same underlying data. + shard_roots = getattr(self._dataset, "_all_shard_roots", []) + if shard_roots and hasattr(self._dataset, "_register_sources"): + num_shards = len(shard_roots) + my_shards = list(range(family_worker_id, num_shards, family_total_workers)) + if not my_shards: + my_shards = [family_worker_id % num_shards] + self._dataset._register_sources(my_shards) + + # -- iteration ------------------------------------------------------------- + + def _iter_all_datasets_weighted(self) -> Iterator[dict[str, Any]]: + """Iterate all datasets with weighted random selection. + + Used when ``shard_across_workers=False`` (every worker sees all + datasets) or as the ``num_workers=0`` fallback. + """ + iterators = [iter(d["dataset"]) for d in self._datasets] + ratios = [d["ratio"] for d in self._datasets] + total = sum(ratios) + weights = [r / total for r in ratios] + + gc_count = 0 + + while True: + chosen = random.choices(range(len(self._datasets)), weights=weights, k=1)[0] + resolution = self._datasets[chosen]["resolution"] + try: + yield self._transform(next(iterators[chosen]), resolution=resolution) + except StopIteration: + iterators[chosen] = iter(self._datasets[chosen]["dataset"]) + try: + yield self._transform(next(iterators[chosen]), resolution=resolution) + except StopIteration: + continue + gc_count = _maybe_gc(_GC_INTERVAL, gc_count) + + def __iter__(self) -> Iterator[dict[str, Any]]: + if self._dataset is not None: + gc_count = 0 + for sample in self._dataset: + yield self._transform(sample, resolution=self._resolution) + gc_count = _maybe_gc(_GC_INTERVAL, gc_count) + return + + if not self._shard_across_workers: + self._ensure_sources_registered() + yield from self._iter_all_datasets_weighted() + return + + # num_workers=0 fallback (shard_across_workers=True but no worker + # processes exist, so assign_worker was never called). + log.warning( + "ActionUnifiedIterableDataset: num_workers=0 fallback — " + "loading ALL datasets in main process. Use only for debugging." + ) + self._ensure_sources_registered() + yield from self._iter_all_datasets_weighted() + + +class MapToIterableAdapter(IterableDataset): + """Wraps a map-style ``Dataset`` as an ``IterableDataset``. + + Each iteration yields a sample from a uniformly random index, using + ``random.randint`` for O(1) time and zero extra memory. The per-worker + RNG seed (set by :func:`~.dataloaders.create_action_worker_init_fn`) ensures + different DataLoader workers produce different random sequences. + + Args: + dataset: A map-style ``Dataset`` with ``__len__`` and ``__getitem__``. + """ + + def __init__(self, dataset: Dataset) -> None: + super().__init__() + self.dataset = dataset + + def __len__(self) -> int: # type: ignore[override] + return len(self.dataset) # type: ignore[arg-type] + + def __iter__(self) -> Iterator: + n = len(self.dataset) # type: ignore[arg-type] + while True: + yield self.dataset[random.randint(0, n - 1)] + + def __getattr__(self, name: str) -> Any: + """Forward attribute lookups to the inner dataset for transparency.""" + if name == "dataset": + raise AttributeError(name) + return getattr(self.dataset, name) + + +def dataset_entry( + name: str, + dataset: Dataset | IterableDataset, + ratio: float = 1.0, + resolution: str | None = None, +) -> dict: + """Factory for a single dataset descriptor used inside ``wrap_dataset``. + + Wrapping each entry with ``LazyCall(dataset_entry)(...)`` gives it a + ``_target_`` so that ``instantiate`` recurses into the nested dataset + config automatically. + + Args: + name: Identifier for the dataset. + dataset: The dataset instance. + ratio: Sampling weight. Defaults to 1.0. + resolution: Optional resolution tier (e.g. ``"256"``, ``"480"``) for + this dataset. When ``None``, falls back to ``wrap_dataset``'s + global ``resolution`` (which may be ``None`` for auto-detect). + """ + return {"name": name, "dataset": dataset, "ratio": ratio, "resolution": resolution} + + +def wrap_dataset( + list_of_datasets: Sequence[dict] | list[dict] | Dataset | IterableDataset, + resolution: str | None = None, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + tokenizer_config: dict | None = None, + cfg_dropout_rate: float = 0.0, + caption_key: str = "ai_caption", + text_token_key: str = "text_token_ids", + video_temporal_downsample: int = 4, + max_action_dim: int = 32, + shard_across_workers: bool = False, + action_channel_masking: bool = True, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + append_idle_frames: bool = False, + idle_frames_dropout: float = 0.05, + format_prompt_as_json: bool = False, +) -> ActionUnifiedIterableDataset: + """Factory that wraps one or more datasets with the Action transform pipeline. + + ``list_of_datasets`` accepts either: + + * A **list of dicts**, where each dict has the keys: + - ``name`` (``str``): identifier for the dataset. + - ``dataset`` (``Dataset | IterableDataset``): the dataset instance. + - ``ratio`` (``float``, optional): sampling weight. Defaults to ``1``. + - ``resolution`` (``str | None``, optional): resolution tier for this + dataset. When missing, falls back to ``wrap_dataset``'s global + ``resolution`` (which may be ``None`` for auto-detect). + * A **single** ``Dataset`` or ``IterableDataset`` for backward compatibility + (auto-wrapped as ``[{"name": "default", "dataset": , "ratio": 1}]``). + + Map-style datasets are automatically wrapped with + :class:`MapToIterableAdapter` so the returned dataset is always an + ``IterableDataset``. This means callers can mix map-style and + iterable-style datasets freely. + + Args: + list_of_datasets: The dataset(s) to wrap. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``). + Spatial dimensions are resized and reflection-padded to the closest + predefined target from ``VIDEO_RES_SIZE_INFO``. When ``None``, the + tier is auto-detected per sample via ``get_vision_data_resolution``. + Defaults to ``None``. + pad_keys: Data-dict keys whose values should be resized and padded. Pass + an empty list or ``None`` to disable padding. Defaults to ``["video"]``. + tokenizer_config: A lazy-instantiable config dict for the VLM tokenizer. When + ``None``, text tokenization is skipped. Defaults to ``None``. + cfg_dropout_rate: Probability of replacing the caption with an empty string for + classifier-free guidance. Defaults to ``0.0``. + caption_key: The data-dict key that contains the input caption string. + Defaults to ``"ai_caption"``. + text_token_key: The data-dict key where tokenized text IDs will be stored. + Defaults to ``"text_token_ids"``. + video_temporal_downsample: Temporal downsampling factor of the video tokenizer. + Used when building a ``SequencePlan`` for ``"inverse_dynamics"`` mode. + Defaults to 4. + max_action_dim: Target action dimension to pad to. The ``"action"`` tensor + in every sample is padded along its last dimension. Defaults to 32. + action_channel_masking: When ``True`` (default), stores the original action + dimension in ``"raw_action_dim"`` so the model masks loss/noise/velocity + on padded channels. Set to ``False`` to disable (original behavior). + shard_across_workers: When ``True``, the returned dataset + supports rank-level dataset assignment and worker-level shard + distribution via ``assign_worker()``. When ``False`` (default), + every worker iterates all datasets with weighted random selection. + append_duration_fps_timestamps: Whether to append duration and FPS metadata to the + caption before tokenization. Defaults to ``True``. + append_resolution_info: Whether to append resolution metadata to the + caption before tokenization. Defaults to ``True``. + append_idle_frames: Whether to append the idle-frame count out of the + total action frames (Pi0.7-style metadata) to the caption before + tokenization. The dataset is responsible for populating + ``data_dict["idle_frames"]``; samples without it are silently + skipped. Defaults to ``False`` so existing experiments are + unaffected. + idle_frames_dropout: Per-field dropout rate for the idle-frame segment. + Independent of ``cfg_dropout_rate`` (which empties the whole + caption). Defaults to 0.05. + format_prompt_as_json: Whether to replace the plain text prompt with a + structured JSON-compatible dictionary before tokenization. Defaults + to ``False``. + + Returns: + A :class:`ActionUnifiedIterableDataset` wrapping the dataset(s) with the + configured transforms applied. + + Raises: + TypeError: If the dataset(s) are not ``Dataset`` or ``IterableDataset``. + ValueError: If ``list_of_datasets`` is an empty list. + """ + if pad_keys is None: + pad_keys = ["video"] + + # ------------------------------------------------------------------ + # Backward compatibility: single dataset -> list-of-dicts + # ------------------------------------------------------------------ + if isinstance(list_of_datasets, (Dataset, IterableDataset)): + list_of_datasets = [{"name": "default", "dataset": list_of_datasets, "ratio": 1}] + + if ( + not isinstance(list_of_datasets, Sequence) + or isinstance(list_of_datasets, (str, bytes)) + or len(list_of_datasets) == 0 + ): + raise ValueError( + "list_of_datasets must be a non-empty list/sequence of dicts or a single Dataset/IterableDataset, " + f"got {type(list_of_datasets).__name__}" + ) + + # ------------------------------------------------------------------ + # Parse list-of-dicts, wrapping map-style datasets with + # MapToIterableAdapter so every dataset is iterable. Compute effective + # resolution per entry (per-entry overrides global). + # ------------------------------------------------------------------ + datasets: list[dict] = [] + for entry in list_of_datasets: + if not isinstance(entry, Mapping): + raise TypeError(f"Each entry in list_of_datasets must be a dict/mapping, got {type(entry).__name__}") + name: str = entry["name"] + dataset: Dataset | IterableDataset = entry["dataset"] + ratio: float = float(entry.get("ratio", 1)) + resolution: str | None = entry.get("resolution", None) + if resolution is not None: + res_key = str(resolution) if isinstance(resolution, int) else resolution + if res_key not in VIDEO_RES_SIZE_INFO: + raise ValueError( + f"Resolution '{resolution}' for dataset '{name}' not found in VIDEO_RES_SIZE_INFO. " + f"Available: {list(VIDEO_RES_SIZE_INFO.keys())}" + ) + if not isinstance(dataset, IterableDataset): + dataset = MapToIterableAdapter(dataset) + datasets.append({"name": name, "dataset": dataset, "ratio": ratio, "resolution": resolution}) + + # ------------------------------------------------------------------ + # Build the transform pipeline (resolution supplied at call time) + # ------------------------------------------------------------------ + transform = ActionTransformPipeline( + pad_keys=pad_keys, + keep_aspect_ratio=keep_aspect_ratio, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + caption_key=caption_key, + text_token_key=text_token_key, + video_temporal_downsample=video_temporal_downsample, + max_action_dim=max_action_dim, + action_channel_masking=action_channel_masking, + append_duration_fps_timestamps=append_duration_fps_timestamps, + append_resolution_info=append_resolution_info, + append_idle_frames=append_idle_frames, + idle_frames_dropout=idle_frames_dropout, + format_prompt_as_json=format_prompt_as_json, + ) + + _suppress_iterable_dataset_len_warning() + + return ActionUnifiedIterableDataset( + datasets=datasets, + transform=transform, + shard_across_workers=shard_across_workers, + ) diff --git a/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py b/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py new file mode 100644 index 00000000..ebb99ab2 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Viewpoint type definitions and caption augmentor for Action datasets. + +Provides a ``Viewpoint`` type alias for camera perspective labels and a +``ViewpointTextInfo`` augmentor that appends a human-readable viewpoint +description to the caption string. +""" + +from __future__ import annotations + +from typing import Literal + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + +Viewpoint = Literal["ego_view", "third_person_view", "wrist_view", "concat_view"] + +DEFAULT_VIEWPOINT_TEMPLATES: dict[str, str] = { + "ego_view": "This video is captured from a first-person perspective looking at the scene.", + "third_person_view": "This video is captured from a third-person perspective looking towards the agent from the front.", + "wrist_view": "This video is captured from a wrist-mounted camera.", + "concat_view": "This video contains concatenated views from multiple camera perspectives.", +} + + +class ViewpointTextInfo(Augmentor): + """Augmentor that appends viewpoint type description to captions. + + Reads a viewpoint label from ``data_dict[viewpoint_key]`` and appends + the corresponding template sentence to the caption. Designed to run + after the raw ``ai_caption`` is set but before duration/FPS metadata + is appended. + + Args: + input_keys: Input keys (kept for API compatibility). + output_keys: Output keys (kept for API compatibility). + args: Configuration arguments: + - caption_key (str): Key for caption in data_dict. Default: ``"ai_caption"`` + - viewpoint_key (str): Key for viewpoint label. Default: ``"viewpoint"`` + - templates (dict): Override mapping from viewpoint to sentence. + Default: :data:`DEFAULT_VIEWPOINT_TEMPLATES` + - separator (str): Separator between caption and metadata. Default: ``". "`` + - enabled (bool): Whether augmentation is enabled. Default: ``True`` + """ + + def __init__( + self, + input_keys: list | None = None, + output_keys: list | None = None, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], output_keys or [], args) + + self.caption_key: str = args.get("caption_key", "ai_caption") if args else "ai_caption" + self.viewpoint_key: str = args.get("viewpoint_key", "viewpoint") if args else "viewpoint" + self.templates: dict[str, str] = ( + args.get("templates", DEFAULT_VIEWPOINT_TEMPLATES) if args else DEFAULT_VIEWPOINT_TEMPLATES + ) + self.default_separator: str = args.get("separator", ". ") if args else ". " + self.enabled: bool = args.get("enabled", True) if args else True + + def __call__(self, data_dict: dict) -> dict | None: + """Append viewpoint description to the caption. + + If the sample provides an ``"additional_view_description"`` key (a + free-form string describing the concatenated camera layout), it is + appended after the generic ``concat_view`` template. This allows each + dataset to supply its own description of which cameras are tiled and + how. + + Args: + data_dict: Sample dictionary containing caption and viewpoint. + + Returns: + The mutated *data_dict*, or the original unchanged if the + viewpoint key is missing or unrecognized. + """ + if not self.enabled: + return data_dict + + viewpoint = data_dict.get(self.viewpoint_key) + if viewpoint is None: + raise ValueError( + f"ViewpointTextInfo: missing key {self.viewpoint_key!r} in data_dict. " + f"All action datasets must provide a viewpoint label." + ) + + # Append dataset-specific concat_view details after the base template. + additional_view_description = data_dict.pop("additional_view_description", None) + template = self.templates.get(viewpoint) + + if template is None: + log.warning( + f"ViewpointTextInfo: unrecognized viewpoint {viewpoint!r}. " + f"Known viewpoints: {sorted(self.templates.keys())}. Skipping.", + rank0_only=False, + ) + return data_dict + + if additional_view_description: + separator = " " if template.endswith(".") else self.default_separator + template = template + separator + additional_view_description.rstrip() + + caption = data_dict.get(self.caption_key) + if not isinstance(caption, str) or caption == "": + return data_dict + + caption = caption.rstrip() + separator = " " if caption.endswith(".") else self.default_separator + data_dict[self.caption_key] = caption + separator + template + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/__init__.py b/cosmos3/_src/vfm/datasets/augmentors/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py b/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py new file mode 100644 index 00000000..00f66796 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +class AppendFPSFramesForImage(Augmentor): + def __init__( + self, input_keys: Optional[list] = None, output_keys: Optional[list] = None, args: Optional[dict] = None + ) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Remove the input keys from the data dict. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with keys removed. + """ + data_dict["fps"] = 30.0 # set image model fps = 30, which is the most common fps we used to train video. + data_dict["num_frames"] = 1 + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py b/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py new file mode 100644 index 00000000..077dfbbf --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Augmentor that appends audio captions to video captions. + +Reads an audio caption from the metadata JSON and appends it to the existing +video caption string before tokenization. This allows the model to condition +on both visual and audio descriptions. + +Placed AFTER text_transform (which sets ai_caption) and BEFORE text_tokenization. +""" + +import sys + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +def _debug(msg: str) -> None: + """Write debug message to stderr (unbuffered, reliable in worker processes).""" + sys.stderr.write(f"[AudioCaptionAppender] {msg}\n") + sys.stderr.flush() + + +class AudioCaptionAppender(Augmentor): + """Appends audio caption text from metadata to the video caption. + + Args: + input_keys: Expected to be ["metas", "ai_caption"] but read from data_dict directly. + output_keys: Not used. + args: Dictionary with: + - audio_caption_key: Metadata key for audio caption (default: "audio_caption") + - separator: Text inserted between video and audio captions (default: " Audio description: ") + - sound_key: Key to check if sound data exists (default: "sound") + """ + + def __init__(self, input_keys: list, output_keys: list | None = None, args: dict | None = None) -> None: + super().__init__(input_keys, output_keys, args) + args = args or {} + self.audio_caption_key = args.get("audio_caption_key", "caption_audio") + self.separator = args.get("separator", " Audio description: ") + self.sound_key = args.get("sound_key", "sound") + self.caption_key = "ai_caption" + log.warning( + f"AudioCaptionAppender initialized: audio_caption_key='{self.audio_caption_key}', " + f"sound_key='{self.sound_key}', metas_key='{input_keys[0]}'", + rank0_only=True, + ) + + def _find_audio_caption(self, meta_dict: dict) -> str | None: + """Find audio caption in metas, supporting both flat and nested formats. + + Flat format (e.g., metas_w_audio_caps): + {"caption_audio": "...", ...} + + Nested format (e.g., midtrain dataset): + {"0_156": {"caption_sound": "..."}, ...} + The key is a frame range like "0_156" containing a dict with "caption_sound". + """ + # Try flat key first + value = meta_dict.get(self.audio_caption_key) + if isinstance(value, str) and len(value) > 0: + return value + + # Try nested: look for a dict value containing "caption_sound" + for key, val in meta_dict.items(): + if isinstance(val, dict) and "caption_sound" in val: + caption = val["caption_sound"] + if isinstance(caption, str) and len(caption) > 0: + return caption + + return None + + def __call__(self, data_dict: dict) -> dict | None: + """Append audio caption to the video caption if available. + + Only appends when sound data is present in the sample. If the metadata + does not contain the audio_caption_key, the video caption is left unchanged. + Always cleans up metas from data_dict since this is the last augmentor that reads it. + """ + metas_key = self.input_keys[0] + has_sound = self.sound_key in data_dict and data_dict.get(self.sound_key) is not None + meta_dict = data_dict.get(metas_key) + + if has_sound and meta_dict is not None: + audio_caption = self._find_audio_caption(meta_dict) + if isinstance(audio_caption, str) and len(audio_caption) > 0: + current_caption = data_dict.get(self.caption_key, "") + data_dict[self.caption_key] = current_caption + self.separator + audio_caption + + # Clean up metas from data_dict — this augmentor is the last consumer of metas + if metas_key in data_dict: + del data_dict[metas_key] + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py b/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py new file mode 100644 index 00000000..344304e8 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Audio parsing augmentor for T2A (Text-to-Audio) datasets. + +For audio-only datasets (AudioCaps, WavCaps, etc.) that have no video, +this augmentor: +1. Decodes audio from bytes +2. Creates a full-length dummy video (all zeros) matching the audio duration +3. Outputs data compatible with the v3 video training pipeline + +The dummy video ensures compatibility with the model architecture which +requires vision tokens in the sequence (sound_gen requires vision_gen). +The dummy video is fully conditioned (all frames clean), so it contributes +no loss — effectively making this a tv2s (text+video→sound) mode where +the video is a placeholder. +""" + +from typing import Optional + +import torch +from torchcodec.decoders import AudioDecoder + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +class AudioParsingForFullClips(Augmentor): + """Audio parsing augmentor for audio-only datasets. + + Loads audio from bytes, creates a dummy video of matching duration, + and outputs data compatible with the VideoParsingWithFullFrames pipeline. + + Args: + input_keys: [meta_key, audio_key] — keys to fetch metadata and audio bytes + output_keys: Optional output keys + args: Dictionary with: + - target_sample_rate: Target audio sample rate (default: 48000) + - target_channels: Target audio channels (default: 2 for stereo) + - dummy_video_fps: FPS for dummy video (default: 24) + - dummy_video_size: (H, W) for dummy video (default: (256, 256)) + - max_audio_duration_sec: Max audio duration in seconds (default: 30.0) + - min_audio_duration_sec: Min audio duration in seconds (default: 1.0) + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) == 2, "AudioParsingForFullClips requires two input keys: [meta_key, audio_key]" + self.meta_key = input_keys[0] + self.audio_key = input_keys[1] + + self.target_sample_rate = args.get("target_sample_rate", 48000) + self.target_channels = args.get("target_channels", 2) + self.dummy_video_fps = args.get("dummy_video_fps", 24.0) + self.dummy_video_size = args.get("dummy_video_size", (256, 256)) + self.max_audio_duration_sec = args.get("max_audio_duration_sec", 30.0) + self.min_audio_duration_sec = args.get("min_audio_duration_sec", 1.0) + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + audio_bytes = data_dict[self.audio_key] + except Exception: + log.warning( + f"Cannot find audio data. url: {data_dict.get('__url__', '?')}, key: {data_dict.get('__key__', '?')}", + rank0_only=False, + ) + return None + + if not isinstance(audio_bytes, bytes): + log.warning("Audio data is not bytes, skipping", rank0_only=False) + return None + + # Decode audio + try: + audio_decoder = AudioDecoder(audio_bytes) + audio_metadata = audio_decoder.metadata + orig_sample_rate = audio_metadata.sample_rate + + audio_samples = audio_decoder.get_samples_played_in_range() + audio_chunk = audio_samples.data # [C,N_orig] + del audio_decoder + except Exception as e: + log.warning(f"Failed to decode audio: {e}", rank0_only=False) + return None + + # Compute duration + audio_duration_sec = audio_chunk.shape[1] / orig_sample_rate + + # Filter by duration + if audio_duration_sec < self.min_audio_duration_sec: + log.debug(f"Audio too short: {audio_duration_sec:.2f}s < {self.min_audio_duration_sec}s", rank0_only=False) + return None + if audio_duration_sec > self.max_audio_duration_sec: + # Crop to max duration + max_samples = int(self.max_audio_duration_sec * orig_sample_rate) + audio_chunk = audio_chunk[:, :max_samples] + audio_duration_sec = self.max_audio_duration_sec + + # Resample if needed + if orig_sample_rate != self.target_sample_rate: + import torchaudio + + audio_chunk = torchaudio.functional.resample( + audio_chunk, orig_freq=orig_sample_rate, new_freq=self.target_sample_rate + ) # [C,N_resampled] + + # Handle channel count (mono → stereo or vice versa) + if audio_chunk.shape[0] == 1 and self.target_channels == 2: + audio_chunk = audio_chunk.repeat(2, 1) # [2,N_resampled] + elif audio_chunk.shape[0] > self.target_channels: + audio_chunk = audio_chunk[: self.target_channels] # [C_target,N_resampled] + + # Create dummy video matching audio duration + # VAE compress temporal by 4x, with 1 as condition → num_frames must be 1 + 4N + num_video_frames = int(audio_duration_sec * self.dummy_video_fps) + N = (num_video_frames - 1) // 4 + num_video_frames = max(1 + 4 * N, 1) + + h, w = self.dummy_video_size + dummy_video = torch.zeros(3, num_video_frames, h, w, dtype=torch.uint8) # [3,T,H,W] + + # Build output compatible with VideoParsingWithFullFrames + video_info = { + "frame_start": 0, + "frame_end": num_video_frames - 1, + "num_frames": num_video_frames, + "video": dummy_video, + "fps": self.dummy_video_fps, + "conditioning_fps": self.dummy_video_fps, + "n_orig_video_frames": num_video_frames, + "sound": audio_chunk, + "audio_sample_rate": self.target_sample_rate, + } + data_dict["video"] = video_info + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py b/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py new file mode 100644 index 00000000..46cf4c0b --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +class CaptionFilter(Augmentor): + """ + Caption filter augmentor for predict2 training. + + This augmentor filters video samples based on caption content with configurable behavior: + - contain_keyword=True: Only return videos that contain keywords in captions + - contain_keyword=False: Only return videos that do NOT contain keywords in captions + + When a sample doesn't match the filter criteria, it returns None, which causes + the webdataset pipeline to skip that sample and continue to the next one. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + """ + Initialize the caption filter. + + Args: + input_keys: List containing the caption key (e.g., ["ai_caption"] or text embeddings key) + output_keys: Not used for filtering, can be None + args: Dictionary with filtering parameters: + - "keywords": List of keywords to filter by (e.g., ["camera pan"]) + - "contain_keyword": Boolean flag for filtering behavior: + * True: Only return videos that contain keywords + * False: Only return videos that do NOT contain keywords + - "log_filtered": Whether to log filtered samples (default: False) + - "filter_stats": Whether to track filtering statistics (default: True) + - "dont_apply_on_webdataset_names": List of webdataset names to not apply the filter on, it will just pass through without checking contain or not contain keywords + """ + super().__init__(input_keys, output_keys, args) + + # Parse arguments + if args is None: + args = {} + + self.keywords = args.get("keywords", []) + self.contain_keyword = args.get("contain_keyword", False) # Default to exclude mode + self.log_filtered = args.get("log_filtered", False) + self.filter_stats = args.get("filter_stats", True) + self.dont_apply_on_webdataset_names = args.get("dont_apply_on_webdataset_names", []) + + # Validate input_keys + if not input_keys or len(input_keys) == 0: + raise ValueError("CaptionFilter requires at least one input key for the caption field") + + self.caption_key = input_keys[0] # Use the first input key as the caption key + + # Statistics tracking + if self.filter_stats: + self.total_samples = 0 + self.filtered_samples = 0 + + # Validate configuration + if not self.keywords: + log.warning("CaptionFilter: No keywords provided, filter will not filter any samples") + + mode_str = "contain" if self.contain_keyword else "exclude" + log.info( + f"CaptionFilter initialized in '{mode_str}' mode with {len(self.keywords)} keywords using caption key '{self.caption_key}': {self.keywords}" + ) + + def __call__(self, data_dict: dict) -> Optional[dict]: + """ + Filter data based on caption content. + + This checks the caption field specified by the input_keys parameter. + Depending on contain_keyword flag: + - True: Returns data_dict only if caption contains any keyword, None otherwise + - False: Returns data_dict only if caption contains NO keywords, None otherwise + + Args: + data_dict: Input data dictionary containing the caption field specified in input_keys + + Returns: + data_dict: Original data dict if caption passes filter + None: If caption should be filtered out (causes sample to be skipped) + """ + data_dict_root = data_dict["__url__"].root + if any(n in data_dict_root for n in self.dont_apply_on_webdataset_names): + return data_dict + + if self.filter_stats: + self.total_samples += 1 + + # Check if caption key exists + if self.caption_key not in data_dict: + if self.log_filtered: + log.warning(f"CaptionFilter: No '{self.caption_key}' found in data_dict, passing through") + return data_dict + + caption = data_dict[self.caption_key] + if not isinstance(caption, str) or not caption.strip(): + if self.log_filtered: + log.warning(f"CaptionFilter: '{self.caption_key}' is empty or not a string, got {type(caption)}") + return data_dict + + # Check if any keywords are found in the caption + search_caption = caption.lower() + keyword_found = False + matched_keyword = None + + for keyword in self.keywords: + if keyword.lower() in search_caption: + keyword_found = True + matched_keyword = keyword + break + + # Apply filtering logic based on contain_keyword flag + should_filter = False + if self.contain_keyword: + # Include mode: filter out if NO keywords found + should_filter = not keyword_found + else: + # Exclude mode: filter out if ANY keyword found + should_filter = keyword_found + + if should_filter: + if self.log_filtered: + if self.contain_keyword: + log.info(f"CaptionFilter: excluded sample (no keywords found) - caption: '{caption[:100]}...'") + else: + log.info( + f"CaptionFilter: excluded sample due to keyword '{matched_keyword}' - caption: '{caption[:100]}...'" + ) + + if self.filter_stats: + self.filtered_samples += 1 + return None + + # Sample passes filter + return data_dict + + def get_filter_stats(self) -> dict: + """ + Get filtering statistics. + + Returns: + Dictionary with filtering statistics + """ + if not self.filter_stats: + return {"stats_disabled": True} + + filter_rate = (self.filtered_samples / self.total_samples * 100) if self.total_samples > 0 else 0 + mode_str = "contain" if self.contain_keyword else "exclude" + + return { + "total_samples": self.total_samples, + "filtered_samples": self.filtered_samples, + "passed_samples": self.total_samples - self.filtered_samples, + "filter_rate_percent": filter_rate, + "mode": mode_str, + "keywords": self.keywords, + } diff --git a/cosmos3/_src/vfm/datasets/augmentors/cropping.py b/cosmos3/_src/vfm/datasets/augmentors/cropping.py new file mode 100644 index 00000000..8e1d2a20 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/cropping.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torchvision.transforms.functional as transforms_F +from PIL import Image + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + + +class CropToMultiple(Augmentor): + """Crops images/videos to the nearest multiple of a specified value using center crop. + + This augmentor crops the height and width of images/videos to be divisible by + a given multiple (default 16). The crop is centered, removing equal amounts + from opposite edges. + + Supports: + - PIL Images (for image data) + - Torch tensors with shape (C, H, W) or (C, T, H, W) (for video data) + + Example: + Input: 209x187 with multiple=16 + Output: 208x176 (center cropped to nearest lower multiple of 16) + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.multiple = 16 + if self.args is not None and "multiple" in self.args: + self.multiple = self.args["multiple"] + + def __call__(self, data_dict: dict) -> dict: + """Center crops images/videos to the nearest multiple of the specified value. + + Args: + data_dict (dict): Input data dict containing images/videos to crop. + + Returns: + data_dict (dict): Output dict with center cropped images/videos. + """ + for key in self.input_keys: + if key not in data_dict: + continue + + data = data_dict[key] + + # Get dimensions based on data type + if isinstance(data, Image.Image): + # PIL Image: size returns (width, height) + w, h = data.size + elif isinstance(data, torch.Tensor): + # Torch tensor: (C, H, W) or (C, T, H, W) + if data.ndim == 3: + _, h, w = data.shape + elif data.ndim == 4: + _, _, h, w = data.shape + else: + raise ValueError(f"Unexpected tensor dimensions: {data.ndim}, expected 3 or 4") + else: + raise ValueError(f"Unexpected data type: {type(data)}, expected PIL Image or torch Tensor") + + # Calculate new dimensions (nearest lower multiple) + new_h = (h // self.multiple) * self.multiple + new_w = (w // self.multiple) * self.multiple + + # Center crop: calculate offsets to center the crop + if new_h != h or new_w != w: + top = (h - new_h) // 2 + left = (w - new_w) // 2 + # log.info(f"Data cropped from ({h}, {w}) to ({new_h}, {new_w})") + data_dict[key] = transforms_F.crop(data, top=top, left=left, height=new_h, width=new_w) + + # Store final dimensions for downstream use (e.g., resolution text info) + data_dict["final_height"] = new_h + data_dict["final_width"] = new_w + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py b/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py new file mode 100644 index 00000000..998c014f --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + +# Global template for duration and FPS text timestamps +DEFAULT_TEMPLATE = "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS." + + +class DurationFPSTextTimeStamps(Augmentor): + """ + Augmentor that appends video duration and FPS as text timestamps to captions. + + This augmentor should run AFTER TextTransformForVideo to append metadata + to the already-selected caption in data_dict["ai_caption"]. + + IMPORTANT: Reads num_frames from the actual video tensor shape to get the + FINAL frame count after all video processing (subsampling, etc.) is complete. + + Example: + Original caption: "A cat playing with a ball" + Augmented caption: "A cat playing with a ball. The video is 1.4 seconds long and is of 24 FPS" + + Args: + input_keys (list): Input keys (not used, kept for API compatibility) + output_keys (list): Output keys (not used, kept for API compatibility) + args (dict): Configuration arguments: + - caption_key (str): Key for caption in data_dict. Default: "ai_caption" + - video_key (str): Key for video tensor in data_dict. Default: "video" + - fps_key (str): Key for FPS value in data_dict. Default: "conditioning_fps" + - template (str): Format string for metadata text. Default: DEFAULT_TEMPLATE constant + - separator (str): Separator between caption and metadata. Default: ". " + - enabled (bool): Whether augmentation is enabled. Default: True + - skip_on_error (bool): If True, skip on errors and return original data_dict. If False, return None. Default: True + - num_multiplier_key (str): Key for num_multiplier value in data_dict. Default: "num_multiplier" + """ + + def __init__( + self, input_keys: Optional[list] = None, output_keys: Optional[list] = None, args: Optional[dict] = None + ) -> None: + super().__init__(input_keys, output_keys, args) + + # Configuration with sensible defaults + self.caption_key = args.get("caption_key", "ai_caption") if args else "ai_caption" + self.video_key = args.get("video_key", "video") if args else "video" + self.fps_key = args.get("fps_key", "conditioning_fps") if args else "conditioning_fps" + self.template = args.get("template", DEFAULT_TEMPLATE) if args else DEFAULT_TEMPLATE + self.default_separator = args.get("separator", ". ") if args else ". " + self.enabled = args.get("enabled", True) if args else True + self.skip_on_error = args.get("skip_on_error", True) if args else True + self.num_multiplier_key = args.get("num_multiplier_key", "num_multiplier") if args else "num_multiplier" + + def __call__(self, data_dict: dict) -> dict | None: + """ + Append video duration and FPS as text timestamps to the caption. + + Args: + data_dict (dict): Input data dict containing caption, fps, and video tensor + + Returns: + data_dict (dict): Output dict with augmented caption, or None if error and skip_on_error=False + """ + if not self.enabled: + return data_dict + # Get caption - must exist at this point (set by TextTransformForVideo) + if self.caption_key not in data_dict: + if self.skip_on_error: + log.warning( + f"DurationFPSTextTimeStamps: '{self.caption_key}' not found in data_dict. Skipping.", + rank0_only=False, + ) + return data_dict + else: + return None + caption = data_dict[self.caption_key] + if (not isinstance(caption, str) and not isinstance(caption, dict)) or caption == "": + if self.skip_on_error: + return data_dict + else: + return None + + # Use pre-calculated conditioning_fps from VideoParsing augmentor + # This already accounts for frame skipping (fps / num_multiplier) + fps_value = data_dict[self.fps_key] + if isinstance(fps_value, torch.Tensor): + fps = fps_value.item() if fps_value.numel() == 1 else fps_value[0].item() + else: + fps = float(fps_value) + + # Extract ACTUAL number of frames from the video tensor shape + # This is critical - we need the final frame count after all processing + video = data_dict[self.video_key] + + # Video shape is (C, T, H, W) + num_frames = video.shape[1] + + # Compute duration and append to caption + if fps > 0: + duration = int(num_frames / fps) + if isinstance(caption, str): + # Case 1: Caption is a string (existing behavior). + metadata_text = self.template.format(duration=duration, fps=fps) + + # Choose separator based on whether caption ends with a period + separator = " " if caption.rstrip().endswith(".") else self.default_separator + + # Update caption text + data_dict[self.caption_key] = caption + separator + metadata_text + elif isinstance(caption, dict): + # Case 2: Caption is JSON. Add structured duration/FPS fields. + data_dict[self.caption_key].update( + { + "duration": str(duration) + "s", + "fps": fps, + } + ) + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py b/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py new file mode 100644 index 00000000..e280fdbd --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py @@ -0,0 +1,192 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Augmentor that appends idle-frame count metadata to the caption. + +The label is a Pi0.7-style episode-metadata field encoded as plain text. It +records how many frames of the action chunk were "idle" out of the total action +frames (i.e. the relative-pose delta is close to identity and the gripper +command does not change). The upstream dataset is responsible for populating +``data_dict[idle_frames_key]`` via +:func:`projects.cosmos3.vfm.datasets.action.pose_utils.compute_idle_frames`. + +Per-field dropout (default 5%) is applied here, matching Pi0.7's approach of +independently dropping each metadata component. This is complementary to the +global ``cfg_dropout_rate`` in :class:`TextTokenizerTransform`, which still +empties the whole caption. +""" + +from __future__ import annotations + +import random + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + +DEFAULT_TEMPLATE = "IdleFrames: {n} out of {m}." +FALLBACK_TEMPLATE = "IdleFrames: {n}." + + +class IdleFramesTextInfo(Augmentor): + """Augmentor that appends ``IdleFrames: N out of M.`` to the caption. + + Reads ``data_dict[idle_frames_key]`` (set by the dataset layer) and appends + a textual marker to the caption, modeled after + :class:`ResolutionTextInfo` and :class:`DurationFPSTextTimeStamps`. + + Per-field dropout is supported: with probability ``dropout_rate`` the + segment is omitted entirely (the caption is left unchanged). This is + independent from the global classifier-free-guidance dropout in the + tokenizer. + + Example: + Original caption: "pick up the cup" + Augmented: "pick up the cup. IdleFrames: 0 out of 16." + + Args: + input_keys (list): Input keys (not used, kept for API compatibility). + output_keys (list): Output keys (not used, kept for API compatibility). + args (dict): Configuration arguments: + - caption_key (str): Key for caption in data_dict. Default ``"ai_caption"``. + - idle_frames_key (str): Key for the idle-frame integer in data_dict. + Default ``"idle_frames"``. + - total_frames_key (str): Optional key for the total frame integer + in data_dict. Default ``"idle_frames_total"``. + - action_key (str): Key for the action tensor used to infer total + frames when ``total_frames_key`` is missing. Default ``"action"``. + - template (str): Format string for the appended segment. + Default ``"IdleFrames: {n} out of {m}."``. + - separator (str): Separator inserted between the original caption + and the new segment. Default ``". "``. + - dropout_rate (float): Probability of skipping the append step + (per-field dropout). Default 0.05. + - enabled (bool): Whether the augmentor is active. Default True. + """ + + def __init__( + self, + input_keys: list | None = None, + output_keys: list | None = None, + args: dict | None = None, + ) -> None: + super().__init__(input_keys, output_keys, args) + + args = args or {} + self.caption_key: str = args.get("caption_key", "ai_caption") + self.idle_frames_key: str = args.get("idle_frames_key", "idle_frames") + self.total_frames_key: str = args.get("total_frames_key", "idle_frames_total") + self.action_key: str = args.get("action_key", "action") + self.template: str = args.get("template", DEFAULT_TEMPLATE) + self.default_separator: str = args.get("separator", ". ") + self.dropout_rate: float = float(args.get("dropout_rate", 0.05)) + self.enabled: bool = bool(args.get("enabled", True)) + + if not 0.0 <= self.dropout_rate <= 1.0: + raise ValueError(f"dropout_rate must be in [0, 1]; got {self.dropout_rate}") + + def _get_scalar_int(self, value: object, key: str) -> int | None: + """Parse an optional scalar integer metadata value.""" + + if value is None: + return None + + if isinstance(value, torch.Tensor): + if value.numel() != 1: + log.warning( + f"IdleFramesTextInfo: expected scalar tensor at '{key}', got shape {tuple(value.shape)}. Skipping.", + rank0_only=False, + ) + return None + return int(value.item()) + + try: + return int(value) + except (TypeError, ValueError): + log.warning( + f"IdleFramesTextInfo: expected integer-compatible value at " + f"'{key}', got {type(value).__name__}. Skipping.", + rank0_only=False, + ) + return None + + def _get_total_frames(self, data_dict: dict) -> int | None: + """Resolve the total action-frame count for the idle-frame text.""" + + total_frames = self._get_scalar_int(data_dict.get(self.total_frames_key), self.total_frames_key) + if total_frames is not None: + return total_frames + + action = data_dict.get(self.action_key) + if isinstance(action, torch.Tensor): + if action.ndim == 0: + log.warning( + f"IdleFramesTextInfo: expected action tensor at " + f"'{self.action_key}' to have a frame dimension. Skipping total frames.", + rank0_only=False, + ) + return None + return int(action.shape[0]) + + try: + return len(action) if action is not None else None + except TypeError: + return None + + def __call__(self, data_dict: dict) -> dict | None: + """Append ``IdleFrames: N out of M.`` to ``data_dict[caption_key]`` in place. + + Returns the input dict unchanged when: + + - the augmentor is disabled, + - the per-field dropout fires, + - ``idle_frames_key`` is missing or ``None`` (e.g. non-action sample), + - the caption is missing, empty, or not a string/dict (unconditional case). + + For dict-typed captions (the JSON-caption code path), the idle-frame + integer is added under ``"idle_frames"`` and the total count, when + available, is added under ``"idle_frames_total"``. + """ + if not self.enabled: + return data_dict + + if random.random() < self.dropout_rate: + return data_dict + + n = self._get_scalar_int(data_dict.get(self.idle_frames_key), self.idle_frames_key) + if n is None: + return data_dict + + m = self._get_total_frames(data_dict) + + if self.caption_key not in data_dict: + return data_dict + caption = data_dict[self.caption_key] + + if isinstance(caption, str): + if caption == "": + return data_dict + metadata_text = self.template.format(n=n, m=m) if m is not None else FALLBACK_TEMPLATE.format(n=n) + separator = " " if caption.rstrip().endswith(".") else self.default_separator + data_dict[self.caption_key] = caption + separator + metadata_text + elif isinstance(caption, dict): + data_dict[self.caption_key]["idle_frames"] = n + if m is not None: + data_dict[self.caption_key]["idle_frames_total"] = m + else: + return data_dict + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py b/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py new file mode 100644 index 00000000..96315f4e --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py @@ -0,0 +1,382 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Augmentors for image editing tasks in the cosmos3 VFM pipeline. + +These augmentors process conversation-format image editing data and produce +the output format expected by the main training pipeline: + - images: List[torch.Tensor] (source + target images as a two-frame "video") + - image_size: List[torch.Tensor] + - ai_caption: List[str] + - selected_caption_type: List[str] + - fps: List[float] + - num_frames: List[int] + - dataset_name: str + - sequence_plan: SequencePlan +""" + +from __future__ import annotations + +import random + +import torch +import torchvision.transforms.functional as transforms_F +from PIL import Image + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan + + +class ExtractImageEditingConversation(Augmentor): + """Extract and validate image editing conversation from standard annotation format. + + This augmentor processes the cosmos-interleaved conversation format for image editing: + - Validates that the conversation has exactly one round (user + assistant) + - User message must contain at least one image and text instruction + - Assistant message must contain exactly one image (the edited result) + - If multi-round conversation is found, only the first round is kept + + Input Format (from data_dict): + - texts: Dict containing "content" with conversation data + - mllm_media_list: Dict mapping image keys to PIL images (for understanding) + - diffusion_media_list: Dict mapping image keys to PIL images (for diffusion/VAE) + + Output Format (added to data_dict): + - source_image: PIL.Image (the input image for editing) + - target_image: PIL.Image (the edited output image) + - editing_instruction: str (the user's editing instruction) + """ + + def __init__( + self, + input_keys: list | None = None, + max_round: int = 1, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], None, args) + self.max_round = max_round + + def __call__(self, data_dict: dict) -> dict | None: + """Extract image editing conversation. + + Args: + data_dict: Input data dictionary. + + Returns: + Updated data_dict with source_image, target_image, editing_instruction, + or None if the data is invalid. + """ + # Validate required keys + for required_key in ["mllm_media_list", "diffusion_media_list", "texts"]: + if required_key not in data_dict: + log.warning( + f"{required_key} not found in data_dict: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + mllm_media_list = data_dict["mllm_media_list"] + diffusion_media_list = data_dict["diffusion_media_list"] + + # Get conversation content + try: + texts_content = data_dict["texts"].get("content") + if texts_content is None: + log.warning( + f"texts.content is None: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Handle case where content is a list of conversation options + if isinstance(texts_content, list) and len(texts_content) > 0: + if isinstance(texts_content[0], list): + # Multiple conversation options, randomly select one + selected_conversations = random.choice(texts_content) + else: + selected_conversations = texts_content + else: + log.warning( + f"Unexpected texts.content format: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + except Exception as e: + log.warning( + f"Error accessing texts.content: {data_dict.get('__key__', 'unknown')}, {str(e)}", + rank0_only=False, + ) + return None + + # For image editing, we only keep the first round (user + assistant) + # Trim to first round if multiple rounds exist + if len(selected_conversations) > 2: + log.warning( + f"Multi-round conversation found ({len(selected_conversations)} messages), " + f"keeping only first round: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + selected_conversations = selected_conversations[:2] + + if len(selected_conversations) < 2: + log.warning( + f"Expected at least 2 messages (user + assistant), got {len(selected_conversations)}: " + f"{data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Validate roles: first must be user, second must be assistant + user_msg = selected_conversations[0] + assistant_msg = selected_conversations[1] + + if user_msg.get("role") != "user": + log.warning( + f"First message role is not 'user': {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + if assistant_msg.get("role") != "assistant": + log.warning( + f"Second message role is not 'assistant': {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Extract user content: must have at least one image and one text + user_content = user_msg.get("content", []) + if isinstance(user_content, str): + user_content = [{"type": "text", "text": user_content}] + + user_text_parts: list[str] = [] + user_image_key: str | None = None + + for item in user_content: + if not isinstance(item, dict): + continue + content_type = item.get("type") + if content_type == "text": + user_text_parts.append(item.get("text", "")) + elif content_type == "image": + if user_image_key is None: + user_image_key = item.get("image") + # If multiple user images, we only take the first one + + if user_image_key is None: + log.warning( + f"No image found in user message: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + editing_instruction = " ".join(user_text_parts).strip() + if not editing_instruction: + log.warning( + f"No text instruction found in user message: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Extract assistant content: must have exactly one image + assistant_content = assistant_msg.get("content", []) + if isinstance(assistant_content, str): + log.warning( + f"Assistant content is text-only (no image): {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + assistant_image_key: str | None = None + for item in assistant_content: + if not isinstance(item, dict): + continue + if item.get("type") == "image": + assistant_image_key = item.get("image") + break + + if assistant_image_key is None: + log.warning( + f"No image found in assistant message: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Validate images exist in media lists + for media_key in [user_image_key, assistant_image_key]: + if media_key not in diffusion_media_list: + log.warning( + f"Image {media_key} not found in diffusion_media_list: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + # Get PIL images + source_image = diffusion_media_list[user_image_key] + target_image = diffusion_media_list[assistant_image_key] + + # Handle video (list of frames) - use first frame + if isinstance(source_image, list): + source_image = source_image[0] if source_image else None + if isinstance(target_image, list): + target_image = target_image[0] if target_image else None + + if source_image is None or target_image is None: + log.warning( + f"Source or target image is None: {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + data_dict["source_image"] = source_image + data_dict["target_image"] = target_image + data_dict["editing_instruction"] = editing_instruction + + return data_dict + + +class ImageEditingToTrainingFormat(Augmentor): + """Convert extracted image editing data to the training-compatible format. + + This augmentor takes the source image, target image, and editing instruction + and produces the output format expected by the main training pipeline. + + Images are assumed to have been already resized by an upstream augmentor + (e.g. ``OmniInterleavedMediaResize``). This augmentor only normalises the + PIL images to tensors and assembles the remaining metadata fields. + + Input (from data_dict): + - source_image: PIL.Image (already resized by upstream augmentor) + - target_image: PIL.Image (already resized by upstream augmentor) + - editing_instruction: str + + Output (added to data_dict): + - images: list[torch.Tensor] — ``[source (C,H_s,W_s), target (C,H_t,W_t)]`` + - ai_caption: str + - selected_caption_type: str + - fps: float + - num_frames: int + - sequence_plan: SequencePlan + """ + + def __init__( + self, + input_keys: list | None = None, + mean: float = 0.5, + std: float = 0.5, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], None, args) + self.mean = mean + self.std = std + + def _normalize_image(self, image: Image.Image) -> torch.Tensor: + """Convert PIL image to normalized tensor (C, H, W).""" + tensor = transforms_F.to_tensor(image) + tensor = transforms_F.normalize(tensor, mean=[self.mean] * 3, std=[self.std] * 3) + return tensor + + def __call__(self, data_dict: dict) -> dict | None: + """Convert image editing data to training format. + + Args: + data_dict: Input data dictionary with source_image, target_image, editing_instruction. + + Returns: + Updated data_dict with training-compatible fields, or None on error. + """ + source_image: Image.Image = data_dict.get("source_image") + target_image: Image.Image = data_dict.get("target_image") + editing_instruction: str = data_dict.get("editing_instruction", "") + + if source_image is None or target_image is None: + return None + + try: + # Normalize PIL images to tensors (upstream augmentor already handled resizing) + source_tensor = self._normalize_image(source_image) # [C,H_s,W_s] + target_tensor = self._normalize_image(target_image) # [C,H_t,W_t] + + # Store as list of tensors for the batch collation. + # Each image keeps its own spatial size; the model encodes them separately. + data_dict["images"] = [source_tensor, target_tensor] + + # Set text fields + data_dict["ai_caption"] = editing_instruction + data_dict["selected_caption_type"] = "editing_instruction" + + # Set metadata + data_dict["fps"] = 30.0 # Same as standard image training + data_dict["num_frames"] = 2 # Source + target = 2 frames + data_dict["image_size"] = [ + torch.tensor( + [source_image.height, source_image.width, source_image.height, source_image.width], + dtype=torch.float, + ), # [4] + torch.tensor( + [target_image.height, target_image.width, target_image.height, target_image.width], + dtype=torch.float, + ), # [4] + ] + # Set the dataset name if not already present + if "dataset_name" not in data_dict: + data_dict["dataset_name"] = "image_editing" + + # Build sequence plan for image editing. + # The number of vision items per sample (e.g. 2 for source + target) is tracked + # by GenerationDataClean.num_vision_items_per_sample (set in get_data_and_condition). + # In pack_input_sequence, all items except the last are fully conditioned; + # the last item uses condition_frame_indexes_vision ([] = fully generated). + data_dict["sequence_plan"] = SequencePlan( + has_text=True, + has_vision=True, + condition_frame_indexes_vision=[], # Target (last item) is fully generated + ) + + except Exception as e: + log.warning( + f"Error processing image editing data: {data_dict.get('__key__', 'unknown')}, {str(e)}", + rank0_only=False, + ) + return None + + return data_dict + + +class RemoveKeys(Augmentor): + """Remove specified keys from the data dictionary. + + This is useful for cleaning up intermediate keys that are not needed + downstream (e.g. raw PIL images, media lists) so that every remaining + value is a tensor, number, dict, or list — as required by the dataloader + collation. + + Args: + input_keys: Keys to remove from ``data_dict``. + """ + + def __init__( + self, + input_keys: list | None = None, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], None, args) + + def __call__(self, data_dict: dict) -> dict: + for key in self.input_keys: + data_dict.pop(key, None) + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py b/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py new file mode 100644 index 00000000..c77dd29d --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.vfm.datasets.utils import IMAGE_RES_SIZE_INFO + +# Map dataset_resolution_type to resolution tier key in IMAGE_RES_SIZE_INFO +_DATASET_RESOLUTION_TIER: dict[str, str] = {"gt480p": "480", "gt720p": "720", "gt1080p": "1080"} + + +class ImageResolutionFilter(Augmentor): + """ + Filters out image samples whose (width, height) are below the minimum for + the sample's aspect ratio when dataset_resolution_type is not "all". + Mirrors the resolution check used in video_parsing. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.image_key = args.get("image_key", "images") if args else "images" + self.dataset_resolution_type = args.get("dataset_resolution_type", "all") if args else "all" + self.resolution_tier = _DATASET_RESOLUTION_TIER.get(self.dataset_resolution_type) + + def __call__(self, data_dict: dict) -> dict | None: + image = data_dict.get(self.image_key) + if image is None: + return data_dict + + # PIL Image has .size as (width, height) + width, height = image.size + + aspect_ratio: str | None = None + if "__url__" in data_dict: + aspect_ratio = data_dict["__url__"].meta.opts["aspect_ratio"] + + # If the resolution of the image is smaller than the minimum resolution for the aspect ratio, skip the sample. This will ensure that we do not upsample any image. + if self.resolution_tier is not None: + min_w, min_h = IMAGE_RES_SIZE_INFO[self.resolution_tier][aspect_ratio] + if width < min_w and height < min_h: + return None + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py b/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py new file mode 100644 index 00000000..4eb93fb3 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py @@ -0,0 +1,278 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Visual transformation augmentors for Omni models. +""" + +import math +from typing import Dict, List, Optional + +import torch +import torchvision.transforms.functional as transforms_F +from PIL import Image + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_image_size + + +class ResizeToPaddingDivisor(Augmentor): + """Resize images so that both width and height are multiples of padding_divisor.""" + + def __init__(self, input_keys: list, padding_divisor: int = 16) -> None: + super().__init__(input_keys) + self.padding_divisor = padding_divisor + + def __call__(self, data_dict: dict) -> dict: + """Resize images to the nearest multiple of padding_divisor. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with resized images and metadata + """ + + if self.output_keys is None: + self.output_keys = self.input_keys + + # Get original image size + orig_w, orig_h = obtain_image_size(data_dict, self.input_keys) + + # Calculate new dimensions as multiples of padding_divisor + new_w = math.ceil(orig_w / self.padding_divisor) * self.padding_divisor + new_h = math.ceil(orig_h / self.padding_divisor) * self.padding_divisor + + # Resize images + for inp_key, out_key in zip(self.input_keys, self.output_keys): + data_dict[out_key] = transforms_F.resize( + data_dict[inp_key], + size=(new_h, new_w), + interpolation=transforms_F.InterpolationMode.BICUBIC, + antialias=True, + ) + if out_key != inp_key: + del data_dict[inp_key] + + # Store image size information (new_h, new_w, orig_h, orig_w) + data_dict["image_size"] = torch.tensor([new_h, new_w, orig_h, orig_w], dtype=torch.float) # [4] + + return data_dict + + +class InterleavedMediaResize(Augmentor): + """Resizes interleaved media content (images and videos) for both diffusion and MLLM models. + + This augmentor processes mixed media content containing both images and videos, creating two + versions of each media item: one optimized for diffusion models and another for Multimodal + Large Language Models (MLLMs). It preserves aspect ratios while ensuring dimensions meet + specific constraints for each model type. + + The resizing process follows these steps: + 1. Maintains aspect ratio while ensuring no side exceeds the maximum allowed length + 2. Adjusts dimensions to be divisible by model-specific padding constants + 3. Uses high-quality LANCZOS resampling for optimal visual quality + + Args: + input_keys (List, optional): List containing the key to access media content in data_dict. + Must contain exactly one key. Defaults to ['media_list']. + max_diffusion_image_side_length (int, optional): Maximum side length for diffusion model + images. Defaults to 1024. + max_mllm_image_side_length (int, optional): Maximum side length for MLLM images. + Defaults to 768. + diffusion_image_padding_constant (int, optional): Divisor for diffusion model image + dimensions. Both width and height must be divisible by this value. Defaults to 16. + mllm_image_padding_constant (int, optional): Divisor for MLLM image dimensions. + Both width and height must be divisible by this value. Defaults to 28. + use_center_crop (bool, optional): If True, uses center cropping to ensure dimensions + are divisible by padding constants, avoiding distortion. If False, uses resizing + which may cause slight distortion. Defaults to False. + args (Optional[dict], optional): Additional arguments passed to parent class. + Defaults to None. + + Input Format: + The data_dict should contain a key (specified in input_keys) with value structured as: + { + "image_0": PIL.Image, # Single image + "image_1": PIL.Image, # Another single image + "video_0": List[PIL.Image], # Video as list of frames + "video_1": List[PIL.Image], # Another video + ... + } + + Output Format: + The method adds two new keys to data_dict: + - 'diffusion_media_content': Resized media for diffusion models + - 'mllm_media_content': Resized media for MLLMs + + Both follow the same structure as the input, with resized versions of each media item. + + Example: + >>> # Using resize (default, may cause slight distortion) + >>> augmentor = OmniInterleavedMediaResize( + ... input_keys=['media_list'], + ... max_diffusion_image_side_length=1024, + ... max_mllm_image_side_length=768 + ... ) + >>> + >>> # Using center crop (no distortion) + >>> augmentor_crop = OmniInterleavedMediaResize( + ... input_keys=['media_list'], + ... max_diffusion_image_side_length=1024, + ... max_mllm_image_side_length=768, + ... use_center_crop=True + ... ) + >>> + >>> data_dict = { + ... 'media_list': { + ... 'image_0': pil_image, + ... 'video_0': [frame1, frame2, frame3] + ... } + ... } + >>> result = augmentor(data_dict) + >>> # result now contains 'diffusion_media_content' and 'mllm_media_content' + + Note: + - Images are only scaled down, never up, to preserve quality + - Videos are processed frame by frame, maintaining temporal consistency + - Unsupported media types will raise a ValueError + - When use_center_crop=True, images are center-cropped to achieve padding divisibility + without distortion. When False, images are resized which may cause slight distortion. + """ + + def __init__( + self, + input_keys: List = ["media_list"], + max_diffusion_image_side_length: int = 1024, + max_mllm_image_side_length: int = 768, + diffusion_image_padding_constant: int = 16, + use_center_crop: bool = False, + args: Optional[dict] = None, + ) -> None: + super().__init__(input_keys, None, args) + self.max_diffusion_image_side_length = max_diffusion_image_side_length + self.max_mllm_image_side_length = max_mllm_image_side_length + self.diffusion_image_padding_constant = diffusion_image_padding_constant + self.use_center_crop = use_center_crop + + def __call__(self, data_dict: Dict) -> Dict: + assert len(self.input_keys) == 1, ( + "This transform only supports one input key. Try to organize all the media contents under one key." + ) + if self.input_keys[0] not in data_dict: + print(f"Input key {self.input_keys[0]} not found in data_dict: {data_dict['__key__']}") + return None + original_media_content = data_dict[self.input_keys[0]] + + diffusion_media_content = {} + mllm_media_content = {} + + for key, media in original_media_content.items(): + # Check if it's an image or video + if isinstance(media, Image.Image): + # Process single image + diffusion_media_content[key] = self._resize_image( + media, + self.max_diffusion_image_side_length, + self.diffusion_image_padding_constant, + self.use_center_crop, + ) + mllm_media_content[key] = self._resize_image( + media, + self.max_mllm_image_side_length, + None, # we don't need to resize the mllm media content to a specific padding constant since it will be handled by the processor + self.use_center_crop, + ) + elif isinstance(media, list) and all(isinstance(frame, Image.Image) for frame in media): + # Process video (list of images) + diffusion_media_content[key] = [ + self._resize_image( + frame, + self.max_diffusion_image_side_length, + self.diffusion_image_padding_constant, + self.use_center_crop, + ) + for frame in media + ] + mllm_media_content[key] = [ + self._resize_image( + frame, + self.max_mllm_image_side_length, + None, # we don't need to resize the mllm media content to a specific padding constant since it will be handled by the processor + self.use_center_crop, + ) + for frame in media + ] + else: + raise ValueError(f"Unsupported media type for key {key}: {type(media)}") + + # Add the resized media content to data_dict + data_dict["diffusion_media_list"] = diffusion_media_content + data_dict["mllm_media_list"] = mllm_media_content + + return data_dict + + def _resize_image( + self, image: Image.Image, max_side_length: int, padding_divisor=None, use_center_crop: bool = False + ) -> Image.Image: + """Resize image while preserving aspect ratio and ensuring dimensions are divisible by padding_divisor. + + Args: + image: Input PIL Image + max_side_length: Maximum allowed side length + padding_divisor: Both dimensions must be divisible by this value + use_center_crop: If True, use center crop to achieve divisibility; if False, use resize + + Returns: + Resized PIL Image + """ + # Get original dimensions + width, height = image.size + + # Calculate scale factor to ensure max side length constraint + scale_factor = min(max_side_length / width, max_side_length / height) + + # Only scale down, not up + if scale_factor < 1.0: + new_width = max(1, int(width * scale_factor)) + new_height = max(1, int(height * scale_factor)) + else: + new_width = width + new_height = height + + # Resize image to maintain aspect ratio + resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # Calculate target dimensions that are divisible by padding_divisor + if padding_divisor is not None: + final_width = max(1, (new_width // padding_divisor)) * padding_divisor + final_height = max(1, (new_height // padding_divisor)) * padding_divisor + else: + final_width = new_width + final_height = new_height + + # If dimensions need adjustment + if final_width != new_width or final_height != new_height: + if use_center_crop: + # Use center crop to achieve target dimensions + left = (new_width - final_width) // 2 + top = (new_height - final_height) // 2 + right = left + final_width + bottom = top + final_height + resized_image = resized_image.crop((left, top, right, bottom)) + else: + # Use resize (may cause distortion) + resized_image = resized_image.resize((final_width, final_height), Image.Resampling.LANCZOS) + + return resized_image diff --git a/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py b/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py new file mode 100644 index 00000000..c41bf43e --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from typing import Optional + +import numpy as np +import omegaconf +import torch +from torchcodec.decoders import VideoDecoder +from torchvision.transforms.v2 import Resize + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.augmentors.video_parsing import VideoParsingWithFullFrames + +# Local copies of the torchcodec decoder helpers so this module does not depend on +# private symbols of ``video_parsing.py``. Behavior matches the originals. +_PostDecodeTransforms = list[Callable[[torch.Tensor], torch.Tensor]] | None +_SUPPORTS_VIDEO_DECODER_TRANSFORMS: bool | None = None +_WARNED_POST_DECODE_TRANSFORMS = False + + +def _create_video_decoder( + video: bytes, + seek_mode: str, + num_ffmpeg_threads: int, + transforms: _PostDecodeTransforms = None, +) -> tuple[VideoDecoder, _PostDecodeTransforms]: + global _SUPPORTS_VIDEO_DECODER_TRANSFORMS, _WARNED_POST_DECODE_TRANSFORMS + + kwargs = {"seek_mode": seek_mode, "num_ffmpeg_threads": num_ffmpeg_threads} + if transforms is None: + return VideoDecoder(video, **kwargs), None + + if _SUPPORTS_VIDEO_DECODER_TRANSFORMS is not False: + try: + decoder = VideoDecoder(video, transforms=transforms, **kwargs) + _SUPPORTS_VIDEO_DECODER_TRANSFORMS = True + return decoder, None + except TypeError as e: + if "transforms" not in str(e): + raise + _SUPPORTS_VIDEO_DECODER_TRANSFORMS = False + + if not _WARNED_POST_DECODE_TRANSFORMS: + log.warning( + "Installed torchcodec does not support VideoDecoder(transforms=...); " + "applying video transforms after frame decode.", + rank0_only=False, + ) + _WARNED_POST_DECODE_TRANSFORMS = True + return VideoDecoder(video, **kwargs), transforms + + +def _apply_post_decode_transforms( + frames: torch.Tensor, transforms: _PostDecodeTransforms +) -> torch.Tensor: # frames: [T,C,H,W], returns: [T,C,H,W] + if transforms is None: + return frames + + for transform in transforms: + frames = transform(frames) # [T,C,H,W] + return frames + + +class VideoTransferAlignedFullFramesParsing(VideoParsingWithFullFrames): + """Decode RGB and precomputed control videos with one shared v3 frame plan. + + This is the variable-length counterpart of the fixed-window transfer parser. + The RGB stream determines the sampled stride and frame indices. Any extra + input video streams, such as depth or segmentation, are decoded with the same + frame indices so the control video stays temporally aligned with the target. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + assert len(input_keys) >= 2, "VideoTransferAlignedFullFramesParsing requires [metas, video, ...]." + super().__init__(input_keys=input_keys[:2], output_keys=output_keys, args=args) + self.input_keys = input_keys + self.control_video_keys = input_keys[2:] + + def _build_rgb_decode_transform(self, data_dict: dict, meta_dict: dict) -> list[Resize] | None: + if not self.perform_resize: + return None + + img_size = obtain_augmentation_size(data_dict, {"size": self.size}) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + orig_w, orig_h = meta_dict["width"], meta_dict["height"] + + scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + assert target_size[0] <= img_h and target_size[1] <= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + return [Resize(target_size)] + + def _sample_frame_indices(self, decoder_len: int) -> tuple[list[int], int]: + stride = self._sample_stride_with_bias(self.max_stride, self.min_stride) + frame_indices = np.arange(0, decoder_len, stride).tolist() + max_num_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) + if max_num_frames < 1: + return [], stride + + # Wan VAE temporal compression expects 1 + 4N video frames. + num_video_frames = 1 + 4 * ((max_num_frames - 1) // 4) + return frame_indices[:num_video_frames], stride + + def _decode_frames_at( + self, + video: bytes, + frame_indices: list[int], + transforms: list[Resize] | None = None, + ) -> torch.Tensor: # returns [C,T,H,W] + video_decoder, post_decode_transforms = _create_video_decoder( + video, + self.seek_mode, + self.video_decode_num_threads, + transforms, + ) + try: + frame_batch = video_decoder.get_frames_at(frame_indices) + frames = frame_batch.data # [T,C,H,W] + frames = _apply_post_decode_transforms(frames, post_decode_transforms) # [T,C,H,W] + frames = frames.permute(1, 0, 2, 3) # [C,T,H,W] + finally: + del video_decoder + return frames # [C,T,H,W] + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + rgb_transform = self._build_rgb_decode_transform(data_dict, meta_dict) + try: + rgb_decoder = VideoDecoder( + video, + seek_mode=self.seek_mode, + num_ffmpeg_threads=self.video_decode_num_threads, + ) + decoder_len = len(rgb_decoder) + del rgb_decoder + + frame_indices, stride = self._sample_frame_indices(decoder_len) + if len(frame_indices) == 0: + log.warning( + f"VideoTransferAlignedFullFramesParsing: no valid frame indices. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + video_frames = self._decode_frames_at(video, frame_indices, rgb_transform) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode RGB video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + base_video_info = { + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "frame_indices": frame_indices, + "num_frames": len(frame_indices), + "fps": meta_dict["framerate"], + "conditioning_fps": meta_dict["framerate"] / stride, + "num_multiplier": stride, + "n_orig_video_frames": decoder_len, + } + data_dict[self.video_key] = { + **base_video_info, + "video": video_frames, # [C,T,H,W] + } + + for control_video_key in self.control_video_keys: + control_video = data_dict.get(control_video_key) + if not isinstance(control_video, bytes): + log.warning( + f"VideoTransferAlignedFullFramesParsing: missing bytes for {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + try: + control_frames = self._decode_frames_at(control_video, frame_indices) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + data_dict[control_video_key] = { + **base_video_info, + "video": control_frames, # [C,T,H,W] + } + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py b/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py new file mode 100644 index 00000000..b7056e39 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +class KeyRenamer(Augmentor): + """Renames keys in data_dict. Runs as the first augmentor to normalize key names. + + Args: + input_keys: Not used (required by Augmentor interface). + output_keys: Not used. + args: Dictionary with: + - rename_map: dict[str, str] mapping old_key -> new_key. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.rename_map: dict[str, str] = args.get("rename_map", {}) if args else {} + + def __call__(self, data_dict: dict) -> dict: + if not self.rename_map: + return data_dict + + for old_key, new_key in self.rename_map.items(): + if old_key in data_dict: + data_dict[new_key] = data_dict.pop(old_key) + return data_dict + + +class DataDictMerger(Augmentor): + def __init__(self, input_keys: list, output_keys: list, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict | None: + r"""Merge the dictionary associated with the input keys into data_dict. Only keys in output_keys are merged. + + Supports transfer-style keys (e.g. depth_pervideo_video_depth_anything): when "depth" in key + assigns key_dict["video"] to data_dict["depth"]; when "segmentation" in key assigns + key_dict["video"] or key_dict to data_dict["segmentation"]. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with dictionary associated with the input keys merged. + """ + for key in self.input_keys: + if key not in data_dict: + log.warning( + f"DataDictMerger dataloader error: missing {key}, {data_dict['__url__']}, {data_dict['__key__']}", + rank0_only=False, + ) + return None + key_dict = data_dict.pop(key) + if "depth" in key and "depth" in self.output_keys: + data_dict["depth"] = key_dict["video"] + elif "segmentation" in key and "segmentation" in self.output_keys: + data_dict["segmentation"] = key_dict["video"] if "video" in key_dict else key_dict + if isinstance(key_dict, dict): + for sub_key in key_dict: + if sub_key in self.output_keys and sub_key not in data_dict: + data_dict[sub_key] = key_dict[sub_key] + del key_dict + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py b/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py new file mode 100644 index 00000000..3a092f18 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py @@ -0,0 +1,360 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Augmentors for handling video loading from pickled bytes.""" + +import io +import pickle as pkl +import random +import re +from typing import Dict, Optional + +import numpy as np +import torch +from PIL import Image, UnidentifiedImageError +from qwen_vl_utils.vision_process import smart_nframes, smart_resize +from torchvision import transforms +from torchvision.transforms import InterpolationMode + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + +Image.MAX_IMAGE_PIXELS = 933120000 +_VIDEO_EXTENSIONS = "mp4 avi webm mov".split() + +VIDEO_DECODER_OPTIONS = {} + + +def token_to_pixels(token_length: int, patch_size: int = 14, temporal_patch_size: int = 2) -> int: + """Convert token length to pixels based on patch size and temporal patch size.""" + + merged_patch_size = patch_size * 2 + return token_length * merged_patch_size**2 * temporal_patch_size + + +def pixels_to_token(pixels: int, patch_size: int = 14, temporal_patch_size: int = 2) -> int: + """Convert pixels to token length based on patch size and temporal patch size.""" + + merged_patch_size = patch_size * 2 + return pixels // merged_patch_size**2 // temporal_patch_size + + +def tensor_to_pil_images(video_tensor): + """ + Convert a video tensor of shape (C, T, H, W) or (T, C, H, W) to a list of PIL images. + + Args: + video_tensor (torch.Tensor): Video tensor with shape (C, T, H, W) or (T, C, H, W) + + Returns: + list[PIL.Image.Image]: List of PIL images + """ + # Check tensor shape and convert if needed + if video_tensor.shape[0] == 3 and video_tensor.shape[1] > 3: # (C, T, H, W) + # Convert to (T, C, H, W) + video_tensor = video_tensor.permute(1, 0, 2, 3) # [T,C,H,W] + + # Convert to numpy array with shape (T, H, W, C) + video_np = video_tensor.permute(0, 2, 3, 1).cpu().numpy() # [T,H,W,C] + + # Ensure values are in the right range for PIL (0-255, uint8) + if video_np.dtype == np.float32 or video_np.dtype == np.float64: + if video_np.max() <= 1.0: + video_np = (video_np * 255).astype(np.uint8) + else: + video_np = video_np.astype(np.uint8) + + # Convert each frame to a PIL image + pil_images = [Image.fromarray(frame) for frame in video_np] + + return pil_images + + +def _video_decoder_qwen_func( + key: str, + data: bytes, + min_fps_thres: int = 4, + max_fps_thres: int = 60, + target_fps: float = 2.0, + min_video_token_length: int = 16, + max_video_token_length: int = 8192, + num_threads: int = 0, + random_augmentation: bool = False, + fps_random_range: list[float] = [0.5, 1.5], + max_video_token_length_random_range: list[float] = [0.75, 1.25], + frame_count_random_range: Optional[list[int]] = None, + start_frame: Optional[int] = None, + end_frame: Optional[int] = None, + **kwargs, +) -> dict | None: + """Actual video decoder function. + + Args: + key (str): Video file name/key + data (bytes): Video binary data + min_fps_thres (int, optional): Minimum FPS threshold. Defaults to 4. + max_fps_thres (int, optional): Maximum FPS threshold. Defaults to 60. + target_fps (float, optional): Target FPS. Defaults to 2.0. + min_video_token_length (int, optional): Minimum token length. Defaults to 16. + max_video_token_length (int, optional): Maximum token length. Defaults to 8192. + num_threads (int, optional): Number of threads for decord. Defaults to 0. + random_augmentation (bool, optional): Whether to randomize the FPS and max_video_token_length. Defaults to False. + fps_random_range (list[float], optional): Random FPS range. Defaults to [10.0, 24.0]. + max_video_token_length_random_range (list[float], optional): Random max_video_token_length range. Defaults to [0.75, 1.25]. + frame_count_random_range (list[int], optional): Random frame count range. If provided, take priority over fps_random_range. + start_frame (Optional[int], optional): Start frame. Defaults to None. If both start_frame and end_frame are provided, the video will be decoded from start_frame to end_frame. + end_frame (Optional[int], optional): End frame. Defaults to None. If both start_frame and end_frame are provided, the video will be decoded from start_frame to end_frame. + + Raises: + ValueError: Video fps lower than 1, skipping + ValueError: Video fps lower than min_fps_thres, skipping + ValueError: Video fps higher than max_fps_thres, skipping + + Returns: + dict | None: Dictionary with video frames tensor and target FPS + """ + import decord + + # Check video extension + extension = re.sub(r".*[.]", "", key) + if extension.lower() not in _VIDEO_EXTENSIONS: + return None + + # Read video + video_buffer = io.BytesIO(data) + video_reader = decord.VideoReader(video_buffer, num_threads=num_threads) + total_frames, video_fps = len(video_reader), video_reader.get_avg_fps() + + if start_frame is not None and end_frame is not None: + total_frames = end_frame - start_frame + + if video_fps < 1: + raise ValueError("Video fps lower than 1, skipping") + if video_fps < min_fps_thres: + raise ValueError(f"Video fps {video_fps} lower than {min_fps_thres}, skipping") + if video_fps > max_fps_thres: + raise ValueError(f"Video fps {video_fps} higher than {max_fps_thres}, skipping") + + if random_augmentation: + if frame_count_random_range is not None: + # Random number of frames + min_frames_range, max_frames_range = frame_count_random_range + min_frames_range = min(min_frames_range, total_frames) + max_frames_range = min(max_frames_range, total_frames) + target_frames = random.uniform(min_frames_range, max_frames_range) + target_fps = target_frames / total_frames * video_fps + else: + # randomize fps + target_fps = ( + random.uniform(fps_random_range[0], fps_random_range[1]) * target_fps + if random.random() < 0.5 + else target_fps + ) + # randomize max_video_token_length + max_video_token_length = int( + random.uniform(max_video_token_length_random_range[0], max_video_token_length_random_range[1]) + * max_video_token_length + ) + log.debug(f"random_augmentation: max_video_token_length: {max_video_token_length}, target_fps: {target_fps}") + + patch_size = 14 + min_height_width = 56 # https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py#L57 + temporal_patch_size = 2 + min_pixels: int = token_to_pixels(min_video_token_length, patch_size, temporal_patch_size) + max_pixels: int = token_to_pixels(max_video_token_length, patch_size, temporal_patch_size) + max_frames: int = max_pixels // (min_height_width) ** 2 // temporal_patch_size + + # sample based on target fps + nframes = smart_nframes(dict(fps=target_fps), total_frames=total_frames, video_fps=video_fps) + nframes = min(nframes, max_frames) + if start_frame is not None and end_frame is not None: + idx = torch.linspace(start_frame, end_frame - 1, nframes).round().long().tolist() # [nframes] + else: + idx = torch.linspace(0, total_frames - 1, nframes).round().long().tolist() # [nframes] + video_frames = video_reader.get_batch(idx).asnumpy() + video_frames = torch.tensor(video_frames).permute(0, 3, 1, 2) # [T,C,H,W] + sample_fps = nframes / max(total_frames, 1e-6) * video_fps + + # recompute max_pixels based on number of sampled frames + nframes, _, height, width = video_frames.shape + max_pixels = max_pixels // nframes + resized_height, resized_width = smart_resize( + height, + width, + min_pixels=min_pixels, + max_pixels=max_pixels, + ) + video_frames = transforms.functional.resize( + video_frames, + [resized_height, resized_width], + interpolation=InterpolationMode.BICUBIC, + antialias=True, + ).float() + video_frames = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] + + # Clean up + video_reader.seek(0) # set video reader point back to 0 to clean up cache + del video_reader # delete the reader to avoid memory leak + + return dict(videos=video_frames, fps=sample_fps) + + +class PKLToMedia(Augmentor): + """ + Converts PKL bytes stored in a data dictionary into media. + + Handles input formats for the specified input key: + A dictionary mapping media names (str) to bytes objects. + + The output format is a dictionary mapping names to their respective decoded objects: + Input dict[str, bytes] -> Output dict[str, torch.Tensor | PIL.Image] + + Corrupted or non-decodable bytes are skipped with a warning. + """ + + def __init__( + self, + input_key: str = "media", + output_key: str = "media", + min_fps_thres: int = 4, + max_fps_thres: int = 60, + target_fps: float = 4.0, + min_video_token_length: int = 16, + max_video_token_length: int = 8192, + num_threads: int = 0, + random_augmentation: bool = False, + is_input_in_dict: bool = False, + use_start_frame_end_frame: bool = False, + frame_count_random_range: Optional[list[int]] = None, + ) -> None: + """ + Args: + input_key (str): Key in the data_dict containing video/image data. + output_key (str): Key to store the resulting video frame tensors or PIL images. + min_fps_thres (int): Minimum FPS threshold for video decoding. + max_fps_thres (int): Maximum FPS threshold for video decoding. + target_fps (float): Target FPS for video decoding. + min_video_token_length (int): Minimum token length for video decoding. + max_video_token_length (int): Maximum token length for video decoding. + num_threads (int): Number of threads for video decoding. + random_augmentation (bool): Whether to apply random augmentation during decoding. + is_input_in_dict (bool): Whether the input key is in the data_dict instead of pkl files. (For cosmos predict2 videos) + use_start_frame_end_frame (bool): Whether to use start_frame and end_frame to decode the video. (For cosmos predict2 videos) + frame_count_random_range (list[int], optional): Random frame count range. Defaults to None. + """ + self.input_key = input_key + self.output_key = output_key + self.video_decoder_params = { + "min_fps_thres": min_fps_thres, + "max_fps_thres": max_fps_thres, + "target_fps": target_fps, + "min_video_token_length": min_video_token_length, + "max_video_token_length": max_video_token_length, + "num_threads": num_threads, + "random_augmentation": random_augmentation, + "frame_count_random_range": frame_count_random_range, + } + self.is_input_in_dict = is_input_in_dict + self.use_start_frame_end_frame = use_start_frame_end_frame + + def _bytes_to_video_frames(self, video_bytes: bytes, identifier: str = "video") -> Optional[Dict]: + """Converts video bytes to video frame tensors using the video decoder.""" + try: + result = _video_decoder_qwen_func( + key=f"{identifier}.mp4", # Add .mp4 extension for the decoder + data=video_bytes, + **self.video_decoder_params, + ) + result["videos"] = tensor_to_pil_images(result["videos"]) # 3,T,H,W -> list of PIL images + if result is not None: + return result + else: + log.warning(f"Skipping item '{identifier}': Video decoder returned None.") + return None + except Exception as e: + log.warning(f"Skipping item '{identifier}': Error decoding video bytes: {e}") + return None + + def _bytes_to_pil(self, image_bytes: bytes, identifier: str = "image") -> Optional[Image.Image]: + """Converts a single bytes object to a PIL Image.""" + try: + with io.BytesIO(image_bytes) as stream: + img = Image.open(stream) + img.load() # Verify the image data + return img.convert("RGB") # Convert to standard RGB format + except UnidentifiedImageError: + log.warning(f"Skipping item '{identifier}': Cannot identify image file from bytes.") + except Exception as e: + log.warning(f"Skipping item '{identifier}': Error decoding image bytes: {e}") + return None + + def __call__(self, data_dict: Dict) -> Dict: + """ + Processes the data_dict to convert video/image bytes to their respective formats. + + Args: + data_dict (Dict): The input data dictionary. + + Returns: + Dict: The modified data dictionary with video frame tensors and/or PIL images. + """ + input_key = self.input_key + output_key = self.output_key + + if input_key not in data_dict: + log.debug( + f"Input key '{input_key}' not found in data_dict. Skipping PKLToMedia. Available keys: {data_dict.keys()}" + ) + return data_dict + + if not self.is_input_in_dict: + data = pkl.loads(data_dict[input_key]) + else: + data = data_dict[input_key] + + output_data = {} + + if isinstance(data, dict): + for name, item in data.items(): + if isinstance(item, bytes): + # Determine if this is video or image based on the key name + if "video" in name.lower(): + # Decode as video + result = self._bytes_to_video_frames(item, identifier=f"{input_key}['{name}']") + if result: + output_data[name] = result + elif "image" in name.lower(): + # Decode as image + result = self._bytes_to_pil(item, identifier=f"{input_key}['{name}']") + if result: + output_data[name] = result + else: + log.warning( + f"Skipping item with key '{name}' in '{input_key}': Key does not contain 'video' or 'image'." + ) + else: + log.warning(f"Skipping item with key '{name}' in '{input_key}': Expected bytes, got {type(item)}.") + else: + raise ValueError( + f"Input key '{input_key}' has unsupported type {type(data)}. " + f"Expected dict[str, bytes] for video/image data." + ) + + # Add the processed data and optionally remove the input key + data_dict[output_key] = output_data + if input_key != output_key: + del data_dict[input_key] + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py b/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py new file mode 100644 index 00000000..ef03adec --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor + +# Default templates for resolution info +DEFAULT_IMAGE_TEMPLATE = "This image is of {height}x{width} resolution." +DEFAULT_VIDEO_TEMPLATE = "This video is of {height}x{width} resolution." + + +class ResolutionTextInfo(Augmentor): + """ + Augmentor that appends resolution (height x width) info to captions. + + This augmentor should run AFTER CropToMultiple (which sets final_height/final_width) + and AFTER text transforms (so ai_caption exists), but BEFORE tokenization. + + Reads resolution from metadata keys (final_height, final_width) set by CropToMultiple. + Does NOT fall back to tensor shape to avoid incorrect latent dimensions. + + Automatically detects whether the input is an image or video based on which + key is present in the data_dict, and uses the appropriate template. + + Example: + Original caption: "A cat playing with a ball" + Augmented (image): "A cat playing with a ball. This image is 512x512." + Augmented (video): "A cat playing with a ball. This video is 480x854." + + Args: + input_keys (list): Input keys (not used, kept for API compatibility) + output_keys (list): Output keys (not used, kept for API compatibility) + args (dict): Configuration arguments: + - caption_key (str): Key for caption in data_dict. Default: "ai_caption" + - video_key (str): Key for video tensor in data_dict. Default: "video" + - image_size_key (str): Key for image size tensor in data_dict. Default: "image_size" + - image_template (str): Format string for image metadata. Default: DEFAULT_IMAGE_TEMPLATE + - video_template (str): Format string for video metadata. Default: DEFAULT_VIDEO_TEMPLATE + - separator (str): Separator between caption and metadata. Default: ". " + - enabled (bool): Whether augmentation is enabled. Default: True + """ + + def __init__( + self, input_keys: Optional[list] = None, output_keys: Optional[list] = None, args: Optional[dict] = None + ) -> None: + super().__init__(input_keys, output_keys, args) + + # Configuration with sensible defaults + self.caption_key = args.get("caption_key", "ai_caption") if args else "ai_caption" + self.image_key = args.get("image_key", "images") if args else "images" + self.video_key = args.get("video_key", "video") if args else "video" + self.image_size_key = args.get("image_size_key", "image_size") if args else "image_size" + self.image_template = args.get("image_template", DEFAULT_IMAGE_TEMPLATE) if args else DEFAULT_IMAGE_TEMPLATE + self.video_template = args.get("video_template", DEFAULT_VIDEO_TEMPLATE) if args else DEFAULT_VIDEO_TEMPLATE + self.default_separator = args.get("separator", ". ") if args else ". " + self.enabled = args.get("enabled", True) if args else True + + def __call__(self, data_dict: dict) -> dict | None: + """ + Append resolution (height x width) as text timestamps to the caption. + + Args: + data_dict (dict): Input data dict containing caption and image/video tensor + + Returns: + data_dict (dict): Output dict with augmented caption. + """ + if not self.enabled: + return data_dict + + # Get caption - must exist at this point (set by text transforms) + assert self.caption_key in data_dict, f"caption_key '{self.caption_key}' not found in data_dict." + caption = data_dict[self.caption_key] + + if (not isinstance(caption, str) and not isinstance(caption, dict)) or caption == "": + # This is for unconditional case. + return data_dict + + # Detect image vs video to select template + is_video = self.video_key in data_dict + is_image = self.image_key in data_dict + + if isinstance(caption, str): + # Case 1: Caption is a string. In this case, we create a string template for + # resolution, aspect ratio info and add it + if not is_video and not is_image: + raise ValueError("Neither video_key nor image_key found in data_dict.") + + template = self.video_template if is_video else self.image_template + + # Get dimensions from metadata keys (set by CropToMultiple) + image_size = data_dict.get(self.image_size_key) + height = int(image_size[0]) + width = int(image_size[1]) + + # Format metadata text + metadata_text = template.format(height=height, width=width) + + # Choose separator based on whether caption ends with a period + separator = " " if caption.rstrip().endswith(".") else self.default_separator + + # Update caption + data_dict[self.caption_key] = caption + separator + metadata_text + + elif isinstance(caption, dict): + # Case 2: Caption is a dictionary. This is for the json caption case. + # In this case, we add resolution and aspect ratio in json fields + aspect_ratio = data_dict["__url__"].meta.opts["aspect_ratio"] + height = int(data_dict["image_size"][0]) + width = int(data_dict["image_size"][1]) + data_dict[self.caption_key].update( + { + "resolution": {"H": height, "W": width}, + "aspect_ratio": aspect_ratio, + } + ) + + else: + raise ValueError(f"Unsupported caption type: {type(caption)}") + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py b/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py new file mode 100644 index 00000000..3f28d562 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Augmentor for creating sequence plans with random conditional frames. + +Supports two sampling strategies: +- weighted dict (``conditioning_config``): explicit frame-count → probability pairs +- uniform (``uniform_conditioning=True``): k ~ Uniform{0, T_latent-1}, where T_latent + is computed from the actual video length using the VAE temporal compression factor +""" + +import random +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan + + +class SequencePlanAugmentor(Augmentor): + """Augmentor that creates SequencePlan with random conditional frames. + + Samples k conditioning frames and writes ``condition_frame_indexes_vision = list(range(k))`` + into the SequencePlan. Downstream packing code reads this field to set condition_mask. + + Args: + input_keys: List of input keys (not used, but required by Augmentor interface). + output_keys: List of output keys (not used, but required by Augmentor interface). + args: Dictionary containing: + - "conditioning_config" (dict[int, float], optional): Weighted distribution + mapping latent-frame counts to unnormalized probabilities. + Example: {0: 0.5, 4: 0.3, 8: 0.2}. Clamped to T_latent-1 at runtime. + - "uniform_conditioning" (bool, default False): When True, samples + k ~ Uniform{0, T_latent-1}. Takes precedence over conditioning_config when + both are set. At least one of uniform_conditioning or conditioning_config + must be provided. + - "temporal_compression_factor" (int, default 4): VAE temporal compression + factor used to convert pixel frame count N to T_latent = 1 + (N-1) // tcf. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + if args is None: + args = {} + + self.conditioning_config = args.get("conditioning_config") + self.uniform_conditioning = args.get("uniform_conditioning", False) + self.temporal_compression_factor = args.get("temporal_compression_factor", 4) + + if self.conditioning_config is None and not self.uniform_conditioning: + raise ValueError("args must provide 'conditioning_config' or set 'uniform_conditioning=True'") + + # Validate and normalize probabilities + if self.conditioning_config is not None: + # Validate keys are non-negative integers + for num_frames, prob in self.conditioning_config.items(): + if not isinstance(num_frames, int) or num_frames < 0: + raise ValueError(f"conditioning_config keys must be non-negative integers, got {num_frames}") + if not isinstance(prob, (int, float)) or prob < 0: + raise ValueError(f"conditioning_config values must be non-negative numbers, got {prob}") + + # Normalize probabilities to sum to 1.0 + total_prob = sum(self.conditioning_config.values()) + if total_prob <= 0: + raise ValueError("conditioning_config probabilities must sum to a positive number") + + self.normalized_config = {k: v / total_prob for k, v in self.conditioning_config.items()} + else: + self.normalized_config = {0: 1.0} + + def __call__(self, data_dict: dict) -> dict: + """Create a SequencePlan with random conditional frames. + + Args: + data_dict: Input data dictionary. Should contain "video" key to determine + the number of frames available. + + Returns: + data_dict: Output dictionary with "sequence_plan" key added. + """ + # Get video to determine available frames + video = data_dict.get("video") + if video is None or (self.conditioning_config is None and not self.uniform_conditioning): + # This is an image batch + sequence_plan = SequencePlan( + has_text=True, # Has text prompt! + has_vision=True, + condition_frame_indexes_vision=[], # No conditioning frames! + ) + data_dict["sequence_plan"] = sequence_plan + return data_dict + + # Determine number of frames + # Video should be a tensor with shape (C, T, H, W) by this point in the pipeline + if isinstance(video, torch.Tensor): + assert video.ndim == 4, "video should be a tensor with shape (C, T, H, W)" + num_frames = video.shape[1] + else: + # If video is not a tensor or dict, we can't determine the exact number + # Use a conservative approach - will be limited by max available frames + num_frames = None + + T_latent = 1 + (num_frames - 1) // self.temporal_compression_factor if num_frames is not None else 1 + + # Sample number of conditional frames + if self.uniform_conditioning: + num_conditional_frames = random.randint(0, max(0, T_latent - 1)) + else: + frames_options = list(self.normalized_config.keys()) + weights = list(self.normalized_config.values()) + num_conditional_frames = random.choices(frames_options, weights=weights, k=1)[0] + num_conditional_frames = min(num_conditional_frames, T_latent - 1) if num_frames is not None else 0 + + # Create condition_frame_indexes_vision list + # Conditional frames are always the first N frames + condition_frame_indexes_vision = list(range(num_conditional_frames)) + + # Create SequencePlan + sequence_plan = SequencePlan( + has_text=True, + has_vision=True, + condition_frame_indexes_vision=condition_frame_indexes_vision, + ) + + # Add sequence plan to data dict + data_dict["sequence_plan"] = sequence_plan + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py b/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py new file mode 100644 index 00000000..14c98260 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Augmentor that builds a SequencePlan for sound-enabled training. + +This augmentor creates a SequencePlan based on the presence of sound data +in the sample, following the same pattern as Action's ActionTransformPipeline +which builds sequence plans for action-enabled training. + +Placed at the END of the augmentor pipeline (after video/audio extraction +and text transforms) so that all data shapes are known. +""" + +from typing import Optional + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.vfm.datasets.sound_data_utils import VALID_SOUND_MODES, build_sequence_plan_for_sound + + +class SoundSequencePlanBuilder(Augmentor): + """Builds a SequencePlan for sound-enabled samples. + + Inspects the data dict for sound data and creates an appropriate + SequencePlan. If no sound is present, creates a video-only plan. + + Args: + input_keys: Not used (reads from data_dict directly) + output_keys: Not used + args: Dictionary with: + - mode: Generation mode ("t2vs", "tv2s", "ts2v", "ti2sv"). Default: "t2vs" + - video_key: Key to find video data. Default: "video" + - sound_key: Key to find sound data. Default: "sound" + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.mode = args.get("mode", "t2vs") + self.video_key = args.get("video_key", "video") + self.sound_key = args.get("sound_key", "sound") + + assert self.mode in VALID_SOUND_MODES, f"Invalid mode: {self.mode}. Must be one of {VALID_SOUND_MODES}" + + def __call__(self, data_dict: dict) -> dict | None: + """Add sound fields to the existing SequencePlan. + + Only modifies ``has_sound`` and ``condition_frame_indexes_sound``. + All other fields (vision conditioning, action conditioning, etc.) set + by upstream augmentors are preserved. + + If no upstream plan exists, creates a minimal one with sensible defaults. + """ + video = data_dict.get(self.video_key) + sound = data_dict.get(self.sound_key) + + if video is None: + return None # Can't proceed without video + + if not hasattr(video, "shape"): + return None + + video_length = video.shape[1] # (C, T, H, W) → T + + existing_plan = data_dict.get("sequence_plan") + + if existing_plan is not None: + # Update only the sound fields on the existing plan + if sound is not None and hasattr(sound, "shape"): + sound_plan = build_sequence_plan_for_sound( + mode=self.mode, + video_latent_length=video_length, + sound_latent_length=0, + ) + existing_plan.has_sound = sound_plan.has_sound + existing_plan.condition_frame_indexes_sound = sound_plan.condition_frame_indexes_sound + else: + existing_plan.has_sound = False + existing_plan.condition_frame_indexes_sound = [] + else: + # No upstream plan — build a complete one from scratch + if sound is not None and hasattr(sound, "shape"): + data_dict["sequence_plan"] = build_sequence_plan_for_sound( + mode=self.mode, + video_latent_length=video_length, + sound_latent_length=0, + ) + else: + from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan + + data_dict["sequence_plan"] = SequencePlan( + has_text=True, + has_vision=True, + has_sound=False, + ) + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py b/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py new file mode 100644 index 00000000..d1e3aac8 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Augmentor for tokenizing input text + +import json +import random +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.lazy_config import instantiate as lazy_instantiate +from cosmos3._src.vfm.datasets.sequence_packing import add_special_tokens +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import tokenize_caption + +_MAX_NUM_TOKENS = 4096 + + +class TextTokenizerTransform(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + tokenizer_config = self.args["tokenizer_config"] + self.cfg_dropout_rate = self.args["cfg_dropout_rate"] + self.use_system_prompt = self.args.get("use_system_prompt", False) + + # Initialize the text tokenizer + vlm_tokenizer = lazy_instantiate(tokenizer_config) + vlm_tokenizer, _ = add_special_tokens(vlm_tokenizer) + self.vlm_tokenizer = vlm_tokenizer + + def __call__(self, data_dict: dict) -> dict: + input_caption = data_dict[self.input_keys[0]] + + if isinstance(input_caption, dict): + # Encode dict into a json string. This json string is then passed to the transformer tokenizer. + input_caption = json.dumps(input_caption) + data_dict[self.input_keys[0]] = input_caption + + if self.cfg_dropout_rate > 0: + # If CFG is used, randomly dropout the input caption + # We dropout the input caption by replacing it with an empty string + if random.random() < self.cfg_dropout_rate: + input_caption = "" + data_dict[self.input_keys[0]] = input_caption + text_ids = tokenize_caption( + input_caption, + self.vlm_tokenizer, + is_video=False, + use_system_prompt=self.use_system_prompt, + ) + text_ids = text_ids[:_MAX_NUM_TOKENS] # truncate the text ids to the maximum number of tokens + # This will take care of wierd edge cases where we generate extremely long captions + data_dict[self.output_keys[0]] = torch.tensor(text_ids) # [N_tokens] + return data_dict + + +_SYSTEM_PROMPT_IMAGE_EDITING = "You are a helpful assistant who will edit images based on the user's instructions." + +_SYSTEM_PROMPT_TRANSFER = "You are a helpful assistant that generates images or videos following the user's instructions and control signals (edge maps, blur, depth, or segmentation)." + +_SYSTEM_PROMPTS = { + "editing": _SYSTEM_PROMPT_IMAGE_EDITING, + "transfer": _SYSTEM_PROMPT_TRANSFER, +} + + +def _tokenize_caption_with_system_prompt(caption: str, tokenizer: object, system_prompt: str) -> list[int]: + """Tokenize a caption with a task-specific system prompt (editing or transfer). + + Args: + caption: The instruction or caption text. + tokenizer: The Qwen2 tokenizer (must support ``apply_chat_template``). + system_prompt: System prompt for the conversation (e.g. editing or transfer). + + Returns: + List of token IDs. + """ + conversations = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": caption}, + ] + tokenizer_output = tokenizer.apply_chat_template( + conversations, + tokenize=True, + add_generation_prompt=True, + add_vision_id=False, + ) + return tokenizer_output + + +class TextTokenizerTransformForEditing(Augmentor): + """Tokenizer augmentor for interleaved tasks: image editing or transfer (control-conditioned generation). + + Uses a task-specific system prompt. Pass args["task"] = "editing" (default) or "transfer". + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + tokenizer_config = self.args["tokenizer_config"] + self.cfg_dropout_rate = self.args.get("cfg_dropout_rate", 0.0) + task = self.args.get("task", "editing") + self._system_prompt = _SYSTEM_PROMPTS.get(task, _SYSTEM_PROMPTS["editing"]) + + vlm_tokenizer = lazy_instantiate(tokenizer_config) + vlm_tokenizer, _ = add_special_tokens(vlm_tokenizer) + self.vlm_tokenizer = vlm_tokenizer + + def __call__(self, data_dict: dict) -> dict | None: + input_caption = data_dict.get(self.input_keys[0], "") + if self.cfg_dropout_rate > 0 and random.random() < self.cfg_dropout_rate: + input_caption = "" + data_dict[self.input_keys[0]] = input_caption + text_ids = _tokenize_caption_with_system_prompt(input_caption, self.vlm_tokenizer, self._system_prompt) + data_dict[self.output_keys[0]] = torch.tensor(text_ids) # [N_tokens] + return data_dict + + +class TextTokenizerTransformForTransfer(TextTokenizerTransformForEditing): + """Tokenizer augmentor for transfer (control-conditioned) generation. Uses transfer system prompt.""" + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + args = dict(args) if args else {} + args["task"] = "transfer" + super().__init__(input_keys, output_keys, args) diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py b/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py new file mode 100644 index 00000000..452e646d --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py @@ -0,0 +1,295 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import random +from typing import Optional + +from cosmos3._src.imaginaire.datasets.augmentors.v3_text_transforms import pad_and_resize +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.data_sources.data_registration import _CAPTION_EMBEDDING_KEY_MAPPING_IMAGES + +# For the qwen captions, we have 3 variants: short, medium, long +# In addition, for synthetic data, we create prompt embeddings as well. +# There is quite a bit of entropy in the way prompt data is saved. +# Captions are saved as "prompts", while the corresponding embeddings are saved as "original_prompt" +# This part will be cleaned after synthetic data is cleaned to be in the same format as real data. +_AVAILABLE_QWEN_CAPTIONS = ["qwen2p5_7b_short", "qwen2p5_7b_medium", "qwen2p5_7b_long"] +_AVAILABLE_QWEN3_30B_A3B_CAPTIONS = [ + "qwen3_30b_a3b_short", + "qwen3_30b_a3b_descriptive", + "qwen3_30b_a3b_dense", +] +# used for new caption in Nov 2025 +_AVAILABLE_CAPTIONS_V2 = ["caption_short", "caption_medium", "caption_long"] +# used for sft v1 +_AVAILABLE_CAPTIONS_SFT_V1 = [ + "gemini_v1_dense", + "gemini_v2_dense", + "qwen3vl_30B_v1_dense", + "qwen3vl_30B_v2_dense", + "qwen3vl_235B_v1_dense", + "qwen3vl_235B_v2_dense", +] +# used for genplan ablation +# captions are saved as "caption_long" as a JSON string, like {"dense": "xxx", "dense_bbox": "xxx"} +_AVAILABLE_CAPTIONS_GENPLAN = ["dense", "dense_bbox"] +_CAPTION_EMBEDDING_MAPPING = { + "qwen2p5_7b_short": "qwen2p5_7b_short", + "qwen2p5_7b_medium": "qwen2p5_7b_medium", + "qwen2p5_7b_long": "qwen2p5_7b_long", + "prompts": "original_prompt", +} + + +class TextTransformForImage(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs camera transformation. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with camera attributes added + """ + + caption_type = self.args["caption_type"] + embedding_key_in_dict = _CAPTION_EMBEDDING_KEY_MAPPING_IMAGES[caption_type] + embedding_type = self.args["embedding_type"] + embedding_input_key_prefix = "" if embedding_type == "t5_xxl" else "umt5_" + + captions_key, embeddings_key = ( + f"captions_{caption_type}", + f"{embedding_input_key_prefix}embeddings_captions_{embedding_key_in_dict}", + ) + decoded_captions_ai = data_dict[captions_key] + decoded_embeddings_ai = data_dict[embeddings_key] + + try: + # Hotfix: Some captions are labeled as "captions" and some are labeled as "caption" + # This issue needs to be fixed in the synthetic data. This is a hack and will be removed + # once the data is cleaned. + caption_key = "captions" if "captions" in decoded_captions_ai else "caption" + embedding_key = "t5_xxl_fp8" if embedding_type == "t5_xxl" else "umt5_xxl" + if caption_type == "qwen2p5_7b_v4": + selected_caption_type = random.choice(_AVAILABLE_QWEN_CAPTIONS) + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + t5_embedding = decoded_embeddings_ai[selected_caption_type]["embeddings"][embedding_key] + data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "prompts": + data_dict["ai_caption"] = decoded_captions_ai["caption"]["prompt"] + t5_embedding = decoded_embeddings_ai[_CAPTION_EMBEDDING_MAPPING[caption_type]]["embeddings"][ + embedding_key + ] + data_dict["selected_caption_type"] = caption_type + else: + assert caption_type == "ai_v3p1", f"Caption type {caption_type} not supported" + if decoded_captions_ai["had_parse_issue"]: + data_dict["ai_caption"] = decoded_captions_ai["captions"]["kosmos_2"] + t5_embedding = decoded_embeddings_ai["kosmos2"]["embeddings"][embedding_key] + else: + data_dict["ai_caption"] = decoded_captions_ai["captions"]["vfc"] + t5_embedding = decoded_embeddings_ai["vfc_fidelity"]["embeddings"][embedding_key] + + out_t5, out_t5_mask = pad_and_resize( + t5_embedding, + self.args["t5_tokens"]["num"], + is_mask_all_ones=self.args["is_mask_all_ones"], + ) + data_dict["t5_text_embeddings"] = out_t5 + data_dict["t5_text_mask"] = out_t5_mask + except Exception as e: + log.warning( + f"TextTransform dataloader error: {data_dict['__url__']}, {data_dict['__key__']}\n error {e}", + rank0_only=False, + ) + return None + + del data_dict[captions_key] + del data_dict[embeddings_key] + + return data_dict + + +class TextTransformForImageWithoutEmbeddings(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.caption_prefix = args.get("caption_prefix", None) if args else None + + def _apply_caption_prefix(self, data_dict: dict) -> None: + """Prepend caption_prefix to ai_caption if configured.""" + if not self.caption_prefix or not isinstance(data_dict.get("ai_caption"), str): + return + original = data_dict["ai_caption"] + data_dict["ai_caption"] = self.caption_prefix + " " + original.lstrip() + log.debug( + f"[caption_prefix] before: {original[:120]!r}... | after: {data_dict['ai_caption'][:120]!r}...", + rank0_only=False, + ) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs text transform without any embedding loading. + This is useful for online computation. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with camera attributes added + """ + + caption_type = self.args["caption_type"] + captions_key = f"captions_{caption_type}" + decoded_captions_ai = data_dict[captions_key] + + train_on_captions = self.args.get("train_on_captions", []) + # if [], will infer based on the caption json + # otherwise it will only use the captions in the list + + try: + # Hotfix: Some captions are labeled as "captions" and some are labeled as "caption" + # This issue needs to be fixed in the synthetic data. This is a hack and will be removed + # once the data is cleaned. + caption_key = "captions" if "captions" in decoded_captions_ai else "caption" + if len(train_on_captions) == 0: + # infer which caption types are there + if caption_type in ("generated_gpt_oss_20b", "generated_gpt_oss_120b"): + selected_caption_type = "caption_long" + if caption_key in decoded_captions_ai: # sharded with sila pipeline + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + else: + data_dict["ai_caption"] = decoded_captions_ai[selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "qwen3_30b_a3b": + selected_caption_type = random.choice(_AVAILABLE_QWEN3_30B_A3B_CAPTIONS) + data_dict["ai_caption"] = decoded_captions_ai[selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif any( + caption_type in _AVAILABLE_QWEN_CAPTIONS for caption_type in decoded_captions_ai[caption_key].keys() + ): + # qwen2p5_7b_v4 captions + selected_caption_type = random.choice(_AVAILABLE_QWEN_CAPTIONS) + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "cosmos_captioner_v1p1": + selected_caption_type = "caption_cosmos_captioner_image" + if decoded_captions_ai[caption_key].get(selected_caption_type, "") == "": + # xingqianx: a temporary skip as some data is imperfect. + return None # type: ignore + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "cosmos_captioner_v1p1_structured_json": + # this is made by mistake, should be removed in future. + # it is used for cosmos_lab_image_v1_human_sft. Once we fix it, this should be removed. + selected_caption_type = "caption_cosmos_captioner_image_structured_json" + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif any( + caption_type in _AVAILABLE_CAPTIONS_V2 for caption_type in decoded_captions_ai[caption_key].keys() + ): + # v2 captions + selected_caption_type = random.choice(_AVAILABLE_CAPTIONS_V2) + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "prompts": + data_dict["ai_caption"] = decoded_captions_ai["caption"]["prompt"] + data_dict["selected_caption_type"] = caption_type + else: + assert caption_type == "ai_v3p1", f"Caption type {caption_type} not supported" + if decoded_captions_ai["had_parse_issue"]: + data_dict["ai_caption"] = decoded_captions_ai["captions"]["kosmos_2"] + else: + data_dict["ai_caption"] = decoded_captions_ai["captions"]["vfc"] + else: # use the designated captions + # Validate that all specified caption types exist (except genplan types which are nested) + for cap_type in train_on_captions: + if cap_type not in _AVAILABLE_CAPTIONS_GENPLAN: + assert cap_type in decoded_captions_ai[caption_key].keys(), ( + f"Caption type {cap_type} not found in data" + ) + + selected_caption_type = random.choice(train_on_captions) + + if selected_caption_type in _AVAILABLE_CAPTIONS_GENPLAN: + # Genplan captions are nested inside caption_long as a JSON string + caption_long_data = json.loads(decoded_captions_ai[caption_key]["caption_long"]) + data_dict["ai_caption"] = caption_long_data[selected_caption_type] + else: + data_dict["ai_caption"] = decoded_captions_ai[caption_key][selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type + + except Exception as e: + log.warning( + f"TextTransform dataloader error: {data_dict['__url__']}, {data_dict['__key__']}\n error {e}", + rank0_only=False, + ) + return None + + del data_dict[captions_key] + + self._apply_caption_prefix(data_dict) + return data_dict + + +class TextTransformForImageJsonCaption(Augmentor): + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + self.json_field_dropout_rate = args.get("json_field_dropout_rate", 0.0) if args else 0.0 + + def __call__(self, data_dict: dict) -> dict: + r"""Performs text transform without any embedding loading. + This is useful for online computation. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with camera attributes added + """ + + caption_type = self.args["caption_type"] + captions_key = f"captions_{caption_type}" + + if "cosmos_captioner_v1p1_structured_json" in data_dict[captions_key]: + # this is made by mistake, should be removed in future. + # it is used for cosmos_lab_image_v1_human_sft. Once we fix it, this should be removed. + selected_caption_type = "caption_cosmos_captioner_image_structured_json" + else: + selected_caption_type = "caption_cosmos_captioner_image" + caption_json = data_dict[captions_key]["caption"].get(selected_caption_type, "") + if caption_json == "": + # xingqianx: a temporary skip as some text data is imperfect. + return None # type: ignore + caption_json = json.loads(caption_json) + + # In some erraneous cases, the caption_json is a list + if isinstance(caption_json, list): + caption_json = caption_json[0] + + assert isinstance(caption_json, dict), ( + f"Caption json is not a dict: {caption_json}, url: {data_dict['__url__']}, key: {data_dict['__key__']}" + ) + + # Randomly dropout json keys during training + if self.json_field_dropout_rate > 0: + for key in list(caption_json.keys()): + if random.random() < self.json_field_dropout_rate: + caption_json.pop(key) + + data_dict["ai_caption"] = caption_json + del data_dict[captions_key] + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py b/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py new file mode 100644 index 00000000..001b3c8a --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py @@ -0,0 +1,511 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import random +from typing import Optional + +from cosmos3._src.imaginaire.datasets.augmentors.v3_text_transforms import pad_and_resize +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log + + +class TextTransformForVideo(Augmentor): + def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + # our caption is saved in json with format: {"": "xxx", "": [{"start_frame": x, "end_frame": x, "": xxx}, ...], "": [{"start_frame":...]} + # our t5 embedding is saved in pickle with format: [{"": array1, "": array2}, ...] + self.captions_key: str = args[ + "captions_key" + ] # s3 folder that saves the captions; this get mapped to the key in data_dict to fetch the caption field + self.embeddings_key: Optional[str] = args[ + "embeddings_key" + ] # s3 folder that saves the embeddings; this get mapped to the key in data_dict to fetch the embedding field + self.caption_windows_key: str = args[ + "caption_windows_key" + ] # key to get the caption windows from the caption field + self.caption_type: str = args["caption_type"] # key of caption type to fetch the caption from caption windows + + self._load_embeddings = self.embeddings_key is not None + + if not self._load_embeddings: + # In this case, we don't load the embeddings + log.info("No embeddings key provided, we will not load embeddings") + self.embedding_caption_type = None + self.t5_tokens_num = None + self.is_mask_all_ones = None + self.embedding_style_mapping = None + else: + self.embedding_caption_type: str = args[ + "embedding_caption_type" + ] # key to get the embedding of a particular caption type from the embedding field + self.t5_tokens_num = args["t5_tokens"]["num"] # number of tokens we cap after padding + self.is_mask_all_ones = args["is_mask_all_ones"] # if true, set mask for t5 to all ones + + self.embedding_style_mapping = { + "long": self.embedding_caption_type, + "short": f"{self.embedding_caption_type}_short", + "medium": f"{self.embedding_caption_type}_medium", + "user": f"{self.embedding_caption_type}_user", + } + + self.caption_probs: dict[str, float] = args[ + "caption_probs" + ] # probabilities for user/short/medium/long captions + self.caption_style_mapping = { + "long": self.caption_type, + "short": f"{self.caption_type}_short", + "medium": f"{self.caption_type}_medium", + "user": f"{self.caption_type}_user", + } + assert self.caption_probs.keys() == self.caption_style_mapping.keys(), ( + "The keys for caption_probs, caption_style_mapping, and embedding_style_mapping should match" + ) + + if self._load_embeddings: + assert self.caption_style_mapping.keys() == self.embedding_style_mapping.keys(), ( + "The keys for caption_style_mapping and embedding_style_mapping should match" + ) + + def __call__(self, data_dict: dict) -> dict: + r"""Performs text transformation. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with captions and t5 embeddings added + """ + + try: + windows = data_dict[self.captions_key][self.caption_windows_key] + n_windows = len(windows) + chunk_index = data_dict["chunk_index"] + + if chunk_index == n_windows: + # This will only happen when the number of captions does not match number of chunks due to re-transcoding the videos. + log.warning( + f"TextTransform dataloader error: Found {data_dict['n_orig_video_frames']} in video but captioning is done with videos of {windows[-1]['end_frame']} frames. This mismatch is due to video re-transcoding.", + rank0_only=False, + ) + chunk_index -= 1 + + selected_caption_window = windows[chunk_index] + except Exception as e: + log.warning( + f"TextTransform dataloader error -- url: {data_dict['__url__']}, key: {data_dict['__key__']}, chunk_index: {data_dict['chunk_index']}\n error {e}", + rank0_only=False, + ) + return None + + sampled_caption_style = None + try: + available_caption_styles = [] + for k in selected_caption_window.keys(): + caption_style = k.replace(self.caption_type, "").replace("_", "") + if caption_style == "": # it is long caption by default + available_caption_styles.append("long") + elif caption_style in self.caption_style_mapping: + available_caption_styles.append(caption_style) + else: + assert caption_style in ["startframe", "endframe"], f"Unsupported caption_type {caption_style}" + + probabilities_for_available_caption_styles = { + k: v for k, v in self.caption_probs.items() if k in available_caption_styles + } + sampled_caption_style = random.choices( + list(probabilities_for_available_caption_styles), + weights=probabilities_for_available_caption_styles.values(), + )[0] + data_dict["ai_caption"] = selected_caption_window[self.caption_style_mapping[sampled_caption_style]] + except Exception as e: + log.warning( + f"TextTransform dataloader error -- url: {data_dict['__url__']}, key: {data_dict['__key__']}, selected_caption_window: {selected_caption_window}\n error {e}", + rank0_only=False, + ) + return None + if data_dict["ai_caption"] == "": + log.warning( + f"TextTransform dataloader error -- empty caption! url: {data_dict['__url__']}, key: {data_dict['__key__']}, selected_caption_window: {selected_caption_window}", + rank0_only=False, + ) + return None + + assert data_dict["ai_caption"] is not None and sampled_caption_style is not None + data_dict["sampled_caption_style"] = sampled_caption_style + + del data_dict[self.captions_key] # delete the field as we have extracted ai_caption from it + + if self._load_embeddings: + ai_caption_embedding_data = data_dict[self.embeddings_key] + try: + if self.embedding_caption_type == "vila_caption": + t5_embedding = ai_caption_embedding_data[chunk_index] + else: + t5_embedding = ai_caption_embedding_data[chunk_index][ + self.embedding_style_mapping[sampled_caption_style] + ] + except Exception as e: + log.warning( + f"TextTransform dataloader error -- url: {data_dict['__url__']}, key: {data_dict['__key__']}, chunk_index: {data_dict['chunk_index']}, n embeddings: {len(ai_caption_embedding_data)}, n captions: {n_windows} \n error {e}", + rank0_only=False, + ) + return None + out_t5, out_t5_mask = pad_and_resize( + t5_embedding, + self.t5_tokens_num, + is_mask_all_ones=self.is_mask_all_ones, + ) + data_dict["t5_text_embeddings"] = out_t5 + data_dict["t5_text_mask"] = out_t5_mask + del data_dict[self.embeddings_key] # delete the field as we have extracted t5 embedding from it + + return data_dict + + +class TextTransformForVideoWithFullFrames(Augmentor): + """ + Pair use with VideoParsingWithFullFrames to get the full frames of the video. + The caption is assumed to be for the entire video frames, rather than TextTransformForVideo + which assumes captions are for a specific chunk of frames. + + Audio captions are handled separately by AudioCaptionAppender, which appends + audio descriptions to the video caption after this augmentor runs. + """ + + def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) == 3, "TextTransformForVideoWithFullFrames augmentor only supports three input keys" + self.meta_key = input_keys[0] + self.video_key = input_keys[1] + self.sequence_plan_key = input_keys[2] + self.args = args + self.keep_metas = args.get("keep_metas", False) if args else False + self.caption_prefix = args.get("caption_prefix", None) if args else None + + def _apply_caption_prefix(self, data_dict: dict) -> None: + """Prepend caption_prefix to ai_caption if configured.""" + if not self.caption_prefix or not isinstance(data_dict.get("ai_caption"), str): + return + original = data_dict["ai_caption"] + data_dict["ai_caption"] = self.caption_prefix + " " + original.lstrip() + log.debug( + f"[caption_prefix] before: {original[:120]!r}... | after: {data_dict['ai_caption'][:120]!r}...", + rank0_only=False, + ) + + @staticmethod + def _resolve_multi_chunk_caption(raw_caption: str) -> str: + """Resolve a caption that may be in multi-chunk JSON format. + + Multi-chunk captions are JSON strings encoding a dict of chunks, e.g.: + {"chunk_0_300": {"caption": "...", "start_frame": 0, "end_frame": 300}, ...} + When detected, a chunk is randomly selected and its "caption" text returned. + Plain string captions are returned unchanged. + """ + if not isinstance(raw_caption, str): + return raw_caption + try: + parsed = json.loads(raw_caption) + except (json.JSONDecodeError, TypeError): + return raw_caption + if not isinstance(parsed, dict) or len(parsed) == 0: + return raw_caption + chunk = random.choice(list(parsed.values())) + if isinstance(chunk, dict) and "caption" in chunk: + return chunk["caption"] + return raw_caption + + def __call__(self, data_dict: dict) -> dict: + r"""Performs text transformation. + + Samples a video caption from metadata based on caption_config ratios. + Supports both plain-string captions and multi-chunk JSON captions + (randomly selects one chunk when multiple chunks are present). + Audio captions are handled separately by AudioCaptionAppender. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with captions and t5 embeddings added + """ + caption_config = self.args["caption_config"] + meta_dict = data_dict[self.meta_key] + + for caption_type in caption_config: + assert caption_type in meta_dict, ( + f"Caption type {caption_type} not found in meta_dict (keys = {meta_dict.keys()})" + ) + + # First check if we are doing image to world or video to world + if self.sequence_plan_key in data_dict: + sequence_plan = data_dict[self.sequence_plan_key] + conditioning_frame_indexes_vision = sequence_plan.condition_frame_indexes_vision + if len(conditioning_frame_indexes_vision) > 0: + sampled_caption = self._resolve_multi_chunk_caption(meta_dict["caption_temporal"]) + data_dict["ai_caption"] = sampled_caption + data_dict["sampled_caption_style"] = "caption_temporal" + + self._apply_caption_prefix(data_dict) + if not self.keep_metas: + del data_dict[self.meta_key] + return data_dict + + # Text-to-world: sample from short, medium, long captions + caption_keys = list(caption_config.keys()) + caption_ratios = [caption_config[k]["ratio"] for k in caption_keys] + sampled_caption_type = random.choices(caption_keys, weights=caption_ratios, k=1)[0] + data_dict["ai_caption"] = self._resolve_multi_chunk_caption(meta_dict[sampled_caption_type]) + data_dict["sampled_caption_style"] = sampled_caption_type + + self._apply_caption_prefix(data_dict) + + # Clean up - delete the caption fields that were sampled from + for caption_type in caption_config.keys(): + if caption_type in meta_dict: + del meta_dict[caption_type] + + # Delete metas unless keep_metas=True (set when AudioCaptionAppender runs downstream) + if not self.keep_metas: + del data_dict[self.meta_key] + + return data_dict + + +class TextTransformForVideoTransferFullFrames(Augmentor): + """Read structured captions for the full-frame transfer pipeline. + + Two-level lookup: + + 1. A caption-source key is sampled (with weights) from ``caption_config``. + This key identifies the WebDataset folder / metadata field whose value + is a dict of annotations (e.g. + ``"structured_captions_qwen3-vl-8b-lora-v1.5-merged"``). The sampled + value is looked up first in ``data_dict`` (top-level) and then in + ``meta_dict``. + 2. Inside that caption dict the field ``caption_structured`` is hardcoded + as the JSON-encoded chunked annotation, of the form + ``{"chunk_0_300": {"caption": "", + "start_frame": ..., "end_frame": ...}}``. + + The full-frame pipeline always decodes from the start of the video, so the + first chunk is always selected and its inner JSON-encoded structured payload + is parsed back into a dict before being serialized as ``ai_caption``. + """ + + CAPTION_FIELD = "caption_structured" + + def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) >= 1, "TextTransformForVideoTransferFullFrames requires a metadata input key" + self.meta_key = input_keys[0] + self.args = args or {} + self.caption_prefix = self.args.get("caption_prefix", None) + self.keep_metas = self.args.get("keep_metas", False) + self.caption_options = self._normalize_caption_config(self.args["caption_config"]) + + @staticmethod + def _normalize_caption_config(caption_config: dict | list) -> list[tuple[str, float]]: + if isinstance(caption_config, dict): + options: list[tuple[str, float]] = [] + for caption_key, config in caption_config.items(): + ratio = config.get("ratio", 1.0) if isinstance(config, dict) else float(config) + options.append((caption_key, float(ratio))) + return options + + options = [] + for item in caption_config: + if isinstance(item, str): + options.append((item, 1.0)) + elif isinstance(item, dict): + caption_key = item.get("key") or item.get("caption_key") or item.get("caption_type") or item.get("name") + if caption_key is None: + raise ValueError(f"Caption config entry is missing a caption key: {item}") + options.append((caption_key, float(item.get("ratio", 1.0)))) + else: + caption_key, ratio = item + options.append((caption_key, float(ratio))) + return options + + def _apply_caption_prefix(self, data_dict: dict) -> None: + if not self.caption_prefix or not isinstance(data_dict.get("ai_caption"), str): + return + data_dict["ai_caption"] = self.caption_prefix + " " + data_dict["ai_caption"].lstrip() + + def _lookup_caption_dict(self, data_dict: dict, meta_dict: dict | None, caption_key: str) -> dict | None: + candidate = data_dict.get(caption_key) + if candidate is None and isinstance(meta_dict, dict): + candidate = meta_dict.get(caption_key) + if isinstance(candidate, dict): + return candidate + return None + + def __call__(self, data_dict: dict) -> dict | None: + meta_dict = data_dict.get(self.meta_key) + + available_options: list[tuple[str, float]] = [] + for key, ratio in self.caption_options: + if ratio <= 0: + continue + if self._lookup_caption_dict(data_dict, meta_dict, key) is not None: + available_options.append((key, ratio)) + + if not available_options: + log.warning( + f"TextTransformForVideoTransferFullFrames: none of the configured caption keys " + f"{[key for key, _ in self.caption_options]} hold a caption dict in metadata/sample keys. " + f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + sampled_caption_key = random.choices( + [key for key, _ in available_options], + weights=[ratio for _, ratio in available_options], + k=1, + )[0] + caption_dict = self._lookup_caption_dict(data_dict, meta_dict, sampled_caption_key) + if caption_dict is None or self.CAPTION_FIELD not in caption_dict: + log.warning( + f"TextTransformForVideoTransferFullFrames: caption dict for {sampled_caption_key} is missing the " + f"hardcoded {self.CAPTION_FIELD} field. url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + try: + chunks = json.loads(caption_dict[self.CAPTION_FIELD]) + first_chunk = next(iter(chunks.values())) + structured = json.loads(first_chunk["caption"]) + except Exception as e: + log.warning( + f"TextTransformForVideoTransferFullFrames: failed to decode {sampled_caption_key}.{self.CAPTION_FIELD}. " + f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}, error: {e}", + rank0_only=False, + ) + return None + + data_dict["ai_caption"] = json.dumps(structured) + data_dict["sampled_caption_style"] = sampled_caption_key + self._apply_caption_prefix(data_dict) + + if not self.keep_metas: + data_dict.pop(self.meta_key, None) + for caption_key, _ in self.caption_options: + if caption_key in data_dict: + del data_dict[caption_key] + return data_dict + + +class TextTransformForVideoJsonCaption(Augmentor): + """ + This augmentor is used to transform the caption from a json string to a string. + The caption is assumed to be in the format of a json string. + The caption is then transformed to a string by converting the json string to a dictionary and then converting the dictionary to a string. + The caption is then returned as a string. + + When ``meta_dict["caption_audio"]`` is present and non-empty, its contents + are injected into the caption dict under the ``"audio_description"`` key. + This happens after the JSON field dropout so the audio description is + preserved whenever upstream metadata provides it. + """ + + def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) >= 2, ( + "TextTransformForVideoJsonCaption augmentor requires at least two input keys: [meta_key, video_key]" + ) + self.meta_key = input_keys[0] + self.video_key = input_keys[1] + self.args = args + self.keep_metas = args.get("keep_metas", False) if args else False + + def __call__(self, data_dict: dict) -> dict | None: + r"""Performs text transformation. + + Parses the per-chunk caption JSON, randomly samples one chunk, and writes + the chunk's frame range into ``data_dict`` so a downstream + ``VideoParsingChunkedFrames`` can decode only that frame range. When a + non-empty ``caption_audio`` field is present in the metadata, it is + injected into the caption dict under the ``"audio_description"`` key. + + Args: + data_dict (dict): Input data dict + Returns: + data_dict (dict): Output dict with captions and t5 embeddings added + """ + caption_config = self.args["caption_config"] + json_field_dropout_rate = caption_config["json_field_dropout_rate"] + + try: + meta_dict = data_dict[self.meta_key] + caption = json.loads(meta_dict["caption"]) + + # Contents of caption + # caption = { + # "chunk_0_300": { + # "caption": "...", + # "start_frame": 0, + # "end_frame": 300, + # }, + # "chunk_300_435": { + # "caption": "...", + # "start_frame": 300, + # "end_frame": 435, + # }, + # } + chunk_keys = list(caption.keys()) + if len(chunk_keys) == 0: + log.warning( + f"TextTransformForVideoJsonCaption: empty caption dict. url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + sampled_key = random.choice(chunk_keys) + sampled_chunk = caption[sampled_key] + + data_dict["chunk_index"] = chunk_keys.index(sampled_key) + data_dict["chunk_start_frame"] = int(sampled_chunk["start_frame"]) + data_dict["chunk_end_frame"] = int(sampled_chunk["end_frame"]) + + caption_json = json.loads(sampled_chunk["caption"]) + except Exception as e: + log.warning( + f"TextTransformForVideoJsonCaption dataloader error -- url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}\n error {e}", + rank0_only=False, + ) + return None + + # Randomly dropout json keys during training + if json_field_dropout_rate > 0: + for key in list(caption_json.keys()): + if random.random() < json_field_dropout_rate: + caption_json.pop(key) + + # Inject audio caption from metas as a new field when available. Added after the field + # dropout above so it is preserved whenever upstream metadata provides it. + audio_caption = meta_dict.get("caption_audio") + if isinstance(audio_caption, str) and len(audio_caption) > 0: + caption_json["audio_description"] = audio_caption + + data_dict["ai_caption"] = caption_json + + # Delete metas unless keep_metas=True (set when downstream augmentors still need them, + # e.g. VideoParsingChunkedFrames needs framerate/width/height/nb_frames). + if not self.keep_metas: + del data_dict[self.meta_key] + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py new file mode 100644 index 00000000..2cbc0d5d --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transfer control input augmentors (edge, blur, depth, seg) for cosmos3 VFM; copied from transfer2 to avoid cosmos dependency.""" + +from cosmos3._src.vfm.datasets.augmentors.transfer_control_input.control_input import AddControlInputComb + +__all__ = ["AddControlInputComb"] diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py new file mode 100644 index 00000000..feb81552 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py @@ -0,0 +1,279 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Callable, Optional + +import attrs +import cv2 +import numpy as np +import torch + +from cosmos3._src.vfm.datasets.augmentors.transfer_control_input.fast_blur import BilateralGaussian + + +@attrs.define +class BilateralFilterConfig: + """Configuration for Bilateral filter""" + + use_random: bool = False + # if use_random is False, then optionally define the param values + d: int = 30 + sigma_color: int = 150 + sigma_space: int = 100 + iter: int = 1 + + # if use_random is True, then optionally define the range + d_min: int = 15 + d_max: int = 50 + sigma_color_min: int = 100 + sigma_color_max: int = 300 + sigma_space_min: int = 50 + sigma_space_max: int = 150 + iter_min: int = 1 + iter_max: int = 2 + + # Whether to use GPU kernel (inference only) + use_cuda: bool = False + + +# Blur config default values are tuned for this resolution (longest side). +REFERENCE_RESOLUTION = 720 + + +def _scale_for_resolution(value: float, longest_side: int) -> float: + """Scale a blur parameter from REFERENCE_RESOLUTION to the given longest frame side.""" + if longest_side <= 0: + return value + return value * (longest_side / REFERENCE_RESOLUTION) + + +def _scale_ksize(ksize: int, longest_side: int) -> int: + """Scale kernel size for resolution; result is odd and >= 1.""" + scaled = max(1, int(round(_scale_for_resolution(float(ksize), longest_side)))) + return scaled + 1 if scaled % 2 == 0 else scaled + + +@attrs.define +class GaussianBlurConfig: + """Configuration for Gaussian blur""" + + use_random: bool = False + # if use_random is False, then optionally define the param values + ksize: int = 25 + sigmaX: float = 12.5 + + # if use_random is True, then optionally define the range + ksize_min: int = 21 + ksize_max: int = 29 + sigmaX_min: float = 10.5 + sigmaX_max: float = 14.5 + + +def apply_bilateral_filter( + frames: np.ndarray, + d: int = 9, + sigma_color: float = 75, + sigma_space: float = 75, + iter: int = 1, + bilateral_cuda_module: Optional[Callable] = None, +) -> np.ndarray: + if bilateral_cuda_module is not None: + blurred_image = [] + frames_tensor = torch.from_numpy(frames).cuda() + for _image in frames_tensor.permute(1, 2, 3, 0): + blurred_image.append(bilateral_cuda_module(_image, d // 3, (sigma_color // 2) ** 2, sigma_space**2)) + blurred_image = torch.stack(blurred_image).permute(3, 0, 1, 2) + return blurred_image.cpu().numpy() + + C, T, H, W = frames.shape + blurred_frames = np.empty_like(frames) + + for t in range(T): + frame = np.ascontiguousarray(frames[:, t].transpose(1, 2, 0)) + for _ in range(iter): + frame = cv2.bilateralFilter(frame, d, sigma_color, sigma_space) + if len(frame.shape) == 2: + frame = frame[..., None] + + blurred_frames[:, t] = frame.transpose(2, 0, 1) + + return blurred_frames + + +def _longest_frame_side(frames: np.ndarray) -> int: + """Return the longest spatial dimension (H or W) for CTHW frames.""" + # frames: (C, T, H, W) + return int(max(frames.shape[2], frames.shape[3])) + + +class BilateralFilter: + def __init__(self, config: BilateralFilterConfig) -> None: + self.use_random = config.use_random + self.config = config + assert not (self.use_random and self.config.use_cuda), "Cannot use GPU kernel for training." + self.bilateral_cuda_module = BilateralGaussian() if self.config.use_cuda else None + + def __call__(self, frames: np.ndarray) -> np.ndarray: + config = self.config + longest = _longest_frame_side(frames) + if self.use_random: + d = np.random.randint(config.d_min, config.d_max) + sigma_color = np.random.randint(config.sigma_color_min, config.sigma_color_max) + sigma_space = np.random.randint(config.sigma_space_min, config.sigma_space_max) + iter = np.random.randint(config.iter_min, config.iter_max) + else: + d = config.d + sigma_color = config.sigma_color + sigma_space = config.sigma_space + iter = config.iter + # Scale from reference resolution (720) to current frame size + d = max(1, int(round(_scale_for_resolution(float(d), longest)))) + d = d + 1 if d % 2 == 0 else d # cv2.bilateralFilter requires odd d + sigma_color = max(1.0, _scale_for_resolution(float(sigma_color), longest)) + sigma_space = max(1.0, _scale_for_resolution(float(sigma_space), longest)) + return apply_bilateral_filter(frames, d, sigma_color, sigma_space, iter, self.bilateral_cuda_module) + + +def apply_gaussian_blur(frames: np.ndarray, ksize: int = 5, sigmaX: float = 1.0) -> np.ndarray: + if ksize % 2 == 0: + ksize += 1 # ksize must be odd + + _, T, _, _ = frames.shape + blurred_frames = np.empty_like(frames) + + for t in range(T): + frame = np.ascontiguousarray(frames[:, t].transpose(1, 2, 0)) + frame = cv2.GaussianBlur(frame, (ksize, ksize), sigmaX=sigmaX) + if len(frame.shape) == 2: + frame = frame[..., None] + blurred_frames[:, t] = frame.transpose(2, 0, 1) + + return blurred_frames + + +class GaussianBlur: + def __init__(self, config: GaussianBlurConfig) -> None: + self.use_random = config.use_random + self.config = config + + def __call__(self, frames: np.ndarray) -> np.ndarray: + longest = _longest_frame_side(frames) + if self.use_random: + ksize = np.random.randint(self.config.ksize_min, self.config.ksize_max + 1) + sigmaX = np.random.uniform(self.config.sigmaX_min, self.config.sigmaX_max) + else: + ksize = self.config.ksize + sigmaX = self.config.sigmaX + ksize = _scale_ksize(int(ksize), longest) + sigmaX = max(0.1, _scale_for_resolution(float(sigmaX), longest)) + return apply_gaussian_blur(frames, ksize, sigmaX) + + +@attrs.define +class BlurCombinationConfig: + """Configuration for a combination of blurs with associated probability""" + + # list of choices are: ["gaussian", "bilateral"] + # the corresponding config must be defined for each item in this blur_types list + blur_types: list[str] + probability: float + gaussian_blur: GaussianBlurConfig | None = None + bilateral_filter: BilateralFilterConfig | None = None + + +@attrs.define +class BlurConfig: + """Configuration for blur augmentation with multiple combinations""" + + # probabilities from the list of combinations should add up to 1.0 + blur_combinations: list[BlurCombinationConfig] = [] + + +# For training +random_blur_config = BlurConfig( + blur_combinations=[ + BlurCombinationConfig( + blur_types=["bilateral"], + probability=0.3, + bilateral_filter=BilateralFilterConfig(use_random=True), + ), + BlurCombinationConfig( + blur_types=["gaussian"], + probability=0.5, + gaussian_blur=GaussianBlurConfig(use_random=True), + ), + BlurCombinationConfig( + blur_types=["bilateral", "gaussian"], + probability=0.2, + bilateral_filter=BilateralFilterConfig(use_random=True), + gaussian_blur=GaussianBlurConfig(use_random=True), + ), + ], +) + +# For inference +bilateral_blur_config = BlurConfig( + blur_combinations=[ + BlurCombinationConfig( + blur_types=["bilateral"], + probability=1.0, + bilateral_filter=BilateralFilterConfig(use_random=False), + ), + ], +) + + +class Blur: + def __init__(self, config: BlurConfig | None = None, use_random: bool = True) -> None: + if config is None: + config = random_blur_config if use_random else bilateral_blur_config + probabilities = [combo.probability for combo in config.blur_combinations] + total_prob = sum(probabilities) + assert abs(total_prob - 1.0) < 1e-6, f"Probabilities must sum to 1.0, got {total_prob}" + + self.blur_combinations = config.blur_combinations + self.probabilities = probabilities + self._set_blur_instances() + + def _set_blur_instances(self) -> None: + if not self.blur_combinations: + return + self.blur_combinations_instances = [] + + for blur_combination in self.blur_combinations: + blur_mapping = { + "gaussian": (GaussianBlur, blur_combination.gaussian_blur), + "bilateral": (BilateralFilter, blur_combination.bilateral_filter), + } + + cur_instances = [] + for blur_type in blur_combination.blur_types: + assert blur_type in blur_mapping, f"Unknown {blur_type}. Needs to correct blur_type or blur_mapping." + + blur_class, blur_config = blur_mapping[blur_type] + cur_instances.append(blur_class(blur_config)) + + self.blur_combinations_instances.append(cur_instances) + + assert len(self.blur_combinations_instances) == len(self.blur_combinations), ( + "Number of blur_combinations_instances needs to match number of blur_combinations." + ) + + def __call__(self, frames: np.ndarray) -> np.ndarray: + blur_instances = random.choices(self.blur_combinations_instances, weights=self.probabilities, k=1)[0] + for ins in blur_instances: + frames = ins(frames) + return frames diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py new file mode 100644 index 00000000..08d1a3dd --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py @@ -0,0 +1,672 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Optional, Union + +import cv2 +import numpy as np +import torch +import torchvision.transforms.functional as transforms_F +from pycocotools import mask as mask_utils + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.augmentors.transfer_control_input.blur import Blur, BlurConfig +from cosmos3._src.vfm.datasets.augmentors.transfer_control_input.seg import ( + decode_partial_rle_width1, + segmentation_color_mask, +) + +# Constants for segmentation color processing +# These parameters control the color-based mask extraction process in AddControlInputSeg + +# Color quantization bin size for grouping similar colors together +# Range: 1-100 (smaller values = more granular color detection, larger values = more color grouping) +# Typical range: 10-50, where 25 provides good balance between precision and grouping +_BIN_SIZE = 25 + +# Maximum number of unique colors to examine for mask generation (to limit computation time) +# Range: 10-500 (smaller values = faster processing, larger values = more thorough color search) +# Typical range: 50-200, where 100 balances thoroughness with performance +_MAX_UNIQUE_COLORS = 100 + +# Color distance tolerance for considering pixels as the same color +# Range: 1-100 (smaller values = stricter color matching, larger values = more lenient matching) +# Typical range: 10-60, where 30 provides good tolerance for natural color variations +_COLOR_TOLERANCE = 30 + +# RGB value threshold below which a color is considered "black" and filtered out +# Range: 0-100 (smaller values = stricter black detection, larger values = more colors considered black) +# Typical range: 20-80, where 50 effectively filters out dark/black regions +_BLACK_THRESHOLD = 50 + + +def _maybe_torch_to_numpy(frames: Union[torch.Tensor, list]) -> np.ndarray: + try: + return frames.numpy() + except AttributeError: + return np.array(frames) + + +class AddControlInputEdge(Augmentor): + """ + Add control input to the data dictionary. control input are expanded to 3-channels + steps to add new items: modify this file, configs/conditioner.py, conditioner.py + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = ["control_input_edge"], + args: Optional[dict] = None, + use_random: Optional[bool] = True, + preset_strength: Optional[str] = "medium", + edge_t_lower: Optional[int] = None, + edge_t_upper: Optional[int] = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + self.use_random = use_random + self.preset_strength = preset_strength + self.t_lower = edge_t_lower + self.t_upper = edge_t_upper + + def __call__(self, data_dict: dict) -> dict: + if "control_input_edge" in data_dict: + return data_dict + key_img = self.input_keys[0] + key_out = self.output_keys[0] + frames = data_dict[key_img] + # log.info(f"Adding control input edge. Input key: {key_img}, Output key: {key_out}. Use random: {self.use_random}, Preset strength: {self.preset_strength}") + # Get lower and upper threshold for canny edge detection. + if self.use_random: # always on for training, always off for inference + if self.t_lower is not None and self.t_upper is not None: + # Use provided t_lower and t_upper values + t_lower = self.t_lower + t_upper = self.t_upper + else: + # Generate random values as before + t_lower = np.random.randint(20, 100) # Get a random lower thre + t_diff = np.random.randint(50, 150) # Get a random diff between lower and upper + t_upper = t_lower + t_diff # The upper thre is lower added by the diff + else: + if self.preset_strength == "none" or self.preset_strength == "very_low": + t_lower, t_upper = 20, 50 + elif self.preset_strength == "low": + t_lower, t_upper = 50, 100 + elif self.preset_strength == "medium": + t_lower, t_upper = 100, 200 + elif self.preset_strength == "high": + t_lower, t_upper = 200, 300 + elif self.preset_strength == "very_high": + t_lower, t_upper = 300, 400 + else: + raise ValueError(f"Preset {self.preset_strength} not recognized.") + + frames = _maybe_torch_to_numpy(frames).astype(np.uint8) + is_image = len(frames.shape) < 4 + + # Compute the canny edge map by the two thresholds. + if is_image: + edge_maps = cv2.Canny(frames, t_lower, t_upper)[None, None] + else: + edge_maps = [ + cv2.Canny(img, t_lower, t_upper) for img in frames.transpose((1, 2, 3, 0)) + ] # (C, T, H, W) -> (T, H, W) + edge_maps = np.stack(edge_maps)[None] + edge_maps = torch.from_numpy(edge_maps).expand(3, -1, -1, -1) # [3,T,H,W] + if is_image: + edge_maps = edge_maps[:, 0] + data_dict[key_out] = edge_maps + return data_dict + + +class AddControlInputBlur(Augmentor): + """ + Main class for adding blurred input to the data dictionary. + self.output_keys[0] indicates the types of blur added to the input. + For example, control_input_gaussian_guided indicates that both Gaussian and Guided filters are applied + """ + + def __init__( + self, + input_keys: list, # [key_load, key_img] + output_keys: Optional[list] = ["control_input_blur"], + args: Optional[dict] = None, # not used + use_random: bool = True, # whether to use random parameters + blur_config: BlurConfig | None = None, + downup_preset: str | int = "medium", # preset strength for downup factor + min_downup_factor: int = 4, # minimum downup factor + max_downup_factor: int = 16, # maximum downup factor + downsize_before_blur: bool = True, # whether to downsize before applying blur and then upsize or downup after blur + blur_downsize_factor: list[int] = list(range(1, 5)), # downscale factor for blur + resize_cuda: bool = False, # whether to do resizing on GPU, the result is still moved back to CPU for compatibility. + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + self.use_random = use_random + downup_preset_values = { + "none": 1, + "very_low": min_downup_factor, + "low": min_downup_factor, + "medium": (min_downup_factor + max_downup_factor) // 2, + "high": max_downup_factor, + "very_high": max_downup_factor, + } + blur_downup_preset_values = { + "none": 1, + "very_low": 1, + "low": 4, + "medium": 2, + "high": 1, + "very_high": 4, + } + self.blur = Blur(config=blur_config, use_random=use_random) + + self.preset_strength = downup_preset + self.downup_preset = downup_preset if isinstance(downup_preset, int) else downup_preset_values[downup_preset] + self.downsize_before_blur = downsize_before_blur + self.min_downup_factor = min_downup_factor + self.max_downup_factor = max_downup_factor + self.blur_downsize_factor = blur_downsize_factor + self.blur_downup_preset = blur_downup_preset_values[downup_preset] + self.resize_cuda = resize_cuda + assert not (self.use_random and self.resize_cuda), "Cannot use resize on GPU during training." + + def _load_frame(self, data_dict: dict) -> tuple[np.ndarray, bool]: + key_img = self.input_keys[0] + frames = data_dict[key_img] + frames = _maybe_torch_to_numpy(frames) + is_image = False + if len(frames.shape) < 4: + frames = frames.transpose((2, 0, 1))[:, None] + is_image = True + return frames, is_image + + def __call__(self, data_dict: dict) -> dict: + if "control_input_blur" in data_dict: + # already processed + data_dict[self.output_keys[0]] = data_dict["control_input_blur"] + return data_dict + + key_out = self.output_keys[0] + + frames, is_image = self._load_frame(data_dict) + if self.preset_strength == "none": + if is_image: + frames = frames[:, 0] + data_dict[key_out] = torch.from_numpy(frames) + return data_dict + C, T, H, W = frames.shape + + # --- 1. Downscale Before Blur --- + if self.use_random: + downscale_factor = random.choice(self.blur_downsize_factor) + else: + downscale_factor = self.blur_downup_preset + + if self.downsize_before_blur: + new_W, new_H = W // downscale_factor, H // downscale_factor + downscaled = np.empty((C, T, new_H, new_W), dtype=frames.dtype) + + for t in range(T): + frame = np.ascontiguousarray(frames[:, t].transpose(1, 2, 0)) + resized = cv2.resize(frame, (new_W, new_H), interpolation=cv2.INTER_AREA) + if len(resized.shape) == 2: + resized = resized[..., None] + downscaled[:, t] = resized.transpose(2, 0, 1) + + frames = downscaled + # -------------------------- + + # --- 2. Apply Blur --- + frames = self.blur(frames) + # -------------------------- + + # --- 3. Upscale After Blur --- + if self.downsize_before_blur: + upscaled = np.empty((C, T, H, W), dtype=frames.dtype) + for t in range(T): + frame = np.ascontiguousarray(frames[:, t].transpose(1, 2, 0)) + resized = cv2.resize(frame, (W, H), interpolation=cv2.INTER_LINEAR) + if len(resized.shape) == 2: + resized = resized[..., None] + upscaled[:, t] = resized.transpose(2, 0, 1) + frames = upscaled + + # --- 4. Final Downup Augmentation --- + if self.use_random: + scale_factor = random.randint(self.min_downup_factor, self.max_downup_factor + 1) + else: + scale_factor = self.downup_preset + + final_W, final_H = int(W / scale_factor), int(H / scale_factor) + + final_frames = np.empty_like(frames) + for t in range(T): + frame = np.ascontiguousarray(frames[:, t].transpose(1, 2, 0)) + small = cv2.resize(frame, (final_W, final_H), interpolation=cv2.INTER_CUBIC) + large = cv2.resize(small, (W, H), interpolation=cv2.INTER_CUBIC) + if len(large.shape) == 2: + large = large[..., None] + final_frames[:, t] = large.transpose(2, 0, 1) + + if is_image: + final_frames = final_frames[:, 0] + data_dict[key_out] = torch.from_numpy(final_frames) + + return data_dict + + +class AddControlInputDepth(Augmentor): + """ + Add control input to the data dictionary. control input are expanded to 3-channels + steps to add new items: modify this file, configs/conditioner.py, conditioner.py + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = ["control_input_depth"], + args: Optional[dict] = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + if "control_input_depth" in data_dict: + # already processed + return data_dict + + key_out = self.output_keys[0] + depth = data_dict["depth"] + + frames = data_dict["video"] + _, T, H, W = frames.shape + depth = transforms_F.resize( + depth, + size=(H, W), + interpolation=transforms_F.InterpolationMode.BILINEAR, + ) + data_dict[key_out] = depth + return data_dict + + +class AddControlInputSeg(Augmentor): + """ + Add control input to the data dictionary. control input are expanded to 3-channels + steps to add new items: modify this file, configs/conditioner.py, conditioner.py + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = ["control_input_seg"], + thres_mb_python_decode: Optional[int] = 256, # required: <= 512 for 7b + use_fixed_color_list: bool = False, + num_masks_max: int = 100, + random_sample_num_masks: bool = True, + min_mask_size: float = 0.2, + args: Optional[dict] = None, + **kwargs, + ) -> None: + """ + Args: + thres_mb_python_decode: int, threshold of memory usage for python decode, in MB + use_fixed_color_list: bool, if True, use predefined colors for segmentation masks. If False, generate random colors for segmentation masks. + num_masks_max: int, maximum number of masks to sample + random_sample_num_masks: bool, if True, sample number of masks randomly. If False, sample all masks in the data. + min_mask_size: float, minimum size of the mask area, in fraction of the entire frame. + """ + super().__init__(input_keys, output_keys, args) + self.use_fixed_color_list = use_fixed_color_list + self.num_masks_max = num_masks_max + self.thres_mb_python_decode = thres_mb_python_decode + self.random_sample_num_masks = random_sample_num_masks + self.min_mask_size = min_mask_size + + def get_masks(self, data_dict: dict, num_masks: int = 1) -> tuple[torch.Tensor, bool]: + """ + Get a single mask from the data dictionary. + segmentation: list of dicts + phrase: str + segmentation_mask_rle: dict + data: dict + size: [N, 1] + counts: bytes + mask_shape: [T, H, W] + """ + frames = data_dict["video"] + _, T, H, W = frames.shape + + if not isinstance(data_dict["segmentation"], dict) and num_masks == 1: + # this video is a color-coded segmentation mask, where each color corresponds to a different object + # we need to extract the binary mask for a single object from the video + seg_video = data_dict["segmentation"] + seg_video = transforms_F.resize( + seg_video, + size=(H, W), + interpolation=transforms_F.InterpolationMode.NEAREST, + ) + # Get the first frame of the segmentation video + first_frame = seg_video[:, 0, :, :] + # Get a list of unique colors from the first frame and calculate mask size for each unique color + unique_colors = (first_frame // _BIN_SIZE).view(3, -1).permute(1, 0).unique( + dim=0 + ) * _BIN_SIZE # [N_colors,3] + # Randomly shuffle unique colors and take first N colors + perm = torch.randperm(len(unique_colors)) + unique_colors = unique_colors[perm] + unique_colors = unique_colors[:_MAX_UNIQUE_COLORS] # check up to max colors to save time + mask_sizes = [] + for color in unique_colors: + color_diff = first_frame.to(torch.float32) - color[:, None, None] + color_dists = torch.sqrt(torch.sum(color_diff**2, dim=0)) + mask = color_dists < _COLOR_TOLERANCE + mask_size = mask.sum() / (H * W) # Size as fraction of frame + mask_sizes.append(mask_size) + + # Only keep colors that produce masks >= min_mask_size of frame and not black + valid_color_indices = [ + i + for i, size in enumerate(mask_sizes) + if size >= self.min_mask_size and (unique_colors[i] > _BLACK_THRESHOLD).sum() > 0 + ] + if len(valid_color_indices) == 0: + # If no masks are large enough, return all ones + log.critical("No masks are large enough, returning all ones") + all_masks = np.ones((num_masks, T, H, W)).astype(bool) + return torch.from_numpy(all_masks), False # [num_masks,T,H,W] + else: + # Randomly select one of the valid large masks + valid_color_idx = valid_color_indices[np.random.randint(len(valid_color_indices))] + target_color = unique_colors[valid_color_idx] + # Create binary mask where True means within tolerance of target color + color_diff = seg_video.to(torch.float32) - target_color[:, None, None, None] + color_dists = torch.sqrt(torch.sum(color_diff**2, dim=0, keepdim=True)) # [1,T,H,W] + mask = (color_dists < _COLOR_TOLERANCE).to(torch.bool) # [1,T,H,W] + return mask, True + frame_indices = data_dict["frame_indices"] + frame_start, frame_end = frame_indices[0], frame_indices[-1] + 1 + is_continuous_frame_indices = (frame_end - frame_start) == T + assert len(frame_indices) == T, ( + f"frame_indices length {len(frame_indices)} != T {T}, likely due to video decoder using different fps, i.e. sample with stride. Need to return frame indices from video decoder." + ) + + all_masks = np.ones((num_masks, T, H, W)).astype(bool) # [num_masks,T,H,W] + + # sample number of masks + mask_ids = np.arange(len(data_dict["segmentation"])).tolist() + if len(data_dict["segmentation"]) == 0 or num_masks == 0: + return torch.from_numpy(all_masks), False # [num_masks,T,H,W] + if num_masks == 1: # Try up to 16 masks to find a large enough mask + mask_ids_select = np.random.choice(mask_ids, min(len(mask_ids), 16), replace=False) + else: + mask_ids_select = np.random.choice(mask_ids, num_masks, replace=False) + + for idx, mid in enumerate(mask_ids_select): + mask = data_dict["segmentation"][mid] + if type(mask) != dict: # data has sharding issue, skip this mask + return torch.from_numpy(all_masks), False # [num_masks,T,H,W] + shape = mask["segmentation_mask_rle"]["mask_shape"] + num_byte_per_mb = 1024 * 1024 + # total number of elements in uint8 (1 byte) / num_byte_per_mb + if mask["segmentation_mask_rle"]["data"]["size"][0] / num_byte_per_mb > self.thres_mb_python_decode: + # Switch to python decode if the mask is too large to avoid out of shared memory + if is_continuous_frame_indices and ( + T * shape[1] * shape[2] / num_byte_per_mb <= self.thres_mb_python_decode + ): + log.critical( + f"Using python decode for mask of shape {shape}, Continuous frame indices, frame_start: {frame_start}, frame_end: {frame_end}" + ) + rle = decode_partial_rle_width1( + mask["segmentation_mask_rle"]["data"], + frame_start * shape[1] * shape[2], + frame_end * shape[1] * shape[2], + ) + partial_shape = (frame_end - frame_start, shape[1], shape[2]) + rle = rle.reshape(partial_shape) * 255 + rle = np.stack( + [cv2.resize(_image_np, (W, H), interpolation=cv2.INTER_NEAREST) for _image_np in rle] + ) + else: # need to call decode_partial_rle_width1 multiple times + # It takes too much time to decode the mask, so we skip it and select another modality instead + log.critical(f"Skipping python decode for mask of shape {shape}") + return torch.from_numpy(all_masks), False # [num_masks,T,H,W] + else: + rle = mask_utils.decode(mask["segmentation_mask_rle"]["data"]) + rle = rle.reshape(shape) * 255 + # Select the frames that are in the video + if len(rle) < frame_end: # Pad the mask if it is shorter than original video + rle = np.vstack([rle, [rle[-1]] * (frame_end - len(rle))]) + rle = np.stack([cv2.resize(rle[i], (W, H), interpolation=cv2.INTER_NEAREST) for i in frame_indices]) + if num_masks == 1: # if we only need one mask and the current mask is large enough, return it + if (rle > 0).sum() / rle.size >= self.min_mask_size: + # log.critical(f"Found a large enough mask with size {(rle > 0).sum() / rle.size}") + all_masks[0] = rle.astype(bool) + break + elif idx == len(mask_ids_select) - 1: + log.critical("No large enough mask found, returning all ones") + else: # if we need multiple masks, return all masks + all_masks[idx] = rle.astype(bool) + del rle + return torch.from_numpy(all_masks), True # [num_masks,T,H,W] + + def __call__(self, data_dict: dict) -> dict: + if "control_input_seg" in data_dict: + # already processed + return data_dict + + key_out = self.output_keys[0] + if not isinstance(data_dict["segmentation"], dict): + # already have a color-coded segmentation mask video, directly use it + seg = data_dict["segmentation"] + seg = transforms_F.resize( + seg, + size=data_dict["video"].shape[-2:], + interpolation=transforms_F.InterpolationMode.NEAREST, + ) + data_dict[key_out] = seg + return data_dict + + # sample number of masks + if self.random_sample_num_masks: + num_masks = np.random.randint(0, min(self.num_masks_max + 1, len(data_dict["segmentation"]) + 1)) + else: + num_masks = len(data_dict["segmentation"]) + + all_masks, success = self.get_masks(data_dict, num_masks) + + if not success: + data_dict["preprocess_failed"] = True + del all_masks # free memory + return data_dict + + key_out = self.output_keys[0] + # control_input_seg is the colored segmentation mask, value in [0,255], shape (3, T, H, W) + data_dict[key_out] = torch.from_numpy( + segmentation_color_mask(all_masks, self.use_fixed_color_list) + ) # [3,T,H,W] + if num_masks > 0: + data_dict[key_out + "_mask"] = all_masks[random.randint(0, num_masks - 1)].clone()[None] # [1,T,H,W] + del all_masks # free memory + return data_dict + + +class AddControlInputIdentity(Augmentor): + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = ["control_input_identity"], + args: Optional[dict] = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict: + key_img = self.input_keys[0] + key_out = self.output_keys[0] + frames = _maybe_torch_to_numpy(data_dict[key_img]) # CTHW for video, HWC for image + is_image = len(frames.shape) < 4 + if is_image: + frames = frames.transpose((2, 0, 1)) + data_dict[key_out] = torch.from_numpy(frames).clone() # [C,T,H,W] for video, [C,H,W] for image + return data_dict + + +class AddControlInputHdmapBbox(Augmentor): + """ + Add control input to the data dictionary. control input are expanded to 3-channels + steps to add new items: modify this file, configs/conditioner.py, conditioner.py + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = ["control_input_hdmap_bbox"], + args: Optional[dict] = None, + use_random: Optional[bool] = True, + preset_strength: Optional[str] = "medium", + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + self.use_random = use_random + self.preset_strength = preset_strength + + def __call__(self, data_dict: dict) -> dict: + if "control_input_hdmap_bbox" in data_dict: + return data_dict + key_input = self.input_keys[0] + key_out = self.output_keys[0] + data_dict[key_out] = data_dict[key_input] + return data_dict + + +CTRL_HINT_KEYS = { + "control_input_edge": AddControlInputEdge, + "control_input_blur": AddControlInputBlur, + "control_input_depth": AddControlInputDepth, + "control_input_seg": AddControlInputSeg, + "control_input_inpaint": AddControlInputIdentity, + "control_input_hdmap_bbox": AddControlInputHdmapBbox, +} + + +class AddControlInputComb(Augmentor): + """ + Add control input to the data dictionary. control input are expanded to 3-channels + steps to add new items: modify this file, configs/conditioner.py, conditioner.py + """ + + def __init__( + self, + input_keys: list, + output_keys: Optional[list] = None, + args: Optional[dict] = None, + use_random: bool = True, + control_input_type: str = "edge_blur_depth_seg", + use_control_mask_prob: float = 0.0, + num_control_inputs_prob: list[float] = [1.0, 0.0, 0.0, 0.0], + num_control_inputs: int | None = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + self.use_random = use_random + self.control_hint_keys = ["control_input_" + key for key in control_input_type.split("_")] + self.use_control_mask_prob = use_control_mask_prob + self.num_control_inputs_prob = num_control_inputs_prob[: len(self.control_hint_keys)] + self.num_control_inputs = num_control_inputs + self.comb = {} + for output_key, class_name in CTRL_HINT_KEYS.items(): + aug = class_name( + input_keys=input_keys, output_keys=[output_key], args=args, use_random=use_random, **kwargs + ) + self.comb[output_key] = aug + + def __call__(self, data_dict: dict) -> dict: + success = False + if self.use_random: + # Randomly select a number of control inputs (or use fixed num_control_inputs when set) + num_keys_prob = self.num_control_inputs_prob + ctrl_hint_keys = self.control_hint_keys + if self.num_control_inputs is not None: + num_keys = max(1, min(self.num_control_inputs, len(ctrl_hint_keys))) + else: + num_keys = random.choices(range(len(ctrl_hint_keys)), weights=num_keys_prob, k=1)[0] + 1 + output_keys = np.random.choice(ctrl_hint_keys, size=num_keys, replace=False) + # output_keys = np.random.choice(["control_input_edge", "control_input_blur", "control_input_depth"], size=num_keys, replace=False) + zero_input = torch.zeros_like(data_dict[self.input_keys[0]]) # [C,T,H,W] + zero_mask = torch.zeros(*data_dict[self.input_keys[0]][:1].shape, dtype=torch.bool) # [1,T,H,W] + ones_mask = torch.ones(*data_dict[self.input_keys[0]][:1].shape, dtype=torch.bool) # [1,T,H,W] + use_control_mask = random.random() < self.use_control_mask_prob + for cur_key in ctrl_hint_keys: + cur_mask_key = cur_key + "_mask" + if cur_key in output_keys: + data_dict["preprocess_failed"] = False + data_dict = self.comb[cur_key](data_dict) + # log.critical(f"self.use_control_mask_prob: {self.use_control_mask_prob}") + if use_control_mask or cur_key == "control_input_inpaint": + # Get mask for the control input + if cur_mask_key not in data_dict: + data_dict[cur_mask_key], success = self.comb["control_input_seg"].get_masks( + data_dict, num_masks=1 + ) + else: + data_dict[cur_mask_key] = ones_mask + + # If preprocess failed or cannot get inpaint mask, use control_input_edge instead + if data_dict["preprocess_failed"] or (cur_key == "control_input_inpaint" and not success): + data_dict[cur_key] = zero_input + data_dict[cur_mask_key] = zero_mask + if num_keys == 1 and "control_input_edge" in ctrl_hint_keys: + new_key = "control_input_edge" + log.critical(f"Preprocess failed for {cur_key}, using {new_key} instead") + if new_key in data_dict: + del data_dict[new_key] + data_dict = self.comb[new_key](data_dict) + data_dict[new_key + "_mask"] = ones_mask + else: + data_dict[cur_key] = zero_input + data_dict[cur_mask_key] = zero_mask + + # Free memory: remove unused depth/segmentation to avoid OOM later + if "control_input_depth" not in output_keys and "depth" in data_dict: + del data_dict["depth"] + if "control_input_seg" not in output_keys and "segmentation" in data_dict: + del data_dict["segmentation"] + if "segmentation" in data_dict and isinstance(data_dict["segmentation"], dict): + del data_dict["segmentation"] + + if "control_input_inpaint" in output_keys and success: # Post-process the inpaint mask + inpaint_mask_key = "control_input_inpaint_mask" + if random.random() < 0.5: # randomly negate the mask + data_dict[inpaint_mask_key] = ~data_dict[inpaint_mask_key] + # Make sure the inpaint mask does not overlap with other masks + for cur_key in ctrl_hint_keys: + cur_mask_key = cur_key + "_mask" + if cur_mask_key == inpaint_mask_key: + continue + if torch.all(data_dict[cur_mask_key]) or torch.all(~data_dict[cur_mask_key]): # dummy mask + continue + # Remove overlap by zeroing overlapping regions in mask1 + overlap = data_dict[cur_mask_key] & data_dict[inpaint_mask_key] + if overlap.any(): + data_dict[inpaint_mask_key] = data_dict[inpaint_mask_key] & ~overlap + + else: + for k, v in self.comb.items(): + data_dict = v(data_dict) + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py new file mode 100644 index 00000000..453ae7ac --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ctypes +from ctypes import POINTER, c_float, c_int, c_ubyte, sizeof + +import torch + + +class BilateralGaussian: + class NppiSize(ctypes.Structure): + _fields_ = [("width", c_int), ("height", c_int)] + + class NppiPoint(ctypes.Structure): + _fields_ = [("x", c_int), ("y", c_int)] + + def __init__(self): + self.npp_i_lib = self._load_npp_library() + self._setup_buffer_size_function() + self._setup_bilateral_function() + + def _load_npp_library(self): + return ctypes.CDLL("libnppif.so") + + def _setup_buffer_size_function(self): + self.get_buffer_size_func = self.npp_i_lib.nppiFilterCannyBorderGetBufferSize + self.get_buffer_size_func.restype = c_int + self.get_buffer_size_func.argtypes = [BilateralGaussian.NppiSize, POINTER(c_int)] # oSizeROI # bufferSize + + def _setup_bilateral_function(self): + self.bilateral_function = self.npp_i_lib.nppiFilterBilateralGaussBorder_8u_C3R + self.bilateral_function.restype = c_int + self.bilateral_function.argtypes = [ + POINTER(c_ubyte), # pSrc + c_int, # nSrcStep + BilateralGaussian.NppiSize, # oSrcSize + BilateralGaussian.NppiPoint, # oSrcOffset + POINTER(c_ubyte), # pDst + c_int, # nDstStep + BilateralGaussian.NppiSize, # oSizeROI + c_int, # nRadius + c_int, # nStepBetweenSrcPixels + c_float, # nValSquareSigma + c_float, # nPosSquareSigma + c_int, # eBorderType + ] + + def _prepare_input(self, image_tensor): + if not image_tensor.is_cuda: + image_tensor = image_tensor.cuda() + if image_tensor.dtype != torch.uint8: + image_tensor = (image_tensor * 255).byte() + return image_tensor + + def _get_buffer_size(self, roi): + buffer_size = c_int(0) + status = self.get_buffer_size_func(roi, ctypes.byref(buffer_size)) + if status != 0: + raise RuntimeError(f"Failed to get buffer size, status: {status}") + return buffer_size.value + + def __call__(self, image_tensor, radius=30, color_sigma_square=150 * 150, sigma_space_square=100 * 100): + # Prepare input + image_tensor = self._prepare_input(image_tensor) + + height, width, channels = image_tensor.shape + output = torch.empty_like(image_tensor) + + src_ptr = ctypes.cast(image_tensor.data_ptr(), POINTER(c_ubyte)) + dst_ptr = ctypes.cast(output.data_ptr(), POINTER(c_ubyte)) + + roi = BilateralGaussian.NppiSize(width, height) + + status = self.bilateral_function( + src_ptr, + width * channels * sizeof(c_ubyte), + BilateralGaussian.NppiSize(width, height), + BilateralGaussian.NppiPoint(0, 0), + dst_ptr, + width * channels * sizeof(c_ubyte), + roi, + c_int(radius), + 1, # step size + c_float(color_sigma_square), + c_float(sigma_space_square), + 2, # border replicate + ) + + if status != 0: + raise RuntimeError(f"NPP Canny edge detection failed with status {status}") + + return output diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py new file mode 100644 index 00000000..bc2967e5 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py @@ -0,0 +1,183 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random + +import matplotlib.colors as mcolors +import numpy as np + +# Array of 23 highly distinguishable colors in RGB format +PREDEFINED_COLORS_SEGMENTATION = np.array( + [ + [255, 0, 0], # Red + [0, 255, 0], # Green + [0, 0, 255], # Blue + [255, 255, 0], # Yellow + [0, 255, 255], # Cyan + [255, 0, 255], # Magenta + [255, 140, 0], # Dark Orange + [255, 105, 180], # Hot Pink + [0, 0, 139], # Dark Blue + [0, 128, 128], # Teal + [75, 0, 130], # Indigo + [128, 0, 128], # Purple + [255, 69, 0], # Red-Orange + [34, 139, 34], # Forest Green + [128, 128, 0], # Olive + [70, 130, 180], # Steel Blue + [255, 215, 0], # Gold + [255, 222, 173], # Navajo White + [144, 238, 144], # Light Green + [255, 99, 71], # Tomato + [221, 160, 221], # Plum + [0, 255, 127], # Spring Green + [255, 255, 255], # White + ] +) + + +def generate_distinct_colors() -> np.ndarray: + """ + Generate `n` visually distinguishable and randomized colors. + + Returns: + np.ndarray, (3) + """ + # Randomize hue, saturation, and lightness within a range + hue = random.uniform(0, 1) # Full spectrum of hues + saturation = random.uniform(0.1, 1) # Vibrant colors + lightness = random.uniform(0.2, 1.0) # Avoid too dark + + r, g, b = mcolors.hsv_to_rgb((hue, saturation, lightness)) + return (np.array([r, g, b]) * 255).astype(np.uint8) + + +def segmentation_color_mask(segmentation_mask: np.ndarray, use_fixed_color_list: bool = False) -> np.ndarray: + """ + Convert segmentation mask to color mask + Args: + segmentation_mask: np.ndarray, shape (num_masks, T, H, W) + Returns: + np.ndarray, shape (3, T, H, W), with each mask converted to a color mask, value [0,255] + """ + + num_masks, T, H, W = segmentation_mask.shape + segmentation_mask_sorted = [segmentation_mask[i] for i in range(num_masks)] + # Sort the segmentation mask by the number of non-zero pixels, from most to least + segmentation_mask_sorted = sorted(segmentation_mask_sorted, key=lambda x: np.count_nonzero(x), reverse=True) + + output = np.zeros((3, T, H, W), dtype=np.uint8) + if use_fixed_color_list: + predefined_colors_permuted = PREDEFINED_COLORS_SEGMENTATION[ + np.random.permutation(len(PREDEFINED_COLORS_SEGMENTATION)) + ] + else: + predefined_colors_permuted = [generate_distinct_colors() for _ in range(num_masks)] + # index the segmentation mask from last channel to first channel, i start from num_masks-1 to 0 + for i in range(num_masks): + mask = segmentation_mask_sorted[i] + color = predefined_colors_permuted[i % len(predefined_colors_permuted)] + + # Create boolean mask and use it for assignment + bool_mask = mask > 0 + for c in range(3): + output[c][bool_mask] = color[c] + + return output + + +def decode_partial_rle_width1(rle_obj: dict, start_row: int, end_row: int) -> np.ndarray: + """ + Decode a partial RLE encoded mask with width = 1. In SAM2 output, the video mask (num_frame, height, width) are reshaped to (total_size, 1). + Sometimes the video mask could be large, e.g. 1001x1080x1092 shape and it takes >1GB memory if using pycocotools, resulting in segmentation faults when training with multiple GPUs and data workers. + This function is used to decode the mask for a subset of frames to reduce memory usage. + + Args: + rle_obj (dict): RLE object containing: + - 'size': A list [height, width=1] indicating the dimensions of the mask. + - 'counts': A bytes or string object containing the RLE encoded data. + start_row (int): The starting row (inclusive). It's computed from frame_start * height * width. + end_row (int): The ending row (exclusive). It's computed from frame_end * height * width. + + Returns: + numpy.ndarray: Decoded binary mask for the specified rows as a 1D numpy array. + """ + height, width = rle_obj["size"] + + # Validate row range + if width != 1: + raise ValueError("This function is optimized for width=1.") + if start_row < 0 or end_row > height or start_row >= end_row: + raise ValueError("Invalid row range specified.") + + # Decode the RLE counts + counts = rle_obj["counts"] + if isinstance(counts, str): + counts = np.frombuffer(counts.encode("ascii"), dtype=np.uint8) + elif isinstance(counts, bytes): + counts = np.frombuffer(counts, dtype=np.uint8) + else: + raise ValueError("Unsupported format for counts. Must be str or bytes.") + + # Interpret counts as a sequence of run lengths + run_lengths = [] + current_val = 0 + i = 0 + while i < len(counts): + x = int(0) + k = 0 + more = True + while more: + c = int(counts[i]) - 48 + x |= (c & 0x1F) << (5 * k) + more = (c & 0x20) != 0 + i += 1 + k += 1 + if not more and (c & 0x10): + x |= -1 << (5 * k) + if len(run_lengths) > 2: + x += run_lengths[-2] + + run_lengths.append(x) + current_val += x + if current_val > end_row: + break + # Initialize the partial mask + idx_start = start_row + idx_end = end_row + partial_mask = np.zeros(idx_end - idx_start, dtype=np.uint8) + partial_height = end_row - start_row + idx = 0 # Current global index + for i, run in enumerate(run_lengths): + run_start = idx + run_end = idx + run + if run_end <= idx_start: + # Skip runs entirely before the region + idx = run_end + continue + if run_start >= idx_end: + # Stop decoding once we pass the region + break + + # Calculate overlap with the target region + start = max(run_start, idx_start) + end = min(run_end, idx_end) + if start < end: + partial_start = start - idx_start + partial_end = end - idx_start + partial_mask[partial_start:partial_end] = i % 2 + + idx = run_end + return partial_mask.reshape((partial_height, 1), order="F") diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py new file mode 100644 index 00000000..b44428e8 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py @@ -0,0 +1,329 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Augmentors for transfer (control-conditioned) image and video generation in the cosmos3 VFM pipeline. + +Transfer training conditions the model on control signals (edge, blur, depth, or segmentation) +to generate images or videos, aligned with cosmos/transfer2. This module provides: + +- **TransferToTrainingFormat**: Converts (control_input, target) into the joint dataloader format + with SequencePlan (condition frame + generated frame), for both image and video outputs. + +- **VideoTransferSampleFrame**: For video→image transfer: samples a single frame index consistently + across control and video tensors, producing image-sized tensors from 4D video inputs. +- **AddControlFromVideoComb**: Uses AddControlInputComb (in transfer_control_input) to compute one of edge/blur/depth/seg + from video or precomputed fields and writes the chosen control to data_dict["control_input"]. +- **SampleResolution**: Samples a resolution from a list and sets data_dict["_res_size_map"] so downstream + resize/padding use that resolution (used to combine multiple resolutions in one dataloader). +""" + +from __future__ import annotations + +import random +from typing import cast + +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.augmentors.transfer_control_input import AddControlInputComb +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + + +class SampleResolution(Augmentor): + """Sample one resolution from a list and set data_dict['_res_size_map'] for downstream resize/padding. + + When used before ResizeLargestSideAspectPreserving and ReflectionPadding, those augmentors will + use obtain_augmentation_size(), which reads _res_size_map when present. This allows one dataloader + to produce samples at different resolutions (e.g. 480 and 720) by sampling per sample. + + resolutions_weights: Optional sampling weights for each resolution (same length as resolutions). + Weights are used by random.choices and need not sum to 1. If None, sampling is uniform. + """ + + def __init__( + self, + input_keys: list, + output_keys: list | None = None, + args: dict | None = None, + resolutions: list[str] | None = None, + resolutions_weights: list[float] | None = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys, args) + self.resolutions = list(resolutions) if resolutions else [] + assert len(self.resolutions) > 0, "SampleResolution requires at least one resolution." + for r in self.resolutions: + assert r in VIDEO_RES_SIZE_INFO, f"Unknown resolution {r}; known: {list(VIDEO_RES_SIZE_INFO.keys())}" + self.resolutions_weights = resolutions_weights + if self.resolutions_weights is not None: + assert len(self.resolutions_weights) == len(self.resolutions), ( + "resolutions_weights must have same length as resolutions." + ) + + def __call__(self, data_dict: dict) -> dict: + if self.resolutions_weights is not None: + res = random.choices(self.resolutions, weights=self.resolutions_weights, k=1)[0] + else: + res = random.choice(self.resolutions) + data_dict["_res_size_map"] = VIDEO_RES_SIZE_INFO[res] + return data_dict + + +class TransferToTrainingFormat(Augmentor): + """Convert (control_input, target) into joint-dataloader training format with SequencePlan. + + Reads data_dict["control_input"] and data_dict["video"] (target). Normalizes control to + mean/std 0.5, then writes [control_tensor, target_tensor] into data_dict[output_media_key] + ("images" for image transfer, "video" for video transfer). Also sets num_frames, + dataset_name, fps, ai_caption, selected_caption_type, sequence_plan, and image_size. + + Supports both image (3D: C,H,W) and video (4D: C,T,H,W); for image output, 4D tensors + are sliced to the first frame. Same output structure as ImageEditingToTrainingFormat. + """ + + def __init__( + self, + input_keys: list | None = None, + mean: float = 0.5, + std: float = 0.5, + output_media_key: str = "images", + conditioning_config: dict[int, float] | None = None, + share_vision_temporal_positions: bool = True, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], None, args) + self.mean = mean + self.std = std + self.output_media_key = output_media_key + self.conditioning_config = conditioning_config + self.share_vision_temporal_positions = share_vision_temporal_positions + + if self.conditioning_config is not None: + for num_frames, prob in self.conditioning_config.items(): + if not isinstance(num_frames, int) or num_frames < 0: + raise ValueError(f"conditioning_config keys must be non-negative integers, got {num_frames}") + if not isinstance(prob, (int, float)) or prob < 0: + raise ValueError(f"conditioning_config values must be non-negative numbers, got {prob}") + total_prob = sum(self.conditioning_config.values()) + if total_prob <= 0: + raise ValueError("conditioning_config probabilities must sum to a positive number") + self.normalized_conditioning_config = {k: v / total_prob for k, v in self.conditioning_config.items()} + else: + self.normalized_conditioning_config = None + + def _normalize_tensor(self, x: torch.Tensor) -> torch.Tensor: + """Normalize channel-wise to given mean/std. Accepts values in [0,1] or [0,255] (auto-detected).""" + if x.dtype == torch.uint8 or x.max() > 1.0: + x = x.float() / 255.0 + return transforms_F.normalize(x, mean=[self.mean] * 3, std=[self.std] * 3) + + def __call__(self, data_dict: dict) -> dict | None: + control_norm = data_dict.get("control_input") + target_norm = data_dict.get("video") + + if control_norm is None or target_norm is None: + log.warning( + f"TransferToTrainingFormat: missing control or target (video): {data_dict.get('__key__', 'unknown')}", + rank0_only=False, + ) + return None + + try: + if control_norm.dim() == 2: + control_norm = control_norm.unsqueeze(0).expand(3, -1, -1) # [3,H,W] + # is_video = control.dim() == 4 and isinstance(target_norm, torch.Tensor) and target_norm.dim() == 4 + if self.output_media_key == "video": + # Video: (C, T, H, W) each; normalize per frame + # control_norm = self._normalize_tensor(control.float()) + num_frames = control_norm.shape[1] + data_dict["video"] = [control_norm, target_norm] + data_dict["num_frames"] = num_frames + data_dict["dataset_name"] = "video_transfer" + data_dict["fps"] = data_dict.get("fps", 24.0) + else: + # Image: (C, H, W) + if target_norm.dim() == 4: + target_norm = target_norm[:, 0] + if control_norm.dim() == 4: + control_norm = control_norm[:, 0] + # control_norm = self._normalize_tensor(control.float()) + data_dict["images"] = [control_norm, target_norm] + data_dict["num_frames"] = 2 + data_dict["dataset_name"] = "image_transfer" + data_dict["fps"] = 30.0 + data_dict.setdefault("ai_caption", "") + data_dict.setdefault("selected_caption_type", "transfer_caption") + + num_condition_frames = 0 + if self.normalized_conditioning_config is not None: + frames_options = list(self.normalized_conditioning_config.keys()) + weights = list(self.normalized_conditioning_config.values()) + num_condition_frames = random.choices(frames_options, weights=weights, k=1)[0] + if self.output_media_key == "video" and target_norm.dim() == 4: + max_cond = target_norm.shape[1] - 1 + num_condition_frames = min(num_condition_frames, max_cond) + + if num_condition_frames > 0 and target_norm.shape[1] > 1: + condition_frames_indexes = list(range(num_condition_frames)) + else: + condition_frames_indexes = [] + + data_dict["sequence_plan"] = SequencePlan( + has_text=True, + has_vision=True, + condition_frame_indexes_vision=condition_frames_indexes, + # ControlNet-style transfer: control item and target item are + # spatio-temporally aligned (same source video, frame-synced). + # Forces shared temporal mRoPE grid across both items so the + # model sees control_t=k and target_t=k as the same time index. + share_vision_temporal_positions=self.share_vision_temporal_positions, + ) + except Exception as e: + log.warning( + f"TransferToTrainingFormat error: {data_dict.get('__key__', 'unknown')}, {e}", + rank0_only=False, + ) + return None + + # duplicate image_size for each vision input/output + data_dict["image_size"] = [data_dict["image_size"]] * len(data_dict[self.output_media_key]) + + return data_dict + + +class VideoTransferSampleFrame(Augmentor): + """Sample a single frame index from video tensors for image→image transfer. + + For each key in input_keys (default ["control_input", "video"]), resolves the + tensor (e.g. unwraps data_dict["video"]["video"]). Picks one temporal index t + (random if random_frame=True, else 0) and for every 4D tensor (C, T, H, W) + replaces it in-place with the slice at t, yielding (C, 1, H, W). 3D tensors + are left unchanged. All keys must be present; returns None if any is missing. + """ + + def __init__( + self, + input_keys: list | None = None, + args: dict | None = None, + random_frame: bool = True, + ) -> None: + self.input_keys = input_keys or ["control_input", "video"] + super().__init__(self.input_keys, None, args) + self.random_frame = random_frame + + def _get_tensor(self, data_dict: dict, key: str) -> torch.Tensor | None: + """Return the tensor for key; if key is 'video' and value is a dict, return value['video'].""" + val = data_dict.get(key) + if val is None: + return None + if isinstance(val, dict) and key == "video": + return val.get("video") + return val + + def __call__(self, data_dict: dict) -> dict | None: + # Resolve tensors; find T from first 4D tensor. Require all keys present. + tensors: list[tuple[str, torch.Tensor]] = [] + T: int | None = None + for key in self.input_keys: + raw = self._get_tensor(data_dict, key) + if raw is None or not isinstance(raw, torch.Tensor): + return None + tensor = cast(torch.Tensor, raw) + if tensor.dim() == 4: + if T is None: + T = tensor.shape[1] + if T == 0: + return None + tensors.append((key, tensor)) + else: + tensors.append((key, tensor)) + + if T is None: + # No 4D tensor; nothing to sample + return data_dict + + t_idx = random.randint(0, T - 1) if self.random_frame else 0 + + for key, tensor in tensors: + if tensor.dim() == 4: + sampled = tensor[:, t_idx : t_idx + 1] + else: + sampled = tensor + data_dict[key] = sampled + + return data_dict + + +class AddControlFromVideoComb(Augmentor): + """Compute one control signal from video via AddControlInputComb and set control_input. + + Delegates to AddControlInputComb (edge/blur computed from video; depth/seg from data_dict + when present). After the comb runs, selects the first non-zero control among + control_input_edge, control_input_blur, control_input_depth, control_input_seg, + writes it to data_dict["control_input"], and removes the temporary control keys. + + Args: + control_input_type: e.g. "edge_blur", "edge_blur_depth_seg" (which controls to consider). + num_control_inputs_prob: Probability distribution over number of combined controls; + this wrapper uses only the single chosen control. + """ + + CONTROL_KEYS = ("control_input_edge", "control_input_blur", "control_input_depth", "control_input_seg") + + def __init__( + self, + input_keys: list, + output_keys: list | None = None, + args: dict | None = None, + control_input_type: str = "edge_blur_depth_seg", + use_random: bool = True, + num_control_inputs_prob: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0), + num_control_inputs: int | None = None, + **kwargs, + ) -> None: + super().__init__(input_keys, output_keys or ["control_input"], args) + self._comb = AddControlInputComb( + input_keys=input_keys, + output_keys=None, + use_random=use_random, + control_input_type=control_input_type, + num_control_inputs_prob=list(num_control_inputs_prob), + num_control_inputs=num_control_inputs, + **kwargs, + ) + + def __call__(self, data_dict: dict) -> dict | None: + data_dict = self._comb(data_dict) + if data_dict is None: + return None + # Pick first control key that exists and has non-zero data (comb sets unchosen to zeros). + for key in self.CONTROL_KEYS: + if key in data_dict: + t = data_dict[key] + if isinstance(t, torch.Tensor) and t.numel() > 0 and t.abs().sum() > 0: + data_dict["control_input"] = t + break + else: + # No break: no valid control found (e.g. all chosen controls failed or are zero). + log.warning("AddControlFromVideoComb: no non-zero control found", rank0_only=False) + return None + for key in self.CONTROL_KEYS: + data_dict.pop(key, None) + data_dict.pop(key + "_mask", None) + return data_dict diff --git a/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py b/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py new file mode 100644 index 00000000..ba78bf9b --- /dev/null +++ b/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py @@ -0,0 +1,853 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Optional + +import numpy as np +import omegaconf +import torch +from einops import rearrange +from torchcodec.decoders import AudioDecoder, VideoDecoder +from torchvision.transforms.v2 import Resize, UniformTemporalSubsample + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + +# Map dataset_resolution_type to resolution tier key in VIDEO_RES_SIZE_INFO +_DATASET_RESOLUTION_TIER: dict[str, str] = {"gt480p": "480", "gt720p": "720", "gt1080p": "1080"} + +_MIN_FPS = 10 +_MAX_FPS = 60 + + +class VideoParsing(Augmentor): + """ + This augmentor is used to parse the video bytes and get the video frames. + the return dict is back-compatible with old datasets, which video decoding happens in the decoder stage. + + Now uses torchcodec instead of decord for video decoding, with optional audio extraction. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) == 2, "VideoParsing augmentor only supports two input keys" + self.meta_key = input_keys[0] + self.video_key = input_keys[1] + + self.key_for_caption = args["key_for_caption"] + assert self.key_for_caption in [ + "t2w_windows", + "i2w_windows_later_frames", + ], "key_for_caption must be either t2w_windows or i2w_windows_later_frames" + self.min_duration = args["min_duration"] + self.min_fps = args["min_fps"] + self.max_fps = args["max_fps"] + self.num_frames = args["num_video_frames"] + self.use_native_fps = args["use_native_fps"] # orginal fps if (total_frames // self.num_frames == 1). + # a list of allowed num_multiplers (how many frames are skipped) + # default is 1 - 100 which allows virtually any num_multipler possible + self.allowed_num_multiplers = args.get("allowed_num_multiplers", list(range(1, 100))) + log.info(f"allowed_num_multiplers in video_parsing with use_native_fps: {self.allowed_num_multiplers}") + self.use_original_fps = args["use_original_fps"] # use original fps without sampling + + # Dynamic FPS mode: sample stride from valid range based on video properties + self.use_dynamic_fps = args.get("use_dynamic_fps", False) + # low_fps_bias: 0.0 = favor original FPS (stride=1), 0.5 = uniform, 1.0 = favor slow-mo (high stride) + self.low_fps_bias = args.get("low_fps_bias", 0.5) + assert 0.0 <= self.low_fps_bias <= 1.0, f"low_fps_bias must be in [0, 1], got {self.low_fps_bias}" + + # Validate mutually exclusive modes + mode_count = sum([self.use_dynamic_fps, self.use_native_fps, self.use_original_fps]) + assert mode_count <= 1, ( + f"Only one FPS mode can be enabled at a time. Got: " + f"use_dynamic_fps={self.use_dynamic_fps}, " + f"use_native_fps={self.use_native_fps}, " + f"use_original_fps={self.use_original_fps}" + ) + + if self.use_dynamic_fps: + log.info( + f"use_dynamic_fps mode enabled: stride will be sampled from valid range per video " + f"with low_fps_bias={self.low_fps_bias} (0.0=favor original FPS, 0.5=uniform, 1.0=favor slow-mo)" + ) + + if self.use_native_fps or self.use_original_fps: + assert self.num_frames > 0, "num_frames must be greater than 0 when use_native_fps is True" + if self.use_dynamic_fps: + assert self.num_frames > 0, "num_frames must be greater than 0 when use_dynamic_fps is True" + if self.num_frames > 0: + self.sampler = UniformTemporalSubsample(self.num_frames) + self.video_decode_num_threads = args.get("video_decode_num_threads", 1) + + # Audio extraction parameters + self.extract_audio = args.get("extract_audio", False) + self.audio_sample_rate = args.get("audio_sample_rate", 44100) + self.seek_mode = args.get("seek_mode", "exact") + + def _extract_audio_chunk( + self, video_bytes: bytes, video_fps: float, frame_indices: list[int] + ) -> torch.Tensor | None: # returns [C,N_audio] or None + """ + Extract audio chunk corresponding to the given frame indices. + + Args: + video_bytes: Raw video bytes + video_fps: Video frames per second + frame_indices: List of frame indices being extracted + + Returns: + Audio tensor of shape (C, N) or None if audio extraction fails + """ + try: + # Create audio decoder + audio_decoder = AudioDecoder(video_bytes) + + # Calculate time range for audio corresponding to video frames + time_start = frame_indices[0] / video_fps + time_end = (frame_indices[-1] + 1) / video_fps # +1 to include the last frame's duration + + # Get audio samples for the specific time range + audio_metadata = audio_decoder.metadata + orig_sample_rate = audio_metadata.sample_rate + + audio_samples = audio_decoder.get_samples_played_in_range(start_seconds=time_start, stop_seconds=time_end) + audio_chunk = audio_samples.data # [C,N_orig] + + # Resample if needed + if orig_sample_rate != self.audio_sample_rate: + import librosa + + audio_np = audio_chunk.numpy() + resampled_audio_np = librosa.resample( + audio_np, orig_sr=orig_sample_rate, target_sr=self.audio_sample_rate, axis=-1 + ) + audio_chunk = torch.from_numpy(resampled_audio_np) # [C,N_resampled] + + # Clean up audio decoder + del audio_decoder + + return audio_chunk + + except Exception as e: + log.warning(f"Failed to extract audio: {e}", rank0_only=False) + return None + + def _sample_stride_with_bias(self, max_stride: int) -> int: + """Sample a stride from [1, max_stride] with bias controlled by low_fps_bias. + + Args: + max_stride: Maximum valid stride value. + + Returns: + Sampled stride value. + + The bias controls the probability distribution: + - low_fps_bias=0.0: Favor stride=1 (original FPS) + - low_fps_bias=0.5: Uniform distribution + - low_fps_bias=1.0: Favor high strides (slow-mo / lower FPS) + """ + if max_stride == 1: + return 1 + + # Linear interpolation from (1 - bias) to bias, clamped to min 0.01 + strides = np.arange(1, max_stride + 1) + weights = np.linspace(1 - self.low_fps_bias, self.low_fps_bias, max_stride) + weights = np.maximum(weights, 0.01) + probs = weights / weights.sum() + + return int(np.random.choice(strides, p=probs)) + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception as e: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", rank0_only=False + ) + return None + + if not isinstance(video, bytes): + return data_dict + + video_info = { + "fps": meta_dict["framerate"], + "n_orig_video_frames": meta_dict["nb_frames"], + } + + if video_info["fps"] < self.min_fps: + log.warning(f"Video FPS {video_info['fps']} is less than min_fps {self.min_fps}", rank0_only=False) + return None + if video_info["fps"] > self.max_fps: + log.warning(f"Video FPS {video_info['fps']} is greater than max_fps {self.max_fps}", rank0_only=False) + return None + + options: list = list((i, item) for i, item in enumerate(meta_dict[self.key_for_caption])) + + # Skip the last window if possible. + # All windows except the last are 5 seconds long. The last window has a duration in the range [2.5s, 7.5), which is less preferred. + if len(options) > 1: + options = options[:-1] + + # shuffle options + random.shuffle(options) + video_frames = None + dynamic_conditioning_fps = None # Track conditioning FPS for dynamic mode + for chunk_index, option in options: + start_frame = option["start_frame"] + end_frame = option["end_frame"] + if (end_frame - start_frame) < self.min_duration * video_info["fps"]: + continue + + if self.use_native_fps or self.use_original_fps or self.use_dynamic_fps: + if (end_frame - start_frame) < self.num_frames: + continue + + # Create video decoder with torchcodec (directly from bytes) + video_decoder = VideoDecoder( + video, seek_mode=self.seek_mode, num_ffmpeg_threads=self.video_decode_num_threads + ) + + if self.use_dynamic_fps or self.use_native_fps or self.use_original_fps: + # Shared: Handle alpamayo - skip first 5 frames + if "alpamayo" in data_dict["__url__"].root: + start_frame += 5 + if (end_frame - start_frame) < self.num_frames: + continue + + total_frames = end_frame - start_frame + + # Compute num_multiplier based on mode + if self.use_dynamic_fps: + # Dynamic FPS mode: compute valid strides and sample with bias + max_stride = total_frames // self.num_frames + if max_stride < 1: + # Not enough frames even for stride=1, skip this chunk + continue + + # Sample stride with low_fps_bias controlling the distribution + num_multiplier = self._sample_stride_with_bias(max_stride) + + # Compute conditioning FPS based on sampled stride + dynamic_conditioning_fps = video_info["fps"] / num_multiplier + + fps_mode_desc = ( + "original_fps (contiguous)" if num_multiplier == 1 else f"subsampled (stride={num_multiplier})" + ) + log.info( + f"Dynamic FPS mode: video_fps={video_info['fps']}, total_frames={total_frames}, " + f"max_stride={max_stride}, sampled_stride={num_multiplier}, " + f"conditioning_fps={dynamic_conditioning_fps:.2f}, mode={fps_mode_desc}, " + f"low_fps_bias={self.low_fps_bias}", + rank0_only=False, + ) + elif self.use_native_fps: + # take mid self.num_frames frames from start frame to end frame. + # always try lower fps if possible. + num_multiplier = total_frames // self.num_frames + if num_multiplier not in self.allowed_num_multiplers: + log.debug( + f"Skipping chunk (native_fps): stride not allowed. num_multiplier={num_multiplier}, allowed={self.allowed_num_multiplers}" + ) + continue + else: # self.use_original_fps + # Original FPS mode: no frame skipping + num_multiplier = 1 + + # Shared: Check if we have enough frames for the selected stride + expected_length = self.num_frames * num_multiplier + if total_frames < expected_length: + log.info( + f"Skipping chunk: not enough frames for stride. total_frames={total_frames}, expected={expected_length}, num_multiplier={num_multiplier}", + rank0_only=False, + ) + continue + + # Shared: Select frames from the center of the window + _start_frame = start_frame + (total_frames - expected_length) // 2 + _end_frame = _start_frame + expected_length + frame_indices = list(range(_start_frame, _end_frame, num_multiplier)) + assert len(frame_indices) == self.num_frames, "frame_indices length is not equal to num_frames" + + # Decode frames with torchcodec + frame_batch = video_decoder.get_frames_at(frame_indices) + video_frames = frame_batch.data # [T,C,H,W] + video_frames = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] + + # Clean up video decoder + del video_decoder + + # Extract audio if requested + audio_chunk = None + if self.extract_audio: + audio_chunk = self._extract_audio_chunk(video, video_info["fps"], frame_indices) # [C,N_audio] + + break + + else: + frame_indices = list(range(start_frame, end_frame)) + num_multiplier = 1 # No frame skipping in this block of code. + + # online hot-fix for alpamayo data. Skip the first 5 frames as there is chance that the first five frames contain black frames. + if "alpamayo" in data_dict["__url__"].root: + assert len(frame_indices) >= 5, ( + "Getting less than 5 frames for alpamayo videos. There is no way to skip the first five frames." + ) + frame_indices = frame_indices[5:] + start_frame += 5 + + # Decode frames with torchcodec + try: + frame_batch = video_decoder.get_frames_at(frame_indices) + except Exception as e: + # Some segmentation videos for Transfer are not long enough as the target video, skip them. + log.warning( + f"Video is not long enough, return None. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}, start_frame: {start_frame}, end_frame: {end_frame}, frame_indices: {frame_indices}", + rank0_only=False, + ) + return None + video_frames = frame_batch.data # [T,C,H,W] + video_frames = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] + + # Clean up video decoder + del video_decoder + + # Extract audio if requested + audio_chunk = None + if self.extract_audio: + audio_chunk = self._extract_audio_chunk(video, video_info["fps"], frame_indices) # [C,N_audio] + + break + + if video_frames is None: + log.warning( + f"No valid video frames found, return None. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + video_info["chunk_index"] = chunk_index + video_info["frame_start"] = start_frame + video_info["frame_end"] = end_frame + video_info["num_frames"] = end_frame - start_frame # type: ignore + if self.num_frames > 0 and not (self.use_dynamic_fps or self.use_native_fps or self.use_original_fps): + # Uniform temporal subsampling mode (default when no FPS mode is enabled) + video_frames = rearrange( + self.sampler(rearrange(video_frames, "c t h w -> t c h w")), "t c h w -> c t h w" + ) # [C,T_sub,H,W] where T_sub = self.num_frames + num_multiplier = ( + end_frame - start_frame + ) / self.num_frames # Specifically for the uniform temporal subsampling case. + + video_info["video"] = video_frames + video_info["num_multiplier"] = num_multiplier # Store the frame skipping multiplier + + + # 1. Our video parser stores the original video FPS of the video. + # 2. We have multiple modes of frame selection -- consecutive chunk of frames or subsampled frames. + # Here's what we do in each case: + # + # A. Dynamic FPS mode (use_dynamic_fps=True): + # - We compute max possible stride based on total_frames // num_frames. + # - We sample a stride uniformly from [1, max_stride]. + # - We compute conditioning_fps = native_fps / stride. + # - This gives us a diverse range of effective FPS values. + # + # B. Consecutive chunk of frames (use_original_fps=True): + # - We use the stored FPS and the number of frames in the video. + # - We calculate the duration in seconds using the above two values. + # - conditioning_fps = native_fps (num_multiplier=1) + # + # C. Subsampled frames (use_native_fps=True or uniform subsampling): + # - We check the skipping_rate (1 / num_multiplier) in case of subsampling. + # - We adjust the conditioning FPS by the skipping_rate (faithful to original video's motion). + # - conditioning_fps = native_fps / num_multiplier + # - We calculate the duration in seconds using the adjusted conditioning FPS and the number of frames. + if dynamic_conditioning_fps is not None: + # Dynamic FPS mode: use the pre-computed conditioning FPS + video_info["conditioning_fps"] = dynamic_conditioning_fps + else: + # Other modes: compute effective FPS from stride + video_info["conditioning_fps"] = ( + video_info["fps"] / num_multiplier + ) # Effective FPS for RoPE modulation and text timestamps + + # Add audio if extracted + if audio_chunk is not None: + video_info["audio"] = audio_chunk + video_info["audio_sample_rate"] = self.audio_sample_rate + + # update data_dict, make it back-compatible with old datasets, which video decoding happens in the decoder stage. + data_dict[self.video_key] = video_info + + return data_dict + + +class VideoParsingWithFullFrames(Augmentor): + """ + This augmentor is used to parse the video bytes and get the video frames. + The caption is assumed to be for the entire video frames, rather than VideoParsing which assume captions are for a specific chunk of frames + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + assert len(input_keys) == 2, "VideoParsingWithFullFrames augmentor only supports two input keys" + self.meta_key = input_keys[0] + self.video_key = input_keys[1] + self.args = args + + # Dynamic FPS mode options + # If use_dynamic_fps=True, then we sample fps from a valid range of values. + # If use_dynamic_fps=False, then we use the original fps of the video (no frame skipping). + self.use_dynamic_fps = args.get("use_dynamic_fps", False) + # low_fps_bias: 0.0 = favor original FPS (stride=1), 0.5 = uniform, 1.0 = favor slow-mo (high stride) + self.max_stride = args.get("max_stride", 3) + self.min_stride = args.get("min_stride", 1) + assert self.max_stride >= self.min_stride, ( + f"max_stride ({self.max_stride}) must be >= min_stride ({self.min_stride})" + ) + self.min_fps = args.get("min_fps", _MIN_FPS) + self.max_fps = args.get("max_fps", _MAX_FPS) + if self.use_dynamic_fps: + log.info(f"use_dynamic_fps mode enabled: stride will be sampled from valid range per video ") + + self.video_decode_num_threads = args.get("video_decode_num_threads", 1) + self.seek_mode = args.get("seek_mode", "exact") + + self.size = args.get("size", None) + self.perform_resize = self.size is not None + + # Audio extraction parameters + self.extract_audio = args.get("extract_audio", False) + self.audio_sample_rate = args.get("audio_sample_rate", 48000) + # When True, emit placeholder sound=None and audio_sample_rate + # without extracting audio. Keeps output keys consistent across + # datasets that share the same dataloader (some with audio, some + # without). + self.emit_placeholder_sound = args.get("emit_placeholder_sound", False) + + # Resolution filter: when not "all", skip samples whose (width, height) are below the + # minimum for this aspect ratio in VIDEO_RES_SIZE_INFO[tier]. + self.dataset_resolution_type = args.get("dataset_resolution_type", "all") + self.resolution_tier = _DATASET_RESOLUTION_TIER.get(self.dataset_resolution_type) + + def _sample_stride_with_bias(self, max_stride: int, min_stride: int = 1) -> int: + """Sample a stride from [min_stride, max_stride] with bias controlled by low_fps_bias. + + Args: + max_stride: Maximum valid stride value. + min_stride: Minimum valid stride value. + + Returns: + Sampled stride value. + max_stride=3, min_stride=1, probs = [0.86681333, 0.11731043, 0.01587624] + These values are chosen to approximately match our old ablations. + TODO @pchattopadhy: Do ablations with this scheme + """ + assert max_stride >= min_stride, f"max_stride ({max_stride}) must be >= min_stride ({min_stride})" + if max_stride == min_stride: + return min_stride + + # Samples native fps stride mostly and picks low fps with some probability. + strides = np.arange(min_stride, max_stride + 1) + weights = np.exp(-2 * strides) + probs = weights / weights.sum() + return int(np.random.choice(strides, p=probs)) + + def _validate_and_probe(self, video: Optional[bytes], meta_dict: dict, data_dict: dict) -> bool: + """Validate video bytes, back-fill missing metadata via probing, and + enforce fps/resolution filters. + Returns True if the video is valid, False otherwise. + """ + + if not isinstance(video, bytes): + raise ValueError(f"Video is not bytes. url: {data_dict['__url__']}, key: {data_dict['__key__']}") + + if len(video) == 0: + log.warning( + f"Empty video bytes. url: {data_dict['__url__']}, key: {data_dict['__key__']}", rank0_only=False + ) + return False + + # Back-fill missing metadata keys (width, height, framerate, nb_frames) by probing the + # video stream header. Also probe when the sidecar framerate looks abnormal to verify + # against the actual video stream. + _needs_probe = any(k not in meta_dict for k in ("width", "height", "framerate", "nb_frames")) + _metadata_fps = meta_dict.get("framerate", 0) + _fps_suspicious = _metadata_fps > _MAX_FPS or _metadata_fps < _MIN_FPS + _needs_probe = _needs_probe or _fps_suspicious + if _needs_probe: + _probe = VideoDecoder(video, seek_mode=self.seek_mode) + meta_dict.setdefault("width", _probe.metadata.width) + meta_dict.setdefault("height", _probe.metadata.height) + meta_dict.setdefault("nb_frames", _probe.metadata.num_frames) + meta_dict["framerate"] = _probe.metadata.average_fps + del _probe + + # Skip videos with framerates outside [min_fps, max_fps] + if meta_dict["framerate"] > self.max_fps: + log.warning( + f"Skipping video with framerate {meta_dict['framerate']} > max_fps {self.max_fps}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return False + if meta_dict["framerate"] < self.min_fps: + log.warning( + f"Skipping video with framerate {meta_dict['framerate']} < min_fps {self.min_fps}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return False + + # Resolution check: skip sample if (width, height) are below the minimum for this aspect ratio + width = meta_dict["width"] + height = meta_dict["height"] + aspect_ratio: str | None = None + + if "__url__" in data_dict: + aspect_ratio = data_dict["__url__"].meta.opts["aspect_ratio"] + + # If the resolution of the video is smaller than the minimum resolution for the aspect ratio, skip the sample. This will ensure that we do not upsample any video. + if self.resolution_tier is not None: + min_w, min_h = VIDEO_RES_SIZE_INFO[self.resolution_tier][aspect_ratio] + if width < min_w and height < min_h: + return False + + return True + + def __call__(self, data_dict: dict) -> dict | None: + + # if in future we need to train with batch size > 1, need to pad frames + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception as e: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", rank0_only=False + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + # Resize video frames if size is specified. This computes a scaling ratio that fits the + # video within the target size bounds while preserving the original aspect ratio. + # The resize transform is applied during decoding via VideoDecoder's transforms parameter. + if self.perform_resize: + img_size = obtain_augmentation_size(data_dict, {"size": self.size}) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + orig_w, orig_h = meta_dict["width"], meta_dict["height"] + + # Compute uniform scaling ratio to fit video within target bounds (aspect-ratio preserving) + scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + + assert target_size[0] <= img_h and target_size[1] <= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + transform = [Resize(target_size)] + else: + transform = None + + # Adding try-expcept because some of the data is bad and video decoding call fail. + try: + video_decoder = VideoDecoder( + video, + seek_mode=self.seek_mode, + num_ffmpeg_threads=self.video_decode_num_threads, + transforms=transform, + ) + num_video_frames = len(video_decoder) + + stride = self._sample_stride_with_bias(self.max_stride, self.min_stride) + frame_indices = np.arange(0, num_video_frames, stride).tolist() + + # VAE compress temporal by 4x, with 1 as condition + # thus the max_video_frames must be 1 + 4N + num_video_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) + N = (num_video_frames - 1) // 4 + num_video_frames = 1 + 4 * N + frame_indices = frame_indices[0:num_video_frames] + + frame_batch = video_decoder.get_frames_at(frame_indices) + video_frames = frame_batch.data # [T,C,H,W] + video_frames = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] (T = num_video_frames) + + del video_decoder + except Exception as e: + log.warning( + f"Failed to decode video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + video_info = { + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "num_frames": len(frame_indices), + "video": video_frames, + "fps": meta_dict["framerate"], + "conditioning_fps": meta_dict["framerate"] / stride, + "n_orig_video_frames": num_video_frames, + } + + # Extract audio for the same time range as the video frames + if self.extract_audio: + audio_chunk = self._extract_audio_chunk( + video_bytes=video, video_fps=meta_dict["framerate"], frame_indices=frame_indices + ) + if audio_chunk is not None: + video_info["sound"] = audio_chunk + else: + video_info["sound"] = None + # Always include audio_sample_rate when extract_audio is enabled, + # even if audio extraction failed, so the collate function has a + # consistent set of keys across all samples in the batch. + video_info["audio_sample_rate"] = self.audio_sample_rate + elif self.emit_placeholder_sound: + video_info["sound"] = None + video_info["audio_sample_rate"] = self.audio_sample_rate + + data_dict[self.video_key] = video_info + + return data_dict + + def _extract_audio_chunk( + self, video_bytes: bytes, video_fps: float, frame_indices: list[int] + ) -> torch.Tensor | None: # returns [C,N_audio] or None + """Load audio from the clip, resample, and truncate to match video duration. + + Args: + video_bytes: Raw video bytes + video_fps: Video frames per second, used to compute video duration for truncation. + frame_indices: Frame indices extracted from the video. + + Returns: + Audio tensor of shape (C, N) or None if extraction fails. + """ + try: + # Quick check: probe container for audio streams before AudioDecoder init. + # AudioDecoder is slow when no audio stream exists. We use torchcodec._core + # (internal API) to read container metadata without setting up a decode pipeline. + # If this breaks on a future torchcodec upgrade, remove this block — AudioDecoder + # will still work, just slower on videos without audio. + try: + from torchcodec._core import create_from_bytes, get_container_metadata + + _handle = create_from_bytes(video_bytes) + _meta = get_container_metadata(_handle) + _has_audio = _meta.best_audio_stream_index is not None + del _handle, _meta + if not _has_audio: + return None + except (ImportError, AttributeError): + pass # Fall through to AudioDecoder if _core API is unavailable + + audio_decoder = AudioDecoder(video_bytes) + all_samples = audio_decoder.get_all_samples() + audio = all_samples.data # [C,N_orig] + orig_sr = all_samples.sample_rate + del audio_decoder, all_samples + + if orig_sr != self.audio_sample_rate: + import librosa + + audio = torch.from_numpy( + librosa.resample(audio.numpy(), orig_sr=orig_sr, target_sr=self.audio_sample_rate, axis=-1) + ) # [C,N_resampled] + + # Truncate audio to match the extracted video frame duration. + if len(frame_indices) > 0 and video_fps > 0: + video_duration = (frame_indices[-1] + 1) / video_fps + max_audio_samples = int(video_duration * self.audio_sample_rate) + if audio.shape[-1] > max_audio_samples: + audio = audio[:, :max_audio_samples] # [C,N_truncated] + + return audio.clone() # [C,N_audio] + + except Exception as e: + log.warning(f"Failed to extract audio: {e}", rank0_only=False) + return None + + +class VideoParsingChunkedFrames(VideoParsingWithFullFrames): + """ + This augmentor is used to parse the video bytes and get the video frames for a chunk of frames. + In the new scheme, we process + - Full frames if num_frames < 400 + - If num_frames >= 400, we caption only for the first n frame chunk + In this case, the video extraction needs to only extract the first n frame chunk + + Additionally, in robotics and AV data, we do multi-chunk captioning. + In this case, we need to sample a chunk uniformly at random and extract the video frames only for that chunk. + + The chunk's frame range is supplied by an upstream ``TextTransformForVideoJsonCaption`` + augmentor via ``data_dict["chunk_start_frame"]`` and ``data_dict["chunk_end_frame"]``. + Only frames in ``[chunk_start_frame, chunk_end_frame)`` (and the matching audio range) + are decoded. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + + def __call__(self, data_dict: dict) -> dict | None: + + # if in future we need to train with batch size > 1, need to pad frames + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception as e: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", rank0_only=False + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + # The chunk frame range must be supplied by an upstream caption-parsing augmentor + # (e.g. TextTransformForVideoJsonCaption). + if "chunk_start_frame" not in data_dict or "chunk_end_frame" not in data_dict: + log.warning( + f"VideoParsingChunkedFrames: missing chunk_start_frame/chunk_end_frame in data_dict. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + chunk_start = int(data_dict["chunk_start_frame"]) + chunk_end = int(data_dict["chunk_end_frame"]) + + # Resize video frames if size is specified. This computes a scaling ratio that fits the + # video within the target size bounds while preserving the original aspect ratio. + # The resize transform is applied during decoding via VideoDecoder's transforms parameter. + if self.perform_resize: + img_size = obtain_augmentation_size(data_dict, {"size": self.size}) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + orig_w, orig_h = meta_dict["width"], meta_dict["height"] + + # Compute uniform scaling ratio to fit video within target bounds (aspect-ratio preserving) + scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + + assert target_size[0] <= img_h and target_size[1] <= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + transform = [Resize(target_size)] + else: + transform = None + + # Adding try-expcept because some of the data is bad and video decoding call fail. + try: + video_decoder = VideoDecoder( + video, + seek_mode=self.seek_mode, + num_ffmpeg_threads=self.video_decode_num_threads, + transforms=transform, + ) + decoder_len = len(video_decoder) + + # Clamp the chunk range to what the decoder actually has. + chunk_start_clamped = max(0, min(chunk_start, decoder_len)) + chunk_end_clamped = max(chunk_start_clamped, min(chunk_end, decoder_len)) + if chunk_end_clamped <= chunk_start_clamped: + log.warning( + f"VideoParsingChunkedFrames: empty chunk after clamping. " + f"chunk=[{chunk_start},{chunk_end}), decoder_len={decoder_len}, " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + del video_decoder + return None + + stride = self._sample_stride_with_bias(self.max_stride, self.min_stride) + frame_indices = np.arange(chunk_start_clamped, chunk_end_clamped, stride).tolist() + + # VAE compress temporal by 4x, with 1 as condition + # thus the max_video_frames must be 1 + 4N + num_video_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) + N = (num_video_frames - 1) // 4 + num_video_frames = 1 + 4 * N + if num_video_frames < 1: + log.warning( + f"VideoParsingChunkedFrames: chunk too short for stride. " + f"chunk=[{chunk_start_clamped},{chunk_end_clamped}), stride={stride}, " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + del video_decoder + return None + frame_indices = frame_indices[0:num_video_frames] + if len(frame_indices) == 0: + del video_decoder + return None + + frame_batch = video_decoder.get_frames_at(frame_indices) + video_frames = frame_batch.data # [T,C,H,W] + video_frames = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] (T = num_video_frames) + + del video_decoder + except Exception as e: + log.warning( + f"Failed to decode video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + video_info = { + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "num_frames": len(frame_indices), + "video": video_frames, + "fps": meta_dict["framerate"], + "conditioning_fps": meta_dict["framerate"] / stride, + "n_orig_video_frames": num_video_frames, + } + + # Extract audio for the same time range as the chunk's video frames. + if self.extract_audio: + audio_chunk = self._extract_audio_chunk( + video_bytes=video, video_fps=meta_dict["framerate"], frame_indices=frame_indices + ) + if audio_chunk is not None: + video_info["sound"] = audio_chunk + else: + video_info["sound"] = None + # Always include audio_sample_rate when extract_audio is enabled, + # even if audio extraction failed, so the collate function has a + # consistent set of keys across all samples in the batch. + video_info["audio_sample_rate"] = self.audio_sample_rate + elif self.emit_placeholder_sound: + video_info["sound"] = None + video_info["audio_sample_rate"] = self.audio_sample_rate + + data_dict[self.video_key] = video_info + + # Cleanup: this augmentor is the last consumer of metas in the json-caption pipeline. + # Also drop the chunk range markers now that the chunk has been decoded. + data_dict.pop(self.meta_key, None) + data_dict.pop("chunk_start_frame", None) + data_dict.pop("chunk_end_frame", None) + + return data_dict diff --git a/cosmos3/_src/vfm/datasets/joint_dataloader.py b/cosmos3/_src/vfm/datasets/joint_dataloader.py new file mode 100644 index 00000000..2a69f5ca --- /dev/null +++ b/cosmos3/_src/vfm/datasets/joint_dataloader.py @@ -0,0 +1,870 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque +from dataclasses import dataclass +from typing import Any, ClassVar, Dict, Union + +import numpy as np +import torch +import webdataset +from torch.utils.data.dataloader import default_collate + +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log + +_TIMING_KEYS = {"_sample_time", "_aug_time", "_pre_aug_time", "_aug_step_times"} +_BATCH_TIMING_KEYS = { + "_worker_batch_time", + "_worker_aug_time", + "_worker_io_time", + "_worker_aug_step_times", + "_worker_id", +} + + +def custom_collate_fn(batch): + """ + Collate function that works like default_collate for all keys other than "text_token_ids", "images", and "video". + For "text_token_ids", "images", and "video" it simply returns them in a list, instead of stacking them as a tensor. + """ + list_collate_keys = { + "text_token_ids", + "images", + "video", + "action", + "domain_id", + "sequence_plan", + "sound", + "raw_action_dim", + "image_size", + } + + # Handle the case where the batch is already a dictionary (e.g. column-wise batching) + if isinstance(batch, dict): + return {key: (value if key in list_collate_keys else default_collate(value)) for key, value in batch.items()} + + # Handle standard list of samples + elem = batch[0] + if isinstance(elem, dict): + + result = {} + for key in elem: + if key in _TIMING_KEYS: + continue + if key in list_collate_keys: + result[key] = [d[key] for d in batch] + else: + result[key] = default_collate([d[key] for d in batch]) + result.update(_aggregate_worker_timing(batch)) + return result + else: + return default_collate(batch) + + +def _aggregate_worker_timing(samples: list[dict]) -> dict: + """Extract per-sample timing keys, aggregate into per-batch scalars.""" + info: dict[str, float | int] = {} + if "_sample_time" in samples[0]: + info["_worker_batch_time"] = sum(s.get("_sample_time", 0.0) for s in samples) + if "_aug_time" in samples[0]: + aug_total = sum(s.get("_aug_time", 0.0) for s in samples) + info["_worker_aug_time"] = aug_total + if "_worker_batch_time" in info: + info["_worker_io_time"] = info["_worker_batch_time"] - aug_total + if "_aug_step_times" in samples[0]: + agg: dict[str, float] = {} + for s in samples: + for step_name, t in s.get("_aug_step_times", {}).items(): + agg[step_name] = agg.get(step_name, 0.0) + t + info["_worker_aug_step_times"] = agg + worker_info = torch.utils.data.get_worker_info() + info["_worker_id"] = worker_info.id if worker_info is not None else 0 + return info + + +@dataclass +class _PackingMetrics: + """Per-batch packing statistics collected during the packing loop. + + Also serves as the single source of truth for packing-related metric names + via ``STATS_SPEC``, which the dataloading monitor callback consumes to + drive accumulation and logging. + """ + + current_sequence_length: int = 0 + num_samples: int = 0 + dropped_count: int = 0 + from_buffer: int = 0 + from_workers: int = 0 + + STATS_SPEC: ClassVar[list[tuple[str, str, str]]] = [ + # (batch_key, wandb_suffix, aggregation_type) + ("_num_tokens", "token_fraction", "scalar"), + ("_num_samples", "samples_per_batch", "list"), + ("_from_buffer", "from_buffer", "list"), + ("_from_workers", "from_workers", "list"), + ("_buffer_size", "buffer_size", "list"), + ("_dropped_count", "dropped", "scalar"), + ] + + def attach_to(self, output_batch: dict, buffer_size: int) -> None: + """Write packing statistics into the output batch dict.""" + output_batch["_num_tokens"] = self.current_sequence_length + output_batch["_num_samples"] = self.num_samples + output_batch["_from_buffer"] = self.from_buffer + output_batch["_from_workers"] = self.from_workers + output_batch["_buffer_size"] = buffer_size + output_batch["_dropped_count"] = self.dropped_count + + +class JointDataLoader(webdataset.WebLoader): + r""" + A joint dataloader that supports loading both images and videos. + """ + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None, + max_samples_per_batch: int | None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + ): + """ + Initialize the JointDataLoader with multiple datasets. + + The effective mini-batch size can be controlled with either max_sequence_length or + max_samples_per_batch. To use max_sequence_length, max_samples_per_batch needs to be None. + Vice versa, to use max_samples_per_batch, max_sequence_length needs to be None. + max_sequence_length and max_samples_per_batch cannot both be None simultaneously. + + Args: + dataloaders: key - dataset_name; value - {"dataloader": dataloader, "ratio": data_ratio} + tokenizer_spatial_compression_factor: The spatial compression factor of the tokenizer. + tokenizer_temporal_compression_factor: The temporal compression factor of the tokenizer. + patch_spatial: Spatial pathification factor. + max_samples_per_batch: Max number of samples per packed batch (alternative to max_sequence_length). + sound_latent_fps: Sound tokenizer latent rate in Hz (e.g. 25). If 0, sound tokens are not counted. + audio_sample_rate: Audio sample rate in Hz (e.g. 48000). Used with sound_latent_fps to estimate + sound token count. + + Example: + joint_loader = IterativeJointDataLoader( + dataloaders{ + "image_data": { + "dataloader": webdataset.WebLoader(...), + "ratio": 4, + }, + "video_data": { + "dataloader": torch.utils.data.DataLoader(...), + "ratio": 1, + }, + } + ) + """ + self.dataloader_list, self.dataset_name_list, self.data_ratios = [], [], [] + self.tokenizer_spatial_compression_factor = tokenizer_spatial_compression_factor + self.tokenizer_temporal_compression_factor = tokenizer_temporal_compression_factor + self.patch_spatial = patch_spatial + self.max_sequence_length = max_sequence_length + self.max_samples_per_batch = max_samples_per_batch + self.sound_latent_fps = sound_latent_fps + self.audio_sample_rate = audio_sample_rate + + assert (self.max_sequence_length is None) != (self.max_samples_per_batch is None), ( + "Exactly one of max_sequence_length or max_samples_per_batch must be None, but not both." + ) + + for dataset_name, dataloader_data in dataloaders.items(): + assert set(dataloader_data.keys()) == {"dataloader", "ratio"}, f"Invalid config: {dataloader_data}" + if dataloader_data["ratio"] <= 0: + continue + self.dataset_name_list.append(dataset_name) + self.dataloader_list.append(instantiate(dataloader_data["dataloader"], collate_fn=custom_collate_fn)) + self.data_ratios.append(dataloader_data["ratio"]) + + self.global_id = 0 + self.ratio_sum = sum(self.data_ratios) + + total = self.ratio_sum if self.ratio_sum > 0 else 1.0 + lines = [f"JointDataLoader: {len(self.dataset_name_list)} streams"] + for name, ratio in zip(self.dataset_name_list, self.data_ratios): + lines.append(f" {name}: ratio={ratio:.4g} ({ratio / total:.1%})") + log.info("\n".join(lines)) + + self.data_len = 0 + self.dataloaders = [iter(dataloader) for dataloader in self.dataloader_list] + self.buffers = [deque() for _ in range(len(self.dataloader_list))] + for data in self.dataloader_list: + self.data_len += len(data) + + def _compute_num_tokens_per_sample(self, data_batch: dict) -> int: + """ + This function computes the number of tokens per sample in the data batch. + This includes text + vision generation tokens + action tokens. + + Args: + data_batch (dict): The data batch containing the text tokens. + + Returns: + int: The number of tokens per sample. + """ + + # The token sequence we have is + # [] + # The spatial dimension of image tokens is compressed by + # vae spatial downsampling factor + pathification + # The temporal dimension of image tokens is compressed by + # vae temporal downsampling factor + # Action tokens have 1 token per time step (no spatial dimension) + + text_token_ids = data_batch["text_token_ids"] + if isinstance(text_token_ids, list): + num_text_tokens = text_token_ids[0].shape[0] + else: + num_text_tokens = text_token_ids.shape[1] + + num_tokens = num_text_tokens + 1 + + # Vision part + is_image_batch = "images" in data_batch + input_images_or_videos = data_batch["images" if is_image_batch else "video"] + + # iterate over all the media in the batch + for media in input_images_or_videos if isinstance(input_images_or_videos, list) else [input_images_or_videos]: + if is_image_batch: + _, H, W = media.shape + T = 1 + else: + _, T, H, W = media.shape + + vae_spatial_downsample = self.tokenizer_spatial_compression_factor * self.patch_spatial + vae_temporal_downsample = self.tokenizer_temporal_compression_factor + + latent_h_shape = H // vae_spatial_downsample + latent_w_shape = W // vae_spatial_downsample + latent_t_shape = 1 + (T - 1) // vae_temporal_downsample + + num_vision_tokens = latent_h_shape * latent_w_shape * latent_t_shape + 2 + num_tokens += num_vision_tokens + + # Action part: each action time step is 1 token. + # Action tensor shape is (T_action, D) per sample; stored as a single-element list. + if "action" in data_batch: + list_of_actions = data_batch["action"] + for action in list_of_actions: + # skip None actions + if action is None: + continue + num_action_tokens = action.shape[0] + num_tokens += num_action_tokens + + # Sound part — estimate sound tokens from audio waveform length + if self.sound_latent_fps > 0 and "sound" in data_batch: + sound_data = data_batch["sound"] + if isinstance(sound_data, list) and len(sound_data) > 0: + first_sound = sound_data[0] + # Unwrap nested list if needed + if isinstance(first_sound, list): + first_sound = first_sound[0] + if first_sound is not None and isinstance(first_sound, torch.Tensor): + num_audio_samples = first_sound.shape[-1] + audio_duration = num_audio_samples / self.audio_sample_rate + num_sound_tokens = int(audio_duration * self.sound_latent_fps) + num_tokens += num_sound_tokens + + return num_tokens + + # Keys whose value per sample is a list of tensors to be flattened into one list in the batch + _FLATTEN_LIST_KEYS = {"image_size"} + + def _update_output_batch(self, output_batch: dict, output: dict): + for key, value in output.items(): + if key in _BATCH_TIMING_KEYS: + if key not in output_batch: + output_batch[key] = value + elif key in self._FLATTEN_LIST_KEYS and isinstance(value, list): + if key not in output_batch: + output_batch[key] = value + else: + output_batch[key].extend(value) + elif key not in output_batch: + output_batch[key] = [value] + else: + output_batch[key].append(value) + + def __len__(self) -> int: + return self.data_len + + # Keys where each sample may hold multiple tensors (e.g. multiple video + # clips in a packed sequence). Kept as single-element lists per sample + # via v[i:i+1] so that _update_output_batch yields list[list[Tensor]]. + _MULTI_ITEM_KEYS = {"text_token_ids", "images", "video", "action", "sound"} + + def _get_next_sample(self, index_id: int) -> dict: + """Pop the next single-sample dict from the buffer for the given dataloader. + + If the buffer is empty, fetches the next collated batch from the inner + dataloader and splits it into individual samples. + + Splitting rules: + - Multi-item list values (keys in ``_MULTI_ITEM_KEYS``): sliced + via ``v[i:i+1]`` to yield a single-element list ``[tensor]``. + A packed sequence can contain multiple items per key. + - Per-sequence metadata list values (all other list keys, e.g. + ``sequence_plan``, ``domain_id``): direct-indexed via ``v[i]`` + to yield the bare element. + - Tensor values ``(B, ...)``: sliced to ``(1, ...)`` via + ``v[i : i + 1]`` to preserve the batch dimension. + + After ``_update_output_batch`` accumulates samples, the packed output + batch has the following shapes: + - Multi-item keys (``text_token_ids``, ``video``, ``images``, + ``action``): ``list[list[Tensor]]`` — each inner list has one + element from one sub-sample. + - Per-sequence metadata keys (``sequence_plan``, ``domain_id``, + ``dataset_name``, etc.): ``list[element]`` — flat list. + - Tensor-origin keys: ``list[Tensor(1, ...)]``. + + Args: + index_id: Index of the dataloader to fetch from. + + Returns: + A single-sample dictionary. + """ + buffer = self.buffers[index_id] + if not buffer: + try: + batch = next(self.dataloaders[index_id]) + except StopIteration: + raise + + is_image_batch = "images" in batch + input_images_or_videos = batch["images" if is_image_batch else "video"] + batch_size = len(input_images_or_videos) + + for i in range(batch_size): + sample = {} + for k, v in batch.items(): + if k in _BATCH_TIMING_KEYS: + sample[k] = v + elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS: + # For multi-item keys (images, video, etc.), the collated + # value is a list with one element per sample. If the element + # is itself a list (e.g. image editing: [src, tgt]), use v[i] + # directly to avoid wrapping it in a redundant single-element + # list. Otherwise keep the v[i:i+1] slice so that + # _update_output_batch produces list[list[Tensor]]. + elem = v[i] + if isinstance(elem, list): + sample[k] = elem + else: + sample[k] = v[i : i + 1] + elif isinstance(v, list): + sample[k] = v[i] + else: + sample[k] = v[i : i + 1] + buffer.append(sample) + + return buffer.popleft() + + def set_start_iteration(self, iteration: int): + self.global_id = iteration + + def __iter__(self): + raise NotImplementedError("__iter__ function is not implemented yet") + + +class IterativeJointDataLoader(JointDataLoader): + r""" + An iterative joint dataloader that supports loading multiple modalities. + + The behavior depends on the ``seed`` parameter: + + - **seed is not None** (Default): + The modality is randomly selected at each iteration based on the probability distribution + derived from the ratios. The random state is seeded with ``seed + global_id``, ensuring + that all ranks select the same modality at the same iteration (assuming synchronized global_id). + This prevents load imbalance due to mixed resolutions across ranks. + + - **seed is None**: + The modality selection follows a deterministic round-robin pattern based on the ratios. + For example, with 2 modalities (image and video) and ratio 2:1: + - Iterations 0, 1: all ranks process images + - Iteration 2: all ranks process videos + - ... and so on. + This also ensures all ranks process the same modality at the same iteration. + """ + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + seed: int | None = 42, + ): + super().__init__( + dataloaders, + tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor, + patch_spatial, + max_sequence_length, + max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + ) + self.seed = seed + # Calculate probabilities for random sampling + total_ratio = sum(self.data_ratios) + self.data_probs = np.array([ratio / total_ratio for ratio in self.data_ratios]) + + def __iter__(self): + while True: + if self.seed is not None: + rng = np.random.RandomState(self.seed + self.global_id) + index_id = rng.choice(len(self.dataloader_list), p=self.data_probs) + else: + data_id = self.global_id % self.ratio_sum + index_id = self._get_dataloader_index(data_id) + + metrics = _PackingMetrics() + output_batch = dict() + skipped_samples = deque() + lookahead_limit = 10 + lookahead_count = 0 + + while True: + # Check max samples limit first + if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch: + break + + # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop. + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + had_buffer = len(self.buffers[index_id]) > 0 + try: + output = self._get_next_sample(index_id) + except StopIteration: + break # No more data in this dataloader + + if had_buffer: + metrics.from_buffer += 1 + else: + metrics.from_workers += 1 + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.info( + f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + metrics.dropped_count += 1 + continue + + # current_sequence_length > 0 and selected sample is too large to fit in the remaining space. + # Instead of stopping immediately (creating large padding), we buffer this large sample + # and try to find a smaller one that fits in the remaining space. + skipped_samples.append(output) + lookahead_count += 1 + continue + + metrics.current_sequence_length += num_tokens_in_current_sample + metrics.num_samples += 1 + output["dataset_name"] = self.dataset_name_list[index_id] + self._update_output_batch(output_batch, output) + + # Add back skipped samples to the buffer for the next batch. + # appendleft puts item at HEAD. So we insert S3, then S2, then S1. + for sample in reversed(skipped_samples): + self.buffers[index_id].appendleft(sample) + + if len(output_batch) == 0: + return + + metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id])) + self.global_id += 1 + yield output_batch + + def _get_dataloader_index(self, data_id): + """Maps global id to the corresponding dataloader index based on ratio.""" + for i, r in enumerate(self.data_ratios): + if data_id < r: + return i + data_id -= r + raise ValueError("Invalid data_id") + + +class RankPartitionedDataLoader: + """Assigns each rank to exactly one dataset based on ratios. + + For N GPUs with datasets having ratios r_1:r_2:...:r_k, the first + N * r_1 / sum(r) ranks are assigned dataset 1, the next N * r_2 / sum(r) + ranks are assigned dataset 2, etc. Each rank instantiates a single + PyTorch DataLoader for its assigned dataset. + + The sharding information (``shard_world_size`` and ``shard_rank``) is set + on each dataset so that it shards data only across ranks that share the + same dataset, rather than across the full world. + + Example: + With 128 GPUs and datasets ``{"video": {"dataset": ..., "ratio": 3}, + "image": {"dataset": ..., "ratio": 1}}``: + + - Ranks 0-95 -> video (shard_world_size=96, shard_rank=0..95) + - Ranks 96-127 -> image (shard_world_size=32, shard_rank=0..31) + """ + + def __init__( + self, + datasets: dict[str, dict[str, Any]], + **dataloader_kwargs: Any, + ): + """ + Args: + datasets: Mapping of dataset name to config dict with keys: + + - ``"dataset"`` (required): a lazy config or dataset instance. + - ``"ratio"`` (required): positive int weight. + - ``"dataloader_kwargs"`` (optional): dict of keyword arguments + that override the top-level ``**dataloader_kwargs`` for this + dataset only (e.g. different ``num_workers`` or ``batch_size``). + + **dataloader_kwargs: Default kwargs forwarded to + ``torch.utils.data.DataLoader``. ``collate_fn`` defaults to + ``custom_collate_fn`` if not given. + """ + world_size = torch.distributed.get_world_size() + rank = torch.distributed.get_rank() + log.info(f"RankPartitionedDataLoader: world_size: {world_size} and rank: {rank}", rank0_only=False) + + _VALID_KEYS = {"dataset", "ratio", "dataloader_kwargs"} + names: list[str] = [] + dataset_configs: list[Any] = [] + ratios: list[int] = [] + per_dataset_kwargs: list[dict[str, Any]] = [] + for name, cfg in datasets.items(): + extra = set(cfg.keys()) - _VALID_KEYS + assert not extra, f"Dataset {name!r}: unexpected keys {extra}. Allowed: {_VALID_KEYS}" + if cfg["ratio"] <= 0: + log.warning( + f"RankPartitionedDataLoader: Skipping dataset {name} with ratio {cfg['ratio']}", rank0_only=False + ) + continue + names.append(name) + dataset_configs.append(cfg["dataset"]) + ratios.append(cfg["ratio"]) + per_dataset_kwargs.append(cfg.get("dataloader_kwargs", {})) + + assert len(names) > 0, "No datasets with positive ratios provided." + assert world_size >= len(names), ( + f"world_size ({world_size}) must be >= number of datasets ({len(names)}) " + f"so each dataset gets at least one rank." + ) + + total_ratio = sum(ratios) + ideal = [r / total_ratio * world_size for r in ratios] + allocations = [max(1, int(q)) for q in ideal] + remaining = world_size - sum(allocations) + if remaining > 0: + remainders = sorted(range(len(ratios)), key=lambda i: ideal[i] - allocations[i], reverse=True) + for j in range(remaining): + allocations[remainders[j]] += 1 + elif remaining < 0: + deficit = -remaining + while deficit > 0: + best = max( + (i for i in range(len(allocations)) if allocations[i] > 1), + key=lambda i: (allocations[i] - ideal[i], allocations[i]), + ) + allocations[best] -= 1 + deficit -= 1 + + expected_ratios = [r / total_ratio for r in ratios] + actual_ratios = [a / world_size for a in allocations] + lines = [f"RankPartitionedDataLoader allocation ({world_size} GPUs):"] + start = 0 + for i, (name, alloc) in enumerate(zip(names, allocations)): + end = start + alloc - 1 + lines.append( + f" {name} (ratio {ratios[i]}): ranks {start}-{end} ({alloc} GPUs) " + f"| expected {expected_ratios[i]:.2%}, actual {actual_ratios[i]:.2%}" + ) + start += alloc + log.info("\n".join(lines), rank0_only=False) + + cumulative = 0 + my_dataset_idx = -1 + for i, alloc in enumerate(allocations): + if rank < cumulative + alloc: + my_dataset_idx = i + break + cumulative += alloc + assert my_dataset_idx >= 0 + + shard_rank = rank - cumulative + shard_world_size = allocations[my_dataset_idx] + + dataset: Any = instantiate(dataset_configs[my_dataset_idx]) + dataset.shard_world_size = shard_world_size + dataset.shard_rank = shard_rank + dataset.shard_id = my_dataset_idx + + merged_kwargs = {**dataloader_kwargs, **per_dataset_kwargs[my_dataset_idx]} + merged_kwargs.setdefault("collate_fn", custom_collate_fn) + self.dataloader = torch.utils.data.DataLoader(dataset, **merged_kwargs) + self.dataset_name = names[my_dataset_idx] + self.dataset = dataset + + def __iter__(self): + return iter(self.dataloader) + + def __len__(self) -> int: + return len(self.dataloader) + + +class PackingDataLoader(JointDataLoader): + """Packs multiple samples from a single dataloader into token-budget-constrained batches. + + Unlike the other ``JointDataLoader`` subclasses which manage multiple + dataloaders with configurable ratios, this class wraps a single dataloader + and greedily packs consecutive samples until the token budget + (``max_sequence_length``) or sample count limit (``max_samples_per_batch``) + is reached. + """ + + def __init__( + self, + dataloader: torch.utils.data.DataLoader | webdataset.WebLoader, + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + dataset_name: str = "default", + ): + """ + Args: + dataloader: A single dataloader (or lazy config) to draw samples from. + tokenizer_spatial_compression_factor: Spatial compression factor of the tokenizer. + tokenizer_temporal_compression_factor: Temporal compression factor of the tokenizer. + patch_spatial: Spatial patchification factor. + max_sequence_length: Max total tokens per packed batch. Mutually exclusive with + ``max_samples_per_batch``. + max_samples_per_batch: Max number of samples per packed batch. Mutually exclusive + with ``max_sequence_length``. + sound_latent_fps: Sound tokenizer latent rate in Hz. If 0, sound tokens are not counted. + audio_sample_rate: Audio sample rate in Hz. + dataset_name: Name tag attached to every sample in the output batch. + """ + wrapped = {dataset_name: {"dataloader": dataloader, "ratio": 1}} + super().__init__( + dataloaders=wrapped, + tokenizer_spatial_compression_factor=tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor=tokenizer_temporal_compression_factor, + patch_spatial=patch_spatial, + max_sequence_length=max_sequence_length, + max_samples_per_batch=max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + ) + + def __iter__(self): + inner = self.dataloader_list[0] + ds_name = getattr(inner, "dataset_name", self.dataset_name_list[0]) + + while True: + current_sequence_length = 0 + num_samples = 0 + output_batch: dict = {} + + skipped_samples: deque = deque() + lookahead_limit = 10 + lookahead_count = 0 + + while True: + if self.max_samples_per_batch is not None and num_samples >= self.max_samples_per_batch: + break + + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + try: + output = self._get_next_sample(0) + except StopIteration: + break + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.error( + f"PackingDataLoader: Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + continue + + skipped_samples.append(output) + lookahead_count += 1 + continue + + current_sequence_length += num_tokens_in_current_sample + num_samples += 1 + output["dataset_name"] = ds_name + self._update_output_batch(output_batch, output) + + for sample in reversed(skipped_samples): + self.buffers[0].appendleft(sample) + + if len(output_batch) == 0: + return + + self.global_id += 1 + yield output_batch + + +class RandomJointDataLoader(JointDataLoader): + r""" + A random joint dataloader that supports loading multiple modalities with stochastic sampling. + + In this dataloader, the modality is randomly selected at each iteration based on the + probability distribution derived from the ratios. Each rank independently samples a + modality, so different ranks may process different modalities at the same iteration. + + For example, with 2 modalities (image and video) and ratio 2:1: + - Each iteration has 66.7% probability of selecting images + - Each iteration has 33.3% probability of selecting videos + - The selection is independent across iterations and ranks + + Note: Unlike IterativeJointDataLoader, this does not guarantee synchronized modality + selection across ranks. + """ + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + ): + super().__init__( + dataloaders, + tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor, + patch_spatial, + max_sequence_length, + max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + ) + + # Convert data ratios to probabilities + self.data_ratios = np.array([ratio / sum(self.data_ratios) for ratio in self.data_ratios]) + + def __iter__(self): + while True: + index_id = np.random.choice(len(self.dataloader_list), p=self.data_ratios) + + metrics = _PackingMetrics() + output_batch = dict() + skipped_samples = deque() + lookahead_limit = 10 + lookahead_count = 0 + + while True: + # Check max samples limit first + if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch: + break + + # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop. + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + had_buffer = len(self.buffers[index_id]) > 0 + try: + output = self._get_next_sample(index_id) + except StopIteration: + break # No more data in this dataloader + + if had_buffer: + metrics.from_buffer += 1 + else: + metrics.from_workers += 1 + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.info( + f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + metrics.dropped_count += 1 + continue + + # current_sequence_length > 0 and selected sample is too large to fit in the remaining space. + # Instead of stopping immediately (creating large padding), we buffer this large sample + # and try to find a smaller one that fits in the remaining space. + skipped_samples.append(output) + lookahead_count += 1 + continue + + metrics.current_sequence_length += num_tokens_in_current_sample + metrics.num_samples += 1 + output["dataset_name"] = self.dataset_name_list[index_id] + self._update_output_batch(output_batch, output) + + # Add back skipped samples to the buffer for the next batch. + # appendleft puts item at HEAD. So we insert S3, then S2, then S1. + for sample in reversed(skipped_samples): + self.buffers[index_id].appendleft(sample) + + if len(output_batch) == 0: + return + + metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id])) + yield output_batch diff --git a/cosmos3/_src/vfm/datasets/local_datasets/__init__.py b/cosmos3/_src/vfm/datasets/local_datasets/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/local_datasets/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/datasets/local_datasets/helper.py b/cosmos3/_src/vfm/datasets/local_datasets/helper.py new file mode 100644 index 00000000..00cc5c59 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/local_datasets/helper.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared helpers for local datasets (S3, video decoding, aspect ratio).""" + +import io +import json +import subprocess +import time +from collections.abc import Generator +from pathlib import Path +from typing import Any + +import numpy as np +from boto3.s3.transfer import TransferConfig +from botocore.config import Config + +from cosmos3._src.imaginaire.utils import log + +client_config = Config( + response_checksum_validation="when_required", + request_checksum_calculation="when_required", + connect_timeout=10, + read_timeout=5, +) +transfer_config = TransferConfig(use_threads=True, max_concurrency=8, multipart_chunksize=8 * 1024 * 1024) + + +def parse_s3_url(s3_url: str) -> tuple[str, str]: + s3_url = s3_url.removeprefix("s3://") + bucket, key = s3_url.split("/", 1) + return bucket, key + + +def download_from_s3(s3_client: Any, s3_url: str, max_tries: int = 20) -> bytes | None: + """Download a file from S3.""" + if not s3_url.startswith("s3://"): + return Path(s3_url).read_bytes() + tries = 0 + while True: + tries += 1 + try: + bucket, key = parse_s3_url(s3_url) + buffer = io.BytesIO() + s3_client.download_fileobj(Bucket=bucket, Key=key, Fileobj=buffer, Config=transfer_config) + data = buffer.getvalue() + return data + except Exception as e: + log.error(f"Error downloading from S3 (try {tries}): {e}\n{s3_url}") + if tries >= max_tries: + return None + time.sleep(1) + + +def get_video_metadata(video_path: str) -> dict: + """ + Get video metadata using ffprobe. + + Args: + video_path: Path to the video file + + Returns: + Dictionary containing width, height, fps, and total_frames + """ + cmd = [ + "ffprobe", + "-v", + "quiet", + "-print_format", + "json", + "-show_streams", + "-select_streams", + "v:0", + video_path, + ] + result = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, check=True, text=True) + probe_data = json.loads(result.stdout) + + # Decode output + stream = probe_data["streams"][0] + width = int(stream["width"]) + height = int(stream["height"]) + fps_parts = stream["r_frame_rate"].split("/") + video_fps = float(fps_parts[0]) / float(fps_parts[1]) + if "nb_frames" in stream: + total_frames = int(stream["nb_frames"]) + else: + duration = float(stream.get("duration") or 0) + total_frames = int(duration * video_fps) + + return dict(width=width, height=height, fps=video_fps, total_frames=total_frames) + + +def ffmpeg_decode_video( + video_path: str, scale_hw: tuple[int, int] | None = None, num_threads: int = 1 +) -> Generator[np.ndarray, None, None]: + """ + Decode video frames using ffmpeg and yield HWC uint8 RGB frames. + + Args: + video_path: Path to the video file + scale_hw: Tuple of width and height to scale the video to (default: None) + + Yields: + np.ndarray: HWC uint8 RGB frames + """ + if scale_hw is None: + metadata = get_video_metadata(video_path) + out_width = metadata["width"] + out_height = metadata["height"] + else: + out_height, out_width = scale_hw + + # Calculate frame size in bytes + frame_size = out_width * out_height * 3 # 3 channels (RGB) + + # Build ffmpeg command to decode and output raw RGB frames + ffmpeg_cmd = [ + "ffmpeg", + "-loglevel", + "quiet", + "-threads", + str(num_threads), + "-filter_threads", + str(num_threads), + "-filter_complex_threads", + str(num_threads), + "-i", + video_path, + "-threads", + str(num_threads), + "-filter_threads", + str(num_threads), + "-filter_complex_threads", + str(num_threads), + "-pix_fmt", + "rgb24", + "-sws_flags", + "bicubic+accurate_rnd", # lanczos too much ringing on graphics + *(["-vf", f"scale={scale_hw[1]}:{scale_hw[0]}"] if scale_hw else []), # WH + "-f", + "rawvideo", + "-vsync", + "0", + "-", + ] + + process = subprocess.Popen( + ffmpeg_cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, # Set to None to print errors + bufsize=-1, + ) + + try: + while True: + raw_frame = process.stdout.read(frame_size) + + if len(raw_frame) != frame_size: + assert len(raw_frame) == 0, f"Incomplete frame: {len(raw_frame)} bytes" + break + + frame = np.frombuffer(raw_frame, dtype=np.uint8) + frame = frame.reshape((out_height, out_width, 3)) + + yield frame + finally: + process.stdout.close() + process.wait() + + +def get_aspect_ratio(width: int, height: int) -> str: + """Compute aspect ratio bucket from width and height.""" + ratio = width / height + + if ratio < 0.65: + return "9,16" # 0.5625 + elif ratio < 0.88: + return "3,4" # 0.75 + elif ratio < 1.16: + return "1,1" # 1.0 + elif ratio < 1.55: + return "4,3" # 1.3333 + else: + return "16,9" # 1.7778 + + +def save_video_frames_to_mp4( + frames: np.ndarray | Any, + output_path: str, + fps: float = 24.0, + overlay_frame_id: bool = False, + fps_to_show: float | None = None, +) -> None: + """Encode video frames to MP4 using FFmpeg. + + Args: + frames: Video frames as numpy (T, H, W, 3) or torch tensor (C, T, H, W), uint8. + output_path: Path for the output .mp4 file. + fps: Output video frame rate. + overlay_frame_id: If True, draw frame index (0, 1, ...) on each frame via FFmpeg drawtext. + fps_to_show: If provided, draw the FPS value on the video instead of the actual FPS. + """ + cpu_fn = getattr(frames, "cpu", None) + if callable(cpu_fn): + frames = cpu_fn().numpy() # type: ignore[union-attr] + frames = np.asarray(frames, dtype=np.uint8) + if frames.ndim == 4 and frames.shape[0] == 3: + # CTHW -> THWC + frames = np.transpose(frames, (1, 2, 3, 0)) + if frames.ndim != 4 or frames.shape[-1] != 3: + raise ValueError("frames must be (T, H, W, 3) or (C, T, H, W) uint8") + t, h, w, _ = frames.shape + cmd = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-s", + f"{w}x{h}", + "-r", + str(fps), + "-i", + "pipe:0", + ] + if overlay_frame_id: + # %{n} = frame index (0-based); add fps and resolution as literal text + drawtext_frame = "drawtext=text='%{n}':x=10:y=10:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + drawtext_fps = ( + f"drawtext=text='fps: {fps_to_show or fps}':x=10:y=40:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + ) + drawtext_res = f"drawtext=text='{w}x{h}':x=10:y=70:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + cmd += ["-vf", ",".join([drawtext_frame, drawtext_fps, drawtext_res])] + cmd += [ + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + output_path, + ] + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + _, stderr = process.communicate(input=frames.tobytes()) + if process.returncode != 0: + log.error(f"FFmpeg failed: {stderr.decode()}") + raise RuntimeError(f"FFmpeg exited with {process.returncode}") diff --git a/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py b/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py new file mode 100644 index 00000000..f328d4a9 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py @@ -0,0 +1,691 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# SFT dataset loader — reads video metadata + captions from a JSONL file on S3. +import gzip +import hashlib +import io +import json +import os +import random +import tempfile +from pathlib import Path +from typing import Any, Optional + +import boto3 +import numpy as np +import torch + +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.lazy_config import instantiate as lazy_instantiate +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.local_datasets.helper import ( + client_config, + download_from_s3, + ffmpeg_decode_video, + get_aspect_ratio, + get_video_metadata, + parse_s3_url, +) +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan, add_special_tokens +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import tokenize_caption + +_MAX_NUM_TOKENS = 1024 +_DURATION_TEMPLATE = "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS." +_RESOLUTION_TEMPLATE = "This video is of {height}x{width} resolution." + +# Caption types available in the SFT JSONL. +# Format: {model}_{style} +# model: qwen3_235b | qwen3_32b | qwen3p5_397b +# style: short | temporal | descriptive | dense +CAPTION_TYPES_AND_WEIGHTS: dict[str, float] = { + # short: 10% total + "qwen3_235b_short": 0.1, + "qwen3_32b_short": 0.1, + "qwen3p5_397b_short": 0.1, + # descriptive: 20% total + "qwen3_235b_descriptive": 0.2, + "qwen3_32b_descriptive": 0.2, + "qwen3p5_397b_descriptive": 0.2, + # dense: 70% total + "qwen3_235b_dense": 0.7, + "qwen3_32b_dense": 0.7, + "qwen3p5_397b_dense": 0.7, + # temporal: 0% total + "qwen3_235b_temporal": 0.0, + "qwen3_32b_temporal": 0.0, + "qwen3p5_397b_temporal": 0.0, +} +CAPTION_TYPES = list(CAPTION_TYPES_AND_WEIGHTS.keys()) +CAPTION_WEIGHTS = list(CAPTION_TYPES_AND_WEIGHTS.values()) + + +class SFTDataset(torch.utils.data.IterableDataset): + """Dataset for loading SFT video clips with captions from JSONL metadata on S3.""" + + def __init__( + self, + metadata: list[dict], + num_video_frames: int, + resolution: str, + s3_credentials: dict, + temporal_interval_mode: str = "entire_chunk", + frame_selection_mode: str = "center", + tokenizer_config: Optional[Any] = None, + cfg_dropout_rate: float = 0.0, + use_system_prompt: bool = False, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + cfg_dropout_keep_metadata: bool = False, + caption_suffix: str = "", + conditioning_fps: float = 24, + conditioning_fps_noise_std: float = 0.0, + conditioning_config: dict[int, float] | None = None, + temporal_compression_factor: int = 4, + ): + assert temporal_interval_mode in ("force_one", "max_30fps", "entire_chunk"), ( + f"Unknown temporal_interval_mode={temporal_interval_mode!r}" + ) + assert frame_selection_mode in ("center", "first", "random"), ( + f"Unknown frame_selection_mode={frame_selection_mode!r}" + ) + assert temporal_compression_factor >= 1, "temporal_compression_factor must be >= 1" + self.metadata = metadata + self.num_video_frames = num_video_frames + self.resolution = resolution + self.s3_credentials = s3_credentials + self.temporal_interval_mode = temporal_interval_mode + self.frame_selection_mode = frame_selection_mode + self.tokenizer_config = tokenizer_config + self.cfg_dropout_rate = cfg_dropout_rate + self.use_system_prompt = use_system_prompt + self.append_duration_fps_timestamps = append_duration_fps_timestamps + self.append_resolution_info = append_resolution_info + self.cfg_dropout_keep_metadata = cfg_dropout_keep_metadata + self.caption_suffix = caption_suffix.strip() + self.conditioning_fps = conditioning_fps + self.conditioning_fps_noise_std = conditioning_fps_noise_std + + self.temporal_compression_factor = temporal_compression_factor + self.conditioning_config: dict[int, float] | None = None + if conditioning_config is not None: + total_prob = sum(conditioning_config.values()) + assert total_prob > 0, "conditioning_config probabilities must sum to a positive number" + self.conditioning_config = {k: v / total_prob for k, v in conditioning_config.items()} + log.info(f"Conditioning config: {self.conditioning_config}") + # They will be set by the RankPartitionedDataLoader + self.shard_world_size = None + self.shard_rank = None + self.shard_id = 0 + self.is_initialized = False + self.output_sizes = VIDEO_RES_SIZE_INFO[resolution] + + self.vlm_tokenizer = lazy_instantiate(self.tokenizer_config) + self.vlm_tokenizer, _ = add_special_tokens(self.vlm_tokenizer) + + def __len__(self): + return len(self.metadata) + + def _tokenize_caption(self, caption: str) -> tuple[list[int], str]: + text_ids = tokenize_caption( + caption, + self.vlm_tokenizer, + is_video=True, + use_system_prompt=self.use_system_prompt, + ) + if len(text_ids) > _MAX_NUM_TOKENS: + log.warning(f"Text ids are too long, truncating: {len(text_ids)} > {_MAX_NUM_TOKENS}") + text_ids = text_ids[:_MAX_NUM_TOKENS] + return text_ids, caption + + def process_one_sample(self, metadata: dict) -> dict | None: + """Process a single SFT sample: download, decode, and prepare for training. + + A random t2w_window is picked from the video's list of windows each time. + """ + windows = metadata["t2w_windows"] + win_idx = random.randrange(len(windows)) + t2w_window = windows[win_idx] + window_start = t2w_window["start_frame"] + window_end = t2w_window["end_frame"] + + # Compute output resolution + input_w, input_h = metadata["width"], metadata["height"] + target_w, target_h = self.output_sizes[metadata["aspect_ratio"]] + resize_ratio = max(target_w / input_w, target_h / input_h) + resize_h, resize_w = (round(input_h * resize_ratio), round(input_w * resize_ratio)) + crop_y, crop_x = (round((resize_h - target_h) / 2), round((resize_w - target_w) / 2)) + + video_bytes = download_from_s3(self.s3_client, metadata["s3_path"]) + if video_bytes is None: + log.warning(f"Failed to download video from S3: {metadata['s3_path']}") + return None + + # Decode all frames to (T, H, W, 3) + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=True) as tmp_input: + tmp_input.write(video_bytes) + tmp_input.flush() + input_video_path = tmp_input.name + video_info = get_video_metadata(input_video_path) + original_fps = video_info["fps"] + total_frames = video_info["total_frames"] + + # Constrain to the t2w window + actual_end = min(window_end, total_frames - 1) + frames_in_window = actual_end - window_start + 1 + + if self.num_video_frames == -1: + # Native chunk mode: use start/end/interval directly from the window + temporal_interval = t2w_window["temporal_interval"] + start_frame = window_start + end_frame = actual_end + else: + if frames_in_window < self.num_video_frames: + log.warning( + f"Not enough frames in window: {metadata['uuid']}, " + f"frames_in_window: {frames_in_window}, required: {self.num_video_frames}" + ) + return None + + # Compute temporal interval + if self.temporal_interval_mode == "force_one": + temporal_interval = 1 + elif self.temporal_interval_mode == "max_30fps": + temporal_interval = max(1, int(original_fps / 30.0)) + elif self.temporal_interval_mode == "entire_chunk": + temporal_interval = frames_in_window // self.num_video_frames + temporal_interval = max(1, temporal_interval) + else: + raise ValueError(f"Unknown temporal_interval_mode: {self.temporal_interval_mode}") + + num_frames_before_downsample = (self.num_video_frames - 1) * temporal_interval + 1 + if self.frame_selection_mode == "first": + start_frame = window_start + elif self.frame_selection_mode == "center": + start_frame = window_start + (frames_in_window - num_frames_before_downsample) // 2 + elif self.frame_selection_mode == "random": + max_offset = frames_in_window - num_frames_before_downsample + start_frame = window_start + random.randint(0, max(0, max_offset)) + else: + raise ValueError(f"Unknown frame_selection_mode: {self.frame_selection_mode}") + end_frame = start_frame + num_frames_before_downsample - 1 + + fps = original_fps / temporal_interval + + video_chunk = [] + for idx, frame in enumerate( + ffmpeg_decode_video(input_video_path, scale_hw=(resize_h, resize_w), num_threads=2) + ): + if idx < start_frame: + continue + elif idx <= end_frame: + if (idx - start_frame) % temporal_interval == 0: + video_chunk.append(frame) + else: + break + + if not video_chunk: + log.warning( + f"No frames decoded for sample: {metadata['uuid']} " + f"(start={start_frame}, end={end_frame}, path={metadata['s3_path']})" + ) + return None + + video_chunk = np.stack(video_chunk, axis=0) # [T,H,W,3] + + # Truncate temporally to temporal_compression_factor * N + 1 + target_t = (video_chunk.shape[0] - 1) // self.temporal_compression_factor * self.temporal_compression_factor + 1 + + # Apply spatial center crop and temporal truncation + video_chunk = video_chunk[:target_t, crop_y : crop_y + target_h, crop_x : crop_x + target_w] # [T,H,W,3] + + # THWC -> CTHW + video_chunk = np.transpose(video_chunk, (3, 0, 1, 2)) # [3,T,H,W] + video = torch.from_numpy(np.ascontiguousarray(video_chunk)).to(torch.uint8) # [3,T,H,W] + padding_mask = torch.zeros((1, target_h, target_w), dtype=torch.float32) + # image_size: [target_h, target_w, orig_h, orig_w] in pixel space, for the model to crop the video + image_size = torch.tensor([target_h, target_w, target_h, target_w], dtype=torch.float32) + + available_types = [ct for ct in CAPTION_TYPES if ct in t2w_window] + if "qwen3_32b_rewrite-dense" in t2w_window: + caption_key = "qwen3_32b_rewrite-dense" + elif "caption" in t2w_window: + caption_key = "caption" + elif available_types: + available_weights = [CAPTION_TYPES_AND_WEIGHTS[ct] for ct in available_types] + caption_key = random.choices(available_types, weights=available_weights, k=1)[0] + else: + log.warning( + f"No known caption key found in t2w_window for sample {metadata['uuid']}. " + f"Keys: {list(t2w_window)}. Skipping sample." + ) + return None + caption = t2w_window[caption_key] + caption = caption.strip().rstrip(".") + "." + + num_decoded_frames = video.shape[1] + cond_fps = fps if self.conditioning_fps < 0 else self.conditioning_fps + if self.conditioning_fps_noise_std > 0: + noise_factor = np.exp(np.random.randn() * self.conditioning_fps_noise_std) + cond_fps = cond_fps * noise_factor + + if self.caption_suffix: + caption = (caption + " " + self.caption_suffix).strip() + + # CFG dropout: when cfg_dropout_keep_metadata is True, dropout fires + # before appending resolution/duration/FPS so that metadata text is + # preserved even under unconditional guidance. + if self.cfg_dropout_keep_metadata and self.cfg_dropout_rate > 0: + if random.random() < self.cfg_dropout_rate: + caption = "" + + if self.append_duration_fps_timestamps: + duration = num_decoded_frames / cond_fps + suffix = _DURATION_TEMPLATE.format(duration=duration, fps=cond_fps) + caption = caption + " " + suffix + if self.append_resolution_info: + suffix = _RESOLUTION_TEMPLATE.format(height=target_h, width=target_w) + caption = caption + " " + suffix + caption = caption.strip() + + if not self.cfg_dropout_keep_metadata and self.cfg_dropout_rate > 0: + if random.random() < self.cfg_dropout_rate: + caption = "" + text_ids, caption = self._tokenize_caption(caption) + + ret = dict( + __key__=f"{metadata['uuid']}_w{win_idx}", + __url__=metadata["s3_path"], + fps=original_fps, + n_orig_video_frames=total_frames, + chunk_index=win_idx, + frame_start=start_frame, + frame_end=end_frame, + num_frames=video.shape[1], + video=video, + num_multiplier=temporal_interval, + conditioning_fps=cond_fps, + padding_mask=padding_mask, + image_size=image_size, + ai_caption=caption, + sampled_caption_style=caption_key, + text_token_ids=torch.tensor(text_ids), + ) + + if self.conditioning_config is not None: + num_frames_pixel = video.shape[1] + t_latent = 1 + (num_frames_pixel - 1) // self.temporal_compression_factor + frames_options = list(self.conditioning_config.keys()) + weights = list(self.conditioning_config.values()) + num_cond = random.choices(frames_options, weights=weights, k=1)[0] + num_cond = min(num_cond, t_latent - 1) + ret["sequence_plan"] = SequencePlan( + has_text=True, + has_vision=True, + condition_frame_indexes_vision=list(range(num_cond)), + ) + + return ret + + def __iter__(self): + assert not self.is_initialized, "Dataset can only be initialized once." + assert len(self.metadata) > 0, "Did not find any data." + + self.s3_client = boto3.client( + "s3", + **self.s3_credentials, + config=client_config, + ) + # Ranks of the same pp/tp/cp group will have the same dp rank and thus share the same group id. + # zhao: Cosmos3 does not support TP/SP/CP + if self.shard_world_size is not None: + train_world_size = self.shard_world_size + train_rank = self.shard_rank + log.info(f"Using shard_world_size: {train_world_size} and shard_rank: {train_rank}", rank0_only=False) + else: + train_world_size = torch.distributed.get_world_size() + train_rank = torch.distributed.get_rank() + train_dp_rank = train_rank + train_num_dp_groups = train_world_size + train_dp_group_size = 1 + + # Get data worker rank. Each trainer have multiple dataloaders + worker_info = torch.utils.data.get_worker_info() + if worker_info is not None: + worker_rank = worker_info.id + total_data_ranks = worker_info.num_workers * train_num_dp_groups + data_rank = worker_rank + train_dp_rank * worker_info.num_workers + seed = worker_info.seed + else: + log.warning("No data worker info found. Using default worker rank and number of workers.", rank0_only=False) + total_data_ranks = train_num_dp_groups + data_rank = train_dp_rank + seed = 42 + + log.info( + f"train_world_size: {train_world_size}; " + f"train_rank: {train_rank}; " + f"train_dp_rank: {train_dp_rank}; " + f"train_num_dp_groups: {train_num_dp_groups}; " + f"train_dp_group_size: {train_dp_group_size}; " + f"worker_info: {worker_info}; " + f"total_data_ranks: {total_data_ranks}; " + f"data_rank: {data_rank}; " + f"seed: {seed}" + f"shard_id: {self.shard_id}; " + f"shard_world_size: {self.shard_world_size}; " + f"shard_rank: {self.shard_rank}", + rank0_only=False, + ) + + # Make sure len(self.metadata) is divisible by self.num_groups + multiplier = max(1, total_data_ranks * 50 // len(self.metadata)) + log.info(f"Dataset multiplier: {multiplier}", rank0_only=False) + self.metadata = self.metadata * multiplier # reduce bias caused by sharding + num_pad = total_data_ranks - len(self.metadata) % total_data_ranks + self.metadata = self.metadata + self.metadata[:num_pad] + # Deterministic shuffle based on the sha256 hash of uuid + # Note that the repeated samples are grouped together. + # Split list to keep only the data for this rank + if True: # This gives more diversity + random.Random(self.shard_id).shuffle(self.metadata) + log.info(f"Shuffled metadata for shard {self.shard_id}", rank0_only=False) + self.metadata = self.metadata[data_rank::total_data_ranks] + else: + # Keep the repeated samples together to aid cache hits. + self.metadata.sort(key=lambda x: hashlib.sha256(x["s3_path"].encode("utf-8")).hexdigest()) + # Equally chunk the list (guaranteed to be divisible by total_data_ranks) + chunk_size = len(self.metadata) // total_data_ranks + start = data_rank * chunk_size + end = (data_rank + 1) * chunk_size + log.info( + f"DRank {data_rank} has got a chunk {start}-{end} from {len(self.metadata)} data.", rank0_only=False + ) + self.metadata = self.metadata[start:end] + num_unique_s3_paths = len(set(metadata["s3_path"] for metadata in self.metadata)) + log.info( + f"DRank {data_rank} has {len(self.metadata)} data with {num_unique_s3_paths} unique s3_paths.", + rank0_only=False, + ) + + self.is_initialized = True + + # Make sure the data within a DRank is identical + rng = random.Random(data_rank + self.shard_id * 12345) + while True: + rng.shuffle(self.metadata) + for metadata in self.metadata: + sample = self.process_one_sample(metadata) + if sample is None: + log.warning(f"Failed to process sample {metadata['uuid']}, skipping...") + continue + yield sample + + +def _flatten_metadata_by_window(metadata_list: list[dict]) -> list[dict]: + """Expand metadata so each entry maps to exactly one t2w_window. + + Each output dict is a shallow copy of the original whose ``t2w_windows`` + list contains a single window. The ``uuid`` is suffixed with ``_w{idx}`` + so every entry has a unique identifier. + """ + flat: list[dict] = [] + for entry in metadata_list: + for win_idx, window in enumerate(entry["t2w_windows"]): + flat.append( + { + **entry, + "uuid": f"{entry['uuid']}_w{win_idx}", + "t2w_windows": [window], + } + ) + return flat + + +def _load_sft_metadata_from_s3( + s3_client, + jsonl_url: str, + min_frames: int, + uuid_prefix: str = "", + min_short_edge: int = 0, +) -> list[dict]: + """Load SFT metadata from a single JSONL file on S3. + + Returns one entry per video. Each entry keeps only the windows whose frame + span is at least *min_frames*; videos with no qualifying windows are dropped. + + Args: + s3_client: Boto3 S3 client + jsonl_url: S3 URL to the JSONL metadata file + min_frames: Minimum number of frames required per window + uuid_prefix: Prefix prepended to each uuid for disambiguation when + multiple JSONL files are loaded + min_short_edge: Drop videos whose shortest spatial edge (min of width, + height) is below this value. 0 disables the filter. + """ + log.info(f"Downloading SFT metadata from {jsonl_url}", rank0_only=False) + metadata_list: list[dict] = [] + num_raw_records = 0 + num_raw_windows = 0 + num_filtered_duration = 0 + num_filtered_windows = 0 + num_filtered_short_edge = 0 + + with io.BytesIO() as buffer: + if jsonl_url.startswith("s3://"): + bucket, key = parse_s3_url(jsonl_url) + s3_client.download_fileobj(Bucket=bucket, Key=key, Fileobj=buffer) + else: + path = Path(jsonl_url).absolute() + jsonl_url = str(path) + buffer.write(path.read_bytes()) + buffer.seek(0) + log.info("Finished downloading. Decoding...", rank0_only=False) + + line_iter = gzip.open(buffer, "rb") if jsonl_url.endswith(".gz") else buffer + for line in line_iter: + num_raw_records += 1 + record = json.loads(line.decode("utf-8")) + uuid = f"{uuid_prefix}{record['uuid']}" if uuid_prefix else record["uuid"] + if record["duration"] > 61.0: + print(f"Skipping video with too long duration: {uuid}, {record['duration']} > 61.0") + num_filtered_duration += 1 + continue + if min_short_edge > 0 and min(record["width"], record["height"]) < min_short_edge: + num_filtered_short_edge += 1 + continue + + windows = record.get("t2w_windows") + if not windows: + continue + + kept_windows = [] + for window in windows: + num_raw_windows += 1 + frames_in_window = window["end_frame"] - window["start_frame"] + 1 + if frames_in_window < min_frames: + num_filtered_windows += 1 + else: + kept_windows.append(window) + + if not kept_windows: + continue + + s3_path = record["s3_path"] + if "://" not in s3_path and not s3_path.startswith("/"): + # Relative path to the JSONL file + s3_path = f"{os.path.dirname(jsonl_url)}/{s3_path}" + + aspect_ratio = get_aspect_ratio(record["width"], record["height"]) + metadata_list.append( + { + "uuid": uuid, + "s3_path": s3_path, + "width": record["width"], + "height": record["height"], + "nb_frames": record.get("nb_frames"), + "framerate": record.get("framerate"), + "aspect_ratio": aspect_ratio, + "t2w_windows": kept_windows, + } + ) + + log.info( + f"Finished decoding SFT metadata from {jsonl_url}. " + f"Records: {num_raw_records}, " + f"Duration > 61s: {num_filtered_duration}, " + f"Short edge < {min_short_edge}: {num_filtered_short_edge}, " + f"Windows: {num_raw_windows}, Windows < {min_frames}f: {num_filtered_windows}, " + f"Videos kept: {len(metadata_list)}" + ) + return metadata_list + + +def get_sft_dataset( + jsonl_paths: str | list[str] = "s3://nv-00-10206-vfm/cosmos3_video_sft/human_1k/captions_full.jsonl", + resolution: str = "720", + num_video_frames: int = 93, + temporal_interval_mode: str = "entire_chunk", + frame_selection_mode: str = "center", + tokenizer_config: Optional[Any] = None, + cfg_dropout_rate: float = 0.1, + use_system_prompt: bool = False, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + cfg_dropout_keep_metadata: bool = False, + sample_by_window: bool = False, + min_short_edge: int = 0, + caption_suffix: str = "", + conditioning_fps: float = 24, + conditioning_fps_noise_std: float = 0.0, + conditioning_config: dict[int, float] | None = None, + temporal_compression_factor: int = 4, + **kwargs, +) -> SFTDataset: + """Create SFT video dataset from one or more JSONL files on S3. + + Args: + jsonl_paths: S3 path(s) to JSONL metadata file(s). A single string or + a list of strings. When multiple files are given, their samples are + concatenated and each file's uuids are prefixed with ``/`` + to avoid collisions. + resolution: Output resolution (e.g., "720", "480") + num_video_frames: Number of frames to extract from each video. + Videos with fewer frames are skipped at decode time. + Use -1 to take native chunks from the t2w_window metadata. + temporal_interval_mode: How to compute the temporal interval between sampled frames. + "force_one" — always 1 (consecutive frames at original fps). + "max_30fps" — smallest interval that keeps effective fps <= 30. + "entire_chunk" — spread num_video_frames evenly across the whole window. + frame_selection_mode: Where to select frames within the window. + "center" — center-crop temporally (default). + "first" — take the first num_video_frames from the window start. + tokenizer_config: Config for the tokenizer + cfg_dropout_rate: Dropout rate for the caption + use_system_prompt: Whether to use the system prompt during tokenization + append_duration_fps_timestamps: If True, appends duration/FPS text to captions + append_resolution_info: If True, appends resolution text to captions + cfg_dropout_keep_metadata: If True, CFG dropout fires before appending + duration/FPS/resolution text so that metadata is preserved during + unconditional guidance. If False (default), dropout fires after + and clears the entire caption including metadata. + sample_by_window: If True, each t2w_window is treated as a separate + sample (the dataset length equals the total number of windows). + If False (default), each video uuid is one sample and a random + window is chosen on every access. + min_short_edge: Drop videos whose shortest spatial edge (min of width, + height) is below this value. 0 (default) disables the filter. + caption_suffix: Text appended to every caption before the + duration/FPS/resolution templates, e.g. + ``"Overall, the video is of poor quality."``. Empty string + (default) disables the suffix. + conditioning_fps: FPS value used for duration/FPS conditioning. + A positive value is used directly (default 24). A negative + value (e.g. ``-1``) means the actual effective FPS + (``original_fps / temporal_interval``) is used instead. + conditioning_fps_noise_std: Standard deviation of log-normal + multiplicative noise applied to ``conditioning_fps``. The FPS + is multiplied by ``exp(N(0, std))``. 0.0 (default) disables + the noise. + conditioning_config: Weighted distribution mapping latent-frame counts + to unnormalized probabilities for image-to-video conditioning. + Example: ``{0: 0.7, 1: 0.2, 2: 0.1}``. ``None`` disables + conditioning (all frames are generation targets). + temporal_compression_factor: VAE temporal compression factor used to + convert pixel frame count to latent frame count. + Returns: + SFTDataset instance + """ + log.info(f"Unknown kwargs for get_sft_dataset: {kwargs}") + assert resolution in VIDEO_RES_SIZE_INFO.keys(), "The provided resolution cannot be found in VIDEO_RES_SIZE_INFO." + + if isinstance(jsonl_paths, str): + jsonl_paths = [jsonl_paths] + + if INTERNAL: + with open("credentials/gcs.secret", "r") as f: + credentials = json.load(f) + else: + credentials = {} + + s3_client = boto3.client("s3", **credentials) + + metadata_list: list[dict] = [] + for idx, jsonl_url in enumerate(jsonl_paths): + prefix = f"{idx}/" if len(jsonl_paths) > 1 else "" + metadata_list.extend( + _load_sft_metadata_from_s3( + s3_client, + jsonl_url, + min_frames=61, + uuid_prefix=prefix, + min_short_edge=min_short_edge, + ) + ) + + total_windows = sum(len(m["t2w_windows"]) for m in metadata_list) + log.info( + f"Finished loading metadata from {len(jsonl_paths)} file(s). " + f"Total videos: {len(metadata_list)}, total windows: {total_windows}" + ) + + if sample_by_window: + metadata_list = _flatten_metadata_by_window(metadata_list) + log.info(f"sample_by_window=True: flattened to {len(metadata_list)} samples (one per window)") + + # Deterministic shuffle based on the sha256 hash of uuid + metadata_list.sort(key=lambda x: hashlib.sha256(x["uuid"].encode("utf-8")).hexdigest()) + + dataset = SFTDataset( + metadata=metadata_list, + num_video_frames=num_video_frames, + resolution=resolution, + s3_credentials=credentials, + temporal_interval_mode=temporal_interval_mode, + frame_selection_mode=frame_selection_mode, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + use_system_prompt=use_system_prompt, + append_duration_fps_timestamps=append_duration_fps_timestamps, + append_resolution_info=append_resolution_info, + cfg_dropout_keep_metadata=cfg_dropout_keep_metadata, + caption_suffix=caption_suffix, + conditioning_fps=conditioning_fps, + conditioning_fps_noise_std=conditioning_fps_noise_std, + conditioning_config=conditioning_config, + temporal_compression_factor=temporal_compression_factor, + ) + return dataset diff --git a/cosmos3/_src/vfm/datasets/sequence_packing.py b/cosmos3/_src/vfm/datasets/sequence_packing.py new file mode 100644 index 00000000..db6d5940 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/sequence_packing.py @@ -0,0 +1,2987 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for implementing sequence packing with flexible attention modes. + +This module provides utilities for packing text and image sequences together +with support for different attention patterns (causal, full, noise). + +Key Components: +--------------- +1. Attention Mask Creation: + - create_sparse_mask(): Creates sparse masks for flex attention + - prepare_attention_mask_per_sample(): Creates dense attention masks + +2. Position ID Generation: + - get_flattened_position_ids_extrapolate(): Extrapolation-based position encoding + - get_flattened_position_ids_interpolate(): Interpolation-based position encoding + +3. Tokenizer Setup: + - add_special_tokens(): Adds image boundary tokens to tokenizer + +4. Sequence Packing: + - pack_input_sequence(): Main function for packing text and image sequences + - Helper functions: _pack_text_tokens(), _pack_image_tokens(), _finalize_packed_data() + +Sequence Format: +--------------- +Each sample consists of alternating text and image sections: + [text_tokens] [image_tokens] ... + +Attention Modes: +--------------- +- 'causal': Standard causal/autoregressive attention for text +- 'full': Bidirectional attention for images +- 'noise': Special mode for noise conditioning +""" + +import math +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Dict, List, Tuple + +import torch +from torch.nn.attention.flex_attention import and_masks, or_masks + +from cosmos3._src.imaginaire.attention.checks import check_valid_tuple_or_element +from cosmos3._src.imaginaire.attention.varlen import generate_multi_dim_varlen_parameters +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.models.mot.unified_3dmrope_utils import ( + get_3d_mrope_ids_text_tokens, + get_3d_mrope_ids_vae_tokens, +) +from cosmos3._src.vfm.models.utils.data_and_condition import GenerationDataClean +from cosmos3._src.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer + +MAX_CAUSAL_LEN_IMAGE_BATCH = 0 +MAX_FULL_LEN_IMAGE_BATCH = 0 +MAX_CAUSAL_LEN_VIDEO_BATCH = 0 +MAX_FULL_LEN_VIDEO_BATCH = 0 + + +# ============================================================================ +# Attention mask creation +# ============================================================================ + + +def create_sparse_mask(document_lens, split_lens, attn_modes, device): + """Create a sparse attention mask combining multiple attention patterns. + + Args: + document_lens: List of document lengths + split_lens: List of split lengths within documents + attn_modes: List of attention modes ('causal', 'full', 'noise') for each split + device: Device to place tensors on + + Returns: + Combined mask using flex attention API + """ + + # Build sequence ID tensors for tracking full/noise attention regions + full_and_noise_seq_ids = [] + noise_seq_ids = [] + + for seq_idx, (length, attn_mode) in enumerate(zip(split_lens, attn_modes)): + # Assign sequence ID for full/noise regions, -1 for causal regions + seq_id = seq_idx if attn_mode in ["full", "noise"] else -1 + full_and_noise_seq_ids.extend([seq_id] * length) + + # Assign sequence ID only for noise regions + noise_seq_id = seq_idx if attn_mode == "noise" else -1 + noise_seq_ids.extend([noise_seq_id] * length) + + full_and_noise_seq_id = torch.tensor(full_and_noise_seq_ids, device=device) # [seq_len] + noise_seq_id = torch.tensor(noise_seq_ids, device=device) # [seq_len] + document_id = torch.cat([torch.full((l,), i) for i, l in enumerate(document_lens, start=1)]).to(device) # [seq_len] + + # Define component mask functions + def causal_mask(b, h, q_idx, kv_idx): + """Standard causal attention: query can only attend to prior keys.""" + return q_idx >= kv_idx + + def full_and_noise_mask(b, h, q_idx, kv_idx): + """Allow attention within same full/noise sequence.""" + return (full_and_noise_seq_id[q_idx] == full_and_noise_seq_id[kv_idx]) & (full_and_noise_seq_id[q_idx] >= 0) + + def remove_noise_mask(b, h, q_idx, kv_idx): + """Prevent attending to noise tokens from different sequences.""" + return ~((noise_seq_id[kv_idx] >= 0) & (noise_seq_id[q_idx] != noise_seq_id[kv_idx])) + + def sample_mask(b, h, q_idx, kv_idx): + """Ensure attention stays within same document/sample.""" + return document_id[q_idx] == document_id[kv_idx] + + # Combine all masks: (causal OR full_and_noise) AND remove_noise AND sample + return and_masks(or_masks(causal_mask, full_and_noise_mask), remove_noise_mask, sample_mask) + + +def prepare_attention_mask_per_sample(split_lens, attn_modes, device="cpu"): + """Prepare dense attention mask for a single sample with multiple splits. + + Args: + split_lens: List of integers indicating length of each split within the sample + attn_modes: List of attention modes for each split ('causal', 'full', or 'noise') + device: Device to place the attention mask tensor on + + Returns: + Attention mask tensor of shape (sample_len, sample_len) with -inf for masked positions + """ + sample_len = sum(split_lens) + attention_mask = torch.zeros((sample_len, sample_len), dtype=torch.bool, device=device) # [sample_len,sample_len] + + # First pass: Set up basic attention patterns for each split + current_pos = 0 + for split_len, attn_mode in zip(split_lens, attn_modes): + assert attn_mode in ["causal", "full", "noise"], f"Invalid attention mode: {attn_mode}" + + split_start = current_pos + split_end = current_pos + split_len + + if attn_mode == "causal": + # Causal: lower triangular within split + full attention to previous splits + attention_mask[split_start:split_end, split_start:split_end] = torch.ones( + (split_len, split_len), device=device + ).tril() # [split_len,split_len] + attention_mask[split_start:split_end, :split_start] = 1 + else: # "full" or "noise" + # Full attention within split and to previous splits + attention_mask[split_start:split_end, split_start:split_end] = torch.ones( + (split_len, split_len), device=device + ) # [split_len,split_len] + attention_mask[split_start:split_end, :split_start] = 1 + + current_pos += split_len + + # Second pass: Handle noise mode - mask out noise columns except within same split + current_pos = 0 + for split_len, attn_mode in zip(split_lens, attn_modes): + if attn_mode == "noise": + split_start = current_pos + split_end = current_pos + split_len + + # Zero out the entire column for noise tokens + attention_mask[:, split_start:split_end] = 0 + # But allow self-attention within the noise split + attention_mask[split_start:split_end, split_start:split_end] = 1 + + current_pos += split_len + + # Convert boolean mask to float with -inf for masked positions + attention_mask = torch.zeros_like(attention_mask, dtype=torch.float).masked_fill_( + ~attention_mask, float("-inf") + ) # [sample_len,sample_len] + + return attention_mask + + +# ============================================================================ +# Tokenizer utilities +# ============================================================================ + + +def add_special_tokens(tokenizer): + """Add image-related special tokens to tokenizer if not already present. + + Args: + tokenizer: Tokenizer to add special tokens to + + Returns: + Tuple of (modified tokenizer, dict of new token IDs) + """ + # Collect existing special tokens + existing_special_tokens = [] + for key, value in tokenizer.special_tokens_map.items(): + if isinstance(value, str): + existing_special_tokens.append(value) + elif isinstance(value, list): + existing_special_tokens.extend(value) + + # Define image boundary tokens to add if missing + tokens_to_add = [] + if "<|vision_start|>" not in existing_special_tokens: + tokens_to_add.append("<|vision_start|>") + if "<|vision_end|>" not in existing_special_tokens: + tokens_to_add.append("<|vision_end|>") + + # Add new tokens to tokenizer vocabulary + if tokens_to_add: + tokenizer.add_tokens(tokens_to_add) + + # Get token IDs for image boundary tokens + new_token_ids = { + "start_of_generation": tokenizer.convert_tokens_to_ids("<|vision_start|>"), + "end_of_generation": tokenizer.convert_tokens_to_ids("<|vision_end|>"), + } + + return tokenizer, new_token_ids + + +# ============================================================================ +# Data structures +# ============================================================================ + + +@dataclass +class ModalityData: + """Unified container for a single generation modality's data. + + This dataclass serves dual purposes: + 1. During packing: Acts as a builder, accumulating data in lists + 2. After finalize(): Holds finalized tensors ready for model consumption + + Attributes: + sequence_indexes: Indices in the packed sequence where this modality's tokens appear. + List during building, Tensor after finalize(). + timesteps: Diffusion timesteps for each noised token. + List during building, Tensor after finalize(). + mse_loss_indexes: Indices where MSE loss should be computed (noised tokens only). + List during building, Tensor after finalize(). + token_shapes: Shape metadata for each sample's tokens. + For vision: list of (T, H, W) tuples. + For action: list of (T,) tuples. + tokens: The actual latent tokens. List during build, Tensor after finalize(). + condition_mask: Mask indicating clean frames (1=clean, 0=noised). Only after finalize(). + noisy_frame_indexes: Indices of noised frames. Constructed from condition_mask during + sequence packing to reduce GPU->CPU synchronization later. Only after finalize(). + domain_id: Domain ID for multi-domain training. Only after finalize(). NOTE: only used for action modality. + raw_action_dim: Raw action dimension. Only after finalize(). NOTE: only used for action modality. + """ + + # Core tracking (list during build, tensor after finalize) + sequence_indexes: list[int] | torch.Tensor = field(default_factory=list) + timesteps: list[float] | torch.Tensor = field(default_factory=list) + mse_loss_indexes: list[int] | torch.Tensor = field(default_factory=list) + # list[tuple[int,int,int]] for vision, list[tuple[int]] for action, list[tuple[int,int,int]] for sound + token_shapes: list = field(default_factory=list) + + # Populated during finalization (from GenerationDataClean / noise path) + tokens: list[torch.Tensor] = field(default_factory=list) + condition_mask: list[torch.Tensor] = field(default_factory=list) + noisy_frame_indexes: list[torch.Tensor] = field(default_factory=list) + domain_id: list[torch.Tensor] = field(default_factory=list) + raw_action_dim: list[torch.Tensor | None] | None = field(default_factory=list) + + def to_cuda(self) -> None: + """Move all tensor fields to CUDA in-place.""" + if isinstance(self.sequence_indexes, torch.Tensor): + self.sequence_indexes = self.sequence_indexes.cuda() + if isinstance(self.timesteps, torch.Tensor): + self.timesteps = self.timesteps.cuda() + if isinstance(self.mse_loss_indexes, torch.Tensor): + self.mse_loss_indexes = self.mse_loss_indexes.cuda() + self.tokens = [token.cuda() for token in self.tokens] + self.condition_mask = [cm.cuda() for cm in self.condition_mask] + self.noisy_frame_indexes = [ni.cuda() for ni in self.noisy_frame_indexes] + self.domain_id = [d.cuda() for d in self.domain_id] + # raw_action_dim is optional (e.g., when action-channel masking is disabled). + if self.raw_action_dim is not None: + self.raw_action_dim = [d.cuda() if d is not None else None for d in self.raw_action_dim] + + +@dataclass +class PackedSequence: + """Unified sequence container - works as builder during packing and final output. + + This dataclass replaces the old SequenceStatus + PackedSequence pattern: + - Build phase: Accumulate data using lists, modalities use ModalityData builders + - After finalize(): Ready for model consumption with tensors + + Attributes: + # Sequence structure + sample_lens: Length of each sample in the packed sequence. + split_lens: Length of each split (text/vision/action sections). + attn_modes: Attention mode for each split ('causal', 'full'). + is_image_batch: Whether this batch contains images (vs videos). + sequence_length: Total length of packed sequence. Computed during finalize(). + + # Build-time tracking (not used after finalize) + curr: Current position in the packed sequence during building. + + # Text modality (list during build, tensor after finalize) + text_ids: All text token IDs (including special tokens). + text_indexes: Indices where text tokens appear in sequence. + position_ids: RoPE position IDs for all tokens. + + # Loss computation - Cross Entropy (text) + label_ids: Label IDs for cross-entropy loss. + ce_loss_indexes: Indices for computing cross-entropy loss. + ce_loss_weights: Weights for cross-entropy loss. + + # Generation modalities - named fields for type safety + vision: Vision modality data (images/videos). None if no vision in batch. + action: Action modality data (robotics). None if no actions in batch. + sound: Sound modality data (audio). None if no sound in batch. + """ + + # Sequence structure + sample_lens: list[int] = field(default_factory=list) + split_lens: list[int] = field(default_factory=list) + attn_modes: list[str] = field(default_factory=list) + is_image_batch: bool = False + sequence_length: int = 0 + + # Build-time tracking (used during packing, not after finalize) + curr: int = 0 + + # Text modality (list during build, tensor after finalize) + text_ids: list[int] | torch.Tensor = field(default_factory=list) + text_indexes: list[int] | torch.Tensor = field(default_factory=list) + position_ids: list[int] | torch.Tensor = field(default_factory=list) + + # Loss computation - Cross Entropy (text) + label_ids: list[int] | torch.Tensor | None = field(default_factory=list) + ce_loss_indexes: list[int] | torch.Tensor | None = field(default_factory=list) + ce_loss_weights: list[float] | torch.Tensor | None = field(default_factory=list) + + # Build-time mRoPE tracking (used during packing, not after finalize) + # When _use_mrope=True, position_ids accumulates (3, N) tensors instead of ints, + # and finalize() produces a (3, total_seq_len) tensor instead of (total_seq_len,). + _use_mrope: bool = False + # Running temporal index for mRoPE position ID generation within a single sample. + # Reset to 0 at the start of each sample, then advanced by text and vision helpers + # as segments are packed. Action reuses the pre-vision snapshot (parallel temporal + # range) without advancing it. Float when FPS modulation is enabled. + # E.g. offset=0 -> text(4 tokens) -> offset=4 -> vision(3 frames) -> offset=7. + _mrope_temporal_offset: int | float = 0 + _mrope_reset_spatial: bool = True + + # Temporal causal: whether supertoken 0's action slot contains null tokens. + # True for all training calls and AR frame 0; False for AR frame N>0 (real actions). + # Used by three_way_attention to zero out V for null action tokens (inline when attention_meta.null_action_supertokens=True). + null_action_supertokens: bool = False + + # Temporal causal: number of action tokens prefixing each vision supertoken. + # Equals temporal_compression_factor when actions are packed inline; 0 when + # action_gen=False or for non-temporal-causal layouts. Single source of truth + # for downstream attention/KV-cache code (per-supertoken layout is + # num_action_tokens_per_supertoken + H_p * W_p). + num_action_tokens_per_supertoken: int = 0 + + # Generation modalities - NAMED FIELDS for type safety + vision: ModalityData | None = None + action: ModalityData | None = None + sound: ModalityData | None = None + + def finalize( + self, + gen_data_clean: GenerationDataClean, + ) -> "PackedSequence": + """Convert all lists to tensors and compute derived values. + + Args: + gen_data_clean: GenerationDataClean for metadata (e.g., action domain IDs). + + Returns: + New PackedSequence instance with tensors instead of lists. + """ + # Compute sequence length + sequence_length = sum(self.sample_lens) + sample_lens = self.sample_lens.copy() + split_lens = self.split_lens.copy() + attn_modes = self.attn_modes.copy() + + # Prepare loss-related tensors (cross-entropy) + label_ids: torch.Tensor | None = None + ce_loss_indexes: torch.Tensor | None = None + ce_loss_weights: torch.Tensor | None = None + if self.label_ids and len(self.label_ids) > 0: + label_ids = torch.tensor(self.label_ids) # [N_ce_tokens] + ce_loss_indexes = torch.tensor(self.ce_loss_indexes) # [N_ce_tokens] + ce_loss_weights = torch.tensor(self.ce_loss_weights) # [N_ce_tokens] + + # The condition_mask and noisy_frame_indexes are kept as lists to support variable shapes. + + # Finalize vision modality + vision: ModalityData | None = None + if self.vision is not None and len(self.vision.sequence_indexes) > 0: + vision = ModalityData( + sequence_indexes=torch.tensor(self.vision.sequence_indexes, dtype=torch.long), # [N_vision_tokens] + timesteps=torch.tensor(self.vision.timesteps), # [N_vision_noisy_tokens] + mse_loss_indexes=torch.tensor( + self.vision.mse_loss_indexes, dtype=torch.long + ), # [N_vision_noisy_tokens] + token_shapes=list(self.vision.token_shapes), + tokens=self.vision.tokens, + condition_mask=list(self.vision.condition_mask), + noisy_frame_indexes=list(self.vision.noisy_frame_indexes), + ) + + # Finalize action modality + action: ModalityData | None = None + if self.action is not None and len(self.action.sequence_indexes) > 0: + action = ModalityData( + sequence_indexes=torch.tensor(self.action.sequence_indexes, dtype=torch.long), # [N_action_tokens] + timesteps=torch.tensor(self.action.timesteps), # [N_action_noisy_tokens] + mse_loss_indexes=torch.tensor( + self.action.mse_loss_indexes, dtype=torch.long + ), # [N_action_noisy_tokens] + token_shapes=list(self.action.token_shapes), + tokens=self.action.tokens, + condition_mask=list(self.action.condition_mask), # Keep as list to support variable shapes + noisy_frame_indexes=list(self.action.noisy_frame_indexes), + domain_id=( + gen_data_clean.action_domain_id + if gen_data_clean.action_domain_id is not None + else [torch.zeros(1, dtype=torch.long)] * len(self.action.token_shapes) + ), + raw_action_dim=gen_data_clean.raw_action_dim, + ) + + # Finalize sound modality (placeholder for future) + sound: ModalityData | None = None + if self.sound is not None and len(self.sound.sequence_indexes) > 0: + sound = ModalityData( + sequence_indexes=torch.tensor(self.sound.sequence_indexes, dtype=torch.long), # [N_sound_tokens] + timesteps=torch.tensor(self.sound.timesteps), # [N_sound_noisy_tokens] + mse_loss_indexes=torch.tensor(self.sound.mse_loss_indexes, dtype=torch.long), # [N_sound_noisy_tokens] + token_shapes=list(self.sound.token_shapes), + tokens=self.sound.tokens, + condition_mask=list(self.sound.condition_mask), + noisy_frame_indexes=list(self.sound.noisy_frame_indexes), + ) + + # Finalize position IDs: 3D mRoPE (3, seq_len) or 1D RoPE (seq_len,) + if self._use_mrope and len(self.position_ids) > 0 and isinstance(self.position_ids[0], torch.Tensor): + mrope_tensors: list[torch.Tensor] = self.position_ids # type: ignore[assignment] + position_ids = torch.cat(mrope_tensors, dim=1) # [3,actual_seq_len] + else: # Original 1D RoPE from Bagel, where all the media tokens share the same 1D position ID + position_ids = torch.tensor(self.position_ids) # [seq_len] + + return PackedSequence( + # Sequence structure + sequence_length=sequence_length, + sample_lens=sample_lens, + split_lens=split_lens, + attn_modes=attn_modes, + is_image_batch=gen_data_clean.is_image_batch, + # Text modality (converted to tensors) + text_ids=torch.tensor(self.text_ids, dtype=torch.long), # [N_text_tokens] + text_indexes=torch.tensor(self.text_indexes, dtype=torch.long), # [N_text_tokens] + position_ids=position_ids, # [seq_len] or [3,seq_len] + # Loss computation - Cross Entropy + label_ids=label_ids, + ce_loss_indexes=ce_loss_indexes, + ce_loss_weights=ce_loss_weights, + # Generation modalities + vision=vision, + action=action, + sound=sound, + # Temporal causal + null_action_supertokens=self.null_action_supertokens, + num_action_tokens_per_supertoken=self.num_action_tokens_per_supertoken, + ) + + def to_cuda(self) -> None: + """Move all tensor fields to CUDA in-place.""" + if isinstance(self.text_ids, torch.Tensor): + self.text_ids = self.text_ids.cuda() + if isinstance(self.text_indexes, torch.Tensor): + self.text_indexes = self.text_indexes.cuda() + if isinstance(self.position_ids, torch.Tensor): + self.position_ids = self.position_ids.cuda() + if isinstance(self.label_ids, torch.Tensor): + self.label_ids = self.label_ids.cuda() + if isinstance(self.ce_loss_indexes, torch.Tensor): + self.ce_loss_indexes = self.ce_loss_indexes.cuda() + if isinstance(self.ce_loss_weights, torch.Tensor): + self.ce_loss_weights = self.ce_loss_weights.cuda() + if self.vision is not None: + self.vision.to_cuda() + if self.action is not None: + self.action.to_cuda() + if self.sound is not None: + self.sound.to_cuda() + + +@dataclass +class SequencePlan: + """Plan describing which modalities are present in a sample. + + This dataclass tracks the presence of different modalities (text, vision, action) + and their conditioning configurations for a dataset sample. Unlike SequencePlan + which holds the actual tensor data, this class provides a lightweight summary + of what modalities exist and how they should be conditioned. + + Attributes: + has_text: Whether text/caption tokens are present for this sample. + Used for text-conditioned generation (e.g., text-to-image/video). + has_vision: Whether vision input (image or video latents) is present. + Defaults to False. + condition_frame_indexes_vision: Indexes of latent vision frames that are clean/conditioning. + [] means all frames are noised/supervised. + All frames specified means all frames are clean (no MSE supervision). + For multi-item samples (e.g. image editing where each sample has multiple + separately-encoded images), this applies to each vision item individually. + The number of items per sample is tracked by + ``GenerationDataClean.num_vision_items_per_sample``. + has_action: Whether action input is present for robotics/embodied AI tasks. + Defaults to False. + condition_frame_indexes_action: Indexes of action steps that are clean/conditioning. + [] means all steps are noised/supervised. + All steps specified means all steps are clean (no MSE supervision). + """ + + # -- understanding (text conditioning) -- + has_text: bool + + # -- vision modality -- + has_vision: bool = False + condition_frame_indexes_vision: list[int] = field(default_factory=list) + # If True, all vision items in this sample share the same temporal mRoPE grid + # (controlnet-style transfer: target frame i is spatio-temporally aligned with + # control frame i). Each item gets the same temporal_offset; spatial reset + # behavior is unchanged. Requires num_vision_items_per_sample > 1, equal latent_t, + # and equal fps across items. Default False preserves single-clip and + # image-editing semantics where items represent distinct time states. + share_vision_temporal_positions: bool = False + + # -- action modality -- + has_action: bool = False + condition_frame_indexes_action: list[int] = field(default_factory=list) + action_start_frame_offset: int = 1 + + # -- sound modality -- + has_sound: bool = False + condition_frame_indexes_sound: list[int] = field(default_factory=list) + + def as_dict(self) -> dict: + return { + "has_text": self.has_text, + "has_vision": self.has_vision, + "has_action": self.has_action, + "has_sound": self.has_sound, + "condition_frame_indexes_vision": self.condition_frame_indexes_vision, + "condition_frame_indexes_action": self.condition_frame_indexes_action, + "condition_frame_indexes_sound": self.condition_frame_indexes_sound, + "share_vision_temporal_positions": self.share_vision_temporal_positions, + } + + +# ============================================================================ +# Helper functions for packing sequences +# ============================================================================ + + +def compute_text_split_length( + num_caption_tokens: int, + special_tokens: Dict[str, int], + has_generation: bool = True, +) -> int: + """Compute the total text split length without mutating any state. + + This is the number of token positions occupied by the text split in a + packed sequence: caption tokens + optional BOS + EOS + optional BOV. + + Args: + num_caption_tokens: Number of raw caption token IDs (before special tokens). + special_tokens: Dictionary of special token IDs (checked for ``"bos_token_id"``). + has_generation: Whether a start-of-generation (BOV) token follows text. + + Returns: + Total text split length (positions consumed in the packed sequence). + """ + n = num_caption_tokens + if "bos_token_id" in special_tokens: + n += 1 + n += 1 # EOS + if has_generation: + n += 1 # start-of-generation / BOV + return n + + +def _pack_text_tokens( + packed_seq: PackedSequence, + text_ids: List[int], + special_tokens: Dict[str, int], + curr_rope_id: int, + has_generation: bool, + use_float_positions: bool = False, +) -> Tuple[int, int, int]: + """Pack text tokens into the sequence. + + Args: + packed_seq: PackedSequence instance to accumulate data into. + text_ids: List of text token IDs (integers). + special_tokens: Dictionary of special token IDs. + curr_rope_id: Current RoPE position ID. + has_generation: Whether there's media/action after text. + use_float_positions: If True, generate float position IDs for 3D mRoPE + (for consistency with FPS-modulated vision tokens). + + Returns: + Tuple of (updated curr_rope_id, split_length, sample_length). + """ + # Ensure we're in build mode (fields are lists, not tensors) + assert isinstance(packed_seq.text_ids, list), "PackedSequence must be in build mode" + assert isinstance(packed_seq.text_indexes, list) + assert isinstance(packed_seq.position_ids, list) + assert isinstance(packed_seq.label_ids, list) + assert isinstance(packed_seq.ce_loss_indexes, list) + assert isinstance(packed_seq.ce_loss_weights, list) + + curr = packed_seq.curr + + # Prepend BOS token if available + if "bos_token_id" in special_tokens: + shifted_text_ids = [special_tokens["bos_token_id"]] + text_ids + else: + shifted_text_ids = text_ids + + split_len = 0 + + # Add text tokens to sequence + packed_seq.text_ids.extend(shifted_text_ids) + packed_seq.text_indexes.extend(range(curr, curr + len(shifted_text_ids))) + + # Configure loss computation for text tokens + packed_seq.ce_loss_indexes.extend(range(curr, curr + len(shifted_text_ids))) + packed_seq.ce_loss_weights.extend([1.0] * len(shifted_text_ids)) + packed_seq.label_ids.extend(text_ids[1:] + [special_tokens["eos_token_id"]]) + + curr += len(shifted_text_ids) + split_len += len(shifted_text_ids) + + # Add EOS token + packed_seq.text_ids.append(special_tokens["eos_token_id"]) + packed_seq.text_indexes.append(curr) + curr += 1 + split_len += 1 + + # Add start-of-generation token, but only if there's media/action present. + if has_generation: + packed_seq.text_ids.append(special_tokens["start_of_generation"]) + packed_seq.text_indexes.append(curr) + curr += 1 + split_len += 1 + + # Sanity check -- compute_text_split_length() is called elsewhere. + assert split_len == compute_text_split_length(len(text_ids), special_tokens, has_generation) + + # Update position IDs and attention mode for text split + if packed_seq._use_mrope: + text_mrope_ids, packed_seq._mrope_temporal_offset = get_3d_mrope_ids_text_tokens( + num_tokens=split_len, + temporal_offset=packed_seq._mrope_temporal_offset, + use_float_positions=use_float_positions, + ) # text_mrope_ids: [3,split_len] + packed_seq.position_ids.append(text_mrope_ids) + else: + packed_seq.position_ids.extend(range(curr_rope_id, curr_rope_id + split_len)) + packed_seq.attn_modes.append("causal") + packed_seq.split_lens.append(split_len) + + packed_seq.curr = curr + return curr_rope_id + split_len, split_len, split_len + + +def _pack_vision_tokens( + packed_seq: PackedSequence, + input_vision_tokens: torch.Tensor, + condition_frame_indexes_vision: list[int], + input_timestep: float | torch.Tensor, + curr_rope_id: int, + latent_patch_size: int = 1, + vision_fps: float | None = None, + enable_fps_modulation: bool = False, + base_fps: float = 24.0, + temporal_compression_factor: int = 4, +) -> int: + """Pack vision tokens into the sequence. + + Args: + packed_seq: PackedSequence instance to accumulate data into. + input_vision_tokens: Vision latent tokens (C, T, H, W). + condition_frame_indexes_vision: Indexes of conditioning frames. + input_timestep: Diffusion timestep. Either a float (teacher_forcing/none — all frames + share the same sigma) or a Tensor(T_max,) (diffusion_forcing — per-frame sigma; + indexed as input_timestep[frame_idx] for each noisy frame). + curr_rope_id: Current RoPE position ID. + latent_patch_size: Patch size for latent patchification. + vision_fps: Frames per second of the video. Used when enable_fps_modulation=True. + enable_fps_modulation: If True, scale temporal position IDs based on video FPS. + base_fps: Base FPS for normalization (default 24.0). + temporal_compression_factor: VAE temporal compression factor (default 4). + Returns: + Vision split length. + """ + # Ensure we're in build mode + assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode" + + curr = packed_seq.curr + vision_split_len = 0 + + # Initialize vision modality if not present. + if packed_seq.vision is None: + packed_seq.vision = ModalityData() + + # Ensure vision modality is in build mode + assert isinstance(packed_seq.vision.sequence_indexes, list) + assert isinstance(packed_seq.vision.mse_loss_indexes, list) + assert isinstance(packed_seq.vision.timesteps, list) + assert isinstance(packed_seq.vision.tokens, list) + + # Compute position IDs for image patches + _, _, latent_t, latent_h, latent_w = input_vision_tokens.shape + if latent_patch_size < 1: + raise ValueError(f"latent_patch_size must be >= 1, got {latent_patch_size}") + # Use ceil to support latent dims not divisible by patch size (padding handled in network) + patch_h = math.ceil(latent_h / latent_patch_size) + patch_w = math.ceil(latent_w / latent_patch_size) + packed_seq.vision.token_shapes.append((latent_t, patch_h, patch_w)) + packed_seq.vision.tokens.append(input_vision_tokens) + + # Add image token indexes and loss information + num_vision_tokens = latent_t * patch_h * patch_w + packed_seq.vision.sequence_indexes.extend(range(curr, curr + num_vision_tokens)) + + # Supervise vision tokens based on conditioning frames + condition_set = {idx for idx in condition_frame_indexes_vision if 0 <= idx < latent_t} + assert isinstance(packed_seq.vision.condition_mask, list) + + vision_condition_mask = torch.zeros( + (latent_t, 1, 1), device=input_vision_tokens.device, dtype=input_vision_tokens.dtype + ) # [T,1,1] + for frame_idx in condition_set: + vision_condition_mask[frame_idx, 0, 0] = 1.0 + packed_seq.vision.condition_mask.append(vision_condition_mask) + + vision_noisy_frame_indexes = torch.tensor( + [idx for idx in range(latent_t) if idx not in condition_set], + device=input_vision_tokens.device, + dtype=torch.long, + ) # [N_noisy_frames] + assert isinstance(packed_seq.vision.noisy_frame_indexes, list) + packed_seq.vision.noisy_frame_indexes.append(vision_noisy_frame_indexes) + + frame_token_stride = patch_h * patch_w + for frame_idx in range(latent_t): + if frame_idx in condition_set: + continue + frame_start = curr + frame_idx * frame_token_stride + frame_end = frame_start + frame_token_stride + packed_seq.vision.mse_loss_indexes.extend(range(frame_start, frame_end)) + if isinstance(input_timestep, torch.Tensor): + frame_ts = input_timestep[frame_idx].item() + else: + frame_ts = input_timestep + packed_seq.vision.timesteps.extend([frame_ts] * frame_token_stride) + + curr += num_vision_tokens + vision_split_len += num_vision_tokens + + # Update position IDs for image split + if packed_seq._use_mrope: + # Determine FPS for this vision segment (None disables FPS modulation) + effective_fps = vision_fps if enable_fps_modulation else None + + vision_mrope_ids, packed_seq._mrope_temporal_offset = get_3d_mrope_ids_vae_tokens( + grid_t=latent_t, + grid_h=patch_h, + grid_w=patch_w, + temporal_offset=packed_seq._mrope_temporal_offset, + reset_spatial_indices=packed_seq._mrope_reset_spatial, + fps=effective_fps, + base_fps=base_fps, + temporal_compression_factor=temporal_compression_factor, + ) # vision_mrope_ids: [3,N_vision_tokens] + packed_seq.position_ids.append(vision_mrope_ids) + else: + # All image tokens share the same RoPE position ID + packed_seq.position_ids.extend([curr_rope_id] * vision_split_len) + + packed_seq.curr = curr + return vision_split_len + + +def _pack_action_tokens( + packed_seq: PackedSequence, + input_action_tokens: torch.Tensor, + condition_frame_indexes_action: list[int], + input_timestep: float, + curr_rope_id: int, + action_temporal_offset: int | float = 0, + enable_fps_modulation: bool = False, + base_fps: float = 24.0, + action_fps: float | None = None, + base_temporal_compression_factor: int | None = None, + action_start_frame_offset: int = 1, +) -> int: + """Pack action tokens into the sequence. + + Args: + packed_seq: PackedSequence instance to accumulate data into. + input_action_tokens: Action latent tokens (T, D). + condition_frame_indexes_action: Indexes of conditioning action steps. + input_timestep: Diffusion timestep. + curr_rope_id: Current RoPE position ID. + action_temporal_offset: Temporal offset for action mRoPE IDs (typically + the vision start offset so action aligns temporally with vision). + enable_fps_modulation: If True, scale temporal position IDs based on FPS. + base_fps: Base FPS for normalization (default 24.0). + action_fps: Frames per second of the action data. Used when enable_fps_modulation=True. + base_temporal_compression_factor: Base temporal compression factor for FPS scaling. + Should be set to the vision temporal compression factor (e.g. 4) so that action + tokens advance at frame rate (4x finer) relative to vision latent frames. + Only affects behavior when FPS modulation is enabled. + action_start_frame_offset: Frame offset for aligning action[0] with the + corresponding vision frame. Default 1 aligns action[0] with vision frame 1. + Returns: + Number of action tokens added. + """ + # Ensure we're in build mode + assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode" + + curr = packed_seq.curr + action_split_len = input_action_tokens.shape[0] + + # Initialize action modality if not present + if packed_seq.action is None: + packed_seq.action = ModalityData() + + # Ensure action modality is in build mode + assert isinstance(packed_seq.action.sequence_indexes, list) + assert isinstance(packed_seq.action.mse_loss_indexes, list) + assert isinstance(packed_seq.action.timesteps, list) + assert isinstance(packed_seq.action.tokens, list) + + # Add token indexes and loss information + action_indexes = list(range(curr, curr + action_split_len)) + packed_seq.action.sequence_indexes.extend(action_indexes) + packed_seq.action.token_shapes.append((action_split_len,)) + packed_seq.action.tokens.append(input_action_tokens) + + + condition_set = {idx for idx in condition_frame_indexes_action if 0 <= idx < action_split_len} + assert isinstance(packed_seq.action.condition_mask, list) + + action_condition_mask = torch.zeros( + (action_split_len, 1), device=input_action_tokens.device, dtype=input_action_tokens.dtype + ) # [T_action,1] + for frame_idx in condition_set: + action_condition_mask[frame_idx, 0] = 1.0 + packed_seq.action.condition_mask.append(action_condition_mask) + + action_noisy_frame_indexes = torch.tensor( + [idx for idx in range(action_split_len) if idx not in condition_set], + device=input_action_tokens.device, + dtype=torch.long, + ) # [N_noisy_action_frames] + assert isinstance(packed_seq.action.noisy_frame_indexes, list) + packed_seq.action.noisy_frame_indexes.append(action_noisy_frame_indexes) + + frame_token_stride = 1 # Action has 1 token per frame (no spatial dimension) + for frame_idx in range(action_split_len): + if frame_idx in condition_set: + continue + frame_start = curr + frame_idx * frame_token_stride + frame_end = frame_start + frame_token_stride + packed_seq.action.mse_loss_indexes.extend(range(frame_start, frame_end)) + packed_seq.action.timesteps.extend([input_timestep] * frame_token_stride) + + # Update RoPE position IDs for action tokens. + if packed_seq._use_mrope: + # 3D mRoPE: action tokens use a 1x1 spatial grid with start_frame_offset=1 + # so action[0] (null token) aligns with vision frame 1, not frame 0. + effective_fps = action_fps if enable_fps_modulation else None + + action_mrope_ids, _ = get_3d_mrope_ids_vae_tokens( + grid_t=action_split_len, + grid_h=1, + grid_w=1, + temporal_offset=action_temporal_offset, + reset_spatial_indices=packed_seq._mrope_reset_spatial, + fps=effective_fps, + base_fps=base_fps, + temporal_compression_factor=1, # Action is at frame rate (no temporal compression) + base_temporal_compression_factor=base_temporal_compression_factor, + start_frame_offset=action_start_frame_offset, # Align action[0] with vision frame action_start_frame_offset + ) # action_mrope_ids: [3,N_action_tokens] + packed_seq.position_ids.append(action_mrope_ids) + # Note: we don't update _mrope_temporal_offset here because action tokens + # share the temporal space with vision tokens (they run in parallel). + else: + # All action tokens share the SAME RoPE position as vision tokens (see docs/sequence_packing.md). + packed_seq.position_ids.extend([curr_rope_id] * action_split_len) + + packed_seq.curr = curr + action_split_len + return action_split_len + + +def _pack_sound_tokens( + packed_seq: PackedSequence, + input_sound_tokens: torch.Tensor, + condition_frame_indexes_sound: list[int], + input_timestep: float, + curr_rope_id: int, + sound_temporal_offset: int | float = 0, + enable_fps_modulation: bool = False, + base_fps: float = 24.0, + sound_fps: float | None = None, +) -> int: + """Pack sound/audio tokens into the sequence. + + Sound latents have shape [C, T] where C is channels and T is temporal frames. + Sound tokens are added to the unified generation split to maintain FactoredSequencePack's + 2-split invariant (causal + full). + + Args: + packed_seq: PackedSequence instance to accumulate data into. + input_sound_tokens: Sound latent tokens (C, T). + condition_frame_indexes_sound: Indexes of conditioning frames. + [] means all frames are noised/supervised. + All frames specified means all frames are clean (no MSE supervision). + input_timestep: Diffusion timestep. + curr_rope_id: Current RoPE position ID. + sound_temporal_offset: Temporal offset for m-RoPE position IDs (aligned with vision start). + enable_fps_modulation: If True, scale temporal positions by FPS ratio. + base_fps: Base FPS for normalization (default 24.0). + sound_fps: Sound latent FPS (e.g., 25.0). Used for FPS-aware m-RoPE positions. + + Returns: + Number of sound tokens added. + """ + # Ensure we're in build mode + assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode" + + curr = packed_seq.curr + + # Sound latent shape: [C, T] → T tokens + _, sound_split_len = input_sound_tokens.shape + + # Initialize sound modality if not present + if packed_seq.sound is None: + packed_seq.sound = ModalityData() + + # Ensure sound modality is in build mode + assert isinstance(packed_seq.sound.sequence_indexes, list) + assert isinstance(packed_seq.sound.mse_loss_indexes, list) + assert isinstance(packed_seq.sound.timesteps, list) + assert isinstance(packed_seq.sound.tokens, list) + + # Add token indexes - sound uses (T, 1, 1) shape for compatibility with 3D RoPE + packed_seq.sound.token_shapes.append((sound_split_len, 1, 1)) + packed_seq.sound.sequence_indexes.extend(range(curr, curr + sound_split_len)) + packed_seq.sound.tokens.append(input_sound_tokens) + + # Supervise sound tokens based on conditioning frames + condition_set = {idx for idx in condition_frame_indexes_sound if 0 <= idx < sound_split_len} + assert isinstance(packed_seq.sound.condition_mask, list) + + # Condition mask: shape (T, 1) — 1 = clean/conditioning, 0 = noised/supervised + sound_condition_mask = torch.zeros( + (sound_split_len, 1), device=input_sound_tokens.device, dtype=input_sound_tokens.dtype + ) # [T_sound,1] + for frame_idx in condition_set: + sound_condition_mask[frame_idx, 0] = 1.0 + packed_seq.sound.condition_mask.append(sound_condition_mask) + + sound_noisy_frame_indexes = torch.tensor( + [idx for idx in range(sound_split_len) if idx not in condition_set], + device=input_sound_tokens.device, + dtype=torch.long, + ) # [N_noisy_sound_frames] + assert isinstance(packed_seq.sound.noisy_frame_indexes, list) + packed_seq.sound.noisy_frame_indexes.append(sound_noisy_frame_indexes) + + # Add to MSE loss indexes and timesteps for non-conditioning frames + for frame_idx in range(sound_split_len): + if frame_idx in condition_set: + continue + # Sound has 1 token per frame (no spatial dimension) + frame_start = curr + frame_idx + frame_end = frame_start + 1 + packed_seq.sound.mse_loss_indexes.extend(range(frame_start, frame_end)) + packed_seq.sound.timesteps.extend([input_timestep]) + + # Update RoPE position IDs for sound tokens. + if packed_seq._use_mrope: + # 3D mRoPE: sound tokens use a 1x1 spatial grid, aligned with vision temporal positions. + # sound[0] aligns with vision frame 0 (start_frame_offset=0, unlike action which offsets by 1). + effective_fps = sound_fps if enable_fps_modulation else None + + sound_mrope_ids, _ = get_3d_mrope_ids_vae_tokens( + grid_t=sound_split_len, + grid_h=1, + grid_w=1, + temporal_offset=sound_temporal_offset, + reset_spatial_indices=packed_seq._mrope_reset_spatial, + fps=effective_fps, + base_fps=base_fps, + temporal_compression_factor=1, # Sound latent is already at sound_latent_fps (no further compression) + start_frame_offset=0, # Sound[0] aligns with vision frame 0 + ) # sound_mrope_ids: [3,N_sound_tokens] + packed_seq.position_ids.append(sound_mrope_ids) + # Note: we don't update _mrope_temporal_offset here because sound tokens + # share the temporal space with vision tokens (they run in parallel). + else: + # All sound tokens share the SAME RoPE position as vision/action tokens (unified generation split). + packed_seq.position_ids.extend([curr_rope_id] * sound_split_len) + + packed_seq.curr = curr + sound_split_len + return sound_split_len + + +def _pack_supertokens_temporal_causal( + packed_seq: "PackedSequence", + input_vision_tokens: torch.Tensor, + input_action_tokens: torch.Tensor | None, + condition_frame_indexes_vision: list[int], + input_timestep: float | torch.Tensor, + curr_rope_id: int, + latent_patch_size: int, + temporal_compression_factor: int, + action_dim: int, + vision_fps: float | None = None, + action_fps: float | None = None, + enable_fps_modulation: bool = False, + base_fps: float = 24.0, + pack_action_tokens: bool = True, +) -> tuple[int, bool]: + """Pack vision and (optionally) action tokens in supertoken order for temporal causal attention. + + Buffer layout per frame: + pack_action_tokens=True: [action_t (tcf), vision_t (H*W)] — supertoken size tcf + H*W + pack_action_tokens=False: [vision_t (H*W)] — supertoken size H*W + + Use ``pack_action_tokens=False`` when ``config.action_gen=False``; the resulting + ``num_action_tokens_per_supertoken=0`` is stamped on the pack and read by the + attention builder so NATTEN metadata stays in sync automatically. + + mRoPE layout (with actions, unified_3d_mrope only): + - Null actions (frame 0): all tcf tokens at ``temporal_offset``. + - Real training actions (frames 1..T-1): ``start_frame_offset=1`` so the + last action in group i co-locates with vision frame i. + - AR real actions (single supertoken): ``start_frame_offset=0``. + - Interleaved per frame as cat([action_ids, vision_ids]). + + ``input_timestep`` is float (TF/none) or Tensor(T_max,) (DF, per-frame sigma). + Conditioning frames are excluded from mse_loss_indexes either way. + + Returns (total_split_len, null_action_flag); null_action_flag is False when + pack_action_tokens=False. + """ + assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode" + + _, _, latent_t, latent_h, latent_w = input_vision_tokens.shape + patch_h = math.ceil(latent_h / latent_patch_size) + patch_w = math.ceil(latent_w / latent_patch_size) + tcf = temporal_compression_factor + patches_per_frame = patch_h * patch_w + supertoken_len = tcf + patches_per_frame if pack_action_tokens else patches_per_frame # S + + # Initialize modalities if needed + if packed_seq.vision is None: + packed_seq.vision = ModalityData() + if pack_action_tokens and packed_seq.action is None: + packed_seq.action = ModalityData() + + assert isinstance(packed_seq.vision.sequence_indexes, list) + assert isinstance(packed_seq.vision.mse_loss_indexes, list) + assert isinstance(packed_seq.vision.timesteps, list) + assert isinstance(packed_seq.vision.tokens, list) + assert isinstance(packed_seq.vision.condition_mask, list) + if pack_action_tokens: + assert isinstance(packed_seq.action.sequence_indexes, list) + assert isinstance(packed_seq.action.mse_loss_indexes, list) + assert isinstance(packed_seq.action.timesteps, list) + assert isinstance(packed_seq.action.tokens, list) + assert isinstance(packed_seq.action.condition_mask, list) + + device = input_vision_tokens.device + dtype = input_vision_tokens.dtype + + null_action_flag: bool + if pack_action_tokens: + # Build all_action_tokens: shape (latent_t * tcf, action_dim) + # + # Cases: + # 1. Training with conditioning frame (latent_t > 1, real_actions < latent_t*tcf): + # Prepend tcf null tokens for frame 0, then real actions for frames 1..T-1. + # 2. KV-cache continuation (latent_t > 1, real_actions == latent_t*tcf): all supertokens + # carry real actions (no conditioning frame in-segment). + # 3. AR frame N>0 (latent_t == 1, action provided): real actions, no null prefix. + # 4. AR frame 0 / image2video (action is None): all null tokens. + if input_action_tokens is not None: + # input_action_tokens shape: (1, T*tcf, D) or (T*tcf, D) for training; (tcf, D) for AR frame N>0 + if input_action_tokens.dim() == 3: + real_actions = input_action_tokens.squeeze(0) # [T*tcf,action_dim] or [N,action_dim] + else: + real_actions = input_action_tokens # [N,action_dim] + null_tokens = torch.zeros(tcf, action_dim, device=device, dtype=real_actions.dtype) # [tcf,action_dim] + if latent_t == 1: + # AR frame N>0: single supertoken with real actions, no null prefix + all_action_tokens = real_actions # [tcf,action_dim] + null_action_flag = False + elif real_actions.shape[0] == latent_t * tcf: + # All frames have real actions (e.g. KV-cache continuation segments) + all_action_tokens = real_actions + null_action_flag = False + else: + # Conditioning frame present: null for supertoken 0, real for 1..T-1 + all_action_tokens = torch.cat([null_tokens, real_actions], dim=0) # [T*tcf,action_dim] + null_action_flag = True + else: + # AR frame 0 or image2video: all action tokens are null + all_action_tokens = torch.zeros( + latent_t * tcf, action_dim, device=device, dtype=dtype + ) # [T*tcf,action_dim] + null_action_flag = True + else: + # pack_action_tokens=False: action tokens must not be supplied. + assert input_action_tokens is None, ( + "pack_action_tokens=False requires input_action_tokens=None; got a non-None tensor." + ) + null_action_flag = False + + # Record vision token shapes and tokens + packed_seq.vision.token_shapes.append((latent_t, patch_h, patch_w)) + packed_seq.vision.tokens.append(input_vision_tokens) + + # Vision conditioning mask: (T, 1, 1) + condition_set_vision = {idx for idx in condition_frame_indexes_vision if 0 <= idx < latent_t} + vision_condition_mask = torch.zeros((latent_t, 1, 1), device=device, dtype=dtype) # [T,1,1] + for fidx in condition_set_vision: + vision_condition_mask[fidx, 0, 0] = 1.0 + packed_seq.vision.condition_mask.append(vision_condition_mask) + + vision_noisy_frame_indexes = torch.tensor( + [idx for idx in range(latent_t) if idx not in condition_set_vision], + device=device, + dtype=torch.long, + ) # [N_noisy_frames] + packed_seq.vision.noisy_frame_indexes.append(vision_noisy_frame_indexes) + + if pack_action_tokens: + # Action token shapes: latent_t * tcf total (including null tokens) + packed_seq.action.token_shapes.append((latent_t * tcf,)) + packed_seq.action.tokens.append(all_action_tokens) + + # Action conditioning mask: all action tokens are conditioning (not supervised) + # Null tokens are always conditioning; real actions are conditioning too (they are inputs) + action_condition_mask = torch.ones((latent_t * tcf, 1), device=device, dtype=dtype) # [T*tcf,1] + packed_seq.action.condition_mask.append(action_condition_mask) + + # Pack in interleaved supertoken order: [action_t, vision_t] for each frame t + # (or just [vision_t] per frame when pack_action_tokens=False) + curr = packed_seq.curr + total_split_len = 0 + + # mRoPE: snapshot offset before this sample, compute IDs + if packed_seq._use_mrope: + temporal_offset = packed_seq._mrope_temporal_offset + effective_vision_fps = vision_fps if enable_fps_modulation else None + + # AR frame N>=1 with action_gen=True (latent_t==1 and real actions supplied): + # shift both vision and action by start_frame_offset=1 so the last action in + # the group co-locates with vision frame N, mirroring training's layout. + # All other cases (training latent_t>1, AR action_gen=False, AR frame 0 null) + # keep start_frame_offset=0. The caller in pack_input_sequence_autoregressive + # seeds temporal_offset accordingly (N-1 frames back when this shift applies). + ar_with_real_actions = latent_t == 1 and pack_action_tokens and input_action_tokens is not None + vision_sfo = 1 if ar_with_real_actions else 0 + + vision_ids_flat, new_offset = get_3d_mrope_ids_vae_tokens( + grid_t=latent_t, + grid_h=patch_h, + grid_w=patch_w, + temporal_offset=temporal_offset, + reset_spatial_indices=packed_seq._mrope_reset_spatial, + fps=effective_vision_fps, + base_fps=base_fps, + temporal_compression_factor=tcf, + start_frame_offset=vision_sfo, + ) # vision_ids_flat: [3,T*patch_h*patch_w] + + if pack_action_tokens: + effective_action_fps = action_fps if enable_fps_modulation else None + + # Action IDs: null for frame 0 (all tcf tokens share temporal_offset, + # co-located with vision frame 0), real for frames 1..T-1. + # Real tokens (training and AR) use start_frame_offset=1 so the last + # action in a group co-locates with vision frame i. + fps_active = effective_action_fps is not None + t_dtype = torch.float32 if fps_active else torch.long + t_offset = float(temporal_offset) if fps_active else int(temporal_offset) + null_t = torch.full((tcf,), t_offset, dtype=t_dtype) # [tcf] + null_hw = torch.zeros(tcf, dtype=t_dtype) # [tcf] + null_ids = torch.stack([null_t, null_hw, null_hw]) # [3,tcf] + + def _real_action_ids(n_frames: int, start_frame_offset: int) -> torch.Tensor: + flat, _ = get_3d_mrope_ids_vae_tokens( + grid_t=n_frames * tcf, + grid_h=1, + grid_w=1, + temporal_offset=temporal_offset, + reset_spatial_indices=packed_seq._mrope_reset_spatial, + fps=effective_action_fps, + base_fps=base_fps, + temporal_compression_factor=1, + base_temporal_compression_factor=tcf, + start_frame_offset=start_frame_offset, + ) + return flat.reshape(3, n_frames, tcf) # [3,n_frames,tcf] + + if latent_t > 1 and input_action_tokens is not None: + if real_actions.shape[0] == latent_t * tcf: + # KV continuation: real action in every supertoken (including frame 0) + action_ids_3d = _real_action_ids(latent_t, start_frame_offset=0) + else: + # Training with conditioning frame: supertoken 0 = null, 1..T-1 = real + null_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf] + real_ids_3d = _real_action_ids(latent_t - 1, start_frame_offset=1) # [3,T-1,tcf] + action_ids_3d = torch.cat([null_ids_3d, real_ids_3d], dim=1) # [3,T,tcf] + elif latent_t > 1: + # No action tensor (all-null layout): same ID structure as training w/ conditioning frame. + null_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf] + real_ids_3d = _real_action_ids(latent_t - 1, start_frame_offset=1) # [3,T-1,tcf] + action_ids_3d = torch.cat([null_ids_3d, real_ids_3d], dim=1) # [3,T,tcf] + elif input_action_tokens is None: + # AR frame 0 / image2video: only null + action_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf] + else: + # AR frame N>=1: single supertoken with real actions. start_frame_offset=1 + # matches training (last action co-locates with vision frame N); caller + # seeds temporal_offset to (N-1) frame-strides back to compensate. + action_ids_3d = _real_action_ids(1, start_frame_offset=1) # [3,1,tcf] + + # (3, T*H*W) → (3, T, H*W) + vision_ids_3d = vision_ids_flat.reshape(3, latent_t, patches_per_frame) # [3,T,patch_h*patch_w] + + # Interleave per frame: (3, T, tcf+H*W) → (3, T*S) + interleaved_ids = torch.cat([action_ids_3d, vision_ids_3d], dim=2).reshape( + 3, latent_t * supertoken_len + ) # [3,T*S] + packed_seq.position_ids.append(interleaved_ids) + else: + # No action tokens: just vision IDs, already in (3, T*H*W) order. + packed_seq.position_ids.append(vision_ids_flat) + + packed_seq._mrope_temporal_offset = new_offset + + for frame_t in range(latent_t): + if pack_action_tokens: + # Pack action tokens for this frame (indexes only; tokens already stored in packed_seq.action.tokens) + action_indexes = list(range(curr, curr + tcf)) + packed_seq.action.sequence_indexes.extend(action_indexes) + # Action tokens are never in MSE loss (always conditioning) + curr += tcf + total_split_len += tcf + + if not packed_seq._use_mrope: + packed_seq.position_ids.extend([curr_rope_id] * tcf) + + # Pack vision tokens for this frame + frame_indexes = list(range(curr, curr + patches_per_frame)) + packed_seq.vision.sequence_indexes.extend(frame_indexes) + curr += patches_per_frame + total_split_len += patches_per_frame + + if not packed_seq._use_mrope: + packed_seq.position_ids.extend([curr_rope_id] * patches_per_frame) + + # Vision MSE loss: supervise non-conditioning frames + if frame_t not in condition_set_vision: + packed_seq.vision.mse_loss_indexes.extend(frame_indexes) + frame_ts = input_timestep[frame_t].item() if isinstance(input_timestep, torch.Tensor) else input_timestep + packed_seq.vision.timesteps.extend([frame_ts] * patches_per_frame) + + packed_seq.curr = curr + return total_split_len, null_action_flag + + +# ============================================================================ +# Main packing function +# ============================================================================ + + +def pack_input_sequence( + sequence_plans: list[SequencePlan], + input_text_indexes: list[list[int]], + gen_data_clean: GenerationDataClean, + input_timesteps: torch.Tensor, + special_tokens: dict[str, int], + max_num_tokens: int | None = None, + latent_patch_size: int = 1, + skip_text_tokens: bool = False, + include_end_of_generation_token: bool = False, + position_embedding_type: str = "3d_rope", + unified_3d_mrope_reset_spatial_ids: bool = True, + unified_3d_mrope_temporal_modality_margin: int = 0, + enable_fps_modulation: bool = False, + base_fps: float = 24.0, + temporal_compression_factor: int = 4, + video_temporal_causal: bool = False, + action_dim: int = 32, + initial_mrope_temporal_offset: int | float = 0, +) -> PackedSequence: + """ + Pack a sequence of input strings and VAE latents into a packed tensor format. + Uses SequencePlan to determine which modalities are present for each sample, + and maintains separate indices for text, vision, action, and sound to handle variable modality presence. + + Args: + sequence_plans: List of SequencePlan items describing which modalities are present. + input_text_indexes: List of text token ID sequences (only for samples where has_text=True). + gen_data_clean: GenerationDataClean containing vision, action, and sound tensors. + - x0_tokens_vision: Vision tensors for samples where has_vision=True + - x0_tokens_action: Action tensors for samples where has_action=True + - x0_tokens_sound: Sound tensors (list of [C, T]) for samples where has_sound=True + input_timesteps: Diffusion timesteps for each sample. Shape (B,) or (B, 1) for + teacher_forcing/none (all frames share the same sigma), or (B, T_max) for + diffusion_forcing (per-frame independent sigma). Entries are extracted per + sample as a float (numel==1) or Tensor(T_max,) for per-frame indexing. + special_tokens: Dictionary containing special token IDs (eos_token_id, start_of_generation, end_of_generation) + max_num_tokens: Maximum number of tokens in the packed sequence + latent_patch_size: Patch size used by the network to pack latents + skip_text_tokens: If True, skip packing text tokens + include_end_of_generation_token: If True, append end-of-generation token + position_embedding_type: Position embedding type for vision tokens: + - "3d_rope": Additive 3D RoPE embeddings + 1D position IDs for attention + - "flattened_sin_cos": Additive flattened sin/cos embeddings + 1D position IDs + - "unified_3d_mrope": No additive embedding + 3D position IDs for Qwen3VL-style mRoPE + unified_3d_mrope_reset_spatial_ids: If True (default), spatial (H, W) indices + start from 0 for each vision segment. If False, spatial indices are offset + by the temporal offset (Qwen2VL-style). Only used when position_embedding_type="unified_3d_mrope". + enable_fps_modulation: If True, scale temporal position IDs based on video FPS + to reflect real time. Requires fps_vision in gen_data_clean. + Uses the same flag as diffusion_expert_config.enable_fps_modulation. + base_fps: Base FPS for normalization (default 24.0). + Uses the same value as diffusion_expert_config.base_fps. + temporal_compression_factor: VAE temporal compression factor (default 4). + Obtained from the VAE tokenizer at runtime. + Returns: + PackedSequence containing all packed tensors and metadata. See PackedSequence for field details. + """ + del max_num_tokens + + assert special_tokens is not None, "Special tokens must be provided" + assert isinstance(input_timesteps, torch.Tensor), "input_timesteps must be a tensor" + if input_timesteps.is_cuda: + raise ValueError("input_timesteps must be on CPU, not CUDA") + if isinstance(input_text_indexes, torch.Tensor): + raise ValueError("input_text_tokens must be a list, not a tensor") + + # Initialize packed sequence (acts as builder during packing) + packed_seq = PackedSequence() + + # Configure 3D mRoPE on the builder (enabled when position_embedding_type is unified_3d_mrope) + packed_seq._use_mrope = position_embedding_type == "unified_3d_mrope" + packed_seq._mrope_reset_spatial = unified_3d_mrope_reset_spatial_ids + + # Maintain separate indices for each modality + idx_text = 0 + idx_vision = 0 + idx_action = 0 + idx_sound = 0 + null_action_flags: list[bool] = [] # collected from TC path; asserted consistent after the loop + + # Validate: all samples must have text (causal split is always required for two-way attention). + # CFG dropout only drops text *content*, not the structural text split. + if not skip_text_tokens: + for plan in sequence_plans: + assert plan.has_text, "All sequence plans must have has_text=True when skip_text_tokens=False" + + # Pack each sample based on its sequence plan + for sample_idx, sequence_plan in enumerate(sequence_plans): + curr_rope_id = 0 + sample_len = 0 + + # mRoPE temporal offset resets per sample. + # initial_mrope_temporal_offset is non-zero only for AR inference (frame N seeds at N*tcf). + packed_seq._mrope_temporal_offset = initial_mrope_temporal_offset + + _ts = input_timesteps[sample_idx] + input_timestep = _ts.item() if _ts.numel() == 1 else _ts # float (TF) or Tensor(T_max,) (DF) + + # Pack text tokens if has_text=True and not skipped + if sequence_plan.has_text and not skip_text_tokens: + text_ids = input_text_indexes[idx_text] + idx_text += 1 + + has_generation_for_sample = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound + curr_rope_id, _, text_sample_len = _pack_text_tokens( + packed_seq, + text_ids, + special_tokens, + curr_rope_id, + has_generation=has_generation_for_sample, + use_float_positions=enable_fps_modulation, + ) + sample_len += text_sample_len + + # End of text modality, add an offset as the boundary between text and vision. + packed_seq._mrope_temporal_offset += unified_3d_mrope_temporal_modality_margin + + # Save temporal offset before vision for action tokens (action uses same offset as vision start) + vision_start_temporal_offset = packed_seq._mrope_temporal_offset + + # Pack vision (and optionally action) tokens + if video_temporal_causal and sequence_plan.has_vision: + # Temporal causal path: when sequence_plan.has_action=True, interleaved supertokens + # [action_t, vision_t]; when False, supertokens are just vision patches. + assert position_embedding_type == "unified_3d_mrope", ( + "video_temporal_causal=True requires position_embedding_type='unified_3d_mrope'" + ) + input_vision_tokens = gen_data_clean.x0_tokens_vision[idx_vision] + idx_vision += 1 + + vision_fps = None + if ( + enable_fps_modulation + and gen_data_clean.fps_vision is not None + and idx_vision - 1 < len(gen_data_clean.fps_vision) + ): + vision_fps = float(gen_data_clean.fps_vision[idx_vision - 1].item()) + + input_action_tokens_tc: torch.Tensor | None = None + action_fps_tc: float | None = None + if sequence_plan.has_action: + input_action_tokens_tc = gen_data_clean.x0_tokens_action[idx_action] + if ( + enable_fps_modulation + and gen_data_clean.fps_action is not None + and idx_action < len(gen_data_clean.fps_action) + ): + action_fps_tc = float(gen_data_clean.fps_action[idx_action].item()) + idx_action += 1 + + supertoken_split_len, null_flag = _pack_supertokens_temporal_causal( + packed_seq=packed_seq, + input_vision_tokens=input_vision_tokens, + input_action_tokens=input_action_tokens_tc, + condition_frame_indexes_vision=sequence_plan.condition_frame_indexes_vision, + input_timestep=input_timestep, + curr_rope_id=curr_rope_id, + latent_patch_size=latent_patch_size, + temporal_compression_factor=temporal_compression_factor, + action_dim=action_dim, + vision_fps=vision_fps, + action_fps=action_fps_tc, + enable_fps_modulation=enable_fps_modulation, + base_fps=base_fps, + pack_action_tokens=sequence_plan.has_action, + ) + null_action_flags.append(null_flag) + # We assume all samples in a batch share the same has_action layout, so + # stamp the supertoken layout constant directly here. This is the + # single source of truth read by downstream attention / KV-cache + # code (no recomputation in the network). + packed_seq.num_action_tokens_per_supertoken = temporal_compression_factor if sequence_plan.has_action else 0 + sample_len += supertoken_split_len + vision_split_len = supertoken_split_len + action_split_len = 0 # Already absorbed into supertoken_split_len + + else: + # Standard path: vision and action packed separately + if sequence_plan.has_vision: + # Determine how many vision items this sample owns. + # For multi-item samples (e.g. image editing), num_vision_items_per_sample + # records [2, 2, ...]; for standard T2I/T2V it is None (1 item per sample). + num_vis = ( + gen_data_clean.num_vision_items_per_sample[sample_idx] + if gen_data_clean.num_vision_items_per_sample is not None + else 1 + ) + + vision_split_len = 0 + # Controlnet-style transfer: when set, all vision items share the same + # temporal mRoPE grid. We snapshot the offset before the loop and + # rewind to it before each item, so every item produces identical + # temporal IDs. Each _pack_vision_tokens call still advances the + # offset by latent_t internally; in shared-grid mode the post-loop + # offset equals snapshot + latent_t (single-clip semantics for + # downstream EOV / next-modality tokens). + shared_grid = sequence_plan.share_vision_temporal_positions and num_vis > 1 + items_temporal_offset_snapshot = packed_seq._mrope_temporal_offset + shared_latent_t: int | None = None + shared_patch_h: int | None = None + shared_patch_w: int | None = None + # FPS is recorded per-sample (shape [B]); for multi-item samples + # (transfer / image-edit) every vision item in this sample shares + # the same conditioning FPS, so we read by sample_idx, not by the + # flat idx_vision counter (which would alias to a neighbor sample's + # fps and corrupt RoPE FPS modulation). + sample_vision_fps: float | None = None + if ( + enable_fps_modulation + and gen_data_clean.fps_vision is not None + and sample_idx < len(gen_data_clean.fps_vision) + ): + sample_vision_fps = float(gen_data_clean.fps_vision[sample_idx].item()) + + for item_idx in range(num_vis): + input_vision_tokens = gen_data_clean.x0_tokens_vision[idx_vision] + vision_fps = sample_vision_fps + idx_vision += 1 + + # Determine conditioning for this vision item. + # For multi-item mode: all items except the last are fully conditioned + # (all frames are clean); the last item uses the SequencePlan's + # condition_frame_indexes_vision (typically [] = fully generated). + if num_vis > 1 and item_idx < num_vis - 1: + # Conditioning item (e.g. source image): mark all frames as clean + latent_t = input_vision_tokens.shape[2] + item_condition_frames = list(range(latent_t)) + else: + # Generation item (single-item mode or last item in multi-item) + item_condition_frames = sequence_plan.condition_frame_indexes_vision + + if shared_grid: + item_latent_t = input_vision_tokens.shape[2] + item_latent_h = input_vision_tokens.shape[3] + item_latent_w = input_vision_tokens.shape[4] + if shared_latent_t is None: + shared_latent_t = item_latent_t + shared_patch_h = item_latent_h + shared_patch_w = item_latent_w + else: + assert item_latent_t == shared_latent_t, ( + f"share_vision_temporal_positions requires equal latent_t across items, " + f"got item {item_idx} latent_t={item_latent_t} vs first={shared_latent_t}" + ) + assert item_latent_h == shared_patch_h and item_latent_w == shared_patch_w, ( + f"share_vision_temporal_positions requires equal spatial grid across items, " + f"got item {item_idx} (H,W)=({item_latent_h},{item_latent_w}) " + f"vs first=({shared_patch_h},{shared_patch_w})" + ) + # Rewind so this item starts at the same temporal offset as item 0. + packed_seq._mrope_temporal_offset = items_temporal_offset_snapshot + + item_split_len = _pack_vision_tokens( + packed_seq=packed_seq, + input_vision_tokens=input_vision_tokens, + condition_frame_indexes_vision=item_condition_frames, + input_timestep=input_timestep, + curr_rope_id=curr_rope_id, + latent_patch_size=latent_patch_size, + vision_fps=vision_fps, + enable_fps_modulation=enable_fps_modulation, + base_fps=base_fps, + temporal_compression_factor=temporal_compression_factor, + ) + vision_split_len += item_split_len + sample_len += vision_split_len + + else: + vision_split_len = 0 + + # Pack action tokens if has_action=True + if sequence_plan.has_action: + input_action_tokens = gen_data_clean.x0_tokens_action[idx_action] + + # Get FPS for action (action may have its own FPS independent of vision) + action_fps: float | None = None + if ( + enable_fps_modulation + and gen_data_clean.fps_action is not None + and idx_action < len(gen_data_clean.fps_action) + ): + action_fps = float(gen_data_clean.fps_action[idx_action].item()) + + idx_action += 1 + + action_split_len = _pack_action_tokens( + packed_seq=packed_seq, + input_action_tokens=input_action_tokens, + condition_frame_indexes_action=sequence_plan.condition_frame_indexes_action, + input_timestep=input_timestep, + curr_rope_id=curr_rope_id, + action_temporal_offset=vision_start_temporal_offset, + enable_fps_modulation=enable_fps_modulation, + base_fps=base_fps, + action_fps=action_fps, + base_temporal_compression_factor=temporal_compression_factor, + action_start_frame_offset=sequence_plan.action_start_frame_offset, + ) + sample_len += action_split_len + else: + action_split_len = 0 + + # Pack sound tokens if has_sound=True + if sequence_plan.has_sound: + input_sound_tokens = gen_data_clean.x0_tokens_sound[idx_sound] + + # Get FPS for sound (from gen_data_clean, like vision and action) + sound_fps: float | None = None + if ( + enable_fps_modulation + and gen_data_clean.fps_sound is not None + and idx_sound < len(gen_data_clean.fps_sound) + ): + sound_fps = float(gen_data_clean.fps_sound[idx_sound].item()) + + idx_sound += 1 + + sound_split_len = _pack_sound_tokens( + packed_seq=packed_seq, + input_sound_tokens=input_sound_tokens, + condition_frame_indexes_sound=sequence_plan.condition_frame_indexes_sound, + input_timestep=input_timestep, + curr_rope_id=curr_rope_id, + sound_temporal_offset=vision_start_temporal_offset, + enable_fps_modulation=enable_fps_modulation, + base_fps=base_fps, + sound_fps=sound_fps, + ) + sample_len += sound_split_len + else: + sound_split_len = 0 + + # Add end-of-generation token if needed + eov_len = 0 + has_any_generation = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound + if include_end_of_generation_token and has_any_generation: + # Type narrowing: we're in build mode, fields are lists + assert isinstance(packed_seq.text_ids, list) + assert isinstance(packed_seq.text_indexes, list) + assert isinstance(packed_seq.position_ids, list) + + packed_seq.text_ids.append(special_tokens["end_of_generation"]) + packed_seq.text_indexes.append(packed_seq.curr) + + # EOV position IDs: 3D mRoPE or 1D RoPE + if packed_seq._use_mrope: + # Use float dtype when FPS modulation is enabled for consistency + eov_dtype = torch.float32 if enable_fps_modulation else torch.long + eov_mrope_ids = torch.full((3, 1), packed_seq._mrope_temporal_offset, dtype=eov_dtype) # [3,1] + packed_seq.position_ids.append(eov_mrope_ids) # type: ignore[arg-type] + packed_seq._mrope_temporal_offset += 1 + else: + packed_seq.position_ids.append(curr_rope_id) # type: ignore[arg-type] + + packed_seq.curr += 1 + eov_len = 1 + sample_len += 1 + + combined_split_len = vision_split_len + action_split_len + sound_split_len + eov_len + packed_seq.attn_modes.append("full") + packed_seq.split_lens.append(combined_split_len) + packed_seq.sample_lens.append(sample_len) + + # Assert consistent null_action_supertokens across all TC samples, then set once + if null_action_flags: + assert len(set(null_action_flags)) == 1, ( + f"Inconsistent null_action_supertokens across samples: {null_action_flags}. " + "All samples in a batch must have the same structure (all training or all AR inference)." + ) + packed_seq.null_action_supertokens = null_action_flags[0] + + # Finalize and return packed data + return packed_seq.finalize( + gen_data_clean=gen_data_clean, + ) + + +# ============================================================================ +# SequencePack:Operations on packed sequences +# ============================================================================ + +""" +SequencePack is a dictionary-based container for packed sequences. +We provide two implementations: + +JointSequencePack: Stores all sub-sequences for all-sequences in a single tensor. + It is more flexible but is less performant. In this implementation, understanding tokens + can be placed in either causal or full-attention sub-sequences. +FactoredSequencePack: + Stores causal/undersanding and full/generation sub-sequences as separate tensors. + It is less flexible but is more performant. In this implementation, understanding tokens + must be on the causal sub-sequence, and generation tokens must be in the full-attention sub-sequence. + +NOTES: + - We are aiming to deprecate and remove JointSequencePack; keeping it available for backwards compatibility at the moment. + - The reason we're implementing them via dict instead of python classes is to make torch.compile + activation checkpointing to work. + +is_sharded (bool): + This flag indicates whether the sequence pack contains global data or a local shard for Context Parallelism (CP). + - When True, tensors represent only the local slice (Global_Length / CP_World_Size). + - Padding and reconstruction logic is skipped in `from_joint`. + - Operations requiring global context (e.g., `get_all_seq`, position ID reconstruction) are not allowed when is_sharded is True. +""" + + +# "Fake" types for readability; everything is plain dict at runtime. +FactoredSequencePack = dict[str, Any] +JointSequencePack = dict[str, Any] +SequencePack = FactoredSequencePack | JointSequencePack + +# ------------------------------------ +# SequencePack: internal helpers +# ------------------------------------ + + +def _find_non_causal_text_token_idx( + attn_modes: List[str], split_lens: List[int], und_token_indexes: List[int] +) -> List[int]: + """ + Find the indexes of the "und" tokens that are under the "full" mode. + This are indices into the full_only_seq. + """ + # Return indexes *into* full_only_seq, not into the original packed sequence. + # The order within full_only_seq is the concatenation of each "full" split in order. + out = [] + full_offset = 0 + packed_idx = 0 + und_token_set = set(und_token_indexes) + for attn_mode, split_len in zip(attn_modes, split_lens): + if attn_mode == "full": + split_indices = range(packed_idx, packed_idx + split_len) + # For this "full" split, find the und tokens within this split, mapped local to full_only_seq offset + for local_idx, split_idx in enumerate(split_indices): + if split_idx in und_token_set: + out.append(full_offset + local_idx) + full_offset += split_len + packed_idx += split_len + return out + + +def _compute_mode_indices_and_offsets( + split_lens: torch.Tensor | List[int], attn_modes: List[str], mode: str, device: torch.device +) -> tuple[torch.Tensor, torch.Tensor]: + """ + Compute indices from a joint tensor that are in the given mode. + """ + indices = [] + offsets = [0] + next_offset = 0 + start = 0 + + if isinstance(split_lens, torch.Tensor): + split_lens = split_lens.tolist() + + for i, (split_len, attn_mode) in enumerate(zip(split_lens, attn_modes)): + if attn_mode == mode: + indices.extend(range(start, start + split_len)) + next_offset += split_len + offsets.append(next_offset) + start += split_len + return torch.tensor(indices, dtype=torch.int32, device=device), torch.tensor( # [N_mode_tokens], [N_mode_splits+1] + offsets, dtype=torch.int32, device=device + ) + + +# Pad causal_seq and full_only_seq to have length 2048 if not already at that size +def _pad_to_N(N, x: torch.Tensor) -> torch.Tensor: + assert x.shape[0] <= N + padded = x.new_zeros((N, *x.shape[1:])) + padded[: x.shape[0]] = x + return padded + + +def _round_up_to_N(n: int, cp_world_size: int = 1, pad_for_cuda_graphs: bool = False) -> int: + if pad_for_cuda_graphs: + # Reduce recompilations / CUDA graph re-captures by bucketing lengths. + # <= 2K: 128, <= 4K: 256, <= 8K: 512, <= 16K: 1024, > 16K: 2048 + if n <= 2048: + alignment = 128 + elif n <= 4096: + alignment = 256 + elif n <= 8192: + alignment = 512 + elif n <= 16384: + alignment = 1024 + else: + alignment = 2048 + n = ((n + alignment - 1) // alignment) * alignment + + # ensure it's divisible by cp_world_size + if cp_world_size > 1: + remainder = n % cp_world_size + if remainder != 0: + n += cp_world_size - remainder + + return n + + +def _pad( + causal_seq: torch.Tensor, full_only_seq: torch.Tensor, max_causal_len: int, max_full_len: int +) -> tuple[torch.Tensor, torch.Tensor]: + causal_seq = _pad_to_N(max_causal_len, causal_seq) + full_only_seq = _pad_to_N(max_full_len, full_only_seq) + return causal_seq, full_only_seq + + +def _ensure_core_metadata(pack: SequencePack) -> None: + required = [ + "sample_offsets", + "max_sample_len", + "max_causal_len", + "max_full_len", + "_causal_indices", + "_full_indices", + "_causal_seq_offsets", + "_full_only_seq_offsets", + "is_sharded", + ] + for key in required: + if key not in pack: + raise KeyError(f"Missing required pack field: {key}") + + +def _init_sequence_pack( + sample_lens: List[int], + split_lens: List[int], + attn_modes: List[str], + device: torch.device, +) -> dict[str, Any]: + _max_sample_len = max(sample_lens) + _max_causal_len = max((split_lens[i] for i in range(len(split_lens)) if attn_modes[i] == "causal"), default=0) + _max_full_len = max((split_lens[i] for i in range(len(split_lens)) if attn_modes[i] == "full"), default=0) + + sample_lens_cu = torch.tensor([0] + sample_lens, device=device, dtype=torch.int32) # [N_samples+1] + _sample_offsets = torch.cumsum(sample_lens_cu, dim=0, dtype=torch.int32) # [N_samples+1] + + _causal_indices, _causal_seq_offsets = _compute_mode_indices_and_offsets(split_lens, attn_modes, "causal", device) + _full_indices, _full_only_seq_offsets = _compute_mode_indices_and_offsets(split_lens, attn_modes, "full", device) + + return dict( + sample_offsets=_sample_offsets, + max_sample_len=_max_sample_len, + max_causal_len=_max_causal_len, + max_full_len=_max_full_len, + _causal_indices=_causal_indices, + _full_indices=_full_indices, + _causal_seq_offsets=_causal_seq_offsets, + _full_only_seq_offsets=_full_only_seq_offsets, + _num_causal_tokens=len(_causal_indices), + _num_full_tokens=len(_full_indices), + split_lens=split_lens, + attn_modes=attn_modes, + ) + + +# ------------------------------------ +# SequencePack constructors +# ------------------------------------ + + +def _round_up_for_cuda_graphs_or_cp( + causal_seq: torch.Tensor, + full_only_seq: torch.Tensor, + need_causal: int, + need_full: int, + is_image_batch: bool, + pad_for_cuda_graphs: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + """Pad causal/full sequences to the required lengths, growing global bounds for CUDA graphs.""" + if pad_for_cuda_graphs: + global \ + MAX_CAUSAL_LEN_IMAGE_BATCH, \ + MAX_FULL_LEN_IMAGE_BATCH, \ + MAX_CAUSAL_LEN_VIDEO_BATCH, \ + MAX_FULL_LEN_VIDEO_BATCH + if is_image_batch: + if need_causal > MAX_CAUSAL_LEN_IMAGE_BATCH: + MAX_CAUSAL_LEN_IMAGE_BATCH = need_causal + log.info(f"Growing MAX_CAUSAL_LEN_IMAGE_BATCH to {MAX_CAUSAL_LEN_IMAGE_BATCH}", rank0_only=False) + if need_full > MAX_FULL_LEN_IMAGE_BATCH: + MAX_FULL_LEN_IMAGE_BATCH = need_full + log.info(f"Growing MAX_FULL_LEN_IMAGE_BATCH to {MAX_FULL_LEN_IMAGE_BATCH}", rank0_only=False) + causal_seq, full_only_seq = _pad( + causal_seq, + full_only_seq, + max_causal_len=MAX_CAUSAL_LEN_IMAGE_BATCH, + max_full_len=MAX_FULL_LEN_IMAGE_BATCH, + ) + else: + if need_causal > MAX_CAUSAL_LEN_VIDEO_BATCH: + MAX_CAUSAL_LEN_VIDEO_BATCH = need_causal + log.info(f"Growing MAX_CAUSAL_LEN_VIDEO_BATCH to {MAX_CAUSAL_LEN_VIDEO_BATCH}", rank0_only=False) + if need_full > MAX_FULL_LEN_VIDEO_BATCH: + MAX_FULL_LEN_VIDEO_BATCH = need_full + log.info(f"Growing MAX_FULL_LEN_VIDEO_BATCH to {MAX_FULL_LEN_VIDEO_BATCH}", rank0_only=False) + causal_seq, full_only_seq = _pad( + causal_seq, + full_only_seq, + max_causal_len=MAX_CAUSAL_LEN_VIDEO_BATCH, + max_full_len=MAX_FULL_LEN_VIDEO_BATCH, + ) + elif need_causal != int(causal_seq.shape[0]) or need_full != int(full_only_seq.shape[0]): + causal_seq, full_only_seq = _pad(causal_seq, full_only_seq, need_causal, need_full) + return causal_seq, full_only_seq + + +def factored_from_joint_sequence( + packed_sequence: torch.Tensor, + attn_modes: List[str], + split_lens: List[int], + sample_lens: List[int], + packed_und_token_indexes: torch.Tensor, + packed_gen_token_indexes: torch.Tensor, + is_image_batch: bool = False, + cp_world_size: int = 1, + pad_for_cuda_graphs: bool = False, +) -> FactoredSequencePack: + """ + Create a factored sequence pack from a packed sequence and metadata. + NOTE: Some arguments seem redundant because they in principle support more flexible sequence setups. + This constructor checks that the required invariants for FactoredSequencePack are satisfied. + NOTE: This constructor checks that there are no "und" tokens under "full" mode, and no "gen" tokens under "causal" mode, + since this is a requirement for FactoredSequencePack. + Args: + packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences. + attn_modes (List[str]): List of attention modes. Must be alternating ["causal", "full", ... "causal", "full"] + split_lens (List[int]): Length of each subsequence. len(split_lens) == len(attn_modes) + sample_lens (List[int]): Length of each sequence. len(sample_lens) == number of samples. + packed_und_token_indexes (torch.Tensor): The indexes of the understanding tokens in the packed sequence. + packed_gen_token_indexes (torch.Tensor): The indexes of the generating tokens in the packed sequence. + """ + del packed_gen_token_indexes + + non_causal_text_idxs = _find_non_causal_text_token_idx(attn_modes, split_lens, packed_und_token_indexes.tolist()) + assert len(non_causal_text_idxs) == 0, "non_causal_text_idxs should be empty" + + assert sum(sample_lens) == packed_sequence.shape[0], ( + "sum(sample_lens) must be equal to the length of the packed sequence" + ) + + meta = _init_sequence_pack(sample_lens, split_lens, attn_modes, packed_sequence.device) + causal_seq = packed_sequence[meta["_causal_indices"]] # [N_causal_tokens,D] + full_only_seq = packed_sequence[meta["_full_indices"]] # [N_full_tokens,D] + + need_causal = _round_up_to_N(int(causal_seq.shape[0]), cp_world_size, pad_for_cuda_graphs) + need_full = _round_up_to_N(int(full_only_seq.shape[0]), cp_world_size, pad_for_cuda_graphs) + + causal_seq, full_only_seq = _round_up_for_cuda_graphs_or_cp( + causal_seq, + full_only_seq, + need_causal, + need_full, + is_image_batch, + pad_for_cuda_graphs, + ) + + pack: FactoredSequencePack = { + **meta, + "max_num_tokens": sum(sample_lens), + "causal_seq": causal_seq, + "full_only_seq": full_only_seq, + "is_sharded": False, + } + return pack + + +def _validate_single_dim_params(params: Mapping, layer_idx: int, num_dims: int | None) -> dict: + """ + Helper function to validate NATTEN parameters for a dimensionality profile. + + Args: + params (Mapping): parameter dict with window_size/window_size_float and other params + layer_idx (int): layer index for error messages + num_dims (int | None): 1, 2, 3, or None (for single-profile format) + + Returns: + dict: validated parameter dict with proper types + """ + if not isinstance(params, Mapping): + dim_str = f" ({num_dims}-D)" if num_dims else "" + raise ValueError(f"Parameters for layer {layer_idx}{dim_str} must be a dict or None, got {params=}.") + + is_causal = False if "is_causal" not in params else params["is_causal"] + + if "window_size_float" in params: + window_size_float = params["window_size_float"] + if ( + not isinstance(window_size_float, Sequence) + or len(window_size_float) not in [1, 2, 3] + or any(not isinstance(x, float) for x in window_size_float) + ): + raise ValueError(f"'window_size_float' must be a float tuple of size 1, 2, or 3, got {window_size_float=}") + window_size_float = tuple(k for k in window_size_float) + + num_dims = len(window_size_float) + + def check_stride_dilation(x): + if isinstance(x, float): + if 0.0 <= x <= 1.0: + return tuple(x for _ in range(num_dims)) + elif ( + isinstance(x, Sequence) + and len(x) == num_dims + and all(isinstance(y, float) and 0.0 <= y <= 1.0 for y in x) + ): + return tuple(y for y in x) + else: + raise ValueError(f"Invalid natten float parameter: {x=}") + + stride_float = 0.0 if "stride_float" not in params else params["stride_float"] + dilation_float = 0.0 if "dilation_float" not in params else params["dilation_float"] + + stride_float = check_stride_dilation(stride_float) + dilation_float = check_stride_dilation(dilation_float) + is_causal = check_valid_tuple_or_element( + is_causal, num_dims=num_dims, typename=bool, raise_error=True, param_name="is_causal" + ) + + if any(x in params for x in ["window_size", "stride", "dilation"]): + raise ValueError( + f"Please either use _float parameters, or integer ones, and not mix the two. Got {params=}." + ) + + return { + "window_size_float": window_size_float, + "stride_float": stride_float, + "dilation_float": dilation_float, + "is_causal": is_causal, + } + + elif "window_size" in params: + window_size = params["window_size"] + num_dims = len(window_size) + + stride = 1 if "stride" not in params else params["stride"] + dilation = 1 if "dilation" not in params else params["dilation"] + + if any("_float" in x for x in params.keys()): + raise ValueError( + f"Please either use _float parameters, or integer ones, and not mix the two. Got {params=}." + ) + + window_size = check_valid_tuple_or_element( + window_size, num_dims=num_dims, typename=int, raise_error=True, param_name="window_size" + ) + stride = check_valid_tuple_or_element( + stride, num_dims=num_dims, typename=int, raise_error=True, param_name="stride" + ) + dilation = check_valid_tuple_or_element( + dilation, num_dims=num_dims, typename=int, raise_error=True, param_name="dilation" + ) + is_causal = check_valid_tuple_or_element( + is_causal, num_dims=num_dims, typename=bool, raise_error=True, param_name="is_causal" + ) + + return {"window_size": window_size, "stride": stride, "dilation": dilation, "is_causal": is_causal} + else: + raise ValueError( + "Sparse parameters for a layer must have key 'window_size' or 'window_size_float', " + f"got {params=} in layer index {layer_idx}." + ) + + +def verify_natten_parameter_list( + natten_parameter_list: list | None, + num_layers: int, +) -> list | None: + """ + Converts list of NATTEN parameters into expected types, and assigns defaults to unset + parameters. + This needs to be done separately during model initialization, and not forward pass. + There are no torch operations in this function. + + Args: + natten_parameter_list (list | None): list of NATTEN parameters. Must be either None, or a + list of mappings, one for each layer. Each list element must be either None, + representing no sparsity / masking (full dense attention), or a mapping of NATTEN + parameters. + + Parameters can be specified directly with integer or float format: + - 'window_size_float' (required), 'stride_float', 'dilation_float' + - 'window_size' (required), 'stride', 'dilation' + + Or, parameters can be specified for multiple dimensionality profiles in case of + mixed-training (i.e. image and video training) using keys "1d", "2d", "3d": + - Each key maps to either None (dense attention) or a parameter dict + + Integer and float parameters cannot be used together in the same layer! + Additionally, you can specify 'is_causal'. + + Examples: + ``` + # 50 percent sparsity along each dimension in a 2-D token layout + {'window_size_float': (0.5, 0.5)} # valid + + # 50 percent sparsity along each dimension in a 2-D token layout + # Maximum dilation along first dimension, no dilation along second dimension + {'window_size_float': (0.5, 0.5), 'dilation_float': (1.0, 0.0)} # valid + + # Fixed window size of 8x8, dilation of 2x1. + + {'window_size': (8, 8), 'dilation': (2, 1)} # valid + + # Multi-profile: different parameters for 2D (images) and 3D (videos) + { + "2d": {"window_size_float": (0.5, 0.5)}, + "3d": {"window_size_float": (1.0, 0.5, 0.5)} + } # valid + + # Multi-profile: 2D uses dense attention, 3D uses sparse + { + "2d": None, + "3d": {"window_size_float": (1.0, 0.5, 0.5)} + } # valid + + # Invalid: + {'window_size_float': (0.5, 0.5), 'dilation': (2, 1)} + ``` + + num_layers (int): number of layers in the model. Just used to verify list length. + + Returns: + output_parameter_list (list | None): verified and type-checked NATTEN parameters, or None if + no parameters passed. + """ + + if natten_parameter_list is not None: + parameter_list_out = [] + if not isinstance(natten_parameter_list, Sequence): + raise ValueError(f"Argument 'natten_parameter_list' must be a list or None, got {natten_parameter_list=}.") + + if len(natten_parameter_list) != num_layers: + raise ValueError( + "Number of elements in 'natten_parameter_list' must match number of layers " + f"in the model, got {num_layers=}, {len(natten_parameter_list)=}." + ) + + for i, layer_parameters in enumerate(natten_parameter_list): + if layer_parameters is None: + log.debug(f"Layer {i} will use DENSE attention.") + parameter_list_out.append(None) + continue + + if not isinstance(layer_parameters, Mapping): + raise ValueError( + f"Sparse parameters for a layer must be a dict or None, got {layer_parameters=} in layer index {i}." + ) + + # Detect format: multi-profile if has keys "1d", "2d", or "3d" + dim_keys = {"1d", "2d", "3d"} + has_dim_keys = any(k in layer_parameters for k in dim_keys) + + if has_dim_keys: + # Multi-profile format: validate each explicitly defined dimensionality profile + validated_multi_profile = {} + for dim_str, dim_int in [("1d", 1), ("2d", 2), ("3d", 3)]: + if dim_str in layer_parameters: + dim_params = layer_parameters[dim_str] + if dim_params is None: + validated_multi_profile[dim_int] = None + else: + validated_multi_profile[dim_int] = _validate_single_dim_params(dim_params, i, dim_int) + else: + # Single-profile format: validate and convert to multi-profile format + # Infer dimensionality from parameter tuple length + validated_params = _validate_single_dim_params(layer_parameters, i, None) + if "window_size_float" in validated_params: + num_dims = len(validated_params["window_size_float"]) + else: # "window_size" + num_dims = len(validated_params["window_size"]) + validated_multi_profile = {num_dims: validated_params} + + # If all explicitly defined profiles are None, treat as fully dense layer + if all(v is None for v in validated_multi_profile.values()): + log.debug(f"Layer {i} will use DENSE attention (all profiles None).") + parameter_list_out.append(None) + else: + parameter_list_out.append(validated_multi_profile) + log.info(f"Layer {i} NATTEN parameters: {validated_multi_profile}") + + return parameter_list_out + + return None + + +def generate_natten_metadata( + token_shapes: list[tuple[int, int, int]], + head_dim: int, + num_layers: int, + device: torch.device, + dtype: torch.dtype, + requires_grad: bool, + natten_parameter_list: list | None = None, +) -> list | None: + """ + Generates list of metadata required by Variable-Sized (variable-length) operations in NATTEN. + Required when training with three_way attention and NATTEN (multi-dimensional / sparse + attention). + + Args: + token_shapes (list[tuple]): list of integer tuples corresponding to the + post-tokenization/patchify token layout shapes in the packed sequence. Must strictly be + integer tuples with the same profile (all 1D, 2D, or 3D). 1s will be automatically + stripped (i.e. [(1, 8, 8), (1, 16, 16)] is interpreted as [(8, 8), (16, 16)]). + + head_dim (int): Attention head dimension (used to select NATTEN kernel configurations). + + num_layers (int): number of layers in the model. Just used to verify list length. + + device (torch.device): PyTorch device for offset tensors (should match QKV device). + + dtype (torch.dtype): Expected QKV dtype. + + requires_grad (bool): Determines whether backprop is expected, and sets up metadata for + backward pass as well. + + natten_parameter_list (list | None): list of NATTEN parameters. Must be either None, or a + list of mappings, one for each layer. Each list element must be either None, + representing no sparsity / masking (full dense attention), or a mapping of NATTEN + parameters in either integer or float format: + - 'window_size_float' (required), 'stride_float', 'dilation_float' + - 'window_size' (required), 'stride', 'dilation' + + Integer and float parameters cannot be used together in the same layer! + Additionally, you can specify 'is_causal'. + + Examples: + ``` + # 50 percent sparsity along each dimension in a 2-D token layout + {'window_size_float': (0.5, 0.5)} # valid + + # 50 percent sparsity along each dimension in a 2-D token layout + # Maximum dilation along first dimension, no dilation along second dimension + {'window_size_float': (0.5, 0.5), 'dilation_float': (1.0, 0.0)} # valid + + # Fixed window size of 8x8, dilation of 2x1. + + {'window_size': (8, 8), 'dilation': (2, 1)} # valid + + # Invalid: + {'window_size_float': (0.5, 0.5), 'dilation': (2, 1)} + ``` + + Returns: + natten_metadata_list (list | None): list of NATTEN varlen metadata, or Nones (dense layers). + Each non-None element will be a dictionary containing final parameters, and varlen + metadata (offset and size tensors, max lengths). + NOTE: to avoid excessive recompilations in torch.compile, we must carefully index into + this list during model.forward, and ideally using the iteration counter from the loop + over layers (nn.ModuleList). + """ + + + if token_shapes is None or len(token_shapes) < 1: + raise ValueError("'token_shapes' is required for 'three_way' attention.") + + natten_metadata = None + + if natten_parameter_list is not None: + natten_metadata = [] + if not isinstance(natten_parameter_list, list): + raise ValueError(f"Argument 'natten_parameter_list' must be a list or None, got {natten_parameter_list=}.") + + if len(natten_parameter_list) != num_layers: + raise ValueError( + "Number of elements in 'natten_parameter_list' must match number of layers " + f"in the model, got {num_layers=}, {len(natten_parameter_list)=}." + ) + + # We need to filter out 1s from shapes + def filter_shape(shape: tuple) -> tuple: + return tuple(x for x in shape if x > 1) + + # Infer token layout rank (dimensionality) + num_dims = max([len(filter_shape(token_shape)) for token_shape in token_shapes]) + + # Single pass: check if all layers support this dimensionality and if any need processing + needs_processing = False + for i, layer_parameters in enumerate(natten_parameter_list): + if layer_parameters is None: + continue + + # Fail fast if this dimensionality is not defined + if num_dims not in layer_parameters: + raise ValueError( + f"Layer {i}: batch has {num_dims}D data but parameters are not defined for {num_dims}D. " + f"Defined dimensionalities: {sorted(layer_parameters.keys())}" + ) + + # Check if this layer needs processing for this dimensionality + if layer_parameters[num_dims] is not None: + needs_processing = True + + # Early exit if all layers are dense for this dimensionality profile + if not needs_processing: + log.debug(f"All layers use DENSE attention for {num_dims}D data.") + return None + + # We actually need to process, so validate and filter all shapes + token_layout_list = [] + for shape in token_shapes: + assert isinstance(shape, tuple) + shape_filtered = filter_shape(shape) + assert len(shape_filtered) == num_dims, ( + f"All data in batch must have same dimensionality, got {num_dims}D and {len(shape_filtered)}D" + ) + token_layout_list.append(shape_filtered) + + log.debug(f"Batch dimensionality: {num_dims}D, token_layout_list={token_layout_list}") + + for i, layer_parameters in enumerate(natten_parameter_list): + if layer_parameters is None: + natten_metadata.append(None) + continue + + # Get parameters for this dimensionality (already validated above) + dim_params = layer_parameters[num_dims] + + if dim_params is None: + # Dense attention for this dimensionality + natten_metadata.append(None) + continue + + # Use dim_params (parameters for this specific dimensionality) + window_size_list = [] + stride_list = [] + dilation_list = [] + + if "window_size_float" in dim_params: + window_size_float = dim_params["window_size_float"] + stride_float = dim_params["stride_float"] + dilation_float = dim_params["dilation_float"] + + for token_layout in token_layout_list: + window_size_ = tuple( + min(x, max(2, int(k * float(x)))) for k, x in zip(window_size_float, token_layout) + ) + stride_ = tuple(min(k, max(1, int(s * float(k)))) for s, k in zip(stride_float, window_size_)) + max_dilation = tuple(x // k for k, x in zip(window_size_, token_layout)) + dilation_ = tuple(min(m, max(1, int(d * float(m)))) for d, m in zip(dilation_float, max_dilation)) + + window_size_list.append(window_size_) + stride_list.append(stride_) + dilation_list.append(dilation_) + + assert len(window_size_list) == len(stride_list) == len(dilation_list) == len(token_layout_list) + + log.debug(f"Layer {i}: {window_size_list=}") + log.debug(f"Layer {i}: {stride_list=}") + log.debug(f"Layer {i}: {dilation_list=}") + + elif "window_size" in dim_params: + window_size = dim_params["window_size"] + stride = dim_params["stride"] + dilation = dim_params["dilation"] + + window_size_list = [window_size for _ in range(len(token_layout_list))] + stride_list = [stride for _ in range(len(token_layout_list))] + dilation_list = [dilation for _ in range(len(token_layout_list))] + else: + raise ValueError( + "Sparse parameters for a layer must have key 'window_size' or 'window_size_float', " + f"got {dim_params=} in layer index {i}." + ) + + is_causal = dim_params["is_causal"] + + # Create varlen metadata for natten varlen/varsized ops + + # full size, that's why constant window sizes aren't allowed. + + natten_metadata.append( + generate_multi_dim_varlen_parameters( + token_layout_list=token_layout_list, + head_dim=head_dim, + device=device, + dtype=dtype, + requires_grad=requires_grad, + # + window_size_list=window_size_list, + stride_list=stride_list, + dilation_list=dilation_list, + # + is_causal=is_causal, + ) + ) + + return natten_metadata + + +def generate_temporal_causal_natten_metadata( + vision_token_shapes: list[tuple[int, int, int]], + num_action_tokens_per_supertoken: int, + num_layers: int, + head_dim: int, + device: torch.device, + dtype: torch.dtype, + requires_grad: bool, +) -> list: + """Generate per-layer varlen metadata for temporal causal attention on supertokens. + + Each sample's generation tokens are laid out as T_i supertokens of size + S_i = num_action_tokens_per_supertoken + H_i*W_i. Metadata encodes + is_causal=(True, False): causal across T, full within S. All layers share + the same metadata (full window, no spatial sparsity). + + Unlike generate_natten_metadata, this function does not apply filter_shape — (T, S) layouts + are passed directly even when T=1. NATTEN handles T=1 causal masking correctly (trivially + full attention within S). + + Args: + vision_token_shapes: List of (T, H, W) per sample. + num_action_tokens_per_supertoken: Number of action tokens prefixing each + supertoken (0 when actions are not packed inline). + num_layers: Number of transformer layers. + head_dim: Attention head dimension. + device: Target device. + dtype: Target dtype. + requires_grad: Whether metadata tensors require gradient. + + Returns: + List of length num_layers, each element the same NATTEN varlen metadata dict. + """ + # T=1: NATTEN requires kernel_size >= 2 and kernel_size <= token_layout, which are mutually + # exclusive when T=1. Fall back to full dense attention (None) — a single supertoken trivially + # attends to only itself, so temporal causality is already satisfied. + # Mixed T=1/T>1 batches are rejected: NATTEN can't mask T=1 samples, and falling back to dense + # attention for the whole batch would break temporal causality for the T>1 samples. + # Ensure min_frames >= 5 in the dataloader so that T_latent = 1 + (N-1)//tcf >= 2 always. + has_short = any(t < 2 for t, h, w in vision_token_shapes) + if has_short: + if not all(t < 2 for t, h, w in vision_token_shapes): + raise ValueError( + "Mixed T=1 and T>1 samples in causal training batch: NATTEN cannot apply " + "causal masking when any sample has T=1 (kernel_size constraint), and falling " + "back to dense attention would break temporal causality for T>1 samples. " + "Ensure all samples have T_latent >= 2 (set min_frames >= 5 in the dataloader)." + ) + return [None] * num_layers + token_layout_list = [(t, num_action_tokens_per_supertoken + h * w) for t, h, w in vision_token_shapes] + metadata = generate_multi_dim_varlen_parameters( + token_layout_list=token_layout_list, + head_dim=head_dim, + device=device, + dtype=dtype, + requires_grad=requires_grad, + is_causal=(True, False), + ) + return [metadata] * num_layers + + +def joint_from_joint_sequence( + packed_sequence: torch.Tensor, + attn_modes: List[str], + split_lens: List[int], + sample_lens: List[int], + packed_und_token_indexes: torch.Tensor, + packed_gen_token_indexes: torch.Tensor, + is_image_batch: bool = False, + cp_world_size: int = 1, + pad_for_cuda_graphs: bool = False, +) -> JointSequencePack: + f""" + Create a JointSequencePack from a packed sequence and metadata. + This is in order to support the legacy joint flex-attention implementation. + Differently from FactoredSequencePack, it has less strict requirements on the packed sequence. + + Args: + packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences. + attn_modes (List[str]): List of attention modes. Supports any sequence of {"causal", "full", "noise"} + split_lens (List[int]): Length of each subsequence. len(split_lens) == len(attn_modes) + sample_lens (List[int]): Length of each sequence. In this mode, sequences may have different number of splits, + as opposed to FactoredSequencePack where each sequence has exactly two splits.. + packed_und_token_indexes (torch.Tensor): The indexes of the understanding tokens in the packed sequence. + packed_gen_token_indexes (torch.Tensor): The indexes of the generating tokens in the packed sequence. + """ + assert sum(sample_lens) == packed_sequence.shape[0], ( + "sum(sample_lens) must be equal to the length of the packed sequence" + ) + meta = _init_sequence_pack(sample_lens, split_lens, attn_modes, packed_sequence.device) + pack: JointSequencePack = { + **meta, + "max_num_tokens": sum(sample_lens), + "packed_sequence": packed_sequence, + "packed_und_token_indexes": packed_und_token_indexes, + "packed_gen_token_indexes": packed_gen_token_indexes, + "is_sharded": False, + } + return pack + + +def zeros_like(orig: FactoredSequencePack | JointSequencePack, shape: Tuple[int, ...] | torch.Size | None = None): + """ + Create a new sequence pack with the same metadata as the original, but with all tokens set to zero. + Args: + orig (FactoredSequencePack | JointSequencePack): The original sequence pack to copy metadata from. + shape (Tuple[int, ...] | torch.Size | None): The shape of the new sequence pack. If None, the shape will be the same as the original. + """ + _ensure_core_metadata(orig) + if "packed_sequence" in orig: + if shape is None: + shape_ = orig["packed_sequence"].shape + else: + assert len(shape) >= 1 and shape[0] == -1 + shape_ = (orig["packed_sequence"].shape[0],) + tuple(shape)[1:] + packed_sequence = torch.zeros( + shape_, device=orig["packed_sequence"].device, dtype=orig["packed_sequence"].dtype + ) # [seq_len,D] + return from_joint(packed_sequence, orig) + else: + if shape is None: + shape_causal = orig["causal_seq"].shape + shape_full = orig["full_only_seq"].shape + else: + assert len(shape) >= 1 and shape[0] == -1 + shape_causal = (orig["causal_seq"].shape[0],) + tuple(shape)[1:] + shape_full = (orig["full_only_seq"].shape[0],) + tuple(shape)[1:] + causal_seq = torch.zeros( + shape_causal, device=orig["causal_seq"].device, dtype=orig["causal_seq"].dtype + ) # [N_causal_tokens,D] + full_only_seq = torch.zeros( + shape_full, device=orig["full_only_seq"].device, dtype=orig["full_only_seq"].dtype + ) # [N_full_tokens,D] + return from_mode_splits(causal_seq, full_only_seq, orig) + + +def from_joint(packed_sequence: torch.Tensor, metadata_source: FactoredSequencePack | JointSequencePack): + """ + Create a new sequence pack from a packed sequence and another sequence pack with the same metadata. + Args: + packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences. + metadata_source (FactoredSequencePack | JointSequencePack): The metadata source to copy from. + """ + _ensure_core_metadata(metadata_source) + if "packed_sequence" in metadata_source: + out = dict(metadata_source) + out["packed_sequence"] = packed_sequence + return out + else: + if metadata_source["is_sharded"]: + # Use sharded sequences as is when is_sharded is True (used in Context Parallel) + causal_seq = packed_sequence[: len(metadata_source["causal_seq"])] # [N_causal_tokens,D] + full_only_seq = packed_sequence[len(metadata_source["causal_seq"]) :] # [N_full_tokens,D] + else: + causal_seq = packed_sequence[metadata_source["_causal_indices"]] # [N_causal_tokens,D] + full_only_seq = packed_sequence[metadata_source["_full_indices"]] # [N_full_tokens,D] + causal_seq, full_only_seq = _pad( + causal_seq, + full_only_seq, + max_causal_len=metadata_source["causal_seq"].shape[0], + max_full_len=metadata_source["full_only_seq"].shape[0], + ) + + return from_mode_splits(causal_seq, full_only_seq, metadata_source) + + +def from_mode_splits( + causal_seq: torch.Tensor, + full_only_seq: torch.Tensor, + orig: FactoredSequencePack | JointSequencePack, + is_sharded: bool | None = None, +): + """ + Create a new sequence pack from two mode splits. + Args: + causal_seq (torch.Tensor): The causal sequence. + full_only_seq (torch.Tensor): The full-only sequence. + orig (FactoredSequencePack | JointSequencePack): The metadata source to copy from. + is_sharded (bool | None): If True, create a local pack for context parallel. + If None, inherits from orig. + """ + _ensure_core_metadata(orig) + if is_sharded is None: + is_sharded = orig.get("is_sharded", False) + + if "packed_sequence" in orig: + all_len = int(orig["_causal_indices"].shape[0] + orig["_full_indices"].shape[0]) + packed_sequence = causal_seq.new_zeros((all_len, *causal_seq.shape[1:])) # [seq_len,D] + packed_sequence[orig["_causal_indices"]] = causal_seq + packed_sequence[orig["_full_indices"]] = full_only_seq + return from_joint(packed_sequence, orig) + else: + out = dict(orig) + out["causal_seq"] = causal_seq + out["full_only_seq"] = full_only_seq + out["is_sharded"] = is_sharded + return out + + +def from_und_gen_splits(und_seq: torch.Tensor, gen_seq: torch.Tensor, orig: FactoredSequencePack | JointSequencePack): + """ + Create a new sequence pack from two und/gen splits. + Args: + und_seq (torch.Tensor): The understanding sequence. + gen_seq (torch.Tensor): The generating sequence. + orig (FactoredSequencePack | JointSequencePack): The metadata source to copy from. + """ + # If we have a joint pack (single packed_sequence), place by und/gen indexes. + if "packed_sequence" in orig and "packed_und_token_indexes" in orig and "packed_gen_token_indexes" in orig: + all_len = int(und_seq.shape[0] + gen_seq.shape[0]) + packed_sequence = und_seq.new_zeros((all_len, *und_seq.shape[1:])) # [seq_len,D] + packed_sequence[orig["packed_und_token_indexes"]] = und_seq + packed_sequence[orig["packed_gen_token_indexes"]] = gen_seq + return from_joint(packed_sequence, orig) + # Otherwise, treat und/gen as mode splits (und == causal; gen == full). + return from_mode_splits(und_seq, gen_seq, orig) + + +# ------------------------------------ +# Getters and setters for SequencePack +# ------------------------------------ +def get_und_seq(pack: SequencePack) -> torch.Tensor: + """ + Get all understanding tokens in a sequence pack in a single tensor. + + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the understanding sequence from. + Returns: + torch.Tensor: All understanding tokens concatenated over all sequences in the batch. + """ + if "causal_seq" in pack: + return pack["causal_seq"] + if "packed_sequence" in pack and "packed_und_token_indexes" in pack: + return pack["packed_sequence"][pack["packed_und_token_indexes"]] + raise KeyError("Cannot derive und_seq from provided pack") + + +def set_und_seq(pack: SequencePack, value: torch.Tensor) -> None: + """ + Override the understanding tokens in a sequence pack. + The order of tokens passed in must correspond to the order of tokens returned by get_und_seq. + + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the understanding sequence in. + value (torch.Tensor): The understanding sequence to set. + """ + if "packed_sequence" in pack and "packed_und_token_indexes" in pack: + pack["packed_sequence"][pack["packed_und_token_indexes"]] = value + elif "causal_seq" in pack: + pack["causal_seq"] = value + else: + raise KeyError("Cannot set und_seq from provided pack") + + +def get_gen_seq(pack: SequencePack) -> torch.Tensor: + """ + Get all generating tokens in a sequence pack in a single tensor. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the generating sequence from. + Returns: + torch.Tensor: All generating tokens concatenated over all sequences in the batch. + """ + if "full_only_seq" in pack: + return pack["full_only_seq"] + if "packed_sequence" in pack and "packed_gen_token_indexes" in pack: + return pack["packed_sequence"][pack["packed_gen_token_indexes"]] + raise KeyError("Cannot derive gen_seq from provided pack") + + +def set_gen_seq(pack: SequencePack, value: torch.Tensor) -> None: + """ + Override the generating tokens in a sequence pack. + The order of tokens passed in must correspond to the order of tokens returned by get_gen_seq. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the generating sequence in. + value (torch.Tensor): The generating sequence to set. + """ + if "packed_sequence" in pack and "packed_gen_token_indexes" in pack: + pack["packed_sequence"][pack["packed_gen_token_indexes"]] = value + elif "full_only_seq" in pack: + pack["full_only_seq"] = value + else: + raise KeyError("Cannot set gen_seq from provided pack") + + +def get_all_seq(pack: SequencePack) -> torch.Tensor: + """ + Get all tokens in a sequence pack in a single tensor. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the all sequence from. + Returns: + torch.Tensor: All tokens concatenated over all sequences in the batch. + """ + if "all_seq" in pack: + return pack["all_seq"] + if "packed_sequence" in pack: + return pack["packed_sequence"] + if "causal_seq" in pack and "full_only_seq" in pack: + _ensure_core_metadata(pack) + if pack["is_sharded"]: + assert False, "get_all_seq is not supported in context parallel sharded mode" + else: + out = pack["causal_seq"].new_zeros( + int(pack["_causal_indices"].shape[0] + pack["_full_indices"].shape[0]), *pack["causal_seq"].shape[1:] + ) # [seq_len,D] + if pack["causal_seq"].shape[0] > 0: + out[pack["_causal_indices"]] = pack["causal_seq"][: pack["_causal_indices"].shape[0]] + if pack["full_only_seq"].shape[0] > 0: + out[pack["_full_indices"]] = pack["full_only_seq"][: pack["_full_indices"].shape[0]] + return out + raise KeyError("Cannot derive all_seq from provided pack") + + +def set_all_seq(pack: SequencePack, value: torch.Tensor) -> None: + """ + Override the all tokens in a sequence pack. + The order of tokens passed in must correspond to the order of tokens returned by get_all_seq. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the all sequence in. + value (torch.Tensor): The all sequence to set. + """ + if "packed_sequence" in pack: + pack["packed_sequence"] = value + elif "causal_seq" in pack and "full_only_seq" in pack: + _ensure_core_metadata(pack) + pack["causal_seq"][: pack["_causal_indices"].shape[0]] = value[pack["_causal_indices"]] + pack["full_only_seq"][: pack["_full_indices"].shape[0]] = value[pack["_full_indices"]] + else: + pack["all_seq"] = value + + +def get_causal_seq(pack: SequencePack) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Get the causal sequence and its offsets in a sequence pack. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the causal sequence from. + Returns: + Tuple[torch.Tensor, torch.Tensor]: The concatenated causal sub-sequences and the starting offset for each sub-sequence. + """ + _ensure_core_metadata(pack) + if "causal_seq" in pack: + return pack["causal_seq"], pack["_causal_seq_offsets"] + assert "packed_sequence" in pack + return pack["packed_sequence"][pack["_causal_indices"]], pack["_causal_seq_offsets"] + + +def get_full_only_seq(pack: SequencePack) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Get the full-only sequence and its offsets in a sequence pack. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the full-only sequence from. + Returns: + Tuple[torch.Tensor, torch.Tensor]: The concatenated full-only sub-sequences and the starting offset for each sub-sequence. + """ + _ensure_core_metadata(pack) + if "full_only_seq" in pack: + return pack["full_only_seq"], pack["_full_only_seq_offsets"] + assert "packed_sequence" in pack + return pack["packed_sequence"][pack["_full_indices"]], pack["_full_only_seq_offsets"] + + +def get_device_and_dtype(pack: SequencePack) -> Tuple[torch.device, torch.dtype]: + """ + Get the device and dtype of a sequence pack. + Args: + pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the device and dtype from. + Returns: + Tuple[torch.device, torch.dtype]: The device and dtype of the sequence pack. + """ + if "packed_sequence" in pack: + return pack["packed_sequence"].device, pack["packed_sequence"].dtype + if "causal_seq" in pack and "full_only_seq" in pack: + return pack["causal_seq"].device, pack["causal_seq"].dtype + raise KeyError("Cannot derive device and dtype from provided pack") + + +def build_sequence_plans_from_data_batch( + data_batch: dict, + input_video_key, + input_image_key: str, +) -> list[SequencePlan]: + """Build or retrieve sequence plans from a data batch dictionary. + + This function extracts sequence plans from the data batch if they exist, + otherwise creates default SequencePlan objects for each sample + in the batch. + + Args: + data_batch: Dictionary containing the data batch from the dataloader. + Expected keys include 'video' or other tensors to determine batch size. + If 'sequence_plan' key exists, those plans are returned directly. + + Returns: + List of SequencePlan objects, one per sample in the batch. + """ + + # For new modalities, please generate the sequence_plan in the dataset class!!!! + + # If sequence_plan already exists in data_batch, return it + if "sequence_plan" in data_batch: + return data_batch["sequence_plan"] + + assert "action" not in data_batch or data_batch["action"] is None, "Action data SHOULD have sequence_plans!" + assert "sound" not in data_batch or data_batch["sound"] is None, "Sound data SHOULD have sequence_plans!" + + + # Determine batch size from available tensors + batch_size = 0 + for key in [input_video_key, input_image_key]: + if key in data_batch: + val = data_batch[key] + if isinstance(val, torch.Tensor): + batch_size = val.shape[0] + break + elif isinstance(val, list): + batch_size = len(val) + break + + if batch_size == 0: + raise ValueError( + f"Cannot determine batch size from data_batch. Expected {input_video_key}, {input_image_key}, or similar key." + ) + + # Build default SequencePlan objects + return [ + SequencePlan( + has_text=True, # Has text prompt! + has_vision=True, + condition_frame_indexes_vision=[], # No conditioning frames! + ) + for _ in range(batch_size) + ] + + +# ============================================================================ +# Demo/Test function +# ============================================================================ + + +def main(): + """Demonstrate sequence packing with sample text and images.""" + # Initialize tokenizer and add special tokens + tokenizer = Qwen2Tokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct") + tokenizer, _ = add_special_tokens(tokenizer) + + # Define special tokens (Note: Qwen models don't have bos_token_id) + special_tokens = { + "eos_token_id": tokenizer.eos_token_id, + "start_of_generation": tokenizer.convert_tokens_to_ids("<|vision_start|>"), + "end_of_generation": tokenizer.convert_tokens_to_ids("<|vision_end|>"), + } + + # Sample text inputs + input_strings = ["Hello world", "How are you?", "I am fine"] + + # Tokenize input strings + input_text_tokens = [tokenizer.encode(text, add_special_tokens=False) for text in input_strings] + + # Create sample images (in practice, these would be VAE latents) + input_images = torch.stack([torch.randn(3, 1, 64, 64) for _ in range(3)]) # [B, C, T, H, W] format + + # Diffusion timesteps for each image + input_timesteps = torch.tensor([0.0, 0.5, 0.9]) + + # Create GenerationDataClean for images + gen_data_clean_images = GenerationDataClean( + batch_size=3, + is_image_batch=True, + raw_state_vision=input_images, + x0_tokens_vision=torch.randn(3, 16, 8, 8), # dummy tokenized latents + raw_state_action=None, + ) + + # Create SequencePlan for each sample (all have text and vision) + sequence_plans = [ + SequencePlan( + has_text=True, + has_vision=True, + has_action=False, + condition_frame_indexes_vision=[], + condition_frame_indexes_action=[], + ) + for _ in range(3) + ] + + # Pack sequences + packed_data = pack_input_sequence( + sequence_plans=sequence_plans, + input_text_indexes=input_text_tokens, + gen_data_clean=gen_data_clean_images, + input_timesteps=input_timesteps, + special_tokens=special_tokens, + include_end_of_generation_token=True, + ) + + # Display results (after finalize, fields are tensors) + print(f"Packed sequence length: {packed_data.sequence_length}") + assert isinstance(packed_data.text_ids, torch.Tensor) + print(f"Packed text IDs shape: {packed_data.text_ids.shape}") + if packed_data.vision: + assert isinstance(packed_data.vision.sequence_indexes, torch.Tensor) + print(f"VAE token indexes shape: {packed_data.vision.sequence_indexes.shape}") + print(f"Packed position_ids: {packed_data.position_ids}") + + ################## + ## Video data + input_videos = torch.stack([torch.randn(3, 5, 64, 64) for _ in range(2)]) # [B, C, T, H, W] format + + # Diffusion timesteps for each video + input_timesteps_video = torch.tensor([0.5, 0.9]) + + # Create GenerationDataClean for videos + gen_data_clean_videos = GenerationDataClean( + batch_size=2, + is_image_batch=False, + raw_state_vision=input_videos, + x0_tokens_vision=torch.randn(2, 16, 2, 8, 8), # dummy tokenized latents + raw_state_action=None, + ) + + # Create SequencePlan for video samples + sequence_plans_video = [ + SequencePlan( + has_text=True, + has_vision=True, + has_action=False, + condition_frame_indexes_vision=[], + condition_frame_indexes_action=[], + ) + for _ in range(2) + ] + + # Pack sequences + packed_data = pack_input_sequence( + sequence_plans=sequence_plans_video, + input_text_indexes=input_text_tokens[0:2], + gen_data_clean=gen_data_clean_videos, + input_timesteps=input_timesteps_video, + special_tokens=special_tokens, + include_end_of_generation_token=True, + ) + + # Display results (after finalize, fields are tensors) + print(f"Packed sequence length: {packed_data.sequence_length}") + assert isinstance(packed_data.text_ids, torch.Tensor) + print(f"Packed text IDs shape: {packed_data.text_ids.shape}") + if packed_data.vision: + assert isinstance(packed_data.vision.sequence_indexes, torch.Tensor) + print(f"VAE token indexes shape: {packed_data.vision.sequence_indexes.shape}") + print(f"Packed position_ids: {packed_data.position_ids}") + + +def get_und_position_ids(position_ids: torch.Tensor, meta: dict[str, Any]) -> torch.Tensor: + """ + Get the understanding position ids in a sequence pack. + Args: + position_ids (torch.Tensor): The position ids. Shape (seq_len,) for 1D RoPE + or (3, seq_len) for 3D mRoPE. + meta (dict[str, Any]): The metadata. + Returns: + torch.Tensor: The understanding position ids. + """ + assert not meta["is_sharded"], "get_und_position_ids is not supported in context parallel sharded mode" + if position_ids.dim() == 2: + # 3D mRoPE: position_ids is (3, seq_len) + return position_ids[:, meta["_causal_indices"]] # [3,N_causal_tokens] + return position_ids[meta["_causal_indices"]] # [N_causal_tokens] + + +def get_gen_position_ids(position_ids: torch.Tensor, meta: dict[str, Any]) -> torch.Tensor: + """ + Get the generating position ids in a sequence pack. + Args: + position_ids (torch.Tensor): The position ids. Shape (seq_len,) for 1D RoPE + or (3, seq_len) for 3D mRoPE. + meta (dict[str, Any]): The metadata. + Returns: + torch.Tensor: The generating position ids. + """ + assert not meta["is_sharded"], "get_gen_position_ids is not supported in context parallel sharded mode" + if position_ids.dim() == 2: + # 3D mRoPE: position_ids is (3, seq_len) + return position_ids[:, meta["_full_indices"]] # [3,N_full_tokens] + return position_ids[meta["_full_indices"]] # [N_full_tokens] + + +if __name__ == "__main__": + main() diff --git a/cosmos3/_src/vfm/datasets/utils.py b/cosmos3/_src/vfm/datasets/utils.py new file mode 100644 index 00000000..263d6a40 --- /dev/null +++ b/cosmos3/_src/vfm/datasets/utils.py @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from typing import List, Tuple + +IMAGE_RES_SIZE_INFO: dict[str, dict[str, tuple[int, int]]] = { + # Our desired 256 resolution is the one below (commented). + + # Desired: "256": {"1,1": (336, 336), "4,3": (384, 288), "3,4": (288, 384), "16,9": (448, 256), "9,16": (256, 448)}, + "256": { + "1,1": (256, 256), + "4,3": (320, 256), + "3,4": (256, 320), + "16,9": (320, 192), + "9,16": (192, 320), + }, + "480": {"1,1": (640, 640), "4,3": (736, 544), "3,4": (544, 736), "16,9": (832, 480), "9,16": (480, 832)}, + # 704 resolutions are nicely divisible by 32 + "704": {"1,1": (960, 960), "4,3": (1088, 832), "3,4": (832, 1088), "16,9": (1280, 704), "9,16": (704, 1280)}, + "720": {"1,1": (960, 960), "4,3": (1104, 832), "3,4": (832, 1104), "16,9": (1280, 720), "9,16": (720, 1280)}, + "1080": {"1,1": (1440, 1440), "4,3": (1664, 1248), "3,4": (1248, 1664), "16,9": (1920, 1080), "9,16": (1080, 1920)}, + "1280": {"1,1": (1712, 1712), "4,3": (1968, 1472), "3,4": (1472, 1968), "16,9": (2272, 1280), "9,16": (1280, 2272)}, + "2048": { + "1,1": (2728, 2728), + "4,3": (3160, 2368), + "3,4": (2368, 3160), + "16,9": (3640, 2048), + "9,16": (2048, 3640), + }, + "gt_2048": { + "1,1": (5464, 5464), + "4,3": (6304, 4728), + "3,4": (4728, 6304), + "16,9": (7280, 4096), + "9,16": (4096, 7280), + }, +} + +VIDEO_RES_SIZE_INFO: dict[str, dict[str, tuple[int, int]]] = { + # Our desired 256 resolution is the one below (commented). + + # Desired: "256": {"1,1": (336, 336), "4,3": (384, 288), "3,4": (288, 384), "16,9": (448, 256), "9,16": (256, 448)}, + "256": { + "1,1": (256, 256), + "4,3": (320, 256), + "3,4": (256, 320), + "16,9": (320, 192), + "9,16": (192, 320), + }, + "480": {"1,1": (640, 640), "4,3": (736, 544), "3,4": (544, 736), "16,9": (832, 480), "9,16": (480, 832)}, + # 704 resolutions are nicely divisible by 32 + "704": {"1,1": (960, 960), "4,3": (1088, 832), "3,4": (832, 1088), "16,9": (1280, 704), "9,16": (704, 1280)}, + "720": {"1,1": (960, 960), "4,3": (1104, 832), "3,4": (832, 1104), "16,9": (1280, 720), "9,16": (720, 1280)}, + "1080": {"1,1": (1440, 1440), "4,3": (1664, 1248), "3,4": (1248, 1664), "16,9": (1920, 1080), "9,16": (1080, 1920)}, + "1280": {"1,1": (1712, 1712), "4,3": (1968, 1472), "3,4": (1472, 1968), "16,9": (2272, 1280), "9,16": (1280, 2272)}, + "2048": { + "1,1": (2728, 2728), + "4,3": (3160, 2368), + "3,4": (2368, 3160), + "16,9": (3640, 2048), + "9,16": (2048, 3640), + }, + "gt_2048": { + "1,1": (5464, 5464), + "4,3": (6304, 4728), + "3,4": (4728, 6304), + "16,9": (7280, 4096), + "9,16": (4096, 7280), + }, +} + + +def get_aspect_ratios_from_wdinfos(wdinfos: list[str]) -> list[str]: + aspect_ratios = [] + for wdinfo in wdinfos: + aspect_ratio_match = re.search(r"aspect_ratio_(\d+_\d+)", wdinfo) + aspect_ratios.append(aspect_ratio_match.group(1)) + + return aspect_ratios + + +def get_wdinfos_w_aspect_ratio(wdinfos: list[str]) -> List[Tuple[str, str]]: + aspect_ratios = get_aspect_ratios_from_wdinfos(wdinfos) + + # return a list of (wdinfo_path, aspect_ratio) pairs + return [(wdinfo, aspect_ratio.replace("_", ",")) for wdinfo, aspect_ratio in zip(wdinfos, aspect_ratios)] + + +def parse_frame_range_from_wdinfo(wdinfo: str) -> tuple[int, int] | None: + """ + Parse frame range from wdinfo path. + + Args: + wdinfo: wdinfo path string containing frames_X_Y pattern + + Returns: + Tuple of (min_frames, max_frames) if found, None otherwise + + Example: + >>> parse_frame_range_from_wdinfo("wdinfo/v4/tv_drama/resolution_720/aspect_ratio_16_9/frames_300_400/wdinfo.json") + (300, 400) + """ + match = re.search(r"frames_(\d+)_(\d+)", wdinfo) + if match: + return (int(match.group(1)), int(match.group(2))) + return None + + +def filter_wdinfos_by_frame_range( + wdinfos: list[str], + min_frames: int | None = None, + max_frames: int | None = None, +) -> list[str]: + """ + Filter wdinfo files based on frame range. + + The frame range in wdinfo path (e.g., frames_300_400) represents videos + with frames between those values. This function filters wdinfo files + based on the wdinfo's upper bound (wdinfo_max): + - min_frames is EXCLUSIVE: wdinfo_max must be > min_frames + - max_frames is INCLUSIVE: wdinfo_max must be <= max_frames + + Args: + wdinfos: List of wdinfo paths + min_frames: Minimum number of frames (exclusive). If None, no lower bound. + max_frames: Maximum number of frames (inclusive). If None, no upper bound. + + Returns: + Filtered list of wdinfo paths + + Example: + >>> wdinfos = [ + ... "wdinfo/frames_400_500/wdinfo.json", + ... "wdinfo/frames_500_600/wdinfo.json", + ... "wdinfo/frames_600_700/wdinfo.json", + ... ] + >>> filter_wdinfos_by_frame_range(wdinfos, min_frames=500, max_frames=600) + ['wdinfo/frames_500_600/wdinfo.json'] + # frames_400_500 excluded because wdinfo_max (500) <= min_frames (500) + # frames_500_600 included because wdinfo_max (600) > min_frames (500) AND <= max_frames (600) + # frames_600_700 excluded because wdinfo_max (700) > max_frames (600) + """ + if min_frames is None and max_frames is None: + return wdinfos + + filtered = [] + for wdinfo in wdinfos: + frame_range = parse_frame_range_from_wdinfo(wdinfo) + if frame_range is None: + # If no frame range in path, include by default + filtered.append(wdinfo) + continue + + wdinfo_min, wdinfo_max = frame_range + + # Filter based on wdinfo's upper bound (wdinfo_max): + # - min_frames is exclusive: wdinfo_max must be > min_frames + # - max_frames is inclusive: wdinfo_max must be <= max_frames + include = True + if min_frames is not None and wdinfo_max <= min_frames: + include = False + if max_frames is not None and wdinfo_max > max_frames: + include = False + + if include: + filtered.append(wdinfo) + + return filtered diff --git a/cosmos3/_src/vfm/diffusion/__init__.py b/cosmos3/_src/vfm/diffusion/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/diffusion/rectified_flow.py b/cosmos3/_src/vfm/diffusion/rectified_flow.py new file mode 100644 index 00000000..93c92330 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/rectified_flow.py @@ -0,0 +1,183 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable + +import torch +from diffusers import FlowMatchEulerDiscreteScheduler + + +class TrainTimeWeight: + def __init__( + self, + noise_scheduler, + weight: str = "uniform", + ): + # Map reweighting -> uniform to support inference for existing checkpoints. + if weight == "reweighting": + weight = "uniform" + + self.weight = weight + self.noise_scheduler = noise_scheduler + + assert self.weight == "uniform", "Only uniform loss weight is supported in RF" + + def __call__(self, t, tensor_kwargs) -> torch.Tensor: # t: [B], returns [B] + if self.weight == "uniform": + wts = torch.ones_like(t) # [B] + else: + raise NotImplementedError(f"Time weight '{self.weight}' is not implemented.") + + return wts + + +class TrainTimeSampler: + _WAVER_MODE_S = 1.29 + + def __init__( + self, + distribution: str = "uniform", + ): + self.distribution = distribution + + @torch.no_grad() + def __call__( + self, + batch_size: int, + device: torch.device = torch.device("cpu"), + dtype: torch.dtype = torch.float32, + ) -> torch.Tensor: + """ + Sample time tensor for training + + Returns: + torch.Tensor: Time tensor, shape (batch_size,) + """ + if self.distribution == "uniform": + t = torch.rand((batch_size,)).to(device=device, dtype=dtype) # [B] + elif self.distribution == "logitnormal": + t = torch.sigmoid(torch.randn((batch_size,))).to(device=device, dtype=dtype) # [B] + elif self.distribution == "waver": + u = torch.rand((batch_size,), dtype=torch.float32) # [B] + t = 1.0 - u - self._WAVER_MODE_S * (torch.cos(torch.pi / 2.0 * u) ** 2 - 1 + u) # [B] + t = t.to(device=device, dtype=dtype) # [B] + else: + raise NotImplementedError(f"Time distribution '{self.dist}' is not implemented.") + + return t # [B] + + +class RectifiedFlow: + def __init__( + self, + velocity_field: Callable, + train_time_distribution: TrainTimeSampler | str = "uniform", + train_time_weight_method: str = "uniform", + use_dynamic_shift: bool = False, + shift: int = 3, + device: torch.device = torch.device("cpu"), + dtype: torch.dtype = torch.float32, + ): + r"""Initialize the RectifiedFlow class. + + Args: + velocity_field (`Callable`): + A function that predicts the velocity given the current state and time. + train_time_distribution (`TrainTimeSampler` or `str`, *optional*, defaults to `"uniform"`): + Distribution for sampling training times. + Can be an instance of `TrainTimeSampler` or a string specifying the distribution type. + train_time_weight (`TrainTimeWeight` or `str`, *optional*, defaults to `"uniform"`): + Weight applied to training times. + Can be an instance of `TrainTimeWeight` or a string specifying the weight type. + """ + self.velocity_field = velocity_field + self.train_time_sampler: TrainTimeSampler = ( + train_time_distribution + if isinstance(train_time_distribution, TrainTimeSampler) + else TrainTimeSampler(train_time_distribution) + ) + + if use_dynamic_shift: + self.noise_scheduler = FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=use_dynamic_shift) + else: + self.noise_scheduler = FlowMatchEulerDiscreteScheduler(shift=shift) + self.train_time_weight = TrainTimeWeight(self.noise_scheduler, train_time_weight_method) + + self.device = torch.device(device) if isinstance(device, str) else device + self.dtype = torch.dtype(dtype) if isinstance(dtype, str) else dtype + + def sample_train_time(self, batch_size: int): + r"""This method calls the `TrainTimeSampler` to sample training times. + + Returns: + t (`torch.Tensor`): + A tensor of sampled training times with shape `(batch_size,)`, + matching the class specified `device` and `dtype`. + """ + time = self.train_time_sampler(batch_size, device=self.device, dtype=self.dtype) + return time + + def get_discrete_timestamp(self, u, tensor_kwargs): + r"""This method map time from 0,1 to discrete steps""" + + indices = (u.squeeze() * self.noise_scheduler.config.num_train_timesteps).long() # [B] + timesteps = self.noise_scheduler.timesteps.to(**tensor_kwargs)[indices] # [B] + return timesteps.unsqueeze(0) if timesteps.ndim == 0 else timesteps # [B] + + def get_sigmas(self, timesteps, tensor_kwargs): # timesteps: [B], returns [B] + sigmas = self.noise_scheduler.sigmas.to(**tensor_kwargs) # [N_timesteps+1] + schedule_timesteps = self.noise_scheduler.timesteps.to(**tensor_kwargs) # [N_timesteps] + step_indices = [(schedule_timesteps == t).nonzero().squeeze().tolist() for t in timesteps] + assert len(step_indices) == timesteps.shape[0], "Number of indices do not match the given timesteps." + sigma = sigmas[step_indices].flatten() # [B] + + return sigma # [B] + + def get_interpolation( + self, + x_0: list[torch.Tensor], # each element: [B,C,T,H,W] or [B,D1,...,Dn] + x_1: list[torch.Tensor], # each element: [B,C,T,H,W] or [B,D1,...,Dn] + t: list[torch.Tensor], # each element: [B] or [B,1,1,1,1] + ): + r""" + This method computes interpolation `X_t` and their time derivatives `dotX_t` at the specified time points `t`. + Note that `x_0` is the noise, and `x_1` is the clean data. This is aligned with the notation in the recified flow community, + but different from the notation in the diffusion community. + + Args: + x_0 (`torch.Tensor`): + noise, shape `(B, D1, D2, ..., Dn)`, where `B` is the batch size, and `D1, D2, ..., Dn` are the data dimensions. + x_1 (`torch.Tensor`): + clean data, with the same shape as `x_0` + t (`torch.Tensor`): + A tensor of time steps with values in `[0, 1]`. Can be shape `(B,)` or + pre-broadcast to `(B, 1, T, ..., 1)` matching `x_1`'s dimensionality along batch and temporal dimension. + + Returns: + (x_t, dot_x_t) (`Tuple[torch.Tensor, torch.Tensor]`): + - x_t (`torch.Tensor`): The interpolated state, with shape `(B, D1, D2, ..., Dn)`. + - dot_x_t (torch.Tensor): The time derivative of the interpolated state, with the same shape as `x_t`. + """ + assert len(x_0) == len(x_1), "x_0 and x_1 must have the same length." + assert len(x_0) == len(t), "Batch size of x_0 and x_1 must match." + assert len(t) == len(x_1), "Batch size of t must match x_1." + + x_t = [] + dot_x_t = [] + for i in range(len(x_0)): + x_t.append(x_0[i] * t[i] + x_1[i] * (1 - t[i])) # [B,C,T,H,W]; t[i] broadcasts [B] or [B,1,1,1,1] + dot_x_t.append(x_0[i] - x_1[i]) # [B,C,T,H,W] + + return x_t, dot_x_t # each list element: [B,C,T,H,W] diff --git a/cosmos3/_src/vfm/diffusion/samplers/__init__.py b/cosmos3/_src/vfm/diffusion/samplers/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/diffusion/samplers/edm.py b/cosmos3/_src/vfm/diffusion/samplers/edm.py new file mode 100644 index 00000000..3fd89190 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/edm.py @@ -0,0 +1,295 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A general framework for various sampling algorithm from a diffusion model. +Impl based on +* Refined Exponential Solver (RES) in https://arxiv.org/pdf/2308.02157 +* also clude other impl, DDIM, DEIS, DPM-Solver, EDM sampler. +Most of sampling algorihtm, Runge-Kutta, Multi-step, etc, can be impl in this framework by \ + adding new step function in get_runge_kutta_fn or get_multi_step_fn. +""" + +import math +from typing import Any, Callable, List, Literal, Optional, Tuple, Union + +import attrs +import torch + +from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.imaginaire.functional.multi_step import get_multi_step_fn, is_multi_step_fn_supported +from cosmos3._src.imaginaire.functional.runge_kutta import get_runge_kutta_fn, is_runge_kutta_fn_supported +from cosmos3._src.imaginaire.utils import log + +COMMON_SOLVER_OPTIONS = Literal["2ab", "2mid", "1euler"] + + +@make_freezable +@attrs.define(slots=False) +class SolverConfig: + is_multi: bool = False + rk: str = "2mid" + multistep: str = "2ab" + # following parameters control stochasticity, see EDM paper + # BY default, we use deterministic with no stochasticity + s_churn: float = 0.0 + s_t_max: float = float("inf") + s_t_min: float = 0.05 + s_noise: float = 1.0 + + +@make_freezable +@attrs.define(slots=False) +class SolverTimestampConfig: + nfe: int = 50 + t_min: float = 0.002 + t_max: float = 80.0 + order: float = 7.0 + is_forward: bool = False # whether generate forward or backward timestamps + + +@make_freezable +@attrs.define(slots=False) +class EDMSamplerConfig: + solver: SolverConfig = attrs.field(factory=SolverConfig) + timestamps: SolverTimestampConfig = attrs.field(factory=SolverTimestampConfig) + sample_clean: bool = True # whether run one last step to generate clean image + convert_sigmas_to_rf: bool = True # whether convert sigmas to RF sigmas + + +def get_rev_ts( + t_min: float, t_max: float, num_steps: int, ts_order: Union[int, float], is_forward: bool = False +) -> torch.Tensor: + """ + Generate a sequence of reverse time steps. + + Args: + t_min (float): The minimum time value. + t_max (float): The maximum time value. + num_steps (int): The number of time steps to generate. + ts_order (Union[int, float]): The order of the time step progression. + is_forward (bool, optional): If True, returns the sequence in forward order. Defaults to False. + + Returns: + torch.Tensor: A tensor containing the generated time steps in reverse or forward order. + + Raises: + ValueError: If `t_min` is not less than `t_max`. + TypeError: If `ts_order` is not an integer or float. + """ + if t_min >= t_max: + raise ValueError("t_min must be less than t_max") + + if not isinstance(ts_order, (int, float)): + raise TypeError("ts_order must be an integer or float") + + step_indices = torch.arange(num_steps + 1, dtype=torch.float64) # [num_steps+1] + time_steps = ( + t_max ** (1 / ts_order) + step_indices / num_steps * (t_min ** (1 / ts_order) - t_max ** (1 / ts_order)) + ) ** ts_order # [num_steps+1] + + if is_forward: + return time_steps.flip(dims=(0,)) # [num_steps+1] + + return time_steps # [num_steps+1] + + +class EDMSampler(torch.nn.Module): + def __init__(self, cfg: Optional[EDMSamplerConfig] = None): + super().__init__() + if cfg is None: + cfg = EDMSamplerConfig() + self.cfg = cfg + + @torch.no_grad() + def forward( + self, + x0_fn: Callable, + x_sigma_max: torch.Tensor, # [B,StateShape] + num_steps: int = 35, + sigma_min: float = 0.002, + sigma_max: float = 80, + rho: float = 7, + S_churn: float = 0, + S_min: float = 0, + S_max: float = float("inf"), + S_noise: float = 1, + solver_option: str = "2ab", + ) -> torch.Tensor: # [B,StateShape] + in_dtype = x_sigma_max.dtype + + def float64_x0_fn(x_B_StateShape: torch.Tensor, t_B: torch.Tensor) -> torch.Tensor: + return x0_fn(x_B_StateShape.to(in_dtype), t_B.to(in_dtype)).to(torch.float64) + + is_multistep = is_multi_step_fn_supported(solver_option) + is_rk = is_runge_kutta_fn_supported(solver_option) + assert is_multistep or is_rk, f"Only support multistep or Runge-Kutta method, got {solver_option}" + + solver_cfg = SolverConfig( + s_churn=S_churn, + s_t_max=S_max, + s_t_min=S_min, + s_noise=S_noise, + is_multi=is_multistep, + rk=solver_option, + multistep=solver_option, + ) + timestamps_cfg = SolverTimestampConfig(nfe=num_steps, t_min=sigma_min, t_max=sigma_max, order=rho) + sampler_cfg = EDMSamplerConfig(solver=solver_cfg, timestamps=timestamps_cfg, sample_clean=True) + + return self._forward_impl(float64_x0_fn, x_sigma_max, sampler_cfg).to(in_dtype) + + @torch.no_grad() + def _forward_impl( + self, + denoiser_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + noisy_input_B_StateShape: torch.Tensor, + sampler_cfg: Optional[EDMSamplerConfig] = None, + callback_fns: Optional[List[Callable]] = None, + ) -> torch.Tensor: + """ + Internal implementation of the forward pass. + + Args: + denoiser_fn: Function to denoise the input. + noisy_input_B_StateShape: Input tensor with noise. + sampler_cfg: Configuration for the sampler. + callback_fns: List of callback functions to be called during sampling. + + Returns: + torch.Tensor: Denoised output tensor. + """ + sampler_cfg = self.cfg if sampler_cfg is None else sampler_cfg + solver_order = 1 if sampler_cfg.solver.is_multi else int(sampler_cfg.solver.rk[0]) + num_timestamps = sampler_cfg.timestamps.nfe // solver_order + + sigmas_L = get_rev_ts( + sampler_cfg.timestamps.t_min, sampler_cfg.timestamps.t_max, num_timestamps, sampler_cfg.timestamps.order + ).to(noisy_input_B_StateShape.device) # [L] + + if self.cfg.convert_sigmas_to_rf: + sigmas_L = sigmas_L / (1 + sigmas_L) # [L] + + denoised_output = differential_equation_solver( + denoiser_fn, sigmas_L, sampler_cfg.solver, callback_fns=callback_fns + )(noisy_input_B_StateShape) # [B,StateShape] + + if sampler_cfg.sample_clean: + # Override denoised_output with fully denoised version + ones = torch.ones( + denoised_output.size(0), device=denoised_output.device, dtype=denoised_output.dtype + ) # [B] + denoised_output = denoiser_fn(denoised_output, sigmas_L[-1] * ones) # [B,StateShape] + + return denoised_output # [B,StateShape] + + +def fori_loop(lower: int, upper: int, body_fun: Callable[[int, Any], Any], init_val: Any) -> Any: + """ + Implements a for loop with a function. + + Args: + lower: Lower bound of the loop (inclusive). + upper: Upper bound of the loop (exclusive). + body_fun: Function to be applied in each iteration. + init_val: Initial value for the loop. + + Returns: + The final result after all iterations. + """ + val = init_val + for i in range(lower, upper): + # Add log during sampling to meet APS job health requirement of one log every 2mins + if i % 10 == 0: + log.info(f"fori_loop: {i}") + val = body_fun(i, val) + return val + + +def differential_equation_solver( + x0_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + sigmas_L: torch.Tensor, # [L] + solver_cfg: SolverConfig, + callback_fns: Optional[List[Callable]] = None, +) -> Callable[[torch.Tensor], torch.Tensor]: + """ + Creates a differential equation solver function. + + Args: + x0_fn: Function to compute x0 prediction. + sigmas_L: Tensor of sigma values with shape [L,]. + solver_cfg: Configuration for the solver. + callback_fns: Optional list of callback functions. + + Returns: + A function that solves the differential equation. + """ + num_step = len(sigmas_L) - 1 + + if solver_cfg.is_multi: + update_step_fn = get_multi_step_fn(solver_cfg.multistep) + else: + update_step_fn = get_runge_kutta_fn(solver_cfg.rk) + + eta = min(solver_cfg.s_churn / (num_step + 1), math.sqrt(1.2) - 1) + + def sample_fn(input_xT_B_StateShape: torch.Tensor) -> torch.Tensor: + """ + Samples from the differential equation. + + Args: + input_xT_B_StateShape: Input tensor with shape [B, StateShape]. + + Returns: + Output tensor with shape [B, StateShape]. + """ + ones_B = torch.ones( + input_xT_B_StateShape.size(0), device=input_xT_B_StateShape.device, dtype=torch.float64 + ) # [B] + + def step_fn( + i_th: int, state: Tuple[torch.Tensor, Optional[List[torch.Tensor]]] + ) -> Tuple[torch.Tensor, Optional[List[torch.Tensor]]]: + input_x_B_StateShape, x0_preds = state # [B,StateShape] + sigma_cur_0, sigma_next_0 = sigmas_L[i_th], sigmas_L[i_th + 1] # scalar, scalar + + # algorithm 2: line 4-6 + if solver_cfg.s_t_min < sigma_cur_0 < solver_cfg.s_t_max: + hat_sigma_cur_0 = sigma_cur_0 + eta * sigma_cur_0 # scalar + input_x_B_StateShape = input_x_B_StateShape + ( + hat_sigma_cur_0**2 - sigma_cur_0**2 + ).sqrt() * solver_cfg.s_noise * torch.randn_like(input_x_B_StateShape) # [B,StateShape] + sigma_cur_0 = hat_sigma_cur_0 # scalar + + if solver_cfg.is_multi: + x0_pred_B_StateShape = x0_fn(input_x_B_StateShape, sigma_cur_0 * ones_B) # [B,StateShape]; sigma: [B] + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_pred_B_StateShape, x0_preds + ) # [B,StateShape] + else: + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_fn + ) # [B,StateShape] + + if callback_fns: + for callback_fn in callback_fns: + callback_fn(**locals()) + + return output_x_B_StateShape, x0_preds + + x_at_eps, _ = fori_loop(0, num_step, step_fn, [input_xT_B_StateShape, None]) + return x_at_eps # [B,StateShape] + + return sample_fn diff --git a/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py b/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py new file mode 100644 index 00000000..7b05b720 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixed-step sampler for DMD2-distilled student models. + +Uses an explicit, fixed sigma schedule (t_list) baked in at construction time. +Each step predicts x0 via a single velocity forward pass, then either: + - ODE: Euler step x_next = x_t + (sigma_next - sigma_cur) * v + - SDE: re-noise x0 to sigma_next with fresh noise + +This is incompatible with multi-step solvers (UniPC, EDM) because DMD2 students +are trained as one-shot denoisers at specific discrete sigmas, not as smooth +score functions. + +When ``shift`` is passed at call time, the schedule is derived dynamically via +the flow-matching shift formula (same as UniPC): + sigmas = shift * s / (1 + (shift - 1) * s), s = linspace(sigma_max, sigma_min, num_steps) +In this case ``num_steps`` is required. Otherwise ``self.t_list`` is used. +""" + +import torch + +from cosmos3._src.vfm.diffusion.samplers.utils import run_multiseed + + +class FixedStepSampler: + def __init__( + self, + t_list: list[float], + sample_type: str = "ode", + num_train_timesteps: float = 1000.0, + ) -> None: + assert len(t_list) >= 1, "t_list must have at least 1 entry" + assert sample_type in ("ode", "sde"), f"sample_type must be 'ode' or 'sde', got {sample_type}" + # Auto-append 0.0 if not present (convention: t_list in config excludes final step) + self.t_list = t_list if t_list[-1] == 0.0 else t_list + [0.0] + assert len(self.t_list) >= 2, "t_list must have at least 2 entries after appending 0.0" + self.sample_type = sample_type + self.num_train_timesteps = num_train_timesteps + + def _build_t_list(self, num_steps: int, shift: float, device: torch.device) -> list[float]: + """Compute a shifted sigma schedule with ``num_steps`` integration steps.""" + sigma_max = 1.0 + sigma_min = 1.0 / self.num_train_timesteps + sigmas = torch.linspace(sigma_max, sigma_min, num_steps, device=device) + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) + return sigmas.tolist() + [0.0] + + def __call__( + self, + velocity_fn, + noise: torch.Tensor | list[torch.Tensor], + num_steps: int | None = None, + shift: float | None = None, + seed: int | list[int] | None = None, + ) -> torch.Tensor | list[torch.Tensor]: + """Run the fixed-step sampling loop. + + Matches the UniPC sampler call signature so both can be used + interchangeably in ``generate_samples_from_batch``. + + ``noise`` and ``seed`` must both be single values or both be lists + (of the same length). When lists are provided, each element + corresponds to one independent sample; the return value is then a + list of denoised tensors. When single values are provided, a + single tensor is returned. + + Args: + velocity_fn: ``velocity_fn(noise=..., timestep=...) -> velocity``. + noise: Initial noise. Either a single ``torch.Tensor`` of shape + ``(D,)`` or a ``list[torch.Tensor]`` where each element has + shape ``(D,)``. + seed: RNG seed for SDE mode. Either a single ``int`` or a + ``list[int]`` with the same length as ``noise``. + num_steps: Number of denoising steps. Required when ``shift`` is + given; optional otherwise (asserted to equal + ``len(t_list) - 1`` when provided). + shift: When set, derive the sigma schedule dynamically using the + flow-matching shift formula instead of ``self.t_list``. + + Returns: + Denoised sample(s). A single ``torch.Tensor`` when ``noise`` is a + tensor, or a ``list[torch.Tensor]`` when ``noise`` is a list. + """ + if isinstance(noise, list): + device = noise[0].device + else: + device = noise.device + + if shift is not None: + assert num_steps is not None, "num_steps is required when shift is provided" + t_list = self._build_t_list(num_steps, shift, device) + else: + if num_steps is not None: + assert num_steps == len(self.t_list) - 1, ( + f"num_steps={num_steps} must match the schedule length len(t_list)-1={len(self.t_list) - 1}" + ) + t_list = self.t_list + + latent = noise + + for step_idx, (sigma_cur, sigma_next) in enumerate( + zip(t_list[:-1], t_list[1:]), + ): + timestep = torch.tensor(sigma_cur * self.num_train_timesteps, device=device) + v_pred = velocity_fn(latent, timestep.reshape(1, 1)) + + def _sde_step(seed: int | None, latent: torch.Tensor, v_pred: torch.Tensor) -> torch.Tensor: + x0_pred = latent - sigma_cur * v_pred + + if sigma_next > 0: + if self.sample_type == "ode": + # Euler ODE step + latent = latent + (sigma_next - sigma_cur) * v_pred + else: + if seed is not None: + torch.manual_seed(seed + step_idx) + eps_fresh = torch.randn_like(x0_pred) + latent = (1.0 - sigma_next) * x0_pred + sigma_next * eps_fresh + else: + latent = x0_pred + return latent + + latent = run_multiseed( + _sde_step, + seed=seed, + latent=latent, + v_pred=v_pred, + ) + + return latent diff --git a/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py b/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py new file mode 100644 index 00000000..606b9bbd --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py @@ -0,0 +1,768 @@ +# Copied from https://github.com/huggingface/diffusers/blob/v0.31.0/src/diffusers/schedulers/scheduling_unipc_multistep.py +# Convert unipc for flow matching +# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved. + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput +from diffusers.utils import deprecate + + +class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin): + """ + `UniPCMultistepScheduler` is a training-free framework designed for the fast sampling of diffusion models. + + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic + methods the library implements for all schedulers such as loading and saving. + + Args: + num_train_timesteps (`int`, defaults to 1000): + The number of diffusion steps to train the model. + solver_order (`int`, default `2`): + The UniPC order which can be any positive integer. The effective order of accuracy is `solver_order + 1` + due to the UniC. It is recommended to use `solver_order=2` for guided sampling, and `solver_order=3` for + unconditional sampling. + prediction_type (`str`, defaults to "flow_prediction"): + Prediction type of the scheduler function; must be `flow_prediction` for this scheduler, which predicts + the flow of the diffusion process. + thresholding (`bool`, defaults to `False`): + Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such + as Stable Diffusion. + dynamic_thresholding_ratio (`float`, defaults to 0.995): + The ratio for the dynamic thresholding method. Valid only when `thresholding=True`. + sample_max_value (`float`, defaults to 1.0): + The threshold value for dynamic thresholding. Valid only when `thresholding=True` and `predict_x0=True`. + predict_x0 (`bool`, defaults to `True`): + Whether to use the updating algorithm on the predicted x0. + solver_type (`str`, default `bh2`): + Solver type for UniPC. It is recommended to use `bh1` for unconditional sampling when steps < 10, and `bh2` + otherwise. + lower_order_final (`bool`, default `True`): + Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can + stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10. + disable_corrector (`list`, default `[]`): + Decides which step to disable the corrector to mitigate the misalignment between `epsilon_theta(x_t, c)` + and `epsilon_theta(x_t^c, c)` which can influence convergence for a large guidance scale. Corrector is + usually disabled during the first few steps. + solver_p (`SchedulerMixin`, default `None`): + Any other scheduler that if specified, the algorithm becomes `solver_p + UniC`. + use_karras_sigmas (`bool`, *optional*, defaults to `False`): + Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`, + the sigmas are determined according to a sequence of noise levels {σi}. + use_exponential_sigmas (`bool`, *optional*, defaults to `False`): + Whether to use exponential sigmas for step sizes in the noise schedule during the sampling process. + timestep_spacing (`str`, defaults to `"linspace"`): + The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and + Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information. + steps_offset (`int`, defaults to 0): + An offset added to the inference steps, as required by some model families. + final_sigmas_type (`str`, defaults to `"zero"`): + The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final + sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0. + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + solver_order: int = 2, + prediction_type: str = "flow_prediction", + shift: Optional[float] = 1.0, + use_dynamic_shifting=False, + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + predict_x0: bool = True, + solver_type: str = "bh2", + lower_order_final: bool = True, + disable_corrector: List[int] = [], + solver_p: SchedulerMixin = None, + timestep_spacing: str = "linspace", + steps_offset: int = 0, + final_sigmas_type: Optional[str] = "zero", # "zero", "sigma_min" + ): + if solver_type not in ["bh1", "bh2"]: + if solver_type in ["midpoint", "heun", "logrho"]: + self.register_to_config(solver_type="bh2") + else: + raise NotImplementedError(f"{solver_type} is not implemented for {self.__class__}") + + self.predict_x0 = predict_x0 + # setable values + self.num_inference_steps = None + alphas = np.linspace(1, 1 / num_train_timesteps, num_train_timesteps)[::-1].copy() + sigmas = 1.0 - alphas + sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32) # [num_train_timesteps] + + if not use_dynamic_shifting: + # when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # [num_train_timesteps] # pyright: ignore + + self.sigmas = sigmas # [num_train_timesteps] + self.timesteps = sigmas * num_train_timesteps # [num_train_timesteps] + + self.model_outputs = [None] * solver_order + self.timestep_list = [None] * solver_order + self.lower_order_nums = 0 + self.disable_corrector = disable_corrector + self.solver_p = solver_p + self.last_sample = None + self._step_index = None + self._begin_index = None + + self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication + self.sigma_min = self.sigmas[-1].item() + self.sigma_max = self.sigmas[0].item() + + @property + def step_index(self): + """ + The index counter for current timestep. It will increase 1 after each scheduler step. + """ + return self._step_index + + @property + def begin_index(self): + """ + The index for the first timestep. It should be set from pipeline with `set_begin_index` method. + """ + return self._begin_index + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index + def set_begin_index(self, begin_index: int = 0): + """ + Sets the begin index for the scheduler. This function should be run from pipeline before the inference. + + Args: + begin_index (`int`): + The begin index for the scheduler. + """ + self._begin_index = begin_index + + # Modified from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler.set_timesteps + def set_timesteps( + self, + num_inference_steps: Union[int, None] = None, + device: Union[str, torch.device] = None, + sigmas: Optional[List[float]] = None, + mu: Optional[Union[float, None]] = None, + shift: Optional[Union[float, None]] = None, + use_kerras_sigma: bool = False, + ): + """ + Sets the discrete timesteps used for the diffusion chain (to be run before inference). + Args: + num_inference_steps (`int`): + Total number of the spacing of the time steps. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + if self.config.use_dynamic_shifting and mu is None: + raise ValueError(" you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`") + + if use_kerras_sigma: + # force to use the exact sigma used in edm sampler + sigma_max = 200 + sigma_min = 0.01 + rho = 7 + sigmas = np.arange(num_inference_steps + 1) / num_inference_steps + min_inv_rho = sigma_min ** (1 / rho) + max_inv_rho = sigma_max ** (1 / rho) + sigmas = (max_inv_rho + sigmas * (min_inv_rho - max_inv_rho)) ** rho + sigmas = sigmas / (1 + sigmas) + else: + if sigmas is None: + sigmas = np.linspace(self.sigma_max, self.sigma_min, num_inference_steps + 1).copy()[:-1] # pyright: ignore + + if self.config.use_dynamic_shifting: + sigmas = self.time_shift(mu, 1.0, sigmas) # pyright: ignore + else: + if shift is None: + shift = self.config.shift + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # pyright: ignore + + if self.config.final_sigmas_type == "sigma_min": + sigma_last = ((1 - self.alphas_cumprod[0]) / self.alphas_cumprod[0]) ** 0.5 + elif self.config.final_sigmas_type == "zero": + sigma_last = 0 + else: + raise ValueError( + f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}" + ) + + timesteps = sigmas * self.config.num_train_timesteps + sigmas = np.concatenate([sigmas, [sigma_last]]).astype(np.float32) # pyright: ignore + + self.sigmas = torch.from_numpy(sigmas) # [num_inference_steps+1] + self.timesteps = torch.from_numpy(timesteps).to(device=device, dtype=torch.int64) # [num_inference_steps] + + self.num_inference_steps = len(timesteps) + + self.model_outputs = [ + None, + ] * self.config.solver_order + self.lower_order_nums = 0 + self.last_sample = None + if self.solver_p: + self.solver_p.set_timesteps(self.num_inference_steps, device=device) + + # add an index counter for schedulers that allow duplicated timesteps + self._step_index = None + self._begin_index = None + self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication + + # Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample + def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor: + """ + "Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the + prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by + s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing + pixels from saturation at each step. We find that dynamic thresholding results in significantly better + photorealism as well as better image-text alignment, especially when using very large guidance weights." + + https://arxiv.org/abs/2205.11487 + """ + dtype = sample.dtype + batch_size, channels, *remaining_dims = sample.shape + + if dtype not in (torch.float32, torch.float64): + sample = sample.float() # upcast for quantile calculation, and clamp not implemented for cpu half + + # Flatten sample for doing quantile calculation along each image + sample = sample.reshape(batch_size, channels * np.prod(remaining_dims)) # [B,C*spatial] + + abs_sample = sample.abs() # "a certain percentile absolute pixel value" # [B,C*spatial] + + s = torch.quantile(abs_sample, self.config.dynamic_thresholding_ratio, dim=1) # [B] + s = torch.clamp( + s, min=1, max=self.config.sample_max_value + ) # When clamped to min=1, equivalent to standard clipping to [-1, 1] # [B] + s = s.unsqueeze(1) # [B,1] + sample = ( + torch.clamp(sample, -s, s) / s + ) # "we threshold xt0 to the range [-s, s] and then divide by s" # [B,C*spatial] + + sample = sample.reshape(batch_size, channels, *remaining_dims) # [B,C,...] + sample = sample.to(dtype) + + return sample + + # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler._sigma_to_t + def _sigma_to_t(self, sigma): + return sigma * self.config.num_train_timesteps + + def _sigma_to_alpha_sigma_t(self, sigma): + return 1 - sigma, sigma + + # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.set_timesteps + def time_shift(self, mu: float, sigma: float, t: torch.Tensor): + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + def convert_model_output( + self, + model_output: torch.Tensor, + *args, + sample: torch.Tensor = None, + **kwargs, + ) -> torch.Tensor: + r""" + Convert the model output to the corresponding type the UniPC algorithm needs. + + Args: + model_output (`torch.Tensor`): + The direct output from the learned diffusion model. + timestep (`int`): + The current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + + Returns: + `torch.Tensor`: + The converted model output. + """ + timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None) + if sample is None: + if len(args) > 1: + sample = args[1] + else: + raise ValueError("missing `sample` as a required keyward argument") + if timestep is not None: + deprecate( + "timesteps", + "1.0.0", + "Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + + sigma = self.sigmas[self.step_index] + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma) + + # print("sigma_t ==>", self.step_index, sigma, sigma_t, alpha_t, sample.shape, model_output.shape) + if self.predict_x0: + if self.config.prediction_type == "flow_prediction": + sigma_t = self.sigmas[self.step_index] + x0_pred = sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`," + " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler." + ) + + if self.config.thresholding: + x0_pred = self._threshold_sample(x0_pred) + # print("self.config.thresholding", self.config.thresholding) + return x0_pred + else: + if self.config.prediction_type == "flow_prediction": + sigma_t = self.sigmas[self.step_index] + epsilon = sample - (1 - sigma_t) * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`," + " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler." + ) + + if self.config.thresholding: + sigma_t = self.sigmas[self.step_index] + x0_pred = sample - sigma_t * model_output + x0_pred = self._threshold_sample(x0_pred) + epsilon = model_output + x0_pred + + return epsilon + + def multistep_uni_p_bh_update( + self, + model_output: torch.Tensor, + *args, + sample: torch.Tensor = None, + order: int = None, # pyright: ignore + **kwargs, + ) -> torch.Tensor: + """ + One step for the UniP (B(h) version). Alternatively, `self.solver_p` is used if is specified. + + Args: + model_output (`torch.Tensor`): + The direct output from the learned diffusion model at the current timestep. + prev_timestep (`int`): + The previous discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + order (`int`): + The order of UniP at this timestep (corresponds to the *p* in UniPC-p). + + Returns: + `torch.Tensor`: + The sample tensor at the previous timestep. + """ + prev_timestep = args[0] if len(args) > 0 else kwargs.pop("prev_timestep", None) + if sample is None: + if len(args) > 1: + sample = args[1] + else: + raise ValueError(" missing `sample` as a required keyward argument") + if order is None: + if len(args) > 2: + order = args[2] + else: + raise ValueError(" missing `order` as a required keyward argument") + if prev_timestep is not None: + deprecate( + "prev_timestep", + "1.0.0", + "Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + model_output_list = self.model_outputs + + s0 = self.timestep_list[-1] + m0 = model_output_list[-1] + x = sample + + if self.solver_p: + x_t = self.solver_p.step(model_output, s0, x).prev_sample + return x_t + + sigma_t, sigma_s0 = self.sigmas[self.step_index + 1], self.sigmas[self.step_index] # pyright: ignore + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t) + alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0) + + lambda_t = torch.log(alpha_t) - torch.log(sigma_t) + lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0) + + h = lambda_t - lambda_s0 + device = sample.device + + rks = [] + D1s = [] + for i in range(1, order): + si = self.step_index - i # pyright: ignore + mi = model_output_list[-(i + 1)] + alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si]) + lambda_si = torch.log(alpha_si) - torch.log(sigma_si) + rk = (lambda_si - lambda_s0) / h + rks.append(rk) + D1s.append((mi - m0) / rk) # pyright: ignore + + rks.append(1.0) + rks = torch.tensor(rks, device=device) # [order] + + R = [] + b = [] + + hh = -h if self.predict_x0 else h + h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1 + h_phi_k = h_phi_1 / hh - 1 + + factorial_i = 1 + + if self.config.solver_type == "bh1": + B_h = hh + elif self.config.solver_type == "bh2": + B_h = torch.expm1(hh) + else: + raise NotImplementedError() + + for i in range(1, order + 1): + R.append(torch.pow(rks, i - 1)) # [order] + b.append(h_phi_k * factorial_i / B_h) + factorial_i *= i + 1 + h_phi_k = h_phi_k / hh - 1 / factorial_i + + R = torch.stack(R) # [order,order] + b = torch.tensor(b, device=device) # [order] + + if len(D1s) > 0: + D1s = torch.stack(D1s, dim=1) # [B,order-1,C,T,H,W] + # for order 2, we use a simplified version + if order == 2: + rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device) # [1] + else: + rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1]).to(device).to(x.dtype) # [order-1] + else: + D1s = None + + if self.predict_x0: + x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) # [B,C,T,H,W] # pyright: ignore + else: + pred_res = 0 + x_t = x_t_ - alpha_t * B_h * pred_res # [B,C,T,H,W] + else: + x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) # [B,C,T,H,W] # pyright: ignore + else: + pred_res = 0 + x_t = x_t_ - sigma_t * B_h * pred_res # [B,C,T,H,W] + + x_t = x_t.to(x.dtype) # [B,C,T,H,W] + return x_t # [B,C,T,H,W] + + def multistep_uni_c_bh_update( + self, + this_model_output: torch.Tensor, + *args, + last_sample: torch.Tensor = None, + this_sample: torch.Tensor = None, + order: int = None, # pyright: ignore + **kwargs, + ) -> torch.Tensor: + """ + One step for the UniC (B(h) version). + + Args: + this_model_output (`torch.Tensor`): + The model outputs at `x_t`. + this_timestep (`int`): + The current timestep `t`. + last_sample (`torch.Tensor`): + The generated sample before the last predictor `x_{t-1}`. + this_sample (`torch.Tensor`): + The generated sample after the last predictor `x_{t}`. + order (`int`): + The `p` of UniC-p at this step. The effective order of accuracy should be `order + 1`. + + Returns: + `torch.Tensor`: + The corrected sample tensor at the current timestep. + """ + this_timestep = args[0] if len(args) > 0 else kwargs.pop("this_timestep", None) + if last_sample is None: + if len(args) > 1: + last_sample = args[1] + else: + raise ValueError(" missing`last_sample` as a required keyward argument") + if this_sample is None: + if len(args) > 2: + this_sample = args[2] + else: + raise ValueError(" missing`this_sample` as a required keyward argument") + if order is None: + if len(args) > 3: + order = args[3] + else: + raise ValueError(" missing`order` as a required keyward argument") + if this_timestep is not None: + deprecate( + "this_timestep", + "1.0.0", + "Passing `this_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + + model_output_list = self.model_outputs + + m0 = model_output_list[-1] + x = last_sample + x_t = this_sample + model_t = this_model_output + + sigma_t, sigma_s0 = self.sigmas[self.step_index], self.sigmas[self.step_index - 1] # pyright: ignore + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t) + alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0) + + lambda_t = torch.log(alpha_t) - torch.log(sigma_t) + lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0) + + h = lambda_t - lambda_s0 + device = this_sample.device + + rks = [] + D1s = [] + for i in range(1, order): + si = self.step_index - (i + 1) # pyright: ignore + mi = model_output_list[-(i + 1)] + alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si]) + lambda_si = torch.log(alpha_si) - torch.log(sigma_si) + rk = (lambda_si - lambda_s0) / h + rks.append(rk) + D1s.append((mi - m0) / rk) # pyright: ignore + + rks.append(1.0) + rks = torch.tensor(rks, device=device) # [order] + + R = [] + b = [] + + hh = -h if self.predict_x0 else h + h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1 + h_phi_k = h_phi_1 / hh - 1 + + factorial_i = 1 + + if self.config.solver_type == "bh1": + B_h = hh + elif self.config.solver_type == "bh2": + B_h = torch.expm1(hh) + else: + raise NotImplementedError() + + for i in range(1, order + 1): + R.append(torch.pow(rks, i - 1)) # [order] + b.append(h_phi_k * factorial_i / B_h) + factorial_i *= i + 1 + h_phi_k = h_phi_k / hh - 1 / factorial_i + + R = torch.stack(R) # [order,order] + b = torch.tensor(b, device=device) # [order] + + if len(D1s) > 0: + D1s = torch.stack(D1s, dim=1) # [B,order-1,C,T,H,W] + else: + D1s = None + + # for order 1, we use a simplified version + if order == 1: + rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device) # [1] + else: + rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype) # [order] + + if self.predict_x0: + x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) # [B,C,T,H,W] + else: + corr_res = 0 + D1_t = model_t - m0 # [B,C,T,H,W] + x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t) # [B,C,T,H,W] + else: + x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) # [B,C,T,H,W] + else: + corr_res = 0 + D1_t = model_t - m0 # [B,C,T,H,W] + x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t) # [B,C,T,H,W] + x_t = x_t.to(x.dtype) # [B,C,T,H,W] + return x_t # [B,C,T,H,W] + + def index_for_timestep(self, timestep, schedule_timesteps=None): + if schedule_timesteps is None: + schedule_timesteps = self.timesteps + + indices = (schedule_timesteps == timestep).nonzero() + + # The sigma index that is taken for the **very** first `step` + # is always the second index (or the last index if there is only 1) + # This way we can ensure we don't accidentally skip a sigma in + # case we start in the middle of the denoising schedule (e.g. for image-to-image) + pos = 1 if len(indices) > 1 else 0 + + return indices[pos].item() + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler._init_step_index + def _init_step_index(self, timestep): + """ + Initialize the step_index counter for the scheduler. + """ + + if self.begin_index is None: + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + self._step_index = self.index_for_timestep(timestep) + else: + self._step_index = self._begin_index + + def step( + self, + model_output: torch.Tensor, + timestep: Union[int, torch.Tensor], + sample: torch.Tensor, + return_dict: bool = True, + generator=None, + ) -> Union[SchedulerOutput, Tuple]: + """ + Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with + the multistep UniPC. + + Args: + model_output (`torch.Tensor`): + The direct output from learned diffusion model. + timestep (`int`): + The current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + return_dict (`bool`): + Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`. + + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned, otherwise a + tuple is returned where the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if self.step_index is None: + self._init_step_index(timestep) + + # print("self.step_index ==> ", self.step_index) + + use_corrector = ( + self.step_index > 0 and self.step_index - 1 not in self.disable_corrector and self.last_sample is not None # pyright: ignore + ) + + model_output_convert = self.convert_model_output(model_output, sample=sample) + + if use_corrector: + sample = self.multistep_uni_c_bh_update( + this_model_output=model_output_convert, + last_sample=self.last_sample, + this_sample=sample, + order=self.this_order, + ) + + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self.timestep_list[i] = self.timestep_list[i + 1] + + self.model_outputs[-1] = model_output_convert + self.timestep_list[-1] = timestep # pyright: ignore + + if self.config.lower_order_final: + this_order = min(self.config.solver_order, len(self.timesteps) - self.step_index) # pyright: ignore + else: + this_order = self.config.solver_order + + self.this_order = min(this_order, self.lower_order_nums + 1) # warmup for multistep + assert self.this_order > 0 + + self.last_sample = sample + prev_sample = self.multistep_uni_p_bh_update( + model_output=model_output, # pass the original non-converted model output, in case solver-p is used + sample=sample, + order=self.this_order, + ) + + if self.lower_order_nums < self.config.solver_order: + self.lower_order_nums += 1 + + # upon completion increase step index by one + self._step_index += 1 # pyright: ignore + + if not return_dict: + return (prev_sample, model_output_convert) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.Tensor, *args, **kwargs) -> torch.Tensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.Tensor`): + The input sample. + + Returns: + `torch.Tensor`: + A scaled input sample. + """ + return sample + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.add_noise + def add_noise( + self, + original_samples: torch.Tensor, + noise: torch.Tensor, + timesteps: torch.IntTensor, + ) -> torch.Tensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + schedule_timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + # begin_index is None when the scheduler is used for training or pipeline does not implement set_begin_index + if self.begin_index is None: + step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timesteps] + elif self.step_index is not None: + # add_noise is called after first denoising step (for inpainting) + step_indices = [self.step_index] * timesteps.shape[0] + else: + # add noise is called before first denoising step to create initial latent(img2img) + step_indices = [self.begin_index] * timesteps.shape[0] + + sigma = sigmas[step_indices].flatten() # [B] + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) # [B,1,...] broadcast-ready + + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma) + noisy_samples = alpha_t * original_samples + sigma_t * noise # [B,C,T,H,W] + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/cosmos3/_src/vfm/diffusion/samplers/unipc.py b/cosmos3/_src/vfm/diffusion/samplers/unipc.py new file mode 100644 index 00000000..f166c49e --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/unipc.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Optional + +import attrs +import torch + +from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.imaginaire.utils.progress_bar import progress_bar +from cosmos3._src.vfm.diffusion.samplers.fm_solvers_unipc import FlowUniPCMultistepScheduler +from cosmos3._src.vfm.diffusion.samplers.utils import run_multiseed + + +@make_freezable +@attrs.define(slots=False) +class UniPCSamplerConfig: + num_train_timesteps: int = 1000 + shift: float = 1.0 + use_dynamic_shifting: bool = False + + +class UniPCSampler(torch.nn.Module): + def __init__(self, cfg: Optional[UniPCSamplerConfig] = None, tensor_kwargs: Optional[dict] = None): + super().__init__() + if cfg is None: + cfg = UniPCSamplerConfig() + self.cfg = cfg + self.tensor_kwargs = tensor_kwargs + + @torch.no_grad() + def forward( + self, + velocity_fn: Callable, + noise: torch.Tensor | list[torch.Tensor], + num_steps: int = 35, + shift: float | None = None, + seed: int | list[int] | None = None, + ) -> torch.Tensor | list[torch.Tensor]: + """Run the UniPC multi-step sampling loop. + + ``noise`` and ``seed`` must both be single values or both be lists + (of the same length). When lists are provided, each element + corresponds to one independent sample with its own RNG generator + and scheduler; the return value is then a list of denoised tensors. + When single values are provided, a single tensor is returned. + + Args: + velocity_fn: ``velocity_fn(noise=..., timestep=...) -> velocity``. + noise: Initial noise. Either a single ``torch.Tensor`` of shape + ``(C, T, H, W)`` or a ``list[torch.Tensor]`` where each + element has shape ``(C, T, H, W)``. + seed: RNG seed. Either a single ``int`` or a ``list[int]`` with + the same length as ``noise``. + num_steps: Number of denoising steps. + shift: Flow-matching shift factor. Defaults to ``self.cfg.shift``. + + Returns: + Denoised sample(s). A single ``torch.Tensor`` when ``noise`` is a + tensor, or a ``list[torch.Tensor]`` when ``noise`` is a list. + """ + if shift is None: + shift = self.cfg.shift + assert isinstance(shift, float), "Shift must be a float" + + def _init_sample_scheduler(seed: int | None) -> tuple[torch.Generator, FlowUniPCMultistepScheduler]: + seed_g = torch.Generator(device=self.tensor_kwargs["device"]) + if seed is not None: + seed_g.manual_seed(seed) + sample_scheduler = FlowUniPCMultistepScheduler( + num_train_timesteps=self.cfg.num_train_timesteps, + shift=self.cfg.shift, + use_dynamic_shifting=self.cfg.use_dynamic_shifting, + ) + sample_scheduler.set_timesteps(num_steps, device=self.tensor_kwargs["device"], shift=shift) + return seed_g, sample_scheduler + + seed_g, sample_scheduler = run_multiseed(_init_sample_scheduler, seed=seed) + + timesteps = sample_scheduler[0].timesteps if isinstance(sample_scheduler, list) else sample_scheduler.timesteps + latent = noise + + for timestep in progress_bar(timesteps, desc="Sampling", total=len(timesteps)): + velocity_pred = velocity_fn(latent, timestep.reshape(1, 1)) + + def _scheduler_step( + seed_g: torch.Generator, + sample_scheduler: FlowUniPCMultistepScheduler, + velocity_pred: torch.Tensor, + latent: torch.Tensor, + ) -> torch.Tensor: + # multistep_uni_p_bh_update and multistep_uni_c_bh_update both use einsum patterns + # like "k,bkc...->bc...", which expect the tensor to have at least shape + # [B, C, ...] — where b is the batch dimension. Therefore, we need to unsqueeze + # the latent tensor to [B, C, ...] before passing it to the scheduler. + return sample_scheduler.step( + model_output=velocity_pred, + timestep=timestep, + sample=latent.unsqueeze(0), + return_dict=False, + generator=seed_g, + )[0].squeeze(0) + + latent = run_multiseed( + _scheduler_step, + seed_g=seed_g, + sample_scheduler=sample_scheduler, + velocity_pred=velocity_pred, + latent=latent, + ) + + return latent diff --git a/cosmos3/_src/vfm/diffusion/samplers/utils.py b/cosmos3/_src/vfm/diffusion/samplers/utils.py new file mode 100644 index 00000000..9e4b74c0 --- /dev/null +++ b/cosmos3/_src/vfm/diffusion/samplers/utils.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Callable + + +def run_multiseed(fn: Callable, **kwargs: list[Any] | Any) -> Any: + """Run a callable once per seed, indexing all list kwargs in lockstep. + + All keyword arguments must be **either** all lists or all non-lists. + Mixing is not allowed. + + - **All non-list**: ``fn`` is called once with the kwargs as-is, and its + return value is passed through directly. + - **All list**: every list must have the same length *N*. ``fn`` is called + *N* times — call *i* receives ``{k: v[i] for k, v in kwargs}``. + Results are collected into a list. If ``fn`` returns a tuple, the + results are transposed into a tuple of lists. + + Args: + fn: Callable to invoke per seed. + **kwargs: Keyword arguments for ``fn``. Must be **all lists** (one + element per seed, all the same length) or **all non-lists** (single + call). + + Returns: + - All non-list kwargs: the raw return value of ``fn``. + - All list kwargs, ``fn`` returns a tuple: a ``tuple`` of lists, + transposed across calls. + - All list kwargs, ``fn`` returns non-tuple: a ``list`` of return + values. + + Raises: + AssertionError: If kwargs mix lists and non-lists, or if list kwargs + have differing lengths. + + Examples: + Single call (no lists):: + + run_multiseed(lambda x, y: x + y, x=1, y=2) # returns 3 + + Multiple calls with all-list kwargs:: + + run_multiseed(lambda x, y: x * y, x=[1, 2, 3], y=[10, 20, 30]) + # returns [10, 40, 90] + + Tuple return transposition:: + + run_multiseed(lambda x: (x, -x), x=[1, 2]) + # returns ([1, 2], [-1, -2]) + """ + all_list = all(isinstance(v, list) for v in kwargs.values()) + all_non_list = all(not isinstance(v, list) for v in kwargs.values()) + assert all_list or all_non_list, "All kwargs must be lists or all must be non-lists, cannot mix" + + if all_non_list: + return fn(**kwargs) + + lengths = {len(v) for v in kwargs.values()} + assert len(lengths) == 1, f"All list arguments must have the same length, got {lengths}" + num_calls = lengths.pop() + + results = [] + for i in range(num_calls): + kwargs_i = {k: v[i] for k, v in kwargs.items()} + results.append(fn(**kwargs_i)) + + if results and isinstance(results[0], tuple): + return tuple(list(items) for items in zip(*results)) + return results diff --git a/cosmos3/_src/vfm/evaluation/__init__.py b/cosmos3/_src/vfm/evaluation/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/evaluation/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/evaluation/action/__init__.py b/cosmos3/_src/vfm/evaluation/action/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/evaluation/action/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/evaluation/action/libero/__init__.py b/cosmos3/_src/vfm/evaluation/action/libero/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/evaluation/action/libero/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py b/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py new file mode 100644 index 00000000..25699bd6 --- /dev/null +++ b/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py @@ -0,0 +1,1015 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Closed-loop evaluation for LIBERO using the Action HTTP inference server. + +# Single-view example (agentview camera): +PYTHONPATH=. python cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py \ + --server_url http://localhost:8000 \ + --task_suite libero_10 \ + --num_trials_per_task 10 \ + --action_horizon 16 \ + --camera agentview \ + --save_gifs --gif_fps 20 \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --action_dim 10 \ + --output_dir results/libero_closed_loop_10_single_view + +# Multi-view example (agentview + wrist cameras): +PYTHONPATH=. python cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py \ + --server_url http://localhost:8000 \ + --task_suite libero_goal \ + --num_trials_per_task 2 \ + --action_horizon 16 \ + --camera agentview,wrist \ + --save_gifs --gif_fps 20 \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --action_dim 10 \ + --output_dir results/libero_closed_loop_goal_multiview +""" + +from __future__ import annotations + +import argparse +import base64 +import io +import json +import os +import random +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +import requests +from PIL import Image +from scipy.spatial.transform import Rotation as R + +from cosmos3._src.vfm.datasets.action.libero_pose_utils import ( + libero_rotation_format, + libero_rotation_space_from_action_dim, +) +from cosmos3._src.vfm.datasets.action.pose_utils import convert_rotation +from cosmos3._src.vfm.datasets.action.viewpoint_utils import DEFAULT_VIEWPOINT_TEMPLATES + +benchmark: Any +get_libero_path: Any +OffScreenRenderEnv: Any + + +TASK_MAX_STEPS: dict[str, int] = { + "libero_spatial": 220, + "libero_object": 280, + "libero_goal": 300, + "libero_10": 520, + "libero_90": 400, +} + + +_CAMERA_PROMPT_NAMES: dict[str, str] = { + "agentview": "third-person view", + "wrist": "wrist-mounted camera", +} + + +def _append_prompt_sentence(prompt: str, sentence: str) -> str: + """Append one metadata sentence using the same separator convention as training augmentors.""" + if sentence in prompt: + return prompt + prompt = prompt.rstrip() + if not prompt: + return sentence.rstrip() + separator = " " if prompt.rstrip().endswith(".") else ". " + return prompt + separator + sentence.rstrip() + + +def _concat_view_layout_description(cameras: list[str]) -> str: + """Describe the horizontal camera layout sent by ``ActionEnvironmentClient``.""" + camera_names = [_CAMERA_PROMPT_NAMES[camera] for camera in cameras] + if len(camera_names) == 2: + return f"The left half shows the {camera_names[0]}; the right half shows the {camera_names[1]}." + layout = ", ".join(camera_names) + return f"The views are concatenated horizontally from left to right as: {layout}." + + +def _augment_task_prompt_with_viewpoint(task_description: str, cameras: list[str]) -> str: + """Mirror DROID-style concat-view caption augmentation for closed-loop LIBERO eval.""" + if len(cameras) <= 1: + return task_description + prompt = _append_prompt_sentence(task_description, DEFAULT_VIEWPOINT_TEMPLATES["concat_view"]) + return _append_prompt_sentence(prompt, _concat_view_layout_description(cameras)) + + +def _rotation_repr_to_mat(rotation: np.ndarray, rotation_space: str) -> np.ndarray: + """Convert a single LIBERO rotation block to a 3x3 rotation matrix.""" + matrix = convert_rotation( + rotation, + libero_rotation_format(rotation_space), + "matrix", + normalize_matrix=rotation_space != "3d", + ) + if not isinstance(matrix, np.ndarray): + raise TypeError(f"Expected NumPy rotation matrix, got {type(matrix)!r}") + return matrix + + +@dataclass +class EpisodeResult: + success: bool + steps: int + error: str | None + actions: list[list[float]] + + +class ActionEnvironmentClient: + """Client for interacting with the Action model server.""" + + server_url: str + domain_name: str + prompt: str + image_size: int + timeout: float + + def __init__( + self, + server_url: str, + domain_name: str, + prompt: str, + image_size: int, + timeout: float, + ) -> None: + self.server_url = server_url.rstrip("/") + self.domain_name = domain_name + self.prompt = prompt + self.image_size = image_size + self.timeout = timeout + + def check_health(self) -> bool: + """Check if the model server is healthy.""" + try: + resp = requests.get(f"{self.server_url}/", timeout=5.0) + return resp.status_code == 200 + except requests.RequestException: + return False + + def get_info(self) -> dict[str, str]: + """Get model server info.""" + resp = requests.get(f"{self.server_url}/info", timeout=5.0) + resp.raise_for_status() + return resp.json() + + def notify_next_episode(self) -> None: + """Notify server to advance to next episode (used with dataset action server).""" + try: + requests.post( + f"{self.server_url}/next_episode", + json={"prompt": self.prompt}, + timeout=5.0, + ) + except requests.RequestException: + pass + + def encode_image(self, image: np.ndarray) -> str: + """Encode a numpy image (H, W, 3) uint8 to base64 PNG, resizing to image_size.""" + if image.dtype != np.uint8: + if image.max() <= 1.0: + image = (image * 255.0).round().astype(np.uint8) + else: + image = image.astype(np.uint8) + pil_img = Image.fromarray(image) + if pil_img.size != (self.image_size, self.image_size): + pil_img = pil_img.resize( + (self.image_size, self.image_size), + resample=Image.Resampling.BILINEAR, + ) + buf = io.BytesIO() + pil_img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("ascii") + + def encode_image_raw(self, image: np.ndarray) -> str: + """Encode a numpy image (H, W, 3) uint8 to base64 PNG without resizing.""" + if image.dtype != np.uint8: + if image.max() <= 1.0: + image = (image * 255.0).round().astype(np.uint8) + else: + image = image.astype(np.uint8) + pil_img = Image.fromarray(image) + buf = io.BytesIO() + pil_img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("ascii") + + def resize_image(self, image: np.ndarray) -> np.ndarray: + """Resize image to model input size.""" + if image.dtype != np.uint8: + if image.max() <= 1.0: + image = (image * 255.0).round().astype(np.uint8) + else: + image = image.astype(np.uint8) + pil_img = Image.fromarray(image) + if pil_img.size != (self.image_size, self.image_size): + pil_img = pil_img.resize( + (self.image_size, self.image_size), + resample=Image.Resampling.BILINEAR, + ) + return np.array(pil_img) + + def concatenate_images(self, images: list[np.ndarray]) -> np.ndarray: + """Resize each image and concatenate horizontally (side-by-side). + + Args: + images: List of images with shape (H, W, 3). + + Returns: + Concatenated image with shape (image_size, image_size*num_views, 3). + """ + resized = [self.resize_image(img) for img in images] + return np.concatenate(resized, axis=1) + + def predict(self, observation: np.ndarray | list[np.ndarray]) -> dict[str, Any]: + """Send observation(s) to model server and get predicted actions. + + Args: + observation: Single image as np.ndarray or list of images for multi-view. + For multi-view, images are resized and concatenated horizontally before sending. + """ + if isinstance(observation, list): + # Multi-view: resize each, concatenate horizontally, and send as single image + concatenated = self.concatenate_images(observation) + encoded = self.encode_image_raw(concatenated) + else: + # Single view: send single image + encoded = self.encode_image(observation) + + payload = { + "image": encoded, + "prompt": self.prompt, + "domain_name": self.domain_name, + "image_size": self.image_size, + } + + resp = requests.post( + f"{self.server_url}/predict", + json=payload, + headers={"Content-Type": "application/json"}, + timeout=self.timeout, + ) + resp.raise_for_status() + + result = resp.json() + if "error" in result and result["error"]: + raise RuntimeError(f"Model server error: {result['error']}") + return result + + +def _find_accessible_dri_nodes() -> list[Path]: + dri_path = Path("/dev/dri") + if not dri_path.exists(): + return [] + nodes = list(dri_path.glob("renderD*")) + list(dri_path.glob("card*")) + return [node for node in nodes if os.access(node, os.R_OK | os.W_OK)] + + +def _resolve_mujoco_backend(requested_backend: str) -> tuple[str, str]: + requested_backend = requested_backend.lower() + if requested_backend != "auto": + return requested_backend, "requested" + + env_backend = os.environ.get("MUJOCO_GL") + if env_backend: + return env_backend.lower(), "env" + + if _find_accessible_dri_nodes(): + return "egl", "auto-gpu" + return "osmesa", "auto-cpu" + + +def _configure_mujoco_env(requested_backend: str) -> str: + backend, source = _resolve_mujoco_backend(requested_backend) + if backend not in {"egl", "osmesa", "glfw"}: + raise ValueError(f"Unsupported MuJoCo GL backend: {backend!r}. Use auto, egl, osmesa, or glfw.") + + os.environ["MUJOCO_GL"] = backend + if backend == "egl": + os.environ["PYOPENGL_PLATFORM"] = "egl" + elif backend == "osmesa": + os.environ["PYOPENGL_PLATFORM"] = "osmesa" + return f"{backend} ({source})" + + +def _import_libero() -> None: + global benchmark, get_libero_path, OffScreenRenderEnv + try: + from libero.libero import benchmark as libero_benchmark + from libero.libero import get_libero_path as libero_get_libero_path + from libero.libero.envs import OffScreenRenderEnv as libero_offscreen_render_env + except ImportError as exc: # pragma: no cover - environment-specific dependency + raise RuntimeError( + "Failed to import LIBERO. Make sure the LIBERO environment is activated. " + f"python={sys.executable!r}, import_error={exc!r}" + ) from exc + + benchmark = libero_benchmark + get_libero_path = libero_get_libero_path + OffScreenRenderEnv = libero_offscreen_render_env + + +def _wait_for_server(client: ActionEnvironmentClient, timeout_s: float) -> None: + start = time.perf_counter() + while time.perf_counter() - start < timeout_s: + if client.check_health(): + return + time.sleep(1.0) + raise RuntimeError(f"Timed out waiting for server at {client.server_url}") + + +def _get_libero_env( + task: Any, + *, + resolution: int, + seed: int, + render_gpu_device_id: int, +) -> tuple[Any, str]: + task_description = str(task.language) + task_bddl_file = os.path.join(get_libero_path("bddl_files"), task.problem_folder, task.bddl_file) + env_args = { + "bddl_file_name": task_bddl_file, + "camera_heights": resolution, + "camera_widths": resolution, + "render_gpu_device_id": render_gpu_device_id, + } + env = OffScreenRenderEnv(**env_args) + env.seed(seed) + return env, task_description + + +def _get_libero_dummy_action() -> list[float]: + return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0] + + +def _get_libero_image( + obs: dict[str, Any], + camera: str, + *, + flip_images: bool, + rotate_180: bool, +) -> np.ndarray: + if camera == "agentview": + image = obs["agentview_image"] + elif camera == "wrist": + image = obs["robot0_eye_in_hand_image"] + else: + raise ValueError(f"Unsupported camera={camera!r}. Use 'agentview' or 'wrist'.") + + if rotate_180: + image = image[::-1, ::-1] + if flip_images: + image = np.flipud(image) + return image + + +def _get_libero_images( + obs: dict[str, Any], + cameras: list[str], + *, + flip_images: bool, + rotate_180: bool, +) -> list[np.ndarray]: + """Get images from multiple cameras.""" + return [_get_libero_image(obs, camera, flip_images=flip_images, rotate_180=rotate_180) for camera in cameras] + + +def _ensure_uint8_image(image: np.ndarray) -> np.ndarray: + if image.dtype != np.uint8: + if image.max() <= 1.0: + image = (image * 255.0).round().astype(np.uint8) + else: + image = image.astype(np.uint8) + return image + + +def _save_gif(frames: list[Image.Image], output_path: Path, fps: int) -> None: + if not frames: + return + duration_ms = int(1000 / fps) if fps > 0 else 100 + output_path.parent.mkdir(parents=True, exist_ok=True) + first, *rest = frames + first.save( + output_path, + save_all=True, + append_images=rest, + duration=duration_ms, + loop=0, + ) + + +def _decode_b64_frames(b64_frames: list[str]) -> list[Image.Image]: + """Decode a list of base64-encoded PNG strings into PIL Images.""" + images: list[Image.Image] = [] + for b64 in b64_frames: + raw = base64.b64decode(b64) + images.append(Image.open(io.BytesIO(raw)).convert("RGB")) + return images + + +def _save_comparison_gif( + comparison_windows: list[tuple[list[Image.Image], list[Image.Image]]], + output_path: Path, + fps: int, + target_height: int = 256, + separator_width: int = 4, +) -> None: + """Create and save a side-by-side comparison GIF (Action prediction | env rollout). + + Each window is a (action_frames, env_frames) pair from one prediction call. + Frames are paired index-by-index; the conditioning frame (index 0) of + subsequent windows is skipped to avoid duplicating the boundary frame. + """ + from PIL import ImageDraw + + combined_frames: list[Image.Image] = [] + banner_h = 16 + + for window_idx, (action_frames, env_frames) in enumerate(comparison_windows): + n = min(len(action_frames), len(env_frames)) + start = 1 if window_idx > 0 else 0 + for i in range(start, n): + action_img = action_frames[i] + env_img = env_frames[i] + + action_w = int(action_img.width * target_height / action_img.height) + env_w = int(env_img.width * target_height / env_img.height) + action_resized = action_img.resize((action_w, target_height), Image.Resampling.BILINEAR) + env_resized = env_img.resize((env_w, target_height), Image.Resampling.BILINEAR) + + total_w = action_w + separator_width + env_w + total_h = target_height + banner_h + combined = Image.new("RGB", (total_w, total_h), color=0) + + draw = ImageDraw.Draw(combined) + draw.rectangle([(0, 0), (action_w, banner_h)], fill=(30, 30, 60)) + draw.rectangle([(action_w + separator_width, 0), (total_w, banner_h)], fill=(30, 60, 30)) + draw.text((4, 1), "Action Prediction", fill=(100, 180, 255)) + draw.text((action_w + separator_width + 4, 1), "Environment", fill=(100, 255, 100)) + + combined.paste(action_resized, (0, banner_h)) + combined.paste(env_resized, (action_w + separator_width, banner_h)) + combined_frames.append(combined) + + if combined_frames: + _save_gif(combined_frames, output_path, fps) + + +def _select_action_chunk(actions: list[list[float]], action_horizon: int) -> list[list[float]]: + if action_horizon <= 0 or action_horizon >= len(actions): + return actions + return actions[:action_horizon] + + +def _format_action(action: list[float], action_dim: int) -> list[float]: + if len(action) < action_dim: + raise ValueError(f"Action dimension {len(action)} smaller than expected {action_dim}") + return action[:action_dim] + + +def _remap_gripper_to_neg1_pos1(action: list[float]) -> list[float]: + """Remap gripper value from [0, 1] (training data range) to [-1, 1] (LIBERO env range). + + The training dataset stores gripper in [0, 1], but the LIBERO simulation + environment expects gripper commands in [-1, 1]. This applies the linear + mapping: gripper_env = gripper_model * 2 - 1. + """ + action = list(action) # avoid mutating the caller's list + action[-1] = max(-1.0, min(1.0, action[-1] * 2.0 - 1.0)) * -1 + return action + + +def _infer_rotation_space(action_dim: int, rotation_space: str) -> str: + if rotation_space != "auto": + return rotation_space + return libero_rotation_space_from_action_dim(action_dim) + + +def _obs_to_pose(obs: dict[str, Any]) -> tuple[np.ndarray, np.ndarray]: + position = np.asarray(obs["robot0_eef_pos"], dtype=np.float32) + quat = np.asarray(obs["robot0_eef_quat"], dtype=np.float32) + rotation = R.from_quat(quat).as_matrix() + return position, rotation + + +def _anchored_action_to_delta( + anchored_action: np.ndarray, + base_pose: tuple[np.ndarray, np.ndarray], + current_pose: tuple[np.ndarray, np.ndarray], + rotation_space: str, +) -> np.ndarray: + anchored_translation = anchored_action[:3] + rotation_dim = anchored_action.shape[0] - 4 + anchored_rotation = anchored_action[3 : 3 + rotation_dim] + gripper = anchored_action[3 + rotation_dim : 4 + rotation_dim] + + base_pos, base_rot = base_pose + current_pos, current_rot = current_pose + + if rotation_space == "3d": + anchored_rot = R.from_rotvec(anchored_rotation).as_matrix() + elif rotation_space == "6d": + anchored_rot = _rotation_repr_to_mat(anchored_rotation, rotation_space) + elif rotation_space == "9d": + anchored_rot = anchored_rotation.reshape(3, 3) + else: + raise ValueError(f"Unsupported rotation_space={rotation_space!r}. Use 3d/6d/9d.") + target_rot = base_rot @ anchored_rot + target_pos = base_pos + base_rot @ anchored_translation + delta_pos = target_pos - current_pos + delta_rot = target_rot @ current_rot.T + delta_rotvec = R.from_matrix(delta_rot).as_rotvec() + + return np.concatenate([delta_pos, delta_rotvec, gripper], axis=0) + + +def _framewise_action_to_delta( + framewise_action: np.ndarray, + rotation_space: str, +) -> np.ndarray: + """Convert a frame-wise policy action to LIBERO's 7D simulator command. + + Frame-wise actions are already per-step deltas in the LIBERO controller's + convention (see ``LiberoDataset`` with ``action_space='frame_wise_relative'``), + so the only conversion required is decoding the chosen rotation + representation back to a rotation vector. No anchor/current pose is needed. + """ + if rotation_space == "3d": + return framewise_action + + translation = framewise_action[:3] + rotation_dim = framewise_action.shape[0] - 4 + rotation_repr = framewise_action[3 : 3 + rotation_dim] + gripper = framewise_action[3 + rotation_dim : 4 + rotation_dim] + rotation_delta = _rotation_repr_to_mat(rotation_repr, rotation_space) + + delta_pos = translation + delta_rotvec = R.from_matrix(rotation_delta).as_rotvec() + return np.concatenate([delta_pos, delta_rotvec, gripper], axis=0) + + +def _run_episode( + env: Any, + client: ActionEnvironmentClient, + *, + cameras: list[str], + flip_images: bool, + rotate_180: bool, + action_horizon: int, + action_dim: int, + action_space: str, + rotation_space: str, + max_steps: int, + warmup_steps: int, + initial_state: np.ndarray | None, + gif_path: Path | None, + gif_fps: int, + comparison_path: Path | None = None, +) -> EpisodeResult: + env.reset() + if initial_state is not None: + obs = env.set_init_state(initial_state) + else: + obs = env.get_observation() + + action_queue: list[list[float]] = [] + base_pose: tuple[np.ndarray, np.ndarray] | None = None + step = 0 + success = False + gif_frames: list[Image.Image] = [] + action_log: list[list[float]] = [] + is_multi_view = len(cameras) > 1 + resolved_rotation_space = _infer_rotation_space(action_dim, rotation_space) + + comparison_windows: list[tuple[list[Image.Image], list[Image.Image]]] = [] + + def record_frame(current_obs: dict[str, Any]) -> None: + if gif_path is None: + return + image = _get_libero_image( + current_obs, + cameras[0], + flip_images=flip_images, + rotate_180=rotate_180, + ) + image = _ensure_uint8_image(image) + gif_frames.append(Image.fromarray(image).convert("RGB")) + + def capture_comparison_frame(current_obs: dict[str, Any]) -> Image.Image: + """Capture an env frame matching Action's input view (multi-view concatenated if applicable).""" + if is_multi_view: + imgs = _get_libero_images(current_obs, cameras, flip_images=flip_images, rotate_180=rotate_180) + concat = client.concatenate_images(imgs) + return Image.fromarray(_ensure_uint8_image(concat)).convert("RGB") + img = _get_libero_image(current_obs, cameras[0], flip_images=flip_images, rotate_180=rotate_180) + return Image.fromarray(_ensure_uint8_image(img)).convert("RGB") + + record_frame(obs) + + while step < max_steps: + if step < warmup_steps: + dummy = _get_libero_dummy_action() + obs, _, _, _ = env.step(dummy) + action_log.append(dummy) + step += 1 + record_frame(obs) + continue + + if not action_queue: + if is_multi_view: + observation_imgs = _get_libero_images( + obs, + cameras, + flip_images=flip_images, + rotate_180=rotate_180, + ) + result = client.predict(observation_imgs) + else: + observation_img = _get_libero_image( + obs, + cameras[0], + flip_images=flip_images, + rotate_180=rotate_180, + ) + result = client.predict(observation_img) + actions = result.get("action", []) + if not actions: + return EpisodeResult(False, step, "Empty action chunk from server", action_log) + action_queue = _select_action_chunk(actions, action_horizon) + + if comparison_path is not None: + action_video_b64 = result.get("video", []) + if action_video_b64: + action_frames = _decode_b64_frames(action_video_b64) + env_comparison_frames = [capture_comparison_frame(obs)] + comparison_windows.append((action_frames, env_comparison_frames)) + + if action_space == "relative": + base_pose = _obs_to_pose(obs) + + raw_action = _format_action(action_queue.pop(0), action_dim) + if action_space == "relative": + if base_pose is None: + raise RuntimeError("Missing base pose for relative action conversion") + current_pose = _obs_to_pose(obs) + action = _anchored_action_to_delta( + np.asarray(raw_action, dtype=np.float32), + base_pose, + current_pose, + resolved_rotation_space, + ) + action_list = action.tolist() + else: + action = _framewise_action_to_delta( + np.asarray(raw_action, dtype=np.float32), + resolved_rotation_space, + ) + action_list = action.tolist() + + # Remap gripper from [0, 1] (model/training range) to [-1, 1] (LIBERO env range) + action_list = _remap_gripper_to_neg1_pos1(action_list) + + action_log.append(action_list) + obs, _, done, info = env.step(action_list) + step += 1 + record_frame(obs) + + if comparison_path is not None and comparison_windows: + comparison_windows[-1][1].append(capture_comparison_frame(obs)) + + if isinstance(info, dict) and info.get("success"): + success = True + break + if done: + success = True if not isinstance(info, dict) else bool(info.get("success", True)) + break + + if gif_path is not None: + _save_gif(gif_frames, gif_path, gif_fps) + if comparison_path is not None and comparison_windows: + _save_comparison_gif(comparison_windows, comparison_path, gif_fps) + return EpisodeResult(success, step, None, action_log) + + +def _load_initial_states( + task_suite: Any, + task_id: int, + *, + task_description: str, + initial_states_path: str, + episode_idx: int, +) -> np.ndarray | None: + default_initial_states = task_suite.get_task_init_states(task_id) + + if initial_states_path == "DEFAULT": + return np.array(default_initial_states[episode_idx]) + + with open(initial_states_path, "r", encoding="utf-8") as f: + all_initial_states = json.load(f) + + task_key = task_description.replace(" ", "_") + episode_key = f"demo_{episode_idx}" + if not all_initial_states[task_key][episode_key]["success"]: + return None + return np.array(all_initial_states[task_key][episode_key]["initial_state"]) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="LIBERO closed-loop evaluation via Action HTTP server") + parser.add_argument( + "--server_url", type=str, required=True, help="Base URL for Action server (e.g., http://host:8000)" + ) + parser.add_argument("--task_suite", type=str, default="libero_spatial", choices=sorted(TASK_MAX_STEPS.keys())) + parser.add_argument("--num_trials_per_task", type=int, default=10) + parser.add_argument("--task_ids", type=str, default="", help="Comma-separated task IDs to evaluate (default: all)") + parser.add_argument("--image_size", type=int, default=256, help="Model input image size") + parser.add_argument("--env_image_size", type=int, default=256, help="Environment render resolution") + parser.add_argument("--action_horizon", type=int, default=0, help="Actions to execute per request (0=full chunk)") + parser.add_argument("--action_dim", type=int, default=10, help="Action dimension for LIBERO") + parser.add_argument( + "--action_space", + type=str, + default="frame_wise_relative", + choices=["relative", "frame_wise_relative"], + help="Action space expected from the model (relative=anchored, frame_wise_relative=framewise deltas).", + ) + parser.add_argument( + "--rotation_space", + type=str, + default="auto", + choices=["auto", "3d", "6d", "9d"], + help="Rotation representation for anchored actions (auto infers from action_dim).", + ) + parser.add_argument("--domain_name", type=str, default="libero") + parser.add_argument( + "--camera", + type=str, + default="agentview", + help="Camera(s) to use. Single camera: 'agentview' or 'wrist'. Multiple cameras: comma-separated, e.g., 'agentview,wrist'.", + ) + parser.add_argument("--flip_images", action="store_true", help="Flip images vertically before encoding") + parser.add_argument( + "--rotate_180", + action=argparse.BooleanOptionalAction, + default=True, + help="Rotate images by 180 degrees before encoding (default: True; pass --no-rotate-180 to disable)", + ) + parser.add_argument("--warmup_steps", type=int, default=10, help="Stabilization steps with dummy actions") + parser.add_argument("--max_steps", type=int, default=0, help="Override max steps per episode (0=default)") + parser.add_argument("--timeout", type=float, default=30.0, help="HTTP request timeout in seconds") + parser.add_argument("--wait_timeout", type=float, default=60.0, help="Seconds to wait for server health") + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--save_gifs", action="store_true", help="Save per-episode GIFs of rendered frames") + parser.add_argument( + "--save_comparison", + action="store_true", + help="Save side-by-side comparison GIFs (Action prediction vs environment rollout)", + ) + parser.add_argument("--gif_fps", type=int, default=20, help="Frames per second for saved GIFs") + parser.add_argument( + "--mujoco_gl", + type=str, + default="auto", + choices=["auto", "egl", "osmesa", "glfw"], + help="MuJoCo GL backend (auto picks egl if /dev/dri is accessible, else osmesa).", + ) + parser.add_argument( + "--render_gpu_device_id", + type=int, + default=-1, + help="GPU device index for EGL rendering (-1 uses default device).", + ) + parser.add_argument( + "--initial_states_path", + type=str, + default="DEFAULT", + help='Path to initial states JSON. Use "DEFAULT" for benchmark defaults.', + ) + parser.add_argument("--output_dir", type=str, default="", help="Directory to save evaluation summary JSON") + return parser.parse_args() + + +def main() -> None: + args = _parse_args() + random.seed(args.seed) + np.random.seed(args.seed) + + if args.save_gifs and not args.output_dir: + raise ValueError("--save_gifs requires --output_dir to be set") + if args.save_comparison and not args.output_dir: + raise ValueError("--save_comparison requires --output_dir to be set") + + # Parse cameras from comma-separated string + cameras = [c.strip() for c in args.camera.split(",") if c.strip()] + if not cameras: + raise ValueError("At least one camera must be specified") + for cam in cameras: + if cam not in ("agentview", "wrist"): + raise ValueError(f"Unsupported camera={cam!r}. Use 'agentview' or 'wrist'.") + + mujoco_backend = _configure_mujoco_env(args.mujoco_gl) + _import_libero() + + client = ActionEnvironmentClient( + server_url=args.server_url, + domain_name=args.domain_name, + prompt="", + image_size=args.image_size, + timeout=args.timeout, + ) + print(f"MuJoCo GL backend: {mujoco_backend}", flush=True) + print("Waiting for model server...", flush=True) + _wait_for_server(client, args.wait_timeout) + print(f"Connected to model server: {client.get_info()}", flush=True) + + benchmark_dict = benchmark.get_benchmark_dict() + task_suite = benchmark_dict[args.task_suite]() + num_tasks = int(task_suite.n_tasks) + + if args.task_ids: + selected_task_ids = [int(t) for t in args.task_ids.split(",") if t.strip()] + else: + selected_task_ids = list(range(num_tasks)) + + max_steps = args.max_steps if args.max_steps > 0 else TASK_MAX_STEPS[args.task_suite] + + total_episodes = 0 + total_successes = 0 + task_results: list[dict[str, Any]] = [] + + output_dir = Path(args.output_dir) if args.output_dir else None + gif_root = output_dir / "gifs" if output_dir and args.save_gifs else None + comparison_root = output_dir / "comparisons" if output_dir and args.save_comparison else None + + for task_id in selected_task_ids: + task = task_suite.get_task(task_id) + env, task_description = _get_libero_env( + task, + resolution=args.env_image_size, + seed=args.seed, + render_gpu_device_id=args.render_gpu_device_id, + ) + + task_episodes = 0 + task_successes = 0 + episode_results: list[dict[str, Any]] = [] + + for episode_idx in range(args.num_trials_per_task): + episode_t0 = time.perf_counter() + client.prompt = _augment_task_prompt_with_viewpoint(task_description, cameras) + initial_state = _load_initial_states( + task_suite, + task_id, + task_description=task_description, + initial_states_path=args.initial_states_path, + episode_idx=episode_idx, + ) + if initial_state is None: + episode_elapsed_s = time.perf_counter() - episode_t0 + episode_results.append( + { + "episode": episode_idx, + "success": False, + "steps": 0, + "error": "Skipped due to failed expert demo", + "elapsed_s": round(episode_elapsed_s, 3), + } + ) + print( + f"Task {task_id} | Episode {episode_idx + 1}/{args.num_trials_per_task} | " + "success=False steps=0 " + f"elapsed_s={episode_elapsed_s:.1f} " + "error='Skipped due to failed expert demo'", + flush=True, + ) + continue + + gif_path = ( + gif_root / f"task_{task_id:03d}" / f"episode_{episode_idx:03d}.gif" if gif_root is not None else None + ) + comparison_path = ( + comparison_root / f"task_{task_id:03d}" / f"episode_{episode_idx:03d}.gif" + if comparison_root is not None + else None + ) + try: + result = _run_episode( + env, + client, + cameras=cameras, + flip_images=args.flip_images, + rotate_180=args.rotate_180, + action_horizon=args.action_horizon, + action_dim=args.action_dim, + action_space=args.action_space, + rotation_space=args.rotation_space, + max_steps=max_steps, + warmup_steps=args.warmup_steps, + initial_state=initial_state, + gif_path=gif_path, + gif_fps=args.gif_fps, + comparison_path=comparison_path, + ) + except Exception as exc: + result = EpisodeResult(False, 0, str(exc), []) + episode_elapsed_s = time.perf_counter() - episode_t0 + + task_episodes += 1 + total_episodes += 1 + if result.success: + task_successes += 1 + total_successes += 1 + + episode_results.append( + { + "episode": episode_idx, + "success": result.success, + "steps": result.steps, + "error": result.error, + "elapsed_s": round(episode_elapsed_s, 3), + } + ) + + # Save per-episode action log as JSON + if output_dir is not None and result.actions: + action_log_dir = output_dir / "actions" / f"task_{task_id:03d}" + action_log_dir.mkdir(parents=True, exist_ok=True) + action_log_path = action_log_dir / f"episode_{episode_idx:03d}.json" + action_log_path.write_text( + json.dumps(result.actions, indent=2), + encoding="utf-8", + ) + + client.notify_next_episode() + + print( + f"Task {task_id} | Episode {episode_idx + 1}/{args.num_trials_per_task} | " + f"success={result.success} steps={result.steps} elapsed_s={episode_elapsed_s:.1f}", + flush=True, + ) + + task_success_rate = float(task_successes) / float(task_episodes) if task_episodes > 0 else 0.0 + task_results.append( + { + "task_id": task_id, + "task_description": task_description, + "episodes": task_episodes, + "successes": task_successes, + "success_rate": task_success_rate, + "episode_results": episode_results, + } + ) + print( + f"Task {task_id} summary: {task_successes}/{task_episodes} ({task_success_rate * 100:.1f}%)", + flush=True, + ) + + overall_success_rate = float(total_successes) / float(total_episodes) if total_episodes > 0 else 0.0 + summary = { + "task_suite": args.task_suite, + "total_episodes": total_episodes, + "total_successes": total_successes, + "overall_success_rate": overall_success_rate, + "num_trials_per_task": args.num_trials_per_task, + "selected_task_ids": selected_task_ids, + "action_space": args.action_space, + "rotation_space": _infer_rotation_space(args.action_dim, args.rotation_space), + "action_dim": args.action_dim, + "task_results": task_results, + } + + print( + f"Overall success rate: {total_successes}/{total_episodes} ({overall_success_rate * 100:.1f}%)", + flush=True, + ) + + if output_dir is not None: + output_dir.mkdir(parents=True, exist_ok=True) + summary_path = output_dir / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") + print(f"Saved summary to {summary_path}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py b/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py new file mode 100644 index 00000000..680da660 --- /dev/null +++ b/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py @@ -0,0 +1,665 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +HTTP server that serves ground-truth actions from LIBERO LeRobot datasets. + +Same HTTP interface as `cosmos3.scripts.action_policy_server` (the model-backed +server), enabling drop-in replacement for closed-loop evaluation to verify the +action pipeline with known-good GT actions. + +Endpoints: +- POST /predict: Return next chunk of GT actions for the given task (matched by prompt) +- GET /info: Return dataset info (tasks, episode counts) +- POST /next_episode: Advance to next episode for the task specified in request body +- POST /reset: Reset all per-task episode/step tracking + +Episode advancement: + The server auto-advances to the next episode when the current episode's actions + are exhausted. For early-termination cases (e.g. success before all actions are + consumed), call POST /next_episode with {"prompt": ""} between episodes. + +Example usage: + + +PYTHONPATH=. python cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py \ + --repo_id libero_10 \ + --root /path/to/libero_10_no_noops_1.0.0_lerobot_aligned \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --pose_coordinate_frame opencv \ + --action_chunk_size 16 \ + --send_video \ + --camera_mode agentview \ + --port 8000 + +# Multiple datasets: +PYTHONPATH=. python cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py \ + --repo_id libero_10,libero_goal \ + --root /path/to/libero_10,/path/to/libero_goal \ + --action_space relative \ + --rotation_space 6d \ + --pose_coordinate_frame opencv \ + --action_chunk_size 16 \ + --port 8000 +""" + +from __future__ import annotations + +import argparse +import base64 +import datetime +import io +import json +import socket +import threading +import time +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any + +import numpy as np +import torch +from PIL import Image + +from cosmos3._src.vfm.datasets.action.libero_pose_utils import ( + libero_rotation_format, +) +from cosmos3._src.vfm.datasets.action.pose_utils import convert_rotation + + +def _ts() -> str: + return datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +def _get_local_ip() -> str: + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + return str(s.getsockname()[0]) + except Exception: + return socket.gethostbyname(socket.gethostname()) + + +# --------------------------------------------------------------------------- +# Action processing (mirrors LIBERODataset.__getitem__ logic) +# --------------------------------------------------------------------------- + + +def _compute_anchored_actions( + state_raw: torch.Tensor, + action_raw: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Compute anchored relative actions, same as LIBERODataset._compute_anchored_actions. + + Actions are expressed in state_raw[0]'s local coordinate frame. + + Args: + state_raw: (T+1, 8) states [x, y, z, ax, ay, az, grip1, grip2]. + action_raw: (T+1, 7) actions [dx, dy, dz, dax, day, daz, grip]. + + Returns: + anchored_translation (T, 3), anchored_rotation (T, 3, 3), gripper (T, 1). + """ + p_states = state_raw[:, :3] + rotvec_states = state_raw[:, 3:6] + delta_p = action_raw[:-1, :3] + delta_rotvec = action_raw[:-1, 3:6] + gripper = action_raw[:-1, 6:7] + + R_states = convert_rotation(rotvec_states, "axisangle", "matrix") + R_deltas = convert_rotation(delta_rotvec, "axisangle", "matrix") + + p_0 = p_states[0] + R_0_T = R_states[0].T + + p_t = p_states[:-1] + R_t = R_states[:-1] + + p_target = p_t + delta_p + R_target = torch.bmm(R_deltas, R_t) + + anchored_p = (R_0_T @ (p_target - p_0).T).T + R_0_T_expanded = R_0_T.unsqueeze(0).expand(R_target.shape[0], -1, -1) + anchored_R = torch.bmm(R_0_T_expanded, R_target) + + return anchored_p, anchored_R, gripper + + +def _convert_rotation_to_repr(rotation_matrix: torch.Tensor, rotation_space: str) -> torch.Tensor: + return convert_rotation(rotation_matrix, "matrix", libero_rotation_format(rotation_space)) + + +def _process_action_chunk( + action_raw: torch.Tensor, + state_raw: torch.Tensor, + action_space: str, + rotation_space: str, +) -> torch.Tensor: + """Process a chunk of raw actions with the same logic as LIBERODataset.__getitem__. + + Args: + action_raw: (chunk+1, 7) raw actions covering chunk+1 consecutive frames. + state_raw: (chunk+1, 8) raw states covering chunk+1 consecutive frames. + action_space: "relative" or "frame_wise_relative". + rotation_space: "3d", "6d", or "9d". + + Returns: + Processed actions (chunk, D) where D depends on rotation_space. + """ + if action_space == "relative": + translation, rotation_matrix, gripper = _compute_anchored_actions(state_raw, action_raw) + elif action_space == "frame_wise_relative": + action = action_raw[:-1].clone() + translation = action[:, :3] + rotation_matrix = convert_rotation(action[:, 3:6], "axisangle", "matrix") + gripper = action[:, 6:] + else: + raise ValueError(f"Unsupported action_space: {action_space}") + + rotation = _convert_rotation_to_repr(rotation_matrix, rotation_space) + return torch.cat([translation, rotation, gripper], dim=-1) + + +# --------------------------------------------------------------------------- +# Data structures +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class EpisodeData: + action_raw: torch.Tensor # (N, 7) per-frame raw actions for the full episode + state_raw: torch.Tensor # (N, 8) per-frame raw states for the full episode + task_description: str + dataset_ref_idx: int # index into DatasetActionService._hf_datasets + frame_start: int # first global frame index in the HF dataset + frame_end: int # one-past-last global frame index + + +@dataclass(frozen=True) +class DatasetServerConfig: + repo_id: list[str] + root: list[str | None] + action_space: str + rotation_space: str + pose_coordinate_frame: str + action_chunk_size: int + max_action_dim: int + split: str + send_video: bool + camera_mode: str + image_size: int + + +# --------------------------------------------------------------------------- +# Service +# --------------------------------------------------------------------------- + + +class DatasetActionService: + """Serves GT actions (and optionally GT video) from pre-loaded LIBERO LeRobot episodes.""" + + def __init__(self, cfg: DatasetServerConfig) -> None: + self.cfg = cfg + self.episodes_by_task: dict[str, list[EpisodeData]] = {} + self._hf_datasets: list[Any] = [] + self._lerobot_datasets: list[Any] = [] + self._task_state: dict[str, dict[str, int]] = {} + self._lock = threading.Lock() + + if cfg.camera_mode in ("concat_view", "both"): + self._image_keys = ["observation.images.image", "observation.images.wrist_image"] + elif cfg.camera_mode == "wrist_image": + self._image_keys = ["observation.images.wrist_image"] + else: + self._image_keys = ["observation.images.image"] + + self._load_datasets() + + def _load_datasets(self) -> None: + from lerobot.datasets.lerobot_dataset import LeRobotDataset + + for repo_id, root in zip(self.cfg.repo_id, self.cfg.root): + print(f"[{_ts()}] [dataset-server] loading repo_id={repo_id} root={root} ...", flush=True) + t0 = time.monotonic() + + dataset = LeRobotDataset(repo_id=repo_id, root=root) + tasks_df = dataset.meta.tasks + hf = dataset.hf_dataset + ds_ref_idx = len(self._hf_datasets) + self._hf_datasets.append(hf) + + if self.cfg.send_video: + delta_ts: dict[str, list[float]] = {k: [0.0] for k in self._image_keys} + video_dataset = LeRobotDataset(repo_id=repo_id, root=root, delta_timestamps=delta_ts) + self._lerobot_datasets.append(video_dataset) + else: + self._lerobot_datasets.append(None) + + for ep_meta in dataset.meta.episodes: + ep_idx = int(ep_meta["episode_index"]) # type: ignore[index] + start = int(ep_meta["dataset_from_index"]) # type: ignore[index] + end = int(ep_meta["dataset_to_index"]) # type: ignore[index] + + ep_slice = hf.select(range(start, end)) + actions = torch.tensor(np.array(ep_slice["action"], dtype=np.float32)) + states = torch.tensor(np.array(ep_slice["observation.state"], dtype=np.float32)) + + task_idx = int(ep_slice[0]["task_index"]) + matching = tasks_df[tasks_df["task_index"] == task_idx] + task_desc = str(matching.iloc[0].name) if not matching.empty else f"task_{task_idx}" + + self.episodes_by_task.setdefault(task_desc, []).append( + EpisodeData( + action_raw=actions, + state_raw=states, + task_description=task_desc, + dataset_ref_idx=ds_ref_idx, + frame_start=start, + frame_end=end, + ) + ) + + dt = time.monotonic() - t0 + print( + f"[{_ts()}] [dataset-server] loaded {repo_id}: {dataset.meta.total_episodes} episodes in {dt:.1f}s", + flush=True, + ) + + total_tasks = len(self.episodes_by_task) + total_eps = sum(len(eps) for eps in self.episodes_by_task.values()) + print( + f"[{_ts()}] [dataset-server] ready: {total_tasks} tasks, {total_eps} episodes " + f"send_video={self.cfg.send_video} camera_mode={self.cfg.camera_mode}", + flush=True, + ) + + def _load_video_frames(self, episode: EpisodeData, step: int, num_frames: int) -> list[str]: + """Load GT video frames from the dataset and encode as base64 PNGs. + + Uses the LeRobotDataset wrapper (not the raw HF dataset) so that video-backed + datasets are decoded correctly via the configured video backend. + + Args: + episode: Episode data with dataset reference. + step: Step offset within the episode (0-based). + num_frames: Number of frames to load (typically action_chunk_size + 1). + + Returns: + List of base64-encoded PNG strings. + """ + lr_dataset = self._lerobot_datasets[episode.dataset_ref_idx] + if lr_dataset is None: + return [] + image_size = self.cfg.image_size + b64_frames: list[str] = [] + + for i in range(num_frames): + global_idx = episode.frame_start + step + i + if global_idx >= episode.frame_end: + break + + item = lr_dataset[global_idx] + + pil_images: list[Image.Image] = [] + for key in self._image_keys: + img_tensor = item[key] + if isinstance(img_tensor, torch.Tensor): + # LeRobot returns (T, C, H, W) with delta_timestamps=[0.0] -> (1, C, H, W) + if img_tensor.dim() == 4: + img_tensor = img_tensor[0] + # (C, H, W) float [0, 1] -> PIL + arr = (img_tensor.permute(1, 2, 0).clamp(0, 1) * 255).to(torch.uint8).numpy() + img = Image.fromarray(arr) + elif isinstance(img_tensor, Image.Image): + img = img_tensor + else: + img = Image.fromarray(np.asarray(img_tensor, dtype=np.uint8)) + img = img.convert("RGB").resize((image_size, image_size), Image.Resampling.BILINEAR) + pil_images.append(img) + + if len(pil_images) > 1: + total_w = sum(im.width for im in pil_images) + combined = Image.new("RGB", (total_w, image_size)) + x = 0 + for im in pil_images: + combined.paste(im, (x, 0)) + x += im.width + frame = combined + else: + frame = pil_images[0] + + buf = io.BytesIO() + frame.save(buf, format="PNG") + b64_frames.append(base64.b64encode(buf.getvalue()).decode("ascii")) + + return b64_frames + + # -- state management -- + + def _get_task_state(self, prompt: str) -> dict[str, int]: + if prompt not in self._task_state: + self._task_state[prompt] = {"episode_idx": 0, "step": 0} + return self._task_state[prompt] + + def _resolve_prompt(self, prompt: str) -> str: + """Resolve prompt to a known task description (exact or substring match).""" + if prompt in self.episodes_by_task: + return prompt + prompt_lower = prompt.lower().strip() + for task_desc in self.episodes_by_task: + if task_desc.lower().strip() == prompt_lower: + return task_desc + for task_desc in self.episodes_by_task: + td_lower = task_desc.lower().strip() + if prompt_lower in td_lower or td_lower in prompt_lower: + return task_desc + raise ValueError( + f"Task not found for prompt: {prompt!r}. Available tasks: {sorted(self.episodes_by_task.keys())}" + ) + + # -- endpoints -- + + def get_info(self) -> dict[str, Any]: + return { + "type": "dataset_action_server", + "action_space": self.cfg.action_space, + "rotation_space": self.cfg.rotation_space, + "action_chunk_size": self.cfg.action_chunk_size, + "tasks": {k: len(v) for k, v in sorted(self.episodes_by_task.items())}, + } + + def predict(self, req: dict[str, Any]) -> dict[str, Any]: + prompt = req.get("prompt") + if not isinstance(prompt, str): + raise ValueError("'prompt' must be a string") + + resolved_prompt = self._resolve_prompt(prompt) + + with self._lock: + state = self._get_task_state(resolved_prompt) + episodes = self.episodes_by_task[resolved_prompt] + + ep_idx = state["episode_idx"] % len(episodes) + episode = episodes[ep_idx] + step = state["step"] + + # Number of valid actions = num_frames - 1 (need pairs of consecutive frames) + max_actions = len(episode.action_raw) - 1 + + if step >= max_actions: + state["episode_idx"] = (ep_idx + 1) % len(episodes) + state["step"] = 0 + ep_idx = state["episode_idx"] + episode = episodes[ep_idx] + step = 0 + max_actions = len(episode.action_raw) - 1 + + chunk_size = min(self.cfg.action_chunk_size, max_actions - step) + # Slice chunk+1 frames for action computation (needs next-frame state) + raw_slice_end = step + chunk_size + 1 + action_chunk_raw = episode.action_raw[step:raw_slice_end] + state_chunk_raw = episode.state_raw[step:raw_slice_end] + + processed = _process_action_chunk( + action_chunk_raw, + state_chunk_raw, + self.cfg.action_space, + self.cfg.rotation_space, + ) + + # Pad to max_action_dim (same as the Action transform pipeline) + t, d = processed.shape + if d < self.cfg.max_action_dim: + processed = torch.cat( + [processed, torch.zeros(t, self.cfg.max_action_dim - d)], + dim=-1, + ) + + state["step"] += chunk_size + + action_list = processed.float().numpy().tolist() + + video_b64: list[str] = [] + if self.cfg.send_video: + video_b64 = self._load_video_frames(episode, step, num_frames=chunk_size + 1) + + print( + f"[{_ts()}] [dataset-server] predict prompt={resolved_prompt!r} " + f"ep={ep_idx} step={step}..{state['step']} actions={len(action_list)} " + f"video_frames={len(video_b64)}", + flush=True, + ) + return {"action": action_list, "video": video_b64} + + def next_episode(self, prompt: str | None = None) -> dict[str, Any]: + with self._lock: + if prompt is not None: + resolved = self._resolve_prompt(prompt) + state = self._get_task_state(resolved) + episodes = self.episodes_by_task[resolved] + state["episode_idx"] = (state["episode_idx"] + 1) % len(episodes) + state["step"] = 0 + print( + f"[{_ts()}] [dataset-server] next_episode task={resolved!r} -> ep={state['episode_idx']}", + flush=True, + ) + return {"task": resolved, "episode_idx": state["episode_idx"]} + + for task in self._task_state: + episodes = self.episodes_by_task.get(task, []) + self._task_state[task]["episode_idx"] = (self._task_state[task]["episode_idx"] + 1) % max( + len(episodes), 1 + ) + self._task_state[task]["step"] = 0 + print(f"[{_ts()}] [dataset-server] next_episode (all tasks)", flush=True) + return {"advanced_all": True} + + def reset(self) -> dict[str, str]: + with self._lock: + self._task_state.clear() + print(f"[{_ts()}] [dataset-server] reset", flush=True) + return {"status": "reset"} + + +# --------------------------------------------------------------------------- +# HTTP handler +# --------------------------------------------------------------------------- + + +class _DatasetHandler(BaseHTTPRequestHandler): + server: ThreadingHTTPServer # type: ignore[assignment] + + def _send_json(self, status_code: int, payload: dict[str, Any]) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status_code) + self.send_header("Content-Type", "application/json") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + try: + self.wfile.write(body) + except (BrokenPipeError, ConnectionResetError): + return + + def _read_json_body(self) -> dict[str, Any] | None: + try: + length = int(self.headers.get("Content-Length") or "0") + except ValueError: + self._send_json(400, {"error": "Invalid Content-Length"}) + return None + body = self.rfile.read(max(0, length)) + if not body: + return {} + try: + req = json.loads(body.decode("utf-8")) + except Exception as e: + self._send_json(400, {"error": f"Invalid JSON: {e}"}) + return None + if not isinstance(req, dict): + self._send_json(400, {"error": "JSON body must be an object"}) + return None + return req + + def do_GET(self) -> None: # noqa: N802 + service: DatasetActionService = getattr(self.server, "service") + if self.path == "/info": + self._send_json(200, service.get_info()) + elif self.path == "/": + self._send_json(200, {"status": "ok"}) + else: + self._send_json(404, {"error": "Not found"}) + + def do_POST(self) -> None: # noqa: N802 + service: DatasetActionService = getattr(self.server, "service") + + if self.path in ("/", "/predict"): + req = self._read_json_body() + if req is None: + return + try: + out = service.predict(req) + except Exception as e: + print(f"[{_ts()}] [dataset-server] predict ERROR: {e}", flush=True) + self._send_json(400, {"action": [], "error": str(e)}) + return + self._send_json(200, out) + + elif self.path == "/next_episode": + req = self._read_json_body() + prompt = req.get("prompt") if req else None + try: + out = service.next_episode(prompt) + except Exception as e: + self._send_json(400, {"error": str(e)}) + return + self._send_json(200, out) + + elif self.path == "/reset": + out = service.reset() + self._send_json(200, out) + + else: + self._send_json(404, {"error": "Not found"}) + + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 + return + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="HTTP server serving ground-truth actions from LIBERO LeRobot datasets." + ) + parser.add_argument( + "--repo_id", + type=str, + required=True, + help="Comma-separated LeRobot repo IDs (e.g. libero_10,libero_goal)", + ) + parser.add_argument( + "--root", + type=str, + required=True, + help="Comma-separated local paths to dataset roots (one per repo_id)", + ) + parser.add_argument( + "--action_space", + type=str, + default="frame_wise_relative", + choices=["relative", "frame_wise_relative"], + help="Action space (must match closed-loop eval's --action_space).", + ) + parser.add_argument( + "--rotation_space", + type=str, + default="6d", + choices=["3d", "6d", "9d"], + help="Rotation representation (must match closed-loop eval's action_dim).", + ) + parser.add_argument( + "--pose_coordinate_frame", + type=str, + default="native", + choices=["native", "opencv"], + help="Pose/action coordinate frame. Accepted for compatibility with LIBERO eval launchers.", + ) + parser.add_argument("--action_chunk_size", type=int, default=16, help="Number of actions per predict call") + parser.add_argument("--max_action_dim", type=int, default=32, help="Pad actions to this dimension") + parser.add_argument("--split", type=str, default="full", help="Dataset split (train/val/full)") + parser.add_argument( + "--send_video", + action="store_true", + help="Include GT video frames (base64 PNGs) in /predict responses, same format as the Action server.", + ) + parser.add_argument( + "--camera_mode", + type=str, + default="image", + choices=["agentview", "wrist_image", "concat_view", "both"], + help="Camera view(s) to include in video frames.", + ) + parser.add_argument("--image_size", type=int, default=256, help="Resize video frames to this height/width") + parser.add_argument("--host", type=str, default="0.0.0.0") + parser.add_argument("--port", type=int, default=8000) + args = parser.parse_args() + + repo_ids = [r.strip() for r in args.repo_id.split(",") if r.strip()] + roots = [r.strip() for r in args.root.split(",") if r.strip()] + if len(repo_ids) != len(roots): + raise ValueError(f"Number of repo_ids ({len(repo_ids)}) must match number of roots ({len(roots)})") + + cfg = DatasetServerConfig( + repo_id=repo_ids, + root=roots, + action_space=args.action_space, + rotation_space=args.rotation_space, + pose_coordinate_frame=args.pose_coordinate_frame, + action_chunk_size=int(args.action_chunk_size), + max_action_dim=int(args.max_action_dim), + split=args.split, + send_video=bool(args.send_video), + camera_mode=args.camera_mode, + image_size=int(args.image_size), + ) + + service = DatasetActionService(cfg) + local_ip = _get_local_ip() + + print( + f"[{_ts()}] [dataset-server] starting host={args.host} port={args.port} " + f"action_space={cfg.action_space} rotation_space={cfg.rotation_space} " + f"action_chunk_size={cfg.action_chunk_size}", + flush=True, + ) + print(f"[{_ts()}] [dataset-server] Server accessible at: http://{local_ip}:{args.port}/", flush=True) + print(f"[{_ts()}] [dataset-server] Endpoints:", flush=True) + print(f" - GET / : Health check", flush=True) + print(f" - GET /info : Dataset info (tasks, episode counts)", flush=True) + print(f" - POST /predict : Get next GT action chunk (same interface as Action server)", flush=True) + print(f" - POST /next_episode : Advance to next episode for a task", flush=True) + print(f" - POST /reset : Reset all per-task state", flush=True) + + httpd = ThreadingHTTPServer((args.host, int(args.port)), _DatasetHandler) + setattr(httpd, "service", service) + httpd.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/cosmos3/_src/vfm/models/__init__.py b/cosmos3/_src/vfm/models/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/hf_model.py b/cosmos3/_src/vfm/models/hf_model.py new file mode 100644 index 00000000..d3625a8c --- /dev/null +++ b/cosmos3/_src/vfm/models/hf_model.py @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Minimal HFModel for the vfm/ unified VLM training path. + +Responsibilities: + - ``__init__``: meta-init the underlying HF model via the appropriate + AutoClass (``AutoModelForImageTextToText`` / ``AutoModel`` / + ``AutoModelForCausalLM`` — see ``HFModel`` for selection rules); + no weights are loaded. + - ``apply_gradient_checkpointing``: wraps HF's standard + ``gradient_checkpointing_enable`` API. + - ``tie_embeddings``: re-establishes the input/output embedding tie after + FSDP wrapping + meta-materialization. + - ``load_weights``: dispatches to ``load_vlm_model`` (VLM) or + ``load_language_model`` (LLM) from ``safetensors_loader.py`` based on + ``vision_config``; returns the set of checkpoint keys that were loaded. + - ``forward``: pass-through returning logits. + +FSDP wrapping lives in ``vfm/models/parallelize_vlm.py::parallelize()``, +NOT here. +""" + +import torch +import torch.nn as nn +from accelerate import init_on_device +from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoModelForImageTextToText + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.models.utils.safetensors_loader import load_language_model, load_vlm_model +from cosmos3._src.vfm.utils.parallelism import ParallelDims + + +def _tensor_names_to_skip_for(model_type: str) -> list[str]: + """Per-model-type tensor-name regex skip list for load_vlm_model. + + Mirrors the upstream HF-model ``tensor_names_to_skip`` property from the + legacy VLM policy registry. Patterns match the **resolved model key** + (post-name_converter). Patterns are used twice inside load_vlm_model: + (1) the per-tensor loop skips copying matched model keys, (2) the + completeness check tolerates matched model keys that are absent from + the checkpoint. + + Registered VLMs (see + cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py): + - Qwen3-VL dense (2B/4B/8B/32B): no skips needed. + - NemotronH_Nano_VL_V2 (nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16): + RADIO backbone buffers — initialized by the module, not from ckpt. + """ + table: dict[str, list[str]] = { + "NemotronH_Nano_VL_V2": [ + r"vision_model\.radio_model\.summary_idxs", + r"vision_model\.radio_model\.input_conditioner\.norm_mean", + r"vision_model\.radio_model\.input_conditioner\.norm_std", + ], + } + return table.get(model_type, []) + + +class HFModel(nn.Module): + """Minimal HF model wrapper for the vfm/ unified VLM training path. + + Loads any HF causal LM or VL model on the meta device (no GPU memory) + via the appropriate AutoClass — see selection rules below. Weights are NOT + loaded in ``__init__``. Call :meth:`load_weights` after FSDP wrapping + + explicit meta-tensor materialization so each rank only fills its own shard. + + AutoClass selection (by vision_config presence + ``auto_map``): + - VLM with standard transformers registration (e.g. Qwen3-VL) + → ``AutoModelForImageTextToText``. Returns the full conditional-generation + class (e.g. ``Qwen3VLForConditionalGeneration``), which exposes ``.logits`` + on forward output. ``AutoModelForCausalLM`` raises ``ValueError`` for VLM + configs (``Qwen3VLConfig`` is not registered for that auto class), so it + cannot be used here. + - VLM with custom ``auto_map`` (e.g. NemotronVL): the registered entry maps + the full causal-LM class through ``AutoModel`` rather than + ``AutoModelForImageTextToText`` — use ``AutoModel`` for this case only. + - LLM (no ``vision_config``) → ``AutoModelForCausalLM``. Standard causal LM + with ``.logits``. + + Do NOT use ``AutoModel`` for the standard VLM path — it returns the backbone + only (e.g. ``Qwen3VLModel``), which does NOT have ``.logits``. + + FSDP / TP wrapping is applied externally by ``parallelize()`` in + ``vfm/models/parallelize_vlm.py``. + """ + + def __init__( + self, + model_name_or_path: str, + dtype: torch.dtype = torch.bfloat16, + attn_implementation: str = "flash_attention_2", + trust_remote_code: bool = True, + ): + super().__init__() + self.model_name_or_path = model_name_or_path + hf_config = AutoConfig.from_pretrained(model_name_or_path, trust_remote_code=trust_remote_code) + self.hf_config = hf_config + + # AutoClass selection by model type: + # - Standard VLM (Qwen3-VL, etc.): AutoModelForImageTextToText returns the full causal + # LM with .logits (Qwen3VLForConditionalGeneration, etc.). + # - Custom VLM with auto_map (e.g. NemotronVL): AutoModelForImageTextToText is not + # registered; use AutoModel instead which maps to the full causal LM via auto_map. + # - LLM (no vision_config): AutoModelForCausalLM → standard causal LM with .logits. + is_vlm = getattr(hf_config, "vision_config", None) is not None + auto_map = getattr(hf_config, "auto_map", None) or {} + if is_vlm: + if "AutoModelForImageTextToText" in auto_map or not auto_map: + # Standard VLM or no auto_map (rely on registered transformers type) + model_cls = AutoModelForImageTextToText + else: + # Custom VLM: use AutoModel which maps to the full causal-LM class via auto_map + model_cls = AutoModel + else: + model_cls = AutoModelForCausalLM + + # Meta init: allocates no GPU memory. FSDP2's ``fully_shard`` does NOT + # auto-materialize meta tensors; the caller (see ``vlm_model._init_vlm``) + # must explicitly materialize via ``_apply(empty_like, ...)`` between + # ``parallelize()`` and ``load_weights()``. + with init_on_device("meta", include_buffers=False): + self._model = model_cls.from_config( + hf_config, + attn_implementation=attn_implementation, + torch_dtype=dtype, + trust_remote_code=trust_remote_code, + ) + log.info(f"HFModel: {hf_config.model_type} ({'VLM' if is_vlm else 'LLM'}), dtype={dtype}") + + # Normalize floating-point *parameter* dtypes to ``dtype``. HF's + # ``from_config`` installs ``torch.set_default_dtype(dtype)`` around the + # init, but some HF submodules (and vendored remote-code classes) read + # ``config.torch_dtype`` directly or build tensors with an explicit + # ``dtype=`` kwarg, so their params can end up in the checkpoint's dtype + # (typically bf16) while the rest of the model is in ``dtype``. FSDP2's + # ``_init_mp_dtypes`` then asserts "uniform original parameter dtype … + # {bf16, fp32}". Normalize on meta (no GPU memory) so all FSDP units see + # a single original dtype. Buffers are left alone — ``inv_freq`` etc. + # must stay fp32 (enforced by e.g. qwen3_vl.py's inv_freq assertion). + n_cast = 0 + with torch.no_grad(): + for p in self._model.parameters(recurse=True): + if p.is_floating_point() and p.dtype != dtype: + p.data = p.data.to(dtype) + n_cast += 1 + if n_cast: + log.info(f"HFModel: normalized {n_cast} param(s) to {dtype} post-from_config") + + # Patch Qwen3-VL forward for text-only batches (no pixel_values / image_grid_thw). + # Required to avoid errors when a batch contains only text: every FSDP rank must + # call visual() each step for all-gather sync; the patch runs a lightweight dummy + # image and slices the output to [0:0] so it contributes no features. + # Must happen BEFORE parallelize() so FSDP captures the patched forward. + if hf_config.model_type == "qwen3_vl" and hasattr(self._model, "model"): + from cosmos3._src.vfm.utils.monkey_patch import patch_qwen3_vl_forward + + patch_qwen3_vl_forward(self._model.model) + log.info("HFModel: applied patch_qwen3_vl_forward for text-only batch support") + + def apply_gradient_checkpointing(self) -> None: + """Enable gradient checkpointing via HF's standard API.""" + self._model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False}) + log.info("HFModel: gradient checkpointing enabled") + + def tie_embeddings(self) -> None: + """Tie output embedding weight to input embedding, matching post_to_empty_hook behavior. + + Must be called AFTER ``parallelize()`` and AFTER the explicit + meta-tensor materialization step (FSDP2 does not auto-materialize — + see ``vlm_model._init_vlm`` step e), and BEFORE ``load_weights()`` so + the tied pointer survives weight loading. + + Two strategies, matching the HF API split: + 1. ``get_output_embeddings()`` path — standard for most HF models. + 2. ``_tied_weights_keys`` fallback — some VLMs (notably + ``Qwen3VLForConditionalGeneration``) define ``lm_head`` and + ``_tied_weights_keys = ["lm_head.weight"]`` but do NOT override + ``get_output_embeddings``. For those, walk the dotted key to the + owning module and assign its ``.weight`` directly. See spec §8.3. + + Reference: the legacy VLM HF-model tie_embeddings implementation. + """ + if not getattr(self.hf_config, "tie_word_embeddings", False): + return + input_embeddings = self._model.get_input_embeddings() + if input_embeddings is None: + return + output_embeddings = self._model.get_output_embeddings() + if output_embeddings is not None: + output_embeddings.weight = input_embeddings.weight + log.info("HFModel: tied input/output embeddings via get_output_embeddings") + return + # Fallback: HF models that use _tied_weights_keys instead of + # overriding get_output_embeddings (e.g. Qwen3VLForConditionalGeneration + # defines _tied_weights_keys = ["lm_head.weight"] but returns None + # from the default get_output_embeddings). Walk the dotted key to + # the owning module and assign the Parameter directly. + tied_keys = getattr(self._model, "_tied_weights_keys", None) or () + if not tied_keys: + return + for key in tied_keys: + parts = key.split(".") + *mod_path, attr = parts + target = self._model + for name in mod_path: + target = getattr(target, name, None) + if target is None: + log.warning( + f"HFModel.tie_embeddings: could not resolve path {key!r} on " + f"{type(self._model).__name__}; skipping tie (weights will " + f"remain untied for this key)." + ) + break + else: + setattr(target, attr, input_embeddings.weight) + log.info(f"HFModel: tied {key} via _tied_weights_keys fallback") + + def load_weights( + self, + checkpoint_path: str, + credential_path: str | None = None, + parallel_dims: ParallelDims | None = None, + extra_skip_patterns: list[str] | None = None, + ) -> set[str]: + r"""Load weights from a HF model directory (safetensors format). + + Dispatches on model type: + - VLM (vision_config present): ``load_vlm_model`` (universal + suffix-lookup loader inherited from the legacy VLM path; MoE VLMs + explicitly blocked — see spec §2.2). + - LLM (no vision_config): ``load_language_model`` — handles VFM-specific + per-family key remapping for Qwen3 / Nemotron (unchanged from today). + + Must be called AFTER ``parallelize()`` so parameters are DTensors with + CUDA local views. For tied-embedding models, ``tie_embeddings()`` must + be called between ``parallelize()`` and this function. + + Args: + checkpoint_path: Local path to a directory containing .safetensors files. + credential_path: S3 credential file, or None for local filesystem. + parallel_dims: ``ParallelDims`` instance (from + ``projects.cosmos3.vfm.utils.parallelism``). The loader uses + it via :func:`~projects.cosmos3.vfm.models.utils.safetensors_loader._get_dp_shard_mesh` + to obtain the 1-D ``dp_shard`` sub-mesh (or None when + ``dp_shard <= 1``) for striping checkpoint reads across + FSDP shard ranks. When non-None, the caller MUST have + called ``parallel_dims.build_meshes()`` first — neither + this method nor ``load_vlm_model`` re-checks this. Pass + ``parallel_dims=None`` for the single-rank fallback used + by single-process / non-distributed runs. + extra_skip_patterns: Optional list of regex patterns appended to + ``tensor_names_to_skip`` inside ``load_vlm_model``. Use when + overlaying an LLM-only checkpoint onto a VLM model (e.g. swapping + the language tower while preserving visual + projector params) + — pass patterns like ``r"model\.visual\."`` so those keys are + skipped during the overlay. Only takes effect on the VLM + dispatch path; ignored when the model is a pure LLM (no + ``vision_config``). + + Returns: + Set of model state-dict keys that were loaded from the checkpoint. + """ + is_vlm = getattr(self.hf_config, "vision_config", None) is not None + if is_vlm: + keys_loaded = load_vlm_model( + model=self._model, + checkpoint_path=checkpoint_path, + credential_path=credential_path, + parallel_dims=parallel_dims, + tensor_names_to_skip=_tensor_names_to_skip_for(self.hf_config.model_type), + extra_skip_patterns=extra_skip_patterns, + ) + else: + keys_loaded = load_language_model( + model=self._model, + checkpoint_path=checkpoint_path, + credential_path=credential_path if credential_path else "", + parallel_dims=parallel_dims, + ) + log.info(f"HFModel: weights loaded from {checkpoint_path} ({len(keys_loaded)} keys)") + return keys_loaded + + # Keys added by the VLM collate_fn (vlm/datasets/collate_fn.py) that are NOT valid + # HF model forward arguments. These must be stripped before calling self._model.forward(). + # A blocklist (not a whitelist) is used so that legitimate kwargs passed via the model's + # **kwargs — e.g. second_per_grid_ts for Qwen3-VL temporal encoding, output_router_logits + # for MoE load-balancing — are forwarded correctly even when not named in the signature. + _COLLATE_NON_MODEL_KEYS: frozenset[str] = frozenset( + { + "token_mask", + "pad_token_id", + "ignore_index", + "collated", + "raw_image", + "raw_video", + # image_sizes is collected by collate_fn but is NOT a Qwen3-VL forward arg + # (Qwen3-VL uses image_grid_thw instead). Strip it so strict HF signatures + # don't reject it. + # a future Phase extends to those, remove this entry. + "image_sizes", + } + ) + + def forward(self, **kwargs) -> torch.Tensor: + """Pass-through forward. Returns logits (B, T, V). + + Strips collate-added non-model keys (see ``_COLLATE_NON_MODEL_KEYS``: + token_mask, pad_token_id, ignore_index, collated, raw_image, raw_video, + image_sizes) before forwarding. Forces use_cache=False for training. + All remaining keys (including ``**kwargs`` pass-throughs such as + second_per_grid_ts) are forwarded unchanged. + + For nemotron_vl: attention_mask is also dropped. NemotronVLModel.get_rope_index + strips padding positions when attention_mask is present, returning position_ids + shorter than inputs_embeds (padded_len). With right-padding + causal attention, + valid tokens never attend to padding tokens regardless, so dropping attention_mask + is equivalent and avoids the shape mismatch. + """ + filtered = {k: v for k, v in kwargs.items() if k not in self._COLLATE_NON_MODEL_KEYS} + if self.hf_config.model_type == "nemotron_vl": + filtered.pop("attention_mask", None) + filtered["use_cache"] = False + out = self._model(**filtered) + return out.logits diff --git a/cosmos3/_src/vfm/models/llm/__init__.py b/cosmos3/_src/vfm/models/llm/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/llm/qwen3/__init__.py b/cosmos3/_src/vfm/models/llm/qwen3/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json b/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json new file mode 100644 index 00000000..5050f2fd --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json @@ -0,0 +1,30 @@ +{ + "architectures": [ + "Qwen3ForCausalLM" + ], + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 1024, + "initializer_range": 0.02, + "intermediate_size": 3072, + "max_position_embeddings": 40960, + "max_window_layers": 28, + "model_type": "qwen3", + "num_attention_heads": 16, + "num_hidden_layers": 28, + "num_key_value_heads": 8, + "rms_norm_eps": 1e-06, + "rope_scaling": null, + "rope_theta": 1000000, + "sliding_window": null, + "tie_word_embeddings": true, + "torch_dtype": "bfloat16", + "transformers_version": "4.51.0", + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 151936 + } diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py b/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py b/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py new file mode 100644 index 00000000..4a3bb1c0 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py @@ -0,0 +1,259 @@ +# Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +# This is adapted from src/transformers/models/qwen3/configuration_qwen3.py. +"""Qwen3 model configuration""" + +from typing import Optional + +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_rope_utils import rope_config_validation +from transformers.utils import logging + +logger = logging.get_logger(__name__) + + +# BAGEL-style: Add layer_type_validation function inline for compatibility +ALLOWED_LAYER_TYPES = ( + "full_attention", + "sliding_attention", + "chunked_attention", + "linear_attention", +) + + +def layer_type_validation(layer_types: list[str]) -> None: + """Check that each entry in `layer_types` are allowed.""" + if not all(layer_type in ALLOWED_LAYER_TYPES for layer_type in layer_types): + raise ValueError(f"The `layer_types` entries must be in {ALLOWED_LAYER_TYPES}") + + +class Qwen3Config(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3Model`]. It is used to instantiate a + Qwen3 model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-8B [Qwen/Qwen3-8B](https://huggingface.co/Qwen/Qwen3-8B). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + + Args: + vocab_size (`int`, *optional*, defaults to 151936): + Vocabulary size of the Qwen3 model. Defines the number of different tokens that can be represented by the + `inputs_ids` passed when calling [`Qwen3Model`] + hidden_size (`int`, *optional*, defaults to 4096): + Dimension of the hidden representations. + intermediate_size (`int`, *optional*, defaults to 22016): + Dimension of the MLP representations. + num_hidden_layers (`int`, *optional*, defaults to 32): + Number of hidden layers in the Transformer encoder. + num_attention_heads (`int`, *optional*, defaults to 32): + Number of attention heads for each attention layer in the Transformer encoder. + num_key_value_heads (`int`, *optional*, defaults to 32): + This is the number of key_value heads that should be used to implement Grouped Query Attention. If + `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if + `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When + converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed + by meanpooling all the original heads within that group. For more details, check out [this + paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`. + head_dim (`int`, *optional*, defaults to 128): + The attention head dimension. + hidden_act (`str` or `function`, *optional*, defaults to `"silu"`): + The non-linear activation function (function or string) in the decoder. + max_position_embeddings (`int`, *optional*, defaults to 32768): + The maximum sequence length that this model might ever be used with. + initializer_range (`float`, *optional*, defaults to 0.02): + The standard deviation of the truncated_normal_initializer for initializing all weight matrices. + rms_norm_eps (`float`, *optional*, defaults to 1e-06): + The epsilon used by the rms normalization layers. + use_cache (`bool`, *optional*, defaults to `True`): + Whether or not the model should return the last key/values attentions (not used by all models). Only + relevant if `config.is_decoder=True`. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether the model's input and output word embeddings should be tied. + rope_theta (`float`, *optional*, defaults to 10000.0): + The base period of the RoPE embeddings. + rope_scaling (`Dict`, *optional*): + Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type + and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value + accordingly. + Expected contents: + `rope_type` (`str`): + The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope', + 'llama3'], with 'default' being the original RoPE implementation. + `factor` (`float`, *optional*): + Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In + most scaling types, a `factor` of x will enable the model to handle sequences of length x * + original maximum pre-trained length. + `original_max_position_embeddings` (`int`, *optional*): + Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during + pretraining. + `attention_factor` (`float`, *optional*): + Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention + computation. If unspecified, it defaults to value recommended by the implementation, using the + `factor` field to infer the suggested value. + `beta_fast` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear + ramp function. If unspecified, it defaults to 32. + `beta_slow` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear + ramp function. If unspecified, it defaults to 1. + `short_factor` (`list[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to short contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `long_factor` (`list[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to long contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `low_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE + `high_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE + attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`): + Whether to use a bias in the query, key, value and output projection layers during self-attention. + use_sliding_window (`bool`, *optional*, defaults to `False`): + Whether to use sliding window attention. + sliding_window (`int`, *optional*, defaults to 4096): + Sliding window attention (SWA) window size. If not specified, will default to `4096`. + max_window_layers (`int`, *optional*, defaults to 28): + The number of layers using full attention. The first `max_window_layers` layers will use full attention, while any + additional layer afterwards will use SWA (Sliding Window Attention). + layer_types (`list`, *optional*): + Attention pattern for each layer. + attention_dropout (`float`, *optional*, defaults to 0.0): + The dropout ratio for the attention probabilities. + + ```python + >>> from transformers import Qwen3Model, Qwen3Config + + >>> # Initializing a Qwen3 style configuration + >>> configuration = Qwen3Config() + + >>> # Initializing a model from the Qwen3-8B style configuration + >>> model = Qwen3Model(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3" + keys_to_ignore_at_inference = ["past_key_values"] # noqa: RUF012 + + # Default tensor parallel plan for base model `Qwen3` + base_model_tp_plan = { # noqa: RUF012 + "layers.*.self_attn.q_proj": "colwise", + "layers.*.self_attn.k_proj": "colwise", + "layers.*.self_attn.v_proj": "colwise", + "layers.*.self_attn.o_proj": "rowwise", + "layers.*.mlp.gate_proj": "colwise", + "layers.*.mlp.up_proj": "colwise", + "layers.*.mlp.down_proj": "rowwise", + } + base_model_pp_plan = { # noqa: RUF012 + "embed_tokens": (["input_ids"], ["inputs_embeds"]), + "layers": (["hidden_states", "attention_mask"], ["hidden_states"]), + "norm": (["hidden_states"], ["hidden_states"]), + } + + def __init__( + self, + vocab_size: Optional[int] = 151936, + hidden_size: Optional[int] = 4096, + intermediate_size: Optional[int] = 22016, + num_hidden_layers: Optional[int] = 32, + num_attention_heads: Optional[int] = 32, + num_key_value_heads: Optional[int] = 32, + head_dim: Optional[int] = 128, + hidden_act: Optional[str] = "silu", + max_position_embeddings: Optional[int] = 32768, + initializer_range: Optional[float] = 0.02, + rms_norm_eps: Optional[float] = 1e-6, + use_cache: Optional[bool] = True, + tie_word_embeddings: Optional[bool] = False, + rope_theta: Optional[float] = 10000.0, + rope_scaling: Optional[dict] = None, + attention_bias: Optional[bool] = False, + use_sliding_window: Optional[bool] = False, + sliding_window: Optional[int] = 4096, + max_window_layers: Optional[int] = 28, + layer_types: Optional[list] = None, + attention_dropout: Optional[float] = 0.0, + **kwargs, + ) -> None: + self.vocab_size = vocab_size + self.max_position_embeddings = max_position_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.use_sliding_window = use_sliding_window + self.sliding_window = sliding_window if self.use_sliding_window else None + self.max_window_layers = max_window_layers + + # for backward compatibility + if num_key_value_heads is None: + num_key_value_heads = num_attention_heads + + self.num_key_value_heads = num_key_value_heads + self.head_dim = head_dim + self.hidden_act = hidden_act + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.rope_scaling = rope_scaling + self.attention_bias = attention_bias + self.attention_dropout = attention_dropout + # Validate the correctness of rotary position embeddings parameters + # BC: if there is a 'type' field, move it to 'rope_type'. + if self.rope_scaling is not None and "type" in self.rope_scaling: + self.rope_scaling["rope_type"] = self.rope_scaling["type"] + rope_config_validation(self) + + self.layer_types = layer_types + if self.layer_types is None: + self.layer_types = [ + ( + "sliding_attention" + if self.sliding_window is not None and i >= self.max_window_layers + else "full_attention" + ) + for i in range(self.num_hidden_layers) + ] + layer_type_validation(self.layer_types) + + super().__init__( + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + +__all__ = ["Qwen3Config"] diff --git a/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py b/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py new file mode 100644 index 00000000..7b22dd05 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py @@ -0,0 +1,751 @@ +# Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +# This is adapted from src/transformers/models/qwen3/modeling_qwen3.py. +"""PyTorch Qwen3 model.""" + +from functools import wraps +from typing import Any, Callable, Optional, Union + +import torch +from torch import nn +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache, DynamicCache +from transformers.generation import GenerationMixin +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast +from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.processing_utils import Unpack +from transformers.utils import logging +from transformers.utils.deprecation import deprecate_kwarg + +from cosmos3._src.vfm.models.llm.qwen3.configuration_qwen3 import Qwen3Config + +TransformersKwargs = Any + +# Import masking functions from utils for full transformers compatibility +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import ( + create_causal_mask, + create_sliding_window_causal_mask, +) + +logger = logging.get_logger(__name__) + + +def can_return_tuple(func): # noqa: ANN001, ANN202 + """ + Decorator to wrap model method, to call output.to_tuple() if return_dict=False passed as a kwarg or + use_return_dict=False is set in the config. + + Note: + output.to_tuple() convert output to tuple skipping all `None` values. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): # noqa: ANN001, ANN202 + return_dict = self.config.return_dict if hasattr(self, "config") else True + return_dict_passed = kwargs.pop("return_dict", return_dict) + if return_dict_passed is not None: + return_dict = return_dict_passed + output = func(self, *args, **kwargs) + if not return_dict and not isinstance(output, tuple): + output = output.to_tuple() + return output + + return wrapper + + +# Documentation strings +QWEN3_START_DOCSTRING = r""" + This model inherits from [`PreTrainedModel`]. Check the superclass documentation for the generic methods the + library implements for all its model (such as downloading or saving, resizing the input embeddings, pruning heads + etc.) + + This model is also a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass. + Use it as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage + and behavior. + + Parameters: + config ([`Qwen3Config`]): + Model configuration class with all the parameters of the model. Initializing with a config file does not + load the weights associated with the model, only the configuration. Check out the + [`~PreTrainedModel.from_pretrained`] method to load the model weights. +""" + +QWEN3_INPUTS_DOCSTRING = r""" + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. Padding will be ignored by default should you provide + it. + + Indices can be obtained using [`AutoTokenizer`]. See [`PreTrainedTokenizer.encode`] and + [`PreTrainedTokenizer.__call__`] for details. + + [What are input IDs?](../glossary#input-ids) + attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`: + + - 1 for tokens that are **not masked**, + - 0 for tokens that are **masked**. + + [What are attention masks?](../glossary#attention-mask) + position_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Indices of positions of each input sequence tokens in the position embeddings. Selected in the range `[0, + config.n_positions - 1]`. + + [What are position IDs?](../glossary#position-ids) + past_key_values (`Cache` or `tuple(tuple(torch.FloatTensor))`, *optional*): + Pre-computed hidden-states (key and values in the self-attention blocks and in the cross-attention + blocks) that can be used to speed up sequential decoding. This typically consists in the `past_key_values` + returned by the model at a previous stage of decoding, when `use_cache=True` or `config.use_cache=True`. + + Two formats are allowed: + - a [`~cache_utils.Cache`] instance, see our + [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache); + - Tuple of `tuple(torch.FloatTensor)` of length `config.n_layers`, with each tuple having 2 tensors of + shape `(batch_size, num_heads, sequence_length, embed_size_per_head)`). This is also known as the legacy + cache format. + + The model will output the same cache format that is fed as input. If no `past_key_values` are passed, the + legacy cache format will be returned. + + If `past_key_values` are used, the user can optionally input only the last `input_ids` (those that don't + have their past key value states given to this model) of shape `(batch_size, 1)` instead of all `input_ids` + of shape `(batch_size, sequence_length)`. + inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`, *optional*): + Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation. This + is useful if you want more control over how to convert `input_ids` indices into associated vectors than the + model's internal embedding lookup matrix. + use_cache (`bool`, *optional*): + If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding (see + `past_key_values`). + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned + tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for + more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple. + cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*): + Indices depicting the position of the input sequence tokens in the sequence. Contrarily to `position_ids`, + this tensor is not affected by padding. It is used to update the cache in the correct position and to infer + the complete sequence length. +""" + + +class Qwen3RMSNorm(nn.Module): + def __init__(self, hidden_size: int, eps: float = 1e-6) -> None: + """ + Qwen3RMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + def extra_repr(self) -> str: + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Qwen3MLP(nn.Module): + def __init__(self, config: Qwen3Config) -> None: + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x: torch.Tensor) -> torch.Tensor: # x: [B,N,hidden_size] + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) # [B,N,hidden_size] + return down_proj + + +def rotate_half(x: torch.Tensor) -> torch.Tensor: # x: [...,head_dim] + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] # [...,head_dim//2] + x2 = x[..., x.shape[-1] // 2 :] # [...,head_dim//2] + return torch.cat((-x2, x1), dim=-1) # [...,head_dim] + + +def apply_rotary_pos_emb( + q: torch.Tensor, # [B,num_heads,N,head_dim] + k: torch.Tensor, # [B,num_kv_heads,N,head_dim] + cos: torch.Tensor, # [B,N,head_dim] + sin: torch.Tensor, # [B,N,head_dim] + position_ids: Optional[torch.Tensor] = None, + unsqueeze_dim: int = 1, +) -> tuple[torch.Tensor, torch.Tensor]: # ([B,num_heads,N,head_dim], [B,num_kv_heads,N,head_dim]) + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + position_ids (`torch.Tensor`, *optional*): + Deprecated and unused. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + sin = sin.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [B,num_heads,N,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [B,num_kv_heads,N,head_dim] + return q_embed, k_embed + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: # hidden_states: [B,num_kv_heads,N,head_dim] + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states # [B,num_kv_heads,N,head_dim] + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) # [B,num_kv_heads,n_rep,N,head_dim] + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) # [B,num_heads,N,head_dim] + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, # [B,num_heads,N_q,head_dim] + key: torch.Tensor, # [B,num_kv_heads,N_kv,head_dim] + value: torch.Tensor, # [B,num_kv_heads,N_kv,head_dim] + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +) -> tuple[torch.Tensor, torch.Tensor]: # ([B,N_q,num_heads,head_dim], [B,num_heads,N_q,N_kv]) + key_states = repeat_kv(key, module.num_key_value_groups) # [B,num_heads,N_kv,head_dim] + value_states = repeat_kv(value, module.num_key_value_groups) # [B,num_heads,N_kv,head_dim] + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling # [B,num_heads,N_q,N_kv] + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] # [B,1,N_q,N_kv] + attn_weights = attn_weights + causal_mask # [B,num_heads,N_q,N_kv] + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query.dtype + ) # [B,num_heads,N_q,N_kv] + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) # [B,num_heads,N_q,N_kv] + attn_output = torch.matmul(attn_weights, value_states) # [B,num_heads,N_q,head_dim] + attn_output = attn_output.transpose(1, 2).contiguous() # [B,N_q,num_heads,head_dim] + + return attn_output, attn_weights + + +class Qwen3Attention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = True + + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias + ) + self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) # unlike olmo, only on the head dim! + self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) # thus post q_norm does not need reshape + self.sliding_window = config.sliding_window if config.layer_types[layer_idx] == "sliding_attention" else None + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, # [B,N,hidden_size] + position_embeddings: tuple[torch.Tensor, torch.Tensor], # ([B,N,head_dim], [B,N,head_dim]) + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: # ([B,N,hidden_size], optional [B,num_heads,N,N_kv]) + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_heads,N,head_dim] + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_kv_heads,N,head_dim] + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) # [B,num_kv_heads,N,head_dim] + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb( + query_states, key_states, cos, sin + ) # [B,num_heads,N,head_dim], [B,num_kv_heads,N,head_dim] + + if past_key_values is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + key_states, value_states = past_key_values.update( + key_states, value_states, self.layer_idx, cache_kwargs + ) # [B,num_kv_heads,N_kv,head_dim] + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, # diff with Llama + **kwargs, + ) + # attn_output: [B,N,num_heads,head_dim] + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() # [B,N,hidden_size] + attn_output = self.o_proj(attn_output) # [B,N,hidden_size] + return attn_output, attn_weights + + +class Qwen3DecoderLayer(nn.Module): + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + + self.self_attn = Qwen3Attention(config=config, layer_idx=layer_idx) + + self.mlp = Qwen3MLP(config) + self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.attention_type = config.layer_types[layer_idx] + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + output_attentions: Optional[bool] = False, + use_cache: Optional[bool] = False, + cache_position: Optional[torch.LongTensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, # necessary, but kept here for BC + **kwargs: Unpack[TransformersKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + residual = hidden_states # [B,N,hidden_size] + hidden_states = self.input_layernorm(hidden_states) # [B,N,hidden_size] + # Self Attention + hidden_states, self_attn_weights = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + # hidden_states: [B,N,hidden_size] + hidden_states = residual + hidden_states # [B,N,hidden_size] + + # Fully Connected + residual = hidden_states # [B,N,hidden_size] + hidden_states = self.post_attention_layernorm(hidden_states) # [B,N,hidden_size] + hidden_states = self.mlp(hidden_states) # [B,N,hidden_size] + hidden_states = residual + hidden_states # [B,N,hidden_size] + + outputs = (hidden_states,) + + if output_attentions: + outputs += (self_attn_weights,) + + return outputs + + +class Qwen3PreTrainedModel(PreTrainedModel): + config_class = Qwen3Config + config: Qwen3Config + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["Qwen3DecoderLayer"] # noqa: RUF012 + _skip_keys_device_placement = ["past_key_values"] # noqa: RUF012 + _supports_flash_attn = True + _supports_sdpa = True + _supports_flex_attn = True + + _can_compile_fullgraph = True + _supports_attention_backend = True + _can_record_outputs = { # noqa: RUF012 + "hidden_states": Qwen3DecoderLayer, + "attentions": Qwen3Attention, + } + + def _init_weights(self, module: nn.Module) -> None: + std = self.config.initializer_range + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.Embedding): + module.weight.data.normal_(mean=0.0, std=std) + if module.padding_idx is not None: + module.weight.data[module.padding_idx].zero_() + elif isinstance(module, Qwen3RotaryEmbedding): + module._init_weights() + + def init_weights(self) -> None: + self.apply(self._init_weights) + + +class Qwen3RotaryEmbedding(nn.Module): + inv_freq: torch.Tensor # fix linting for `register_buffer` + + def __init__(self, config: Qwen3Config, device: Optional[torch.device] = None) -> None: + super().__init__() + # BC: "rope_type" was originally "type" + if hasattr(config, "rope_scaling") and isinstance(config.rope_scaling, dict): + self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type")) + else: + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + + self.config = config + self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + self.device = device + self._init_weights() + + def _init_weights(self) -> None: + inv_freq, self.attention_scaling = self.rope_init_fn(self.config, self.device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self.original_inv_freq = self.inv_freq + + @torch.no_grad() + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward( + self, x: torch.Tensor, position_ids: torch.Tensor + ) -> tuple[torch.Tensor, torch.Tensor]: # position_ids: [B,N] -> ([B,N,head_dim], [B,N,head_dim]) + inv_freq_expanded = ( + self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device) + ) # [B,head_dim//2,1] + position_ids_expanded = position_ids[:, None, :].float() # [B,1,N] + + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" + with torch.autocast(device_type=device_type, enabled=False): # Force float32 + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) # [B,N,head_dim//2] + emb = torch.cat((freqs, freqs), dim=-1) # [B,N,head_dim] + cos = emb.cos() * self.attention_scaling # [B,N,head_dim] + sin = emb.sin() * self.attention_scaling # [B,N,head_dim] + + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) # [B,N,head_dim], [B,N,head_dim] + + +class Qwen3Model(Qwen3PreTrainedModel): + def __init__(self, config: Qwen3Config) -> None: + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + self.layers = nn.ModuleList( + [Qwen3DecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] + ) + self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3RotaryEmbedding(config=config) + self.gradient_checkpointing = False + self.has_sliding_layers = "sliding_attention" in self.config.layer_types + + # Initialize weights and apply final processing + self.post_init() + + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> BaseModelOutputWithPast: + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + use_cache = use_cache if use_cache is not None else self.config.use_cache + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) # [B,N,hidden_size] + + if use_cache and past_key_values is None: + # Compatibility: DynamicCache constructor changed between transformers versions + try: + past_key_values = DynamicCache(config=self.config) + except TypeError: + # Fallback for older transformers versions (4.51.3) that don't accept config + past_key_values = DynamicCache() + + if cache_position is None: + past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0 + cache_position = torch.arange( + past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device + ) + + if position_ids is None: + position_ids = cache_position.unsqueeze(0) + + # It may already have been prepared by e.g. `generate` + if not isinstance(causal_mask_mapping := attention_mask, dict): + # Prepare mask arguments + mask_kwargs = { + "config": self.config, + "input_embeds": inputs_embeds, + "attention_mask": attention_mask, + "cache_position": cache_position, + "past_key_values": past_key_values, + "position_ids": position_ids, + } + # Create the masks using our minimal implementations + causal_mask_mapping = { + "full_attention": create_causal_mask(**mask_kwargs), + } + # The sliding window alternating layers are not always activated depending on the config + if self.has_sliding_layers: + causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(**mask_kwargs) + + hidden_states = inputs_embeds # [B,N,hidden_size] + + # create position embeddings to be shared across the decoder layers + position_embeddings = self.rotary_emb(hidden_states, position_ids) # ([B,N,head_dim], [B,N,head_dim]) + + # decoder layers + all_hidden_states = () if output_hidden_states else None + all_self_attns = () if output_attentions else None + + for decoder_layer in self.layers[: self.config.num_hidden_layers]: + if output_hidden_states: + all_hidden_states += (hidden_states,) + + layer_outputs = decoder_layer( + hidden_states, + attention_mask=causal_mask_mapping[decoder_layer.attention_type], + position_ids=position_ids, + past_key_values=past_key_values, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_self_attns += (layer_outputs[1],) + + hidden_states = self.norm(hidden_states) # [B,N,hidden_size] + + # add hidden states from the last decoder layer + if output_hidden_states: + all_hidden_states += (hidden_states,) + + if not return_dict: + return tuple( + v + for v in [hidden_states, past_key_values if use_cache else None, all_hidden_states, all_self_attns] + if v is not None + ) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values if use_cache else None, + hidden_states=all_hidden_states, + attentions=all_self_attns, + ) + + +class Qwen3ForCausalLM(Qwen3PreTrainedModel, GenerationMixin): + _tied_weights_keys = ["lm_head.weight"] # noqa: RUF012 + _tp_plan = {"lm_head": "colwise_rep"} # noqa: RUF012 + _pp_plan = {"lm_head": (["hidden_states"], ["logits"])} # noqa: RUF012 + + def __init__(self, config: Qwen3Config) -> None: + super().__init__(config) + self.model = Qwen3Model(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def tie_weights(self) -> None: + """ + Tie the weights between the input embeddings and the output embeddings. + + Since lm_head.weight is in _tied_weights_keys, we always tie the weights + to ensure compatibility with checkpoints that have tied embeddings. + """ + # Always tie weights since lm_head.weight is in _tied_weights_keys + # This ensures compatibility with checkpoints that don't have separate lm_head.weight + self._tie_or_clone_weights(self.lm_head, self.model.embed_tokens) + + @can_return_tuple + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[TransformersKwargs], + ) -> CausalLMOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + Example: + + ```python + >>> from transformers import AutoTokenizer, Qwen3ForCausalLM + + >>> model = Qwen3ForCausalLM.from_pretrained("Qwen/Qwen3-8B") + >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + outputs: BaseModelOutputWithPast = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs.last_hidden_state # [B,N,hidden_size] + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep + logits = self.lm_head(hidden_states[:, slice_indices, :]) # [B,N_keep,vocab_size] + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs) + + if not return_dict: + output = (logits, *outputs[1:]) + return (loss, *output) if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +__all__ = [ + "QWEN3_INPUTS_DOCSTRING", + "QWEN3_START_DOCSTRING", + # Documentation constants + "Qwen3ForCausalLM", + "Qwen3Model", + "Qwen3PreTrainedModel", + "Qwen3RMSNorm", + "Qwen3RotaryEmbedding", + "apply_rotary_pos_emb", + "eager_attention_forward", + "repeat_kv", + # Utility functions + "rotate_half", +] diff --git a/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py b/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py new file mode 100644 index 00000000..adef8600 --- /dev/null +++ b/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py @@ -0,0 +1,976 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Comprehensive test script for Qwen3 LLM with output control integration. + +This script runs all tests: +1. LLM implementation compatibility checks +2. Memory-efficient collection behavior tests +3. Return format control verification +4. Input/output functionality tests +5. HuggingFace model comparison tests +6. Pretrained weights tests + +Usage (run from imaginaire4 directory): + pytest -v cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py --all -s + +Example - Using Qwen3 LLM Model Directly: + import torch + from cosmos3._src.vfm.models.llm.qwen3.qwen3 import Qwen3ForCausalLM + from cosmos3._src.vfm.models.llm.qwen3.configuration_qwen3 import Qwen3Config + from cosmos3._src.vfm.models.llm.qwen2.tokenization_qwen2 import Qwen2Tokenizer + + # Option 1: Load from HuggingFace Hub (original) + model_name = "Qwen/Qwen3-0.6B" + config = Qwen3Config.from_pretrained(model_name) + model = Qwen3ForCausalLM.from_pretrained(model_name, config=config, torch_dtype=torch.float32) + tokenizer = Qwen2Tokenizer.from_pretrained(model_name) + + # Option 2: Load from Local Config (like qwen2 pattern) + config = Qwen3Config.from_json_file( + "cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json" + ) + model = Qwen3ForCausalLM(config=config) # Create with local config + tokenizer = Qwen2Tokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct") # Remote tokenizer + + # Prepare input + prompt = "Give me a short introduction to large language models." + inputs = tokenizer(prompt, return_tensors="pt") + + # Generate with MoT-style output controls + with torch.no_grad(): + outputs = model.generate( + **inputs, + max_new_tokens=50, + do_sample=False, + output_attentions=True, # LLM MoT-style control + output_hidden_states=True, # LLM MoT-style control + return_dict_in_generate=True + ) + + # Decode result + response = tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) + print(f"Generated: {response}") +""" + +import inspect +import os +import sys +import traceback + +import pytest +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +# MoT/Qwen3 imports +from cosmos3._src.vfm.models.llm.qwen3.configuration_qwen3 import Qwen3Config, layer_type_validation +from cosmos3._src.vfm.models.llm.qwen3.qwen3 import Qwen3ForCausalLM, Qwen3Model +from cosmos3._src.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer + + +# GPU device detection +def get_device(): + """Get the best available device (GPU if available, otherwise CPU)""" + if torch.cuda.is_available(): + device = torch.device("cuda") + print(f"Using GPU: {torch.cuda.get_device_name()}") + print(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB") + else: + device = torch.device("cpu") + print("Using CPU (CUDA not available)") + return device + + +# Validate script is run from the correct directory +# Should be run from imaginaire4 directory with: python -m cosmos3._src.vfm.models.llm.qwen3.test_qwen3 # noqa: E501 +current_working_dir = os.getcwd() # Should be imaginaire4 +language_model_dir = "cosmos3/_src/vfm/models/llm" + +# Validate we're running from the correct directory +if not os.path.exists(language_model_dir): + print("ERROR: This script must be run from the imaginaire4 directory.") + print(f"Current directory: {current_working_dir}") + print(f"Expected to find: {language_model_dir}") + print("Please run: cd /path/to/imaginaire4 && python -m cosmos3._src.vfm.models.llm.qwen3.test_qwen3") # noqa: E501 + sys.exit(1) + + +def load_llm_tokenizer(model_name): + """Load tokenizer with fallback chain: Fast / Slow""" + tokenizer = Qwen2Tokenizer.from_pretrained(model_name) + print(" [OK] Using Qwen2Tokenizer") + return tokenizer + + +def initialize_models_and_tokenizers(model_name, device, is_large_model=False): + """Initialize all models and tokenizers once for reuse across tests""" + print(f"\nINITIALIZING MODELS ({model_name})...") + print("=" * 60) + + # Load configuration + print(f"Loading configuration from {model_name}...") + config = Qwen3Config.from_pretrained(model_name) + print(f" Config: vocab_size={config.vocab_size}, hidden_size={config.hidden_size}") + + # Initialize LLM models with pretrained weights + print("Loading LLM models with pretrained weights...") + if not is_large_model: + llm_model = Qwen3Model.from_pretrained(model_name, config=config).to(device) + llm_model.eval() + else: + llm_model = None + + llm_causal_model = Qwen3ForCausalLM.from_pretrained(model_name, config=config).to(device) + llm_causal_model.eval() + print(f" [OK] LLM models loaded on {device}") + + # Initialize HuggingFace model (for comparison) + print("Loading HuggingFace model...") + hf_tokenizer = AutoTokenizer.from_pretrained(model_name) + hf_model = AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype=torch.float32, + device_map="auto" if device.type == "cuda" else None, + ).eval() + print(f" [OK] HuggingFace model loaded on {device.type}") + + # Initialize LLM tokenizer + print("Loading LLM tokenizer...") + llm_tokenizer = load_llm_tokenizer(model_name) + + # Memory usage info + if device.type == "cuda": + print(f" Memory: GPU memory allocated: {torch.cuda.memory_allocated(device) / 1024**3:.2f} GB") + print(f" Memory: GPU memory cached: {torch.cuda.memory_reserved(device) / 1024**3:.2f} GB") + + total_params = sum(p.numel() for p in llm_causal_model.parameters()) + print(f" Info: Total parameters: {total_params:,}") + print("=" * 60) + + models = { + "config": config, + "llm_model": llm_model, + "llm_causal_model": llm_causal_model, + "hf_model": hf_model, + "llm_tokenizer": llm_tokenizer, + "hf_tokenizer": hf_tokenizer, + "device": device, + } + + return models + + +def test_qwen3_local_config_loading(): + """Test loading Qwen3 config from local JSON file and creating model.""" + + print("=" * 80) + print("TESTING QWEN3 LOCAL CONFIG LOADING") + print("=" * 80) + + try: + # Load config from local JSON file + config_path = "cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json" + config = Qwen3Config.from_json_file(config_path) + + # Verify config loaded correctly + assert config.model_type == "qwen3", f"Expected model_type 'qwen3', got '{config.model_type}'" + assert config.hidden_size == 1024, f"Expected hidden_size 1024, got {config.hidden_size}" + assert config.num_hidden_layers == 28, f"Expected 28 layers, got {config.num_hidden_layers}" + assert config.vocab_size == 151936, f"Expected vocab_size 151936, got {config.vocab_size}" + + print(" Config loaded and validated successfully!") + print(f" Model: {config.model_type}") + print(f" Hidden size: {config.hidden_size}") + print(f" Layers: {config.num_hidden_layers}") + print(f" Vocab size: {config.vocab_size}") + + # Test model creation + print("\n Creating models with local config...") + base_model = Qwen3Model(config=config) + causal_model = Qwen3ForCausalLM(config=config) + + assert base_model.config.hidden_size == 1024 + assert len(base_model.layers) == 28 + assert causal_model.config.hidden_size == 1024 + assert hasattr(causal_model, "lm_head") + + print(" Models created successfully with local config") + + # Test basic forward pass + print("\n Testing forward pass...") + batch_size, seq_len = 2, 10 + input_ids = torch.randint(0, config.vocab_size, (batch_size, seq_len)) + + with torch.no_grad(): + outputs = causal_model(input_ids) + logits = outputs.logits + + expected_shape = (batch_size, seq_len, config.vocab_size) + assert logits.shape == expected_shape, f"Expected shape {expected_shape}, got {logits.shape}" + + print(" Forward pass working with correct output dimensions") + print(" Local config loading test PASSED!") + + # Clean up + del base_model, causal_model, config + + return True + + except Exception as e: + print(f" Local config loading test FAILED: {e}") + import traceback + + traceback.print_exc() + return False + + +def cleanup_models(models): + """Clean up models and free GPU memory""" + if models["device"].type == "cuda": + print("Cleaning up GPU memory...") + del models["llm_model"] + del models["llm_causal_model"] + del models["hf_model"] + torch.cuda.empty_cache() + print(f"GPU memory after cleanup: {torch.cuda.memory_allocated(models['device']) / 1024**3:.2f} GB") + + +def llm_output_controls_check(models): + """Test MoT-style output control implementation in Qwen3""" + + print("=" * 80) + print("TESTING QWEN3 MoT-STYLE OUTPUT CONTROLS") + print("=" * 80) + + # Use pre-initialized models + config = models["config"] + model = models["llm_model"] + causal_model = models["llm_causal_model"] + device = models["device"] + + print(" Using pre-initialized Qwen3 components...") + print("[PASS] Import successful") + + # Override output defaults for testing + config.output_attentions = False + config.output_hidden_states = False + print(f"[PASS] Configuration ready (vocab_size={config.vocab_size}, hidden_size={config.hidden_size})") + + # Test 1: Forward method signatures + print("\nTEST 1: Forward Method Signatures") + print("-" * 50) + + # Check Qwen3Model signature + sig = inspect.signature(model.forward) + params = list(sig.parameters.keys()) + required_params = ["output_attentions", "output_hidden_states", "return_dict"] + + missing = [p for p in required_params if p not in params] + if not missing: + print("[PASS] Qwen3Model has all required output control parameters") + else: + print(f"[FAIL] Qwen3Model missing parameters: {missing}") + + # Check Qwen3ForCausalLM signature + sig_causal = inspect.signature(causal_model.forward) + params_causal = list(sig_causal.parameters.keys()) + + missing_causal = [p for p in required_params if p not in params_causal] + if not missing_causal: + print("[PASS] Qwen3ForCausalLM has all required output control parameters") + else: + print(f"[FAIL] Qwen3ForCausalLM missing parameters: {missing_causal}") + + # Test 2: Memory Efficiency + print("\nTEST 2: Memory Efficiency") + print("-" * 50) + + # Create dummy input (small sequence for testing) + dummy_input = torch.randint(0, min(config.vocab_size, 1000), (1, 8)).to(device) + + print("Testing with output_hidden_states=False, output_attentions=False...") + with torch.no_grad(): + outputs_minimal = model(dummy_input, output_hidden_states=False, output_attentions=False, return_dict=True) + + hidden_states_none = outputs_minimal.hidden_states is None + attentions_none = outputs_minimal.attentions is None + + print(f" hidden_states is None: {hidden_states_none}") + print(f" attentions is None: {attentions_none}") + + if hidden_states_none and attentions_none: + print("[PASS] Memory efficiency: Collections are None when not requested") + else: + print("[FAIL] Memory efficiency failed: Collections should be None") + + print("\nTesting with output_hidden_states=True, output_attentions=True...") + with torch.no_grad(): + outputs_full = model(dummy_input, output_hidden_states=True, output_attentions=True, return_dict=True) + + has_hidden_states = outputs_full.hidden_states is not None + has_attentions = outputs_full.attentions is not None + + print(f" hidden_states collected: {has_hidden_states}") + print(f" attentions collected: {has_attentions}") + + if has_hidden_states and has_attentions: + print(f" hidden_states length: {len(outputs_full.hidden_states)}") + print(f" attentions length: {len(outputs_full.attentions)}") + print("[PASS] Full collection: All intermediate outputs captured") + else: + print("[FAIL] Full collection failed: Missing requested outputs") + + # Test 3: Return Format Control + print("\nTEST 3: Return Format Control") + print("-" * 50) + + print("Testing return_dict=False (tuple format)...") + with torch.no_grad(): + tuple_outputs = model(dummy_input, output_hidden_states=True, output_attentions=True, return_dict=False) + + is_tuple = isinstance(tuple_outputs, tuple) + print(f" Returns tuple: {is_tuple}") + print(f" Tuple length: {len(tuple_outputs) if is_tuple else 'N/A'}") + + if is_tuple: + print("[PASS] Tuple format working correctly") + else: + print("[FAIL] Tuple format failed") + + print("\nTesting return_dict=True (dictionary format)...") + with torch.no_grad(): + dict_outputs = model(dummy_input, output_hidden_states=True, output_attentions=True, return_dict=True) + + has_last_hidden = hasattr(dict_outputs, "last_hidden_state") + has_hidden = hasattr(dict_outputs, "hidden_states") + has_attentions = hasattr(dict_outputs, "attentions") + + print(f" Has last_hidden_state: {has_last_hidden}") + print(f" Has hidden_states: {has_hidden}") + print(f" Has attentions: {has_attentions}") + + if has_last_hidden and has_hidden and has_attentions: + print("[PASS] Dictionary format working correctly") + else: + print("[FAIL] Dictionary format missing fields") + + # Test 4: CausalLM Integration + print("\nTEST 4: CausalLM Integration") + print("-" * 50) + + print("Testing Qwen3ForCausalLM output controls...") + with torch.no_grad(): + causal_outputs = causal_model(dummy_input, output_hidden_states=True, output_attentions=True, return_dict=True) + + has_logits = hasattr(causal_outputs, "logits") + has_hidden = causal_outputs.hidden_states is not None + has_attentions = causal_outputs.attentions is not None + + print(f" Has logits: {has_logits}") + print(f" Has hidden_states: {has_hidden}") + print(f" Has attentions: {has_attentions}") + + if has_logits and has_hidden and has_attentions: + print("[PASS] CausalLM output controls working correctly") + else: + print("[FAIL] CausalLM output controls failed") + + # Test 5: Configuration Defaults + print("\nTEST 5: Configuration Defaults") + print("-" * 50) + + # Test with config defaults + with torch.no_grad(): + # Config has output_attentions=False, output_hidden_states=False + default_outputs = model(dummy_input) # No explicit parameters + + hidden_default = default_outputs.hidden_states is None + attentions_default = default_outputs.attentions is None + + print(f" Default hidden_states is None: {hidden_default}") + print(f" Default attentions is None: {attentions_default}") + + if hidden_default and attentions_default: + print("[PASS] Configuration defaults respected") + else: + print("[FAIL] Configuration defaults not working") + + # Test 6: HuggingFace Comparison + print("\nTEST 6: HuggingFace Comparison") + print("-" * 50) + + print("Comparing our LLM implementation with official HuggingFace model...") + try: + comparison_passed = compare_with_huggingface_model(models) + + if comparison_passed: + print("[PASS] Our LLM vs HuggingFace comparison successful") + else: + print("[FAIL] Our LLM vs HuggingFace comparison had differences") + except Exception as e: + print(f"[FAIL] HuggingFace comparison failed: {e}") + comparison_passed = False + + # Final Summary + print("\n" + "=" * 80) + print("SUMMARY: Our LLM INTEGRATION TEST SUMMARY") + print("=" * 80) + print("[PASS] Forward method signatures complete") + print("[PASS] Memory-efficient collection implemented") + print("[PASS] Return format control working") + print("[PASS] CausalLM integration successful") + print("[PASS] Configuration defaults respected") + if comparison_passed: + print("[PASS] HuggingFace comparison successful") + else: + print("[FAIL] HuggingFace comparison failed") + + print("\n Qwen3 MoT-style output controls are working perfectly!") + print("Memory usage is optimized and user has full control over outputs.") + + return True + + +def check_llm_implementation(models): + """Check if our LLM-style implementation is working correctly""" + + print("\nTEST 1: CHECKING our LLM IMPLEMENTATION...") + print("-" * 50) + + # Use pre-initialized models + config = models["config"] + model = models["llm_model"] + device = models["device"] + + # First, check the actual transformers version and environment + print(f"Python: Python executable: {sys.executable}") + print(f"Python: Python version: {sys.version}") + + try: + import transformers + + print(f"Transformers version: {transformers.__version__}") + print(f"Transformers location: {transformers.__file__}") + except Exception as e: + print(f"[ERROR] Error importing transformers: {e}") + return ["transformers_import_error"] + + implementation_status = [] + print("\nCHECKING: CHECKING our LLM FIXES...") + + # Consolidated implementation check + try: + # Test layer_type_validation + layer_type_validation(["full_attention", "sliding_attention"]) + print("[OK] layer_type_validation function working") + implementation_status.append("layer_validation_ok") + + # Model already instantiated and loaded with pretrained weights + print(f"[OK] Model instantiation successful (vocab_size={config.vocab_size}) on {device}") + implementation_status.append("model_instantiation_ok") + + # Test forward pass with dummy input (smaller batch for memory efficiency) + dummy_input = torch.randint(0, min(config.vocab_size, 1000), (1, 8)).to(device) + with torch.no_grad(): + model(dummy_input) + + print("[OK] Forward pass with masking successful") + implementation_status.append("forward_pass_ok") + + # Custom masking functions are implicitly tested by forward pass + print("[OK] Custom masking functions working") + implementation_status.append("masking_functions_ok") + + except Exception as e: + print(f"[ERROR] LLM implementation check failed: {e}") + # Determine which specific check failed based on how far we got + if "layer_validation_ok" not in implementation_status: + implementation_status.append("layer_validation_failed") + if "model_instantiation_ok" not in implementation_status: + implementation_status.append("model_instantiation_failed") + if "forward_pass_ok" not in implementation_status: + implementation_status.append("forward_pass_failed") + if "masking_functions_ok" not in implementation_status: + implementation_status.append("masking_functions_failed") + + print( + f"\nStatus: Implementation Status: " + f"{len([s for s in implementation_status if s.endswith('_ok')])}/{len(implementation_status)} checks passed" + ) + + return implementation_status + + +def check_llm_output_controls(models): + """Check if MoT-style output controls are implemented""" + + print("\nCHECKING MoT-STYLE OUTPUT CONTROLS...") + print("-" * 50) + + # Use pre-initialized models + model = models["llm_model"] + causal_model = models["llm_causal_model"] + + # Check signatures + sig = inspect.signature(model.forward) + params = list(sig.parameters.keys()) + + sig_causal = inspect.signature(causal_model.forward) + params_causal = list(sig_causal.parameters.keys()) + + required_params = ["output_attentions", "output_hidden_states", "return_dict"] + missing = [p for p in required_params if p not in params] + missing_causal = [p for p in required_params if p not in params_causal] + + if missing or missing_causal: + print("[WARNING] Missing MoT output control parameters:") + if missing: + print(f" - Qwen3Model: {missing}") + if missing_causal: + print(f" - Qwen3ForCausalLM: {missing_causal}") + print("\nTesting: NOTE: MoT-style output controls are not yet implemented.") + print("This is the next step after compatibility fixes.") + return False + else: + print("[OK] All MoT output control parameters present") + return True + + +def run_input_output_test(models): + """Run a simple input/output test to verify the model works""" + print("TESTING BASIC INPUT/OUTPUT...") + print("-" * 40) + + # Use pre-initialized models + config = models["config"] + model = models["llm_model"] + causal_model = models["llm_causal_model"] + device = models["device"] + + print(f"Config ready: vocab_size={config.vocab_size}, hidden_size={config.hidden_size}") + + # Test Qwen3Model + print("Testing: Testing Qwen3Model...") + + # Simple input (use smaller batch for memory efficiency with large models) + batch_size, seq_len = 1, 8 + input_ids = torch.randint(0, min(config.vocab_size, 1000), (batch_size, seq_len)).to(device) + + # Test forward pass + with torch.no_grad(): + outputs = model(input_ids) + + print(f" [OK] Input shape: {input_ids.shape}") + print(f" [OK] Output shape: {outputs.last_hidden_state.shape}") + print(f" [OK] Expected: ({batch_size}, {seq_len}, {config.hidden_size})") + + # Test Qwen3ForCausalLM + print("Testing Qwen3ForCausalLM...") + + with torch.no_grad(): + causal_outputs = causal_model(input_ids) + + print(f" [OK] Logits shape: {causal_outputs.logits.shape}") + print(f" [OK] Expected: ({batch_size}, {seq_len}, {config.vocab_size})") + + # Test with attention mask + print("Testing: Testing with attention mask...") + attention_mask = torch.ones_like(input_ids).to(device) + attention_mask[:, -2:] = 0 # Mask last 2 tokens + + with torch.no_grad(): + masked_outputs = causal_model(input_ids, attention_mask=attention_mask) + + print(f" [OK] Masked logits shape: {masked_outputs.logits.shape}") + + # Test generation-like scenario + print("Testing: Testing generation-like scenario...") + with torch.no_grad(): + # Simulate generating one token + next_token_logits = causal_outputs.logits[:, -1, :] # Last position + next_token_probs = torch.softmax(next_token_logits, dim=-1) + next_token = torch.argmax(next_token_probs, dim=-1) + + print(f" [OK] Next token shape: {next_token.shape}") + print(f" [OK] Next tokens: {next_token.tolist()}") + + # HuggingFace comparison is now handled in Test 6 of MoT output controls + + print("[OK] INPUT/OUTPUT TEST PASSED!") + return True + + +def compare_with_huggingface_model(models): + """Compare our LLM Qwen3 implementation with official HuggingFace model""" + print(" Comparing our LLM vs HuggingFace implementations...") + + # Use pre-initialized models + hf_model = models["hf_model"] + llm_model = models["llm_causal_model"] + hf_tokenizer = models["hf_tokenizer"] + llm_tokenizer = models["llm_tokenizer"] + device = models["device"] + + # Verify we're using our LLM implementation + print(f" Our LLM model class: {llm_model.__class__}") + print(f" Our LLM model module: {llm_model.__class__.__module__}") + print(f" HF model class: {hf_model.__class__}") + print(f" HF model module: {hf_model.__class__.__module__}") + + # Check if our custom masking functions are present (our LLM-specific) + llm_module = sys.modules.get("qwen3.modeling_qwen3") + if llm_module and hasattr(llm_module, "create_causal_mask"): + print(" [OK] Our LLM-specific masking functions detected in module") + else: + print(" [WARNING] Our LLM-specific functions not found - may be using HF implementation") + + # Verify module paths + if "qwen3.qwen3" in str(llm_model.__class__.__module__): + print(" [OK] Using our LLM implementation (qwen3.qwen3)") + else: + print(" [WARNING] Not using our LLM implementation!") + + # Models and tokenizers already loaded + + # Prepare test input as specified by user + prompt = '"Give me a short introduction to large language model."' + messages = [{"role": "user", "content": prompt}] + + # Apply chat template (using HF tokenizer for consistency) + text = hf_tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=True, # Switches between thinking and non-thinking modes + ) + + print(f" Chat template applied: {text[:100]}...") + + # Tokenize input and move to device + hf_inputs = hf_tokenizer([text], return_tensors="pt") + llm_inputs = llm_tokenizer([text], return_tensors="pt") + + # Move inputs to device + hf_inputs = {k: v.to(device) for k, v in hf_inputs.items()} + llm_inputs = {k: v.to(device) for k, v in llm_inputs.items()} + + print(f" HF input length: {hf_inputs['input_ids'].shape[1]} tokens") + print(f" Our LLM input length: {llm_inputs['input_ids'].shape[1]} tokens") + + # Compare tokenization + if torch.equal(hf_inputs["input_ids"], llm_inputs["input_ids"]): + print(" [OK] Tokenization identical") + tokenization_matches = True + else: + print(" [WARNING] Tokenization differs between HF and our LLM tokenizers") + print(f" HF tokens: {hf_inputs['input_ids'].tolist()}") + print(f" Our LLM tokens: {llm_inputs['input_ids'].tolist()}") + # Continue with comparison using respective tokenizations + tokenization_matches = False + + # Test forward pass comparison + print(" Comparing forward pass outputs...") + with torch.no_grad(): + hf_outputs = hf_model(**hf_inputs) + llm_outputs = llm_model(**llm_inputs) + + # Compare logits only if tokenization matches + if tokenization_matches: + # Compare logits + logits_close = torch.allclose(hf_outputs.logits, llm_outputs.logits, atol=1e-4, rtol=1e-3) + max_diff = torch.max(torch.abs(hf_outputs.logits - llm_outputs.logits)).item() + + print(f" Logits close (atol=1e-4, rtol=1e-3): {logits_close}") + print(f" Max logits difference: {max_diff:.6f}") + + if not logits_close: + print(" [WARNING] Logits differ significantly") + return False + else: + print(" [SKIP] Logits comparison skipped due to different tokenization") + print(" This is expected if our LLM and HF tokenizers produce different tokens") + + # Test generation comparison (shorter version due to computational cost) + print(" Comparing generation (max 50 tokens)...") + with torch.no_grad(): + # HF generation + hf_generated = hf_model.generate( + **hf_inputs, + max_new_tokens=50, + do_sample=False, # Deterministic + temperature=None, # Clear conflicting params + top_p=None, + top_k=None, + pad_token_id=hf_tokenizer.eos_token_id, + ) + + # Our LLM generation + llm_generated = llm_model.generate( + **llm_inputs, + max_new_tokens=50, + do_sample=False, # Deterministic + temperature=None, # Clear conflicting params + top_p=None, + top_k=None, + pad_token_id=llm_tokenizer.eos_token_id, + ) + + # Extract new tokens only + hf_new_tokens = hf_generated[0][len(hf_inputs["input_ids"][0]) :].tolist() + our_llm_new_tokens = llm_generated[0][len(llm_inputs["input_ids"][0]) :].tolist() + + print(f" HF generated {len(hf_new_tokens)} tokens") + print(f" Our LLM generated {len(our_llm_new_tokens)} tokens") + + # Decode and display the generated text + hf_generated_text = hf_tokenizer.decode(hf_new_tokens, skip_special_tokens=True) + our_llm_generated_text = llm_tokenizer.decode(our_llm_new_tokens, skip_special_tokens=True) + + print(f" HF generated text: '{hf_generated_text}'") + print(f" Our LLM generated text: '{our_llm_generated_text}'") + + # Also show the full conversation (prompt + response) + hf_full_text = hf_tokenizer.decode(hf_generated[0], skip_special_tokens=True) + our_llm_full_text = llm_tokenizer.decode(llm_generated[0], skip_special_tokens=True) + + print(f" HF full conversation:\n{hf_full_text}") + print(f" Our LLM full conversation:\n{our_llm_full_text}") + + # Compare first few tokens + min_len = min(len(hf_new_tokens), len(our_llm_new_tokens), 10) + first_tokens_match = hf_new_tokens[:min_len] == our_llm_new_tokens[:min_len] + + print(f" First {min_len} tokens match: {first_tokens_match}") + + if tokenization_matches: + if first_tokens_match: + print(" [OK] Generation outputs are consistent") + else: + print(" [WARNING] Generation outputs differ") + print(f" HF first tokens: {hf_new_tokens[:min_len]}") + print(f" Our LLM first tokens: {our_llm_new_tokens[:min_len]}") + else: + print(" [INFO] Generation comparison with different input tokenizations") + print(f" HF generated: {hf_new_tokens[:min_len]}") + print(f" Our LLM generated: {our_llm_new_tokens[:min_len]}") + print(" Different outputs expected due to different input tokens") + + # Summary + if tokenization_matches: + print(" [OK] Complete comparison successful - identical tokenization and behavior") + else: + print(" [INFO] Partial comparison successful - Our LLM tokenizer differs but works correctly") + + return True + + +def run_pretrained_weights_test(models): + """Test using actual pretrained weights (already loaded)""" + print("TESTING WITH PRETRAINED WEIGHTS...") + print("-" * 50) + + # Use pre-initialized models with pretrained weights + config = models["config"] + model = models["llm_causal_model"] + tokenizer = models["llm_tokenizer"] + device = models["device"] + + print("Using pre-loaded model with pretrained weights") + print(f" [OK] Vocab size: {config.vocab_size}") + print(f" [OK] Hidden size: {config.hidden_size}") + print(f" [OK] Num layers: {config.num_hidden_layers}") + print(f" [OK] Num heads: {config.num_attention_heads}") + + # Test with a simple prompt + print("Testing: Testing text generation...") + prompt = "The quick brown fox" + print(f" Loading: Input: '{prompt}'") + + # Tokenize input and move to device + inputs = tokenizer(prompt, return_tensors="pt") + input_ids = inputs.input_ids.to(device) + attention_mask = inputs.attention_mask.to(device) + + print(f" Token IDs: Token IDs: {input_ids.tolist()}") + print(f" Length: Input length: {input_ids.shape[1]} tokens") + + # Generate with our LLM implementation + with torch.no_grad(): + # Test basic forward pass first + outputs = model(input_ids, attention_mask=attention_mask) + + print(f" [OK] Logits shape: {outputs.logits.shape}") + + # Get next token probabilities + next_token_logits = outputs.logits[0, -1, :] + next_token_probs = torch.softmax(next_token_logits, dim=-1) + top_tokens = torch.topk(next_token_probs, 5) + + print(" Top 5 next token predictions:") + for i, (prob, token_id) in enumerate(zip(top_tokens.values, top_tokens.indices, strict=False)): + token = tokenizer.decode([token_id]) + print(f" {i + 1}. '{token}' (prob: {prob:.4f})") + + # Test generation + print(" Testing generation...") + with torch.no_grad(): + generated = model.generate( + input_ids, + attention_mask=attention_mask, + max_new_tokens=10, + do_sample=False, # Use greedy decoding for reproducibility + temperature=None, # Clear conflicting params + top_p=None, + top_k=None, + pad_token_id=tokenizer.eos_token_id, + ) + + generated_text = tokenizer.decode(generated[0], skip_special_tokens=True) + print(f" [OK] Generated: '{generated_text}'") + + # Test memory usage info + if device.type == "cuda": + print(f" Status: GPU memory allocated: {torch.cuda.memory_allocated(device) / 1024**3:.2f} GB") + print(f" Status: GPU memory cached: {torch.cuda.memory_reserved(device) / 1024**3:.2f} GB") + + total_params = sum(p.numel() for p in model.parameters()) + print(f" Status: Total parameters: {total_params:,}") + + print("[OK] PRETRAINED WEIGHTS TEST PASSED!") + return True + + +@pytest.mark.L1 +def test_qwen3_llm_implementation(): + print("Qwen3 LLM Integration Test Suite") + print("This script tests our LLM implementation in Qwen3") + print(f"Running from: {os.getcwd()}") + print() + + model_name = "Qwen/Qwen3-0.6B" + + print(f" Testing Model: {model_name}") + print(" Running all tests: compatibility, I/O, HuggingFace comparison, and pretrained weights") + + # Show device info early + device = get_device() + print(f" Device: {device}") + print() + + is_large_model = "4B" in model_name + + try: + # Initialize all models and tokenizers once + models = initialize_models_and_tokenizers(model_name, device, is_large_model=is_large_model) + + if is_large_model: + print(" Info: Large model detected, skipping input/output test") + print(" Info: Large model detected, skipping pretrained weights test") + print(" Info: Large model detected, skipping output controls test") + print(" Info: Large model detected, skipping comprehensive output control tests") + huggingface_passed = compare_with_huggingface_model(models) + if not huggingface_passed: + print("\n[ERROR] HUGGINGFACE COMPARISON FAILED!") + raise Exception("There may be differences between our LLM and HuggingFace implementations.") + else: + print("\n[OK] HUGGINGFACE COMPARISON PASSED!") + + # Phase 0.5: Local config loading test + print("\n" + "=" * 50) + print("TESTING LOCAL CONFIG LOADING") + local_config_success = test_qwen3_local_config_loading() + + if not local_config_success: + print("\n[ERROR] LOCAL CONFIG LOADING FAILED!") + print("Please check the config file and fix any issues.") + cleanup_models(models) + raise Exception("Local config loading test failed.") + + print("\n[OK] LOCAL CONFIG LOADING PASSED!") + + # Phase 1: Check our LLM implementation (compatibility fixes) + implementation_status = check_llm_implementation(models) + + implementation_ok = all(status.endswith("_ok") for status in implementation_status) + + if not implementation_ok: + raise Exception("\n[ERROR] Our LLM implementation has issues!") + + print("\n[OK] Our LLM implementation is working!") + + # Phase 1.5: Run input/output test + print("\n" + "=" * 50) + io_test_passed = run_input_output_test(models) + + if not io_test_passed: + raise Exception("\n[ERROR] INPUT/OUTPUT TEST FAILED!") + + # Phase 1.6: Pretrained weights test + print("\n" + "=" * 50) + print(f"TESTING WITH PRETRAINED WEIGHTS: {model_name}") + print("Note: Pretrained weights are already loaded in the initialized models") + + pretrained_passed = run_pretrained_weights_test(models) + if not pretrained_passed: + print("\n[ERROR] PRETRAINED WEIGHTS TEST FAILED!") + print("There may be compatibility issues with the pretrained model.") + print("However, this doesn't block the LLM integration.") + else: + print("\nSUMMARY: PRETRAINED WEIGHTS TEST PASSED!") + print(f"Our LLM implementation is compatible with official {model_name} weights!") + + # Phase 2: Check if output controls are implemented + output_controls_ok = check_llm_output_controls(models) + + if not output_controls_ok: + print("\nTesting: MoT-style output controls not yet implemented.") + print("This is the next step in the MoT integration process.") + print("\nCurrent Status:") + print("[OK] Compatibility fixes completed") + print("[OK] Masking functions implemented") + print("[OK] Model instantiation working") + print("[PENDING] Output controls (next step)") + + # Phase 3: Run comprehensive output control tests (if implemented) + print("\n Running comprehensive MoT-style LLM output control tests...") + success = llm_output_controls_check(models) + + if success: + print("\nALL TESTS PASSED! ALL TESTS PASSED!") + print(f"Qwen3 successfully implements full MoT-style LLM integration for {model_name}!") + + # Cleanup + cleanup_models(models) + else: + raise Exception("\nOUTPUT CONTROL TESTS FAILED! OUTPUT CONTROL TESTS FAILED!") + + except KeyboardInterrupt: + print("\n\n[INTERRUPTED] Test suite interrupted by user") + # Try to cleanup if models were initialized + if "models" in locals(): + cleanup_models(models) + raise Exception("Test suite interrupted by user.") + except Exception as e: + print(f"\n[FATAL ERROR] Unexpected error during test execution: {e}") + traceback.print_exc() + # Try to cleanup if models were initialized + if "models" in locals(): + cleanup_models(models) + raise Exception(f"Unexpected error during test execution: {e}") + + +if __name__ == "__main__": + test_qwen3_llm_implementation() diff --git a/cosmos3/_src/vfm/models/mot/__init__.py b/cosmos3/_src/vfm/models/mot/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/mot/attention.py b/cosmos3/_src/vfm/models/mot/attention.py new file mode 100644 index 00000000..1bf2278b --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/attention.py @@ -0,0 +1,445 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention + +from cosmos3._src.imaginaire.attention import ( + attention, + merge_attentions, + multi_dimensional_attention_varlen, +) +from cosmos3._src.imaginaire.attention.masks import CausalType +from cosmos3._src.vfm.models.utils.memory import KVToStore, MemoryValue + +flex_attention = torch.compile(flex_attention) + + +class SplitInfo: + def __init__( + self, + split_lens: list[int], + attn_modes: list[str], + sample_lens: list[int], + actual_len: int, + is_three_way: bool = False, + vision_token_shapes: list[tuple[int, int, int]] | None = None, + action_token_shapes: list[tuple[int, ...]] | None = None, + num_action_tokens_per_supertoken: int = 0, + null_action_supertokens: bool = False, + ): + """ + Actual len is the actual non-padded length of the packed sequence. + It's used to trim split_lens, attn_modes and sample_lens, which were + originally padded to max sequence length (likely for flex attention). + """ + assert sum(sample_lens) == sum(split_lens), ( + f"Sum of new sample lens {sum(sample_lens)} is not equal to sum of new split lens {sum(split_lens)}" + ) + + max_causal_len = 0 + max_full_len = 0 + for split_len, attn_mode in zip(split_lens, attn_modes): + if attn_mode == "causal": + max_causal_len = max(max_causal_len, split_len) + elif attn_mode == "full": + max_full_len = max(max_full_len, split_len) + + self.max_causal_len = max_causal_len + self.max_full_len = max_full_len + self.max_sample_len = max(sample_lens) + + self.split_lens = split_lens + self.attn_modes = attn_modes + self.sample_lens = sample_lens + + self.is_three_way = is_three_way + self.vision_token_shapes = vision_token_shapes + self.action_token_shapes = action_token_shapes + self.num_action_tokens_per_supertoken = num_action_tokens_per_supertoken + self.null_action_supertokens = null_action_supertokens + + +AttentionMaskType = BlockMask | SplitInfo + + +_dotproduct_attention_cache = {} + + +from cosmos3._src.vfm.datasets.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, + create_sparse_mask, + factored_from_joint_sequence, + from_joint, + from_mode_splits, + generate_natten_metadata, + generate_temporal_causal_natten_metadata, + get_all_seq, + get_causal_seq, + get_full_only_seq, + joint_from_joint_sequence, +) + + +def two_way_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, +): + """ + Performs two-way attention with causal and full attention. + """ + + causal_q, causal_q_offsets = get_causal_seq(packed_query_states) + causal_k, causal_k_offsets = get_causal_seq(packed_key_states) + causal_v, _ = get_causal_seq(packed_value_states) + full_q, full_q_offsets = get_full_only_seq(packed_query_states) + + sample_offsets = packed_query_states["sample_offsets"] + + use_dont_care_mask = causal_q_offsets is causal_k_offsets + + + causal_res = attention( + causal_q.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=causal_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_causal_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + is_causal=True, + causal_type=CausalType.DontCare if use_dont_care_mask else CausalType.TopLeft, + ) # [1,N_und,heads,head_dim] + + # [1,N_und,heads,head_dim] -> [N_und,heads,head_dim] -> [N_und,heads*head_dim] + causal_out = causal_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_und,heads*head_dim] + + full_res = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + get_all_seq(packed_key_states).unsqueeze(0), # [1,N_all,heads,head_dim] + get_all_seq(packed_value_states).unsqueeze(0), # [1,N_all,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=sample_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_sample_len"], + ) # [1,N_full,heads,head_dim] + + # [1,N_full,heads,head_dim] -> [N_full,heads,head_dim] -> [N_full,heads*head_dim] + full_out = full_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_full,heads*head_dim] + + out_all = from_mode_splits(causal_out, full_out, packed_query_states) + return out_all + + +def three_way_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + natten_metadata: dict | None, + attention_meta: SplitInfo | None = None, +): + """ + Performs three-way attention, with understanding and generations attentions fully decomposed, + and allows sparsity / multi-dimensional masking in the generation tower. + + When attention_meta is provided with null_action_supertokens=True, zeros V for the first + num_action_tokens_per_supertoken tokens of each sample's GEN sequence (null action + supertokens for temporal causal training). The metadata encodes is_causal=(True, False): + causal across T supertokens, full within each supertoken S. + + NOTE: the three-way decomposition is only done so we can handle sparsity in the gen tower, + but a KEY assumption is that the "full" tokens all correspond to the same modality! + We should be careful when extending this to beyond t2i and t2v. + """ + + causal_q, causal_q_offsets = get_causal_seq(packed_query_states) + causal_k, causal_k_offsets = get_causal_seq(packed_key_states) + causal_v, _ = get_causal_seq(packed_value_states) + full_q, full_q_offsets = get_full_only_seq(packed_query_states) + full_k, full_k_offsets = get_full_only_seq(packed_key_states) + full_v, _ = get_full_only_seq(packed_value_states) + + sample_offsets = packed_query_states["sample_offsets"] + + if attention_meta is not None and attention_meta.null_action_supertokens: + # Zero V for the first num_action_tokens_per_supertoken tokens of each + # sample's GEN sequence (null action supertokens at t=0). + # out_i = Σ_j softmax(QKᵀ/√d)_j · V_j — terms with V_j=0 contribute exactly 0 to the output, + # regardless of attention weights. Softmax mass is still allocated to these positions (not + # redistributed), so this differs from hard key masking, but the output contribution is 0. + full_v = full_v.clone() + starts = full_q_offsets[:-1].long() # [B] + null_positions = ( + starts.unsqueeze(1) + torch.arange(attention_meta.num_action_tokens_per_supertoken, device=starts.device) + ).reshape(-1) + full_v[null_positions] = 0 + + use_dont_care_mask = causal_q_offsets is causal_k_offsets + + + causal_res = attention( + causal_q.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=causal_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_causal_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + is_causal=True, + causal_type=CausalType.DontCare if use_dont_care_mask else CausalType.TopLeft, + ) # [1,N_und,heads,head_dim] + # [1,N_und,heads,head_dim] -> [N_und,heads,head_dim] -> [N_und,heads*head_dim] + causal_out = causal_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_und,heads*head_dim] + + # If there's no metadata, it's a dense layer + if natten_metadata is None: + full_sa, full_sa_lse = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + full_k.unsqueeze(0), # [1,N_full,heads,head_dim] + full_v.unsqueeze(0), # [1,N_full,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=full_k_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_full_len"], + return_lse=True, + ) # full_sa: [1,N_full,heads,head_dim], full_sa_lse: [1,N_full,heads] + else: + assert natten_metadata is not None + full_sa, full_sa_lse = multi_dimensional_attention_varlen( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + full_k.unsqueeze(0), # [1,N_full,heads,head_dim] + full_v.unsqueeze(0), # [1,N_full,heads,head_dim] + metadata=natten_metadata, + return_lse=True, + ) # full_sa: [1,N_full,heads,head_dim], full_sa_lse: [1,N_full,heads] + + full_ca, full_ca_lse = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + return_lse=True, + ) # full_ca: [1,N_full,heads,head_dim], full_ca_lse: [1,N_full,heads] + + assert full_sa.shape == full_ca.shape + full_res, _ = merge_attentions( + outputs=[full_sa, full_ca], lse_tensors=[full_sa_lse, full_ca_lse], torch_compile=False + ) # [1,N_full,heads,head_dim] + + # [1,N_full,heads,head_dim] -> [N_full,heads,head_dim] -> [N_full,heads*head_dim] + full_out = full_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_full,heads*head_dim] + + out_all = from_mode_splits(causal_out, full_out, packed_query_states) + return out_all + + +def pad_sequence(tensor, pad_size): + """ + Pad a tensor along the second-to-last dimension. + + Args: + tensor: Input tensor to pad + pad_size: Number of padding elements to add + + Returns: + Padded tensor with zeros added along dim=-2 + """ + if pad_size <= 0: + return tensor + pad_shape = list(tensor.shape) + pad_shape[-2] = pad_size + padding = torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device) + return torch.cat([tensor, padding], dim=-2) # [...,S+pad_size,...] + + +def block_flex_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask, + block_size: int | None = None, +): + packed_queries = get_all_seq(packed_query_states) # [N,heads,head_dim] + packed_keys = get_all_seq(packed_key_states) # [N,heads,head_dim] + packed_values = get_all_seq(packed_value_states) # [N,heads,head_dim] + max_num_tokens = packed_query_states["max_num_tokens"] + + num_attention_heads = packed_queries.shape[1] + head_dim = packed_queries.shape[2] + + # Handle block mask attention with flex_attention + pad_size = max_num_tokens - packed_queries.shape[0] + packed_queries_padded = pad_sequence(packed_queries.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + packed_keys_padded = pad_sequence(packed_keys.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + packed_values_padded = pad_sequence(packed_values.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + + packed_attn_output = flex_attention( + packed_queries_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + packed_keys_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + packed_values_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + enable_gqa=True, + block_mask=attention_mask, + ) # [1,heads,max_num_tokens,head_dim] + assert isinstance(packed_attn_output, torch.Tensor) + + end_index = packed_attn_output.shape[2] - pad_size + packed_attn_output = packed_attn_output[0, :, :end_index, :] # [heads,N,head_dim] + packed_attn_output = packed_attn_output.transpose(0, 1).reshape( + -1, num_attention_heads * head_dim + ) # [N,heads*head_dim] + + return from_joint(packed_attn_output, packed_query_states) + + +def dispatch_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask | SplitInfo, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, +) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + assert memory_value is None, "Base dispatch_attention does not handle MemoryValue" + if isinstance(attention_mask, SplitInfo) and attention_mask.is_three_way: + output = three_way_attention( + packed_query_states, + packed_key_states, + packed_value_states, + natten_metadata=natten_metadata, + attention_meta=attention_mask, + ) + elif isinstance(attention_mask, SplitInfo): + output = two_way_attention(packed_query_states, packed_key_states, packed_value_states) + else: + output = block_flex_attention(packed_query_states, packed_key_states, packed_value_states, attention_mask) + return output, None + + +def build_packed_sequence( + joint_attn_implementation: str, + *, + packed_sequence: torch.Tensor, + attn_modes: list[str], + split_lens: list[int], + sample_lens: list[int], + packed_und_token_indexes: torch.LongTensor, + packed_gen_token_indexes: torch.LongTensor, + num_heads: int, + head_dim: int, + num_layers: int, + token_shapes: list[tuple[int, int, int]] | None = None, + natten_parameter_list: list | None = None, + block_size: int = 128, + is_image_batch: bool = False, + cp_world_size: int = 1, + video_temporal_causal: bool = False, + use_rolling_kv_cache: bool = False, + vision_token_shapes: list[tuple[int, int, int]] | None = None, + action_token_shapes: list[tuple[int, ...]] | None = None, + num_action_tokens_per_supertoken: int = 0, + null_action_supertokens: bool = False, + pad_for_cuda_graphs: bool = False, +) -> tuple[FactoredSequencePack | JointSequencePack, AttentionMaskType, list | None]: + """ + Build the model input pack and attention meta for joint attention. + Returns a tuple: (input_pack, attention_meta). + """ + device = packed_sequence.device + natten_metadata_list = None + if joint_attn_implementation == "flex": + sparse_mask = create_sparse_mask(sample_lens, split_lens, attn_modes, device) + seqlen = sum(sample_lens) + attention_meta = create_block_mask( + sparse_mask, + B=1, + H=num_heads, + Q_LEN=seqlen, + KV_LEN=seqlen, + device=device, + BLOCK_SIZE=block_size, + _compile=True, + ) + make_pack = joint_from_joint_sequence + elif joint_attn_implementation == "two_way": + attention_meta = SplitInfo( + split_lens=split_lens, + attn_modes=attn_modes, + sample_lens=sample_lens, + actual_len=int(packed_sequence.shape[0]), + ) + make_pack = factored_from_joint_sequence + elif joint_attn_implementation == "three_way": + attention_meta = SplitInfo( + split_lens=split_lens, + attn_modes=attn_modes, + sample_lens=sample_lens, + actual_len=int(packed_sequence.shape[0]), + is_three_way=True, + vision_token_shapes=vision_token_shapes, + action_token_shapes=action_token_shapes, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + null_action_supertokens=null_action_supertokens, + ) + make_pack = factored_from_joint_sequence + # The rolling KV-cache path implements temporal causality in + # three_way_attention_with_kv_cache; skip NATTEN metadata. + if not use_rolling_kv_cache: + # Temporal causal: encode (T, S) supertoken layout; spatial NATTEN: encode (H, W) layout. + if video_temporal_causal: + natten_metadata_list = generate_temporal_causal_natten_metadata( + vision_token_shapes=vision_token_shapes, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + num_layers=num_layers, + head_dim=head_dim, + device=device, + dtype=packed_sequence.dtype, + requires_grad=packed_sequence.requires_grad, + ) + else: + natten_metadata_list = generate_natten_metadata( + token_shapes=token_shapes, + head_dim=head_dim, + num_layers=num_layers, + device=device, + dtype=packed_sequence.dtype, + requires_grad=packed_sequence.requires_grad, + natten_parameter_list=natten_parameter_list, + ) + else: + raise ValueError( + f"Invalid joint_attn_implementation: {joint_attn_implementation}. " + "Must be 'two_way', 'three_way', or 'flex'." + ) + + input_pack = make_pack( + packed_sequence=packed_sequence, + attn_modes=attn_modes, + split_lens=split_lens, + sample_lens=sample_lens, + packed_und_token_indexes=packed_und_token_indexes.to(device), + packed_gen_token_indexes=packed_gen_token_indexes.to(device), + is_image_batch=is_image_batch, + cp_world_size=cp_world_size, + pad_for_cuda_graphs=pad_for_cuda_graphs, + ) + # Not needed anymore, can cause recompilations. + input_pack.pop("split_lens", None) + input_pack.pop("attn_modes", None) + return input_pack, attention_meta, natten_metadata_list diff --git a/cosmos3/_src/vfm/models/mot/context_parallel_utils.py b/cosmos3/_src/vfm/models/mot/context_parallel_utils.py new file mode 100644 index 00000000..677d4f7a --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/context_parallel_utils.py @@ -0,0 +1,427 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Context Parallelism Utilities. + +Integration Guide: +------------------ +1. Shard the Input Sequence: + Call `get_context_parallel_sharded_sequence` at the start of the forward pass to split + the global input pack into local shards. + + ```python + input_pack, position_ids = get_context_parallel_sharded_sequence( + attn_implementation, input_pack, position_ids, parallel_dims + ) + ``` + +2. Apply Context Parallel Attention: + Use `context_parallel_attention` inside your attention block. It handles All-to-All + communication (gather seq, scatter heads -> attn -> gather heads, scatter seq). + + ```python + output, kv_to_store = context_parallel_attention( + cp_mesh, query_pack, key_pack, value_pack, mask, local_attn_func + ) + ``` + +3. Gather Final Hidden States (Optional): + Use `get_context_parallel_last_hidden_state` if the full global sequence is needed for + loss or post-processing. +""" + +from typing import Callable + +import torch +import torch.distributed as dist +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor, Replicate, Shard +from torch.nn.attention.flex_attention import BlockMask + +from cosmos3._src.vfm.datasets.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, + from_mode_splits, + get_all_seq, + get_causal_seq, + get_full_only_seq, + get_gen_position_ids, + get_gen_seq, + get_und_position_ids, + get_und_seq, +) +from cosmos3._src.vfm.models.mot.attention import SplitInfo +from cosmos3._src.vfm.models.utils.memory import KVToStore, MemoryValue +from cosmos3._src.vfm.utils.parallelism import ParallelDims + + +def _pad_to_N(N, x: torch.Tensor) -> torch.Tensor: + assert x.shape[0] <= N + padded = x.new_zeros((N, *x.shape[1:])) + padded[: x.shape[0]] = x + return padded + + +def _filter_and_rebase_sparse_index( + global_indices: torch.Tensor, + start_offset: int, + end_offset: int, +) -> torch.Tensor: + """Filters sparse global indices to the local physical slice and shifts them to local 0-based coordinates.""" + + # Keep only global indices that fall within [start_offset, end_offset) + mask = (global_indices >= start_offset) & (global_indices < end_offset) + local_global_indices = global_indices[mask] + + # Subtract the start_offset to make them local (0-based) + local_rebased_indices = local_global_indices - start_offset + + return local_rebased_indices + + +def get_context_parallel_sharded_sequence( + attn_implementation: str, + input_pack: FactoredSequencePack, + position_ids: torch.Tensor, + parallel_dims: ParallelDims | None, +) -> tuple[FactoredSequencePack, torch.Tensor]: + """ + Splits the full input_pack into a local shard for Context Parallelism. + """ + if parallel_dims is None or not parallel_dims.cp_enabled: + return input_pack, position_ids + + assert attn_implementation in ("two_way", "three_way"), ( + f"Context parallel is only supported for two_way and three_way joint attention modes, " + f"got {attn_implementation!r}" + ) + cp_mesh = parallel_dims.cp_mesh + cp_group = cp_mesh.get_group() + rank = dist.get_rank(cp_group) + world_size = dist.get_world_size(cp_group) + + text_seq = get_und_seq(input_pack) + gen_seq = get_gen_seq(input_pack) + assert text_seq.shape[0] % world_size == 0, "text_seq.shape[0] must be divisible by world_size" + assert gen_seq.shape[0] % world_size == 0, "gen_seq.shape[0] must be divisible by world_size" + + text_len = text_seq.shape[0] + text_shard_len = text_len // world_size + text_shard = text_seq.narrow(0, rank * text_shard_len, text_shard_len) + + gen_len = gen_seq.shape[0] + gen_shard_len = gen_len // world_size + gen_shard = gen_seq.narrow(0, rank * gen_shard_len, gen_shard_len) + + text_position_ids = get_und_position_ids(position_ids, input_pack) + gen_position_ids = get_gen_position_ids(position_ids, input_pack) + + # Handle 3D mRoPE position IDs: shape (3, L) + is_mrope = position_ids.dim() == 2 and position_ids.shape[0] == 3 + if is_mrope: + text_position_ids = text_position_ids.transpose(0, 1) # [text_len,3] + gen_position_ids = gen_position_ids.transpose(0, 1) # [gen_len,3] + + # pad to N + text_position_ids = _pad_to_N(text_seq.shape[0], text_position_ids) + gen_position_ids = _pad_to_N(gen_seq.shape[0], gen_position_ids) + + text_position_ids_shard = text_position_ids.narrow(0, rank * text_shard_len, text_shard_len) + gen_position_ids_shard = gen_position_ids.narrow(0, rank * gen_shard_len, gen_shard_len) + + # create local pack + local_pack = from_mode_splits(text_shard, gen_shard, input_pack, is_sharded=True) + local_position_ids = torch.cat( + [text_position_ids_shard, gen_position_ids_shard], dim=0 + ) # [text_shard_len+gen_shard_len] or [text_shard_len+gen_shard_len,3] + + if is_mrope: + local_position_ids = local_position_ids.transpose(0, 1) # [3,text_shard_len+gen_shard_len] + + return local_pack, local_position_ids + + +def get_context_parallel_last_hidden_state( + packed_outputs: FactoredSequencePack, + parallel_dims: ParallelDims | None, +) -> torch.Tensor: + if parallel_dims is None or not parallel_dims.cp_enabled: + return get_all_seq(packed_outputs) + + # since unpatchify assumes full images, for now using all_gather to gather the predictions from all context parallel ranks + # This step can be removed once we make unpatchify work with context parallel local sequences + und_hidden_seq = get_und_seq(packed_outputs) # [text_shard_len,hidden_size] + gen_hidden_seq = get_gen_seq(packed_outputs) # [gen_shard_len,hidden_size] + + gathered_und_seq = all_gather_tensor( + und_hidden_seq, gather_dim=0, cp_mesh=parallel_dims.cp_mesh + ) # [text_len,hidden_size] + gathered_gen_seq = all_gather_tensor( + gen_hidden_seq, gather_dim=0, cp_mesh=parallel_dims.cp_mesh + ) # [gen_len,hidden_size] + + gathered_hidden_pack = from_mode_splits(gathered_und_seq, gathered_gen_seq, packed_outputs, is_sharded=False) + last_hidden_state = get_all_seq(gathered_hidden_pack) + return last_hidden_state + + +def context_parallel_broadcast( + data: torch.Tensor | dict[str, torch.Tensor], parallel_dims: ParallelDims, iteration: int +) -> torch.Tensor | dict[str, torch.Tensor]: + """ + Broadcasts a tensor or a dictionary of tensors to all ranks in the context parallel group. + """ + rank = parallel_dims.cp_rank + cp_world_size = parallel_dims.cp_mesh.size() + cp_data_batch_owner = iteration % cp_world_size + + broadcast_list = [None] + if rank == cp_data_batch_owner: + broadcast_list = [data] + + global_src_rank = dist.get_global_rank(parallel_dims.cp_mesh.get_group(), cp_data_batch_owner) + dist.broadcast_object_list(broadcast_list, src=global_src_rank, group=parallel_dims.cp_mesh.get_group()) + local_data = broadcast_list[0] + assert local_data is not None + return local_data + + +def all_to_all_tensor( + local_input: torch.Tensor, + scatter_dim: int, + gather_dim: int, + cp_mesh: "DeviceMesh", +) -> torch.Tensor: + """ + All-to-all via DTensor redistribute. + Input placement: Shard(gather_dim) -> The dimension we are about to gather was split. + Output placement: Shard(scatter_dim) -> The dimension we are about to scatter will be split. + """ + # Wrap local tensor as DTensor with current placement + # gather_dim is the dimension that is currently sharded locally (so we can gather it to full) + global_dt = DTensor.from_local(local_input, cp_mesh, [Shard(gather_dim)], run_check=False) + + # Redistribute to new placement (shard scatter_dim) + new_dt = global_dt.redistribute(cp_mesh, [Shard(scatter_dim)]) + + # Convert back to local + return new_dt.to_local() + + +def all_gather_tensor( + local_input: torch.Tensor, + gather_dim: int, + cp_mesh: "DeviceMesh", +) -> torch.Tensor: + """ + All-gather via DTensor redistribute. + Input placement: Shard(gather_dim) -> The dimension we are about to gather was split. + Output placement: Replicate() -> Full copy on each rank. + """ + # Wrap local tensor as DTensor with current placement + global_dt = DTensor.from_local(local_input, cp_mesh, [Shard(gather_dim)], run_check=False) + + # Redistribute to new placement (Replicate) + new_dt = global_dt.redistribute(cp_mesh, [Replicate()]) + + # Convert back to local + return new_dt.to_local() + + +def gather_seq_scatter_heads( + x: torch.Tensor, + seq_dim: int, + head_dim: int, + cp_mesh: DeviceMesh, +) -> torch.Tensor: + """ + A func to sync embedding input with alltoall in sequence parallel. + gather sequence dimension and scatter head dim: + For example, when seq_dim is 0, head_dim is 1, the transformation is: + [z, seq/n, h, ...] -> [z, seq, h/n, ...] + Args: + x: shape of [z, seq, h, ...] + seq_dim: the dimension to gather + head_dim: the dimension to scatter + cp_mesh: ulysses sequence parallelism size + Returns: + torch.Tensor: shape of gathered and scattered tensor + """ + return all_to_all_tensor(x, head_dim, seq_dim, cp_mesh) + + +def gather_heads_scatter_seq( + x: torch.Tensor, + head_dim: int, + seq_dim: int, + cp_mesh: DeviceMesh, +) -> torch.Tensor: + """ + A func to sync attention result with alltoall in sequence parallel. + gather head dimension and scatter seq dim: + For example, when seq_dim is 0, head_dim is 1, the transformation is: + [seq, h/n, ...] -> [seq/n, h, ...] + + Args: + x (torch.Tensor): shape of [bsz, seq, h/n, ...] + head_dim (int): the dimension to gather + seq_dim (int): the dimension to scatter + cp_mesh (DeviceMesh): ulysses sequence parallelism size + splits (List[torch.Tensor], optional): Manual splits for variable length scattering + + Returns: + torch.Tensor: shape of [bsz, seq/n, h, ...] + """ + return all_to_all_tensor(x, seq_dim, head_dim, cp_mesh) + + +def context_parallel_attention( + cp_mesh: DeviceMesh, + packed_query_states: FactoredSequencePack, + packed_key_states: FactoredSequencePack, + packed_value_states: FactoredSequencePack, + attention_mask: BlockMask | SplitInfo, + attention_function: Callable, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, +) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + """Ulysses-style context parallel attention for packed und+gen sequences. + + Each rank holds a sequence shard [S/cp, H, D]. Two all-to-all calls convert + between seq-sharded and head-sharded representations: + 1. gather seq, scatter heads → [S, H/cp, D] (head-sharded) + 2. run attention on full sequence with reduced heads + 3. gather heads, scatter seq → [S/cp, H, D] (seq-sharded) + + When ``memory_value`` is present, produces head-sharded ``kv_to_store`` + from the post-all-to-all K/V tensors for the caller to write back via + ``MemoryState.write_for_layer()``. Does **not** write to any cache + directly. + + Args: + cp_mesh: Device mesh for context parallelism. + packed_query_states: Packed Q for both und and gen tokens, seq-sharded [S/cp, H, D]. + packed_key_states: Packed K for both und and gen tokens, seq-sharded [S/cp, H, D]. + packed_value_states: Packed V for both und and gen tokens, seq-sharded [S/cp, H, D]. + attention_mask: Block mask or split info describing causal/full attention pattern. + attention_function: Callable implementing the actual attention kernel. + natten_metadata: Optional neighborhood attention metadata. + memory_value: Optional memory value for KV-cache training / AR inference. + + Returns: + (output_pack, kv_to_store): + output_pack: Packed attention output, seq-sharded [S/cp, H, D]. + kv_to_store: Head-sharded ``(gen_k, gen_v, und_k, und_v)`` when + ``memory_value`` is present, ``None`` otherwise. + """ + cp_group = cp_mesh.get_group() + cp_world_size = torch.distributed.get_world_size(cp_group) + assert cp_world_size > 1, "Context parallel world size must be greater than 1" + q_und_seq, _ = get_causal_seq(packed_query_states) # [text_shard_len,H,head_dim] + q_gen_seq, _ = get_full_only_seq(packed_query_states) # [gen_shard_len,H,head_dim] + k_und_seq, _ = get_causal_seq(packed_key_states) # [text_shard_len,H,head_dim] + k_gen_seq, _ = get_full_only_seq(packed_key_states) # [gen_shard_len,H,head_dim] + v_und_seq, _ = get_causal_seq(packed_value_states) # [text_shard_len,H,head_dim] + v_gen_seq, _ = get_full_only_seq(packed_value_states) # [gen_shard_len,H,head_dim] + + # Check that number of heads is divisible by CP world size + assert q_und_seq.shape[1] % cp_world_size == 0, ( + f"Query heads ({q_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + assert k_und_seq.shape[1] % cp_world_size == 0, ( + f"Key heads ({k_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + assert v_und_seq.shape[1] % cp_world_size == 0, ( + f"Value heads ({v_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + + + # when doing AR-inference with a KV-cache. + + # all2all: gather sequence, scatter heads → head-sharded + q_und_seq = gather_seq_scatter_heads( + q_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + q_gen_seq = gather_seq_scatter_heads( + q_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + k_und_seq = gather_seq_scatter_heads( + k_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + k_gen_seq = gather_seq_scatter_heads( + k_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + v_und_seq = gather_seq_scatter_heads( + v_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + v_gen_seq = gather_seq_scatter_heads( + v_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + + # Build head-sharded kv_to_store when memory is active. + kv_to_store: KVToStore | None = None + if memory_value is not None: + und_len = packed_key_states["_num_causal_tokens"] + gen_len = packed_key_states["_num_full_tokens"] + kv_to_store = ( + k_gen_seq[:gen_len].unsqueeze(0), + v_gen_seq[:gen_len].unsqueeze(0), + k_und_seq[:und_len].unsqueeze(0), + v_und_seq[:und_len].unsqueeze(0), + ) + + q_und_seq_len = q_und_seq.shape[0] + q_gen_seq_len = q_gen_seq.shape[0] + meta = dict(packed_query_states) + packed_query_states_ = from_mode_splits(q_und_seq, q_gen_seq, meta, is_sharded=False) + packed_key_states_ = from_mode_splits(k_und_seq, k_gen_seq, meta, is_sharded=False) + packed_value_states_ = from_mode_splits(v_und_seq, v_gen_seq, meta, is_sharded=False) + + # dispatch_attention returns (output, kv_to_store | None) + attn_output_pack_hp, _inner_kv_to_store = attention_function( + packed_query_states_, + packed_key_states_, + packed_value_states_, + attention_mask, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + attn_output_und_hp = get_und_seq(attn_output_pack_hp) # [text_len,H_local,head_dim] + attn_output_gen_hp = get_gen_seq(attn_output_pack_hp) # [gen_len,H_local,head_dim] + + attn_output_und_hp = attn_output_und_hp[:q_und_seq_len].contiguous() # [text_len,H_local,head_dim] + attn_output_gen_hp = attn_output_gen_hp[:q_gen_seq_len].contiguous() # [gen_len,H_local,head_dim] + + # all2all: gather heads, scatter seq → seq-sharded + attn_output_und_sp = gather_heads_scatter_seq( + attn_output_und_hp, + seq_dim=0, + head_dim=1, + cp_mesh=cp_mesh, + ) # [text_shard_len,H,head_dim] + attn_output_gen_sp = gather_heads_scatter_seq( + attn_output_gen_hp, + seq_dim=0, + head_dim=1, + cp_mesh=cp_mesh, + ) # [gen_shard_len,H,head_dim] + + final_output_pack_sp = from_mode_splits( + attn_output_und_sp, attn_output_gen_sp, packed_query_states, is_sharded=True + ) + + return final_output_pack_sp, kv_to_store diff --git a/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py b/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py new file mode 100644 index 00000000..3a2a6333 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py @@ -0,0 +1,1118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import List, Optional, Tuple + +import torch +from torch import nn +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_utils import PreTrainedModel + +from cosmos3._src.vfm.datasets.sequence_packing import ModalityData, PackedSequence, verify_natten_parameter_list +from cosmos3._src.vfm.models.mot.attention import build_packed_sequence +from cosmos3._src.vfm.models.mot.context_parallel_utils import ( + get_context_parallel_last_hidden_state, + get_context_parallel_sharded_sequence, +) +from cosmos3._src.vfm.models.mot.domain_aware_linear import DomainAwareLinear +from cosmos3._src.vfm.models.mot.modeling_utils import ( + FlattenedSinCosPositionEmbedding, + TimestepEmbedder, + VideoRopePosition3DEmb, +) +from cosmos3._src.vfm.models.utils.memory import MemoryState + + +class Cosmos3VFMNetworkConfig(PretrainedConfig): + def __init__( + self, + vision_gen=True, + action_gen=False, + sound_gen=False, + vlm_config=None, + latent_patch_size=2, + latent_downsample_factor=8, + latent_channel_size=16, + position_embedding_type="3d_rope", + max_latent_h=32, + max_latent_w=32, + max_latent_t=32, + rope_h_extrapolation_ratio=1.0, + rope_w_extrapolation_ratio=1.0, + rope_t_extrapolation_ratio=1.0, + enable_fps_modulation=False, + base_fps=24, + vit_max_num_patch_per_side=70, + connector_act="gelu_pytorch_tanh", + interpolate_pos=False, + timestep_shift=1.0, + timestep_scale=0.001, + predict_text_tokens=False, + joint_attn_implementation="two_way", + action_dim=32, + num_embodiment_domains=32, + temporal_compression_factor_vision=4, + temporal_compression_factor_action=1, + natten_parameter_list=None, + video_temporal_causal=False, + # Sound generation parameters + sound_dim: int | None = None, + temporal_compression_factor_sound=1, + sound_latent_fps: int = 25, + **kwargs, + ): + self.vision_gen = vision_gen + self.sound_gen = sound_gen + self.vlm_config = vlm_config + self.latent_patch_size = latent_patch_size + self.latent_downsample_factor = latent_downsample_factor + self.latent_channel_size = latent_channel_size + self.position_embedding_type = position_embedding_type + self.max_latent_h = max_latent_h + self.max_latent_w = max_latent_w + self.max_latent_t = max_latent_t + self.rope_h_extrapolation_ratio = rope_h_extrapolation_ratio + self.rope_w_extrapolation_ratio = rope_w_extrapolation_ratio + self.rope_t_extrapolation_ratio = rope_t_extrapolation_ratio + self.enable_fps_modulation = enable_fps_modulation + self.base_fps = base_fps + self.vit_max_num_patch_per_side = vit_max_num_patch_per_side + self.connector_act = connector_act + self.interpolate_pos = interpolate_pos + self.timestep_shift = timestep_shift + self.timestep_scale = timestep_scale + self.predict_text_tokens = predict_text_tokens + self.joint_attn_implementation = joint_attn_implementation + self.temporal_compression_factor_vision = temporal_compression_factor_vision + self.natten_parameter_list = natten_parameter_list + self.video_temporal_causal = video_temporal_causal + + # action related parameters + self.action_gen = action_gen # whether to generate action tokens + self.action_dim = action_dim + self.num_embodiment_domains = num_embodiment_domains + self.temporal_compression_factor_action = temporal_compression_factor_action + if self.action_gen: + assert self.vision_gen, ( + "Action generation requires visual generation! We do NOT support action only training!" + ) + + # sound related parameters + self.sound_dim = sound_dim + self.temporal_compression_factor_sound = temporal_compression_factor_sound + self.sound_latent_fps = sound_latent_fps + if self.sound_gen: + assert self.vision_gen, ( + "Sound generation requires visual generation! We do NOT support sound only training!" + ) + + super().__init__(**kwargs) + + +class Cosmos3VFMNetwork(PreTrainedModel): + config_class = Cosmos3VFMNetworkConfig + base_model_prefix = "cosmos3" + + def __init__(self, language_model, config: Cosmos3VFMNetworkConfig): + super().__init__(config) + self.language_model = language_model + self.hidden_size = config.vlm_config.hidden_size + self.use_moe = "Mo" in config.vlm_config.layer_module + self.num_heads = config.vlm_config.num_attention_heads + self.num_kv_heads = config.vlm_config.num_key_value_heads + self.head_dim = config.vlm_config.head_dim + self.num_hidden_layers = config.vlm_config.num_hidden_layers + self.predict_text_tokens = config.predict_text_tokens + + if config.natten_parameter_list is not None and config.joint_attn_implementation != "three_way": + raise NotImplementedError( + f"Sparsity is only supported with 'three_way' attention, but got {config.joint_attn_implementation=}, " + "and 'natten_parameter_list' was not None." + ) + self.natten_parameter_list = verify_natten_parameter_list( + config.natten_parameter_list, num_layers=self.num_hidden_layers + ) + + if config.video_temporal_causal and config.joint_attn_implementation != "three_way": + raise ValueError( + f"video_temporal_causal=True requires joint_attn_implementation='three_way', " + f"but got {config.joint_attn_implementation!r}." + ) + self.video_temporal_causal = config.video_temporal_causal + self.pad_for_cuda_graphs = False + + if config.vision_gen: + self.latent_patch_size = config.latent_patch_size + self.timestep_shift = config.timestep_shift + self.timestep_scale = config.timestep_scale + self.latent_downsample = config.latent_downsample_factor * config.latent_patch_size + self.max_latent_h = config.max_latent_h + self.max_latent_w = config.max_latent_w + self.max_latent_t = config.max_latent_t + self.latent_channel = config.latent_channel_size + self.patch_latent_dim = self.latent_patch_size**2 * self.latent_channel + + self.time_embedder = TimestepEmbedder(self.hidden_size) + self.vae2llm = nn.Linear(self.patch_latent_dim, self.hidden_size) + self.llm2vae = nn.Linear(self.hidden_size, self.patch_latent_dim) + + assert config.position_embedding_type in ["3d_rope", "flattened_sin_cos", "unified_3d_mrope"] + if config.position_embedding_type == "3d_rope": + self.latent_pos_embed = VideoRopePosition3DEmb( + head_dim=self.hidden_size, + len_h=self.max_latent_h, + len_w=self.max_latent_w, + len_t=self.max_latent_t, + h_extrapolation_ratio=config.rope_h_extrapolation_ratio, + w_extrapolation_ratio=config.rope_w_extrapolation_ratio, + t_extrapolation_ratio=config.rope_t_extrapolation_ratio, + enable_fps_modulation=config.enable_fps_modulation, # fps_modulation scales RoPE by fps. By default, disable FPS RoPE modulation. + base_fps=config.base_fps, + base_temporal_compression_factor=config.temporal_compression_factor_vision, + temporal_compression_factor=config.temporal_compression_factor_vision, + ) + elif config.position_embedding_type == "flattened_sin_cos": + self.latent_pos_embed = FlattenedSinCosPositionEmbedding( + max_latent_h=self.max_latent_h, max_latent_w=self.max_latent_w, hidden_size=self.hidden_size + ) + elif config.position_embedding_type == "unified_3d_mrope": + # No additive position embedding - position info is in 3D position IDs for attention + self.latent_pos_embed = None + else: + raise ValueError(f"Unknown position_embedding_type: {config.position_embedding_type!r}") + + if config.action_gen: + self.action_dim = config.action_dim + self.num_embodiment_domains = config.num_embodiment_domains + self.action2llm = DomainAwareLinear(self.action_dim, self.hidden_size, self.num_embodiment_domains) + self.llm2action = DomainAwareLinear(self.hidden_size, self.action_dim, self.num_embodiment_domains) + + if config.position_embedding_type == "3d_rope": + self.action_pos_embed = VideoRopePosition3DEmb( + head_dim=self.hidden_size, + len_h=1, + len_w=1, + len_t=self.max_latent_t * config.temporal_compression_factor_vision, + h_extrapolation_ratio=config.rope_h_extrapolation_ratio, + w_extrapolation_ratio=config.rope_w_extrapolation_ratio, + t_extrapolation_ratio=config.rope_t_extrapolation_ratio, + enable_fps_modulation=config.enable_fps_modulation, + base_fps=config.base_fps, + base_temporal_compression_factor=config.temporal_compression_factor_vision, # vision compression factor is used for base tps + temporal_compression_factor=config.temporal_compression_factor_action, # Action is at frame rate (no temporal compression) + ) + elif config.position_embedding_type == "unified_3d_mrope": + # No additive position embedding - position info is in 3D position IDs for attention + self.action_pos_embed = None + else: + raise ValueError(f"Unknown position_embedding_type: {config.position_embedding_type!r}") + + self.action_modality_embed = nn.Parameter(torch.zeros(self.hidden_size)) + + if config.sound_gen: + self.sound_dim = config.sound_dim + self.sound2llm = nn.Linear(config.sound_dim, self.hidden_size) + self.llm2sound = nn.Linear(self.hidden_size, config.sound_dim) + self.sound_modality_embed = nn.Parameter(torch.zeros(self.hidden_size)) + + self.config = config + self.parallel_dims = None + + def init_weights(self, buffer_device: torch.device | None): + if self.config.vision_gen or self.config.action_gen or self.config.sound_gen: + self.time_embedder._init_weights() + + if self.config.vision_gen: + std = 1.0 / math.sqrt(self.patch_latent_dim) + torch.nn.init.trunc_normal_(self.vae2llm.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.vae2llm.bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2vae.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2vae.bias) + + if self.latent_pos_embed is not None: + self.latent_pos_embed._init_weights() + + if self.config.action_gen: + # DomainAwareLinear uses embeddings for weights, so we initialize them differently + # action2llm: input_size=action_dim, output_size=hidden_size + std = 1.0 / math.sqrt(self.action_dim) + torch.nn.init.trunc_normal_(self.action2llm.fc.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.action2llm.bias.weight) + + # llm2action: input_size=hidden_size, output_size=action_dim + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2action.fc.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2action.bias.weight) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.action_modality_embed, std=std, a=-3 * std, b=3 * std) + + if self.action_pos_embed is not None: + self.action_pos_embed._init_weights() + + if self.config.sound_gen: + # sound2llm: input_size=sound_dim, output_size=hidden_size + std = 1.0 / math.sqrt(self.sound_dim) + torch.nn.init.trunc_normal_(self.sound2llm.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.sound2llm.bias) + + # llm2sound: input_size=hidden_size, output_size=sound_dim + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2sound.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2sound.bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.sound_modality_embed, std=std, a=-3 * std, b=3 * std) + + self.language_model.init_weights(buffer_device=buffer_device) + + def patchify_and_pack_latents( + self, tokens_vision: torch.Tensor, token_shapes_vision: List[Tuple[int, int, int]] + ) -> tuple[torch.Tensor, List[Tuple[int, int, int]]]: + p = self.latent_patch_size + # Patchify and pack the latents + packed_latent = [] + original_latent_shapes = [] # Store original shapes for unpadding later + + # C, T, H, W + for latent, (t, h, w) in zip(tokens_vision, token_shapes_vision): + latent = latent.squeeze(0) # [C,T,H,W] + + # Get original latent dimensions + _, t_actual, h_actual, w_actual = latent.shape + original_latent_shapes.append((t_actual, h_actual, w_actual)) + + # Compute padded dimensions (must be divisible by p) + h_padded = ((h_actual + p - 1) // p) * p + w_padded = ((w_actual + p - 1) // p) * p + + # Zero-pad if dimensions are not divisible by p + if h_padded != h_actual or w_padded != w_actual: + padded = torch.zeros( + (self.latent_channel, t_actual, h_padded, w_padded), + device=latent.device, + dtype=latent.dtype, + ) # [C,T,H_padded,W_padded] + padded[:, :, :h_actual, :w_actual] = latent + latent = padded # [C,T,H_padded,W_padded] + + # Compute number of patches after padding + h_patches = h_padded // p + w_patches = w_padded // p + + # Patchify + latent = latent.reshape( + self.latent_channel, t_actual, h_patches, p, w_patches, p + ) # [C,T,h_patches,p,w_patches,p] + latent = torch.einsum("cthpwq->thwpqc", latent).reshape( + -1, p * p * self.latent_channel + ) # [T*h_patches*w_patches,patch_latent_dim] + packed_latent.append(latent) + + # We assumed latents we get to the network is already noised + packed_latent = torch.cat(packed_latent, dim=0) # [total_vision_patches,patch_latent_dim] + return packed_latent, original_latent_shapes + + def unpatchify_and_unpack_latents( + self, + packed_mse_preds: torch.Tensor, + token_shapes_vision: List[Tuple[int, int, int]], + noisy_frame_indexes_vision: list[torch.Tensor], + original_latent_shapes: List[Tuple[int, int, int]] | None = None, + ) -> list[torch.Tensor]: + p = self.latent_patch_size + unpatchified_latents = [] + + # Split packed_mse_preds back into individual latents based on token_shapes_vision + start_idx = 0 + for i, (t_c, h_c, w_c) in enumerate(token_shapes_vision): + # Get original shape for unpadding (if provided) + if original_latent_shapes is not None: + t_orig, h_orig, w_orig = original_latent_shapes[i] + # Compute padded dimensions used during patchify + h_padded = ((h_orig + p - 1) // p) * p + w_padded = ((w_orig + p - 1) // p) * p + h_patches = h_padded // p + w_patches = w_padded // p + else: + # Fallback: use token shapes directly (assumes no padding was needed) + t_orig, h_orig, w_orig = t_c, h_c * p, w_c * p + h_patches, w_patches = h_c, w_c + + # noisy_frame_indexes_vision is a list of tensors, each with shape (T,), + # where the values are the noisy frame indices. + noisy_frame_indexes = noisy_frame_indexes_vision[i] + t_n = len(noisy_frame_indexes) + + # Initialize with the original shape (after unpadding), zeros for clean frames + output_tensor = torch.zeros( + (self.latent_channel, t_c, h_orig, w_orig), + device=packed_mse_preds.device, + dtype=packed_mse_preds.dtype, + ) # [C,T,H_orig,W_orig] + num_patches = t_n * h_patches * w_patches + if num_patches > 0: + end_idx = start_idx + num_patches + # Extract patches for this latent + latent_patches = packed_mse_preds[start_idx:end_idx] # [num_patches,patch_latent_dim] + # Reshape back to [t_n, h_patches, w_patches, p, p, channels] + latent_patches = latent_patches.reshape( + t_n, h_patches, w_patches, p, p, self.latent_channel + ) # [T_n,h_patches,w_patches,p,p,C] + # Invert the einsum operation: "thwpqc->cthpwq" + latent = torch.einsum("thwpqc->cthpwq", latent_patches) # [C,T_n,h_patches,p,w_patches,p] + # Reshape back to [channels, t_n, h_padded, w_padded] + latent = latent.reshape( + self.latent_channel, t_n, h_patches * p, w_patches * p + ) # [C,T_n,H_padded,W_padded] + + # Crop to original dimensions (unpad the zeros) + latent = latent[:, :, :h_orig, :w_orig] # [C,T_n,H_orig,W_orig] + + # Fill only the noisy frame positions using the actual mask indices + output_tensor[:, noisy_frame_indexes] = latent + + start_idx = end_idx + + unpatchified_latents.append(output_tensor.unsqueeze(0)) # [1,C,T,H,W] + + # Return list of unpatchified latents (supports variable shapes) + return unpatchified_latents + + def pack_action( + self, + tokens_action: list[torch.Tensor], + token_shapes_action: list[tuple[int, ...]], + domain_id_action: list[torch.Tensor], + ) -> tuple[torch.Tensor, torch.Tensor]: + """Pack variable-length action tokens into a 1D sequence for transformer input. + + Args: + tokens_action: List of action tensors, each [T_i, action_dim] (T_i may vary). + token_shapes_action: List of (T_i,) tuples per sample. + domain_id_action: List of domain ID tensors, each of shape [1]. + + Returns: + Tuple of (packed_tokens, per_token_domain_id): + packed_tokens: [total_action_tokens, action_dim] + per_token_domain_id: [total_action_tokens] + """ + packed: list[torch.Tensor] = [] + domain_ids: list[torch.Tensor] = [] + for tokens, shape, d_id in zip(tokens_action, token_shapes_action, domain_id_action): + T = shape[0] + packed.append(tokens[:T]) + domain_ids.append(d_id.expand(T)) + return torch.cat(packed, dim=0), torch.cat(domain_ids, dim=0) + + def unpack_action( + self, + packed_action_preds: torch.Tensor, + token_shapes_action: list[tuple[int, ...]], + noisy_frame_indexes_action: list[torch.Tensor], + ) -> list[torch.Tensor]: + """Unpack action predictions back into per-sample action tensors. + + Args: + packed_action_preds: Packed action predictions of shape (total_noisy_tokens, action_dim) + token_shapes_action: Per-sample token shapes, each (T_i,) tuple. + noisy_frame_indexes_action: List of tensors, each with shape (Tn_i,), where the values + are the noisy frame indices for sample i. + + Returns: + List of per-sample tensors, each of shape (T_i, action_dim), with predictions + placed at noisy positions. Clean positions are left as zeros. + """ + unpacked: list[torch.Tensor] = [] + start_idx = 0 + for shape, noisy_frame_indexes in zip(token_shapes_action, noisy_frame_indexes_action): + T = shape[0] + output = torch.zeros( + (T, self.action_dim), + device=packed_action_preds.device, + dtype=packed_action_preds.dtype, + ) + t_n = len(noisy_frame_indexes) + if t_n > 0: + end_idx = start_idx + t_n + output[noisy_frame_indexes] = packed_action_preds[start_idx:end_idx] + start_idx = end_idx + unpacked.append(output) + return unpacked + + def pack_sound_latents( + self, + tokens_sound: list[torch.Tensor], + token_shapes_sound: list[tuple[int, int, int]], + ) -> torch.Tensor: + """Pack sound latents into a 1D sequence for transformer input. + + Args: + tokens_sound: List of sound latent tensors, each [C, T] + token_shapes_sound: List of (T, 1, 1) tuples per sample + + Returns: + Packed tensor of shape [total_sound_tokens, C] + """ + packed = [] + for sound, shape in zip(tokens_sound, token_shapes_sound): + T = shape[0] + # sound: [C, T] → take first T frames → [C, T] + # Then permute to [T, C] for packing + sound_tokens = sound[:, :T].permute(1, 0) # [T,C] + packed.append(sound_tokens) + return torch.cat(packed, dim=0) # [total_sound_tokens,C] + + def unpack_sound_latents( + self, + packed_sound_preds: torch.Tensor, + token_shapes_sound: list[tuple[int, int, int]], + noisy_frame_indexes_sound: list[torch.Tensor], + ) -> list[torch.Tensor]: + """Unpack sound predictions back into per-sample sound latents. + + Args: + packed_sound_preds: Packed sound predictions of shape (total_noisy_tokens, sound_dim) + token_shapes_sound: List of (T, 1, 1) tuples per sample + noisy_frame_indexes_action: List of tensors, each with shape (T_i,), where the values + are the noisy frame indices. T_i <= max_T. + + Returns: + List of per-sample tensors, each [C, T], with predictions placed at noisy positions. + Clean positions are left as zeros. + """ + unpacked = [] + start_idx = 0 + for shape, noisy_frame_indexes in zip(token_shapes_sound, noisy_frame_indexes_sound): + T = shape[0] + # Initialize output with zeros for clean positions + output = torch.zeros( + (self.sound_dim, T), + device=packed_sound_preds.device, + dtype=packed_sound_preds.dtype, + ) + + t_n = len(noisy_frame_indexes) + + if t_n > 0: + end_idx = start_idx + t_n + # packed_sound_preds: [total_noisy_tokens, C] → transpose and fill at noisy positions + output[:, noisy_frame_indexes] = packed_sound_preds[ + start_idx:end_idx + ].T # packed_sound_preds[...]: [T_n,C] → .T: [C,T_n] + start_idx = end_idx + + unpacked.append(output) + return unpacked + + def _encode_text( + self, + packed_seq: PackedSequence, + ) -> tuple[torch.Tensor, torch.dtype]: + """Embed text tokens and initialize packed_sequence. + + Args: + packed_seq: PackedSequence containing text_ids and text_indexes. + + Returns: + tuple of (packed_sequence, target_dtype) where packed_sequence has text embeddings filled in. + """ + packed_text_embedding = self.language_model.model.embed_tokens(packed_seq.text_ids) # [N_text,hidden_size] + packed_sequence = packed_text_embedding.new_zeros( + size=(packed_seq.sequence_length, self.hidden_size) + ) # [N_total,hidden_size] + packed_sequence[packed_seq.text_indexes] = ( + packed_text_embedding # [N_text,hidden_size] scattered into [N_total,hidden_size] + ) + return packed_sequence, packed_text_embedding.dtype + + def _encode_vision( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps: Optional[torch.Tensor] = None, + ) -> List[Tuple[int, int, int]] | None: + """Project vision tokens and fill into packed_sequence. + + Args: + packed_seq: PackedSequence containing vision tokens and metadata. + packed_sequence: The packed sequence tensor to fill vision embeddings into (modified in-place). + target_dtype: Target dtype for embeddings (typically from text embedding). + fps: Optional FPS tensor for RoPE modulation. + + Returns: + Original latent shapes before padding (for unpadding during decode), or None if no vision tokens. + """ + if packed_seq.vision is None or packed_seq.vision.tokens is None: + # No vision tokens in this batch + return None + + vision = packed_seq.vision + assert vision.tokens is not None # Type narrowing (checked above but reassignment loses it) + assert vision.token_shapes is not None + assert isinstance(vision.sequence_indexes, torch.Tensor) + assert isinstance(vision.timesteps, torch.Tensor) + assert isinstance(vision.mse_loss_indexes, torch.Tensor) + + packed_tokens_vision, original_latent_shapes = self.patchify_and_pack_latents( + vision.tokens, vision.token_shapes + ) # packed_tokens_vision: [total_vision_patches,patch_latent_dim] + + packed_tokens_vision = self.vae2llm(packed_tokens_vision) # [total_vision_patches,hidden_size] + + # Add absolute position embedding only when NOT using unified 3D mRoPE + # (3D mRoPE provides positional information via rotary embeddings instead) + if self.latent_pos_embed is not None: + latent_token_pos_emb = self.latent_pos_embed(vision.token_shapes, fps=fps).to( + target_dtype + ) # [total_vision_patches,hidden_size] + packed_tokens_vision = packed_tokens_vision + latent_token_pos_emb # [total_vision_patches,hidden_size] + + has_noisy_vision = vision.mse_loss_indexes.numel() > 0 + + if has_noisy_vision: + timesteps_vision = vision.timesteps * self.timestep_scale # [N_noisy_frames_vision] + + # Timesteps are computed in FP32 for numerical stability. + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_vision = self.time_embedder( + timesteps_vision + ) # [N_noisy_frames_vision,hidden_size] + packed_timestep_embeds_vision = packed_timestep_embeds_vision.to( + target_dtype + ) # [N_noisy_frames_vision,hidden_size] + + packed_tokens_vision = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_vision, + packed_timestep_embeds=packed_timestep_embeds_vision, + noisy_frame_indexes=vision.noisy_frame_indexes, + token_shapes=vision.token_shapes, + ) # [total_vision_patches,hidden_size] + + packed_sequence[vision.sequence_indexes] = ( + packed_tokens_vision # [total_vision_patches,hidden_size] scattered into [N_total,hidden_size] + ) + return original_latent_shapes + + def _decode_vision( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + original_latent_shapes: List[Tuple[int, int, int]] | None = None, + ) -> None: + """Decode vision tokens from hidden states and update output_dict. + + Args: + packed_seq: PackedSequence containing mse_loss_indexes_vision and token_shapes_vision. + last_hidden_state: Hidden states from the transformer. + output_dict: Output dictionary to update with mse_preds (modified in-place). + original_latent_shapes: Original latent shapes before padding (for unpadding). + """ + vision = packed_seq.vision + # Check if no vision or no noisy vision tokens + has_noisy_vision = ( + vision is not None + and vision.tokens is not None + and isinstance(vision.mse_loss_indexes, torch.Tensor) + and vision.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_vision: + # No noisy vision tokens present. The model is predicting actions + # given clean vision tokens. We need to execute a dummy forward to maintain + # computation graph consistency across ranks (FSDP should torch all weights). + preds_vision = torch.zeros( + [1, self.patch_latent_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,patch_latent_dim] + preds_vision = self.vae2llm(preds_vision) # [1,hidden_size] + preds_vision = self.llm2vae(preds_vision) # [1,patch_latent_dim] + # Return a list of per-sample zero tensors with correct shapes (e.g. (C, T, H, W)), + # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_vision + # gets properly-shaped tensors. Without this, the dummy tensor (1, patch_latent_dim) + # would cause a size mismatch when concatenating vision+action velocities. + # When vision is None (no vision in batch), fall back to [preds_vision] purely for + # gradient graph consistency — it won't be iterated over. + if vision is not None and vision.tokens is not None: + preds_vision_list = [torch.zeros_like(tok) for tok in vision.tokens] + # Inject dummy forward's computation graph so vae2llm/llm2vae params + # stay in the autograd graph (zeros_like creates detached tensors). + preds_vision_list[0] = preds_vision_list[0] + 0.0 * preds_vision.sum() + else: + preds_vision_list = [preds_vision] + output_dict.update(preds_vision=preds_vision_list) + else: + assert vision is not None # Type narrowing + assert isinstance(vision.mse_loss_indexes, torch.Tensor) + assert vision.noisy_frame_indexes is not None + preds_vision = self.llm2vae( + last_hidden_state[vision.mse_loss_indexes] + ) # [total_noisy_vision_patches,patch_latent_dim] + preds_vision = self.unpatchify_and_unpack_latents( + preds_vision, + token_shapes_vision=vision.token_shapes, + noisy_frame_indexes_vision=vision.noisy_frame_indexes, + original_latent_shapes=original_latent_shapes, + ) + output_dict.update(preds_vision=preds_vision) + + def _encode_action( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps_action: Optional[torch.Tensor] = None, + ) -> None: + """Encode action tokens and fill into packed_sequence.""" + if packed_seq.action is None or packed_seq.action.tokens is None: + # No action tokens in this batch + return + + action: ModalityData = packed_seq.action + assert action.token_shapes is not None + assert isinstance(action.sequence_indexes, torch.Tensor) + assert isinstance(action.timesteps, torch.Tensor) + assert isinstance(action.mse_loss_indexes, torch.Tensor) + + # Pack variable-length action tokens into a 1D sequence (same pattern as pack_sound_latents) + packed_tokens_action, per_token_domain_id = self.pack_action( + action.tokens, action.token_shapes, action.domain_id + ) + packed_tokens_action = self.action2llm(packed_tokens_action, per_token_domain_id) + + # Add additive position embedding only if not using unified_3d_mrope + if self.action_pos_embed is not None: + # VideoRopePosition3DEmb expects shapes as (t, h, w). For actions we use a 1x1 spatial grid. + action_shapes_3d = [(ts[0], 1, 1) for ts in action.token_shapes] + action_token_pos_emb = self.action_pos_embed( + action_shapes_3d, + fps=fps_action, + start_frame_offset=1, + ).to(target_dtype) # [B_action*T_action,hidden_size] + packed_tokens_action = packed_tokens_action + action_token_pos_emb # [B_action*T_action,hidden_size] + + packed_tokens_action = packed_tokens_action + self.action_modality_embed.view( + 1, -1 + ) # [B_action*T_action,hidden_size] + + has_noisy_actions = action.mse_loss_indexes.numel() > 0 + if has_noisy_actions: + timesteps_action = action.timesteps * self.timestep_scale # [N_noisy_frames_action] + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_action = self.time_embedder( + timesteps_action + ) # [N_noisy_frames_action,hidden_size] + packed_timestep_embeds_action = packed_timestep_embeds_action.to( + target_dtype + ) # [N_noisy_frames_action,hidden_size] + + packed_tokens_action = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_action, + packed_timestep_embeds=packed_timestep_embeds_action, + noisy_frame_indexes=action.noisy_frame_indexes, + token_shapes=action.token_shapes, + ) # [B_action*T_action,hidden_size] + + packed_sequence[action.sequence_indexes] = ( + packed_tokens_action # [B_action*T_action,hidden_size] scattered into [N_total,hidden_size] + ) + + def _decode_action( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + ) -> None: + """Decode action tokens from hidden states and update output_dict.""" + action = packed_seq.action + # Check if no action or no noisy action tokens + has_noisy_action = ( + action is not None + and action.tokens is not None + and isinstance(action.mse_loss_indexes, torch.Tensor) + and action.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_action: + # dummy forward to maintain computation graph consistency across ranks + preds_action = torch.zeros( + [1, self.action_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,action_dim] + dummy_domain_id = torch.zeros([1], device=last_hidden_state.device, dtype=torch.long) # [1] + preds_action = self.action2llm(preds_action, dummy_domain_id) + self.action_modality_embed.view( + 1, -1 + ) # [1,hidden_size] + preds_action = self.llm2action(preds_action, dummy_domain_id) # [1,action_dim] + # Return a list of per-sample zero tensors with correct shapes (e.g. (T, action_dim)), + # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_action + # gets properly-shaped tensors. Without this, the dummy tensor (1, action_dim) + # would cause a size mismatch when concatenating vision+action velocities. + if action is not None and action.tokens is not None: + preds_action_list = [torch.zeros_like(tok) for tok in action.tokens] + # Inject dummy forward's computation graph so DomainAwareLinear params + # stay in the autograd graph (zeros_like creates detached tensors). + preds_action_list[0] = preds_action_list[0] + 0.0 * preds_action.sum() + # When action is None (no action in batch), fall back to [preds_action] purely for + # gradient graph consistency — it won't be iterated over. + else: + preds_action_list = [preds_action] + output_dict.update(preds_action=preds_action_list) + else: + assert action is not None # Type narrowing + assert isinstance(action.mse_loss_indexes, torch.Tensor) + assert action.condition_mask is not None + assert len(action.domain_id) > 0 + + action_hidden_states = last_hidden_state[action.mse_loss_indexes] # [total_noisy_action_tokens,hidden_size] + + # Build per-token domain IDs for the noisy tokens (same expansion logic as pack_action) + domain_ids: list[torch.Tensor] = [] + for nfi, d_id in zip(action.noisy_frame_indexes, action.domain_id): + domain_ids.append(d_id.expand(len(nfi))) + per_token_domain_id = torch.cat(domain_ids, dim=0) + + preds_action = self.llm2action( + action_hidden_states, per_token_domain_id + ) # [total_noisy_action_tokens,action_dim] + preds_action = self.unpack_action(preds_action, action.token_shapes, action.noisy_frame_indexes) + output_dict.update(preds_action=preds_action) + + def _encode_sound( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps_sound: Optional[torch.Tensor] = None, + ) -> None: + """Encode sound tokens and fill into packed_sequence. + + Args: + packed_seq: PackedSequence containing sound tokens and metadata. + packed_sequence: The packed sequence tensor to fill sound embeddings into (modified in-place). + target_dtype: Target dtype for embeddings (typically from text embedding). + fps_sound: FPS tensor for RoPE modulation. Should be the sound latent rate (e.g., 25 Hz). + """ + if packed_seq.sound is None or packed_seq.sound.tokens is None: + # No sound tokens in this batch + return + + sound = packed_seq.sound + assert sound.token_shapes is not None + assert isinstance(sound.sequence_indexes, torch.Tensor) + assert isinstance(sound.timesteps, torch.Tensor) + assert isinstance(sound.mse_loss_indexes, torch.Tensor) + + # Pack sound latents: list of [C, T] tensors → [total_tokens, C] + packed_tokens_sound = self.pack_sound_latents( + sound.tokens, sound.token_shapes + ) # [total_sound_tokens,sound_dim] + packed_tokens_sound = packed_tokens_sound.to(target_dtype) # [total_sound_tokens,sound_dim] + + # Project sound tokens + modality embedding + + # No additive position embedding is used (unlike legacy video which keeps one for backward compat). + packed_tokens_sound = ( + self.sound2llm(packed_tokens_sound) + self.sound_modality_embed + ) # [total_sound_tokens,hidden_size] + + has_noisy_sound = sound.mse_loss_indexes.numel() > 0 + if has_noisy_sound: + timesteps_sound = sound.timesteps * self.timestep_scale # [N_noisy_frames_sound] + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_sound = self.time_embedder(timesteps_sound) # [N_noisy_frames_sound,hidden_size] + packed_timestep_embeds_sound = packed_timestep_embeds_sound.to( + target_dtype + ) # [N_noisy_frames_sound,hidden_size] + + packed_tokens_sound = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_sound, + packed_timestep_embeds=packed_timestep_embeds_sound, + noisy_frame_indexes=sound.noisy_frame_indexes, + token_shapes=sound.token_shapes, + ) # [total_sound_tokens,hidden_size] + + packed_sequence[sound.sequence_indexes] = ( + packed_tokens_sound # [total_sound_tokens,hidden_size] scattered into [N_total,hidden_size] + ) + + def _decode_sound( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + ) -> None: + """Decode sound tokens from hidden states and update output_dict. + + Args: + packed_seq: PackedSequence containing sound modality data. + last_hidden_state: Hidden states from the transformer. + output_dict: Output dictionary to update with preds_sound (modified in-place). + """ + sound = packed_seq.sound + # Check if no sound or no noisy sound tokens + has_noisy_sound = ( + sound is not None + and sound.tokens is not None + and isinstance(sound.mse_loss_indexes, torch.Tensor) + and sound.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_sound: + # dummy forward to maintain computation graph consistency across ranks + preds_sound = torch.zeros( + [1, self.sound_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,sound_dim] + preds_sound = self.sound2llm(preds_sound) + self.sound_modality_embed # [1,hidden_size] + preds_sound = self.llm2sound(preds_sound) # [1,sound_dim] + if sound is not None and sound.tokens is not None: + preds_sound_list = [torch.zeros_like(tok) for tok in sound.tokens] + preds_sound_list[0] = preds_sound_list[0] + 0.0 * preds_sound.sum() + else: + preds_sound_list = [preds_sound] + output_dict.update(preds_sound=preds_sound_list) + else: + assert sound is not None # Type narrowing + assert isinstance(sound.mse_loss_indexes, torch.Tensor) + assert sound.condition_mask is not None + preds_sound = self.llm2sound( + last_hidden_state[sound.mse_loss_indexes] + ) # [total_noisy_sound_tokens,sound_dim] + preds_sound = self.unpack_sound_latents( + preds_sound, sound.token_shapes, sound.noisy_frame_indexes + ) # list of [C,T] per sample + output_dict.update(preds_sound=preds_sound) + + def forward( + self, + packed_seq: PackedSequence, + fps_vision: Optional[torch.Tensor] = None, + fps_action: Optional[torch.Tensor] = None, + fps_sound: Optional[torch.Tensor] = None, + memory: MemoryState | None = None, + ) -> dict: + """ + Forward pass for Cosmos3VFMNetwork. + + Args: + packed_seq: PackedSequence containing all packed tensors and metadata. + See PackedSequence dataclass for field details. + fps_vision: Optional FPS tensor for vision RoPE modulation. + fps_action: Optional FPS tensor for action RoPE modulation. + fps_sound: Optional FPS tensor for sound RoPE modulation (e.g., sound_latent_fps=25). + memory: Optional MemoryState for persistent KV-cache memory + (AR inference or rolling-KV-cache training). Built by + ``OmniMoTModel.build_memory_state()``. + + Returns: + dict with keys: + - "preds_vision": list[Tensor[C,T,H,W]], one per sample. + - "preds_action": Velocity predictions for action tokens (if action_gen). + - "preds_sound": Velocity predictions for sound tokens (if sound_gen). + - "last_hidden_state": Last hidden state from the transformer. + - "lbl_metadata_*": Load balancing metadata. + - "ce_preds": Cross-entropy predictions (if predict_text_tokens is True). + """ + # Note: During inference with @torch.no_grad(), model may be in training mode + # This is intentional for proper batch norm / dropout behavior + # assert self.training, "Cosmos3VFMNetwork only supports training mode" + + packed_sequence, target_dtype = self._encode_text(packed_seq) # packed_sequence: [N_total,hidden_size] + + # encode vision tokens + original_latent_shapes: List[Tuple[int, int, int]] | None = None + if self.config.vision_gen: + original_latent_shapes = self._encode_vision(packed_seq, packed_sequence, target_dtype, fps_vision) + + # encode action tokens + if self.config.action_gen: + self._encode_action(packed_seq, packed_sequence, target_dtype, fps_action) + + # encode sound tokens + if self.config.sound_gen: + self._encode_sound(packed_seq, packed_sequence, target_dtype, fps_sound) + + assert self.use_moe + assert packed_seq.attn_modes is not None + assert packed_seq.split_lens is not None + + # Get all generation sequence indexes for MoE routing + # IMPORTANT: Include ALL latent tokens (video + action + sound), not just generation targets. + # Condition tokens still need to be routed to diffusion experts; they are excluded from + # LOSS computation, not from routing. + all_gen_indexes = [] + if packed_seq.vision is not None: + assert packed_seq.vision.token_shapes is not None + assert isinstance(packed_seq.vision.sequence_indexes, torch.Tensor) + all_gen_indexes.append(packed_seq.vision.sequence_indexes) + if packed_seq.action is not None and isinstance(packed_seq.action.sequence_indexes, torch.Tensor): + all_gen_indexes.append(packed_seq.action.sequence_indexes) + if packed_seq.sound is not None and isinstance(packed_seq.sound.sequence_indexes, torch.Tensor): + all_gen_indexes.append(packed_seq.sound.sequence_indexes) + vision_sequence_indexes = torch.cat(all_gen_indexes, dim=0) if all_gen_indexes else None # [N_gen_tokens] + + # When temporal causal is enabled the buffer is [action_t0, vision_t0, action_t1, vision_t1, ...]. + # After torch.cat([vision_indexes, action_indexes]) the interleaved order is lost; sorting restores it. + if self.video_temporal_causal: + assert packed_seq.sound is None, "Sound generation is not supported with video_temporal_causal=True." + if vision_sequence_indexes is not None: + vision_sequence_indexes = vision_sequence_indexes.sort().values # [N_gen_tokens] + + vision_token_shapes = packed_seq.vision.token_shapes if packed_seq.vision else None + + # The packer is the single source of truth for the supertoken layout. + # ``num_action_tokens_per_supertoken`` is stamped onto ``packed_seq`` by + # ``_pack_supertokens_temporal_causal`` (= tcf when actions are packed + # inline, 0 otherwise) and read unchanged by the attention builder, the + # NATTEN metadata generator, and the rolling KV-cache state — keeping + # all downstream supertoken geometry automatically in sync with the pack. + num_action_tokens_per_supertoken = packed_seq.num_action_tokens_per_supertoken + + input_pack, attention_meta, natten_metadata_list = build_packed_sequence( + self.config.joint_attn_implementation, + packed_sequence=packed_sequence, + attn_modes=packed_seq.attn_modes, + split_lens=packed_seq.split_lens, + sample_lens=packed_seq.sample_lens, + packed_und_token_indexes=packed_seq.text_indexes, + packed_gen_token_indexes=vision_sequence_indexes, + num_heads=self.num_heads, + is_image_batch=packed_seq.is_image_batch, + head_dim=self.head_dim, + num_layers=self.num_hidden_layers, + token_shapes=packed_seq.vision.token_shapes, + natten_parameter_list=self.natten_parameter_list, + cp_world_size=self.parallel_dims.cp_size if self.parallel_dims else 1, + video_temporal_causal=self.video_temporal_causal, + use_rolling_kv_cache=memory is not None and memory.uses_rolling_kv_cache, + vision_token_shapes=vision_token_shapes, + action_token_shapes=packed_seq.action.token_shapes if packed_seq.action else None, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + null_action_supertokens=packed_seq.null_action_supertokens, + pad_for_cuda_graphs=self.pad_for_cuda_graphs, + ) + + input_pack, packed_position_ids = get_context_parallel_sharded_sequence( + attn_implementation=self.config.joint_attn_implementation, + input_pack=input_pack, + position_ids=packed_seq.position_ids, + parallel_dims=self.parallel_dims, + ) + + packed_outputs, lbl_metadata = self.language_model( + input_pack, + attention_mask=attention_meta, + position_ids=packed_position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + last_hidden_state = get_context_parallel_last_hidden_state( + packed_outputs=packed_outputs, + parallel_dims=self.parallel_dims, + ) # [N_total,hidden_size] + output_dict = dict() + + # decode vision tokens + if self.config.vision_gen: + self._decode_vision(packed_seq, last_hidden_state, output_dict, original_latent_shapes) + + # decode action tokens + if self.config.action_gen: + self._decode_action(packed_seq, last_hidden_state, output_dict) + + # decode sound tokens + if self.config.sound_gen: + self._decode_sound(packed_seq, last_hidden_state, output_dict) + + output_dict.update(last_hidden_state=last_hidden_state) + for lbl_metadata_key, lbl_metadata_value in lbl_metadata.items(): + output_dict.update({f"lbl_metadata_{lbl_metadata_key}": lbl_metadata_value}) + if self.predict_text_tokens: + packed_ce_preds = self.language_model.lm_head( + last_hidden_state[packed_seq.ce_loss_indexes] + ) # [N_ce_tokens,vocab_size] + output_dict["ce_preds"] = packed_ce_preds + + return output_dict + + +def _apply_timestep_embeds_to_noisy_tokens( + packed_tokens: torch.Tensor, + packed_timestep_embeds: torch.Tensor, + noisy_frame_indexes: List[torch.Tensor], + token_shapes: list[tuple[int, ...]], +) -> torch.Tensor: + """Apply timestep embeddings to noisy tokens. + Tn is the number of noisy frames for a given sample. + Tc is the number of clean frames for a given sample. + T is the total number of frames for a given sample. + T = Tn + Tc + + Args: + packed_tokens: The packed tokens to apply timestep embeddings to. + packed_timestep_embeds: The packed timestep embeddings to apply. + noisy_frame_indexes: The frame indices to apply timestep embeddings to + (list of tensors, each with shape (Tn,)). + token_shapes: The token shapes for each sample. Each entry is a tuple + shaped like ``(T, ...)`` where trailing dimensions represent the spatial grid. + + Returns: + The packed tokens with timestep embeddings applied to the noisy tokens. + """ + + # Handle variable token shapes by processing each sample's noisy_frame_indexes individually. + # The noisy indices are first expanded to cover the entire spatial grid of each frame. + # + # For video frames, the spatial grid is (H, W). + # For action frames, the spatial grid is (). + # For sound frames, the spatial grid is (1, 1). + # + # The noisy indices are then flattened into a single tensor overall. When flattening, + # we must ensure that the noisy indices from each sample are unique by adding the + # cumulative sum of the token shapes of previous samples to the noisy indices for + # a given sample. + start_noisy_index = 0 + flattened_noisy_frame_indexes = [] + + for noisy_indexes_i, token_shape_i in zip(noisy_frame_indexes, token_shapes): + assert noisy_indexes_i.numel() <= token_shape_i[0] + spatial_numel_i = math.prod(token_shape_i[1:]) + spatial_indexes_i = torch.arange(spatial_numel_i, device=packed_tokens.device) # [spatial_numel_i] + noisy_indexes_i = ( + (noisy_indexes_i * spatial_numel_i).unsqueeze(-1).expand(-1, spatial_numel_i) + ) # [Tn_i,spatial_numel_i] + noisy_indexes_i = noisy_indexes_i.clone() + spatial_indexes_i + start_noisy_index # [Tn_i,spatial_numel_i] + flattened_noisy_frame_indexes.append(noisy_indexes_i.flatten()) # [Tn_i*spatial_numel_i] + start_noisy_index += math.prod(token_shape_i) + + flattened_noisy_frame_indexes = torch.cat(flattened_noisy_frame_indexes, dim=0) # [total_noisy_patches] + + assert packed_tokens.dim() == 2 + assert packed_timestep_embeds.dim() == 2 + assert packed_timestep_embeds.shape[1] == packed_tokens.shape[1] + assert packed_timestep_embeds.shape[0] <= packed_tokens.shape[0] + assert flattened_noisy_frame_indexes.dim() == 1 + assert flattened_noisy_frame_indexes.shape[0] == packed_timestep_embeds.shape[0] + + flattened_noisy_frame_indexes = flattened_noisy_frame_indexes.unsqueeze(-1).expand( + -1, + packed_tokens.shape[1], + ) # [total_noisy_patches,hidden_size] + + return packed_tokens.scatter_add( + dim=0, + index=flattened_noisy_frame_indexes, + src=packed_timestep_embeds, + ) # [total_tokens,hidden_size] diff --git a/cosmos3/_src/vfm/models/mot/domain_aware_linear.py b/cosmos3/_src/vfm/models/mot/domain_aware_linear.py new file mode 100644 index 00000000..d4f86c27 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/domain_aware_linear.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Domain-aware linear layer for multi-embodiment robot learning. + +This module provides a linear layer with domain-conditioned parameters, +where each domain (embodiment) has its own weight and bias vectors. + +Based on the X-VLA implementation: +https://github.com/2toinf/X-VLA/blob/main/models/transformer.py +""" + +import torch +from torch import nn + + +class DomainAwareLinear(nn.Module): + """Linear layer with domain-conditioned parameters (per-sample). + + Each domain has its own weight and bias vectors, stored in embeddings. + During forward pass, weights are retrieved based on per-sample domain IDs. + + This enables learning domain-specific transformations for different robot + embodiments while sharing the overall model architecture. + """ + + def __init__(self, input_size: int, output_size: int, num_domains: int = 50) -> None: + """Initialize the domain-aware linear layer. + + Args: + input_size: Dimension of input features. + output_size: Dimension of output features. + num_domains: Number of domains (embodiments) to support. + """ + super().__init__() + self.input_size = input_size + self.output_size = output_size + self.num_domains = num_domains + + # Store per-domain weights as embeddings: [num_domains, output_size * input_size] + self.fc = nn.Embedding(num_domains, output_size * input_size) + # Store per-domain biases as embeddings: [num_domains, output_size] + self.bias = nn.Embedding(num_domains, output_size) + + # Initialize weights + nn.init.xavier_uniform_(self.fc.weight) + nn.init.zeros_(self.bias.weight) + + def forward(self, x: torch.Tensor, domain_id: torch.LongTensor) -> torch.Tensor: + """Forward pass with domain-specific weights. + + Args: + x: Input tensor of shape [B, I] or [B, T, I] where B is batch size, + T is sequence length, and I is input_size. + domain_id: Domain indices of shape [B], one per sample in the batch. + + Returns: + Output tensor of shape [B, O] or [B, T, O] where O is output_size. + """ + B = domain_id.shape[0] + + # Retrieve per-sample weights: [B, input_size, output_size] + W = self.fc(domain_id).view(B, self.input_size, self.output_size) # [B,input_size,output_size] + + # Retrieve per-sample biases: [B, output_size] + b = self.bias(domain_id).view(B, self.output_size) # [B,output_size] + + if x.dim() == 2: + # 2D input: [B, I] @ [B, I, O] -> [B, O] + return ( + torch.bmm(x.unsqueeze(1), W).squeeze(1) + b + ) # [B,1,input_size] @ [B,input_size,output_size] -> [B,output_size] + else: + # 3D input: [B, T, I] @ [B, I, O] -> [B, T, O] + # Bias [B, O] -> [B, 1, O] for broadcasting + return torch.bmm(x, W) + b.unsqueeze( + 1 + ) # [B,T,input_size] @ [B,input_size,output_size] -> [B,T,output_size] diff --git a/cosmos3/_src/vfm/models/mot/dot_product_attention.py b/cosmos3/_src/vfm/models/mot/dot_product_attention.py new file mode 100644 index 00000000..2a080705 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/dot_product_attention.py @@ -0,0 +1,452 @@ +# Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# See LICENSE for license information. + +""" +Simplified wrapper around TransformerEngine's C++ pytorch backend. +This supports torch.compile(fullgraph=True). +Lowers to cudnn ultimately. +Only bf16 / fp16 is supported. +Only THD layout is supported. +Currently, tensors are made contiguous -- packed th2d, th3d not supported yet. +""" + +import math +from typing import List, Optional, Tuple + +import torch +import transformer_engine + +_TE_VER = tuple(int(x) for x in transformer_engine.__version__.split(".")[:2]) + + +try: + # transformer_engine 2.8.0 + import transformer_engine.pytorch.attention.dot_product_attention.utils as dpa_utils +except ImportError: + # older transformer_engine + import transformer_engine.pytorch.dot_product_attention.utils as dpa_utils # type: ignore + +import transformer_engine_torch as tex +from transformer_engine.pytorch.constants import ( + TE_DType, +) +from transformer_engine.pytorch.cpp_extensions.fused_attn import ( + AttnBiasType, + AttnMaskType, + QKVLayout, +) + +if _TE_VER >= (2, 8): + from transformer_engine.pytorch.cpp_extensions.fused_attn import SoftmaxType + + +__all__ = ["cudnn_fused_attention"] + + +def get_window_size(attn_mask_type: str) -> Tuple[int, int]: + return dpa_utils.check_set_window_size(attn_mask_type) + + +def cudnn_fused_attention( + query_layer: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + key_layer: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + value_layer: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + cu_seqlens_q: torch.Tensor, + cu_seqlens_kv: torch.Tensor, + max_seqlen_q: Optional[int] = None, + max_seqlen_kv: Optional[int] = None, + attn_mask_type: str = "causal", + attention_dropout: float = 0.0, + training: bool = True, +) -> torch.Tensor: # [total_tokens_q,num_heads*head_dim] + """fused attention fprop""" + + deterministic = torch.are_deterministic_algorithms_enabled() + window_size = get_window_size(attn_mask_type) + softmax_scale = 1.0 / math.sqrt(key_layer.shape[-1]) + + output_tensors = cudnn_fused_attn( + training, + max_seqlen_q, + max_seqlen_kv, + cu_seqlens_q, + cu_seqlens_kv, + query_layer, + key_layer, + value_layer, + window_size, + softmax_scale, + attention_dropout if training else 0.0, + attn_mask_type, + deterministic, + ) + output = output_tensors[0] # [total_tokens_q,num_heads,head_dim] + + # ...hd -> ...(hd) + return output.view(*output.shape[:-2], -1) # [total_tokens_q,num_heads*head_dim] + + +BACKEND_F16arb_ELTS_PER_THREADS = 16 + + +@torch.library.custom_op("cosmos3::cudnn_fused_attn", mutates_args=()) +def cudnn_fused_attn( + is_training: bool, + max_seqlen_q: torch.Tensor, + max_seqlen_kv: torch.Tensor, + cu_seqlens_q: torch.Tensor, + cu_seqlens_kv: torch.Tensor, + q: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + k: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + v: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + window_size: List[int], + attn_scale: float, + dropout: float, + attn_mask_type: str, + deterministic: bool, +) -> List[torch.Tensor]: + attn_bias = None + attn_bias_type = "no_bias" + fast_zero_fill = True + softmax_offset = None + softmax_type = "vanilla" + fake_dtype = q.dtype + + rng_elts_per_thread = BACKEND_F16arb_ELTS_PER_THREADS + s_quantizer = None + o_quantizer = None + rng_gen = None + + + # "thd_thd_thd" format requires contiguous tensors. + # We should benchmark thd_th2d / th3d formats as well. + q = q.contiguous() + k = k.contiguous() + v = v.contiguous() + qkv_layout = "thd_thd_thd" + + cu_seqlens_q_padded = cu_seqlens_q + cu_seqlens_kv_padded = cu_seqlens_kv + + args = ( + max_seqlen_q.item(), + max_seqlen_kv.item(), + is_training, + attn_scale, + dropout, + fast_zero_fill, + QKVLayout[qkv_layout], + AttnBiasType[attn_bias_type], + AttnMaskType[attn_mask_type], + ) + + if _TE_VER >= (2, 8): + args += (SoftmaxType[softmax_type],) + + args += ( + tuple(window_size), + cu_seqlens_q, + cu_seqlens_kv, + q, + k, + v, + fake_dtype, + cu_seqlens_q_padded, + cu_seqlens_kv_padded, + None, # page_table_k, + None, # page_table_v, + s_quantizer, + o_quantizer, + attn_bias, + ) + + if _TE_VER >= (2, 8): + args += (softmax_offset,) + + args += ( + rng_gen, + rng_elts_per_thread, + ) + + if _TE_VER >= (2, 9): + # return_max_logit + args += (False,) + + if _TE_VER >= (2, 10): + # is_cuda_graph + args += (False,) + + + # I'd have to create DotProductAttention class and somehow pass it in here, but argument types for these torch.ops are very strict. + # Moreover, back-propagation would still need additional tweaks to work properly. + output_tensors = tex.fused_attn_fwd(*args) + return output_tensors + + +import math + + +def _get_max_tokens(num_tokens: int) -> int: + """ + Quantize token count: + - t = 0, ..., 1024 -> max_t = 1024 + - t = 1025, ..., 32k -> max_t = next power of 2 + - t = 32k+1, ... -> max_t = increment by 32k steps + + Note: translated from transformer_engine/common/fused_attn/utils.cu::get_max_tokens + """ + if num_tokens <= 0: + return 1024 + log2_t = math.ceil(math.log2(num_tokens)) + if log2_t <= 10: + max_t = 1024 + elif log2_t <= 15: + max_t = 2**log2_t + else: + max_t = ((num_tokens + 32767) // 32768) * 32768 + return max_t + + + +# The goal for this function is to return fake tensors of the correct shape and dtype +# without having to run the actual operator. + + +@cudnn_fused_attn.register_fake +def _( + is_training: bool, + max_seqlen_q: torch.Tensor, + max_seqlen_kv: torch.Tensor, + cu_seqlens_q: torch.Tensor, + cu_seqlens_kv: torch.Tensor, + q: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + k: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + v: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + window_size: List[int], + attn_scale: float, + dropout: float, + attn_mask_type: str, + deterministic: bool, +) -> List[torch.Tensor]: + max_tokens = _get_max_tokens(q.shape[0]) + return [ + q.new_empty(tuple(q.shape[:-1]) + (v.shape[-1],)), # [total_tokens_q,num_heads,head_dim] + q.new_empty( + max_tokens, q.shape[1], 1, dtype=torch.float32 + ), # these are the softmax outputs from cudnn; will always be float32 + q.new_empty((2,)), + ] + + +def cudnn_fused_attn_bwd_setup_context(ctx, inputs, output) -> None: + ( + _, # is_training + max_seqlen_q, + max_seqlen_kv, + cu_seqlens_q, + cu_seqlens_kv, + q, + k, + v, + window_size, + attn_scale, + dropout, + attn_mask_type, + deterministic, + ) = inputs + + out = output[0] + aux_ctx_tensors = output[1:] + qkvo_tensors = (q, k, v, out) + + # assume fwd and bwd always use the same high precision, i.e. torch.float16 or torch.bfloat16 + # used when some tensors are base tensors and loose the "dtype" attribute + ctx.nominal_dtype = q.dtype + + ctx.save_for_backward( + *qkvo_tensors, + cu_seqlens_q, + cu_seqlens_kv, + cu_seqlens_q, + cu_seqlens_kv, + *aux_ctx_tensors, + ) + + ctx.max_seqlen_q = max_seqlen_q + ctx.max_seqlen_kv = max_seqlen_kv + ctx.attn_scale = attn_scale + ctx.dropout_p = dropout + ctx.fast_zero_fill = True + ctx.attn_bias_type = "no_bias" + ctx.attn_mask_type = attn_mask_type + ctx.softmax_type = "vanilla" + ctx.window_size = window_size + ctx.deterministic = deterministic + + +@torch.library.custom_op("cosmos3::cudnn_fused_attn_bwd_op", mutates_args=()) +def cudnn_fused_attn_bwd_op( + max_seqlen_q: torch.Tensor, + max_seqlen_kv: torch.Tensor, + attn_scale: float, + dropout: float, + fast_zero_fill: bool, + attn_bias_type: str, + attn_mask_type: str, + softmax_type: str, + window_size: List[int], + deterministic: bool, + cu_seqlens_q: torch.Tensor, + cu_seqlens_kv: torch.Tensor, + q: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + k: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + v: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + out: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + d_out: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + dqkv_nominal_dtype: torch.dtype, + dqkv_te_dtype: torch.dtype, + aux_ctx_tensors: List[torch.Tensor], + cu_seqlens_q_padded: torch.Tensor, + cu_seqlens_kv_padded: torch.Tensor, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: # dq,dk,dv each [total_tokens,num_heads,head_dim] + qkv_layout = "thd_thd_thd" + args = ( + max_seqlen_q.item(), + max_seqlen_kv.item(), + attn_scale, + dropout, + fast_zero_fill, + QKVLayout[qkv_layout], + AttnBiasType[attn_bias_type], + AttnMaskType[attn_mask_type], + ) + + if _TE_VER >= (2, 8): + args += (SoftmaxType[softmax_type],) + + args += ( + window_size, + deterministic, + cu_seqlens_q, + cu_seqlens_kv, + q, + k, + v, + out, + d_out, + dqkv_nominal_dtype, + TE_DType[dqkv_te_dtype], + aux_ctx_tensors, + cu_seqlens_q_padded, + cu_seqlens_kv_padded, + None, # s_quantizer, + None, # dp_quantizer, + None, # dqkv_quantizer, + ) + + if _TE_VER >= (2, 10): + # is_cuda_graph + args += (False,) + + dq, dk, dv, *rest = tex.fused_attn_bwd(*args) + return dq, dk, dv + + +@cudnn_fused_attn_bwd_op.register_fake +def _( + max_seqlen_q: torch.Tensor, + max_seqlen_kv: torch.Tensor, + attn_scale: float, + dropout: float, + fast_zero_fill: bool, + attn_bias_type: str, + attn_mask_type: str, + softmax_type: str, + window_size: List[int], + deterministic: bool, + cu_seqlens_q: torch.Tensor, + cu_seqlens_kv: torch.Tensor, + q: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + k: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + v: torch.Tensor, # [total_tokens_kv,num_heads,head_dim] + out: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + d_out: torch.Tensor, # [total_tokens_q,num_heads,head_dim] + dqkv_nominal_dtype: torch.dtype, + dqkv_te_dtype: torch.dtype, + aux_ctx_tensors: List[torch.Tensor], + cu_seqlens_q_padded: torch.Tensor, + cu_seqlens_kv_padded: torch.Tensor, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: # dq,dk,dv each [total_tokens,num_heads,head_dim] + return torch.empty_like(q), torch.empty_like(k), torch.empty_like(v) + + +def cudnn_fused_attn_bwd_impl(ctx, grad): + d_out, _, _ = grad + d_out = d_out.contiguous() + + ( + q, + k, + v, + out, + cu_seqlens_q, + cu_seqlens_kv, + cu_seqlens_q_padded, + cu_seqlens_kv_padded, + *aux_ctx_tensors, + ) = ctx.saved_tensors + + if not aux_ctx_tensors[0].is_contiguous(): + aux_ctx_tensors[0] = aux_ctx_tensors[0].contiguous() + + with torch.cuda.nvtx.range("FusedAttnFunc.backward"): + # get nominal data type of dq, dk, dv + # FP16/BF16 attention: torch.float16 or torch.bfloat16 + dqkv_nominal_dtype = ctx.nominal_dtype + + # q, k, v, out, d_out, dq, dk, dv: torch.Tensor; torch.float16 or torch.bfloat16 + dq, dk, dv = cudnn_fused_attn_bwd_op( + ctx.max_seqlen_q, + ctx.max_seqlen_kv, + ctx.attn_scale, + ctx.dropout_p, + ctx.fast_zero_fill, + ctx.attn_bias_type, + ctx.attn_mask_type, + ctx.softmax_type, + ctx.window_size, + ctx.deterministic, + cu_seqlens_q, + cu_seqlens_kv, + q, + k, + v, + out, + d_out, + dqkv_nominal_dtype, + d_out.dtype, + aux_ctx_tensors, + cu_seqlens_q_padded, + cu_seqlens_kv_padded, + ) + + output = ( + None, # is_training + None, # max_seqlen_q + None, # max_seqlen_kv + None, # cu_seqlens_q + None, # cu_seqlens_kv + dq, + dk, + dv, + None, # window_size + None, # attn_scale + None, # dropout + None, # attn_mask_type + None, # deterministic + ) + return output + + +cudnn_fused_attn.register_autograd(cudnn_fused_attn_bwd_impl, setup_context=cudnn_fused_attn_bwd_setup_context) diff --git a/cosmos3/_src/vfm/models/mot/modeling_utils.py b/cosmos3/_src/vfm/models/mot/modeling_utils.py new file mode 100644 index 00000000..2896b0c9 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/modeling_utils.py @@ -0,0 +1,407 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Optional + +import numpy as np +import torch +from einops import rearrange, repeat +from torch import nn +from torch.distributed import ProcessGroup +from transformers.activations import ACT2FN + +from cosmos3._src.vfm.datasets.sequence_packing import ModalityData + + +def has_noisy_tokens(modality_data: ModalityData | None) -> bool: + """Check if a modality has valid noisy tokens for loss computation.""" + return ( + modality_data is not None + and modality_data.tokens is not None + and isinstance(modality_data.mse_loss_indexes, torch.Tensor) + and modality_data.mse_loss_indexes.numel() > 0 + ) + + +# -------------------------------------------------------- +# 2D sine-cosine position embedding (flattened) +# References: +# DiT: https://github.com/facebookresearch/DiT/blob/main/models.py +# -------------------------------------------------------- +def get_2d_sincos_pos_embed( + embed_dim: int, grid_size_h: int, grid_size_w: int, cls_token: bool = False, extra_tokens: int = 0 +) -> np.ndarray: + grid_h = np.arange(grid_size_h, dtype=np.float32) + grid_w = np.arange(grid_size_w, dtype=np.float32) + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + + grid = grid.reshape([2, 1, grid_size_h, grid_size_w]) + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token and extra_tokens > 0: + pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0) + return pos_embed + + +def get_2d_sincos_pos_embed_from_grid(embed_dim: int, grid: np.ndarray) -> np.ndarray: + assert embed_dim % 2 == 0 + + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # [H*W,D/2] + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # [H*W,D/2] + + emb = np.concatenate([emb_h, emb_w], axis=1) # [H*W,D] + return emb + + +def get_1d_sincos_pos_embed_from_grid(embed_dim: int, pos: np.ndarray) -> np.ndarray: + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size [M] + out: [M,D] + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=np.float64) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # [D/2] + + pos = pos.reshape(-1) # [M] + out = np.einsum("m,d->md", pos, omega) # [M,D/2], outer product + + emb_sin = np.sin(out) # [M,D/2] + emb_cos = np.cos(out) # [M,D/2] + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # [M,D] + return emb + + +class FlattenedSinCosPositionEmbedding(nn.Module): + # This module creates a flattened sin-cos position embedding for a given number of patches per side. + # Indices are created for 2D array and flattened into 1D array. + + def __init__(self, max_latent_h: int, max_latent_w: int, hidden_size: int, interpolate_pos: bool = False): + super().__init__() + self.max_latent_h = max_latent_h + self.max_latent_w = max_latent_w + self.hidden_size = hidden_size + self.interpolate_pos = interpolate_pos + self.pos_embed = nn.Parameter(torch.zeros(max_latent_h * max_latent_w, hidden_size), requires_grad=False) + self._init_weights() + + def _get_flattened_position_ids_extrapolate(self, latent_dim_h: int, latent_dim_w: int) -> torch.Tensor: + coords_h = torch.arange(0, latent_dim_h) # [H] + coords_w = torch.arange(0, latent_dim_w) # [W] + pos_ids = (coords_h[:, None] * self.max_latent_w + coords_w).flatten() # [H*W] + return pos_ids + + def _get_flattened_position_ids_interpolate(self, latent_dim_h: int, latent_dim_w: int) -> torch.Tensor: + boundaries = torch.arange(1 / self.max_latent_w, 1.0, 1 / self.max_latent_w) # [max_latent_w-1] + fractional_coords_h = torch.arange(0, 1 - 1e-6, 1 / latent_dim_h) # [H] + fractional_coords_w = torch.arange(0, 1 - 1e-6, 1 / latent_dim_w) # [W] + bucket_coords_h = torch.bucketize(fractional_coords_h, boundaries, right=True) # [H] + bucket_coords_w = torch.bucketize(fractional_coords_w, boundaries, right=True) # [W] + pos_ids = (bucket_coords_h[:, None] * self.max_latent_w + bucket_coords_w).flatten() # [H*W] + return pos_ids + + def _create_flattened_position_ids_packed(self, token_shapes_vision: list[tuple[int, int]]) -> torch.Tensor: + flattened_position_ids = [] + for t, h, w in token_shapes_vision: + if self.interpolate_pos: + flattened_position_ids.append(self._get_flattened_position_ids_interpolate(h, w)) # [H*W] + else: + flattened_position_ids.append(self._get_flattened_position_ids_extrapolate(h, w)) # [H*W] + flattened_position_ids_packed = torch.cat(flattened_position_ids, dim=0) # [N_vision] + return flattened_position_ids_packed + + def _init_weights(self): + # Initialize (and freeze) pos_embed by sin-cos embedding: + pos_embed = get_2d_sincos_pos_embed( + embed_dim=self.hidden_size, grid_size_h=self.max_latent_h, grid_size_w=self.max_latent_w + ) + self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float()) + + def forward(self, token_shapes_vision: list[tuple[int, int]], fps: Optional[torch.Tensor] = None) -> torch.Tensor: + # First create 2D index array + flattened_position_ids_packed = self._create_flattened_position_ids_packed(token_shapes_vision) # [N_vision] + return self.pos_embed[flattened_position_ids_packed] # [N_vision,hidden_size] + + +# -------------------------------------------------------- +# 2D / 3D RoPE Position Embedding +# -------------------------------------------------------- + + +class VideoRopePosition3DEmb(nn.Module): + def __init__( + self, + *, # enforce keyword arguments + head_dim: int, + len_h: int, + len_w: int, + len_t: int, + base_fps: int = 24, + base_temporal_compression_factor: int = 4, + temporal_compression_factor: int = 4, + h_extrapolation_ratio: float = 1.0, + w_extrapolation_ratio: float = 1.0, + t_extrapolation_ratio: float = 1.0, + enable_fps_modulation: bool = False, + **kwargs, # used for compatibility with other positional embeddings; unused in this class + ): + del kwargs + super().__init__() + self.base_tps = base_fps / base_temporal_compression_factor + self.temporal_compression_factor = temporal_compression_factor + self.max_h = len_h + self.max_w = len_w + self.max_t = len_t + self.enable_fps_modulation = enable_fps_modulation + dim = head_dim + dim_h = dim // 6 * 2 + dim_w = dim_h + dim_t = dim - 2 * dim_h + assert dim == dim_h + dim_w + dim_t, f"bad dim: {dim} != {dim_h} + {dim_w} + {dim_t}" + + self.register_buffer( + "dim_spatial_range", + torch.arange(0, dim_h, 2)[: (dim_h // 2)].float() / dim_h, + persistent=True, + ) + self.register_buffer( + "dim_temporal_range", + torch.arange(0, dim_t, 2)[: (dim_t // 2)].float() / dim_t, + persistent=True, + ) + self._dim_h = dim_h + self._dim_t = dim_t + + self.h_ntk_factor = h_extrapolation_ratio ** (dim_h / (dim_h - 2)) + self.w_ntk_factor = w_extrapolation_ratio ** (dim_w / (dim_w - 2)) + self.t_ntk_factor = t_extrapolation_ratio ** (dim_t / (dim_t - 2)) + self._init_weights() + + def _init_weights(self) -> None: + dim_h = self._dim_h + dim_t = self._dim_t + + self.dim_spatial_range = ( + torch.arange(0, dim_h, 2)[: (dim_h // 2)].float().to(self.dim_spatial_range.device) / dim_h + ) + self.dim_temporal_range = ( + torch.arange(0, dim_t, 2)[: (dim_t // 2)].float().to(self.dim_spatial_range.device) / dim_t + ) + + def enable_context_parallel(self, process_group: ProcessGroup): + pass + + def disable_context_parallel(self): + pass + + def generate_embeddings( + self, + latent_shape: torch.Size, + input_fps: Optional[torch.Tensor] = None, + h_ntk_factor: Optional[float] = None, + w_ntk_factor: Optional[float] = None, + t_ntk_factor: Optional[float] = None, + start_frame_offset: int = 0, + ): + """ + Generate embeddings for the given input size. + + Args: + latent_shape (torch.Size): Input tensor size (Batch, Time, Height, Width). + input_fps (Optional[torch.Tensor], optional): Frames per second. Defaults to None. + h_ntk_factor (Optional[float], optional): Height NTK factor. If None, uses self.h_ntk_factor. + w_ntk_factor (Optional[float], optional): Width NTK factor. If None, uses self.w_ntk_factor. + t_ntk_factor (Optional[float], optional): Time NTK factor. If None, uses self.t_ntk_factor. + start_frame_offset (int, optional): Offset for frame indices. Use 1 for action embeddings + so that action frame indices start at 1 instead of 0. Defaults to 0. + + Returns: + Not specified in the original code snippet. + """ + if input_fps is not None: + tps = input_fps / self.temporal_compression_factor + else: + tps = None + + h_ntk_factor = h_ntk_factor if h_ntk_factor is not None else self.h_ntk_factor + w_ntk_factor = w_ntk_factor if w_ntk_factor is not None else self.w_ntk_factor + t_ntk_factor = t_ntk_factor if t_ntk_factor is not None else self.t_ntk_factor + assert h_ntk_factor is not None and w_ntk_factor is not None and t_ntk_factor is not None + + h_theta = 10000.0 * h_ntk_factor + w_theta = 10000.0 * w_ntk_factor + t_theta = 10000.0 * t_ntk_factor + + h_spatial_freqs = 1.0 / (h_theta ** self.dim_spatial_range.float()) # [dim_h/2] + w_spatial_freqs = 1.0 / (w_theta ** self.dim_spatial_range.float()) # [dim_w/2] + temporal_freqs = 1.0 / (t_theta ** self.dim_temporal_range.float()) # [dim_t/2] + + B, T, H, W = latent_shape + assert H <= self.max_h and W <= self.max_w, ( + f"Input dimensions (H={H}, W={W}) exceed the maximum dimensions (max_h={self.max_h}, max_w={self.max_w})" + ) + + # Re-allocate buffer if current video needs more indices than what we have for self.seq + # Only rellocate when needed. + max_needed = max(T, H, W) + seq = torch.arange(max_needed, device=self.dim_spatial_range.device, dtype=torch.float) + + half_emb_h = torch.outer(seq[:H], h_spatial_freqs) # [H,dim_h/2] + half_emb_w = torch.outer(seq[:W], w_spatial_freqs) # [W,dim_w/2] + + # Frame indices for the embedding (always 0, 1, 2, ...) + frame_indices = seq[:T] # [T] + + if self.enable_fps_modulation: + uniform_tps = tps is None or tps.shape == (1,) + assert uniform_tps or B == 1 or T == 1, ( + "For video batch, B should be 1 for non-uniform fps. For image batch, T should be 1." + ) + + # apply sequence scaling in temporal dimension + if tps is None: # image case + assert T == 1, "T should be 1 for image batch." + half_emb_t = torch.outer(frame_indices, temporal_freqs) # [T,dim_t/2] + else: + # Calculate scaled time indices + # Apply start_frame_offset to the time calculation (not frame indices) + # This allows one to manipulate the start frame index of embeddings for cross-modality alignment. + scaled_time = (frame_indices + start_frame_offset) / tps[:1] * self.base_tps # [T] + half_emb_t = torch.outer(scaled_time, temporal_freqs) # [T,dim_t/2] + else: + half_emb_t = torch.outer(frame_indices, temporal_freqs) # [T,dim_t/2] + + rope_embed = torch.cat( + [ + repeat(half_emb_t, "t d -> t h w d", h=H, w=W), # [T,H,W,dim_t/2] + repeat(half_emb_h, "h d -> t h w d", t=T, w=W), # [T,H,W,dim_h/2] + repeat(half_emb_w, "w d -> t h w d", t=T, h=H), # [T,H,W,dim_w/2] + ] + * 2, + dim=-1, + ) # [T,H,W,head_dim] + + return rearrange(rope_embed, "t h w d -> (t h w) d").float() # [T*H*W,head_dim] + + def forward( + self, + token_shapes_vision: list[tuple[int, int, int]], + fps: Optional[torch.Tensor] = None, + start_frame_offset: int = 0, + ) -> torch.Tensor: + """ + With CP, the function assume that the input tensor is already split. + It delegates the embedding generation to generate_embeddings function. + + Args: + token_shapes_vision: List of (t, h, w) tuples for each latent. + fps: Frames per second tensor. + start_frame_offset: Offset for frame indices. Use 1 for action embeddings + so that action frame indices start at 1 instead of 0. Defaults to 0. + """ + + embeddings_packed = [] + for i, latent_shape in enumerate(token_shapes_vision): + # latent_shape: (t, h, w) + shape = (1, latent_shape[0], latent_shape[1], latent_shape[2]) + + # Extract FPS for this specific video + video_fps = None + if fps is not None: + assert i < fps.shape[0], f"Index {i} out of bounds for fps tensor of shape {fps.shape}" + video_fps = fps[i : i + 1] + + embeddings = self.generate_embeddings(shape, input_fps=video_fps, start_frame_offset=start_frame_offset) + embeddings_packed.append(embeddings) + + embeddings_packed = torch.cat(embeddings_packed, dim=0) # [N_vision,head_dim] + return embeddings_packed + + @property + def seq_dim(self): + return 0 + + +# -------------------------------------------------------- +# TimestepEmbedder +# Reference: +# DiT: https://github.com/facebookresearch/DiT/blob/main/models.py +# -------------------------------------------------------- +class TimestepEmbedder(nn.Module): + """ + Embeds scalar timesteps into vector representations. + """ + + def __init__(self, hidden_size, frequency_embedding_size=256): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True), + ) + self.frequency_embedding_size = frequency_embedding_size + self.hidden_size = hidden_size + + def _init_weights(self): + std = 1.0 / math.sqrt(self.frequency_embedding_size) + torch.nn.init.trunc_normal_(self.mlp[0].weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.mlp[0].bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.mlp[2].weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.mlp[2].bias) + + @staticmethod + def timestep_embedding(t, dim, max_period=10000): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + half = dim // 2 + freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to( + device=t.device + ) # [D/2] + args = t[:, None].float() * freqs[None] # [N,D/2] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) # [N,D] + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) # [N,D+1] + return embedding + + def forward(self, t): + t_freq = self.timestep_embedding(t, self.frequency_embedding_size) # [N,frequency_embedding_size] + t_emb = self.mlp(t_freq) # [N,hidden_size] + return t_emb + + +class MLPconnector(nn.Module): + def __init__(self, in_dim: int, out_dim: int, hidden_act: str): + super().__init__() + self.activation_fn = ACT2FN[hidden_act] + self.fc1 = nn.Linear(in_dim, out_dim) + self.fc2 = nn.Linear(out_dim, out_dim) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.fc1(hidden_states) # [N,out_dim] + hidden_states = self.activation_fn(hidden_states) # [N,out_dim] + hidden_states = self.fc2(hidden_states) # [N,out_dim] + return hidden_states diff --git a/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py b/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py new file mode 100644 index 00000000..9682264a --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn as nn +from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + checkpoint_wrapper as ptd_checkpoint_wrapper, +) +from torch.distributed.fsdp import fully_shard + +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig +from cosmos3._src.vfm.utils.parallelism import ParallelDims + + +def apply_ac(model: nn.Module): + """Apply activation checkpointing to the model.""" + + for layer_id, block in model.model.layers.named_children(): + block = ptd_checkpoint_wrapper(block, preserve_rng_state=True) + model.model.layers.register_module(layer_id, block) + + +def apply_compile(model: nn.Module, config: ParallelismConfig): + """ + Apply torch.compile to each TransformerBlock, which makes compilation efficient due to + repeated structure. Alternatively one can compile the whole model (after applying DP). + """ + compile_options = {} + if config.max_autotune_pointwise: + compile_options["max_autotune_pointwise"] = True + if config.coordinate_descent_tuning: + compile_options["coordinate_descent_tuning"] = True + + for layer_id, block in model.model.layers.named_children(): + block = torch.compile( + block, + fullgraph=True, + dynamic=config.compile_dynamic, + mode="reduce-overhead" if config.use_cuda_graphs else None, + options=compile_options or None, + ) + model.model.layers.register_module(layer_id, block) + + +def apply_fsdp( + model: nn.Module, + parallel_dims: ParallelDims, +): + """ + Apply data parallelism (via FSDP2) to the model. + + Args: + model (nn.Module): The model to apply data parallelism to. + parallel_dims (ParallelDims): The device mesh to use for data parallelism and expert parallel. + """ + for _, block in model.model.layers.named_children(): + fully_shard(block, mesh=parallel_dims.dp_mesh) + + +def parallelize_unified_mot( + model: nn.Module, + parallel_dims: ParallelDims | None, + config: ParallelismConfig, +) -> nn.Module: + """Optimize the model using FSDP, activation checkpointing, and torch.compile. + + FSDP reduces memory usage by sharding the model parameters across multiple GPUs. + Activation checkpointing reduces memory usage by selectively checkpointing only + the outputs of each layer. Torch.compile compiles the model for faster training. + """ + if config.use_activation_checkpointing: + apply_ac(model) + if config.use_torch_compile: + apply_compile(model, config) + if parallel_dims is not None and parallel_dims.dp_enabled: + apply_fsdp(model, parallel_dims) + return model diff --git a/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py b/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py new file mode 100644 index 00000000..f5eec1ad --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable + +import torch +from torch.distributed.fsdp import fully_shard +from torch.nn.attention.flex_attention import BlockMask + +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig +from cosmos3._src.vfm.datasets.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, +) +from cosmos3._src.vfm.models.mot.attention import SplitInfo, dispatch_attention +from cosmos3._src.vfm.models.mot.context_parallel_utils import context_parallel_attention +from cosmos3._src.vfm.models.mot.parallelize_unified_mot import parallelize_unified_mot +from cosmos3._src.vfm.models.utils.memory import KVToStore, MemoryValue +from cosmos3._src.vfm.utils.parallelism import ParallelDims + + +class ContextParallelDispatch(torch.nn.Module): + """CP-aware wrapper for the installed attention dispatch function. + + Installed on ``PackedAttentionMoT.dispatch_attention_fn`` when context + parallelism is enabled, replacing whatever dispatch function was there + previously. The call signature of :meth:`forward` matches + ``dispatch_attention`` so the two are interchangeable. + + All paths delegate to :func:`context_parallel_attention`, which wraps + the inner ``wrapped_dispatch`` with Ulysses-style all-to-all + communication. This includes the AR frame 1+ gen-only path — the inner + dispatch routes to ``attention_AR_gen_only`` which operates on the + head-sharded tensors produced by the all-to-all. + + All cache writes flow through the ``MemoryState`` interface; neither this + class nor the CP attention functions write to the cache directly. + """ + + def __init__( + self, + cp_mesh, + wrapped_dispatch: Callable = dispatch_attention, + ): + super().__init__() + self.cp_mesh = cp_mesh + self.wrapped_dispatch = wrapped_dispatch + + def forward( + self, + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask | SplitInfo, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + ) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + if memory_value is not None and not memory_value.supports_context_parallel_attention: + raise ValueError("Context-parallel doesn't work when training with a KV-cache.") + + return context_parallel_attention( + self.cp_mesh, + packed_query_states, + packed_key_states, + packed_value_states, + attention_mask, + attention_function=self.wrapped_dispatch, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + +def apply_compile(model: torch.nn.Module, config: ParallelismConfig): + """Apply torch.compile to the VFM encode/decode heads. + + The MoT-side ``compile_dynamic`` knob on ``ParallelismConfig`` intentionally + does **not** propagate here. The VFM encode/decode paths have no graph + breaks and their input shapes are stable across a prompt, so we always + trace them as a single dynamic graph (``fullgraph=True, dynamic=True``). + This keeps AR inference (which sets ``compile_dynamic=False`` on MoT for + shape-specialized kernels) from accidentally regressing the VFM compile. + """ + + inductor_options = {} + if config.max_autotune_pointwise: + inductor_options["max_autotune_pointwise"] = True + if config.coordinate_descent_tuning: + inductor_options["coordinate_descent_tuning"] = True + + compile_options = { + "fullgraph": True, + "dynamic": True, + "mode": "reduce-overhead" if config.use_cuda_graphs else None, + "options": inductor_options or None, + } + + model._encode_text = torch.compile(model._encode_text, **compile_options) + model._encode_vision = torch.compile(model._encode_vision, **compile_options) + model._encode_action = torch.compile(model._encode_action, **compile_options) + model._decode_vision = torch.compile(model._decode_vision, **compile_options) + model._decode_action = torch.compile(model._decode_action, **compile_options) + return model + + +def context_parallel_unified_mot( + model: torch.nn.Module, + parallel_dims: ParallelDims | None, +) -> torch.nn.Module: + for i in range(len(model.model.layers)): + attn = model.model.layers[i].self_attn + cp_dispatch = ContextParallelDispatch( + parallel_dims.cp_mesh, + wrapped_dispatch=attn.dispatch_attention_fn, + ) + attn.dispatch_attention_fn = cp_dispatch + attn.cp_mesh = parallel_dims.cp_mesh + + return model + + +def parallelize_vfm_network( + model: torch.nn.Module, + parallel_dims: ParallelDims | None, + config: ParallelismConfig, +) -> torch.nn.Module: + """Optimize the model using FSDP, CP, activation checkpointing, and torch.compile. + + FSDP reduces memory usage by sharding the model parameters across multiple GPUs. + Activation checkpointing reduces memory usage by selectively checkpointing only + the outputs of each layer. Torch.compile compiles the model for faster training. + """ + if parallel_dims is not None and parallel_dims.cp_enabled: + model.parallel_dims = parallel_dims + model.language_model = context_parallel_unified_mot( + model.language_model, + parallel_dims=parallel_dims, + ) + + model.language_model = parallelize_unified_mot( + model.language_model, + parallel_dims=parallel_dims, + config=config, + ) + + if config.use_torch_compile and config.compiled_region == "all": + model = apply_compile(model, config) + + if parallel_dims is not None and parallel_dims.dp_enabled: + # Collect parameters to ignore during FSDP wrapping + ignored_params = set() + if model.latent_pos_embed is not None: + ignored_params.update(model.latent_pos_embed.parameters()) + + model = fully_shard( + module=model, + mesh=parallel_dims.dp_mesh, + ignored_params=ignored_params, + ) + + return model diff --git a/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py b/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py new file mode 100644 index 00000000..06c4ea63 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Backward-compatibility shim: this module was renamed to unified_mot.py. +# Existing serialized configs / checkpoints may reference the old module path, +# so we re-export everything from the new location. +from cosmos3._src.vfm.models.mot.unified_mot import * # noqa: F401, F403 +from cosmos3._src.vfm.models.mot.unified_mot import ( # noqa: F401 # explicit re-exports for type checkers + LayerTypes, + MoTDecoderLayer, + Nemotron3DenseVLTextConfig, + Nemotron3DenseVLTextForCausalLM, + Nemotron3DenseVLTextModel, + PackedAttentionMoT, + Qwen3VLMoeTextConfig, + Qwen3VLMoeTextForCausalLM, + Qwen3VLMoeTextModel, + Qwen3VLTextConfig, + Qwen3VLTextForCausalLM, + Qwen3VLTextModel, + Qwen3VLTextMoTDecoderLayer, +) diff --git a/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py b/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py new file mode 100644 index 00000000..30f9cd96 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py @@ -0,0 +1,206 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for generating 3D multi-modal RoPE (mRoPE) position IDs. + +3D mRoPE uses three axes (temporal, height, width) for position embedding, +following the Qwen3VL design for multi-modal RoPE: + +- **Text tokens**: All three axes share the same monotonically increasing position IDs. + For example: (0,0,0), (1,1,1), (2,2,2), ... +- **Vision tokens** (image/video latents): Creates a local 3D grid (T, H, W) with a + temporal offset. For each frame t in [0, T), for each row h in [0, H), for each + column w in [0, W), the position is (temporal_offset + t, h_offset, w_offset). + +The ``reset_spatial_indices`` flag controls spatial axis behavior: +- ``True`` (default): Spatial (H, W) indices start from 0 for each vision segment, + giving the model absolute spatial position within each image/video. +- ``False`` (Qwen2VL-style): All axes are offset by ``temporal_offset``. + +After each segment, the ``temporal_offset`` is updated to ``max(all_positions) + 1`` +(Qwen3VL design), ensuring subsequent segments start at a non-overlapping position. + +**FPS Modulation** (optional): +When ``fps`` is provided, the temporal position IDs are scaled to reflect real time +rather than just frame indices. The formula is: + scaled_time = (frame_index + start_frame_offset) / tps * base_tps +where: + tps = fps / temporal_compression_factor + base_tps = base_fps / base_temporal_compression_factor + +This ensures that videos with different FPS values have comparable temporal position +embeddings, allowing the model to understand temporal relationships across different +video sources. +""" + +import math + +import torch + + +def get_3d_mrope_ids_text_tokens( + num_tokens: int, + temporal_offset: int | float, + use_float_positions: bool = False, +) -> tuple[torch.Tensor, int | float]: + """Generate 3D mRoPE position IDs for text tokens. + + For text tokens, all three axes (temporal, height, width) share the same + monotonically increasing position IDs, starting from ``temporal_offset``. + + Args: + num_tokens: Number of text tokens. + temporal_offset: Current temporal offset to start from. Can be float when + FPS modulation is enabled for vision tokens. + use_float_positions: If ``True``, generate float position IDs (for consistency + with FPS-modulated vision tokens). If ``False``, generate integer IDs. + + Returns: + Tuple of: + - Position IDs tensor of shape ``(3, num_tokens)`` where each row is identical. + - Updated temporal offset (``temporal_offset + num_tokens``). + """ + if use_float_positions: + # Float mode: for consistency with FPS-modulated vision tokens + ids = torch.arange(num_tokens, dtype=torch.float32) + temporal_offset # [num_tokens] + else: + # Integer mode (default) + ids = torch.arange(num_tokens, dtype=torch.long) + int(temporal_offset) # [num_tokens] + + mrope_ids = ids.unsqueeze(0).expand(3, -1).contiguous() # [3,num_tokens] + next_temporal_offset = temporal_offset + num_tokens + return mrope_ids, next_temporal_offset + + +def get_3d_mrope_ids_vae_tokens( + grid_t: int, + grid_h: int, + grid_w: int, + temporal_offset: int | float, + reset_spatial_indices: bool = True, + fps: float | None = None, + base_fps: float = 24.0, + temporal_compression_factor: int = 4, + base_temporal_compression_factor: int | None = None, + start_frame_offset: int = 0, +) -> tuple[torch.Tensor, int | float]: + """Generate 3D mRoPE position IDs for VAE vision tokens (image/video latents). + + Creates a 3D position grid for vision tokens with shape ``(T, H, W)``, then flattens + to produce position IDs for each axis. The flattening order is T-major: + for each temporal frame, iterate over height then width. + + Args: + grid_t: Number of temporal frames in the latent grid. + grid_h: Height of the latent grid (after patchification). + grid_w: Width of the latent grid (after patchification). + temporal_offset: Current temporal offset. Always applied to the temporal axis. + When ``reset_spatial_indices=False``, also applied to spatial axes. + Can be float when FPS modulation is enabled. + reset_spatial_indices: If ``True``, spatial (height, width) indices start from 0 + for each vision segment, giving the model absolute spatial position + within each image/video. If ``False``, spatial indices are also offset by + ``temporal_offset`` (Qwen2VL-style behavior). + fps: Frames per second of the video. ``None`` disables fps modulation + (integer positions); pass the real fps for fps-scaled, possibly + fractional positions. Honored at grid_t=1 too (per-frame AR packs), + where it collapses to ``scaled_t[0] = temporal_offset``. + base_fps: Base FPS for normalization. Default is 24.0. + temporal_compression_factor: VAE temporal compression factor. Default is 4. + base_temporal_compression_factor: Base temporal compression factor. If ``None``, + defaults to ``temporal_compression_factor`` (typical case where base matches actual). + start_frame_offset: Offset added to frame indices before FPS scaling. + Use 1 for action embeddings so they start at frame 1 instead of 0. + + Returns: + Tuple of: + - Position IDs tensor of shape ``(3, grid_t * grid_h * grid_w)``. + Row 0: temporal axis (float if FPS modulation enabled, else long). + Row 1: height axis (long), Row 2: width axis (long). + - Updated temporal offset for the next segment. When FPS modulation is + enabled, this is a float representing the next scaled time position. + Otherwise, it's ``max(all_positions) + 1`` (Qwen3VL design). + """ + # Enabled whenever fps is provided, including grid_t=1 (per-frame AR packs). + # Callers that want integer positions (e.g. images) pass fps=None. + fps_modulation_enabled = fps is not None + + # Default base_temporal_compression_factor to temporal_compression_factor if not specified + effective_base_tcf = ( + base_temporal_compression_factor + if base_temporal_compression_factor is not None + else temporal_compression_factor + ) + + if fps_modulation_enabled: + # FPS modulation: scale temporal indices to reflect real time + # tps = tokens per second (fps divided by temporal compression) + # base_tps = base tokens per second + tps = fps / temporal_compression_factor + base_tps = base_fps / effective_base_tcf + + # Frame indices: 0, 1, 2, ..., grid_t-1 + frame_indices = torch.arange(grid_t, dtype=torch.float32) # [grid_t] + + # Apply FPS scaling: scaled_time = (frame_index + start_frame_offset) / tps * base_tps + scaled_t = (frame_indices + start_frame_offset) / tps * base_tps + temporal_offset # [grid_t] + + # Expand temporal indices for all spatial positions + t_index = scaled_t.view(-1, 1).expand(-1, grid_h * grid_w).flatten() # [grid_t*grid_h*grid_w] + t_dtype = torch.float32 + else: + # No FPS modulation: use integer frame indices + # Apply start_frame_offset for cross-modality alignment (e.g., action tokens start at frame 1) + t_index = ( + ( + torch.arange(grid_t, dtype=torch.long).view(-1, 1).expand(-1, grid_h * grid_w).flatten() + ) # [grid_t*grid_h*grid_w] + + int(temporal_offset) + + start_frame_offset + ) + t_dtype = torch.long + + # Height axis: for each temporal frame, cycles through h values, each repeated w times + h_index = ( + torch.arange(grid_h, dtype=torch.long).view(1, -1, 1).expand(grid_t, -1, grid_w).flatten() + ) # [grid_t*grid_h*grid_w] + + # Width axis: for each temporal frame and height, cycles through w values + w_index = ( + torch.arange(grid_w, dtype=torch.long).view(1, 1, -1).expand(grid_t, grid_h, -1).flatten() + ) # [grid_t*grid_h*grid_w] + + if not reset_spatial_indices: + # Qwen2VL-style: offset all axes by temporal_offset (use int for spatial) + spatial_offset = int(temporal_offset) + h_index = h_index + spatial_offset # [grid_t*grid_h*grid_w] + w_index = w_index + spatial_offset # [grid_t*grid_h*grid_w] + + # Stack into (3, T*H*W) tensor + # Note: When FPS modulation is enabled, temporal axis is float, spatial axes are long + # We convert h_index and w_index to the same dtype as t_index for stacking + if fps_modulation_enabled: + mrope_ids = torch.stack( + [t_index, h_index.to(torch.float32), w_index.to(torch.float32)], dim=0 + ) # [3,grid_t*grid_h*grid_w] + else: + mrope_ids = torch.stack([t_index, h_index, w_index], dim=0) # [3,grid_t*grid_h*grid_w] + + # Compute next temporal offset: max position + 1 + # Use the actual computed positions to handle FPS modulation correctly + max_position = mrope_ids.max().item() + next_temporal_offset = math.ceil(max_position) + 1 + + return mrope_ids, next_temporal_offset diff --git a/cosmos3/_src/vfm/models/mot/unified_mot.py b/cosmos3/_src/vfm/models/mot/unified_mot.py new file mode 100644 index 00000000..a649f560 --- /dev/null +++ b/cosmos3/_src/vfm/models/mot/unified_mot.py @@ -0,0 +1,1041 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from dataclasses import dataclass +from typing import Optional, Tuple + +import torch +from torch import nn +from transformers.utils import ModelOutput + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.sequence_packing import ( + FactoredSequencePack, + from_joint, + from_und_gen_splits, + get_device_and_dtype, + get_gen_seq, + get_und_seq, + set_gen_seq, + set_und_seq, + zeros_like, +) +from cosmos3._src.vfm.models.mot.attention import ( + AttentionMaskType, + dispatch_attention, +) +from cosmos3._src.vfm.models.utils.memory import KVToStore, MemoryState, MemoryValue + +# Nemotron 3 Dense VL imports +from cosmos3._src.vfm.models.vlm.nemotron_3_dense_vl.configuration_nemotron_3_dense_vl import ( + Nemotron3DenseVLTextConfig as _Nemotron3DenseVLTextConfig, +) +from cosmos3._src.vfm.models.vlm.nemotron_3_dense_vl.nemotron_3_dense_vl import ( + MultiModalRotaryEmbedding, + Nemotron3DenseVLMLP, + Nemotron3DenseVLPreTrainedModel, + Nemotron3DenseVLRMSNorm, + apply_rotary_pos_emb_partial, +) + +# Qwen3-VL imports +from cosmos3._src.vfm.models.vlm.qwen3_vl.configuration_qwen3_vl import ( + Qwen3VLTextConfig as _Qwen3VLTextConfig, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl.qwen3_vl import ( + Qwen3VLPreTrainedModel, + Qwen3VLTextMLP, + Qwen3VLTextRMSNorm, + Qwen3VLTextRotaryEmbedding, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl.qwen3_vl import ( + apply_rotary_pos_emb as qwen3_vl_apply_rotary_pos_emb, +) + +# Qwen3-VL-MoE imports +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeTextConfig as _Qwen3VLMoeTextConfig, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.qwen3_vl_moe import ( + LBLMetadata, + Qwen3VLMoePreTrainedModel, + Qwen3VLMoeTextMLP, + Qwen3VLMoeTextRMSNorm, + Qwen3VLMoeTextRotaryEmbedding, + Qwen3VLMoeTextSparseMoeBlock, +) + +# Torch optimization settings +torch._dynamo.config.cache_size_limit = 512 +torch._dynamo.config.accumulated_cache_size_limit = 4096 + +# ----------------------------------------------------------------------------- +# Unified MoT (Mixture of Transformers) implementation supporting: +# - Qwen3-VL Dense, Qwen3-VL MoE, and Nemotron 3 Dense VL +# +# Shared components: +# - PackedAttentionMoT (config-driven QK norm and RoPE) +# - MoTDecoderLayer (used by all variants) +# - _impl_* (shared init/forward) +# +# Variant-specific wrapper classes are needed for different PreTrainedModel bases. +# Sub-layer classes (MLP, RMSNorm, RotaryEmbedding, RoPE fn) are selected via LayerTypes. +# ----------------------------------------------------------------------------- + + +class LayerTypes: + def __init__(self, variant: str): + self.variant = variant + if variant == "qwen3_vl_moe": + self.mlp = Qwen3VLMoeTextMLP + self.rms_norm = Qwen3VLMoeTextRMSNorm + self.rotary_embedding = Qwen3VLMoeTextRotaryEmbedding + self.apply_rotary_pos_emb = qwen3_vl_apply_rotary_pos_emb + elif variant == "nemotron_dense": + self.mlp = Nemotron3DenseVLMLP + self.rms_norm = Nemotron3DenseVLRMSNorm + self.rotary_embedding = MultiModalRotaryEmbedding + self.apply_rotary_pos_emb = apply_rotary_pos_emb_partial + elif variant == "qwen3_vl_dense": + self.mlp = Qwen3VLTextMLP + self.rms_norm = Qwen3VLTextRMSNorm + self.rotary_embedding = Qwen3VLTextRotaryEmbedding + self.apply_rotary_pos_emb = qwen3_vl_apply_rotary_pos_emb + else: + raise ValueError(f"Unknown LayerTypes variant: {variant!r}") + + @property + def is_moe(self) -> bool: + return self.variant == "qwen3_vl_moe" + + +class NaiveCache: + def __init__(self, num_layers): + self.key_cache = {k: None for k in range(num_layers)} + self.value_cache = {k: None for k in range(num_layers)} + + @property + def num_layers(self): + return len(self.key_cache) + + @property + def seq_lens(self): + if self.key_cache[0] is not None: + return self.key_cache[0].shape[0] + else: + return 0 + + +@dataclass +class BaseOutputWithPast(ModelOutput): + packed_query_sequence: torch.FloatTensor = None + past_key_values: Optional[NaiveCache] = None + + +# Qwen3-VL MoT (Mixture of Tokens) implementation +# Combines Qwen3-VL vision-language capabilities with MoT dual-pathway architecture + + +class Qwen3VLTextConfig(_Qwen3VLTextConfig): + r""" + Qwen3VLTextConfig with MoT-specific parameters. + Extends Qwen3VLTextConfig for text component MoT support with comprehensive configuration. + """ + + def __init__( + self, + # MoT-specific parameters with comprehensive defaults + qk_norm_for_text: bool = False, # Whether to apply QK norm in the understanding (text) pathway + qk_norm_for_diffusion: bool = True, # Whether to apply QK norm in the generation (diffusion) pathway + freeze_und: bool = False, # Freeze understanding pathway + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = True, + **kwargs, + ): + # Store MoT-specific parameters + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file): + """ + Enhanced from_json_file that handles both nested and flat configs. + + For nested configs (with text_config section), extracts the text_config. + For flat configs, loads directly. + """ + + # Load the raw JSON + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + + # Check if this is a nested config with text_config section + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + # Extract the text_config section for nested configs + log.debug("Detected nested config, extracting text_config section") + config_dict = config_dict["text_config"] + else: + # Use the config as-is for flat configs + log.debug("Detected flat config, using directly") + + # Create config from the (potentially extracted) dict + return cls(**config_dict) + + +# Qwen3-VL-MoE MoT (Mixture of Tokens) implementation +# Combines Qwen3-VL-MoE vision-language capabilities with MoT dual-pathway architecture + + +class Qwen3VLMoeTextConfig(_Qwen3VLMoeTextConfig): + r""" + Qwen3VLMoeTextConfig with MoT-specific parameters. + Extends Qwen3VLMoeTextConfig for text component MoT support with comprehensive configuration. + """ + + def __init__( + self, + # MoT-specific parameters with comprehensive defaults + qk_norm_for_text: bool = False, # Whether to apply QK norm in the understanding (text) pathway + qk_norm_for_diffusion: bool = True, # Whether to apply QK norm in the generation (diffusion) pathway + freeze_und: bool = False, # Freeze understanding pathway + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = True, + **kwargs, + ): + # Store MoT-specific parameters + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file): + """ + Enhanced from_json_file that handles both nested and flat configs. + For nested configs (with text_config section), extracts the text_config. + For flat configs, loads directly. + """ + + # Load the raw JSON + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + + # Check if this is a nested config with text_config section + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + # Extract the text_config section for nested configs + log.debug("Detected nested config, extracting text_config section") + config_dict = config_dict["text_config"] + else: + # Use the config as-is for flat configs + log.debug("Detected flat config, using directly") + + # Create config from the (potentially extracted) dict + return cls(**config_dict) + + +# Nemotron 3 Dense VL MoT config + +_NEMOTRON_MOT_TEXT_CONFIG_KEYS = { + "vocab_size", + "tie_word_embeddings", + "hidden_size", + "intermediate_size", + "num_hidden_layers", + "num_attention_heads", + "head_dim", + "num_key_value_heads", + "mlp_hidden_act", + "attention_bias", + "mlp_bias", + "initializer_range", + "layer_norm_epsilon", + "residual_in_fp32", + "use_cache", + "num_logits_to_keep", + "pad_token_id", + "bos_token_id", + "eos_token_id", + "sliding_window", + "max_position_embeddings", + "attention_dropout", + "hidden_dropout", + "enable_rope", + "rope_scaling", + "rope_theta", + "enable_mrope", + "mrope_section", + "torch_dtype", +} + + +class Nemotron3DenseVLTextConfig(_Nemotron3DenseVLTextConfig): + """MoT-enabled config for the Nemotron 3 Dense VL text backbone. + + Extends the upstream ``Nemotron3DenseVLTextConfig`` with MoT-specific + fields (per-pathway QK normalisation, freeze control, decoder layer class). + Supports both the VLM nested config and the flat LLM config format. + """ + + def __init__( + self, + qk_norm_for_text: bool = False, + qk_norm_for_diffusion: bool = True, + freeze_und: bool = False, + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = False, + **kwargs, + ) -> None: + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file: str) -> "Nemotron3DenseVLTextConfig": + """Load config from a JSON file, handling both VLM nested and flat LLM formats.""" + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + log.debug("Detected nested config, extracting text_config section") + config_dict = dict(config_dict["text_config"]) + else: + log.debug("Detected flat config, using directly") + if config_dict.get("num_hidden_layers") == 56: + # Upstream VLM stores attention and MLP as separate alternating blocks (56 total); + # MoT combines both into standard transformer layers (28 total). + config_dict = {**config_dict, "num_hidden_layers": 28} + filtered = {k: v for k, v in config_dict.items() if k in _NEMOTRON_MOT_TEXT_CONFIG_KEYS} + return cls(**filtered) + + +# ----------------------------------------------------------------------------- +# Common layers between Qwen3VL Dense, MoE, and Nemotron 3 Dense VL models +# ----------------------------------------------------------------------------- + + +class PackedAttentionMoT(nn.Module): + """ + Dual-pathway packed attention for MoT architectures. + Implements understanding and generation pathways with separate projections. + + Used for Qwen3VL (Dense), Qwen3VL-MoE, and Nemotron 3 Dense VL variants. + QK normalisation and RoPE function are selected via ``layer_types`` and config + attributes (``qk_norm_for_text`` / ``qk_norm_for_diffusion``). + """ + + def __init__(self, config, layer_idx: int, layer_types: LayerTypes): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.hidden_size = config.hidden_size + self.num_attention_heads = config.num_attention_heads + self.num_key_value_heads = config.num_key_value_heads + self.num_key_value_groups = self.num_attention_heads // self.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + + eps = config.rms_norm_eps + + # Understanding pathway projections + self.q_proj = nn.Linear(self.hidden_size, self.num_attention_heads * self.head_dim, bias=config.attention_bias) + self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) + self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) + self.o_proj = nn.Linear(self.num_attention_heads * self.head_dim, self.hidden_size, bias=config.attention_bias) + + # Understanding pathway QK norm + if config.qk_norm_for_text: + self.q_norm = layer_types.rms_norm(self.head_dim, eps=eps) + self.k_norm = layer_types.rms_norm(self.head_dim, eps=eps) + else: + self.q_norm = nn.Identity() + self.k_norm = nn.Identity() + + # Generation pathway QK norm + if config.qk_norm_for_diffusion: + self.q_norm_moe_gen = layer_types.rms_norm(self.head_dim, eps=eps) + self.k_norm_moe_gen = layer_types.rms_norm(self.head_dim, eps=eps) + else: + self.q_norm_moe_gen = nn.Identity() + self.k_norm_moe_gen = nn.Identity() + + # Generation pathway linear projections + self.q_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj_moe_gen = nn.Linear( + self.num_attention_heads * self.head_dim, self.hidden_size, bias=config.attention_bias + ) + + self._apply_rotary_pos_emb = layer_types.apply_rotary_pos_emb + self.dispatch_attention_fn = dispatch_attention + self.cp_mesh = None + + def forward( + self, + pack: FactoredSequencePack, + attention_mask: AttentionMaskType, + packed_position_embeddings: Tuple[FactoredSequencePack, FactoredSequencePack], + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + ) -> tuple[FactoredSequencePack, KVToStore | None]: + """Forward pass with optional memory-augmented attention. + + When ``memory_value`` is provided, ``dispatch_attention_fn`` routes to + the appropriate attention kernel (e.g. three-way KV-cache attention + for training, or AR inference concat + dense attention). + + ``kv_to_store`` is produced when ``memory_value`` is present: + ``(gen_k, gen_v, und_k, und_v)`` for the caller to write back via + ``MemoryState.write_for_layer()``. The tensors are passed with + gradients attached; each ``MemoryState`` decides whether to detach + (e.g. for truncated BPTT) or keep gradients (e.g. teacher forcing). + + Args: + pack: Packed sequence with und/gen tokens + attention_mask: Attention mask (BlockMask or SplitInfo) + packed_position_embeddings: RoPE embeddings (cos, sin) + natten_metadata: Optional NATTEN metadata for neighborhood attention. + memory_value: Optional read-only tensor container for memory-augmented attention. + """ + + q_und_in = self.q_proj(get_und_seq(pack)) # [N_und,num_heads*head_dim] + q_gen_in = self.q_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_heads*head_dim] + + k_und_in = self.k_proj(get_und_seq(pack)) # [N_und,num_kv_heads*head_dim] + k_gen_in = self.k_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_kv_heads*head_dim] + + v_und_in = self.v_proj(get_und_seq(pack)) # [N_und,num_kv_heads*head_dim] + v_gen_in = self.v_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_kv_heads*head_dim] + + q_und = q_und_in.view(-1, self.num_attention_heads, self.head_dim) # [N_und,num_heads,head_dim] + k_und = k_und_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_und,num_kv_heads,head_dim] + v_und = v_und_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_und,num_kv_heads,head_dim] + + q_gen = q_gen_in.view(-1, self.num_attention_heads, self.head_dim) # [N_gen,num_heads,head_dim] + k_gen = k_gen_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_gen,num_kv_heads,head_dim] + v_gen = v_gen_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_gen,num_kv_heads,head_dim] + + q_und = self.q_norm(q_und) # [N_und,num_heads,head_dim] + k_und = self.k_norm(k_und) # [N_und,num_kv_heads,head_dim] + + q_gen = self.q_norm_moe_gen(q_gen) # [N_gen,num_heads,head_dim] + k_gen = self.k_norm_moe_gen(k_gen) # [N_gen,num_kv_heads,head_dim] + + if self.config.freeze_und: + q_und = q_und.detach() + k_und = k_und.detach() + v_und = v_und.detach() + + packed_cos = packed_position_embeddings[0] + packed_sin = packed_position_embeddings[1] + + q_und_, k_und_ = self._apply_rotary_pos_emb( + q_und, + k_und, + get_und_seq(packed_cos), + get_und_seq(packed_sin), + unsqueeze_dim=1, + ) # q_und_: [N_und,num_heads,head_dim], k_und_: [N_und,num_kv_heads,head_dim] + q_gen_, k_gen_ = self._apply_rotary_pos_emb( + q_gen, + k_gen, + get_gen_seq(packed_cos), + get_gen_seq(packed_sin), + unsqueeze_dim=1, + ) # q_gen_: [N_gen,num_heads,head_dim], k_gen_: [N_gen,num_kv_heads,head_dim] + + packed_query_states_ = from_und_gen_splits(q_und_, q_gen_, pack) # [N_und+N_gen,num_heads,head_dim] + packed_key_states_ = from_und_gen_splits(k_und_, k_gen_, pack) # [N_und+N_gen,num_kv_heads,head_dim] + packed_value_states_ = from_und_gen_splits(v_und, v_gen, pack) # [N_und+N_gen,num_kv_heads,head_dim] + + packed_attn_output, kv_to_store = self.dispatch_attention_fn( + packed_query_states_, + packed_key_states_, + packed_value_states_, + attention_mask, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + # Produce kv_to_store for MemoryState.write_for_layer() when the + # dispatch didn't already provide one (e.g. standard or AR frame-0 + # non-CP paths). CP dispatch returns head-sharded kv_to_store + # directly, so kv_to_store is already non-None in that case. + # + # Gradient detach is NOT done here; each MemoryState.write_for_layer() + # decides its own gradient policy (e.g. detach for truncated BPTT, + # keep gradients for teacher forcing). + if memory_value is not None and kv_to_store is None: + und_len = pack["_num_causal_tokens"] + gen_len = pack["_num_full_tokens"] + kv_to_store = ( + k_gen_[:gen_len].unsqueeze(0), + v_gen[:gen_len].unsqueeze(0), + k_und_[:und_len].unsqueeze(0), + v_und[:und_len].unsqueeze(0), + ) + + # Apply projections directly to get final results + und_seq = self.o_proj(get_und_seq(packed_attn_output)) # [N_und,hidden_size] + gen_seq = self.o_proj_moe_gen(get_gen_seq(packed_attn_output)) # [N_gen,hidden_size] + return from_und_gen_splits(und_seq, gen_seq, pack), kv_to_store # [N_und+N_gen,hidden_size] + + +def _impl_init( + self, config: Qwen3VLTextConfig | Qwen3VLMoeTextConfig | Nemotron3DenseVLTextConfig, layer_types: LayerTypes +): + """ + Common implementation for Qwen3VLTextModel, Qwen3VLMoeTextModel, and Nemotron3DenseVLTextModel __init__. + """ + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + assert "Mo" in config.layer_module, "Only MoT layers are supported" + + # Text configuration for decoder layers + + # Embeddings from Qwen3VL base + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + + self.layers = nn.ModuleList( + [MoTDecoderLayer(config, layer_idx, layer_types) for layer_idx in range(config.num_hidden_layers)] + ) + + # Layer norm and rotary embeddings (text-only optimized) + self.norm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + # Pathway-specific normalization + self.norm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + # Rotary embedding (text-only optimized) + self.rotary_emb = layer_types.rotary_embedding(config) + + # Initialize weights and apply final processing + self.post_init() + + +def _impl_init_taylorseer(self, cache_dic=None, current=None): + """ + Initialize TaylorSeer acceleration attributes. + Common implementation for Qwen3VLTextModel.init_taylorseer and Qwen3VLMoeTextModel.init_taylorseer + """ + self.cache_dic = cache_dic or {} + self.current = current or { + "step": 0, + "type": "full", + "stream": "layers_stream", + "layer": 0, + "module": "total", + "activated_steps": [0], + } + # Enable TaylorSeer flag + self.enable_taylorseer = True + + +def _impl_forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, +) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + """ + Training forward pass - Attempted port from qwen2_mot + Common implementation for Qwen3VLTextModel.forward_train and Qwen3VLMoeTextModel.forward_train + + Args: + pack: Packed sequence + attention_mask: Attention mask + position_ids: Position IDs + natten_metadata_list: Optional per-layer NATTEN metadata. + memory: Optional MemoryState for persistent memory across forward passes. + """ + + # Create position embeddings (Qwen3 style) - squeeze once at model level + # tensor below is only used for its dtype and device + device, dtype = get_device_and_dtype(pack) + _meta_tensor = torch.tensor([], dtype=dtype, device=device) + cos, sin = self.rotary_emb( + _meta_tensor, position_ids=position_ids.unsqueeze(0) if position_ids.ndim == 1 else position_ids.unsqueeze(1) + ) # if ndim == 2, then the mrope position_ids is (3, seq_len), we need to put batch dimension in the middle to make it compatible with the rotary_emb + # cos, sin: [1,N,head_dim] (1D pos_ids) or [3,1,N,head_dim] (mrope pos_ids) + cos = cos.squeeze(0) # [N,head_dim] or [3,N,head_dim] + sin = sin.squeeze(0) # [N,head_dim] or [3,N,head_dim] + position_embeddings = ( + from_joint(cos, pack), + from_joint(sin, pack), + ) + + # Tracking the load balancing loss across all layers. For dense models, lbl_metadata_all + # will be a dictionary with empty lists for each pathway. For MoE models, the lists + # for each pathway will be populated with the load balancing loss metadata for each layer. + lbl_metadata_all = dict(und=[], gen=[]) + + hidden_states = pack + + # --- MemoryState: per-step init (outside compile) --- + if memory is not None: + memory.init(hidden_states, device) + + # Derive gen_only once (outside compile) if using MemoryState + memory_gen_only = memory.is_gen_only() if memory is not None else False + + for i, decoder_layer in enumerate(self.layers): + # MemoryState: produce read-only MemoryValue for this layer (outside compile) + memory_value = memory.read_for_layer(i) if memory is not None else None + + hidden_states, lbl_metadata_dict, kv_to_store = decoder_layer( + hidden_states, + attention_mask, + position_embeddings, + natten_metadata=None if natten_metadata_list is None else natten_metadata_list[i], + memory_value=memory_value, + gen_only=memory_gen_only, + ) + + # MemoryState: store K/V produced by this layer (outside compile) + if kv_to_store is not None and memory is not None: + memory.write_for_layer(i, kv_to_store) + + for pathway, lbl_metadata in lbl_metadata_dict.items(): + lbl_metadata_all[pathway].append(lbl_metadata) + + # Compute the load balancing loss across all layers. For dense models, final_lbl_metadata + # will be an empty dictionary. For MoE models, it will be a dictionary with the stacked + # load balancing loss metadata for each pathway. + final_lbl_metadata: dict[str, LBLMetadata] = dict() + for pathway, lbl_metadata_list in lbl_metadata_all.items(): + if len(lbl_metadata_list) > 0: + num_tokens_per_expert = torch.stack( + [lbl_metadata.num_tokens_per_expert for lbl_metadata in lbl_metadata_list] + ) # [num_layers,num_experts] + num_tokens = torch.stack([lbl_metadata.num_tokens for lbl_metadata in lbl_metadata_list]) # [num_layers] + mean_router_prob_per_expert = torch.stack( + [lbl_metadata.mean_router_prob_per_expert for lbl_metadata in lbl_metadata_list] + ) # [num_layers,num_experts] + final_lbl_metadata[pathway] = LBLMetadata( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + mean_router_prob_per_expert=mean_router_prob_per_expert, + ) + + hidden_states_out = zeros_like(hidden_states) + set_und_seq(hidden_states_out, self.norm(get_und_seq(hidden_states))) # [N_und,hidden_size] + set_gen_seq(hidden_states_out, self.norm_moe_gen(get_gen_seq(hidden_states))) # [N_gen,hidden_size] + + return hidden_states_out, final_lbl_metadata + + +def _run_mlp( + mlp: torch.nn.Module, + input: torch.Tensor, +) -> tuple[torch.Tensor, LBLMetadata | None]: + if isinstance(mlp, Qwen3VLMoeTextSparseMoeBlock): + ( + output_tensor, + lbl_metadata, + ) = mlp(input) + else: + output_tensor = mlp(input) + lbl_metadata = None + return output_tensor, lbl_metadata + + +class MoTDecoderLayer(nn.Module): + """ + Unified MoT (Mixture of Transformers) decoder layer. + Features dual-pathway attention for understanding vs generation. + + This is used for both Dense and MoE models. + """ + + def __init__( + self, + config: Qwen3VLTextConfig | Qwen3VLMoeTextConfig | Nemotron3DenseVLTextConfig, + layer_idx: int, + layer_types: LayerTypes, + ): + super().__init__() + self.hidden_size = config.hidden_size + self.freeze_und = config.freeze_und + self.self_attn = PackedAttentionMoT(config, layer_idx, layer_types) + + if ( + hasattr(config, "mlp_only_layers") + and (layer_idx not in config.mlp_only_layers) + and (config.num_experts > 0 and (layer_idx + 1) % config.decoder_sparse_step == 0) + ): + self.mlp = Qwen3VLMoeTextSparseMoeBlock(config) + self.mlp_moe_gen = Qwen3VLMoeTextSparseMoeBlock(config) + else: + self.mlp = layer_types.mlp(config) + self.mlp_moe_gen = layer_types.mlp(config) + + self.input_layernorm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.input_layernorm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + def forward( + self, + input: FactoredSequencePack, + attention_mask, + packed_position_embeddings: Tuple[FactoredSequencePack, FactoredSequencePack], + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + gen_only: bool = False, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata], KVToStore | None]: + """Forward pass with MoT routing and optional memory-augmented attention. + + Returns a 3-tuple: ``(hidden_states, lbl_metadata_dict, kv_to_store)``. + ``kv_to_store`` is non-None when ``memory_value`` is provided, + containing ``(gen_k, gen_v, und_k, und_v)`` to be written back by + ``MemoryState.write_for_layer()`` outside the ``torch.compile`` + boundary. + + Args: + input: Packed sequence with und/gen tokens + attention_mask: Attention mask + packed_position_embeddings: RoPE embeddings (cos, sin) + natten_metadata: Optional NATTEN metadata for neighborhood attention. + memory_value: Read-only tensor container from MemoryState.read_for_layer(). + gen_only: When True, skip the understanding pathway (und K/V come from cache). + """ + # Pre-Attention layernorm + pack_norm_out = from_und_gen_splits( + self.input_layernorm(get_und_seq(input)), # [N_und,hidden_size] + self.input_layernorm_moe_gen(get_gen_seq(input)), # [N_gen,hidden_size] + input, + ) # [N_und+N_gen,hidden_size] + + # Self Attention + Residual + kv_to_store: KVToStore | None = None + if gen_only: + assert natten_metadata is None + # gen_only: skip und, compute gen tokens only (und K/V come from cache) + _gen_norm = get_gen_seq(pack_norm_out) + gen_pack = from_und_gen_splits( + _gen_norm.new_empty(0, _gen_norm.shape[-1]), + _gen_norm, + pack_norm_out, + ) + + # Build position embeddings whose und length matches gen_pack's + # und length (always 0). Required when the outer pack carries + # a padded causal_seq (``pad_for_cuda_graphs=True``): without + # this, the und RoPE inside ``PackedAttentionMoT.forward`` + # would broadcast cos/sin of shape ``(MAX_CAUSAL_LEN, head_dim)`` + # onto a length-0 ``q_und`` / ``k_und`` and crash. When the + # outer pack is unpadded (eager AR path), the und cos/sin + # already have length 0 and this slice is a no-op. + _cos, _sin = packed_position_embeddings + _empty_cos_und = get_und_seq(_cos)[:0] + _empty_sin_und = get_und_seq(_sin)[:0] + gen_position_embeddings = ( + from_und_gen_splits(_empty_cos_und, get_gen_seq(_cos), _cos), + from_und_gen_splits(_empty_sin_und, get_gen_seq(_sin), _sin), + ) + + pack_attn_out, kv_to_store = self.self_attn( + gen_pack, + attention_mask, + gen_position_embeddings, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + gen_attn_out = get_gen_seq(pack_attn_out) + residual_und = gen_attn_out.new_empty(0, gen_attn_out.shape[-1]) + residual_gen = get_gen_seq(input) + gen_attn_out + else: + # STANDARD PATH: Process both und and gen tokens + pack_attn_out, kv_to_store = self.self_attn( + pack_norm_out, + attention_mask, + packed_position_embeddings, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + residual_und = get_und_seq(input) + get_und_seq(pack_attn_out) # [N_und,hidden_size] + residual_gen = get_gen_seq(input) + get_gen_seq(pack_attn_out) # [N_gen,hidden_size] + + # Pre-MLP layernorm and processing + lbl_metadata_dict: dict[str, LBLMetadata] = dict() + + if gen_only: + # gen_only: skip und, compute gen tokens only + ln_out_und = residual_gen.new_empty(0, residual_gen.shape[-1]) + ln_out_gen = self.post_attention_layernorm_moe_gen(residual_gen) + + # UNPAD MLP INPUT (gen only) + gen_len = pack_attn_out["_num_full_tokens"] + ln_out_gen_unpadded = ln_out_gen[:gen_len] # [N_gen_unpadded,hidden_size] + + # Run MLP (gen only) + mlp_out_gen_unpadded, lbl_metadata_gen = _run_mlp(self.mlp_moe_gen, ln_out_gen_unpadded) + # mlp_out_gen_unpadded: [N_gen_unpadded,hidden_size] + + # PAD MLP OUTPUT (gen only) + mlp_out_gen = torch.cat([mlp_out_gen_unpadded, ln_out_gen[gen_len:]], dim=0) # [N_gen,hidden_size] + + # Build metadata dict (no und metadata in optimized path) + if lbl_metadata_gen is not None: + lbl_metadata_dict["gen"] = lbl_metadata_gen + + # Final output with residual (gen only) + mlp_out_und_seq = residual_gen.new_empty(0, residual_gen.shape[-1]) + mlp_out_gen_seq = residual_gen + mlp_out_gen + else: + # STANDARD PATH: Process both und and gen tokens + ln_out_und = self.post_attention_layernorm(residual_und) # [N_und,hidden_size] + ln_out_gen = self.post_attention_layernorm_moe_gen(residual_gen) # [N_gen,hidden_size] + + # UNPAD MLP INPUT =============== + + # artificial expert inbalance due to routing padding tokens. + gen_len = pack_attn_out["_num_full_tokens"] + und_len = pack_attn_out["_num_causal_tokens"] + ln_out_und_unpadded = ln_out_und[:und_len] # [N_und_unpadded,hidden_size] + ln_out_gen_unpadded = ln_out_gen[:gen_len] # [N_gen_unpadded,hidden_size] + + mlp_out_und_unpadded, lbl_metadata_und = _run_mlp(self.mlp, ln_out_und_unpadded) + # mlp_out_und_unpadded: [N_und_unpadded,hidden_size] + mlp_out_gen_unpadded, lbl_metadata_gen = _run_mlp(self.mlp_moe_gen, ln_out_gen_unpadded) + # mlp_out_gen_unpadded: [N_gen_unpadded,hidden_size] + + # PAD MLP OUTPUT =============== + mlp_out_und = torch.cat([mlp_out_und_unpadded, ln_out_und[und_len:]], dim=0) # [N_und,hidden_size] + mlp_out_gen = torch.cat([mlp_out_gen_unpadded, ln_out_gen[gen_len:]], dim=0) # [N_gen,hidden_size] + + if lbl_metadata_und is not None: + lbl_metadata_dict["und"] = lbl_metadata_und + if lbl_metadata_gen is not None: + lbl_metadata_dict["gen"] = lbl_metadata_gen + + mlp_out_und_seq = residual_und + mlp_out_und # [N_und,hidden_size] + mlp_out_gen_seq = residual_gen + mlp_out_gen # [N_gen,hidden_size] + + return from_und_gen_splits(mlp_out_und_seq, mlp_out_gen_seq, input), lbl_metadata_dict, kv_to_store + + +# Backward-compat alias: serialized checkpoint configs reference the old name. +Qwen3VLTextMoTDecoderLayer = MoTDecoderLayer + + +class Qwen3VLTextModel(Qwen3VLPreTrainedModel): + """ + Qwen3VL text model for MoT with dense MLPs. + This is a wrapper around the _impl_forward defined above, + specialized for dense models. + """ + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("qwen3_vl_dense")) + + def init_taylorseer(self, cache_dic=None, current=None): + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Qwen3VLMoeTextModel(Qwen3VLMoePreTrainedModel): + """ + Qwen3VL text model for MoT with MoE MLPs. + This is a wrapper around the _impl_* helpers defined above, + specialized for MoE models. + """ + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("qwen3_vl_moe")) + + def init_taylorseer(self, cache_dic=None, current=None): + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Qwen3VLTextForCausalLM(Qwen3VLPreTrainedModel): + """ + Qwen3VL text causal language model for MoT. + This variant is used for dense-only MLP models. + """ + + _tied_weights_keys = ["lm_head.weight"] + + def __init__(self, config: Qwen3VLTextConfig): + super().__init__(config) + self.model = Qwen3VLTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def init_moe(self) -> None: + """Initialize MoE/MoT weights by copying understanding to generation pathway.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" in name: + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + """Training forward pass - simplified to match qwen3_mot""" + outputs = self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + return outputs + + +class Qwen3VLMoeTextForCausalLM(Qwen3VLMoePreTrainedModel): + """ + Qwen3VL text causal language model for MoT with MoE on the generation pathway. + This variant is used for MoE MLP models. + """ + + _tied_weights_keys = ["lm_head.weight"] + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + self.model = Qwen3VLMoeTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def init_moe(self) -> None: + """Initialize MoE/MoT weights by copying understanding to generation pathway.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" in name: + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, torch.Tensor]]: + """Training forward pass - simplified to match qwen3_mot""" + + outputs = self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + + return outputs + + +# ----------------------------------------------------------------------------- +# Nemotron 3 Dense VL MoT model wrappers +# ----------------------------------------------------------------------------- + + +class Nemotron3DenseVLTextModel(Nemotron3DenseVLPreTrainedModel): + """Nemotron 3 Dense VL text model adapted for MoT training.""" + + def __init__(self, config: Nemotron3DenseVLTextConfig) -> None: + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("nemotron_dense")) + + def init_taylorseer(self, cache_dic=None, current=None) -> None: + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Nemotron3DenseVLTextForCausalLM(Nemotron3DenseVLPreTrainedModel): + """Causal LM head on top of the Nemotron 3 Dense VL MoT text model.""" + + _tied_weights_keys: list[str] = [] + + def __init__(self, config: Nemotron3DenseVLTextConfig) -> None: + super().__init__(config) + self.model = Nemotron3DenseVLTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + self.post_init() + + def init_moe(self) -> None: + """Copy understanding-pathway weights into the generation-pathway parameters.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" not in name: + continue + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + elif any(norm_key in original_name for norm_key in ("q_norm", "k_norm")): + # qk_norm_for_text=False → q_norm/k_norm are nn.Identity() with no parameters; + # the moe_gen counterpart (q_norm_moe_gen) is a real RMSNorm, so skip init here. + pass + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + return self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) diff --git a/cosmos3/_src/vfm/models/omni_mot_model.py b/cosmos3/_src/vfm/models/omni_mot_model.py new file mode 100644 index 00000000..6467608a --- /dev/null +++ b/cosmos3/_src/vfm/models/omni_mot_model.py @@ -0,0 +1,2924 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +from contextlib import contextmanager +from typing import Any, Callable, Dict, Mapping, Optional, Tuple + +import numpy as np +import torch +import torch.distributed as dist +from einops import rearrange +from torch.distributed._composable.fsdp import FSDPModule +from torch.nn.modules.module import _IncompatibleKeys +from torch.nn.utils.clip_grad import clip_grad_norm_ + +from cosmos3._src.imaginaire.flags import DEVICE, TRAINING, Device +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.imaginaire.lazy_config import instantiate as lazy_instantiate +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import log, misc +from cosmos3._src.imaginaire.utils.count_params import count_params +from cosmos3._src.imaginaire.utils.timer import Timer +from cosmos3._src.vfm.configs.base.defaults.model_config import OmniMoTModelConfig +from cosmos3._src.vfm.datasets.sequence_packing import ( + PackedSequence, + SequencePlan, + add_special_tokens, + build_sequence_plans_from_data_batch, + pack_input_sequence, +) +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO +from cosmos3._src.vfm.diffusion.rectified_flow import RectifiedFlow +from cosmos3._src.vfm.diffusion.samplers.edm import EDMSampler +from cosmos3._src.vfm.diffusion.samplers.fixed_step import FixedStepSampler +from cosmos3._src.vfm.diffusion.samplers.unipc import UniPCSampler, UniPCSamplerConfig +from cosmos3._src.vfm.models.mot.cosmos3_vfm_network import Cosmos3VFMNetwork, Cosmos3VFMNetworkConfig +from cosmos3._src.vfm.models.mot.modeling_utils import has_noisy_tokens +from cosmos3._src.vfm.models.mot.parallelize_vfm_network import parallelize_vfm_network +from cosmos3._src.vfm.models.utils.data_and_condition import ( + GenerationDataClean, + GenerationDataNoised, + _expand_per_sample_to_per_vision_item, + unwrap_and_densify, +) +from cosmos3._src.vfm.models.utils.load_balancing_loss import compute_load_balancing_loss +from cosmos3._src.vfm.models.utils.memory import MemoryState +from cosmos3._src.vfm.models.utils.safetensors_loader import load_language_model as load_language_model_safetensors +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import tokenize_caption +from cosmos3._src.vfm.tokenizers.interface import VideoTokenizerInterface +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution +from cosmos3._src.vfm.utils.dtensor_helper import DTensorFastEmaModelUpdater +from cosmos3._src.vfm.utils.model_weights_stats import WeightTrainingStat +from cosmos3._src.vfm.utils.parallelism import ParallelDims + +# Optional dependency for LoRA training +try: + from peft import LoraConfig, inject_adapter_in_model +except ImportError: + LoraConfig = None + inject_adapter_in_model = None + + +class OmniMoTModel(ImaginaireModel): + """ + Mixture of Transformers (MoT) model to be trained with the flow matching objective + for visual / sound / action generation. + """ + + def __init__(self, config: OmniMoTModelConfig): + super().__init__() + self.config = config + log.info(f"OmniMoTModel: config {self.config}") + # 0. Set up precision + self.set_precision() + + # 1. Set data keys and data information + self.set_up_data_key() + + # 2. Text, vision, audio, action tokenizers + self.set_up_tokenizers() + + # 3. FSDP setup. Note: call this before building the model. + self.set_up_parallelism() + + # 4. Build the denoiser network + self.set_up_model() + + # 5. Set up training time scheduler and inference time sampler + self.set_up_scheduler_and_sampler() + + self.log_enc_time_every_n = config.log_enc_time_every_n + + def set_precision(self) -> None: + self.precision = getattr(torch, self.config.parallelism.precision) + self.tensor_kwargs = {"device": DEVICE, "dtype": self.precision} + self.tensor_kwargs_fp32 = {"device": DEVICE, "dtype": torch.float32} + log.warning(f"OmniMoTModel: precision {self.precision}") + + # Disable TF32 for CUDA matrix multiplications since this may impact model quality. + torch.backends.cudnn.allow_tf32 = torch.backends.cuda.matmul.allow_tf32 = False + + def set_up_data_key(self) -> None: + + self.input_video_key = self.config.input_video_key # by default it is video key for Video diffusion model + self.input_image_key = self.config.input_image_key + self.input_caption_key = self.config.input_caption_key + + @misc.timer("OmniMoTModel: set_up_tokenizers") + def set_up_tokenizers(self) -> None: + """ + Variable names follow the naming convention: + - tokenizer__gen if used for generation branch + - tokenizer__und if used for understanding branch + """ + # 1. Text tokenizer + self.vlm_config = self.config.vlm_config + vlm_tokenizer = lazy_instantiate(self.vlm_config.tokenizer) + vlm_tokenizer, special_tokens = add_special_tokens(vlm_tokenizer) + self.vlm_tokenizer = vlm_tokenizer + + self.llm_special_tokens = special_tokens + self.llm_special_tokens["eos_token_id"] = vlm_tokenizer.eos_token_id + + # 2. Vision tokenizer (images/videos) for generation. + self.tokenizer_vision_gen: VideoTokenizerInterface = lazy_instantiate(self.config.tokenizer) + assert self.tokenizer_vision_gen.latent_ch == self.config.state_ch, ( + f"vision tokenizer latent_ch {self.tokenizer_vision_gen.latent_ch} != state_shape {self.config.state_ch}" + ) + if hasattr(self.tokenizer_vision_gen, "reset_dtype"): + self.tokenizer_vision_gen.reset_dtype() + + # 3. Sound/audio tokenizer (optional) + if self.config.sound_gen: + assert self.config.sound_tokenizer is not None, "sound_tokenizer must be provided when sound_gen is True" + self.tokenizer_sound_gen = lazy_instantiate(self.config.sound_tokenizer) + assert self.config.sound_dim is not None, "sound_dim must be provided when sound_gen is True" + assert self.tokenizer_sound_gen.latent_ch == self.config.sound_dim, ( + f"sound tokenizer latent_ch {self.tokenizer_sound_gen.latent_ch} != sound_dim {self.config.sound_dim}" + ) + if hasattr(self.tokenizer_sound_gen, "reset_dtype"): + self.tokenizer_sound_gen.reset_dtype() + log.info(f"Sound tokenizer initialized: {type(self.tokenizer_sound_gen).__name__}") + else: + self.tokenizer_sound_gen = None + + + + def build_net(self, dtype: torch.dtype): + # Build model network and parallelize it. + with torch.device("meta"): + assert self.vlm_config.model_instance is not None, "Model instance should be specified" + + language_model = lazy_instantiate(self.vlm_config.model_instance) + + # (i.e., roughly [0, num_train_timesteps]). The MoT network expects to internally + # rescale timesteps before embedding; avoid hard-coding 1e-3 by computing it from + # the configured scheduler resolution. + num_train_timesteps = self.config.rectified_flow_inference_config.num_train_timesteps + network_config = Cosmos3VFMNetworkConfig( + vlm_config=language_model.config, + latent_patch_size=self.config.diffusion_expert_config.patch_spatial, + latent_downsample_factor=self.config.latent_downsample_factor, + latent_channel_size=self.config.state_ch, + max_latent_h=self.config.diffusion_expert_config.max_vae_latent_side_after_patchify, + max_latent_w=self.config.diffusion_expert_config.max_vae_latent_side_after_patchify, + max_latent_t=self.config.state_t, + rope_h_extrapolation_ratio=self.config.diffusion_expert_config.rope_h_extrapolation_ratio, + rope_w_extrapolation_ratio=self.config.diffusion_expert_config.rope_w_extrapolation_ratio, + rope_t_extrapolation_ratio=self.config.diffusion_expert_config.rope_t_extrapolation_ratio, + enable_fps_modulation=self.config.diffusion_expert_config.enable_fps_modulation, + base_fps=self.config.diffusion_expert_config.base_fps, + vision_gen=self.config.vision_gen, + action_gen=self.config.action_gen, + sound_gen=self.config.sound_gen, + position_embedding_type=self.config.diffusion_expert_config.position_embedding_type, + joint_attn_implementation=self.config.joint_attn_implementation, + timestep_scale=1.0 / float(num_train_timesteps) * self.config.diffusion_expert_config.timestep_range, + action_dim=self.config.max_action_dim, + num_embodiment_domains=self.config.num_embodiment_domains, + temporal_compression_factor_vision=self.tokenizer_vision_gen.temporal_compression_factor, + natten_parameter_list=self.config.natten_parameter_list, + video_temporal_causal=self.config.video_temporal_causal, + # Sound generation parameters + sound_dim=self.config.sound_dim, + sound_latent_fps=self.config.sound_latent_fps, + ) + network_config._attn_implementation_internal = "eager" + net = Cosmos3VFMNetwork( + language_model=language_model, + config=network_config, + ) + net.pad_for_cuda_graphs = self.config.parallelism.use_cuda_graphs + + self.install_attention_dispatch(net) + + net = parallelize_vfm_network( + net, + parallel_dims=self.parallel_dims, + config=self.config.parallelism, + ) + + with misc.timer("meta to cuda and broadcast model states"): + net = net.to(dtype=dtype) + net.to_empty(device=DEVICE) + if DEVICE == Device.CUDA: + # Weight initialization is not needed for other devices (cpu, + # meta), since they are only for checkpoint conversion and smoke + # tests. + net.init_weights(buffer_device=DEVICE) + + return net + + def load_pretrained_model_if_needed(self): + """ + This function is used to load the pretrained model weights from HF if needed. + + 1. If self.vlm_config.load_pretrained is False, we skip loading the pretrained + model weights. + 2. If self.vlm_config.load_pretrained is True, and + self.config.diffusion_expert_config.load_weights_from_pretrained is True, + we load the understanding pathway weights from HF, and copy them to the + generation pathway. + 3. If self.vlm_config.load_pretrained is True, and + self.config.diffusion_expert_config.load_weights_from_pretrained is False, + we load the understanding pathway weights from HF, but do not copy them to + the generation pathway. This is used when we warm-start from a load_path + (but no previous checkpoint exists), and we want to switch the understanding + pathway weights to a new model (e.g., Qwen3-VL to Cosmos-Reason2). + """ + if not self.vlm_config.load_pretrained: + return + + def _load_language_model(net: torch.nn.Module): + load_language_model_safetensors( + model=net.language_model, + checkpoint_path=self.vlm_config.checkpoint_path, + credential_path=self.vlm_config.credential_path, + parallel_dims=self.parallel_dims, + checkpoint_format=getattr(self.vlm_config, "vlm_checkpoint_format", None), + ) + + # When specified, we load pretrained LLM weights. + log.info(f"Loading understanding pathway weights from {self.vlm_config.checkpoint_path}") + _load_language_model(self.net) + if self.config.ema.enabled: + _load_language_model(self.net_ema) + log.info("Successfully loaded understanding pathway weights.") + + if self.config.diffusion_expert_config.load_weights_from_pretrained: + log.info("Copying understanding pathway weights to generation pathway.") + self.net.language_model.init_moe() + if self.config.ema.enabled: + self.net_ema.language_model.init_moe() + log.info("Successfully copied understanding pathway weights to generation pathway.") + + @misc.timer("OmniMoTModel: set_up_model") + def set_up_model(self): + assert hasattr(self, "parallel_dims"), "parallel_dims must be set" + config = self.config + with misc.timer("Creating PyTorch model and ema if enabled"): + self.net = self.build_net(dtype=self.precision) + self._param_count = count_params(self.net, verbose=False) + + if config.ema.enabled: + self.net_ema = self.build_net(dtype=torch.float32) + self.net_ema.requires_grad_(False) + + self.net_ema_worker = DTensorFastEmaModelUpdater() + + + s = config.ema.rate + self.ema_exp_coefficient = np.roots([1, 7, 16 - s**-2, 12 - s**-2]).real.max() + + self.net_ema_worker.copy_to(src_model=self.net, tgt_model=self.net_ema) + + self.set_up_memory() + + torch.cuda.empty_cache() + + def install_attention_dispatch(self, net: torch.nn.Module) -> None: + """Install a custom attention dispatch function on the network. + + Called during ``build_net()`` after the network is constructed but + before parallelization. The base implementation is a no-op; + ``OmniMoTCausalModel`` overrides this to install + ``dispatch_attention_with_memory`` on every attention layer. + """ + pass + + def set_up_memory(self) -> None: + """Initialize memory state used during training (e.g. KV caches). + + The base implementation is a no-op. ``OmniMoTCausalModel`` overrides + this to allocate a KV cache. + """ + pass + + def set_up_parallelism(self) -> None: + """Set up the fsdp for the model.""" + if not torch.distributed.is_initialized(): + self.parallel_dims = None + return + + self.parallel_dims = ParallelDims( + enable_inference_mode=self.config.parallelism.enable_inference_mode, + world_size=torch.distributed.get_world_size(), + dp_shard=self.config.parallelism.data_parallel_shard_degree, + cfgp=self.config.parallelism.cfg_parallel_shard_degree, + cp=self.config.parallelism.context_parallel_shard_degree, + ) + self.parallel_dims.build_meshes(device_type=DEVICE) + + def set_up_scheduler_and_sampler(self): + # Get shift value - support both int and dict-based resolution lookup + # For scheduler initialization, use model's configured resolution + shift_config = self.config.rectified_flow_training_config.shift + if isinstance(shift_config, int): + shift = shift_config + else: + # shift set in RectifiedFlow is only used during inference. + # So, set it to the resolution of the model. + # This part gets executed only when we specify shift as a dict + # This is needed during multi-resolution training. + shift_dict = dict(shift_config) + resolution = self.config.resolution + if resolution not in shift_dict: + raise ValueError( + f"Resolution '{resolution}' not found in shift dict. Available resolutions: {list(shift_dict.keys())}" + ) + shift = shift_dict[resolution] + + # Rectified Flow timestep scheduler and sampler for training (separate for image and video) + if self.config.vision_gen: + self.rectified_flow_image = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_image_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + self.rectified_flow_video = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_video_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + if self.config.action_gen: + self.rectified_flow_action = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_action_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + if self.config.sound_gen: + self.rectified_flow_sound = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_sound_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + + # Denoising sampler (solver) for inference + assert self.config.rectified_flow_inference_config.scheduler_type in ["unipc", "edm"] + if self.config.rectified_flow_inference_config.scheduler_type == "unipc": + unipc_sampler_config = UniPCSamplerConfig( + num_train_timesteps=self.config.rectified_flow_inference_config.num_train_timesteps, + shift=self.config.rectified_flow_inference_config.shift, + use_dynamic_shifting=self.config.rectified_flow_inference_config.use_dynamic_shifting, + ) + self.sampler = UniPCSampler(cfg=unipc_sampler_config, tensor_kwargs=self.tensor_kwargs) + else: + self.sampler = EDMSampler() + + # Fixed-step sampler for distilled models (None for base models) + if self.config.fixed_step_sampler_config is not None: + cfg = self.config.fixed_step_sampler_config + self.fixed_step_sampler = FixedStepSampler( + t_list=list(cfg.t_list), + sample_type=cfg.sample_type, + num_train_timesteps=float(self.config.rectified_flow_inference_config.num_train_timesteps), + ) + else: + self.fixed_step_sampler = None + + def init_optimizer_scheduler( + self, optimizer_config: LazyDict, scheduler_config: LazyDict + ) -> tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LRScheduler]: + """Creates the optimizer and scheduler for the model. + + Args: + optimizer_config (LazyDict): The lazy config for the optimizer. + scheduler_config (LazyDict): The lazy config for the learning rate scheduler. + + Returns: + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + """ + from cosmos3._src.imaginaire.utils.optim_instantiate import get_base_scheduler + + optimizer = lazy_instantiate(optimizer_config, model=self.net) + scheduler = get_base_scheduler(optimizer, self, scheduler_config) + + return optimizer, scheduler + + def _derive_include_end_of_generation_token(self) -> bool: + impl = self.config.joint_attn_implementation + assert impl in ("flex", "two_way", "three_way"), ( + f"Invalid joint_attn_implementation: {impl}. Must be 'flex', 'two_way', or 'three_way'." + ) + return impl == "flex" + + # ------------------------ training hooks ------------------------ + def on_before_zero_grad( + self, optimizer: torch.optim.Optimizer, scheduler: torch.optim.lr_scheduler.LRScheduler, iteration: int + ) -> None: + """ + update the net_ema + """ + del scheduler, optimizer + + if self.config.ema.enabled: + # calculate beta for EMA update + ema_beta = self.ema_beta(iteration) + self.net_ema_worker.update_average(self.net, self.net_ema, beta=ema_beta) + + # ------------------------ helpers ------------------------ + + def _pack_input_sequence( + self, + sequence_plans: list[SequencePlan], + input_text_indexes: list[list[int]], + gen_data_clean: GenerationDataClean, + input_timesteps: torch.Tensor, + include_end_of_generation_token: bool = False, + skip_text_tokens: bool = False, + initial_mrope_temporal_offset: int | float = 0, + ) -> PackedSequence: + """Wrap ``pack_input_sequence`` with all config-derived args pre-filled. + + Centralises the 9 config-derived positional/embedding args so callers only + supply the four per-call arguments (sequence_plans, text tokens, data, timesteps) + plus three optional flags. + """ + assert self.tokenizer_vision_gen is not None + return pack_input_sequence( + sequence_plans=sequence_plans, + input_text_indexes=input_text_indexes, + gen_data_clean=gen_data_clean, + input_timesteps=input_timesteps, + special_tokens=self.llm_special_tokens, + latent_patch_size=self.config.diffusion_expert_config.patch_spatial, + skip_text_tokens=skip_text_tokens, + include_end_of_generation_token=include_end_of_generation_token, + position_embedding_type=self.config.diffusion_expert_config.position_embedding_type, + unified_3d_mrope_reset_spatial_ids=self.config.diffusion_expert_config.unified_3d_mrope_reset_spatial_ids, + unified_3d_mrope_temporal_modality_margin=self.config.diffusion_expert_config.unified_3d_mrope_temporal_modality_margin, + enable_fps_modulation=self.config.diffusion_expert_config.enable_fps_modulation, + base_fps=float(self.config.diffusion_expert_config.base_fps), + temporal_compression_factor=self.tokenizer_vision_gen.temporal_compression_factor, + video_temporal_causal=self.config.video_temporal_causal, + action_dim=self.config.max_action_dim, + initial_mrope_temporal_offset=initial_mrope_temporal_offset, + ) + + # ------------------------ training ------------------------ + + def memory_init_training( + self, + gen_data_clean: GenerationDataClean, + data_batch: dict[str, torch.Tensor], + input_text_indexes: list[list[int]], + ) -> tuple[GenerationDataClean, dict]: + """Prepare the memory for a single training step. + + Called at the start of ``training_step`` to give the causal subclass + an injection point for memory-based segment handling (frame trimming, + segment bookkeeping, cache resets, packing overrides). + + The base implementation returns *gen_data_clean* unmodified and a + default memory_info dict that does not support memory-backed training. + + The ``skip_text`` and ``initial_temporal`` offset fields are required, + and are used for both sequence packing and memory. + + Returns: + ``(gen_data_clean, memory_info)`` where *memory_info* is a dict with keys: + ``skip_text``, ``initial_temporal_offset`` + """ + return gen_data_clean, { + "skip_text": False, + "initial_temporal_offset": 0, + } + + def build_memory_state( + self, + packed_seq: PackedSequence, + memory_info: dict, + ) -> MemoryState | None: + """Construct a ``MemoryState`` from a packed sequence and context dict. + + Called after packing in ``training_step()``, and before ``denoise()`` + in AR inference. The base implementation returns ``None`` (no + persistent memory). ``OmniMoTCausalModel`` overrides this to build + the appropriate ``ARMemoryState`` or ``KVCacheTrainMemoryState``. + + Args: + packed_seq: The packed multi-modal sequence produced by + ``_pack_input_sequence``. + memory_info: Context dict returned by ``memory_init_training()`` + (for the training path) or constructed by the AR inference + caller. See ``memory_init_training()`` for the base keys. + """ + return None + + def pre_noise_memory_hook( + self, + packed_sequence: PackedSequence, + gen_data_clean: GenerationDataClean, + memory_info: dict, + ) -> dict: + """Hook called after sequence packing and before noising. Returns (possibly updated) memory_info. + + The packed sequence still contains clean tokens at this point. + Override in subclasses to run a clean forward pass (e.g. for teacher forcing). + """ + return memory_info + + def training_step( + self, data_batch: dict[str, torch.Tensor], iteration: int + ) -> tuple[dict[str, torch.Tensor], torch.Tensor]: + """ + Performs a single training step for the rectified-flow (flow-matching) model. + + This method executes one iteration of the model's training. It involves: + 1. Tokenizing generation modalities (vision/action/sound) into latents (tokens). + 2. Sampling a training timestep (t) for each modality and constructing noised latents (xt) + per the rectified-flow formulation. + 3. Packing text + generation tokens into a single sequence and running the MoT network to predict + the flow field velocity at the given t. + 4. Computing flow-matching loss (plus optional auxiliary load-balancing losses). + + Args: + data_batch (dict): raw data batch draw from the training data loader. + iteration (int): Current iteration number. + + Returns: + tuple: A tuple containing two elements: + - dict: additional data that used to debug / logging / callbacks + - Tensor: The computed loss for the training step as a PyTorch Tensor. + + """ + if self.parallel_dims is None or self.parallel_dims.cp_rank == 0: + self._update_train_stats(data_batch) + + # Load, apply dropout, and tokenize input captions + input_text_indexes = self._load_and_tokenize_text_data(data_batch, iteration) + + # Build sequence plans if not present. SequencePlan has the conditioning information. + sequence_plans = build_sequence_plans_from_data_batch( + data_batch=data_batch, + input_video_key=self.input_video_key, + input_image_key=self.input_image_key, + ) + + # Get data from raw data batch and tokenize into corresponding tokens for *generation* task + # The unnoised, tokenized data for the generation task. + gen_data_clean = self.get_data_and_condition(data_batch, iteration=iteration) + + gen_data_clean, memory_info = self.memory_init_training(gen_data_clean, data_batch, input_text_indexes) + + # Compute resolution per sample for per-sample shift lookup + # image_size[i] may be (1, 4) from IterativeJointDataLoader or (4,) from custom_collate_fn. + if "image_size" in data_batch: + data_resolutions = [] + for i in range(gen_data_clean.batch_size): + img_size = data_batch["image_size"][i] + if img_size.dim() == 2: + img_size = img_size[0] + target_h = int(img_size[0].item()) + target_w = int(img_size[1].item()) + data_resolutions.append(get_vision_data_resolution((target_h, target_w))) + else: + data_resolutions = None + + # Calculate number of tokens per sample (before 2x2 merge) for dynamic shift + # gen_data_clean.x0_tokens_vision: B, C, T, H, W + assert all(x.shape[0] == 1 for x in gen_data_clean.x0_tokens_vision), ( + "Batch size must be 1 for individual samples" + ) + num_tokens_per_sample = [x.shape[2] * x.shape[3] * x.shape[4] for x in gen_data_clean.x0_tokens_vision] + + # Sample a random noise level (sigma) and corresponding interpolation coefficient ("timesteps" in RF) + # Apply shift per sample based on each sample's resolution + num_vision_latent_frames = [x.shape[2] for x in gen_data_clean.x0_tokens_vision] + timesteps_vision, sigmas_vision = self._get_train_noise_level_vision( + batch_size=gen_data_clean.batch_size, + is_image_batch=gen_data_clean.is_image_batch, + resolutions=data_resolutions, + num_vision_latent_frames=num_vision_latent_frames, + num_tokens=num_tokens_per_sample, + ) # [B, T_vis] each + + # Optional independent action schedule (sampled from rectified_flow_action with + # action-specific shift/high-sigma overrides). Only active when the config opts in and + # the batch contains action data. + # + # Mixed-batch indexing: gen_data_clean.x0_tokens_action (and every packed_sequence.action.* + # field) is *dense* — one entry per sample with has_action=True, in the original batch order + # but skipping non-action samples. To feed each dense action entry its sample's sigma, we + # sample σ for the full batch and reindex with action_sample_indices (the batch positions + # of action-bearing samples). This avoids the mismatch that happens when, e.g., batch + # sample 1 has action but the dense entry 0 would otherwise read σ from batch position 0. + rf_cfg = self.config.rectified_flow_training_config + action_sample_indices = [i for i, plan in enumerate(sequence_plans) if plan.has_action] + if rf_cfg.independent_action_schedule and action_sample_indices: + ts_full, sg_full = self._get_train_noise_level_action(batch_size=gen_data_clean.batch_size) # [B, 1] each + idx = torch.tensor(action_sample_indices, dtype=torch.long) # [n_action] + timesteps_action = ts_full[idx] # [n_action, 1] + sigmas_action = sg_full[idx] # [n_action, 1] + else: + timesteps_action, sigmas_action = (None, None) + + # Broadcast timesteps/sigmas across CP group to ensure consistency + if self.parallel_dims is not None and self.parallel_dims.cp_enabled: + src_rank = 0 # use cp rank 0 to broadcast timesteps/sigmas + cp_group = self.parallel_dims.cp_mesh.get_group() + global_src_rank = torch.distributed.get_global_rank(cp_group, src_rank) + timesteps_vision = timesteps_vision.contiguous() + sigmas_vision = sigmas_vision.contiguous() + torch.distributed.broadcast(timesteps_vision, src=global_src_rank, group=cp_group) + torch.distributed.broadcast(sigmas_vision, src=global_src_rank, group=cp_group) + if sigmas_action is not None: + timesteps_action = timesteps_action.contiguous() + sigmas_action = sigmas_action.contiguous() + torch.distributed.broadcast(timesteps_action, src=global_src_rank, group=cp_group) + torch.distributed.broadcast(sigmas_action, src=global_src_rank, group=cp_group) + + packed_sequence = self._pack_input_sequence( + sequence_plans, + input_text_indexes, + gen_data_clean, + timesteps_vision.cpu(), + skip_text_tokens=memory_info["skip_text"], + initial_mrope_temporal_offset=memory_info["initial_temporal_offset"], + ) + + # Under independent_action_schedule, overwrite the vision-based action timestep the + # packer injected with the action timestep, so the denoiser's action timestep embedding + # matches the sigma used to noise action tokens. + if timesteps_action is not None and packed_sequence.action is not None: + action_has_noisy_tokens = any(nfi.numel() > 0 for nfi in packed_sequence.action.noisy_frame_indexes) + if action_has_noisy_tokens: + sample_ts = timesteps_action.squeeze(1).cpu() # [n_action] + packed_sequence.action.timesteps = torch.cat( + [ + sample_ts[i : i + 1].expand(nfi.numel()) + for i, nfi in enumerate(packed_sequence.action.noisy_frame_indexes) + ] + ).to(dtype=torch.float32) # [N_action_noisy] + else: + timesteps_action, sigmas_action = (None, None) + + # For image editing (multi-item vision), expand per-sample timesteps/sigmas to + # per-vision-item so downstream noise/loss indexing matches the flat x0_tokens_vision + # list. No-op when num_vision_items_per_sample is None (standard T2I/T2V/policy cases). + # Conditioning items get sigma=0 via their condition_mask, so the actual timestep value + # for them does not matter. + timesteps_vision = _expand_per_sample_to_per_vision_item( + timesteps_vision, gen_data_clean.num_vision_items_per_sample + ) # [B_items, T_vis] + sigmas_vision = _expand_per_sample_to_per_vision_item( + sigmas_vision, gen_data_clean.num_vision_items_per_sample + ) # [B_items, T_vis] + + memory_info = self.pre_noise_memory_hook(packed_sequence, gen_data_clean, memory_info) + + # Flow matching/diffusion forward process: noise the input signal with the sampled noise level + gen_data_noised = self._add_noise_to_input( + gen_data_clean, packed_sequence, sigmas_vision, sigmas_action=sigmas_action + ) + self._replace_clean_with_noised(packed_sequence, gen_data_noised) + + # Move packed sequence to CUDA + packed_sequence.to_cuda() + + # Network forward pass + memory = self.build_memory_state(packed_sequence, memory_info) # pylint: disable=assignment-from-none + out_net = self.denoise( + data_batch_packed=packed_sequence, + fps_vision=gen_data_clean.fps_vision, + fps_action=gen_data_clean.fps_action, + fps_sound=gen_data_clean.fps_sound, + memory=memory, + ) + + loss, losses_dict = self._compute_losses( + out_net=out_net, + data_batch_packed=packed_sequence, + gen_data_noised=gen_data_noised, + timesteps=timesteps_vision, + is_image_batch=gen_data_clean.is_image_batch, + timesteps_action=timesteps_action, + ) + + # Pixel-space video shapes for VAE FLOPs estimation in callbacks (e.g. MFU). + _vae_pixel_shapes: list[tuple[int, int, int]] = [] + if gen_data_clean.raw_state_vision is not None: + for _v in gen_data_clean.raw_state_vision: + if _v is not None: + assert _v.dim() in [4, 5], ( + "Currently only [C, T, H, W] and [B, C, T, H, W] formats are supported for the VAE encoding." + ) + t_h_w = ( + (int(_v.shape[2]), int(_v.shape[3]), int(_v.shape[4])) + if _v.dim() == 5 + else (int(_v.shape[1]), int(_v.shape[2]), int(_v.shape[3])) + ) + _vae_pixel_shapes.append(t_h_w) + + _vision_tokens = len(packed_sequence.vision.sequence_indexes) if packed_sequence.vision else 0 + _action_tokens = len(packed_sequence.action.sequence_indexes) if packed_sequence.action else 0 + _sound_tokens = len(packed_sequence.sound.sequence_indexes) if packed_sequence.sound else 0 + + output_batch = { + "x0": gen_data_clean.x0_tokens_vision, + "xt": gen_data_noised.xt_tokens_vision, + "sigma": sigmas_vision, # [B_items, T_vis] + "model_pred": out_net["preds_vision"], + "condition_mask_vision": packed_sequence.vision.condition_mask if packed_sequence.vision else None, + "condition_mask_action": packed_sequence.action.condition_mask if packed_sequence.action else None, + "und_token_length": packed_sequence.text_indexes.shape[0], + "gen_token_length": packed_sequence.sequence_length - packed_sequence.text_indexes.shape[0], + "vision_token_length": _vision_tokens, + "action_token_length": _action_tokens, + "sound_token_length": _sound_tokens, + "is_image_batch": gen_data_clean.is_image_batch, + "batch_size": gen_data_clean.batch_size, + "split_lens": packed_sequence.split_lens, + "attn_modes": packed_sequence.attn_modes, + "vae_pixel_shapes": _vae_pixel_shapes, + **losses_dict, + } + if sigmas_action is not None: + output_batch["sigma_action"] = sigmas_action # [n_action, 1] — dense over action-bearing samples + + return output_batch, loss + + def _compute_flow_matching_loss( + self, + pred: list[torch.Tensor], + target: list[torch.Tensor], + condition_mask: list[torch.Tensor], + timesteps: torch.Tensor, + has_valid_tokens: bool, + rectified_flow: RectifiedFlow, + loss_scale: float | None = None, + raw_action_dim: list[torch.Tensor] | None = None, + normalize_by_active: bool = False, + ) -> torch.Tensor: + """Compute flow matching loss for a modality. + + Args: + pred: Predicted velocity field (list of tensors, one per sample). + target: Target velocity field (list of tensors, one per sample). + Under rectified flow the target is ``v = eps - x0``. + condition_mask: Mask where 1 = clean/conditioning, 0 = noisy/generation (list of tensors). + timesteps: Diffusion timesteps for time weighting. Shape [B,1] for + base/teacher_forcing (all frames share one timestep) or [B,T_max] + for diffusion_forcing (per-frame independent timesteps). Time weights + are applied per-frame before averaging, so non-uniform weight functions + are handled correctly. + has_valid_tokens: Whether this modality has valid noisy tokens. + rectified_flow: The rectified flow object for time weighting. + loss_scale: Optional per-modality loss scale. Falls back to the global + ``rectified_flow_training_config.loss_scale`` when *None*. + normalize_by_active: When True, normalize per-instance loss by the count of + active (noisy) elements rather than all elements. Preserves the + ``sum / active_count`` semantics needed for distillation critics where + conditioned frames contribute no signal and should not dilute the + denominator. + + Returns: + tuple: A tuple containing two elements: + - Flow matching loss (or dummy loss for gradient consistency). + - Per-instance loss (or dummy loss for gradient consistency). + """ + if not has_valid_tokens: + # Dummy loss to maintain backward graph consistency across ranks + dummy_loss = 0.0 * sum(p.sum() for p in pred) + return dummy_loss, dummy_loss.unsqueeze(0) # make per-instance loss 1-D + + # condition_mask[i] is T-first with trailing singletons: [T,1,1] vision, [T,1] action. + # tw_i gets the same shape so w(σ_t) broadcasts element-wise over non-T dims. + per_instance_losses = [] + per_instance_weighted_losses = [] + + for i in range(len(pred)): + T_i = condition_mask[i].shape[0] + sqerr_i = (pred[i] - target[i]) ** 2 # vision:[C,T,H,W] action/sound:[T,D] + noisy_mask_i = 1.0 - condition_mask[i] # vision:[T,1,1] action/sound:[T,1] + if raw_action_dim is not None and raw_action_dim[i] is not None: + sqerr_i = sqerr_i[:, : raw_action_dim[i]] + if normalize_by_active: + active_count = (noisy_mask_i.sum() * (sqerr_i.numel() // noisy_mask_i.numel())).clamp(min=1) + per_instance_losses.append((sqerr_i * noisy_mask_i).sum() / active_count) # [] + else: + per_instance_losses.append((sqerr_i * noisy_mask_i).mean()) # [] + + ts_i = timesteps[i, :T_i] if timesteps.dim() > 1 else timesteps[i] # DF:[T_i] TF:[1] + tw_i = rectified_flow.train_time_weight(ts_i, self.tensor_kwargs_fp32) # DF:[T_i] TF:[1] + tw_i = tw_i.reshape(-1, *([1] * (condition_mask[i].ndim - 1))) # vision:[T_i,1,1] action/sound:[T_i,1] + if normalize_by_active: + per_instance_weighted_losses.append((sqerr_i * tw_i * noisy_mask_i).sum() / active_count) + else: + per_instance_weighted_losses.append((sqerr_i * tw_i * noisy_mask_i).mean()) + + per_instance_loss = torch.stack(per_instance_losses) # [B] + per_instance_weighted_loss = torch.stack(per_instance_weighted_losses) # [B] + return ( + per_instance_weighted_loss.mean(), # [] + per_instance_loss, # [B] + ) + + def _compute_losses( + self, + out_net: dict, + data_batch_packed: PackedSequence, + gen_data_noised: GenerationDataNoised, + timesteps: torch.Tensor, + is_image_batch: bool, + timesteps_action: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + """Compute flow matching loss and auxiliary load balancing losses. + + ``timesteps_action`` is an optional ``[n_action, 1]`` override for the action loss + time-weighting — dense over action-bearing samples, matching ``data_batch_packed.action.*``. + When None, action reuses ``timesteps`` (vision timesteps, legacy behavior). Set by + ``training_step`` under ``independent_action_schedule=True``. + """ + total_loss = 0.0 + losses_dict = {} + # ts_action shape: vision fallback [B_items, T_vis] (legacy) or [n_action, 1] (independent). + ts_action = timesteps if timesteps_action is None else timesteps_action + + rf_cfg = self.config.rectified_flow_training_config + normalize_by_active = rf_cfg.normalize_loss_by_active + if self.config.vision_gen: + assert data_batch_packed.vision is not None, "Vision packed data required when vision_gen is True" + assert isinstance(data_batch_packed.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for loss computation" + ) + rectified_flow_vision = self.rectified_flow_image if is_image_batch else self.rectified_flow_video + + fm_loss_vision, fm_loss_vision_per_instance = self._compute_flow_matching_loss( + pred=out_net["preds_vision"], + target=gen_data_noised.vt_target_vision, + condition_mask=data_batch_packed.vision.condition_mask, + timesteps=timesteps, + has_valid_tokens=has_noisy_tokens(data_batch_packed.vision), + rectified_flow=rectified_flow_vision, + normalize_by_active=normalize_by_active, + ) + loss_scale = ( + rf_cfg.image_loss_scale if is_image_batch and rf_cfg.image_loss_scale is not None else rf_cfg.loss_scale + ) + total_loss += fm_loss_vision * loss_scale + losses_dict["flow_matching_loss_vision"] = fm_loss_vision + losses_dict["flow_matching_loss_vision_per_instance"] = fm_loss_vision_per_instance + else: + losses_dict["flow_matching_loss_vision"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + if self.config.action_gen: + if data_batch_packed.action is not None: + assert isinstance(data_batch_packed.action.condition_mask, list), ( + "Action condition mask must be a list of tensors for loss computation" + ) + assert gen_data_noised.vt_target_action is not None, "Action targets required when action_gen is True" + fm_loss_action, _ = self._compute_flow_matching_loss( + pred=out_net["preds_action"], + target=gen_data_noised.vt_target_action, + condition_mask=data_batch_packed.action.condition_mask, + timesteps=ts_action, + has_valid_tokens=has_noisy_tokens(data_batch_packed.action), + rectified_flow=self.rectified_flow_action, + raw_action_dim=data_batch_packed.action.raw_action_dim, + normalize_by_active=normalize_by_active, + ) + + # Yihuai: In case the video loss is too large (1.5) and covers the action loss (0.05), we scale up the action loss to match the video loss to improve action precision. + total_loss += fm_loss_action * rf_cfg.action_loss_weight + losses_dict["flow_matching_loss_action"] = fm_loss_action + else: + # No action data in this batch. Connect the network's dummy preds_action + # to the loss so action-specific params + # (llm2action, action2llm, action_modality_embed) stay in the backward + # graph. Without this, FSDP reduce-scatter / DDP all-reduce will hang + # when other ranks do have action data. + dummy_loss = 0.0 * sum(p.sum() for p in out_net["preds_action"]) + total_loss += dummy_loss + losses_dict["flow_matching_loss_action"] = dummy_loss + else: + losses_dict["flow_matching_loss_action"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + if self.config.sound_gen: + if data_batch_packed.sound is not None: + assert isinstance(data_batch_packed.sound.condition_mask, list), ( + "Sound condition mask must be a list of tensors for loss computation" + ) + assert gen_data_noised.vt_target_sound is not None, "Sound targets required when sound_gen is True" + # Sound preds/targets are (C, T); condition_mask is (T, 1) — transpose to (1, T) for broadcasting + fm_loss_sound, _ = self._compute_flow_matching_loss( + pred=out_net["preds_sound"], + target=gen_data_noised.vt_target_sound, + condition_mask=[m.T for m in data_batch_packed.sound.condition_mask], + timesteps=timesteps, + has_valid_tokens=has_noisy_tokens(data_batch_packed.sound), + rectified_flow=self.rectified_flow_sound, + normalize_by_active=normalize_by_active, + ) + loss_scale = rf_cfg.sound_loss_scale if rf_cfg.sound_loss_scale is not None else rf_cfg.loss_scale + total_loss += fm_loss_sound * loss_scale + losses_dict["flow_matching_loss_sound"] = fm_loss_sound + else: + # No sound data in this batch. Connect the network's dummy preds_sound + # to the loss so sound-specific params (sound2llm, llm2sound, + # sound_modality_embed) stay in the backward graph. Without this, + # FSDP gradient reduce hangs when other ranks do have sound data. + dummy_loss = 0.0 * sum(p.sum() for p in out_net["preds_sound"]) + total_loss += dummy_loss + losses_dict["flow_matching_loss_sound"] = dummy_loss + else: + losses_dict["flow_matching_loss_sound"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + # 2. Load balancing auxiliary losses + for load_balancing_type in ["und", "gen"]: + lbl_metadata = out_net.get(f"lbl_metadata_{load_balancing_type}", None) + if lbl_metadata is None: + continue + load_balancing_loss = compute_load_balancing_loss( + lbl_metadata, + coeff=getattr(self.config.lbl, f"coeff_{load_balancing_type}"), + method=self.config.lbl.method, + device_mesh=self.parallel_dims.dp_mesh if self.parallel_dims else None, + ) + if load_balancing_loss is not None: + total_loss += load_balancing_loss + losses_dict[f"aux_loss_{load_balancing_type}"] = load_balancing_loss + + return total_loss, losses_dict + + def _update_train_stats(self, data_batch: dict[str, torch.Tensor]) -> None: + is_image = self.is_image_batch(data_batch) + input_key = self.input_image_key if is_image else self.input_video_key + if isinstance(self.net, WeightTrainingStat): + val = data_batch[input_key] + # For image editing data_batch[input_key] is a list-of-lists, not a tensor. + sample_count = len(val) if isinstance(val, list) else val.shape[0] + if is_image: + self.net.accum_image_sample_counter += sample_count + else: + self.net.accum_video_sample_counter += sample_count + + def _load_and_tokenize_text_data(self, data_batch: dict[str, torch.Tensor], iteration: int) -> list[list[int]]: + """ + Load and tokenize the text data from the data batch. + + Args: + data_batch (dict[str, torch.Tensor]): The data batch. + iteration (int): The current iteration number. + + Returns: + list[torch.Tensor]: The input text tokens. + """ + input_text_indexes = [] + + input_captions = data_batch[self.input_caption_key] + input_text_tokens = data_batch["text_token_ids"] + if isinstance(input_text_tokens, list): + # Convert text tokens to list of lists of ints + input_text_tokens = [tokens.tolist() for x in input_text_tokens for tokens in x] + else: + input_text_tokens = [tokens.squeeze(0).tolist() for tokens in input_text_tokens] + + return input_text_tokens + + def _get_train_noise_level_vision( + self, + batch_size: int, + is_image_batch: bool, + num_vision_latent_frames: list[int], + resolutions: list[str] | str | None = None, + num_tokens: list[int] | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Sample the rectified flow interpolation coefficient (timesteps), optionally adjust the sampled + timesteps with high sigma strategy, and obtain the corresponding normalized timestep. + + Args: + batch_size: Batch size for sampling timesteps. + is_image_batch: Whether this is an image batch (vs video). + num_vision_latent_frames: Per-sample vision latent frame counts [T_0, ..., T_{B-1}]. + For causal_training_strategy="diffusion_forcing", resamples B*T_max independent + times and returns tensors of shape [B,T_max]. For base/TF strategies, ignored — + returns shape [B,1] (all frames share the same sigma). + resolutions: Resolution string(s) (e.g., "256", "512") for dict-based shift lookup. + Can be a single string (applied to all samples) or a list of strings (one per sample). + If None, defaults to self.config.resolution (can be used for other modalities). + num_tokens: Number of tokens for each sample (before 2x2 merge). Needed for dynamic shift. + + Returns: + (timesteps, sigmas): Both [B,1] for TF/base, or [B,T_max] for diffusion_forcing. + """ + + + rectified_flow = self.rectified_flow_image if is_image_batch else self.rectified_flow_video + + assert not self.config.rectified_flow_training_config.use_discrete_rf, ( + "Discrete RF is not supported for Cosmos3" + ) + # Continuous RF implementation + max_timestep = rectified_flow.noise_scheduler.config.num_train_timesteps + + # Get shift value(s) - support both int and dict-based resolution lookup + shift_config = self.config.rectified_flow_training_config.shift + if isinstance(shift_config, int): + # Int-based shift: use directly for all samples + shifts = torch.full((batch_size,), shift_config, dtype=torch.float32) + else: + # Convert to plain dict to avoid traceback-based memory leaks when GC is disabled + # (OmegaConf's `in` operator uses exception control flow internally). + shift_dict = dict(shift_config) + if not is_image_batch and "dynamic_shift_base_num_tokens_video" in shift_dict: + # Dynamic shift based on token count + assert num_tokens is not None and len(num_tokens) == batch_size + base_num_tokens = shift_dict["dynamic_shift_base_num_tokens_video"] + shifts = torch.sqrt(torch.tensor(num_tokens, dtype=torch.float32) / base_num_tokens) + elif is_image_batch and "dynamic_shift_base_num_tokens_image" in shift_dict: + assert num_tokens is not None and len(num_tokens) == batch_size + base_num_tokens = shift_dict["dynamic_shift_base_num_tokens_image"] + shifts = torch.sqrt(torch.tensor(num_tokens, dtype=torch.float32) / base_num_tokens) + else: + # Dict-based shift: lookup per sample + if resolutions is None: + raise ValueError("Resolutions must be provided when shift is a dict") + + # Normalize to list format + if isinstance(resolutions, str): + resolutions = [resolutions] * batch_size + + assert len(resolutions) == batch_size, ( + f"Number of resolutions ({len(resolutions)}) must match batch_size ({batch_size})" + ) + + # Lookup shift per sample + shifts_list = [] + for resolution in resolutions: + if resolution not in shift_dict: + raise ValueError( + f"Resolution '{resolution}' not found in shift dict. Available resolutions: {list(shift_dict.keys())}" + ) + shifts_list.append(shift_dict[resolution]) + shifts = torch.tensor(shifts_list, dtype=torch.float32) + + # Sample noise times: B×T_max for DF (one per video latent frame), B×1 for base/TF + if self.config.causal_training_strategy == "diffusion_forcing": + # T_max = max(num_vision_latent_frames) across the batch; trailing entries for shorter + # sequences are unused (sliced away in _add_noise_to_input). + T_max = max(num_vision_latent_frames) + t_raw = ( + rectified_flow.sample_train_time(batch_size * T_max) + .to(**self.tensor_kwargs_fp32) + .reshape(batch_size, T_max) + ) # [B,T_max] + else: + t_raw = rectified_flow.sample_train_time(batch_size).to(**self.tensor_kwargs_fp32).unsqueeze(1) # [B,1] + + # Apply shift and scale: t_raw ∈ [0,1] → timesteps ∈ [0,max_timestep] + # shifts.unsqueeze(1) → [B,1], broadcasts with both [B,1] (base/TF) and [B,T_max] (DF) + t = 1 - t_raw # [B,1] or [B,T_max] + shifts_2d = shifts.unsqueeze(1).to(t_raw.device) # [B,1], broadcasts with [B,1] and [B,T_max] + timesteps = shifts_2d * t / (1 + (shifts_2d - 1) * t) * max_timestep # [B,1] or [B,T_max] + + if self.config.rectified_flow_training_config.use_high_sigma_strategy: + timesteps = self._apply_high_noise_strategy(timesteps, max_timestep) # [B,1] or [B,T_max] + + sigmas = timesteps / max_timestep # [B,1] for base/TF, [B,T_max] for DF + return timesteps, sigmas + + def _apply_high_noise_strategy(self, timesteps: torch.Tensor, max_timestep: int) -> torch.Tensor: + """ + Update the sampled RF timesteps to shift the distribution towards higher noise levels (high sigmas). + + Args: + timesteps (torch.Tensor): Input timesteps. Shape [B,1] for base/TF or [B,T_max] for DF. + max_timestep (int): The maximum timestep value. + + Returns: + torch.Tensor: Timesteps with the same shape as input — [B,1] or [B,T_max]. + """ + mask = ( + torch.rand(timesteps.shape, device=timesteps.device) + < self.config.rectified_flow_training_config.high_sigma_ratio + ) + new_timesteps = ( + torch.rand(timesteps.shape, device=timesteps.device).type_as(timesteps) + * ( + self.config.rectified_flow_training_config.high_sigma_timesteps_max + - self.config.rectified_flow_training_config.high_sigma_timesteps_min + ) + + self.config.rectified_flow_training_config.high_sigma_timesteps_min + ) + timesteps = torch.where(mask, new_timesteps, timesteps) + + return timesteps + + def _get_train_noise_level_action(self, batch_size: int) -> tuple[torch.Tensor, torch.Tensor]: + """Sample ``(timesteps, sigmas)`` of shape ``[batch_size, 1]`` from ``rectified_flow_action``. + + This helper is locally-scoped: it just draws ``batch_size`` independent σ values and + applies action-specific shift / high-sigma config. The caller decides what ``batch_size`` + means semantically — ``training_step`` passes the full batch size and then reindexes to + the dense action-bearing subset with ``action_sample_indices``. + + ``shift_action`` must be an int (or ``None`` to inherit ``shift``). Dict-keyed + per-resolution shifts are vision-only — multi-resolution action training would need + per-sample lookup, which this helper does not implement; if the global ``shift`` is a + dict and ``shift_action`` is None, this raises so the user sets shift_action explicitly. + ``use_high_sigma_strategy_action`` toggles the high-σ strategy for action; when on, the + global ``high_sigma_ratio`` / ``_min`` / ``_max`` apply. σ is a shared scalar per input + slot (no per-frame σ for action). + """ + rf_cfg = self.config.rectified_flow_training_config + rf = self.rectified_flow_action + max_timestep = rf.noise_scheduler.config.num_train_timesteps # int + + # Resolve shift. shift_action, when provided, must be an int. + if rf_cfg.shift_action is not None: + if not isinstance(rf_cfg.shift_action, int): + raise ValueError( + f"shift_action must be an int; got {type(rf_cfg.shift_action).__name__}. " + "Dict-keyed per-resolution shifts are vision-only." + ) + shift_val = rf_cfg.shift_action # int + elif isinstance(rf_cfg.shift, int): + shift_val = rf_cfg.shift # inherit the global int shift + else: + raise ValueError( + "shift_action=None requires the global `shift` to be an int. When `shift` is a " + f"dict (multi-resolution vision training), set shift_action explicitly as an int. " + f"Got shift={rf_cfg.shift!r}." + ) + + t_raw = rf.sample_train_time(batch_size).to(**self.tensor_kwargs_fp32).unsqueeze(1) # [B,1] + t = 1 - t_raw # [B,1] + shifts_2d = torch.full((batch_size, 1), shift_val, dtype=torch.float32, device=t_raw.device) # [B,1] + timesteps = shifts_2d * t / (1 + (shifts_2d - 1) * t) * max_timestep # [B,1] + + if rf_cfg.use_high_sigma_strategy_action: + timesteps = self._apply_high_noise_strategy(timesteps, max_timestep) # [B,1] + + sigmas = timesteps / max_timestep # [B,1] + return timesteps, sigmas + + def _add_noise_to_input( + self, + gen_data_clean: GenerationDataClean, + packed_sequence: PackedSequence, + sigmas: torch.Tensor, + sigmas_action: torch.Tensor | None = None, + ) -> GenerationDataNoised: + """ + Diffusion / Flow matching forward process: apply noise of given noise level (sigmas) to input data. + + Args: + gen_data_clean (GenerationDataClean): The input dataclass containing the clean data *latents* (tokens). + packed_sequence (PackedSequence): Packed sequence with condition masks attached to modalities. + sigmas (torch.Tensor): The noise levels. Shape [B,1] for base/teacher_forcing (all video + latent frames share the same sigma) or [B,T_max] for diffusion_forcing (per-latent-frame + independent sigma). T_max is the number of video latent frames (temporally compressed + tokens), not RGB frames. In all modes, sigmas are multiplied by (1 - condition_mask) + so conditioning latent frames get sigma_eff=0 and only non-conditioned frames contribute + to the loss. + sigmas_action: Optional ``[n_action, 1]`` override for action noising — dense over + action-bearing samples, matching ``packed_sequence.action.*``. When None, action + reuses ``sigmas`` (vision σ, legacy behavior). Set by ``training_step`` when + ``independent_action_schedule=True``. + + Returns: + GenerationDataNoised: A dataclass containing the noise, noisy data (xt), and velocity field (vt). + """ + # Action sigma defaults to the shared vision sigma (legacy behavior). + # Legacy (sigmas_action=None): vision σ of shape [B_items, T_vis]. + # Independent (sigmas_action provided): dense action σ of shape [n_action, 1]. + sigmas_for_action = sigmas if sigmas_action is None else sigmas_action + # Vision + x0_vision = gen_data_clean.x0_tokens_vision # list of [C,T,H,W] + epsilon_vision = [ + torch.randn(x0_vision_i.size(), **self.tensor_kwargs_fp32) for x0_vision_i in x0_vision + ] # list of [C,T,H,W] + + # Derive noisy mask (1 for noised, 0 for clean) for sigmas computation + assert packed_sequence.vision is not None, "Packed vision data required for noise scheduling" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for noise scheduling" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for noise scheduling" + ) + + # Compute sigmas per vision item (supports variable shapes). + # For image editing, x0_tokens_vision is a flat list with multiple items per sample + # and sigmas has already been expanded to match (see _expand_per_sample_to_per_vision_item). + # Conditioning latent frames are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # view(-1,1,1)[:T_latent]: for base/TF sigmas[i] is (1,), view gives (1,1,1) and the slice is a no-op; + # for DF sigmas[i] is (T_max,) — one sigma per video latent frame — view gives (T_max,1,1) + # and [:T_latent] slices to (T_latent,1,1) matching the per-item latent frame count. + num_vision_items = len(packed_sequence.vision.condition_mask) + noisy_mask_vision = [1.0 - cond_mask for cond_mask in packed_sequence.vision.condition_mask] + sigmas_vision = [ + sigmas[i].view(-1, 1, 1)[: x0_vision[i].shape[2]] * noisy_mask_vision[i] for i in range(num_vision_items) + ] + rectified_flow_vision = ( + self.rectified_flow_image if gen_data_clean.is_image_batch else self.rectified_flow_video + ) + xt_vision, vt_vision = rectified_flow_vision.get_interpolation( + epsilon_vision, x0_vision, sigmas_vision + ) # list of [C,T,H,W], list of [C,T,H,W] + + xt_vision = [ + xt_vision_i.to(**self.tensor_kwargs) for xt_vision_i in xt_vision + ] # list of [C,T,H,W]; to make tensor compatible with the precision of the model + + # Action (x0_tokens_action is already a dense list with no None entries). + # Gate on action_gen: the dataset may emit action tensors for models that + # don't consume them (e.g. camera dataset on a vision-only config), in + # which case packed_sequence.action is None and we must skip this block. + x0_action = gen_data_clean.x0_tokens_action # list of [T,action_dim] + if self.config.action_gen and x0_action is not None and len(x0_action) > 0: + assert packed_sequence.action is not None, "Packed action data required when action tokens exist" + assert packed_sequence.action.condition_mask is not None, ( + "Action condition mask required when action tokens exist" + ) + action_batch_size = len(packed_sequence.action.condition_mask) + all_actions_are_conditioning = all( + torch.all(condition_mask == 1).item() for condition_mask in packed_sequence.action.condition_mask + ) + if all_actions_are_conditioning: + epsilon_action = [ + torch.zeros(x0_action_i.size(), **self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + sigmas_action = [ + torch.zeros_like(condition_mask, dtype=torch.float32, device=condition_mask.device) + for condition_mask in packed_sequence.action.condition_mask + ] # list of [T,1] + xt_action = [ + x0_action_i.to(**self.tensor_kwargs) for x0_action_i in x0_action + ] # list of [T,action_dim] + vt_action = [ + torch.zeros(x0_action_i.size(), **self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + else: + epsilon_action = [ + torch.randn(x0_action_i.size(), **self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + # Conditioning action timesteps are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # Action timesteps are aligned 1-to-1 with video latent frames, not RGB frames. + # view(-1,1)[:T_i]: for base/TF sigmas[i] is (1,) → (1,1), slice is a no-op; + # for DF sigmas[i] is (T_max,) → (T_max,1) → (T_i,1) per-action-timestep sigmas. + # condition_mask[i] shape [T_i,1]; result broadcasts with x0 shape [T_i,C]. + sigmas_action = [ + sigmas_for_action[i].view(-1, 1)[: x0_action[i].shape[0]] + * (1.0 - packed_sequence.action.condition_mask[i]) + for i in range(action_batch_size) + ] # list of [T_i,1] + xt_action, vt_action = self.rectified_flow_action.get_interpolation( + epsilon_action, x0_action, sigmas_action + ) # list of [T,action_dim], list of [T,action_dim] + xt_action = [ + xt_action_i.to(**self.tensor_kwargs) for xt_action_i in xt_action + ] # list of [T,action_dim]; to make tensor compatible with the precision of the model + for i in range(len(xt_action)): + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + xt_action[i][:, gen_data_clean.raw_action_dim[i] :] = 0 + + else: + epsilon_action = None + sigmas_action = None + xt_action = None + vt_action = None + + # Sound (x0_tokens_sound is a list of [C, T] tensors, or None) + x0_sound = gen_data_clean.x0_tokens_sound # list of [sound_channels,T_sound] + if x0_sound is not None and len(x0_sound) > 0: + assert packed_sequence.sound is not None, "Packed sound data required when sound tokens exist" + assert packed_sequence.sound.condition_mask is not None, ( + "Sound condition mask required when sound tokens exist" + ) + sound_batch_size = len(packed_sequence.sound.condition_mask) + epsilon_sound = [torch.randn(x0_i.size(), **self.tensor_kwargs_fp32) for x0_i in x0_sound] + # Conditioning frames are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # view(-1,1)[:T_sound].T: for base/TF sigmas[i] is (1,) → (1,1) → no-op → (1,1); + # for DF sigmas[i] is (T_max,) → (T_max,1) → (T_sound,1) → (1,T_sound). + # condition_mask[i] shape [T_sound,1]; .T gives [1,T_sound]; result broadcasts with x0 [C,T_sound]. + sigmas_sound = [ + sigmas[i].view(-1, 1)[: x0_sound[i].shape[1]].T * (1.0 - packed_sequence.sound.condition_mask[i].T) + for i in range(sound_batch_size) + ] + xt_sound, vt_sound = self.rectified_flow_sound.get_interpolation(epsilon_sound, x0_sound, sigmas_sound) + xt_sound = [xt_i.to(**self.tensor_kwargs) for xt_i in xt_sound] + else: + epsilon_sound = None + sigmas_sound = None + xt_sound = None + vt_sound = None + + # create the GenerationDataNoised object + gen_data_noised = GenerationDataNoised( + batch_size=gen_data_clean.batch_size, + # vision + epsilon_vision=epsilon_vision, + xt_tokens_vision=xt_vision, + vt_target_vision=vt_vision, + sigmas_vision=sigmas_vision, + # action + epsilon_action=epsilon_action, + xt_tokens_action=xt_action, + vt_target_action=vt_action, + sigmas_action=sigmas_action, + raw_action_dim=gen_data_clean.raw_action_dim, + # sound + epsilon_sound=epsilon_sound, + xt_tokens_sound=xt_sound, + vt_target_sound=vt_sound, + sigmas_sound=sigmas_sound, + ) + + return gen_data_noised + + def _replace_clean_with_noised( + self, + packed_sequence: PackedSequence, + gen_data_noised: GenerationDataNoised, + ) -> None: + """Replace packed clean tokens with noised tokens.""" + if packed_sequence.vision is not None: + packed_sequence.vision.tokens = gen_data_noised.xt_tokens_vision + if packed_sequence.action is not None and gen_data_noised.xt_tokens_action is not None: + action_all_conditioning = all( + torch.all(condition_mask == 1).item() for condition_mask in packed_sequence.action.condition_mask + ) + if not action_all_conditioning: + packed_sequence.action.tokens = gen_data_noised.xt_tokens_action + if packed_sequence.sound is not None and gen_data_noised.xt_tokens_sound is not None: + packed_sequence.sound.tokens = gen_data_noised.xt_tokens_sound + + # ------------------------ Inference Utils ------------------------ + def _get_inference_text_tokens( + self, data_batch: dict, has_negative_prompt: bool + ) -> tuple[list[list[int]], list[list[int]]]: + """Tokenize conditional and unconditional captions for inference.""" + use_system_prompt = self.vlm_config.use_system_prompt + system_prompt: str | None = data_batch.get("system_prompt") + + cond_tokens = [ + tokenize_caption( + c, + self.vlm_tokenizer, + is_video=False, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + for c in data_batch[self.input_caption_key] + ] + + if has_negative_prompt: + neg_key = "neg_" + self.input_caption_key + assert neg_key in data_batch, f"Negative prompt ({neg_key}) not found" + uncond_captions = data_batch[neg_key] + else: + uncond_captions = [""] * len(cond_tokens) + + uncond_tokens = [ + tokenize_caption( + c, + self.vlm_tokenizer, + is_video=False, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + for c in uncond_captions + ] + return cond_tokens, uncond_tokens + + def _prepare_inference_data( + self, + data_batch: dict, + seed: list[int], + has_negative_prompt: bool = False, + ) -> tuple[ + list[SequencePlan], + GenerationDataClean, + list[list[int]], + list[list[int]], + list[torch.Tensor], + ]: + """ + Prepare all data needed for inference sampling. + Mirrors training_step's data preparation flow. + + This method: + 1. Builds sequence plans (conditioning information) + 2. Gets data and condition (encodes vision) + 3. Tokenizes text (conditional and unconditional for CFG) + 4. Builds a packed sequence to fetch conditioning masks + 5. Initializes noise with conditioning applied (as lists for variable shapes) + 6. If action_gen is True, concatenates action noise with vision noise + + Args: + data_batch: Raw data batch from dataloader. + seed: Random seed(s) for noise generation. + has_negative_prompt: If True, use negative prompt for unconditional branch. + + Returns: + Tuple of: + - sequence_plans: List of SequencePlan objects + - gen_data_clean: GenerationDataClean with encoded tokens + - cond_text_tokens: Conditional text tokens + - uncond_text_tokens: Unconditional text tokens (for CFG) + - initial_noise: List of noise tensors (one per sample), each containing + flattened vision (and optionally action) noise concatenated + """ + # 1. Build sequence plans (same as training) + sequence_plans = build_sequence_plans_from_data_batch( + data_batch=data_batch, + input_video_key=self.input_video_key, + input_image_key=self.input_image_key, + ) + + # 2. Get data and condition (same as training) + # This encodes vision to x0_tokens + gen_data_clean = self.get_data_and_condition(data_batch) + + num_items_per_sample = gen_data_clean.num_vision_items_per_sample # None for standard T2I/T2V + + # 3. Tokenize text (similar to training's _load_and_tokenize_text_data) + cond_text_tokens, uncond_text_tokens = self._get_inference_text_tokens(data_batch, has_negative_prompt) + + # 4. Build packed sequence to fetch conditioning masks + mask_timesteps = torch.zeros((gen_data_clean.batch_size,), dtype=torch.float32) # [B] + packed_sequence = self._pack_input_sequence( + sequence_plans, + cond_text_tokens, + gen_data_clean, + mask_timesteps, + include_end_of_generation_token=self._derive_include_end_of_generation_token(), + ) + + # 5. Initialize vision noise with conditioning + assert packed_sequence.vision is not None, "Packed vision data required for inference noise" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for inference noise" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for inference noise" + ) + assert gen_data_clean.x0_tokens_vision is not None, "Vision data required for inference noise" + n_sample = ( + len(gen_data_clean.x0_tokens_vision) + if gen_data_clean.num_vision_items_per_sample is None + else len(gen_data_clean.num_vision_items_per_sample) + ) + + assert len(seed) == n_sample, ( + f"Seed list length {len(seed)} must have the same length as the number of samples {n_sample}" + ) + + # For image2image, num_items_per_sample could be > 1 (multi-vision), + # so we need to repeat the seed for each vision item. + seed_dict = {"vision": [], "action": [], "sound": []} + for sample_idx in range(n_sample): + num_vision_items = num_items_per_sample[sample_idx] if num_items_per_sample is not None else 1 + seed_dict["vision"].extend([seed[sample_idx]] * num_vision_items) + seed_dict["action"].append(seed[sample_idx]) + seed_dict["sound"].append(seed[sample_idx]) + + # Generate noise and apply conditioning per vision item (supports variable shapes) + noise_vision_list: list[torch.Tensor] = [] + for i, (x0_token, cond_mask) in enumerate( + zip(gen_data_clean.x0_tokens_vision, packed_sequence.vision.condition_mask, strict=True) + ): + pure_noise_i = misc.arch_invariant_rand( + tuple(x0_token.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["vision"][i], # Different seed per sample for diversity + ) # [C,T,H,W] + noise_i = cond_mask * x0_token.to(**self.tensor_kwargs) + (1.0 - cond_mask) * pure_noise_i # [C,T,H,W] + noise_vision_list.append(noise_i) + + # 6. Initialize action noise if action_gen is True + has_action = self.config.action_gen and any(plan.has_action for plan in sequence_plans) + noise_action_list: list[torch.Tensor] | None = None + + if has_action: + assert gen_data_clean.x0_tokens_action is not None, "Action data required when sequence plan has action" + assert packed_sequence.action is not None, "Packed action data required when action_gen is True" + assert packed_sequence.action.condition_mask is not None, "Action condition mask required" + assert isinstance(packed_sequence.action.condition_mask, list), ( + "Action condition mask must be a list of tensors for inference noise" + ) + + # Generate action noise per sample (x0_tokens_action is already dense, no None entries) + noise_action_list = [] + for i, (x0_action, cond_mask_action) in enumerate( + zip(gen_data_clean.x0_tokens_action, packed_sequence.action.condition_mask, strict=True) + ): + pure_noise_action_i = misc.arch_invariant_rand( + tuple(x0_action.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["action"][i], # Different seed per sample for diversity + ) # [T,action_dim] + noise_action_i = ( + cond_mask_action * x0_action.to(**self.tensor_kwargs) + + (1.0 - cond_mask_action) * pure_noise_action_i + ) + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + noise_action_i[:, gen_data_clean.raw_action_dim[i] :] = 0 + noise_action_list.append(noise_action_i) + + # 7. Initialize sound noise if sound_gen is True + has_sound = self.config.sound_gen and any(plan.has_sound for plan in sequence_plans) + noise_sound_list: list[torch.Tensor] | None = None + + if has_sound: + assert gen_data_clean.x0_tokens_sound is not None, "Sound data required when sequence plan has sound" + assert packed_sequence.sound is not None, "Packed sound data required when sound_gen is True" + assert packed_sequence.sound.condition_mask is not None, "Sound condition mask required" + assert isinstance(packed_sequence.sound.condition_mask, list), ( + "Sound condition mask must be a list of tensors for inference noise" + ) + + noise_sound_list = [] + for i, (x0_sound, cond_mask_sound) in enumerate( + zip(gen_data_clean.x0_tokens_sound, packed_sequence.sound.condition_mask, strict=True) + ): + pure_noise_sound_i = misc.arch_invariant_rand( + tuple(x0_sound.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["sound"][i], # Different seed per sample for diversity + ) # [sound_channels,T_sound] + # cond_mask_sound is (T, 1), x0_sound is (C, T) — transpose mask for broadcasting + noise_sound_i = ( + cond_mask_sound.T * x0_sound.to(**self.tensor_kwargs) + + (1.0 - cond_mask_sound.T) * pure_noise_sound_i + ) # [sound_channels,T_sound] + noise_sound_list.append(noise_sound_i) + + # 8. Concatenate vision, action, and sound noise per sample (flattened) + # Order: [vision | action (if present) | sound (if present)] + # noise_action_list and noise_sound_list are dense (only modality-having samples), + # so we use separate indexes. + initial_noise: list[torch.Tensor] = [] + idx_vision = 0 + idx_action = 0 + idx_sound = 0 + + for i in range(n_sample): + parts = [] + + # Flatten and concatenate all vision items for this sample + num_vis = num_items_per_sample[i] if num_items_per_sample is not None else 1 + for _ in range(num_vis): + parts.append(noise_vision_list[idx_vision].reshape(-1)) + idx_vision += 1 + + if noise_action_list is not None and sequence_plans[i].has_action: + parts.append(noise_action_list[idx_action].reshape(-1)) + idx_action += 1 + + if noise_sound_list is not None and sequence_plans[i].has_sound: + parts.append(noise_sound_list[idx_sound].reshape(-1)) + idx_sound += 1 + + initial_noise.append(torch.cat(parts, dim=0)) # [N_tokens_flat] + + return ( + sequence_plans, + gen_data_clean, + cond_text_tokens, + uncond_text_tokens, + initial_noise, + ) + + def _get_velocity( + self, + *, + net: torch.nn.Module | None = None, + noise_x: list[torch.Tensor], + timestep: torch.Tensor, + text_tokens: list[list[int]], + sequence_plans: list[SequencePlan], + gen_data_clean: GenerationDataClean, + skip_text_tokens: bool = False, + ) -> list[torch.Tensor]: + """ + Compute velocity prediction for a single sampling step. + + This method handles the full pipeline for one denoising step: + 1. Splits flattened noise_x into vision (and action) parts per sample + 2. Packs the input sequence with current noisy latents + 3. Runs the network via self.denoise() + 4. Applies velocity masks (zeroes out conditioned parts) + 5. Returns flattened velocities (concatenated vision + action per sample) + + Args: + noise_x: List of noisy latents, each containing concatenated + vision (and optionally action) noise. + len(noise_x) == B, noise_x[i] is shape (D) + timestep: Current timestep for each sample + text_tokens: Tokenized text for each sample + sequence_plans: Pre-computed sequence plans (from _prepare_inference_data) + gen_data_clean: Pre-computed clean data (from _prepare_inference_data) + skip_text_tokens: If True, skip text tokens (for CFG unconditional branch) + + Returns: + Stacked flattened velocity tensors (one per sample), each containing + concatenated vision (and optionally action) velocity + """ + n_samples = len(noise_x) + is_image_batch = gen_data_clean.is_image_batch + has_action = self.config.action_gen and any(plan.has_action for plan in sequence_plans) + num_items = gen_data_clean.num_vision_items_per_sample # None for standard T2I/T2V + has_sound = self.config.sound_gen and any(plan.has_sound for plan in sequence_plans) + + # Split flattened noise_x into vision, action, and sound parts per sample + # Order must match _prepare_inference_data: [vision | action (if present) | sound (if present)] + noise_x_vision: list[torch.Tensor] = [] + noise_x_action: list[torch.Tensor] | None = [] if has_action else None + noise_x_sound: list[torch.Tensor] | None = [] if has_sound else None + + vision_offset = 0 # tracks position in the flat x0_tokens_vision list + idx_action = 0 + idx_sound = 0 + for i in range(n_samples): + n_vis = num_items[i] if num_items is not None else 1 + offset = 0 + for j in range(n_vis): + vision_shape = gen_data_clean.x0_tokens_vision[vision_offset + j].shape + vision_dim = int(torch.prod(torch.tensor(vision_shape))) + noise_vision_ij = noise_x[i][offset : offset + vision_dim].reshape(vision_shape) + noise_x_vision.append(noise_vision_ij) + offset += vision_dim + vision_offset += n_vis + + if has_action and noise_x_action is not None: + assert gen_data_clean.x0_tokens_action is not None + action_shape = gen_data_clean.x0_tokens_action[idx_action].shape + action_dim = int(torch.prod(torch.tensor(action_shape))) + noise_x_action.append(noise_x[i][offset : offset + action_dim].reshape(action_shape)) # [T,action_dim] + offset += action_dim + idx_action += 1 + + # Extract sound if present for this sample + if has_sound and noise_x_sound is not None and sequence_plans[i].has_sound: + assert gen_data_clean.x0_tokens_sound is not None + sound_shape = gen_data_clean.x0_tokens_sound[idx_sound].shape + sound_dim = int(torch.prod(torch.tensor(sound_shape))) + noise_x_sound.append( + noise_x[i][offset : offset + sound_dim].reshape(sound_shape) + ) # [sound_channels,T_sound] + offset += sound_dim + idx_sound += 1 + + gen_data_for_packing = GenerationDataClean( + batch_size=n_samples, + is_image_batch=is_image_batch, + raw_state_vision=gen_data_clean.raw_state_vision, + x0_tokens_vision=noise_x_vision, + fps_vision=gen_data_clean.fps_vision, + # Action fields + raw_state_action=gen_data_clean.raw_state_action if has_action else None, + x0_tokens_action=noise_x_action if has_action else None, + action_domain_id=gen_data_clean.action_domain_id if has_action else None, + fps_action=gen_data_clean.fps_action if has_action else None, + raw_action_dim=gen_data_clean.raw_action_dim if has_action else None, + # Sound fields + raw_state_sound=gen_data_clean.raw_state_sound if has_sound else None, + x0_tokens_sound=noise_x_sound if has_sound else None, + fps_sound=gen_data_clean.fps_sound if has_sound else None, + num_vision_items_per_sample=num_items, + ) + + packed_sequence = self._pack_input_sequence( + sequence_plans, + text_tokens, + gen_data_for_packing, + timestep.cpu(), + include_end_of_generation_token=self._derive_include_end_of_generation_token(), + skip_text_tokens=skip_text_tokens, + ) + + # Set the actual noisy latents (as lists) + if packed_sequence.vision is not None: + packed_sequence.vision.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_vision] + + if has_action and noise_x_action is not None: + assert packed_sequence.action is not None, "packed_sequence.action must exist when has_action is True" + packed_sequence.action.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_action] + packed_sequence.action.domain_id = gen_data_clean.action_domain_id + + if has_sound and noise_x_sound is not None: + assert packed_sequence.sound is not None, "packed_sequence.sound must exist when has_sound is True" + packed_sequence.sound.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_sound] + + packed_sequence.to_cuda() + + # --- Network forward --- + fps_action = gen_data_clean.fps_action if has_action else None + fps_sound = gen_data_clean.fps_sound if has_sound else None + out = self.denoise( + net=net, + data_batch_packed=packed_sequence, + fps_vision=gen_data_clean.fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + ) + + # --- Apply velocity masks --- + # Zero out velocity for conditioned parts (they don't change during sampling) + assert packed_sequence.vision is not None, "packed_sequence.vision must exist for velocity masking" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for masking" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for masking" + ) + # Compute noisy_mask per sample (supports variable shapes) + noisy_mask_vision = [1.0 - cond_mask for cond_mask in packed_sequence.vision.condition_mask] + + # Apply velocity mask per element - check if each sample has noisy tokens + velocity_vision: list[torch.Tensor] = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_vision"], noisy_mask_vision)): + # pred: [C,T,H,W], noisy_mask: [T,1,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + # Apply mask to prediction + velocity_vision.append(pred * noisy_mask.to(dtype=pred.dtype, device=pred.device)) # [C,T,H,W] + else: + # All tokens are conditioned - velocity should be zero + velocity_vision.append(torch.zeros_like(pred)) # [C,T,H,W] + + # Handle action velocity + velocity_action: list[torch.Tensor] | None = None + if ( + has_action + and packed_sequence.action is not None + and packed_sequence.action.condition_mask is not None + and isinstance(packed_sequence.action.condition_mask, list) + ): + noisy_mask_action = [1.0 - cond_mask for cond_mask in packed_sequence.action.condition_mask] + + velocity_action = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_action"], noisy_mask_action)): + # pred: [T,action_dim], noisy_mask: [T,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + v = pred * noisy_mask.to(dtype=pred.dtype, device=pred.device) # [T,action_dim] + else: + v = torch.zeros_like(pred) # [T,action_dim] + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + v[:, gen_data_clean.raw_action_dim[i] :] = 0 + velocity_action.append(v) + + # Handle sound velocity + velocity_sound: list[torch.Tensor] | None = None + if ( + has_sound + and packed_sequence.sound is not None + and packed_sequence.sound.condition_mask is not None + and isinstance(packed_sequence.sound.condition_mask, list) + ): + noisy_mask_sound = [1.0 - cond_mask for cond_mask in packed_sequence.sound.condition_mask] + + velocity_sound = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_sound"], noisy_mask_sound)): + # pred: [sound_channels,T_sound], noisy_mask: [T_sound,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + # noisy_mask is (T, 1), pred is (C, T) — transpose mask for broadcasting + velocity_sound.append( + pred * noisy_mask.T.to(dtype=pred.dtype, device=pred.device) + ) # [sound_channels,T_sound] + else: + velocity_sound.append(torch.zeros_like(pred)) # [sound_channels,T_sound] + + # Concatenate vision, action, and sound velocities per sample (flattened) + # Order must match _prepare_inference_data: [vision | action | sound] + velocity_output: list[torch.Tensor] = [] + vis_offset = 0 + idx_action = 0 + idx_sound = 0 + for i in range(n_samples): + parts = [] + n_vis = num_items[i] if num_items is not None else 1 + + for _ in range(n_vis): + parts.append(velocity_vision[vis_offset].reshape(-1)) + vis_offset += 1 + + if velocity_action is not None and sequence_plans[i].has_action: + parts.append(velocity_action[idx_action].reshape(-1)) + idx_action += 1 + + if velocity_sound is not None and sequence_plans[i].has_sound: + parts.append(velocity_sound[idx_sound].reshape(-1)) + idx_sound += 1 + + velocity_output.append(torch.cat(parts, dim=0)) # [N_tokens_flat] + + return velocity_output + + def _remove_padding_from_latent( + self, x0_tokens_vision: list[torch.Tensor], frame_size: list[torch.Tensor] + ) -> list[torch.Tensor]: + """ + Remove reflection padding from encoded latent vision tokens. + + Each sample in the batch may have different original dimensions, so we process + each sample individually and return a list of latents with varying spatial sizes. + + The padding coordinates are scaled down by the spatial compression factor since + we're operating in latent space. + + Args: + x0_tokens_vision (list[torch.Tensor]): List of encoded latent tensors, + each of shape (1, C, T, H_latent, W_latent) + where H_latent, W_latent include scaled padding. + frame_size (list[torch.Tensor]): List of tensors, each of shape (1,4) or (4,) containing + [target_h, target_w, orig_h, orig_w] for each sample (in pixel space). + + Returns: + list[torch.Tensor]: List of cropped latent tokens, each of shape (1, C, T, H_latent_cropped, W_latent_cropped). + Each element may have different spatial sizes based on original image dimensions. + """ + batch_size = len(x0_tokens_vision) + spatial_factor = self.tokenizer_vision_gen.spatial_compression_factor + cropped_latents = [] + for i in range(batch_size): + # frame_size: [target_h, target_w, orig_h, orig_w] in pixel space + # Normalize: frame_size[i] may be (1, 4) from IterativeJointDataLoader + # or (4,) when loaded from safetensors in the eval/export path. + fs = frame_size[i] + if fs.dim() == 2: + fs = fs[0] + orig_h = int(fs[2].item()) + orig_w = int(fs[3].item()) + + # Scale to latent space + if orig_h // spatial_factor == 0 or orig_w // spatial_factor == 0: + log.warning( + f"Zero-sized latent found: orig_h: {orig_h}, orig_w: {orig_w}, spatial_factor: {spatial_factor}" + ) + + orig_h_latent = max(orig_h // spatial_factor, 1) + orig_w_latent = max(orig_w // spatial_factor, 1) + + # Crop to remove padding: x0_tokens_vision[i] shape is (1, C, T, H, W) + cropped_latent = x0_tokens_vision[i][:, :, :, :orig_h_latent, :orig_w_latent].contiguous() + cropped_latents.append(cropped_latent) + + return cropped_latents + + def _run_classifier_free_guidance( + self, + cond_tokens: list[list[int]], + uncond_tokens: list[list[int]], + skip_text_tokens_for_cfg: bool, + single_velocity_fn: Callable[[list[list[int]], bool], list[torch.Tensor]], + ) -> tuple[list[torch.Tensor], list[torch.Tensor]]: + """Run classifier-free guidance, optionally in parallel via CFG parallelism. + + Args: + cond_tokens: Tokenized text for the conditional branch. + uncond_tokens: Tokenized text for the unconditional branch. + skip_text_tokens_for_cfg: If True, skip text tokens in the + unconditional branch. + single_velocity_fn: Computes velocity for a given set of tokens. + Accepts ``(tokens, skip_text_tokens)`` and returns a list of + velocity tensors (one per sample). + + Returns: + A tuple ``(cond_v, uncond_v)`` where each element is a list of + velocity tensors (one per sample). + """ + if self.parallel_dims is None or not self.parallel_dims.cfgp_enabled: + return ( + single_velocity_fn(cond_tokens, False), + single_velocity_fn(uncond_tokens, skip_text_tokens_for_cfg), + ) + + cfgp_rank = self.parallel_dims.cfgp_rank + cfgp_size = self.parallel_dims.cfgp_size + cfgp_group = self.parallel_dims.cfgp_mesh.get_group() + cfgp_peer = (cfgp_rank + 1) % cfgp_size + + if cfgp_rank == 0: + v_list = single_velocity_fn(cond_tokens, False) + else: + v_list = single_velocity_fn(uncond_tokens, skip_text_tokens_for_cfg) + + other_v_list = [torch.empty_like(v_i) for v_i in v_list] + + ops: list[dist.P2POp] = [] + for v_i, other_v_i in zip(v_list, other_v_list): + ops.append(dist.P2POp(op=dist.isend, tensor=v_i, group_peer=cfgp_peer, group=cfgp_group)) + ops.append(dist.P2POp(op=dist.irecv, tensor=other_v_i, group_peer=cfgp_peer, group=cfgp_group)) + + reqs = dist.batch_isend_irecv(ops) + for req in reqs: + req.wait() + + if cfgp_rank == 0: + return v_list, other_v_list + else: + return other_v_list, v_list + + @torch.no_grad() + def generate_samples_from_batch( + self, + data_batch: Dict, + net: torch.nn.Module | None = None, + sampler: Any | None = None, + guidance: float = 1.5, + guidance_interval: Optional[list[float]] = None, + seed: list[int] | int = 1, + n_sample: int | None = None, + has_negative_prompt: bool = False, + num_steps: int = 35, + shift: float = 5.0, + sigma_max: float = 80.0, + skip_text_tokens_for_cfg: bool = False, + normalize_cfg: bool = False, + **kwargs, + ) -> dict[str, list[torch.Tensor]]: + """ + Generate samples from the batch. Based on given batch, it will automatically determine + whether to generate image or video samples. + + This method follows the same structure as training_step: + 1. Build sequence plans + 2. Get data and condition (encode vision) + 3. Initialize noise with conditioning (as lists for variable shapes) + 4. Run sampling loop with velocity function + 5. Return latents as lists (supports variable shapes) + + Args: + data_batch (dict): Raw data batch from the dataloader. + guidance (float): Classifier-free guidance weight. + guidance_interval (list[float] | None): Optional timestep interval to apply guidance. + For the timesteps (ranging between 0-1000) that fall between the interval, we perform CFG, otherwise, we skip the unconditional generation. + seed (list[int] | int): Random seeds for noise generation. For all new use-cases, + we use a list of seeds, one for each sample. The length of the list must match + the number of samples. Legacy use-cases use a single integer seed which is + incremented by 1 for each sample. But this is not supported anymore, and will + raise an error if used. + n_sample (int | None): Number of samples to generate; defaults to batch size. + has_negative_prompt (bool): If True, use negative prompt for unconditional branch. + num_steps (int): Number of sampling steps for the diffusion process. + shift (float): Time shift parameter for the sampler. + sigma_max (float): Maximum sigma for the EDM sampler. + skip_text_tokens_for_cfg (bool): If True, skip text tokens in unconditional branch. + normalize_cfg (bool): If True, normalize the CFG output. + + Returns: + Dict with keys: + - "vision": List of vision latent tensors (one per sample, variable shapes) + - "action": List of action latent tensors or None (only present when action_gen=True and has_action) + + Raises: + ValueError: If the number of samples does not match the number of noise tensors or seeds. + ValueError: If the seed is a single integer. This is not supported anymore: `seed` must be + a list of integers, one for each sample. + """ + if isinstance(seed, int): + raise ValueError( + "Single integer seed is not supported anymore: `seed` must be a list of integers, one for each sample." + ) + assert isinstance(seed, list) + + if self.parallel_dims is not None and self.parallel_dims.cp_enabled: + seed = _broadcast_seed(seed, self.parallel_dims.cp_mesh.get_group(), self.parallel_dims.cp_rank) + + if self.parallel_dims is not None and self.parallel_dims.cfgp_enabled: + seed = _broadcast_seed(seed, self.parallel_dims.cfgp_mesh.get_group(), self.parallel_dims.cfgp_rank) + + # Prepare all data (initial noise as list of flattened tensors per sample) + ( + sequence_plans, + gen_data_clean, + cond_tokens, + uncond_tokens, + initial_noise, + ) = self._prepare_inference_data(data_batch, seed, has_negative_prompt) + + if n_sample is not None: + assert n_sample == len(initial_noise), ( + f"Number of samples {n_sample} must match number of noise tensors {len(initial_noise)}" + ) + else: + n_sample = len(initial_noise) + + assert n_sample == len(seed), f"Number of samples {n_sample} must match number of seeds {len(seed)}" + + # Create a velocity function for a single sample (for use with self.sampler). + + def velocity_fn(noise_x: list[torch.Tensor], timestep: torch.Tensor) -> list[torch.Tensor]: + # len(noise_x) == B, noise_x[i] is shape (D) + # timestep is shape (B, 1) + torch.compiler.cudagraph_mark_step_begin() + + assert timestep.ndim == 2, f"timestep must be 2D, got {timestep.shape}" + assert timestep.shape == (1, 1), f"timestep must be (1, 1), got {timestep.shape}" + + # Expand timestep to (B, 1) + timestep = timestep.repeat(len(noise_x), 1) + + def _single_velocity_fn(tokens: list[list[int]], skip_text_tokens: bool): + return self._get_velocity( + net=net, + noise_x=noise_x, + timestep=timestep, + text_tokens=tokens, + sequence_plans=sequence_plans, + gen_data_clean=gen_data_clean, + skip_text_tokens=skip_text_tokens, + ) + + # Skip unconditional branch when outside the guidance interval + needs_cfg = guidance != 1.0 + if needs_cfg and guidance_interval is not None: + assert len(guidance_interval) == 2, f"guidance_interval must be [lo, hi], got {guidance_interval}" + t_lo, t_hi = guidance_interval + needs_cfg = t_lo < timestep[0].item() < t_hi + + if not needs_cfg: + return _single_velocity_fn(cond_tokens, skip_text_tokens=False) + + cond_v, uncond_v = self._run_classifier_free_guidance( + cond_tokens=cond_tokens, + uncond_tokens=uncond_tokens, + skip_text_tokens_for_cfg=skip_text_tokens_for_cfg, + single_velocity_fn=_single_velocity_fn, + ) + + v_pred = [u_i + guidance * (c_i - u_i) for c_i, u_i in zip(cond_v, uncond_v)] + + if normalize_cfg: + v_pred = [ + v_i * (torch.norm(c_i) / (torch.norm(v_i) + 1e-8)).clamp(min=0.0, max=1.0) + for v_i, c_i in zip(v_pred, cond_v) + ] + + return v_pred + + # Run sampler for all samples at once. + sampler = sampler or self.sampler + scheduler_type = self.config.rectified_flow_inference_config.scheduler_type + if scheduler_type == "unipc": + log.info(f"Using sampler: UniPC (shift={shift}, num_steps={num_steps})") + else: + log.info(f"Using sampler: EDM (sigma_max={sigma_max}, num_steps={num_steps})") + + if scheduler_type == "unipc": + latents = sampler( + velocity_fn, + initial_noise, + num_steps=num_steps, + shift=shift, + seed=seed, + ) + else: + # EDM Sampler + chunk_sizes = [_x.shape[0] for _x in initial_noise] + initial_noise = torch.cat(initial_noise, dim=0) + + def x0_fn(noise_x: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor: + assert sigma.ndim == 0, f"sigma must be 0D, got {sigma.shape}" + timestep_rf = sigma * float(self.config.rectified_flow_inference_config.num_train_timesteps) + + # Convert noise_x to list of tensors for velocity_fn, and then + # concatenate the results back into a single tensor. + _noise_x = list(torch.split(noise_x, chunk_sizes, dim=0)) + _velocity_pred = velocity_fn(_noise_x, timestep_rf.reshape(1, 1)) + velocity_pred = torch.cat(_velocity_pred, dim=0) + + x0_pred = noise_x - sigma * velocity_pred + return x0_pred + + latents = sampler( + x0_fn, + initial_noise, + num_steps=num_steps, + sigma_max=sigma_max, + sigma_min=0.002, + solver_option="2ab", + ) + latents = list(torch.split(latents, chunk_sizes, dim=0)) + + # Split flattened latents back into vision, action, and sound + # Mirror the per-sample logic from _prepare_inference_data: + # Order: [vision | action (if present) | sound (if present)] + # action/sound lists are dense (only modality-having samples), so use separate indexes. + result_vision: list[torch.Tensor] = [] + result_action: list[torch.Tensor] = [] + result_sound: list[torch.Tensor] = [] + idx_vision = 0 + idx_action = 0 + idx_sound = 0 + num_vision_items = gen_data_clean.num_vision_items_per_sample + + for i in range(n_sample): + offset = 0 + + # Extract vision + n_vis = num_vision_items[i] if num_vision_items is not None else 1 + for j in range(n_vis): + vision_shape = gen_data_clean.x0_tokens_vision[idx_vision + j].shape + vision_dim = int(torch.prod(torch.tensor(vision_shape))) + if j == n_vis - 1: # the last vision item is the only target for each sample. + + result_vision.append(latents[i][offset : offset + vision_dim].reshape(vision_shape)) + else: # the other vision items are the condition inputs that we don't need to return + pass + offset += vision_dim + idx_vision += n_vis + + # Extract action if present + if self.config.action_gen and sequence_plans[i].has_action: + assert gen_data_clean.x0_tokens_action is not None + action_shape = gen_data_clean.x0_tokens_action[idx_action].shape + action_dim = int(torch.prod(torch.tensor(action_shape))) + result_action.append(latents[i][offset : offset + action_dim].reshape(action_shape)) + offset += action_dim + idx_action += 1 + + # Extract sound if present + if self.config.sound_gen and sequence_plans[i].has_sound: + assert gen_data_clean.x0_tokens_sound is not None + sound_shape = gen_data_clean.x0_tokens_sound[idx_sound].shape + sound_dim = int(torch.prod(torch.tensor(sound_shape))) + result_sound.append(latents[i][offset : offset + sound_dim].reshape(sound_shape)) + offset += sound_dim + idx_sound += 1 + + result: dict[str, list[torch.Tensor]] = {"vision": result_vision} + if self.config.action_gen and len(result_action) > 0: + result["action"] = result_action + if self.config.sound_gen and len(result_sound) > 0: + result["sound"] = result_sound + return result + + def _extract_condition_images_for_visualization( + self, + gen_data_clean: GenerationDataClean, + sequence_plans: list[SequencePlan], + n_samples: int, + ) -> list[torch.Tensor | None]: + """Extract condition images from gen_data_clean for visualization. + + For image editing, raw_state_vision is a flat list of individually-encoded + images (e.g. [src1, tgt1, src2, tgt2, ...]). The first vision item for + each sample is the condition (source) image. This method extracts it and + resizes to match the target for side-by-side display. + + Args: + gen_data_clean: Clean data containing raw vision states. + sequence_plans: Sequence plans for each sample. + n_samples: Number of samples to process. + + Returns: + List of condition image tensors (one per sample with condition frames). + """ + condition_images: list[torch.Tensor | None] = [] + + if gen_data_clean.num_vision_items_per_sample is not None: + # Multi-item (image editing): raw_state_vision is flat [src1, tgt1, src2, tgt2, ...] + vision_offset = 0 + for i in range(n_samples): + num_items = gen_data_clean.num_vision_items_per_sample[i] + if num_items >= 2: + cond_frame = gen_data_clean.raw_state_vision[vision_offset] # (1, C, 1, H_s, W_s) + target_frame = gen_data_clean.raw_state_vision[vision_offset + 1] # (1, C, 1, H_t, W_t) + # Resize condition frame to match target size for visualization + if cond_frame.shape[-2:] != target_frame.shape[-2:]: + cond_frame = torch.nn.functional.interpolate( + cond_frame.squeeze(2), # (1, C, H, W) + size=target_frame.shape[-2:], + mode="bilinear", + align_corners=False, + ).unsqueeze(2) # (1, C, 1, H, W) + condition_images.append(cond_frame) + else: + condition_images.append(None) + vision_offset += num_items + else: + # Standard single-item mode: check condition_frame_indexes_vision + for i in range(n_samples): + plan = sequence_plans[i] + if len(plan.condition_frame_indexes_vision) > 0 and gen_data_clean.raw_state_vision is not None: + raw_vision = gen_data_clean.raw_state_vision[i] # (1, C, T, H, W) + condition_images.append(raw_vision[:, :, 0:1, :, :]) + else: + condition_images.append(None) + + return condition_images + + def _slice_gen_data_clean(self, gen_data_clean: GenerationDataClean, start: int, limit: int) -> GenerationDataClean: + """Extract a subset of GenerationDataClean for inference. + + The samples in [start:limit] are extracted from the original GenerationDataClean. + + For image editing (``num_vision_items_per_sample`` is set), the sample index refers to + the *real sample* index. The method computes the correct slice of the flat + ``x0_tokens_vision`` / ``raw_state_vision`` lists using the item counts and + preserves ``num_vision_items_per_sample`` on the returned subset so that + downstream packing works correctly. + + Args: + gen_data_clean: GenerationDataClean to slice. + start: Start index of the slice. + limit: Limit index of the slice. + + Returns: + Sliced GenerationDataClean. + """ + # x0_tokens_action can be an empty list (e.g. image2video mode), not just None + has_action = bool(gen_data_clean.x0_tokens_action) + has_sound = bool(gen_data_clean.x0_tokens_sound) + + # Determine vision slice for this sample + num_items = gen_data_clean.num_vision_items_per_sample + if num_items is not None: + # Multi-item mode: compute flat-list offset + vis_start = sum(num_items[:start]) # number of all the vision tokens before the start + vis_end = sum(num_items[:limit]) + subset_x0_vision = gen_data_clean.x0_tokens_vision[vis_start:vis_end] + subset_raw_vision = ( + gen_data_clean.raw_state_vision[vis_start:vis_end] if gen_data_clean.raw_state_vision else None + ) + subset_num_items = num_items[start:limit] + else: + # Standard single-item mode + subset_x0_vision = gen_data_clean.x0_tokens_vision[start:limit] + subset_raw_vision = ( + gen_data_clean.raw_state_vision[start:limit] if gen_data_clean.raw_state_vision else None + ) + subset_num_items = None + fps_vision = gen_data_clean.fps_vision[start:limit] if gen_data_clean.fps_vision is not None else None + + if has_action: + subset_raw_action = ( + gen_data_clean.raw_state_action[start:limit] if gen_data_clean.raw_state_action else None + ) + x0_tokens_action = gen_data_clean.x0_tokens_action[start:limit] + fps_action = gen_data_clean.fps_action[start:limit] if gen_data_clean.fps_action is not None else None + action_domain_id = gen_data_clean.action_domain_id[start:limit] if gen_data_clean.action_domain_id else None + raw_action_dim = gen_data_clean.raw_action_dim[start:limit] if gen_data_clean.raw_action_dim else None + else: + subset_raw_action = None + x0_tokens_action = None + fps_action = None + action_domain_id = None + raw_action_dim = None + + if has_sound: + subset_raw_sound = gen_data_clean.raw_state_sound[start:limit] if gen_data_clean.raw_state_sound else None + x0_tokens_sound = gen_data_clean.x0_tokens_sound[start:limit] + fps_sound = gen_data_clean.fps_sound[start:limit] if gen_data_clean.fps_sound is not None else None + else: + subset_raw_sound = None + x0_tokens_sound = None + fps_sound = None + + return GenerationDataClean( + batch_size=limit - start, + is_image_batch=gen_data_clean.is_image_batch, + raw_state_vision=subset_raw_vision, + raw_state_action=subset_raw_action, + raw_state_sound=subset_raw_sound, + x0_tokens_vision=subset_x0_vision, + x0_tokens_action=x0_tokens_action, + x0_tokens_sound=x0_tokens_sound, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + action_domain_id=action_domain_id, + raw_action_dim=raw_action_dim, + num_vision_items_per_sample=subset_num_items, + ) + + @torch.no_grad() + def validation_step(self, data_batch: dict[str, torch.Tensor], iteration: int): + pass + + @torch.no_grad() + def forward(self, xt, t): + pass + + def get_data_and_condition(self, data_batch: dict[str, torch.Tensor], iteration: int = 1) -> GenerationDataClean: + """ + - Get raw data of different modalities from databatch + - Tokenize into corresponding latents + - Load other conditioning information if any (fps, etc.) + """ + # Detect whether any sample has multiple vision items (e.g. image editing). + # If so, track the count per sample before all vision items from this batch are flattened into a list. + is_image_batch = self.is_image_batch(data_batch) + sample_vision_list = data_batch[self.input_image_key if is_image_batch else self.input_video_key] + + + # we should always get this information here during training. If we can read this field + # from data_batch it means we are in the visualization callback: + if "num_vision_items_per_sample" not in data_batch: + # Each element must be a list/tuple of tensors (not a bare tensor) to count + # as multi-vision. A bare tensor's len() returns its first dim size (e.g. C=3), + # which would incorrectly trigger the multi-vision path for regular video batches. + has_multiple_vision_per_sample = any( + isinstance(v, (list, tuple)) and len(v) > 1 for v in sample_vision_list + ) + num_vision_items_per_sample: list[int] | None = ( + [len(v) for v in sample_vision_list] if has_multiple_vision_per_sample else None + ) + + # information is only stored in the GenerationDataClean object which will be discarded + # outside the training loop. Error will be raised when the data batch is passed to the + # visualization callbacks. + data_batch["num_vision_items_per_sample"] = num_vision_items_per_sample + + # if has_multiple_vision_per_sample, this means that the input media is a list of lists of tensors, we need to flatten it to a list of tensors + if has_multiple_vision_per_sample: + media_key = self.input_video_key if not is_image_batch else self.input_image_key + data_batch[media_key] = [item.unsqueeze(0) for sublist in sample_vision_list for item in sublist] + if data_batch[media_key][0].dtype == torch.float32 and not is_image_batch: + data_batch["is_preprocessed"] = ( + True # for video batch, is_processed = True means the video data is normalized. However, for the image batch, is_processed = True means the image data is augmented with a temporal dimension. + ) + else: + num_vision_items_per_sample = data_batch["num_vision_items_per_sample"] + + batch_size = ( + len(sample_vision_list) if num_vision_items_per_sample is None else len(num_vision_items_per_sample) + ) + + log_enc_time = False + timer = None + if TRAINING: + import wandb + + log_enc_time = iteration % self.log_enc_time_every_n == 0 and wandb.run + if log_enc_time: + timer = Timer(unit="s") + timer.start() + # Vision (image/video) raw state and tokenized latent state + self._normalize_video_databatch_inplace(data_batch) + self._augment_image_dim_inplace(data_batch) # converts each image tensor to (1, C, 1, H, W) + raw_state_vision = data_batch[self.input_image_key if is_image_batch else self.input_video_key] + x0_tokens_vision = [ + self.encode(raw_state_vision_i).contiguous().float() for raw_state_vision_i in raw_state_vision + ] + + frame_size = data_batch.get("image_size", None) + if frame_size is not None: + x0_tokens_vision = self._remove_padding_from_latent(x0_tokens_vision, frame_size) + + # Action – extract dense action / domain_id without mutating data_batch, + # so downstream callbacks can still read the original per-sample domain_ids. + raw_state_action, action_domain_id = self._normalize_action_databatch(data_batch) + x0_tokens_action = raw_state_action + raw_action_dim = data_batch.get("raw_action_dim", None) + + # Sound/audio - normalize, encode if present and sound_gen is enabled + self._normalize_sound_databatch_inplace(data_batch) + raw_state_sound = data_batch.get("sound", None) + if raw_state_sound is not None and self.tokenizer_sound_gen is not None: + x0_tokens_sound = [self.encode_sound(s).contiguous().float() for s in raw_state_sound] + else: + x0_tokens_sound = None + + # We pass the conditioning FPS along to the denoising function + # It will not be used for RoPE FPS modulation unless enabled in the training config + # Note: conditioning_fps from data is converted to TPS via temporal_compression_factor + # in VideoRopePosition3DEmb. + fps_raw = data_batch.get("conditioning_fps", None) + if isinstance(fps_raw, list): + fps_raw = torch.stack(fps_raw).flatten() # list of scalar tensors -> (B,) + fps_vision = fps_raw.to(**self.tensor_kwargs) if fps_raw is not None else None + fps_action = fps_raw.to(**self.tensor_kwargs) if fps_raw is not None else None + + # Sound FPS for RoPE alignment (constant, from config) + if x0_tokens_sound is not None: + sound_batch_size = len(x0_tokens_sound) + fps_sound = torch.full( + (sound_batch_size,), + self._get_sound_fps_for_rope(), + dtype=torch.float32, + ).to(**self.tensor_kwargs) + else: + fps_sound = None + + if TRAINING and log_enc_time and timer is not None: + timer.end() + elapsed = timer.get_cuda_time() + h, w = raw_state_vision[0].shape[-2], raw_state_vision[0].shape[-1] + resolution_label = "unknown" + for res_name, aspect_ratios in VIDEO_RES_SIZE_INFO.items(): + if (h, w) in aspect_ratios.values(): + resolution_label = res_name + if res_name == "704": + # 720 shares some aspect ratios with 704 (e.g., 1:1 at 960x960); prefer 720. + if (h, w) in VIDEO_RES_SIZE_INFO.get("720", {}).values(): + resolution_label = "720" + break + wandb.log( + { + f"timer/encoding_{resolution_label}p": elapsed, + "timer/encoding": elapsed, + }, + step=iteration, + ) + return GenerationDataClean( + batch_size=batch_size, + is_image_batch=is_image_batch, + raw_state_vision=raw_state_vision, + raw_state_action=raw_state_action, + raw_state_sound=raw_state_sound, + x0_tokens_vision=x0_tokens_vision, + x0_tokens_action=x0_tokens_action, + x0_tokens_sound=x0_tokens_sound, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + action_domain_id=action_domain_id, + num_vision_items_per_sample=num_vision_items_per_sample, + raw_action_dim=raw_action_dim, + ) + + def _normalize_video_databatch_inplace( + self, data_batch: dict[str, torch.Tensor], input_key: str | None = None + ) -> None: + """ + Normalizes video data in-place on a CUDA device to reduce data loading overhead. + + This function modifies the video data tensor within the provided data_batch dictionary + in-place, scaling the uint8 data from the range [0, 255] to the normalized range [-1, 1]. + + Args: + data_batch (dict[str, Tensor]): A dictionary containing the video data under a specific key. + This tensor is expected to be on a CUDA device and have dtype of torch.uint8. + input_key (str | None): The key for the video tensor in the data_batch. Defaults to + `self.input_video_key` if not provided. + + Side Effects: + Modifies the tensor at `input_key` within `data_batch` in-place. + + Note: + This operation is performed directly on the CUDA device to avoid the overhead associated + with moving data to/from the GPU. Ensure that the tensor is already on the appropriate device + and has the correct dtype (torch.uint8) to avoid unexpected behaviors. + """ + IS_PREPROCESSED_KEY = "is_preprocessed" + input_key = self.input_video_key if input_key is None else input_key + # only handle video batch + if input_key in data_batch: + if IS_PREPROCESSED_KEY in data_batch and data_batch[IS_PREPROCESSED_KEY] is True: + for i in range(len(data_batch[input_key])): + assert torch.is_floating_point(data_batch[input_key][i]), "Video data is not in float format." + assert torch.all((data_batch[input_key][i] >= -1.0001) & (data_batch[input_key][i] <= 1.0001)), ( + f"Video data is not in the range [-1, 1]. get data range " + f"[{data_batch[input_key][i].min()}, {data_batch[input_key][i].max()}]" + ) + else: + for i in range(len(data_batch[input_key])): + item = data_batch[input_key][i] + if isinstance(item, torch.Tensor): + item = [item] + assert item[0].dtype == torch.uint8, "Video data is not in uint8 format." + data_batch[input_key][i] = torch.stack(item).to(**self.tensor_kwargs) / 127.5 - 1.0 + data_batch[IS_PREPROCESSED_KEY] = True + + def _normalize_action_databatch( + self, data_batch: dict[str, torch.Tensor] + ) -> tuple[list[torch.Tensor] | None, list[torch.Tensor] | None]: + """Extract dense action and domain_id lists from the data batch. + + The joint dataloader produces action and domain_id data as + ``[[tensor], [None], [tensor], ...]`` (each sample wrapped in a + single-element list). This method unwraps inner lists and filters + out ``None`` entries to produce dense lists suitable for the model, + **without mutating** ``data_batch``. + + Returns: + (dense_action, dense_domain_id): Each is a list of device tensors + containing only non-None entries, or ``None`` if all entries are + ``None`` / the key is absent. + """ + dense_action = unwrap_and_densify(data_batch.get("action", None), self.tensor_kwargs) + dense_domain_id = unwrap_and_densify( + data_batch.get("domain_id", None), {"device": self.tensor_kwargs["device"]} + ) + return dense_action, dense_domain_id + + def _normalize_sound_databatch_inplace(self, data_batch: dict[str, torch.Tensor]) -> None: + """Flatten and densify nested sound lists in-place. + + The joint dataloader produces sound data as + ``[[tensor], [None], [tensor], ...]`` (each sample wrapped in a single-element + list). This method: + + 1. Unwraps inner lists: ``[[t], [None], [t]]`` -> ``[t, None, t]`` + 2. Filters out None entries: ``[t, None, t]`` -> ``[t, t]`` + 3. Moves tensors to the model device. + 4. Sets ``data_batch["sound"]`` to ``None`` if no valid sound data remains. + """ + raw_state_sound = data_batch.get("sound", None) + if not isinstance(raw_state_sound, list) or len(raw_state_sound) == 0: + data_batch["sound"] = None + return + + # Unwrap single-element inner lists produced by IterativeJointDataLoader + if isinstance(raw_state_sound[0], list): + raw_state_sound = [item[0] if isinstance(item, list) else item for item in raw_state_sound] + + # Filter out None entries (samples without audio) and move to device + raw_state_sound = [s.to(self.tensor_kwargs["device"]) for s in raw_state_sound if s is not None] + + if len(raw_state_sound) == 0: + data_batch["sound"] = None + else: + data_batch["sound"] = raw_state_sound + + def _augment_image_dim_inplace(self, data_batch: dict[str, torch.Tensor], input_key: str = None) -> None: + """ + Augments image tensors by adding a temporal dimension (B, C, H, W) -> (B, C, 1, H, W). + + Args: + data_batch (dict[str, Tensor]): A dictionary containing the image data. + input_key (str | None): The key for the image tensor. Defaults to `self.input_image_key`. + + Side Effects: + Modifies the tensor at `input_key` within `data_batch` in-place. + """ + IS_PREPROCESSED_KEY = "is_preprocessed" + + input_key = self.input_image_key if input_key is None else input_key + if input_key in data_batch: + # Check if the data has already been augmented and avoid re-augmenting + if IS_PREPROCESSED_KEY in data_batch and data_batch[IS_PREPROCESSED_KEY] is True: + for i in range(len(data_batch[input_key])): + assert data_batch[input_key][i].shape[2] == 1, ( + f"Image data is claimed be augmented while its shape is {data_batch[input_key][i].shape} for sample {i}" + ) + return + else: + new_image_tensor_list = [] + for i in range(len(data_batch[input_key])): + for img_tensor in data_batch[input_key][i]: + img_tensor = rearrange(img_tensor, "c h w -> 1 c 1 h w").contiguous() + if img_tensor.dtype == torch.uint8: + img_tensor = img_tensor.to(**self.tensor_kwargs) / 127.5 - 1.0 + new_image_tensor_list.append(img_tensor) + data_batch[input_key] = new_image_tensor_list + data_batch[IS_PREPROCESSED_KEY] = True + + # ------------------ Checkpointing ------------------ + + def state_dict(self, prefix: str = "", **kwargs) -> Dict[str, Any]: + final_state_dict = self.net.state_dict(prefix=prefix + "net.", **kwargs) + if self.config.ema.enabled: + ema_state_dict = self.net_ema.state_dict(prefix=prefix + "net_ema.", **kwargs) + final_state_dict.update(ema_state_dict) + return final_state_dict + + def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True, assign: bool = False): + """ + Loads a state dictionary into the model and optionally its EMA counterpart. + + Parameters: + state_dict (Mapping[str, Any]): A dictionary containing separate state + dictionaries for the model and potentially for an EMA version of the model + under the keys 'net' and 'net_ema', respectively. + strict (bool, optional): If True, the method will enforce that the keys in + the state dict match exactly those in the model and EMA model (if applicable). + Defaults to True. + assign (bool, optional): If True and in strict mode, will assign the state dictionary + directly rather than matching keys one-by-one. This is typically used when loading + parts of state dicts or using customized loading procedures. Defaults to False. + """ + if not strict: + raise ValueError("Strict mode is required for OmniMoTModel load_state_dict") + if assign: + raise ValueError("Assign mode is not supported for OmniMoTModel load_state_dict") + + _reg_state_dict = collections.OrderedDict() + _ema_state_dict = collections.OrderedDict() + for k, v in state_dict.items(): + if k.startswith("net."): + _reg_state_dict[k.replace("net.", "")] = v + elif k.startswith("net_ema."): + _ema_state_dict[k.replace("net_ema.", "")] = v + + state_dict = _reg_state_dict + + reg_results: _IncompatibleKeys = self.net.load_state_dict(_reg_state_dict, strict=True, assign=False) + missing_keys = reg_results.missing_keys + unexpected_keys = reg_results.unexpected_keys + + if self.config.ema.enabled: + ema_results: _IncompatibleKeys = self.net_ema.load_state_dict(_ema_state_dict, strict=True, assign=False) + missing_keys += ema_results.missing_keys + unexpected_keys += ema_results.unexpected_keys + else: + assert len(_ema_state_dict) == 0, f"EMA is disabled but EMA state dict is not empty: {len(_ema_state_dict)}" + + return _IncompatibleKeys(missing_keys=missing_keys, unexpected_keys=unexpected_keys) + + # ------------------ public methods ------------------ + + def ema_beta(self, iteration: int) -> float: + """ + Calculate the beta value for EMA update. + weights = weights * beta + (1 - beta) * new_weights + + Args: + iteration (int): Current iteration number. + + Returns: + float: The calculated beta value. + """ + iteration = iteration + self.config.ema.iteration_shift + if iteration < 1: + return 0.0 + return (1 - 1 / (iteration + 1)) ** (self.ema_exp_coefficient + 1) + + def model_param_stats(self) -> Dict[str, int]: + return {"total_learnable_param_num": self._param_count} + + def is_image_batch(self, data_batch: dict[str, torch.Tensor]) -> bool: + """Check if the data_batch contains images (vs. videos). + + We handle two types of data_batch: one from a joint_dataloader where "dataset_name" can + differentiate image_batch and video_batch, another from a single dataloader which we + assume as video_data by default. + """ + is_image = self.input_image_key in data_batch + is_video = self.input_video_key in data_batch + assert is_image != is_video, ( + "Only one of the input_image_key or input_video_key should be present in the data_batch." + ) + return is_image + + def denoise( + self, + net: torch.nn.Module | None = None, + data_batch_packed: PackedSequence | None = None, + fps_vision: torch.Tensor | None = None, + fps_action: torch.Tensor | None = None, + fps_sound: torch.Tensor | None = None, + memory: MemoryState | None = None, + ) -> dict: + """ + Runs the MoT network on a packed multi-modal sequence to predict velocity (v) targets. + + Args: + data_batch_packed: PackedSequence from `pack_input_sequence(...)`. + fps_vision: Optional FPS tensor used for RoPE FPS modulation (if enabled in config). + fps_action: Optional FPS tensor used for action RoPE FPS modulation (if enabled in config). + fps_sound: Optional FPS tensor for sound RoPE modulation (e.g., sound_latent_fps=25). + memory: Optional pre-built MemoryState for autoregressive generation + or KV-cache training. + + Returns: + dict containing: + - "preds_vision": list[Tensor[C,T,H,W]], one per sample. + - "preds_action": Velocity prediction for action modality (if action_gen enabled). + - "preds_sound": Velocity prediction for sound modality (if sound_gen enabled). + - "lbl_metadata_und": Load balancing metadata for understanding pathway (if present). + - "lbl_metadata_gen": Load balancing metadata for generation pathway (if present). + """ + net = net or self.net + out_net = net( + packed_seq=data_batch_packed, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + memory=memory, + ) + output_dict = dict() + output_dict["preds_vision"] = out_net["preds_vision"] + if self.config.action_gen and "preds_action" in out_net: + output_dict["preds_action"] = out_net["preds_action"] + if self.config.sound_gen and "preds_sound" in out_net: + output_dict["preds_sound"] = out_net["preds_sound"] + for key, value in out_net.items(): + if "lbl_metadata_" in key: + output_dict[key] = value + + return output_dict + + @torch.no_grad() + def encode(self, state: torch.Tensor) -> torch.Tensor: + return self.tokenizer_vision_gen.encode(state) + + @torch.no_grad() + def decode(self, latent: torch.Tensor) -> torch.Tensor: + return self.tokenizer_vision_gen.decode(latent) + + @torch.no_grad() + def encode_sound(self, waveform: torch.Tensor) -> torch.Tensor: + """Encode audio waveform into latent tokens. + + Args: + waveform: Audio tensor of shape (C, N). A batch dim is added/removed + internally since AVAE expects (B, C, N). + Mono audio is duplicated to stereo if the tokenizer expects 2 channels. + """ + assert self.tokenizer_sound_gen is not None, "Sound tokenizer not initialized" + # Ensure correct number of channels (AVAE typically expects stereo) + expected_channels = self.tokenizer_sound_gen.audio_channels + if waveform.shape[0] == 1 and expected_channels == 2: + waveform = waveform.repeat(2, 1) # mono → stereo + elif waveform.shape[0] > expected_channels: + waveform = waveform[:expected_channels] + # AVAE expects (B, C, N) + latent = self.tokenizer_sound_gen.encode(waveform.unsqueeze(0)) # [1,sound_channels,T_sound] + return latent.squeeze(0) # [sound_channels,T_sound] + + @torch.no_grad() + def decode_sound(self, latent: torch.Tensor) -> torch.Tensor: + """Decode sound latent tokens back to waveform. + + Args: + latent: Sound latent tensor of shape (C, T). A batch dim is added/removed + internally since AVAE expects (B, C, T). + """ + assert self.tokenizer_sound_gen is not None, "Sound tokenizer not initialized" + # AVAE expects (B, C, T) + waveform = self.tokenizer_sound_gen.decode(latent.unsqueeze(0)) # [1,audio_channels,N_samples] + return waveform.squeeze(0) # [audio_channels,N_samples] + + def _get_sound_fps_for_rope(self) -> float: + """Compute the sound FPS to pass to RoPE for temporal alignment with video. + + Returns the sound tokenizer's latent rate (e.g., 25 Hz for 48kHz/1920 hop). + This is passed as input_fps to the sound RoPE's generate_embeddings(), where + the FPS modulation formula aligns sound indices with video indices. + """ + return float(self.config.sound_latent_fps) + + def get_video_height_width(self) -> Tuple[int, int]: + return VIDEO_RES_SIZE_INFO[self.config.resolution]["9,16"] + + def get_video_latent_height_width(self) -> Tuple[int, int]: + height, width = VIDEO_RES_SIZE_INFO[self.config.resolution]["9,16"] + return ( + height // self.tokenizer_vision_gen.spatial_compression_factor, + width // self.tokenizer_vision_gen.spatial_compression_factor, + ) + + def get_num_video_latent_frames(self) -> int: + return self.config.state_t + + @contextmanager + def ema_scope(self, context=None, is_cpu=False): + if self.config.ema.enabled: + # https://github.com/pytorch/pytorch/issues/144289 + for module in self.net.modules(): + if isinstance(module, FSDPModule): + module.reshard() + self.net_ema_worker.cache(self.net.parameters(), is_cpu=is_cpu) + self.net_ema_worker.copy_to(src_model=self.net_ema, tgt_model=self.net) + if context is not None: + log.info(f"{context}: Switched to EMA weights") + try: + yield None + finally: + if self.config.ema.enabled: + for module in self.net.modules(): + if isinstance(module, FSDPModule): + module.reshard() + self.net_ema_worker.restore(self.net.parameters()) + if context is not None: + log.info(f"{context}: Restored training weights") + + def clip_grad_norm_( + self, + max_norm: float, + norm_type: float = 2.0, + error_if_nonfinite: bool = False, + foreach: Optional[bool] = None, + ): + return clip_grad_norm_( + self.net.parameters(), + max_norm, + norm_type=norm_type, + error_if_nonfinite=error_if_nonfinite, + foreach=foreach, + ) + + def add_lora( + self, + network: torch.nn.Module, + lora_rank: int = 4, + lora_alpha: int = 4, + lora_target_modules: str = "q_proj,k_proj,v_proj,output_proj,mlp.layer1,mlp.layer2", + init_lora_weights: bool = True, + ) -> None: + """Add LoRA (Low-Rank Adaptation) adapters to `self.net`. + + This function injects LoRA adapters into specified modules of the network, + enabling parameter-efficient fine-tuning by training only a small number + of additional parameters. + + Args: + lora_rank: The rank of the LoRA adaptation matrices. Higher rank allows + more expressiveness but uses more parameters (default: 4) + lora_alpha: Scaling parameter for LoRA. Controls the magnitude of the + LoRA adaptation (default: 4) + lora_target_modules: Comma-separated string of module names to target + for LoRA adaptation (default: attention and MLP layers) + init_lora_weights: Whether to initialize LoRA weights properly (default: True) + + Raises: + ImportError: If PEFT library is not installed + ValueError: If invalid parameters are provided + RuntimeError: If LoRA injection fails + """ + assert network is not None, "Network is not initialized" + if LoraConfig is None or inject_adapter_in_model is None: + raise ImportError("PEFT library is required for LoRA training. Please install it with: pip install peft") + + # Validate parameters + if lora_rank <= 0: + raise ValueError(f"LoRA rank must be positive, got {lora_rank}") + if lora_alpha <= 0: + raise ValueError(f"LoRA alpha must be positive, got {lora_alpha}") + + target_modules_list = [module.strip() for module in lora_target_modules.split(",")] + if not target_modules_list: + raise ValueError("LoRA target_modules cannot be empty") + + # Validate target modules exist in model + model_module_names = set(name for name, _ in network.named_modules()) + invalid_modules = [] + for target_module in target_modules_list: + # Check if any module contains this target pattern + if not any(target_module in module_name for module_name in model_module_names): + invalid_modules.append(target_module) + + if invalid_modules: + log.warning(f"Target modules not found in model: {invalid_modules}") + + # Add LoRA to model + self.lora_alpha = lora_alpha + + log.info(f"Adding LoRA adapters: rank={lora_rank}, alpha={lora_alpha}, targets={target_modules_list}") + + lora_config = LoraConfig( + r=lora_rank, + lora_alpha=lora_alpha, + init_lora_weights=init_lora_weights, + target_modules=target_modules_list, + ) + + try: + network = inject_adapter_in_model(lora_config, network) + except Exception as e: + raise RuntimeError(f"Failed to inject LoRA adapters into model: {e}") from e + + # Count and log LoRA parameters + lora_params = 0 + total_params = 0 + for name, param in network.named_parameters(): + total_params += param.numel() + if param.requires_grad: + lora_params += param.numel() + # Upcast LoRA parameters into fp32 + param.data = param.to(torch.float32) + + log.info( + f"LoRA injection successful: {lora_params:,} trainable parameters out of {total_params:,} total ({100 * lora_params / total_params:.3f}%)" + ) + + +def _broadcast_seed(seed: list[int], group: dist.ProcessGroup, rank: int) -> list[int]: + global_src_rank = torch.distributed.get_global_rank(group, 0) + + if rank == 0: + seed_tensor = torch.tensor(seed, dtype=torch.int64, device=DEVICE) # [len(seed)] + else: + seed_tensor = torch.zeros(len(seed), dtype=torch.int64, device=DEVICE) # [len(seed)] + + torch.distributed.broadcast(seed_tensor, src=global_src_rank, group=group) + return seed_tensor.tolist() diff --git a/cosmos3/_src/vfm/models/parallelize_vlm.py b/cosmos3/_src/vfm/models/parallelize_vlm.py new file mode 100644 index 00000000..5d72703f --- /dev/null +++ b/cosmos3/_src/vfm/models/parallelize_vlm.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""FSDP2 wrapping for Cosmos3 VLM ``HFModel`` instances. + +Hosts the single VLM-specific ``parallelize`` entry point used by +``vlm_model.VLMModel._init_vlm``. Lives under ``projects/cosmos3/vfm/models/`` +so the FSDP wrapping concern sits next to the model class it operates on +(mirroring the layout of ``models/mot/parallelize_unified_mot.py`` for the +MoT path). + +Pure parallelism plumbing — :class:`~projects.cosmos3.vfm.utils.parallelism.ParallelDims` +and its meshes — stays in ``vfm/utils/parallelism.py``. +""" + +import torch +from torch.distributed.fsdp import CPUOffloadPolicy, MixedPrecisionPolicy, fully_shard + +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.models.hf_model import HFModel +from cosmos3._src.vfm.utils.parallelism import ParallelDims + + +def parallelize( + model: HFModel, + parallel_dims: ParallelDims, + train_config, +) -> None: + """Apply FSDP2 to an HFModel in-place. + + Uses torch.distributed.fsdp.fully_shard (FSDP2). Each transformer block is + sharded individually for fine-grained memory savings; the outer model is then + wrapped to cover remaining parameters (embeddings, layer norms, lm_head). + + Supported architectures: + - Language models: ``inner.model.layers`` (standard HF LLM structure) + - Vision-language models: additionally ``inner.visual.blocks`` (Qwen3-VL) + + No-op when FSDP is not needed (single-GPU or replicate-only). + + Args: + model: HFModel instance (``_model`` attribute must be on meta or CPU device). + parallel_dims: ParallelDims with meshes already built via + :meth:`ParallelDims.build_meshes`. + train_config: Train sub-config — provides ``param_torch_dtype`` and + ``fsdp_offload``. Pass the TRAIN sub-config object, + not the full config. + """ + if not parallel_dims.dp_shard_enabled: + # No shard axis: dp_shard <= 1. FSDP2 (fully_shard) has nothing to do. + # For replicate-only (dp_replicate > 1, dp_shard == 1), use DDP outside + # this function. + log.info("parallelize: dp_shard <= 1 — skipping FSDP2 wrapping") + return + + mp_policy = MixedPrecisionPolicy( + param_dtype=train_config.param_torch_dtype, + reduce_dtype=torch.float32, + ) + + # 2-D (dp_replicate × dp_shard) mesh for HSDP, or 1-D dp_shard sub-mesh + # for pure FSDP. In the overlay design cp does NOT fold into the FSDP + # shard axis; cp/cfgp are handled by separate meshes. + if parallel_dims.dp_replicate_enabled: + fsdp_mesh = parallel_dims.dp_mesh + else: + fsdp_mesh = parallel_dims.dp_shard_mesh + fsdp_kwargs = {"mesh": fsdp_mesh, "mp_policy": mp_policy} + + inner = model._model + + no_split_names = set(getattr(inner, "_no_split_modules", [])) + wrapped = 0 + for module in reversed(list(inner.modules())): + if type(module).__name__ in no_split_names: + fully_shard(module, **fsdp_kwargs) + wrapped += 1 + log.info(f"Wrapped {wrapped} sub-modules.") + + # Wrap the full inner model to cover remaining parameters + # (embed_tokens, final layer norm, lm_head, visual projector stem, etc.) + cpu_offload_policy = None + if getattr(train_config, "fsdp_offload", False): + cpu_offload_policy = CPUOffloadPolicy() + + fully_shard(inner, offload_policy=cpu_offload_policy, **fsdp_kwargs) + log.info("parallelize: FSDP2 applied to HFModel._model") diff --git a/cosmos3/_src/vfm/models/utils/__init__.py b/cosmos3/_src/vfm/models/utils/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/utils/data_and_condition.py b/cosmos3/_src/vfm/models/utils/data_and_condition.py new file mode 100644 index 00000000..18120c30 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/data_and_condition.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unified data and condition interface where we save the tokenized states and/or +noised latent states for diffusion/flow-matching training. +Used for the VFM generation model. +""" + +from dataclasses import dataclass + +import torch + + +@dataclass(slots=True) +class GenerationDataClean: + """ + Container for tokenized states and conditioning info (clean states) + for the multi-modal (vision, sound, action) MoT training. + Used for the VFM generation model. + """ + + batch_size: int + # Vision (list of per-sample tensors) + is_image_batch: bool + raw_state_vision: list[torch.Tensor] | None = None # raw state in pixel space + x0_tokens_vision: list[torch.Tensor] | None = None # tokenized latent state + fps_vision: torch.Tensor | None = None + + # Image editing: number of vision items per sample. + # When set, x0_tokens_vision is a flat list of individually-encoded image latents + # (e.g. [src1, tgt1, src2, tgt2, ...]) and this field records how many items belong + # to each sample (e.g. [2, 2, ...]). None for standard T2I/T2V (one item per sample). + num_vision_items_per_sample: list[int] | None = None + + # Audio (Sound) + raw_state_sound: torch.Tensor | None = None + x0_tokens_sound: torch.Tensor | None = None + fps_sound: torch.Tensor | None = None + + # Action (dense list of per-sample tensors, only action-having samples) + raw_state_action: list[torch.Tensor] | None = None + x0_tokens_action: list[torch.Tensor] | None = None + fps_action: torch.Tensor | None = None + action_domain_id: list[torch.Tensor] | None = None # per-sample domain IDs, None when no action samples + raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation + + +@dataclass(slots=True) +class GenerationDataNoised: + """Container for states after noise addition, along with other + helper attributes for the flow-matching (gt velocity and noise) + for the multi-modal (vision, sound, action) MoT training. + Used for the VFM generation model. + """ + + batch_size: int + # Vision + epsilon_vision: torch.Tensor # unit gaussian noise tensor + xt_tokens_vision: torch.Tensor # tokens added with noise level t per flow-matching formulation + vt_target_vision: torch.Tensor # gt rectified flow field + sigmas_vision: torch.Tensor | None = None # SNR to add to the vision tokens + + # Audio (Sound) + epsilon_sound: torch.Tensor | None = None + xt_tokens_sound: torch.Tensor | None = None + vt_target_sound: torch.Tensor | None = None + sigmas_sound: torch.Tensor | None = None + + # Action + epsilon_action: torch.Tensor | None = None + xt_tokens_action: torch.Tensor | None = None + vt_target_action: torch.Tensor | None = None + sigmas_action: torch.Tensor | None = None + raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation + + +def unwrap_and_densify(raw: list | torch.Tensor | None, to_kwargs: dict) -> list[torch.Tensor] | None: + """Unwrap nested single-element lists and filter ``None`` entries. + + The joint dataloader can produce data as nested single-element lists + (e.g. ``[[t1], [None], [t2]]``). This helper flattens the nesting, + drops ``None`` entries, and moves the remaining tensors to the target + device/dtype. + + Args: + raw: The raw value from ``data_batch``. May be ``None``, a bare + tensor, or a (possibly nested) list of tensors / ``None`` s. + Each tensor in the list has shape ``(...)``. + to_kwargs: Keyword arguments forwarded to ``torch.Tensor.to`` + (e.g. ``{"device": "cuda"}`` or ``{"device": "cuda", "dtype": torch.bfloat16}``). + + Returns: + A dense list of device tensors each with shape ``(...)``, or ``None`` + if the input is ``None`` or every entry is ``None``. + + Examples: + >>> unwrap_and_densify([[t1], [None], [t2]], {"device": "cuda"}) + [t1.cuda(), t2.cuda()] + >>> unwrap_and_densify(None, {"device": "cuda"}) + None + """ + if raw is None: + return None + if not isinstance(raw, list): + return [raw.to(**to_kwargs)] # list of 1 tensor: [(...)] + # Unwrap single-element inner lists: [[t], [None]] -> [t, None] + if len(raw) > 0 and isinstance(raw[0], list): + raw = [item[0] if isinstance(item, list) else item for item in raw] + # Filter None entries and move to device + dense = [x.to(**to_kwargs) for x in raw if x is not None] # list of B tensors: [(...), ...] + return dense if dense else None + + +def _expand_per_sample_to_per_vision_item( + tensor: torch.Tensor, # [B,...] + num_vision_items_per_sample: list[int] | None, +) -> torch.Tensor: # [N_vision_items,...] + """Expand a per-sample tensor to a per-vision-item tensor. + + For image editing, each sample may contribute multiple vision items + (e.g. source + target). This helper repeats each sample's value for + all of its vision items so that downstream indexing by vision-item + position works correctly. + + Args: + tensor: Per-sample tensor of shape ``(N, ...)``. + num_vision_items_per_sample: Number of vision items per sample, + e.g. ``[2, 2, ...]``. If ``None``, the tensor is returned as-is + (standard single-item-per-sample case). + + Returns: + Tensor of shape ``(sum(num_vision_items_per_sample), ...)``, or the + original tensor when ``num_vision_items_per_sample`` is ``None``. + """ + if num_vision_items_per_sample is None: + return tensor # [B,...] + expanded = [] + for sample_idx, num_items in enumerate(num_vision_items_per_sample): + for _ in range( + num_items + ): # torch.stack(tensor[idx].repeat(num_vision_items_per_sample[idx]) for idx in range(len(num_vision_items_per_sample))) + expanded.append(tensor[sample_idx]) # [...] + return torch.stack(expanded) # [N_vision_items,...] diff --git a/cosmos3/_src/vfm/models/utils/dcp_loader.py b/cosmos3/_src/vfm/models/utils/dcp_loader.py new file mode 100644 index 00000000..0d111a90 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/dcp_loader.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Checkpoint utilities for VFM models. +Contains utilities for loading model checkpoints with key remapping. +""" + +import time + +import torch +import torch.distributed.checkpoint as dcp +from torch.distributed.checkpoint.metadata import STATE_DICT_TYPE, Metadata + +from cosmos3._src.imaginaire.checkpointer.s3_filesystem import S3StorageReader +from cosmos3._src.imaginaire.utils import log + + +class RenameLoadPlanner(dcp.DefaultLoadPlanner): + """ + RenameLoadPlanner that does two renaming operations during pretrained model load. + 1. Renames model parameters from "model." to "model.language_model." if the core model is a VLM. + 2. Renames model parameters from "_orig_mod." to "." since the pretrained model does not have torch compiled modules. + """ + + def set_up_planner( + self, + state_dict: STATE_DICT_TYPE, + metadata: Metadata | None = None, + is_coordinator: bool = False, + ) -> None: + state_dict = remove_orig_mode_from_state_dict(state_dict) + + # Renames model parameters from "model." to "model.language_model." if the core model is a VLM. + # This is necessary because the checkpoint is saved with "model.language_model." for a VLM. + # If the core model is an LLM, this renaming is not necessary. + has_language_model = any("model.language_model." in key for key in metadata.state_dict_metadata) + if has_language_model: + state_dict = insert_language_model_in_state_dict(state_dict) + + # Perform more error checking to ensure the checkpoint is valid. If the keys are missing, + # then the missing keys should be from the generation pathway. All keys from the + # understanding pathway must be present in the checkpoint. Additionally, for 2B and 4B + # dense Qwen VLMs, the `lm_head.weight` key is not present in the checkpoint. For these + # models, the input embedding and generation layer share the same params due to + # `tie_word_embeddings` being set to True in the configs. For the 0.6B LLM, 8B and 32B dense + # VLMs, and the 30B and 235B MoE VLMs, the `lm_head.weight` key is present in the + # checkpoint. + for key in state_dict: + if key not in metadata.state_dict_metadata and "_moe_gen" not in key and "lm_head.weight" not in key: + raise ValueError(f"Missing key: {key} in checkpoint.") + + # If the keys are unexpected, then the unexpected keys should be from the visual part of the + # VLM. All keys in the language part should be used by the Cosmos3 model. + for key in metadata.state_dict_metadata: + if key not in state_dict and "model.visual." not in key: + raise ValueError(f"Unexpected key: {key} in checkpoint.") + + super().set_up_planner( + state_dict=state_dict, + metadata=metadata, + is_coordinator=is_coordinator, + ) + + +def remove_orig_mode_from_state_dict(state_dict: STATE_DICT_TYPE) -> STATE_DICT_TYPE: + """Renames model parameters from "_orig_mod." to "." since the pretrained model does not have torch compiled modules.""" + return {key.replace("_orig_mod.", ""): value for key, value in state_dict.items()} + + +def insert_language_model_in_state_dict(state_dict: STATE_DICT_TYPE) -> STATE_DICT_TYPE: + """Renames model parameters from "model." to "model.language_model." if the core model is a VLM.""" + new_state_dict = {} + for key, value in state_dict.items(): + if key.startswith("model."): + new_state_dict[key.replace("model.", "model.language_model.")] = value + else: + new_state_dict[key] = value + return new_state_dict + + +def load_language_model( + model: torch.nn.Module, checkpoint_path: str, credential_path: str, enable_gcs_patch_in_boto3: bool = False +) -> None: + """ + Universal language model loading function using DCP format. + Handles key remapping for "model.language_model." -> "model." by default. + + Args: + model: The language model to load weights into + checkpoint_path: Path to checkpoint (must end with .safetensors or dcp) + credential_path: Path to S3 credentials + enable_gcs_patch_in_boto3: Whether to enable GCS patch in boto3 for DCP loading from GCS + """ + start_time = time.time() + if not checkpoint_path.strip("/").endswith("dcp"): + raise ValueError(f"Checkpoint path {checkpoint_path} must end with dcp") + + log.info(f"Loading language model weights in DCP format from: {checkpoint_path}") + state_dict = model.state_dict() + + # Choose storage reader + if checkpoint_path.startswith("s3://"): + storage_reader = S3StorageReader( + credential_path=credential_path, + path=checkpoint_path, + enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3, + ) + else: + storage_reader = dcp.FileSystemReader(checkpoint_path) + + # Load checkpoint + dcp.load( + state_dict=state_dict, + storage_reader=storage_reader, + planner=RenameLoadPlanner(allow_partial_load=False), + ) + + log.info(f"Successfully loaded language model from {checkpoint_path}") + log.info(f"Time taken to load language model: {time.time() - start_time} seconds") diff --git a/cosmos3/_src/vfm/models/utils/load_balancing_loss.py b/cosmos3/_src/vfm/models/utils/load_balancing_loss.py new file mode 100644 index 00000000..77f26876 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/load_balancing_loss.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from torch.distributed.tensor import DTensor, Partial +from torch.distributed.tensor.device_mesh import DeviceMesh + +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.qwen3_vl_moe import LBLMetadata + + +def compute_load_balancing_loss( + lbl_metadata: LBLMetadata | None, + coeff: float | None, + method: str, + device_mesh: DeviceMesh | None, +) -> torch.Tensor | None: + """ + Compute the load balancing loss. We compute the load balancing loss + for each layer, and then average the loss across all layers. + + For computing the load balancing loss for each layer, we can either + use the fraction of tokens routed to each expert for this rank ("local" method), or + use the fraction of tokens routed to each expert across all ranks ("global" method). + + Args: + lbl_metadata: The load balancing metadata. Contains the following tensors + - num_tokens_per_expert: [num_layers, num_experts] - The number of + tokens routed to each expert for this rank for each layer. + - num_tokens: [num_layers, 1] - The total number of tokens in the + batch for each layer. + - mean_router_prob_per_expert: [num_layers, num_experts] - The average + probability of routing to each expert for this rank for each layer. + coeff: The coefficient for the load balancing loss. + method: The method for the load balancing loss. Can be "local" or "global". + device_mesh: The device mesh. Only needed if method is "global". + + Returns: + The load balancing loss. None if lbl_metadata is None or coeff is None. + """ + if lbl_metadata is None or coeff is None: + return None + assert method in ["local", "global"], "Invalid method" + + num_tokens_per_expert = lbl_metadata.num_tokens_per_expert + num_experts = num_tokens_per_expert.shape[-1] + num_tokens = lbl_metadata.num_tokens + mean_router_prob_per_expert = lbl_metadata.mean_router_prob_per_expert + + if method == "global": + # Note that these collectives must be executed outside a torch compiled region + # since torch compile could reorder the collectives and cause deadlocks. + assert device_mesh is not None, "MoE models require multiple GPUs." + + num_tokens_per_expert = DTensor.from_local( + num_tokens_per_expert, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + num_tokens = DTensor.from_local( + num_tokens, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + # Compute the fraction of tokens routed to each experts. + # Summing over all experts should be equal to self.top_k. + mean_tokens_per_expert = num_tokens_per_expert.float() / num_tokens.float() + + lbl = torch.mean(torch.sum(mean_tokens_per_expert * mean_router_prob_per_expert, dim=-1) * num_experts) + return lbl * coeff diff --git a/cosmos3/_src/vfm/models/utils/memory.py b/cosmos3/_src/vfm/models/utils/memory.py new file mode 100644 index 00000000..9a6189bf --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/memory.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Abstract interfaces for persistent memory in the MoT transformer stack. + +``MemoryState`` is a *mutable* Python object that lives **outside** the +``torch.compile`` boundary. It is responsible for reading cached tensors +(``read_for_layer``) and writing new tensors back (``write_for_layer``). + +``MemoryValue`` is a *read-only* tensor container that is safe to pass +**into** a compiled decoder layer. Concrete implementations are plain +dataclasses whose fields are tensors (or None). No methods on +``MemoryValue`` should mutate state. + +``KVToStore`` is a type alias for the 4-tuple of tensors +``(gen_k, gen_v, und_k, und_v)`` returned by each compiled layer so +the caller can write them back into the cache outside the compile boundary. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import torch + +# (gen_k, gen_v, und_k, und_v) returned by each compiled layer for the caller +# to write back into the cache outside the torch.compile boundary. +KVToStore = tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] + + +@dataclass +class MemoryValue(ABC): + """Read-only tensor container safe to pass into ``torch.compile``. + + Concrete subclasses (e.g. ``ARMemoryValue``, ``KVTrainMemoryValue``) + are plain dataclasses of tensors. No methods on this class should + mutate state or perform non-trivial computation. + """ + + @property + def supports_context_parallel_attention(self) -> bool: + """Whether this memory value is compatible with context-parallel attention. + + Overridden by ``KVTrainMemoryValue`` to return ``False``. Used by + ``ContextParallelDispatch`` to reject an unsupported combination + without importing the concrete subclass. + """ + return True + + +class MemoryState(ABC): + """Mutable persistent memory that lives outside ``torch.compile``. + + The outer loop in ``_impl_forward`` calls ``read_for_layer`` before + each decoder layer and ``write_for_layer`` after. The ``MemoryState`` + object itself is **never** passed into a compiled region. + """ + + @abstractmethod + def init(self, hidden_states: dict, device: torch.device) -> None: + """Initialization per training step. + + Called once before any transformer layers are processed. + + Args: + hidden_states: The packed sequence (``FactoredSequencePack``). + device: Target device for any new tensors. + """ + + @abstractmethod + def read_for_layer(self, layer_idx: int) -> MemoryValue: + """Produce a read-only tensor snapshot for *layer_idx*. + + Used to retrieve KV values from the cache. + The returned ``MemoryValue`` is passed into the compiled decoder + layer as ``memory_value``. + """ + + @abstractmethod + def write_for_layer(self, layer_idx: int, kv_to_store: KVToStore) -> None: + """Store the K/V tensors produced by *layer_idx* back into the cache. + + Called outside the ``torch.compile`` boundary. + """ + + @abstractmethod + def is_gen_only(self) -> bool: + """Return ``True`` when only the generation pathway should run. + + When ``True``, the decoder layer assumes that the text caption has + already been processed and cached in the MemoryState object. + Used for autoregressive frame-by-frame generation of video. + """ + + @property + def uses_rolling_kv_cache(self) -> bool: + """Whether this memory uses the rolling KV-cache / compile-safe path. + + When ``True``, the network skips NATTEN metadata computation because + temporal causality is handled inside three-way attention instead. + """ + return False diff --git a/cosmos3/_src/vfm/models/utils/safetensors_loader.py b/cosmos3/_src/vfm/models/utils/safetensors_loader.py new file mode 100644 index 00000000..019b51a3 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/safetensors_loader.py @@ -0,0 +1,966 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Multi-rank weight loading utility for distributed model weight loading. +This module provides a utility class to handle multi-rank loading of model weights +from safetensors files, distributing the I/O workload across multiple ranks and +broadcasting tensors to all ranks. + +Borrowed from cosmos_rl.utils.parallelism.MultiRankWeightLoader with modifications +for loading from S3 / GCS and support for Cosmos3 VFM models. +https://github.com/nvidia-cosmos/cosmos-rl/blob/main/cosmos_rl/utils/multi_rank_weight_loader.py +""" + +import re +import time +from typing import Iterator + +import torch +import torch.distributed as dist +from safetensors.torch import load as load_safetensors +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor + +from cosmos3._src.imaginaire.flags import INTERNAL +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.utils.parallelism import ParallelDims + +# Prefixes stripped when matching checkpoint keys to model state-dict keys. +# Order matters: longest first so we get the shortest tail (most specific match). +# Ref: cosmos-rl cosmos_rl/policy/model/hf_models/__init__.py:465-472. +_VLM_KEY_PREFIXES: tuple[str, ...] = ( + "model.language_model.model.", + "model.language_model.", + "language_model.model.", + "language_model.", + "model.", + "", +) + + +def _get_dp_shard_mesh(parallel_dims: ParallelDims | None) -> DeviceMesh | None: + """Get the dp_shard mesh from the parallel dimensions. + + Args: + parallel_dims: The parallel dimensions to use for the conversion. + + Returns: + The dp_shard mesh, or None if dp_shard is not enabled. + """ + if parallel_dims is not None and parallel_dims.dp_shard_enabled: + return parallel_dims.dp_shard_mesh + else: + return None + + +def _build_model_key_by_tail(state_dict: dict) -> dict[str, str]: + """Build a tail → model_key lookup table for suffix-based key matching. + + For each model key, find the longest matching prefix in ``_VLM_KEY_PREFIXES`` + and record ``(tail -> model_key)``. Empty prefix is always a match — a key + with no known prefix becomes its own tail. + """ + table: dict[str, str] = {} + for model_key in state_dict: + for pfx in _VLM_KEY_PREFIXES: + if model_key.startswith(pfx): + tail = model_key[len(pfx) :] + if tail and tail not in table: + table[tail] = model_key + break + return table + + +def _is_moe_vlm(model: torch.nn.Module) -> bool: + """Detect whether an HF VLM is a Mixture-of-Experts model. + + MoE VLMs (Qwen3-VL-30B-A3B, Qwen3-VL-235B-A22B) need replicated-gate + + FSDP-fused-expert shard rules that load_vlm_model does NOT yet implement. + Callers use this to raise NotImplementedError before sharding. + + Detection sources (any one is sufficient): + - ``model.config.text_config.num_experts`` (if present and non-None) + - ``model.config.text_config.num_local_experts`` (if present and non-None) + - Same attributes on ``model.config`` directly (text-only fallback) + - Any state-dict key containing ``.mlp.experts.`` + """ + text_cfg = getattr(model.config, "text_config", None) or model.config + for attr in ("num_experts", "num_local_experts"): + value = getattr(text_cfg, attr, None) + if value is not None and value != 0: + return True + for name in model.state_dict().keys(): + if ".mlp.experts." in name: + return True + return False + + +def _make_name_converter( + state_dict: dict, + hf_conv_map: dict[str, str] | None, +): + """Return a callable that maps checkpoint keys to model keys. + + Two strategies, matching cosmos-rl's flow: + 1. If ``hf_conv_map`` is non-empty (transformers v4 pre-computed pattern + mapping), apply each pattern/replacement as a regex substitution. + 2. Otherwise (transformers v5 or no map), use a direct-match / suffix-lookup + fallback against the model's own state-dict keys. + """ + model_key_by_tail = _build_model_key_by_tail(state_dict) + + def convert(name: str) -> str: + if hf_conv_map: + for pattern, replacement in hf_conv_map.items(): + if re.search(pattern, name): + return re.sub(pattern, replacement, name) + return name + if name in state_dict: + return name + for pfx in _VLM_KEY_PREFIXES: + if name.startswith(pfx): + tail = name[len(pfx) :] + if tail and tail in model_key_by_tail: + return model_key_by_tail[tail] + return name + + return convert + + +class MultiRankCheckpointLoader: + """Utility class for multi-rank loading of model weights from safetensors files. + + Borrowed from cosmos_rl.utils.parallelism.MultiRankWeightLoader with modifications + for loading from S3 / GCS and support for Cosmos3 VFM models. + https://github.com/nvidia-cosmos/cosmos-rl/blob/main/cosmos_rl/utils/multi_rank_weight_loader.py + """ + + # Mapping from dtype to integer for broadcasting + DTYPE_TO_INT = { + torch.float32: 0, + torch.float16: 1, + torch.bfloat16: 2, + torch.int64: 3, + torch.int32: 4, + torch.int8: 5, + torch.uint8: 6, + torch.float8_e4m3fn: 7, + torch.float8_e5m2: 8, + } + # Mapping from integer to dtype for broadcasting + INT_TO_DTYPE = {v: k for k, v in DTYPE_TO_INT.items()} + + def __init__( + self, + dp_shard_mesh: DeviceMesh | None, + ): + """Initialize the multi-rank weight loader. + + Args: + dp_shard_mesh: 1-D ``dp_shard`` mesh, or None if dp_shard is not + enabled. Callers should obtain this via + :func:`_get_dp_shard_mesh` so the ``parallel_dims is None`` and + ``dp_shard <= 1`` cases collapse to the single-rank fallback. + """ + if dp_shard_mesh is None: + self.group = None + self.rank = 0 + self.world_size = 1 + else: + self.group = dp_shard_mesh.get_group() + self.rank = dp_shard_mesh.get_local_rank() + self.world_size = dp_shard_mesh.size() + + def load_files_parallel( + self, + checkpoint_path: str, + credential_path: str, + loading_device: torch.device, + ) -> tuple[ + dict[str, torch.Tensor], + dict[str, tuple[list, int]], + set[str], + ]: + """ + Load safetensors files in parallel across ranks. + + Args: + checkpoint_path: Path to the model directory. + credential_path: Path to the credential file for S3/GCS. + loading_device: Device to load tensors on. + + Returns: + Tuple of (rank_tensors, rank_tensor_metadata, weights_of_ckpt_names): + - rank_tensors: Dict mapping tensor names to tensors loaded by this rank. + - rank_tensor_metadata: Dict mapping tensor names to (shape, dtype_int) tuples. + - weights_of_ckpt_names: Set of all tensor names found by this rank. + """ + rank_tensors = {} # {tensor_name: tensor_data} for this rank + rank_tensor_metadata = {} # {tensor_name: (shape, dtype)} for this rank + weights_of_ckpt_names = set() + + if checkpoint_path.startswith("s3://"): + backend_args = { + "backend": "s3", + "s3_credential_path": credential_path, + } + else: + backend_args = None + + log.info(f"Loading safetensors files from: {checkpoint_path}", rank0_only=False) + log.info(f"Credential path: {credential_path}", rank0_only=False) + for file_idx, file_path in enumerate( + easy_io.list_dir_or_file( + checkpoint_path, + list_dir=False, + list_file=True, + suffix="safetensors", + recursive=False, + backend_args=backend_args, + ) + ): + file_rank = file_idx % self.world_size + if self.rank == file_rank: + log.info(f"Loading safetensors file: {file_path}", rank0_only=False) + full_path = easy_io.join_path(checkpoint_path, file_path, backend_args=backend_args) + # Download the file + weights_data = easy_io.get(full_path, backend_args=backend_args) + state_dict = load_safetensors(weights_data) + for name, tensor in state_dict.items(): + # Apply name converter if provided + weights_of_ckpt_names.add(name) + rank_tensors[name] = tensor.to(device=loading_device) + rank_tensor_metadata[name] = ( + list(tensor.shape), + self.DTYPE_TO_INT.get(tensor.dtype, 0), + ) + + return rank_tensors, rank_tensor_metadata, weights_of_ckpt_names + + def gather_tensor_names_and_build_mapping( + self, weights_of_ckpt_names: set[str], rank_tensors: dict[str, torch.Tensor] + ) -> tuple[set[str], dict[str, int]]: + """ + Gather all tensor names from all ranks and build a tensor-to-rank mapping. + + Args: + weights_of_ckpt_names: Set of tensor names found by this rank. + rank_tensors: Dict of tensors loaded by this rank. + + Returns: + Tuple of (all_tensor_names, tensor_to_rank_map): + - all_tensor_names: Set of all tensor names across all ranks. + - tensor_to_rank_map: Dict mapping tensor names to the rank that loaded them. + """ + if self.world_size > 1: + # all_gather_object requires output list to be pre-initialized with world_size + all_tensor_names_lists: list[list[str] | None] = [None] * self.world_size + dist.all_gather_object(all_tensor_names_lists, list(weights_of_ckpt_names), group=self.group) + # Flatten the list and create a set + all_tensor_names = set() + for names_list in all_tensor_names_lists: + if names_list is not None: + all_tensor_names.update(names_list) + + # Build tensor-to-rank mapping: gather which rank has which tensors + # Create a dict mapping tensor_name -> rank for this rank + local_tensor_to_rank = {name: self.rank for name in rank_tensors.keys()} + all_tensor_to_rank_dicts: list[dict[str, int] | None] = [None] * self.world_size + dist.all_gather_object(all_tensor_to_rank_dicts, local_tensor_to_rank, group=self.group) + + # Merge all dicts to create global mapping + tensor_to_rank_map = {} + for rank_idx, tensor_dict in enumerate(all_tensor_to_rank_dicts): + if tensor_dict is not None: + for tensor_name, _ in tensor_dict.items(): + if tensor_name not in tensor_to_rank_map: + tensor_to_rank_map[tensor_name] = rank_idx + # If duplicate, keep the first one (shouldn't happen, but just in case) + else: + all_tensor_names = weights_of_ckpt_names + tensor_to_rank_map = {name: 0 for name in rank_tensors.keys()} + + return all_tensor_names, tensor_to_rank_map + + def broadcast_tensor( + self, + name: str, + tensor_rank: int, + rank_tensors: dict[str, torch.Tensor], + rank_tensor_metadata: dict[str, tuple[list, int]], + device: torch.device, + ) -> torch.Tensor: + """ + Broadcast a tensor from the rank that has it to all ranks. + + Args: + name: Name of the tensor to broadcast. + tensor_rank: Rank that has the tensor. + rank_tensors: Dict of tensors loaded by this rank. + rank_tensor_metadata: Dict of tensor metadata (shape, dtype) for this rank. + device: Device to create tensors on. + + Returns: + The broadcasted tensor (same on all ranks). + """ + # Get tensor from the rank that has it + if self.rank == tensor_rank: + ckpt_tensor = rank_tensors[name] + tensor_shape, tensor_dtype_int = rank_tensor_metadata[name] + # Move tensor from CPU to GPU if needed (tensors are loaded to CPU to avoid OOM) + ckpt_tensor = ckpt_tensor.to(device=device) + else: + ckpt_tensor = None + tensor_shape = [] + tensor_dtype_int = 0 + + # Broadcast tensor metadata (shape, dtype) from the rank that has it + if self.world_size > 1: + # Ensure all ranks participate in broadcast + if self.rank == tensor_rank: + shape_len = len(tensor_shape) + shape_len_tensor = torch.tensor([shape_len], dtype=torch.long, device=device) + shape_tensor = torch.tensor(tensor_shape, dtype=torch.long, device=device) + dtype_int_tensor = torch.tensor([tensor_dtype_int], dtype=torch.long, device=device) + else: + shape_len_tensor = torch.zeros(1, dtype=torch.long, device=device) + shape_tensor = None # Will be created after knowing shape_len + dtype_int_tensor = torch.zeros(1, dtype=torch.long, device=device) + + # Broadcast shape length first + dist.broadcast(shape_len_tensor, group=self.group, group_src=tensor_rank) + shape_len = shape_len_tensor.item() + + # Create shape_tensor with correct size for all ranks + if self.rank != tensor_rank: + shape_tensor = torch.zeros(shape_len, dtype=torch.long, device=device) + + # Broadcast shape values + dist.broadcast(shape_tensor, group=self.group, group_src=tensor_rank) + + # Broadcast dtype + dist.broadcast(dtype_int_tensor, group=self.group, group_src=tensor_rank) + + if self.rank != tensor_rank: + tensor_shape = shape_tensor.cpu().tolist() + tensor_dtype = self.INT_TO_DTYPE.get(dtype_int_tensor.item(), torch.float32) + ckpt_tensor = torch.empty(tensor_shape, dtype=tensor_dtype, device=device) + + # Broadcast the actual tensor data + dist.broadcast(ckpt_tensor, group=self.group, group_src=tensor_rank) + + # Ensure ckpt_tensor is not None + if ckpt_tensor is None: + raise ValueError( + f"Failed to get tensor {name} on rank {self.rank}. " + f"tensor_rank={tensor_rank}, world_size={self.world_size}, " + f"group={self.group}" + ) + + return ckpt_tensor + + def iterate_tensors( + self, + all_tensor_names: set[str], + tensor_to_rank_map: dict[str, int], + rank_tensors: dict[str, torch.Tensor], + rank_tensor_metadata: dict[str, tuple[list, int]], + device: torch.device, + ) -> Iterator[tuple[str, torch.Tensor]]: + """ + Iterate over all tensors, broadcasting them as needed. + + Args: + all_tensor_names: Set of all tensor names across all ranks. + tensor_to_rank_map: Dict mapping tensor names to the rank that loaded them. + rank_tensors: Dict of tensors loaded by this rank. + rank_tensor_metadata: Dict of tensor metadata (shape, dtype) for this rank. + device: Device to create tensors on. + + Yields: + Tuple of (tensor_name, tensor) for each tensor. + """ + for name in sorted(all_tensor_names): + tensor_rank = tensor_to_rank_map.get(name) + if tensor_rank is None: + continue + + tensor = self.broadcast_tensor(name, tensor_rank, rank_tensors, rank_tensor_metadata, device) + yield name, tensor + + +def convert_weight_from_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """ + Convert weight from HF to Cosmos3 VFM format. This operation does a slice + operation on the tensor to convert the tensor to the current DP shard. + + Args: + tensor: The tensor to convert from HF to Cosmos3 VFM format. + name: The name of the tensor. + parallel_dims: The parallel dimensions to use for the conversion. + + Returns: + A tuple of (name, tensor) where name is the name of the tensor in the + Cosmos3 VFM format and tensor is the tensor in the Cosmos3 VFM format. + If the tensor is not supported, return None for both name and tensor. + """ + dest_name = name.replace("model.language_model.", "model.") + + if dest_name in [ + "lm_head.weight", + "model.embed_tokens.weight", + "model.norm.weight", + ]: + shard = tensor + elif ( + match := re.search( + r"layers\.(\d+)\.(input_layernorm|post_attention_layernorm)\.weight", + dest_name, + ) + ) is not None: + shard = tensor + elif ( + match := re.search( + r"layers\.(\d+)\.self_attn\.(q_norm|k_norm|v_norm)\.weight", + dest_name, + ) + ) is not None: + shard = tensor + elif ( + match := re.search( + r"layers\.(\d+)\.self_attn\.(q_proj|k_proj|v_proj|o_proj)\.weight", + dest_name, + ) + ) is not None: + shard = tensor + elif ( + match := re.search( + r"layers\.(\d+)\.mlp\.(gate_proj|up_proj|down_proj)\.weight", + dest_name, + ) + ) is not None: + # Dense Qwen3 VL model. + shard = tensor + elif ( + match := re.search( + r"layers\.(\d+)\.mlp\.experts\.(gate_up_proj|down_proj)", + dest_name, + ) + ) is not None: + # MoE Qwen3 VL model. + shard = tensor + elif (match := re.search(r"layers\.(\d+)\.mlp\.gate\.weight", dest_name)) is not None: + # MoE Qwen3 VL model. + shard = tensor + elif (match := re.search(r"model.visual", dest_name)) is not None: + # Don't load visual weights. + return None, None + else: + raise ValueError(f"Unexpected weight found in checkpoint: {dest_name}") + + return dest_name, _shard_tensor_first_dim(shard, _get_dp_shard_mesh(parallel_dims)) + + +def _shard_tensor_first_dim( + shard: torch.Tensor, + dp_shard_mesh: DeviceMesh | None, +) -> torch.Tensor: + if dp_shard_mesh is not None: + dp_shard_rank = dp_shard_mesh.get_local_rank() + dp_shard_size = dp_shard_mesh.size() + else: + dp_shard_rank = 0 + dp_shard_size = 1 + shard = shard.contiguous() + if shard.shape[0] % dp_shard_size != 0: + raise ValueError(f"Shard shape {shard.shape} is not divisible by dp_shard_size {dp_shard_size}") + return shard.tensor_split(dp_shard_size, dim=0)[dp_shard_rank].contiguous() + + +def convert_weight_from_nemotron_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """Map Nemotron VLM HF keys (56 hybrid blocks) to Cosmos3 VFM MoT keys (28 paired layers). + + The Nemotron 3 Dense VL checkpoint (NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment) + uses a hybrid layout with 56 alternating attention and MLP blocks, where: + + - Even-indexed blocks (0, 2, 4, ...) contain attention (``mixer.q/k/v/o_proj``) + - Odd-indexed blocks (1, 3, 5, ...) contain MLP (``mixer.up/down_proj``) + - Each block has a ``norm.weight`` (pre-attention or post-attention layer norm) + + The MoT model uses a standard layout with 28 paired layers, each containing both + attention and MLP sub-modules. + + Weight mapping (HF → MoT):: + + model.visual.*, model.projector.*, model.multi_modal_projector.* + → skipped (vision weights, loaded separately) + + model.lm_head.weight / lm_head.weight → lm_head.weight + model.language_model.embeddings.weight → model.embed_tokens.weight + model.language_model.norm_f.weight → model.norm.weight + + model.language_model.layers.{2i}.norm.weight + → model.layers.{i}.input_layernorm.weight + model.language_model.layers.{2i+1}.norm.weight + → model.layers.{i}.post_attention_layernorm.weight + + model.language_model.layers.{2i}.mixer.{q,k,v,o}_proj.weight + → model.layers.{i}.self_attn.{q,k,v,o}_proj.weight + + model.language_model.layers.{2i+1}.mixer.{up,down}_proj.weight + → model.layers.{i}.mlp.{up,down}_proj.weight + """ + if name.startswith("model.visual.") or name.startswith("model.projector."): + return None, None + if name.startswith("model.multi_modal_projector."): + return None, None + + dest_name: str | None = None + if name == "lm_head.weight" or name == "model.lm_head.weight": + dest_name = "lm_head.weight" + elif name == "model.language_model.embeddings.weight": + dest_name = "model.embed_tokens.weight" + elif name == "model.language_model.norm_f.weight": + dest_name = "model.norm.weight" + else: + # Layer norm: even idx → pre-attention (input_layernorm), odd idx → post-attention + m = re.match(r"model\.language_model\.layers\.(\d+)\.norm\.weight", name) + if m is not None: + idx = int(m.group(1)) + paired = idx // 2 + if idx % 2 == 0: + dest_name = f"model.layers.{paired}.input_layernorm.weight" + else: + dest_name = f"model.layers.{paired}.post_attention_layernorm.weight" + else: + # Attention projections: must be at even indices + m = re.match( + r"model\.language_model\.layers\.(\d+)\.mixer\.(q_proj|k_proj|v_proj|o_proj)\.weight", + name, + ) + if m is not None: + idx = int(m.group(1)) + if idx % 2 != 0: + raise ValueError(f"Expected attention block at even layer index, got {name}") + paired = idx // 2 + dest_name = f"model.layers.{paired}.self_attn.{m.group(2)}.weight" + else: + # MLP projections: must be at odd indices + m = re.match( + r"model\.language_model\.layers\.(\d+)\.mixer\.(up_proj|down_proj)\.weight", + name, + ) + if m is not None: + idx = int(m.group(1)) + if idx % 2 != 1: + raise ValueError(f"Expected MLP block at odd layer index, got {name}") + paired = idx // 2 + dest_name = f"model.layers.{paired}.mlp.{m.group(2)}.weight" + + if dest_name is None: + raise ValueError(f"Unexpected Nemotron checkpoint tensor: {name}") + + return dest_name, _shard_tensor_first_dim(tensor, _get_dp_shard_mesh(parallel_dims)) + + +def convert_weight_from_nemotron_llm_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """Map Nemotron pure-LLM HF keys (CosmosNemotronForCausalLM) to MoT language model keys. + + The Nemotron 3 LLM checkpoint (NVIDIA-Nemotron-3-2B-BF16) uses a standard + decoder-only layout with 28 layers, each containing attention and MLP. The key + names are already close to the MoT model's expected layout, so most keys pass + through with minimal renaming. + + Weight mapping (HF → MoT):: + + model.embeddings.weight → model.embed_tokens.weight + lm_head.weight → lm_head.weight + model.norm.weight → model.norm.weight + + model.layers.{i}.input_layernorm.weight → (unchanged) + model.layers.{i}.post_attention_layernorm.weight → (unchanged) + model.layers.{i}.self_attn.{q,k,v,o}_proj.weight → (unchanged) + model.layers.{i}.mlp.{up,down}_proj.weight → (unchanged) + """ + if name == "model.embeddings.weight": + dest_name = "model.embed_tokens.weight" + elif name in ("lm_head.weight", "model.lm_head.weight"): + dest_name = "lm_head.weight" + elif name == "model.norm.weight": + dest_name = "model.norm.weight" + elif re.match(r"model\.layers\.\d+\.(input_layernorm|post_attention_layernorm)\.weight", name): + dest_name = name + elif re.match(r"model\.layers\.\d+\.self_attn\.(q_proj|k_proj|v_proj|o_proj)\.weight", name): + dest_name = name + elif re.match(r"model\.layers\.\d+\.mlp\.(up_proj|down_proj)\.weight", name): + dest_name = name + else: + raise ValueError(f"Unexpected Nemotron LLM checkpoint tensor: {name}") + + return dest_name, _shard_tensor_first_dim(tensor, _get_dp_shard_mesh(parallel_dims)) + + +def _shard_first_dim(tensor: torch.Tensor, world_size: int, rank: int) -> torch.Tensor: + """Slice a tensor along dim 0 for FSDP sharding. + + Matches cosmos-rl weight_converter.py:71-79 semantics: even splits use + tensor_split; uneven splits use ceil-divide with the last rank getting + the remainder (may be smaller than average). This layout must match + FSDP2's local_view shape per rank — caller asserts shape equality. + """ + tensor = tensor.contiguous() + row_size = tensor.shape[0] + if world_size == 1: + return tensor + if row_size % world_size == 0: + return tensor.tensor_split(world_size, dim=0)[rank].contiguous() + avg = (row_size + world_size - 1) // world_size + start = rank * avg + end = min(start + avg, row_size) + return tensor[start:end].contiguous() + + +def detect_vlm_checkpoint_format(all_tensor_names: set[str]) -> str: + """Detect checkpoint layout: Nemotron VLM hybrid, Nemotron LLM, or Qwen3-style.""" + for n in all_tensor_names: + if "model.language_model.layers." in n and ".mixer.q_proj" in n: + return "nemotron_3_dense_vl" + if "model.embeddings.weight" in all_tensor_names: + return "nemotron_3_llm" + return "qwen3" + + +def load_language_model( + model: torch.nn.Module, + checkpoint_path: str, + credential_path: str, + parallel_dims: ParallelDims | None, + checkpoint_format: str | None = None, +) -> set[str]: + """ + Universal language model loading function using SafeTensors (.safetensors) format. + Handles key remapping for "model.language_model." -> "model." by default. + + Args: + model: The language model to load weights into. + checkpoint_path: Path to checkpoint containing .safetensors files. + credential_path: Path to S3 credentials + parallel_dims: The parallel dimensions to use for parallel loading. If None, the loading is done in a single rank. + checkpoint_format: ``"qwen3"``, ``"nemotron_3_dense_vl"``, ``"nemotron_3_llm"``, or None to auto-detect. + + Returns: + Set of weight keys that were loaded into the model. + """ + if not INTERNAL: + from cosmos3._src.imaginaire.utils.checkpoint_db import download_checkpoint, sanitize_uri + + checkpoint_path = download_checkpoint(sanitize_uri(checkpoint_path)) + + start_time = time.time() + log.info(f"Loading language model weights in safetensors format from: {checkpoint_path}") + + lm_state_dict = {} + for name, tensor in model.state_dict().items(): + # Remove the original module (torch compiled module) and checkpoint wrapped module prefixes. + final_name = name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", "") + lm_state_dict[final_name] = tensor + + # Initialize multi-rank weight loader + loader = MultiRankCheckpointLoader(_get_dp_shard_mesh(parallel_dims)) + + # Step 1: Load files in parallel + rank_tensors, rank_tensor_metadata, weights_of_ckpt_names = loader.load_files_parallel( + checkpoint_path=checkpoint_path, + credential_path=credential_path, + loading_device="cpu", + ) + + # Step 2: Gather tensor names and build mapping + all_tensor_names, tensor_to_rank_map = loader.gather_tensor_names_and_build_mapping( + weights_of_ckpt_names, rank_tensors + ) + + resolved_format = checkpoint_format or detect_vlm_checkpoint_format(all_tensor_names) + log.info(f"Language model checkpoint format: {resolved_format}", rank0_only=False) + + # Step 3: Process each tensor + weight_keys_loaded = set() + for name, tensor in loader.iterate_tensors( + all_tensor_names, + tensor_to_rank_map, + rank_tensors, + rank_tensor_metadata, + device="cuda", + ): + if resolved_format == "nemotron_3_dense_vl": + dest_name, dest_weight = convert_weight_from_nemotron_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + elif resolved_format == "nemotron_3_llm": + dest_name, dest_weight = convert_weight_from_nemotron_llm_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + else: + dest_name, dest_weight = convert_weight_from_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + if dest_name is None: + # This is due to the visual weights of VLM models. + continue + + # If the weight is not found in the language model's state dict, then the weight is + # unexpected. The unexpected weights should be from the visual part of the VLM (already + # handled by the previous check). All weights in the language part should be used by + # the Cosmos3 VFM. + if dest_name not in lm_state_dict: + raise ValueError( + f"Unexpected weight found in checkpoint: {name}, " + f"language model's corresponding weight {dest_name} not found." + ) + + target_tensor = lm_state_dict[dest_name] + is_dist_tensor = isinstance(target_tensor, DTensor) + local_view = target_tensor.to_local() if is_dist_tensor else target_tensor + + if dest_weight.device != local_view.device: + dest_weight = dest_weight.to(local_view.device) + + assert local_view.shape == dest_weight.shape, ( + f"Shape mismatch: {local_view.shape} != {dest_weight.shape} " + f"for {dest_name} with original shape {target_tensor.shape}" + ) + with torch.no_grad(): + local_view.data.copy_(dest_weight) + + weight_keys_loaded.add(dest_name) + + # Perform more error checking to ensure the checkpoint is valid. If the keys are missing, + # then the missing keys should be from the generation pathway. All keys from the + # understanding pathway must be present in the checkpoint. Additionally, for 2B and 4B + # dense Qwen VLMs, the `lm_head.weight` key is not present in the checkpoint. For these + # models, the input embedding and generation layer share the same params due to + # `tie_word_embeddings` being set to True in the configs. For the 0.6B LLM, 8B and 32B dense + # VLMs, and the 30B and 235B MoE VLMs, the `lm_head.weight` key is present in the + # checkpoint. + weight_keys_missing = set(lm_state_dict.keys()) - weight_keys_loaded + for weight_key_missing in weight_keys_missing: + if "_moe_gen" not in weight_key_missing: + log.info(f"Missing weight not found in checkpoint: {weight_key_missing}") + if "lm_head.weight" not in weight_key_missing: + raise ValueError(f"Missing weight not found in checkpoint: {weight_key_missing}.") + + log.info(f"Successfully loaded language model from {checkpoint_path}") + log.info(f"Time taken to load language model: {time.time() - start_time} seconds") + return weight_keys_loaded + + +def load_vlm_model( + model: torch.nn.Module, + checkpoint_path: str, + credential_path: str | None, + parallel_dims: ParallelDims | None, + tensor_names_to_skip: list[str] | None = None, + extra_skip_patterns: list[str] | None = None, +) -> set[str]: + """Load a HF VLM checkpoint (safetensors) into an FSDP-wrapped HFModel. + + Both ``tensor_names_to_skip`` and ``extra_skip_patterns`` are lists of + regex patterns applied to the RESOLVED model key (post-name_converter). + Phase-5 skips any model key matched by either list; Phase-6's + completeness check tolerates missing model keys matched by either + list. The two kwargs are semantically identical — separate names let + call sites distinguish "model-type fixed skips" (from + ``_tensor_names_to_skip_for``) from "overlay-specific skips" (from + ``VLMModel._init_vlm`` for the pretrain_weights_path_llm overlay). + + Cosmos-rl-style universal loader — no per-family hand-coded key mapping. + Resolves the FSDP shard sub-group via :func:`_get_dp_shard_mesh`, which + reads ``parallel_dims.dp_shard_mesh`` (the 1-D ``dp_shard`` sub-mesh + populated by ``ParallelDims.build_meshes()``). ``cp`` and ``cfgp`` live + in their own overlay meshes and do NOT participate in checkpoint sharding. + + Preconditions: + - ``parallelize()`` has been called on the HFModel (parameters are DTensors). + - ``HFModel.tie_embeddings()`` has been called before this function so that + tied ``lm_head.weight`` / ``embed_tokens.weight`` share DTensor storage. + - When ``parallel_dims`` is provided AND ``parallel_dims.dp_shard > 1``, + ``parallel_dims.build_meshes()`` MUST have been called by the caller. + Otherwise ``dp_shard_mesh`` returns None and the loader silently falls + back to single-rank loading — every rank reads every file and slices + locally, which is correct for ``dp_shard <= 1`` but a silent perf / + correctness regression for FSDP runs. Pass ``parallel_dims=None`` + explicitly for the single-process / unit-test fallback. + + Raises: + NotImplementedError: for MoE VLMs (not yet supported — see spec §2.2). + ValueError: when the checkpoint is missing a required model parameter. + + Returns: + Set of model state-dict keys successfully loaded from the checkpoint. + """ + start_time = time.time() + log.info(f"Loading VLM weights in safetensors format from: {checkpoint_path}") + + # Phase 1: canonical model state dict with compile/FSDP wrapper prefixes stripped. + vlm_state_dict = { + name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", ""): tensor + for name, tensor in model.state_dict().items() + } + + # Phase 2+3: suffix-lookup table + name converter. + hf_conv_map = getattr(model, "_checkpoint_conversion_mapping", None) + name_converter = _make_name_converter( + vlm_state_dict, + hf_conv_map=hf_conv_map if hf_conv_map else None, + ) + + # Phase 4: MoE precheck — fail early rather than silently mis-shard. + if _is_moe_vlm(model): + raise NotImplementedError( + "load_vlm_model does not yet support MoE VLMs " + "(e.g. Qwen3-VL-30B-A3B, Qwen3-VL-235B-A22B). Expected follow-up MR " + "ports cosmos-rl's is_moe_mlp_fused_into_dp_shard / replicated-gate " + "handling. Use a dense VLM checkpoint (2B, 4B, 8B, 32B) until then." + ) + + # Detect fsdp_offload mode by inspecting a sample parameter's device. In + # offload mode, the FSDP-materialized local_views live on CPU; routing the + # loader's distributed broadcast through CUDA would materialize the full + # checkpoint tensor on GPU transiently (defeats the point of offload). Use + # a single-rank fallback in that case: every rank reads every file on CPU, + # slices locally, no broadcast. I/O-redundant but memory-safe, matching + # the pre-MR _load_vlm_weights behavior under offload. + sample_target = next(iter(vlm_state_dict.values())) if vlm_state_dict else None + sample_local = sample_target.to_local() if isinstance(sample_target, DTensor) else sample_target + offload_mode = sample_local is not None and sample_local.device.type == "cpu" + + # Pick the loader's group: single-rank (no broadcast) in offload mode to + # keep memory off-GPU; the dp_shard sub-mesh otherwise. + loader = MultiRankCheckpointLoader(_get_dp_shard_mesh(parallel_dims) if not offload_mode else None) + rank_tensors, rank_tensor_meta, ckpt_names = loader.load_files_parallel( + checkpoint_path=checkpoint_path, + credential_path=credential_path if credential_path else "", + loading_device="cpu", + ) + all_tensor_names, tensor_to_rank = loader.gather_tensor_names_and_build_mapping( + ckpt_names, + rank_tensors, + ) + + # Phase 5: per-tensor copy. Skip patterns match the MODEL key (post- + # name_converter), not the raw ckpt key — this matches cosmos-rl's + # semantics and avoids fragility with prefix variations. The two lists + # are concatenated; they share Phase-5 skip + Phase-6 tolerance + # semantics. + _all_skip_patterns = (tensor_names_to_skip or []) + (extra_skip_patterns or []) + skip_patterns = [re.compile(p) for p in _all_skip_patterns] + keys_loaded: set[str] = set() + skipped_model_keys: set[str] = set() + + # Broadcast/iterate device: CUDA (NCCL) unless we're in offload mode, in + # which case everything stays on CPU. In offload mode the loader group + # is single-rank, so iterate_tensors doesn't actually broadcast — device + # just controls where the tensor is yielded. + if offload_mode or not torch.cuda.is_available(): + target_device = "cpu" + else: + target_device = "cuda" + + # Resolve the shard axis for the FSDP slicing. Even in offload mode we + # still need the real (shard_rank, shard_size) from parallel_dims so each + # rank takes its own FSDP slice — we just route the LOAD/BROADCAST through + # world_size=1 (single-rank fallback) to avoid the GPU spike. + dp_shard_mesh = _get_dp_shard_mesh(parallel_dims) + if dp_shard_mesh is not None: + shard_rank = dp_shard_mesh.get_local_rank() + shard_size = dp_shard_mesh.size() + else: + shard_rank = 0 + shard_size = 1 + + for ckpt_name, tensor in loader.iterate_tensors( + all_tensor_names, + tensor_to_rank, + rank_tensors, + rank_tensor_meta, + device=target_device, + ): + dest_name = name_converter(ckpt_name) + + if any(p.fullmatch(dest_name) for p in skip_patterns): + skipped_model_keys.add(dest_name) + continue + + if dest_name not in vlm_state_dict: + continue # extra checkpoint key — ignore + + target = vlm_state_dict[dest_name] + is_dtensor = isinstance(target, DTensor) + local_view = target.to_local() if is_dtensor else target + + # Slice using the REAL FSDP shard_rank/shard_size derived from + # parallel_dims.dp_shard_mesh, NOT loader.rank/world_size. In + # offload mode those two differ: the loader runs single-rank + # (world_size=1) but FSDP still shards across N ranks. + shard = _shard_first_dim(tensor, shard_size, shard_rank) + if shard.device != local_view.device: + shard = shard.to(local_view.device) + + if shard.shape != local_view.shape: + raise ValueError( + f"Shape mismatch for {dest_name}: local_view={tuple(local_view.shape)}, shard={tuple(shard.shape)}" + ) + with torch.no_grad(): + local_view.data.copy_(shard) + keys_loaded.add(dest_name) + + # Phase 6: completeness check with tied-embedding AND skip-list tolerance. + missing = set(vlm_state_dict) - keys_loaded - skipped_model_keys + # Also tolerate missing model keys that match a skip pattern directly — + # handles the case where the ckpt doesn't contain the key at all, so the + # Phase 5 loop never saw it and skipped_model_keys didn't accumulate it. + missing = {k for k in missing if not any(p.fullmatch(k) for p in skip_patterns)} + tie = getattr(model.config, "tie_word_embeddings", False) + real_missing = {k for k in missing if not (tie and "lm_head.weight" in k)} + if real_missing: + sample = sorted(real_missing)[:10] + raise ValueError( + f"load_vlm_model: {len(real_missing)} required model parameter(s) not " + f"found in checkpoint '{checkpoint_path}'. First up to 10: {sample}" + ) + log.info( + f"load_vlm_model: loaded {len(keys_loaded)} tensors from {checkpoint_path} in {time.time() - start_time:.1f}s" + ) + return keys_loaded diff --git a/cosmos3/_src/vfm/models/utils/taylorseer.py b/cosmos3/_src/vfm/models/utils/taylorseer.py new file mode 100644 index 00000000..ec846e69 --- /dev/null +++ b/cosmos3/_src/vfm/models/utils/taylorseer.py @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Adapted from https://github.com/Shenyi-Z/TaylorSeer/blob/main/TaylorSeers-xDiT/taylorseer_flux/taylorseer_utils/__init__.py + +import math +from typing import Dict + +import torch + + +def derivative_approximation(cache_dic: Dict, current: Dict, feature: torch.Tensor): + """ + Compute derivative approximation. + + :param cache_dic: Cache dictionary + :param current: Information of the current step + """ + difference_distance = current["activated_steps"][-1] - current["activated_steps"][-2] + + updated_taylor_factors = {} + updated_taylor_factors[0] = feature + + for i in range(cache_dic["max_order"]): + if ( + cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]].get(i, None) is not None + ) and (current["step"] > cache_dic["first_enhance"] - 2): + updated_taylor_factors[i + 1] = ( + updated_taylor_factors[i] + - cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]][i] + ) / difference_distance + else: + break + + cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]] = updated_taylor_factors + + +def taylor_formula(cache_dic: Dict, current: Dict) -> torch.Tensor: + """ + Compute Taylor expansion error. + + :param cache_dic: Cache dictionary + :param current: Information of the current step + """ + x = current["step"] - current["activated_steps"][-1] + # x = current['t'] - current['activated_times'][-1] + output = 0 + + for i in range(len(cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]])): + output += ( + (1 / math.factorial(i)) + * cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]][i] + * (x**i) + ) + + return output + + +def taylor_cache_init(cache_dic: Dict, current: Dict): + """ + Initialize Taylor cache and allocate storage for different-order derivatives in the Taylor cache. + + :param cache_dic: Cache dictionary + :param current: Information of the current step + """ + if (current["step"] == 0) and (cache_dic["taylor_cache"]): + cache_dic["cache"][-1][current["stream"]][current["layer"]][current["module"]] = {} + + +# Copied from https://github.com/Shenyi-Z/TaylorSeer/blob/main/TaylorSeers-xDiT/taylorseer_flux/cache_functions/force_scheduler.py + + +def force_scheduler(cache_dic, current): + if cache_dic["fresh_ratio"] == 0: + # FORA + linear_step_weight = 0.0 + else: + # TokenCache + linear_step_weight = 0.0 + step_factor = torch.tensor(1 - linear_step_weight + 2 * linear_step_weight * current["step"] / current["num_steps"]) + threshold = torch.round(cache_dic["fresh_threshold"] / step_factor) + + # no force constrain for sensitive steps, cause the performance is good enough. + # you may have a try. + + cache_dic["cal_threshold"] = threshold + # return threshold + + +# Copied from https://github.com/Shenyi-Z/TaylorSeer/blob/main/TaylorSeers-xDiT/taylorseer_flux/cache_functions/cal_type.py + + +def cal_type(cache_dic, current): + """ + Determine calculation type for this step + """ + if (cache_dic["fresh_ratio"] == 0.0) and (not cache_dic["taylor_cache"]): + # FORA:Uniform + first_step = current["step"] == 0 + else: + # ToCa: First enhanced + first_step = current["step"] < cache_dic["first_enhance"] + + if not first_step: + fresh_interval = cache_dic["cal_threshold"] + else: + fresh_interval = cache_dic["fresh_threshold"] + + if (first_step) or (cache_dic["cache_counter"] == fresh_interval - 1): + current["type"] = "full" + cache_dic["cache_counter"] = 0 + current["activated_steps"].append(current["step"]) + force_scheduler(cache_dic, current) + + elif cache_dic["taylor_cache"]: + cache_dic["cache_counter"] += 1 + current["type"] = "Taylor" + + elif cache_dic["cache_counter"] % 2 == 1: # 0: ToCa-Aggresive-ToCa, 1: Aggresive-ToCa-Aggresive + cache_dic["cache_counter"] += 1 + current["type"] = "ToCa" + # 'cache_noise' 'ToCa' 'FORA' + elif cache_dic["Delta-DiT"]: + cache_dic["cache_counter"] += 1 + current["type"] = "Delta-Cache" + else: + cache_dic["cache_counter"] += 1 + current["type"] = "ToCa" + + +# Modified from https://github.com/Shenyi-Z/TaylorSeer/blob/main/TaylorSeers-xDiT/taylorseer_flux/cache_functions/cache_init.py + + +def cache_init(self, num_steps: int): + """ + Initialization for cache. + """ + cache_dic = {} + cache = {} + cache_index = {} + cache[-1] = {} + cache_index[-1] = {} + cache_index["layer_index"] = {} + cache[-1]["layers_stream"] = {} + cache_dic["cache_counter"] = 0 + + for j in range(len(self.language_model.model.layers)): + cache[-1]["layers_stream"][j] = {} + cache_index[-1][j] = {} + + cache_dic["Delta-DiT"] = False + cache_dic["cache_type"] = "random" + cache_dic["cache_index"] = cache_index + cache_dic["cache"] = cache + cache_dic["fresh_ratio_schedule"] = "ToCa" + cache_dic["fresh_ratio"] = 0.0 + cache_dic["fresh_threshold"] = 3 + cache_dic["soft_fresh_weight"] = 0.0 + cache_dic["taylor_cache"] = True + cache_dic["max_order"] = 6 + cache_dic["first_enhance"] = 5 + + current = {} + current["activated_steps"] = [0] + current["step"] = 0 + current["num_steps"] = num_steps + + return cache_dic, current diff --git a/cosmos3/_src/vfm/models/vlm/__init__.py b/cosmos3/_src/vfm/models/vlm/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json new file mode 100644 index 00000000..3f1075eb --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json @@ -0,0 +1,31 @@ +{ + "vocab_size": 131072, + "tie_word_embeddings": false, + "hidden_size": 2048, + "intermediate_size": 9216, + "num_hidden_layers": 28, + "num_attention_heads": 16, + "head_dim": 128, + "num_key_value_heads": 8, + "mlp_hidden_act": "relu2", + "attention_bias": false, + "mlp_bias": false, + "initializer_range": 0.02, + "layer_norm_epsilon": 1e-05, + "residual_in_fp32": false, + "use_cache": true, + "num_logits_to_keep": 1, + "pad_token_id": 0, + "bos_token_id": 1, + "eos_token_id": 11, + "sliding_window": null, + "max_position_embeddings": 131072, + "attention_dropout": 0.0, + "hidden_dropout": 0.0, + "enable_rope": true, + "rope_scaling": null, + "rope_theta": 100000000, + "enable_mrope": true, + "mrope_section": [24, 20, 20], + "torch_dtype": "bfloat16" +} diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py new file mode 100644 index 00000000..e4786731 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Configuration for Nemotron 3 Dense VL text backbone (paired hybrid blocks -> standard layers).""" + +from transformers.configuration_utils import PretrainedConfig + + +class Nemotron3DenseVLTextConfig(PretrainedConfig): + """Text config for Nemotron-H style language model after pairing attn+MLP blocks (28 effective layers).""" + + model_type = "nemotron_3_dense_vl_text" + + def __init__( + self, + vocab_size: int = 131072, + tie_word_embeddings: bool = False, + hidden_size: int = 2048, + intermediate_size: int = 9216, + num_hidden_layers: int = 28, + num_attention_heads: int = 16, + head_dim: int = 128, + num_key_value_heads: int = 8, + mlp_hidden_act: str = "relu2", + attention_bias: bool = False, + mlp_bias: bool = False, + initializer_range: float = 0.02, + layer_norm_epsilon: float = 1e-5, + residual_in_fp32: bool = False, + use_cache: bool = True, + num_logits_to_keep: int = 1, + pad_token_id: int = 0, + bos_token_id: int = 1, + eos_token_id: int = 11, + sliding_window: int | None = None, + max_position_embeddings: int = 131072, + attention_dropout: float = 0.0, + hidden_dropout: float = 0.0, + enable_rope: bool = True, + rope_scaling: dict | None = None, + rope_theta: float = 100_000_000.0, + enable_mrope: bool = True, + mrope_section: list[int] | None = None, + torch_dtype: str = "bfloat16", + **kwargs, + ) -> None: + self.vocab_size = vocab_size + self.tie_word_embeddings = tie_word_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.head_dim = head_dim + self.num_key_value_heads = num_key_value_heads + self.mlp_hidden_act = mlp_hidden_act + self.attention_bias = attention_bias + self.mlp_bias = mlp_bias + self.initializer_range = initializer_range + self.layer_norm_epsilon = layer_norm_epsilon + self.residual_in_fp32 = residual_in_fp32 + self.use_cache = use_cache + self.num_logits_to_keep = num_logits_to_keep + self.sliding_window = sliding_window + self.max_position_embeddings = max_position_embeddings + self.attention_dropout = attention_dropout + self.hidden_dropout = hidden_dropout + self.rope_scaling = rope_scaling + self.rope_theta = rope_theta + self.enable_rope = enable_rope + self.enable_mrope = enable_mrope + self.mrope_section = mrope_section if mrope_section is not None else [24, 20, 20] + self.torch_dtype = torch_dtype + self._attn_implementation = kwargs.pop("_attn_implementation", "eager") + super().__init__( + pad_token_id=pad_token_id, + bos_token_id=bos_token_id, + eos_token_id=eos_token_id, + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + @property + def rms_norm_eps(self) -> float: + """Alias for Qwen-style MoT code paths.""" + return self.layer_norm_epsilon diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py new file mode 100644 index 00000000..2afd3f02 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py @@ -0,0 +1,165 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Nemotron-H style text modules for Nemotron 3 Dense VL (ReLU^2 MLP, partial RoPE helper, mRoPE).""" + +from __future__ import annotations + +import functools + +import torch +import torch.nn.functional as F +from torch import nn +from transformers.activations import ACT2FN + +if "relu2" not in ACT2FN: + ACT2FN["relu2"] = lambda x: F.relu(x).square() +from transformers.modeling_rope_utils import dynamic_rope_update +from transformers.modeling_utils import PreTrainedModel + +from cosmos3._src.vfm.models.vlm.nemotron_3_dense_vl.configuration_nemotron_3_dense_vl import ( + Nemotron3DenseVLTextConfig, +) + + +def rotate_half(x: torch.Tensor) -> torch.Tensor: + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_pos_emb_partial( + q: torch.Tensor, + k: torch.Tensor, + cos: torch.Tensor, + sin: torch.Tensor, + unsqueeze_dim: int = 1, +) -> tuple[torch.Tensor, torch.Tensor]: + """Apply RoPE to the first rot_dim channels; remainder passes through (rot_dim == head_dim for 2B Dense).""" + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + rot_dim = cos.shape[-1] + q_rot, q_pass = q[..., :rot_dim], q[..., rot_dim:] + k_rot, k_pass = k[..., :rot_dim], k[..., rot_dim:] + q_embed = (q_rot * cos) + (rotate_half(q_rot) * sin) + k_embed = (k_rot * cos) + (rotate_half(k_rot) * sin) + return torch.cat((q_embed, q_pass), dim=-1), torch.cat((k_embed, k_pass), dim=-1) + + +class Nemotron3DenseVLRMSNorm(nn.Module): + def __init__(self, hidden_size: int, eps: float = 1e-5) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return (self.weight.to(torch.float32) * hidden_states).to(input_dtype) + + def extra_repr(self) -> str: + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Nemotron3DenseVLMLP(nn.Module): + def __init__(self, config: Nemotron3DenseVLTextConfig, layer_idx: int | None = None) -> None: + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias) + if config.mlp_hidden_act in ACT2FN: + self.act_fn = ACT2FN[config.mlp_hidden_act] + else: + self.act_fn = lambda x: F.relu(x).square() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.down_proj(self.act_fn(self.up_proj(x))) + + +class MultiModalRotaryEmbedding(nn.Module): + inv_freq: torch.Tensor + + def __init__(self, config: Nemotron3DenseVLTextConfig, device: torch.device | None = None) -> None: + super().__init__() + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + self.config = config + self.mrope_section = getattr(config, "mrope_section", [24, 20, 20]) + inv_freq, self.attention_scaling = self.compute_default_rope_parameters(self.config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self.register_buffer("original_inv_freq", inv_freq.clone(), persistent=False) + + @staticmethod + def compute_default_rope_parameters( + config: Nemotron3DenseVLTextConfig | None = None, + device: torch.device | None = None, + seq_len: int | None = None, + ) -> tuple[torch.Tensor, float]: + rope_theta = config.rope_theta + dim = config.head_dim + attention_factor = 1.0 + inv_freq = 1.0 / ( + rope_theta ** (torch.arange(0, dim, 2, dtype=torch.int64, device=device).to(dtype=torch.float) / dim) + ) + return inv_freq, attention_factor + + def apply_interleaved_mrope(self, freqs: torch.Tensor, mrope_section: list[int]) -> torch.Tensor: + freqs_t = freqs[0] + for dim, offset in enumerate((1, 2), start=1): + length = mrope_section[dim] * 3 + idx = slice(offset, length, 3) + freqs_t[..., idx] = freqs[dim, ..., idx] + return freqs_t + + @torch.no_grad() + @dynamic_rope_update + def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + if position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) + inv_freq_expanded = self.inv_freq[None, None, :, None].float().expand(3, position_ids.shape[1], -1, 1) + position_ids_expanded = position_ids[:, :, None, :].float() + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" + with torch.autocast(device_type=device_type, enabled=False): + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3) + freqs = self.apply_interleaved_mrope(freqs, self.mrope_section) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() * self.attention_scaling + sin = emb.sin() * self.attention_scaling + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + inv_freq, self.attention_scaling = self.compute_default_rope_parameters(self.config, buffer_device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + +class Nemotron3DenseVLPreTrainedModel(PreTrainedModel): + config_class = Nemotron3DenseVLTextConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _supports_flash_attn = True + _supports_sdpa = True + + def _init_weights(self, module: nn.Module, buffer_device: torch.device | None) -> None: + super()._init_weights(module) + if isinstance(module, MultiModalRotaryEmbedding): + module.init_weights(buffer_device=buffer_device) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + self.apply(functools.partial(self._init_weights, buffer_device=buffer_device)) diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json new file mode 100644 index 00000000..0cd8c646 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json @@ -0,0 +1,63 @@ +{ + "architectures": [ + "Qwen3VLForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2048, + "initializer_range": 0.02, + "intermediate_size": 6144, + "max_position_embeddings": 262144, + "model_type": "qwen3_vl_text", + "num_attention_heads": 16, + "num_hidden_layers": 28, + "num_key_value_heads": 8, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "tie_word_embeddings": true, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": true, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 5, + 11, + 17 + ], + "depth": 24, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1024, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4096, + "model_type": "qwen3_vl", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 2048, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json new file mode 100644 index 00000000..29e92509 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json @@ -0,0 +1,62 @@ +{ + "architectures": [ + "Qwen3VLForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 5120, + "initializer_range": 0.02, + "intermediate_size": 25600, + "max_position_embeddings": 262144, + "model_type": "qwen3_vl_text", + "num_attention_heads": 64, + "num_hidden_layers": 64, + "num_key_value_heads": 8, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 5120, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json new file mode 100644 index 00000000..e8add36d --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json @@ -0,0 +1,63 @@ +{ + "architectures": [ + "Qwen3VLForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2560, + "initializer_range": 0.02, + "intermediate_size": 9728, + "max_position_embeddings": 262144, + "model_type": "qwen3_vl_text", + "num_attention_heads": 32, + "num_hidden_layers": 36, + "num_key_value_heads": 8, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "tie_word_embeddings": true, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": true, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 5, + 11, + 17 + ], + "depth": 24, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1024, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4096, + "model_type": "qwen3_vl", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 2560, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 + } diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json new file mode 100644 index 00000000..0e2df614 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json @@ -0,0 +1,62 @@ +{ + "architectures": [ + "Qwen3VLForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 4096, + "initializer_range": 0.02, + "intermediate_size": 12288, + "max_position_embeddings": 262144, + "model_type": "qwen3_vl_text", + "num_attention_heads": 32, + "num_hidden_layers": 36, + "num_key_value_heads": 8, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 4096, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py new file mode 100644 index 00000000..aba4aff0 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py @@ -0,0 +1,298 @@ +# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +# Source Repository: https://github.com/huggingface/transformers +# This is adapted from src/transformers/models/qwen3_vl/configuration_qwen3_vl.py. +# Commit Hash: 41e5abac5cb49983a08ddef3e8645d6efd23c8f3 +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_rope_utils import rope_config_validation + + +class Qwen3VLVisionConfig(PretrainedConfig): + model_type = "qwen3_vl" + base_config_key = "vision_config" + + def __init__( + self, + depth=27, + hidden_size=1152, + hidden_act="gelu_pytorch_tanh", + intermediate_size=4304, + num_heads=16, + in_channels=3, + patch_size=16, + spatial_merge_size=2, + temporal_patch_size=2, + out_hidden_size=3584, + num_position_embeddings=2304, + deepstack_visual_indexes=[8, 16, 24], + initializer_range=0.02, + **kwargs, + ): + super().__init__(**kwargs) + + self.depth = depth + self.hidden_size = hidden_size + self.hidden_act = hidden_act + self.intermediate_size = intermediate_size + self.num_heads = num_heads + self.in_channels = in_channels + self.patch_size = patch_size + self.spatial_merge_size = spatial_merge_size + self.temporal_patch_size = temporal_patch_size + self.out_hidden_size = out_hidden_size + self.num_position_embeddings = num_position_embeddings + self.initializer_range = initializer_range + self.deepstack_visual_indexes = deepstack_visual_indexes + + +class Qwen3VLTextConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLTextModel`]. It is used to instantiate a + Qwen3-VL model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-4B-Instruct [Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + Args: + vocab_size (`int`, *optional*, defaults to 151936): + Vocabulary size of the Qwen3VL model. Defines the number of different tokens that can be represented by the + `inputs_ids` passed when calling [`Qwen3VLModel`] + hidden_size (`int`, *optional*, defaults to 4096): + Dimension of the hidden representations. + intermediate_size (`int`, *optional*, defaults to 22016): + Dimension of the MLP representations. + num_hidden_layers (`int`, *optional*, defaults to 32): + Number of hidden layers in the Transformer encoder. + num_attention_heads (`int`, *optional*, defaults to 32): + Number of attention heads for each attention layer in the Transformer encoder. + num_key_value_heads (`int`, *optional*, defaults to 32): + This is the number of key_value heads that should be used to implement Grouped Query Attention. If + `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if + `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When + converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed + by meanpooling all the original heads within that group. For more details, check out [this + paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`. + head_dim (`int`, *optional*, defaults to 128): + The dimension of the head. If not specified, will default to `hidden_size // num_attention_heads`. + hidden_act (`str` or `function`, *optional*, defaults to `"silu"`): + The non-linear activation function (function or string) in the decoder. + max_position_embeddings (`int`, *optional*, defaults to 128000): + The maximum sequence length that this model might ever be used with. + initializer_range (`float`, *optional*, defaults to 0.02): + The standard deviation of the truncated_normal_initializer for initializing all weight matrices. + rms_norm_eps (`float`, *optional*, defaults to 1e-06): + The epsilon used by the rms normalization layers. + use_cache (`bool`, *optional*, defaults to `True`): + Whether or not the model should return the last key/values attentions (not used by all models). Only + relevant if `config.is_decoder=True`. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether the model's input and output word embeddings should be tied. + rope_theta (`float`, *optional*, defaults to 5000000.0): + The base period of the RoPE embeddings. + rope_scaling (`Dict`, *optional*): + Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type + and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value + accordingly. + Expected contents: + `rope_type` (`str`): + The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope', + 'llama3'], with 'default' being the original RoPE implementation. + `factor` (`float`, *optional*): + Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In + most scaling types, a `factor` of x will enable the model to handle sequences of length x * + original maximum pre-trained length. + `original_max_position_embeddings` (`int`, *optional*): + Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during + pretraining. + `attention_factor` (`float`, *optional*): + Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention + computation. If unspecified, it defaults to value recommended by the implementation, using the + `factor` field to infer the suggested value. + `beta_fast` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear + ramp function. If unspecified, it defaults to 32. + `beta_slow` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear + ramp function. If unspecified, it defaults to 1. + `short_factor` (`list[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to short contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `long_factor` (`list[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to long contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `low_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE + `high_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE + attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`): + Whether to use a bias in the query, key, value and output projection layers during self-attention. + attention_dropout (`float`, *optional*, defaults to 0.0): + The dropout ratio for the attention probabilities. + + ```python + >>> from transformers import Qwen3VLTextModel, Qwen3VLTextConfig + + >>> # Initializing a Qwen3VL style configuration + >>> configuration = Qwen3VLTextConfig() + + >>> # Initializing a model from the Qwen3-VL-7B style configuration + >>> model = Qwen3VLTextModel(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl_text" + base_config_key = "text_config" + + def __init__( + self, + vocab_size=151936, + hidden_size=4096, + intermediate_size=22016, + num_hidden_layers=32, + num_attention_heads=32, + num_key_value_heads=32, + head_dim=128, + hidden_act="silu", + max_position_embeddings=128000, + initializer_range=0.02, + rms_norm_eps=1e-6, + use_cache=True, + tie_word_embeddings=False, + rope_theta=5000000.0, + rope_scaling=None, + attention_bias=False, + attention_dropout=0.0, + **kwargs, + ): + self.vocab_size = vocab_size + self.max_position_embeddings = max_position_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + + # for backward compatibility + if num_key_value_heads is None: + num_key_value_heads = num_attention_heads + + self.num_key_value_heads = num_key_value_heads + self.head_dim = head_dim + self.hidden_act = hidden_act + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.rope_scaling = rope_scaling + self.attention_bias = attention_bias + self.attention_dropout = attention_dropout + + rope_config_validation(self, ignore_keys={"mrope_section", "mrope_interleaved"}) + + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + +class Qwen3VLConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLModel`]. It is used to instantiate a + Qwen3-VL model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-4B-Instruct [Qwen/Qwen3-VL-4B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + + Args: + text_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLTextConfig`): + The config object or dictionary of the text backbone. + vision_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLVisionConfig`): + The config object or dictionary of the vision backbone. + image_token_id (`int`, *optional*, defaults to 151655): + The image token index to encode the image prompt. + video_token_id (`int`, *optional*, defaults to 151656): + The video token index to encode the image prompt. + vision_start_token_id (`int`, *optional*, defaults to 151652): + The start token index to encode the image prompt. + vision_end_token_id (`int`, *optional*, defaults to 151653): + The end token index to encode the image prompt. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether to tie the word embeddings. + + ```python + >>> from transformers import Qwen3VLForConditionalGeneration, Qwen3VLConfig + + >>> # Initializing a Qwen3-VL style configuration + >>> configuration = Qwen3VLConfig() + + >>> # Initializing a model from the Qwen3-VL-4B style configuration + >>> model = Qwen3VLForConditionalGeneration(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl" + sub_configs = {"vision_config": Qwen3VLVisionConfig, "text_config": Qwen3VLTextConfig} + keys_to_ignore_at_inference = ["past_key_values"] + + def __init__( + self, + text_config=None, + vision_config=None, + image_token_id=151655, + video_token_id=151656, + vision_start_token_id=151652, + vision_end_token_id=151653, + tie_word_embeddings=False, + **kwargs, + ): + if isinstance(vision_config, dict): + self.vision_config = self.sub_configs["vision_config"](**vision_config) + elif vision_config is None: + self.vision_config = self.sub_configs["vision_config"]() + + if isinstance(text_config, dict): + self.text_config = self.sub_configs["text_config"](**text_config) + elif text_config is None: + self.text_config = self.sub_configs["text_config"]() + + self.image_token_id = image_token_id + self.video_token_id = video_token_id + self.vision_start_token_id = vision_start_token_id + self.vision_end_token_id = vision_end_token_id + super().__init__(**kwargs, tie_word_embeddings=tie_word_embeddings) + + +__all__ = ["Qwen3VLConfig", "Qwen3VLTextConfig"] diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py new file mode 100644 index 00000000..da65d4ee --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py @@ -0,0 +1,1651 @@ +# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +# Source Repository: https://github.com/huggingface/transformers +# This is adapted from src/transformers/models/qwen3_vl/modeling_qwen3_vl.py. +# Commit Hash: 41e5abac5cb49983a08ddef3e8645d6efd23c8f3 +"""PyTorch Qwen3-VL model.""" + +import functools +from dataclasses import dataclass +from typing import Any, Callable, Optional, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache, DynamicCache +from transformers.generation import GenerationMixin +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_outputs import BaseModelOutputWithPast, ModelOutput +from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.processing_utils import Unpack +from transformers.utils import is_torchdynamo_compiling +from transformers.utils.deprecation import deprecate_kwarg + +TransformersKwargs = Any + +# Import masking functions from utils for compatibility +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import ( + create_causal_mask, +) + +from .configuration_qwen3_vl import Qwen3VLConfig, Qwen3VLTextConfig, Qwen3VLVisionConfig + + +class Qwen3VLVisionMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.linear_fc1 = nn.Linear(self.hidden_size, self.intermediate_size, bias=True) + self.linear_fc2 = nn.Linear(self.intermediate_size, self.hidden_size, bias=True) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, hidden_state): + return self.linear_fc2(self.act_fn(self.linear_fc1(hidden_state))) + + +class Qwen3VLVisionPatchEmbed(nn.Module): + def __init__(self, config) -> None: + super().__init__() + self.patch_size = config.patch_size + self.temporal_patch_size = config.temporal_patch_size + self.in_channels = config.in_channels + self.embed_dim = config.hidden_size + + kernel_size = [self.temporal_patch_size, self.patch_size, self.patch_size] + self.proj = nn.Conv3d(self.in_channels, self.embed_dim, kernel_size=kernel_size, stride=kernel_size, bias=True) + + def forward( + self, hidden_states: torch.Tensor + ) -> torch.Tensor: # hidden_states: [N_patches,in_channels*temporal_patch_size*patch_size*patch_size] + target_dtype = self.proj.weight.dtype + hidden_states = hidden_states.view( + -1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size + ) # [N_patches,in_channels,temporal_patch_size,patch_size,patch_size] + hidden_states = self.proj(hidden_states.to(dtype=target_dtype)).view( + -1, self.embed_dim + ) # [N_patches,embed_dim] + return hidden_states # [N_patches,embed_dim] + + +class Qwen3VLVisionRotaryEmbedding(nn.Module): + inv_freq: torch.Tensor # fix linting for `register_buffer` + + def __init__(self, dim: int, theta: float = 10000.0) -> None: + super().__init__() + inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim)) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def forward(self, seqlen: int) -> torch.Tensor: + seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype) # [seqlen] + freqs = torch.outer(seq, self.inv_freq) # [seqlen,dim//2] + return freqs # [seqlen,dim//2] + + +class Qwen3VLVisionPatchMerger(nn.Module): + def __init__(self, config: Qwen3VLVisionConfig, use_postshuffle_norm=False) -> None: + super().__init__() + self.hidden_size = config.hidden_size * (config.spatial_merge_size**2) + self.use_postshuffle_norm = use_postshuffle_norm + self.norm = nn.LayerNorm(self.hidden_size if use_postshuffle_norm else config.hidden_size, eps=1e-6) + self.linear_fc1 = nn.Linear(self.hidden_size, self.hidden_size) + self.act_fn = nn.GELU() + self.linear_fc2 = nn.Linear(self.hidden_size, config.out_hidden_size) + + def forward(self, x: torch.Tensor) -> torch.Tensor: # x: [N_tokens,vision_hidden_size] + x = self.norm(x.view(-1, self.hidden_size) if self.use_postshuffle_norm else x).view( + -1, self.hidden_size + ) # [N_merged,hidden_size] + x = self.linear_fc2(self.act_fn(self.linear_fc1(x))) # [N_merged,out_hidden_size] + return x # [N_merged,out_hidden_size] + + +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] # [...,head_dim//2] + x2 = x[..., x.shape[-1] // 2 :] # [...,head_dim//2] + return torch.cat((-x2, x1), dim=-1) # [...,head_dim] + + +def apply_rotary_pos_emb_vision( + q: torch.Tensor, # [N_vision,num_heads,head_dim] + k: torch.Tensor, # [N_vision,num_heads,head_dim] + cos: torch.Tensor, # [N_vision,head_dim] + sin: torch.Tensor, # [N_vision,head_dim] +) -> tuple[torch.Tensor, torch.Tensor]: + orig_q_dtype = q.dtype + orig_k_dtype = k.dtype + q, k = q.float(), k.float() + cos, sin = cos.unsqueeze(-2).float(), sin.unsqueeze(-2).float() # [N_vision,1,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [N_vision,num_heads,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [N_vision,num_heads,head_dim] + q_embed = q_embed.to(orig_q_dtype) + k_embed = k_embed.to(orig_k_dtype) + return q_embed, k_embed # [N_vision,num_heads,head_dim], [N_vision,num_heads,head_dim] + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) # [B,num_kv_heads,n_rep,N,head_dim] + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) # [B,num_heads,N,head_dim] + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, # [B,num_heads,N_q,head_dim] + key: torch.Tensor, # [B,num_kv_heads,N_kv,head_dim] + value: torch.Tensor, # [B,num_kv_heads,N_kv,head_dim] + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) # [B,num_heads,N_kv,head_dim] + value_states = repeat_kv(value, module.num_key_value_groups) # [B,num_heads,N_kv,head_dim] + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling # [B,num_heads,N_q,N_kv] + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] # [B,1,N_q,N_kv] + attn_weights = attn_weights + causal_mask # [B,num_heads,N_q,N_kv] + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query.dtype + ) # [B,num_heads,N_q,N_kv] + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value_states) # [B,num_heads,N_q,head_dim] + attn_output = attn_output.transpose(1, 2).contiguous() # [B,N_q,num_heads,head_dim] + + return attn_output, attn_weights + + +class Qwen3VLVisionAttention(nn.Module): + def __init__(self, config: Qwen3VLVisionConfig) -> None: + super().__init__() + self.dim = config.hidden_size + self.num_heads = config.num_heads + self.head_dim = self.dim // self.num_heads + self.num_key_value_groups = 1 # needed for eager attention + self.qkv = nn.Linear(self.dim, self.dim * 3, bias=True) + self.proj = nn.Linear(self.dim, self.dim) + self.scaling = self.head_dim**-0.5 + self.config = config + self.attention_dropout = 0.0 + self.is_causal = False + + def forward( + self, + hidden_states: torch.Tensor, # [N_vision,hidden_size] + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: # [N_vision,hidden_size] + seq_length = hidden_states.shape[0] + query_states, key_states, value_states = ( + self.qkv(hidden_states).reshape(seq_length, 3, self.num_heads, -1).permute(1, 0, 2, 3).unbind(0) + ) # each: [N_vision,num_heads,head_dim] + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb_vision(query_states, key_states, cos, sin) + # each: [N_vision,num_heads,head_dim] + + query_states = query_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N_vision,head_dim] + key_states = key_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N_vision,head_dim] + value_states = value_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N_vision,head_dim] + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + if self.config._attn_implementation == "flash_attention_2": + # Flash Attention 2: Use cu_seqlens for variable length attention + max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max() + attn_output, _ = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + cu_seq_lens_q=cu_seqlens, + cu_seq_lens_k=cu_seqlens, + max_length_q=max_seqlen, + max_length_k=max_seqlen, + is_causal=False, + **kwargs, + ) + else: + # Other implementations: Process each chunk separately + lengths = cu_seqlens[1:] - cu_seqlens[:-1] + splits = [ + torch.split(tensor, lengths.tolist(), dim=2) for tensor in (query_states, key_states, value_states) + ] + + attn_outputs = [ + attention_interface( + self, + q, + k, + v, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + is_causal=False, + **kwargs, + )[0] + for q, k, v in zip(*splits) + ] + attn_output = torch.cat(attn_outputs, dim=1) # [1,N_vision,num_heads,head_dim] + + attn_output = attn_output.reshape(seq_length, -1).contiguous() # [N_vision,hidden_size] + attn_output = self.proj(attn_output) # [N_vision,hidden_size] + return attn_output # [N_vision,hidden_size] + + +class Qwen3VLVisionBlock(nn.Module): + def __init__(self, config, attn_implementation: str = "sdpa") -> None: + super().__init__() + self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.attn = Qwen3VLVisionAttention(config=config) + self.mlp = Qwen3VLVisionMLP(config=config) + + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: + hidden_states = hidden_states + self.attn( + self.norm1(hidden_states), + cu_seqlens=cu_seqlens, + rotary_pos_emb=rotary_pos_emb, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = hidden_states + self.mlp(self.norm2(hidden_states)) + return hidden_states + + +class Qwen3VLTextRotaryEmbedding(nn.Module): + def __init__(self, config: Qwen3VLTextConfig): + super().__init__() + if hasattr(config, "rope_scaling") and config.rope_scaling is not None: + self.rope_type = config.rope_scaling.get("rope_type", "default") + else: + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + + self.config = config + self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + + self.mrope_section = ( + config.rope_scaling.get("mrope_section", [24, 20, 20]) if config.rope_scaling is not None else [24, 20, 20] + ) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + inv_freq, self.attention_scaling = self.rope_init_fn(self.config, buffer_device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def apply_interleaved_mrope(self, freqs, mrope_section): + """Apply interleaved MRoPE to 3D rotary embeddings. + Reorganizes frequency layout from chunked [TTT...HHH...WWW] to + interleaved [THTHWHTHW...TT], preserving frequency continuity. + args: + x: (3, bs, seq_len, head_dim // 2) + mrope_section: (3,) + returns: + x_t: (bs, seq_len, head_dim // 2) + """ + freqs_t = freqs[0] # just overwrite the first dimension T + for dim, offset in enumerate((1, 2), start=1): # H, W + length = mrope_section[dim] * 3 + idx = slice(offset, length, 3) + freqs_t[..., idx] = freqs[dim, ..., idx] + return freqs_t + + @torch.no_grad() + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward(self, x, position_ids): + assert self.inv_freq.dtype == torch.float32, f"inv_freq must be float32, but got {self.inv_freq.dtype}" + + # In contrast to other models, Qwen3VL has different position ids for the grids + # So we expand the inv_freq to shape (3, ...) + if position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) # [3,B,N] + inv_freq_expanded = ( + self.inv_freq[None, None, :, None].float().expand(3, position_ids.shape[1], -1, 1).to(x.device) + ) # [3,B,head_dim//2,1] + position_ids_expanded = position_ids[:, :, None, :].float() # [3,B,1,N] + + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3) # [3,B,N,head_dim//2] + freqs = self.apply_interleaved_mrope(freqs, self.mrope_section) # [B,N,head_dim//2] + emb = torch.cat((freqs, freqs), dim=-1) # [B,N,head_dim] + cos = emb.cos() * self.attention_scaling # [B,N,head_dim] + sin = emb.sin() * self.attention_scaling # [B,N,head_dim] + + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) # each: [B,N,head_dim] + + +class Qwen3VLTextRMSNorm(nn.Module): + def __init__(self, hidden_size: int, eps: float = 1e-6) -> None: + """ + Qwen3VLTextRMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + def extra_repr(self) -> str: + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + position_ids (`torch.Tensor`, *optional*): + Deprecated and unused. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + sin = sin.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [B,num_heads,N,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [B,num_kv_heads,N,head_dim] + return q_embed, k_embed # [B,num_heads,N,head_dim], [B,num_kv_heads,N,head_dim] + + +class Qwen3VLTextAttention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, config: Qwen3VLTextConfig, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = True + + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias + ) + self.q_norm = Qwen3VLTextRMSNorm(self.head_dim, eps=config.rms_norm_eps) # unlike olmo, only on the head dim! + self.k_norm = Qwen3VLTextRMSNorm( + self.head_dim, eps=config.rms_norm_eps + ) # thus post q_norm does not need reshape + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, # [B,N,hidden_size] + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_heads,N,head_dim] + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_kv_heads,N,head_dim] + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) # [B,num_kv_heads,N,head_dim] + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + # query_states: [B,num_heads,N,head_dim], key_states: [B,num_kv_heads,N,head_dim] + + if past_key_values is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs) + # key_states: [B,num_kv_heads,N_cached,head_dim], value_states: [B,num_kv_heads,N_cached,head_dim] + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) # attn_output: [B,N,num_heads,head_dim] + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() # [B,N,hidden_size] + attn_output = self.o_proj(attn_output) # [B,N,hidden_size] + return attn_output, attn_weights # [B,N,hidden_size], [B,num_heads,N,N_cached] or None + + +class Qwen3VLTextMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj + + +class Qwen3VLTextDecoderLayer(nn.Module): + def __init__(self, config: Qwen3VLTextConfig, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + + self.self_attn = Qwen3VLTextAttention(config=config, layer_idx=layer_idx) + + self.mlp = Qwen3VLTextMLP(config) + self.input_layernorm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + use_cache: Optional[bool] = False, + cache_position: Optional[torch.LongTensor] = None, + output_attentions: Optional[bool] = False, + **kwargs: Unpack[TransformersKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + # Self Attention + hidden_states, self_attn_weights = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + outputs = (hidden_states,) + if output_attentions: + outputs += (self_attn_weights,) + return outputs + + +@dataclass +class Qwen3VLModelOutputWithPast(ModelOutput): + r""" + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + last_hidden_state: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + + +class Qwen3VLPreTrainedModel(PreTrainedModel): + config: Qwen3VLConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["Qwen3VLTextDecoderLayer", "Qwen3VLVisionBlock"] + _skip_keys_device_placement = "past_key_values" + _supports_flash_attn = True + _supports_sdpa = True + + _can_compile_fullgraph = True + _supports_attention_backend = True + _can_record_outputs = { + "hidden_states": Qwen3VLTextDecoderLayer, + "attentions": Qwen3VLTextAttention, + } + + def _init_weights(self, module: nn.Module, buffer_device: torch.device | None) -> None: + """Initialize the weights.""" + super()._init_weights(module) + + if isinstance(module, Qwen3VLTextRotaryEmbedding): + module.init_weights(buffer_device=buffer_device) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + self.apply(functools.partial(self._init_weights, buffer_device=buffer_device)) + + +class Qwen3VLVisionModel(Qwen3VLPreTrainedModel): + config: Qwen3VLVisionConfig + _no_split_modules = ["Qwen3VLVisionBlock"] + + def __init__(self, config, *inputs, **kwargs) -> None: + super().__init__(config, *inputs, **kwargs) + self.spatial_merge_size = config.spatial_merge_size + self.patch_size = config.patch_size + self.spatial_merge_unit = self.spatial_merge_size * self.spatial_merge_size + + self.patch_embed = Qwen3VLVisionPatchEmbed( + config=config, + ) + + self.pos_embed = nn.Embedding(config.num_position_embeddings, config.hidden_size) + self.num_grid_per_side = int(config.num_position_embeddings**0.5) + + head_dim = config.hidden_size // config.num_heads + self.rotary_pos_emb = Qwen3VLVisionRotaryEmbedding(head_dim // 2) + + self.blocks = nn.ModuleList([Qwen3VLVisionBlock(config) for _ in range(config.depth)]) + self.merger = Qwen3VLVisionPatchMerger( + config=config, + use_postshuffle_norm=False, + ) + + self.deepstack_visual_indexes = config.deepstack_visual_indexes + self.deepstack_merger_list = nn.ModuleList( + [ + Qwen3VLVisionPatchMerger( + config=config, + use_postshuffle_norm=True, + ) + for _ in range(len(config.deepstack_visual_indexes)) + ] + ) + + self.gradient_checkpointing = False + + def rot_pos_emb(self, grid_thw: torch.Tensor) -> torch.Tensor: # grid_thw: [N_media,3] + merge_size = self.spatial_merge_size + + max_hw = int(grid_thw[:, 1:].max().item()) + freq_table = self.rotary_pos_emb(max_hw) # [max_hw,head_dim//4] + device = freq_table.device + + total_tokens = int(torch.prod(grid_thw, dim=1).sum().item()) + pos_ids = torch.empty((total_tokens, 2), dtype=torch.long, device=device) # [N_vision,2] + + offset = 0 + for num_frames, height, width in grid_thw: + merged_h, merged_w = height // merge_size, width // merge_size + + block_rows = torch.arange(merged_h, device=device) # block row indices + block_cols = torch.arange(merged_w, device=device) # block col indices + intra_row = torch.arange(merge_size, device=device) # intra-block row offsets + intra_col = torch.arange(merge_size, device=device) # intra-block col offsets + + # Compute full-resolution positions + row_idx = ( + block_rows[:, None, None, None] * merge_size + intra_row[None, None, :, None] + ) # [merged_h,1,merge_size,1] + col_idx = ( + block_cols[None, :, None, None] * merge_size + intra_col[None, None, None, :] + ) # [1,merged_w,1,merge_size] + + row_idx = row_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + col_idx = col_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + + coords = torch.stack((row_idx, col_idx), dim=-1) # [H*W,2] + + if num_frames > 1: + coords = coords.repeat(num_frames, 1) # [T*H*W,2] + + num_tokens = coords.shape[0] + pos_ids[offset : offset + num_tokens] = coords + offset += num_tokens + + embeddings = freq_table[pos_ids] # [N_vision,2,head_dim//4] + embeddings = embeddings.flatten(1) # [N_vision,head_dim//2] + return embeddings # [N_vision,head_dim//2] + + def fast_pos_embed_interpolate(self, grid_thw): + grid_ts, grid_hs, grid_ws = grid_thw[:, 0], grid_thw[:, 1], grid_thw[:, 2] + + idx_list = [[] for _ in range(4)] + weight_list = [[] for _ in range(4)] + + for t, h, w in zip(grid_ts, grid_hs, grid_ws): + h_idxs = torch.linspace(0, self.num_grid_per_side - 1, h) + w_idxs = torch.linspace(0, self.num_grid_per_side - 1, w) + + h_idxs_floor = h_idxs.int() + w_idxs_floor = w_idxs.int() + h_idxs_ceil = (h_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + w_idxs_ceil = (w_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + + dh = h_idxs - h_idxs_floor + dw = w_idxs - w_idxs_floor + + base_h = h_idxs_floor * self.num_grid_per_side + base_h_ceil = h_idxs_ceil * self.num_grid_per_side + + indices = [ + (base_h[None].T + w_idxs_floor[None]).flatten(), + (base_h[None].T + w_idxs_ceil[None]).flatten(), + (base_h_ceil[None].T + w_idxs_floor[None]).flatten(), + (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(), + ] + + weights = [ + ((1 - dh)[None].T * (1 - dw)[None]).flatten(), + ((1 - dh)[None].T * dw[None]).flatten(), + (dh[None].T * (1 - dw)[None]).flatten(), + (dh[None].T * dw[None]).flatten(), + ] + + for i in range(4): + idx_list[i].extend(indices[i].tolist()) + weight_list[i].extend(weights[i].tolist()) + + idx_tensor = torch.tensor(idx_list, dtype=torch.long, device=self.pos_embed.weight.device) # [4,N_vision] + weight_tensor = torch.tensor( + weight_list, dtype=self.pos_embed.weight.dtype, device=self.pos_embed.weight.device + ) # [4,N_vision] + pos_embeds = self.pos_embed(idx_tensor) * weight_tensor[:, :, None] # [4,N_vision,hidden_size] + patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3] # [N_vision,hidden_size] + + patch_pos_embeds = patch_pos_embeds.split([h * w for h, w in zip(grid_hs, grid_ws)]) + # tuple of [H_i*W_i,hidden_size] per media item + + patch_pos_embeds_permute = [] + merge_size = self.config.spatial_merge_size + for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws): + pos_embed = pos_embed.repeat(t, 1) # [T*H*W,hidden_size] + pos_embed = ( + pos_embed.view(t, h // merge_size, merge_size, w // merge_size, merge_size, -1) + # [T,H/ms,ms,W/ms,ms,hidden_size] + .permute(0, 1, 3, 2, 4, 5) + # [T,H/ms,W/ms,ms,ms,hidden_size] + .flatten(0, 4) + # [T*H/ms*W/ms*ms*ms,hidden_size] == [T*H*W,hidden_size] + ) + patch_pos_embeds_permute.append(pos_embed) + patch_pos_embeds = torch.cat(patch_pos_embeds_permute) # [N_vision,hidden_size] + return patch_pos_embeds # [N_vision,hidden_size] + + def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, **kwargs) -> torch.Tensor: + """ + Args: + hidden_states (`torch.Tensor` of shape `(seq_len, hidden_size)`): + The final hidden states of the model. + grid_thw (`torch.Tensor` of shape `(num_images_or_videos, 3)`): + The temporal, height and width of feature shape of each image in LLM. + + Returns: + `torch.Tensor`: hidden_states. + """ + hidden_states = self.patch_embed(hidden_states) # [N_vision,embed_dim] + + pos_embeds = self.fast_pos_embed_interpolate(grid_thw) # [N_vision,hidden_size] + hidden_states = hidden_states + pos_embeds # [N_vision,hidden_size] + + rotary_pos_emb = self.rot_pos_emb(grid_thw) # [N_vision,head_dim//2] + + seq_len, _ = hidden_states.size() + hidden_states = hidden_states.reshape(seq_len, -1) # [N_vision,hidden_size] + rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1) # [N_vision,head_dim//2] + emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1) # [N_vision,head_dim] + position_embeddings = (emb.cos(), emb.sin()) # each: [N_vision,head_dim] + + cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum( + dim=0, + # Select dtype based on the following factors: + # - FA2 requires that cu_seqlens_q must have dtype int32 + # - torch.onnx.export requires that cu_seqlens_q must have same dtype as grid_thw + # See https://github.com/huggingface/transformers/pull/34852 for more information + dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32, + ) + cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0) # [N_media+1] + + deepstack_feature_lists = [] + for layer_num, blk in enumerate(self.blocks): + hidden_states = blk( + hidden_states, + cu_seqlens=cu_seqlens, + position_embeddings=position_embeddings, + **kwargs, + ) + if layer_num in self.deepstack_visual_indexes: + deepstack_feature = self.deepstack_merger_list[self.deepstack_visual_indexes.index(layer_num)]( + hidden_states + ) + deepstack_feature_lists.append(deepstack_feature) + + hidden_states = self.merger(hidden_states) # [N_merged,out_hidden_size] + + return hidden_states, deepstack_feature_lists # [N_merged,out_hidden_size], list of [N_merged,out_hidden_size] + + +class Qwen3VLTextModel(Qwen3VLPreTrainedModel): + config: Qwen3VLTextConfig + _no_split_modules = ["Qwen3VLTextDecoderLayer"] + + def __init__(self, config: Qwen3VLTextConfig): + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + self.layers = nn.ModuleList( + [Qwen3VLTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] + ) + self.norm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3VLTextRotaryEmbedding(config=config) + self.gradient_checkpointing = False + + # Initialize weights and apply final processing + self.post_init() + + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + # args for deepstack + visual_pos_masks: Optional[torch.Tensor] = None, + deepstack_visual_embeds: Optional[list[torch.Tensor]] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Union[tuple, BaseModelOutputWithPast]: + r""" + visual_pos_masks (`torch.Tensor` of shape `(batch_size, seqlen)`, *optional*): + The mask of the visual positions. + deepstack_visual_embeds (`list[torch.Tensor]`, *optional*): + The deepstack visual embeddings. The shape is (num_layers, visual_seqlen, embed_dim). + The feature is extracted from the different visual encoder layers, and fed to the decoder + hidden states. It's from the paper DeepStack(https://arxiv.org/abs/2406.04334). + """ + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + use_cache = use_cache if use_cache is not None else self.config.use_cache + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + # torch.jit.trace() doesn't support cache objects in the output + if use_cache and past_key_values is None and not torch.jit.is_tracing(): + past_key_values = DynamicCache(config=self.config) + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) # [B,N,hidden_size] + + if cache_position is None: + past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0 + cache_position = torch.arange( + past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device + ) # [N] + + # the hard coded `3` is for temporal, height and width. + if position_ids is None: + position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1) # [3,B,N] + elif position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) # [3,B,N] + + if position_ids.ndim == 3 and position_ids.shape[0] == 4: + text_position_ids = position_ids[0] + position_ids = position_ids[1:] + else: + text_position_ids = position_ids[0] + + attention_mask = create_causal_mask( + config=self.config, + input_embeds=inputs_embeds, + attention_mask=attention_mask, + cache_position=cache_position, + past_key_values=past_key_values, + position_ids=text_position_ids, + ) + + hidden_states = inputs_embeds # [B,N,hidden_size] + + # create position embeddings to be shared across the decoder layers + position_embeddings = self.rotary_emb(hidden_states, position_ids) # each: [B,N,head_dim] + + # Initialize collectors like Qwen3 + all_hidden_states = () if output_hidden_states else None + all_self_attns = () if output_attentions else None + + # decoder layers + for layer_idx, decoder_layer in enumerate(self.layers): + if output_hidden_states: + all_hidden_states += (hidden_states,) + + layer_outputs = decoder_layer( + hidden_states, + attention_mask=attention_mask, + position_ids=text_position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + position_embeddings=position_embeddings, + output_attentions=output_attentions, + **kwargs, + ) + hidden_states = layer_outputs[0] + + if output_attentions: + all_self_attns += (layer_outputs[1],) + + # add visual features to the hidden states of first several layers + if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)): + hidden_states = self._deepstack_process( + hidden_states, + visual_pos_masks, + deepstack_visual_embeds[layer_idx], + ) + + hidden_states = self.norm(hidden_states) # [B,N,hidden_size] + + # add hidden states from the last decoder layer + if output_hidden_states: + all_hidden_states += (hidden_states,) + + if not return_dict: + return tuple( + v + for v in [hidden_states, past_key_values if use_cache else None, all_hidden_states, all_self_attns] + if v is not None + ) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values, + hidden_states=all_hidden_states, + attentions=all_self_attns, + ) + + def _deepstack_process( + self, + hidden_states: torch.Tensor, # [B,N,hidden_size] + visual_pos_masks: torch.Tensor, # [B,N] bool + visual_embeds: torch.Tensor, # [N_vision_tokens,hidden_size] + ): + visual_pos_masks = visual_pos_masks.to(hidden_states.device) + visual_embeds = visual_embeds.to(hidden_states.device, hidden_states.dtype) + local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds # [N_vision_tokens,hidden_size] + hidden_states[visual_pos_masks, :] = local_this + return hidden_states # [B,N,hidden_size] + + +class Qwen3VLModel(Qwen3VLPreTrainedModel): + base_model_prefix = "" + _checkpoint_conversion_mapping = {} + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLConfig + _no_split_modules = ["Qwen3VLTextDecoderLayer", "Qwen3VLVisionBlock"] + + def __init__(self, config): + super().__init__(config) + self.visual = Qwen3VLVisionModel._from_config(config.vision_config) + self.language_model = Qwen3VLTextModel._from_config(config.text_config) + self.rope_deltas = None # cache rope_deltas here + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.language_model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.language_model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.language_model = decoder + + def get_decoder(self): + return self.language_model + + def get_rope_index( + self, + input_ids: Optional[torch.LongTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Different from the original implementation, Qwen3VL use timestamps rather than absolute time position ids.""" + + # Since we use timestamps to seperate videos, like , the video_grid_thw should also be split + if video_grid_thw is not None: + video_grid_thw = torch.repeat_interleave(video_grid_thw, video_grid_thw[:, 0], dim=0) + video_grid_thw[:, 0] = 1 + + spatial_merge_size = self.config.vision_config.spatial_merge_size + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + mrope_position_deltas = [] + if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None): + total_input_ids = input_ids + if attention_mask is None: + attention_mask = torch.ones_like(total_input_ids) + position_ids = torch.ones( + 3, + input_ids.shape[0], + input_ids.shape[1], + dtype=input_ids.dtype, + device=input_ids.device, + ) # [3,B,N] + image_index, video_index = 0, 0 + attention_mask = attention_mask.to(total_input_ids.device) + for i, input_ids in enumerate(total_input_ids): + input_ids = input_ids[attention_mask[i] == 1] + image_nums, video_nums = 0, 0 + vision_start_indices = torch.argwhere(input_ids == vision_start_token_id).squeeze(1) + vision_tokens = input_ids[vision_start_indices + 1] + image_nums = (vision_tokens == image_token_id).sum() + video_nums = (vision_tokens == video_token_id).sum() + input_tokens = input_ids.tolist() + llm_pos_ids_list: list = [] + st = 0 + remain_images, remain_videos = image_nums, video_nums + for _ in range(image_nums + video_nums): + if image_token_id in input_tokens and remain_images > 0: + ed_image = input_tokens.index(image_token_id, st) + else: + ed_image = len(input_tokens) + 1 + if video_token_id in input_tokens and remain_videos > 0: + ed_video = input_tokens.index(video_token_id, st) + else: + ed_video = len(input_tokens) + 1 + if ed_image < ed_video: + t, h, w = ( + image_grid_thw[image_index][0], + image_grid_thw[image_index][1], + image_grid_thw[image_index][2], + ) + image_index += 1 + remain_images -= 1 + ed = ed_image + + else: + t, h, w = ( + video_grid_thw[video_index][0], + video_grid_thw[video_index][1], + video_grid_thw[video_index][2], + ) + video_index += 1 + remain_videos -= 1 + ed = ed_video + llm_grid_t, llm_grid_h, llm_grid_w = ( + t.item(), + h.item() // spatial_merge_size, + w.item() // spatial_merge_size, + ) + text_len = ed - st + + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) # [3,text_len] + + # t_index is always 0 because llm_grid_t is always 1 (we use timestamps to encode the temporal information for videos) + t_index = ( + torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten() + ) # [T*H*W] + h_index = ( + torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten() + ) # [T*H*W] + w_index = ( + torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten() + ) # [T*H*W] + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx) # [3,T*H*W] + st = ed + llm_grid_t * llm_grid_h * llm_grid_w + + if st < len(input_tokens): + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + text_len = len(input_tokens) - st + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) # [3,text_len] + + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) # [3,N_unmasked] + position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device) + mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i])) + mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1) # [B,1] + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + else: + if attention_mask is not None: + position_ids = attention_mask.long().cumsum(-1) - 1 # [B,N] + position_ids.masked_fill_(attention_mask == 0, 1) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device) # [3,B,N] + max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0] # [B,1] + mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1] # [B,1] + else: + position_ids = ( + torch.arange(input_ids.shape[1], device=input_ids.device) + .view(1, 1, -1) + .expand(3, input_ids.shape[0], -1) + ) # [3,B,N] + mrope_position_deltas = torch.zeros( + [input_ids.shape[0], 1], + device=input_ids.device, + dtype=input_ids.dtype, + ) # [B,1] + + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + """ + Encodes videos into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values_videos (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input videos. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + # Same implementation as for images + return self.get_image_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + """ + Encodes images into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input images. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + """ + pixel_values = pixel_values.type(self.visual.dtype) + image_embeds, deepstack_image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) + # image_embeds: [N_all_merged,out_hidden_size] + split_sizes = (image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist() + image_embeds = torch.split(image_embeds, split_sizes) + # tuple of [N_merged_i,out_hidden_size] per image + return image_embeds, deepstack_image_embeds + + def get_placeholder_mask( + self, + input_ids: torch.LongTensor, + inputs_embeds: torch.FloatTensor, + image_features: Optional[torch.FloatTensor] = None, + video_features: Optional[torch.FloatTensor] = None, + ): + """ + Obtains multimodal placeholder mask from `input_ids` or `inputs_embeds`, and checks that the placeholder token count is + equal to the length of multimodal features. If the lengths are different, an error is raised. + """ + if input_ids is None: + special_image_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_image_mask = special_image_mask.all(-1) + special_video_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_video_mask = special_video_mask.all(-1) + else: + special_image_mask = input_ids == self.config.image_token_id + special_video_mask = input_ids == self.config.video_token_id + + n_image_tokens = special_image_mask.sum() + special_image_mask = ( + special_image_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + ) # [B,N,hidden_size] bool + if image_features is not None and inputs_embeds[special_image_mask].numel() != image_features.numel(): + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {image_features.shape[0]}" + ) + + n_video_tokens = special_video_mask.sum() + special_video_mask = ( + special_video_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + ) # [B,N,hidden_size] bool + if video_features is not None and inputs_embeds[special_video_mask].numel() != video_features.numel(): + raise ValueError( + f"Videos features and video tokens do not match: tokens: {n_video_tokens}, features {video_features.shape[0]}" + ) + + return special_image_mask, special_video_mask # each: [B,N,hidden_size] bool + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLModelOutputWithPast]: + r""" + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) # [B,N,hidden_size] + + image_mask = None + video_mask = None + + if pixel_values is not None: + image_embeds, deepstack_image_embeds = self.get_image_features(pixel_values, image_grid_thw) + image_embeds = torch.cat(image_embeds, dim=0).to( + inputs_embeds.device, inputs_embeds.dtype + ) # [N_image_tokens,hidden_size] + image_mask, _ = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds + ) # image_mask: [B,N,hidden_size] bool + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) # [B,N,hidden_size] + + if pixel_values_videos is not None: + video_embeds, deepstack_video_embeds = self.get_video_features(pixel_values_videos, video_grid_thw) + video_embeds = torch.cat(video_embeds, dim=0).to( + inputs_embeds.device, inputs_embeds.dtype + ) # [N_video_tokens,hidden_size] + _, video_mask = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, video_features=video_embeds + ) # video_mask: [B,N,hidden_size] bool + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) # [B,N,hidden_size] + + visual_pos_masks = None + deepstack_visual_embeds = None + if image_mask is not None and video_mask is not None: + # aggregate visual_pos_masks and deepstack_visual_embeds + image_mask = image_mask[..., 0] # [B,N] bool + video_mask = video_mask[..., 0] # [B,N] bool + visual_pos_masks = image_mask | video_mask # [B,N] bool + deepstack_visual_embeds = [] + image_mask_joint = image_mask[visual_pos_masks] # [N_visual_tokens] bool + video_mask_joint = video_mask[visual_pos_masks] # [N_visual_tokens] bool + for img_embed, vid_embed in zip(deepstack_image_embeds, deepstack_video_embeds): + embed_joint = img_embed.new_zeros(visual_pos_masks.sum(), img_embed.shape[-1]).to( + img_embed.device + ) # [N_visual_tokens,out_hidden_size] + embed_joint[image_mask_joint, :] = img_embed + embed_joint[video_mask_joint, :] = vid_embed + deepstack_visual_embeds.append(embed_joint) # [N_visual_tokens,out_hidden_size] + elif image_mask is not None: + image_mask = image_mask[..., 0] # [B,N] bool + visual_pos_masks = image_mask + deepstack_visual_embeds = deepstack_image_embeds + elif video_mask is not None: + video_mask = video_mask[..., 0] # [B,N] bool + visual_pos_masks = video_mask + deepstack_visual_embeds = deepstack_video_embeds + + if position_ids is None: + attention_mask_tensor = ( + attention_mask if not isinstance(attention_mask, dict) else attention_mask["full_attention"] + ) + if attention_mask_tensor is not None and attention_mask_tensor.ndim == 4: + attention_mask_tensor = torch.diagonal(attention_mask_tensor[:, 0], dim1=1, dim2=2) # [B,N] + # Only apply conversion for floating point tensors (inverted masks) + if attention_mask_tensor.dtype.is_floating_point: + attention_mask_tensor = attention_mask_tensor / torch.finfo(attention_mask_tensor.dtype).min + attention_mask_tensor = (1.0 - attention_mask_tensor).int() # [B,N] + + # Calculate RoPE index once per generation in the pre-fill stage only. + # When compiling, we can't check tensor values thus we check only input length + # It is safe to assume that `length!=1` means we're in pre-fill because compiled + # models currently cannot do asssisted decoding + prefill_compiled_stage = is_torchdynamo_compiling() and ( + (input_ids is not None and input_ids.shape[1] != 1) + or (inputs_embeds is not None and inputs_embeds.shape[1] != 1) + ) + prefill_noncompiled_stage = not is_torchdynamo_compiling() and ( + (cache_position is not None and cache_position[0] == 0) + or (past_key_values is None or past_key_values.get_seq_length() == 0) + ) + if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None: + position_ids, rope_deltas = self.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + attention_mask=attention_mask_tensor, + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) if cache_position is not None else 0 + ) # [B,1] or scalar + position_ids = torch.arange(seq_length, device=inputs_embeds.device) # [N] + position_ids = position_ids.view(1, -1).expand(batch_size, -1) # [B,N] + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) # [B,1] + position_ids = position_ids.add(delta) # [B,N] + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # [3,B,N] + + outputs = self.language_model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + visual_pos_masks=visual_pos_masks, + deepstack_visual_embeds=deepstack_visual_embeds, + **kwargs, + ) + + return Qwen3VLModelOutputWithPast( + last_hidden_state=outputs.last_hidden_state, + past_key_values=outputs.past_key_values, + rope_deltas=self.rope_deltas, + ) + + +@dataclass +class Qwen3VLCausalLMOutputWithPast(ModelOutput): + r""" + loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided): + Language modeling loss (for next-token prediction). + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.vocab_size)`): + Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax). + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + loss: Optional[torch.FloatTensor] = None + logits: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + + +class Qwen3VLForConditionalGeneration(Qwen3VLPreTrainedModel, GenerationMixin): + _checkpoint_conversion_mapping = {} + _tied_weights_keys = ["lm_head.weight"] + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLConfig + + def __init__(self, config): + super().__init__(config) + self.model = Qwen3VLModel(config) + self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False) + + self.post_init() + + def get_input_embeddings(self): + return self.model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.model.set_decoder(decoder) + + def get_decoder(self): + return self.model.get_decoder() + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + return self.model.get_video_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + return self.model.get_image_features(pixel_values, image_grid_thw) + + # Make modules available through conditional class for BC + @property + def language_model(self): + return self.model.language_model + + @property + def visual(self): + return self.model.visual + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLCausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + + Example: + TODO: Add example + """ + outputs = self.model( + input_ids=input_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] # [B,N,hidden_size] + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep + logits = self.lm_head(hidden_states[:, slice_indices, :]) # [B,N_keep,vocab_size] + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size) + + return Qwen3VLCausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + rope_deltas=outputs.rope_deltas, + ) + + def prepare_inputs_for_generation( + self, + input_ids, + past_key_values=None, + attention_mask=None, + inputs_embeds=None, + cache_position=None, + position_ids=None, + use_cache=True, + pixel_values=None, + pixel_values_videos=None, + image_grid_thw=None, + video_grid_thw=None, + **kwargs, + ): + # Overwritten -- in specific circumstances we don't want to forward image inputs to the model + + model_inputs = super().prepare_inputs_for_generation( + input_ids, + past_key_values=past_key_values, + attention_mask=attention_mask, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + position_ids=position_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + use_cache=use_cache, + **kwargs, + ) + + # Qwen3VL position_ids are prepareed with rope_deltas in forward + model_inputs["position_ids"] = None + + if cache_position[0] != 0: + model_inputs["pixel_values"] = None + model_inputs["pixel_values_videos"] = None + + return model_inputs + + def _get_image_nums_and_video_nums( + self, + input_ids: Optional[torch.LongTensor], + inputs_embeds: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Get the number of images and videos for each sample to calculate the separation length of the sample tensor. + These parameters are not passed through the processor to avoid unpredictable impacts from interface modifications. + + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. + + Returns: + image_nums (`torch.LongTensor` of shape `(batch_size, num_images_sample)`) + video_nums (`torch.LongTensor` of shape `(batch_size, num_videos_sample)`) + """ + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + + if inputs_embeds is not None: + vision_start_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(vision_start_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + image_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + video_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + else: + vision_start_mask = input_ids == vision_start_token_id + image_mask = input_ids == image_token_id + video_mask = input_ids == video_token_id + + vision_first_mask = torch.roll(vision_start_mask, shifts=1, dims=1) + image_nums = torch.sum(vision_first_mask & image_mask, dim=1) + video_nums = torch.sum(vision_first_mask & video_mask, dim=1) + + return image_nums, video_nums + + def _expand_inputs_for_generation( + self, + expand_size: int = 1, + is_encoder_decoder: bool = False, + input_ids: Optional[torch.LongTensor] = None, + **model_kwargs, + ) -> tuple[torch.LongTensor, dict[str, Any]]: + # Overwritten -- Support for expanding tensors without a batch size dimension + # e.g., pixel_values, image_grid_thw, pixel_values_videos, video_grid_thw, second_per_grid_t + # pixel_values.shape[0] is sum(seqlen_images for samples) + # image_grid_thw.shape[0] is sum(num_images for samples) + + if expand_size == 1: + return input_ids, model_kwargs + + visual_keys = ["pixel_values", "image_grid_thw", "pixel_values_videos", "video_grid_thw", "second_per_grid_ts"] + + def _expand_dict_for_generation_visual(dict_to_expand): + image_grid_thw = model_kwargs.get("image_grid_thw", None) + video_grid_thw = model_kwargs.get("video_grid_thw", None) + image_nums, video_nums = self._get_image_nums_and_video_nums( + input_ids, inputs_embeds=model_kwargs.get("inputs_embeds", None) + ) + + def _repeat_interleave_samples(x, lengths, repeat_times): + samples = torch.split(x, lengths) + repeat_args = [repeat_times] + [1] * (x.dim() - 1) + result = torch.cat([sample.repeat(*repeat_args) for sample in samples], dim=0) + return result + + for key in dict_to_expand: + if key == "pixel_values": + # split images into samples + samples = torch.split(image_grid_thw, list(image_nums)) + # compute the sequence length of images for each sample + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "image_grid_thw": + # get the num of images for each sample + lengths = list(image_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "pixel_values_videos": + samples = torch.split(video_grid_thw, list(video_nums)) + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "video_grid_thw": + lengths = list(video_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "second_per_grid_ts": + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=list(video_nums), repeat_times=expand_size + ) + return dict_to_expand + + def _expand_dict_for_generation(dict_to_expand): + for key in dict_to_expand: + if ( + key != "cache_position" + and dict_to_expand[key] is not None + and isinstance(dict_to_expand[key], torch.Tensor) + and key not in visual_keys + ): + dict_to_expand[key] = dict_to_expand[key].repeat_interleave(expand_size, dim=0) + return dict_to_expand + + model_kwargs = _expand_dict_for_generation_visual(model_kwargs) + + if input_ids is not None: + input_ids = input_ids.repeat_interleave(expand_size, dim=0) + + model_kwargs = _expand_dict_for_generation(model_kwargs) + + if is_encoder_decoder: + if model_kwargs.get("encoder_outputs") is None: + raise ValueError("If `is_encoder_decoder` is True, make sure that `encoder_outputs` is defined.") + model_kwargs["encoder_outputs"] = _expand_dict_for_generation(model_kwargs["encoder_outputs"]) + + return input_ids, model_kwargs + + +__all__ = [ + "Qwen3VLVisionModel", + "Qwen3VLForConditionalGeneration", + "Qwen3VLModel", + "Qwen3VLPreTrainedModel", + "Qwen3VLTextModel", +] diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py new file mode 100644 index 00000000..7d212875 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py @@ -0,0 +1,348 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Core masking functions extracted from transformers.masking_utils for BAGEL compatibility +# Original Copyright 2025 HuggingFace Inc. team. Licensed under the Apache License, Version 2.0 + +from typing import Callable, ClassVar, Optional, cast + +import torch +from transformers.cache_utils import Cache +from transformers.configuration_utils import PretrainedConfig +from transformers.tokenization_utils_base import PreTrainedTokenizerBase +from transformers.utils import logging + +# from transformers.utils.generic import GeneralInterface +from transformers.utils.import_utils import is_torch_greater_or_equal + +logger = logging.get_logger(__name__) + +_is_torch_greater_or_equal_than_2_6 = is_torch_greater_or_equal("2.6", accept_dev=True) + +_SYSTEM_PROMPT_IMAGE = "You are a helpful assistant who will generate images from a give prompt." +_SYSTEM_PROMPT_VIDEO = "You are a helpful assistant who will generate videos from a give prompt." +_SYSTEM_PROMPT_TRANSFER = ( + "You are a helpful assistant that generates images or videos following the user's instructions" + " and control signals (edge maps, blur, depth, or segmentation)." +) +_SYSTEM_PROMPT_IMAGE_EDITING = "You are a helpful assistant who will edit images based on the user's instructions." + + +def tokenize_caption( + caption: str, + tokenizer: PreTrainedTokenizerBase, + is_video: bool = False, + use_system_prompt: bool = False, + system_prompt: Optional[str] = None, +) -> list[int]: + """Tokenize a text caption into token IDs using the Qwen2 chat template. + + Wraps the caption in a chat-style conversation (with a "user" role) and applies + the tokenizer's chat template to produce the final token ID sequence, including + any special tokens (e.g., BOS, role markers, generation prompt). + + Args: + caption: The text caption to tokenize. + tokenizer: A HuggingFace ``PreTrainedTokenizerBase`` (e.g. Qwen2Tokenizer or Fast tokenizer). + is_video: If True (and use_system_prompt=True), uses the video system prompt; + otherwise uses the image system prompt. Ignored when ``system_prompt`` is + provided. + use_system_prompt: If True, prepends a system prompt message to the conversation + before the user caption. Ignored when ``system_prompt`` is provided. + system_prompt: When supplied, this exact string is used as the system prompt, + overriding both ``is_video`` and ``use_system_prompt``. + + Returns: + List of token IDs representing the full chat-formatted caption. + """ + conversations = [] + if system_prompt is not None: + conversations.append({"role": "system", "content": system_prompt}) + elif use_system_prompt: + _system_prompt = _SYSTEM_PROMPT_VIDEO if is_video else _SYSTEM_PROMPT_IMAGE + conversations.append({"role": "system", "content": _system_prompt}) + conversations.append({"role": "user", "content": caption}) + + tokenizer_output = tokenizer.apply_chat_template( + conversations, + tokenize=True, + add_generation_prompt=True, + add_vision_id=False, + return_dict=False, + ) + return cast(list[int], tokenizer_output) + + +def causal_mask_function(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool: + """ + This creates a basic lower-diagonal causal mask. + """ + return kv_idx <= q_idx + + +def sliding_window_overlay(sliding_window: int) -> Callable: + """ + This is an overlay depicting a sliding window pattern. Add it on top of a causal mask for a proper sliding + window mask. + """ + + def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool: + return kv_idx > q_idx - sliding_window + + return inner_mask + + +def and_masks(*mask_functions: list[Callable]) -> Callable: + """Returns a mask function that is the intersection of provided mask functions""" + if not all(callable(arg) for arg in mask_functions): + raise RuntimeError(f"All inputs should be callable mask_functions: {mask_functions}") + + def and_mask(batch_idx, head_idx, q_idx, kv_idx): + result = q_idx.new_ones((), dtype=torch.bool) + for mask in mask_functions: + result = result & mask(batch_idx, head_idx, q_idx, kv_idx).to(result.device) + return result + + return and_mask + + +def sliding_window_causal_mask_function(sliding_window: int) -> Callable: + """ + This return the mask_function function to create a sliding window mask. + """ + return and_masks(sliding_window_overlay(sliding_window), causal_mask_function) + + +def padding_mask_function(padding_mask: torch.Tensor) -> Callable: + """ + This return the mask_function function corresponding to a 2D padding mask. + """ + + def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool: + return padding_mask[batch_idx, kv_idx] + + return inner_mask + + +def _vmap_for_bhqkv(mask_function: Callable, bh_indices: bool = True) -> Callable: + """ + Used to vmap our mask_functions over the q_idx and kv_idx dimensions of the inputs. + """ + # We vmap the function 2 times, broadcasting the [q_idx, kv_idx] dimensions + dimensions = [(None, None, None, 0), (None, None, 0, None)] + if bh_indices: + # We extend broadcasting over the [batch_idx, head_idx] dimensions + dimensions.extend([(None, 0, None, None), (0, None, None, None)]) + + for dims in dimensions: + mask_function = torch.vmap(mask_function, in_dims=dims, out_dims=0) + return mask_function + + +def prepare_padding_mask( + attention_mask: Optional[torch.Tensor], kv_length: int, kv_offset: int, _slice: bool = True +) -> Optional[torch.Tensor]: + """ + From the 2D attention mask, prepare the correct padding mask to use by potentially padding it, and slicing + according to the `kv_offset` if `_slice` is `True`. + """ + local_padding_mask = attention_mask + if attention_mask is not None: + # Pad it if necessary + if (padding_length := kv_length + kv_offset - attention_mask.shape[-1]) > 0: + local_padding_mask = torch.nn.functional.pad(attention_mask, (0, padding_length)) + # For flex, we should not slice them, only use an offset + if _slice: + # Equivalent to: `local_padding_mask = attention_mask[:, kv_offset : kv_offset + kv_length]`, + # but without data-dependent slicing (i.e. torch.compile friendly) + mask_indices = torch.arange(kv_length, device=local_padding_mask.device) + mask_indices += kv_offset + local_padding_mask = local_padding_mask[:, mask_indices] + return local_padding_mask + + +def eager_mask( + batch_size: int, + cache_position: torch.Tensor, + kv_length: int, + kv_offset: int = 0, + mask_function: Callable = causal_mask_function, + attention_mask: Optional[torch.Tensor] = None, + dtype: torch.dtype = torch.float32, + **kwargs, +) -> torch.Tensor: + """ + Create a 4D float mask of shape `(batch_size, 1, query_length, kv_length)` where a value of 0 indicates that + the element should take part in the attention computation, and -inf (minimum value for the given `dtype`) that + it should not. + """ + # Potentially pad the 2D mask, and slice it correctly + padding_mask = prepare_padding_mask(attention_mask, kv_length, kv_offset) + + # Similar to `kv_arange = torch.arange(start=kv_offset, end=kv_offset + kv_length, device=cache_position.device)` + # but without data-dependent slicing (i.e. torch.compile friendly) + kv_arange = torch.arange(kv_length, device=cache_position.device) + kv_arange += kv_offset + + # Create the 4D mask easily + causal_mask = _vmap_for_bhqkv(mask_function, bh_indices=False)( + None, None, cache_position, kv_arange + ) # [q_len,kv_length] + causal_mask = causal_mask[None, None, :, :].expand(batch_size, -1, -1, -1) # [B,1,q_len,kv_length] + if padding_mask is not None: + causal_mask = causal_mask * padding_mask[:, None, None, :] # [B,1,q_len,kv_length] + + min_dtype = torch.finfo(dtype).min + # we need 0s where the tokens should be taken into account, and -inf otherwise + mask = torch.where( + causal_mask, torch.tensor(0.0, device=causal_mask.device, dtype=dtype), min_dtype + ) # [B,1,q_len,kv_length] + return mask + + +# class AttentionMaskInterface(GeneralInterface): +class AttentionMaskInterface: + # Class instance object for mask interfaces + _global_mapping: ClassVar = { + "eager": eager_mask, + } + + +# Global AttentionMaskInterface shared by all models +ALL_MASK_ATTENTION_FUNCTIONS: AttentionMaskInterface = AttentionMaskInterface() + + +def _preprocess_mask_arguments( + config: PretrainedConfig, + input_embeds: torch.Tensor, + attention_mask: Optional[torch.Tensor], + cache_position: torch.Tensor, + past_key_values: Optional[Cache], + position_ids: Optional[torch.Tensor], + layer_idx: Optional[int], +) -> tuple[bool, Optional[torch.Tensor], None, int, int]: + """ + Perform some common pre-processing of the mask arguments we get from the modeling code. + """ + # If the mask is already 4D, simply return as-is + if isinstance(attention_mask, torch.Tensor) and len(attention_mask.shape) == 4: + return True, attention_mask, None, None, None + + # For TGI/vLLM backends or other custom attention: we don't need a mask + if config._attn_implementation not in ALL_MASK_ATTENTION_FUNCTIONS._global_mapping: + return True, None, None, None, None + + # Move the mask to correct device, and potentially switch dtype for efficiency + if attention_mask is not None and attention_mask.ndim == 2: + attention_mask = attention_mask.to(device=cache_position.device, dtype=torch.bool) + + # If using a cache, it can give all information about mask sizes based on seen tokens + if past_key_values is not None: + kv_length, kv_offset = past_key_values.get_mask_sizes(cache_position, layer_idx) + # Otherwise, the sizes are simply the input sizes + else: + kv_length, kv_offset = input_embeds.shape[1], 0 + + return False, attention_mask, None, kv_length, kv_offset + + +def create_causal_mask( + config: PretrainedConfig, + input_embeds: torch.Tensor, + attention_mask: Optional[torch.Tensor], + cache_position: torch.Tensor, + past_key_values: Optional[Cache], + position_ids: Optional[torch.Tensor] = None, + **kwargs, +) -> Optional[torch.Tensor]: + """ + Create a standard causal mask based on the attention implementation used (stored in the config). + """ + # For hybrid cache structure, use the full_attention layers + layer_idx = 0 + + early_exit, attention_mask, packed_sequence_mask, kv_length, kv_offset = _preprocess_mask_arguments( + config, input_embeds, attention_mask, cache_position, past_key_values, position_ids, layer_idx + ) + if early_exit: + return attention_mask + + batch_size, dtype = input_embeds.shape[0], input_embeds.dtype + mask_factory_function = causal_mask_function + mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation] + + # Potentially add the padding 2D mask + if attention_mask is not None: + mask_factory_function = and_masks(mask_factory_function, padding_mask_function(attention_mask)) + + # We now create the mask + causal_mask = mask_interface( + batch_size=batch_size, + cache_position=cache_position, + kv_length=kv_length, + kv_offset=kv_offset, + mask_function=mask_factory_function, + attention_mask=attention_mask, + dtype=dtype, + config=config, + ) + return causal_mask + + +def create_sliding_window_causal_mask( + config: PretrainedConfig, + input_embeds: torch.Tensor, + attention_mask: Optional[torch.Tensor], + cache_position: torch.Tensor, + past_key_values: Optional[Cache], + position_ids: Optional[torch.Tensor] = None, + **kwargs, +) -> Optional[torch.Tensor]: + """ + Create a sliding window causal mask based on the attention implementation used (stored in the config). + """ + # For hybrid cache structure, use the sliding_attention layers + layer_idx = 0 + + early_exit, attention_mask, packed_sequence_mask, kv_length, kv_offset = _preprocess_mask_arguments( + config, input_embeds, attention_mask, cache_position, past_key_values, position_ids, layer_idx + ) + if early_exit: + return attention_mask + + sliding_window = getattr(config, "sliding_window", None) + if sliding_window is None: + raise ValueError("Could not find a `sliding_window` argument in the config, or it is not set") + + batch_size, dtype = input_embeds.shape[0], input_embeds.dtype + mask_factory_function = sliding_window_causal_mask_function(sliding_window) + mask_interface = ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation] + + # Potentially add the padding 2D mask + if attention_mask is not None: + mask_factory_function = and_masks(mask_factory_function, padding_mask_function(attention_mask)) + + # We now create the mask + causal_mask = mask_interface( + batch_size=batch_size, + cache_position=cache_position, + kv_length=kv_length, + kv_offset=kv_offset, + mask_function=mask_factory_function, + attention_mask=attention_mask, + dtype=dtype, + config=config, + ) + return causal_mask diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py new file mode 100644 index 00000000..d5d8ebc2 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py @@ -0,0 +1,297 @@ +# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +# Source Repository: https://github.com/huggingface/transformers +# This is adapted from src/transformers/models/qwen3_vl/video_processing_qwen3_vl.py. +# Commit Hash: 41e5abac5cb49983a08ddef3e8645d6efd23c8f3 +"""video processor class for Qwen3-VL.""" + +import math +from typing import Optional, Union + +import numpy as np +import torch +from transformers.feature_extraction_utils import BatchFeature +from transformers.image_utils import ChannelDimension, PILImageResampling, SizeDict, get_image_size +from transformers.processing_utils import Unpack, VideosKwargs +from transformers.utils import TensorType, add_start_docstrings, logging +from transformers.video_processing_utils import BASE_VIDEO_PROCESSOR_DOCSTRING, BaseVideoProcessor +from transformers.video_utils import VideoMetadata, group_videos_by_shape, reorder_videos + +logger = logging.get_logger(__name__) + + +def smart_resize( + num_frames: int, + height: int, + width: int, + temporal_factor: int = 2, + factor: int = 32, + min_pixels: int = 128 * 128, + max_pixels: int = 16 * 16 * 2 * 2 * 2 * 6144, +): + if num_frames < temporal_factor: + raise ValueError(f"t:{num_frames} must be larger than temporal_factor:{temporal_factor}") + if height < factor or width < factor: + raise ValueError(f"height:{height} or width:{width} must be larger than factor:{factor}") + elif max(height, width) / min(height, width) > 200: + raise ValueError( + f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}" + ) + h_bar = round(height / factor) * factor + w_bar = round(width / factor) * factor + t_bar = round(num_frames / temporal_factor) * temporal_factor + + if t_bar * h_bar * w_bar > max_pixels: + beta = math.sqrt((num_frames * height * width) / max_pixels) + h_bar = max(factor, math.floor(height / beta / factor) * factor) + w_bar = max(factor, math.floor(width / beta / factor) * factor) + elif t_bar * h_bar * w_bar < min_pixels: + beta = math.sqrt(min_pixels / (num_frames * height * width)) + h_bar = math.ceil(height * beta / factor) * factor + w_bar = math.ceil(width * beta / factor) * factor + + return h_bar, w_bar + + +class Qwen3VLVideoProcessorInitKwargs(VideosKwargs): + patch_size: Optional[int] + temporal_patch_size: Optional[int] + merge_size: Optional[int] + min_frames: Optional[int] + max_frames: Optional[int] + + +@add_start_docstrings( + "Constructs a fast Qwen3-VL image processor that dynamically resizes videos based on the original videos.", + BASE_VIDEO_PROCESSOR_DOCSTRING, + """ + patch_size (`int`, *optional*, defaults to 16): + The spacial patch size of the vision encoder. + temporal_patch_size (`int`, *optional*, defaults to 2): + The temporal patch size of the vision encoder. + merge_size (`int`, *optional*, defaults to 2): + The merge size of the vision encoder to llm encoder. + """, +) +class Qwen3VLVideoProcessor(BaseVideoProcessor): + resample = PILImageResampling.BICUBIC + size = {"shortest_edge": 128 * 32 * 32, "longest_edge": 32 * 32 * 768} + image_mean = [0.5, 0.5, 0.5] + image_std = [0.5, 0.5, 0.5] + do_resize = True + do_rescale = True + do_normalize = True + do_convert_rgb = True + patch_size = 16 + temporal_patch_size = 2 + merge_size = 2 + fps = 2 + min_frames = 4 + max_frames = 768 + do_sample_frames = True + valid_kwargs = Qwen3VLVideoProcessorInitKwargs + model_input_names = ["pixel_values_videos", "video_grid_thw"] + + def __init__(self, **kwargs: Unpack[Qwen3VLVideoProcessorInitKwargs]): + super().__init__(**kwargs) + if self.size is not None and ( + self.size.get("shortest_edge", None) is None or self.size.get("longest_edge", None) is None + ): + raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.") + + def _further_process_kwargs( + self, + size: Optional[SizeDict] = None, + **kwargs, + ) -> dict: + """ + Update kwargs that need further processing before being validated + Can be overridden by subclasses to customize the processing of kwargs. + """ + if size is not None and ("shortest_edge" not in size or "longest_edge" not in size): + raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.") + + return super()._further_process_kwargs(size=size, **kwargs) + + def sample_frames( + self, + metadata: VideoMetadata, + num_frames: Optional[int] = None, + fps: Optional[Union[int, float]] = None, + **kwargs, + ): + """ + Default sampling function which uniformly samples the desired number of frames between 0 and total number of frames. + If `fps` is passed along with metadata, `fps` frames per second are sampled uniformty. Arguments `num_frames` + and `fps` are mutually exclusive. + + Args: + video (`torch.Tensor`): + Video that need to be sampled. + metadata (`VideoMetadata`): + Metadata of the video containing information about total duration, fps and total number of frames. + num_frames (`int`, *optional*): + Maximum number of frames to sample. Defaults to `self.num_frames`. + fps (`int` or `float`, *optional*): + Target frames to sample per second. Defaults to `self.fps`. + Returns: + torch.Tensor: + Sampled video frames. + """ + if fps is not None and num_frames is not None: + raise ValueError("`num_frames` and `fps` are mutually exclusive arguments, please use only one!") + + total_num_frames = metadata.total_num_frames + fps = fps if fps is not None else self.fps + + # If num_frames is not given but fps is, calculate num_frames from fps + if num_frames is None and fps is not None: + if metadata.fps is None: + metadata.fps = 24 + logger.warning_once( + "Asked to sample `fps` frames per second but no video metadata was provided which is required when sampling with `fps`. " + "Defaulting to `fps=24`. Please provide `video_metadata` for more accurate results." + ) + num_frames = int(total_num_frames / metadata.fps * fps) + num_frames = min(min(max(num_frames, self.min_frames), self.max_frames), total_num_frames) + + if num_frames is None: + num_frames = min(max(total_num_frames, self.min_frames), self.max_frames) + + indices = np.linspace(0, total_num_frames - 1, num_frames).round().astype(int) + + return indices + + def _preprocess( + self, + videos: list[torch.Tensor], + do_convert_rgb: bool = True, + do_resize: bool = True, + size: Optional[SizeDict] = None, + interpolation: PILImageResampling = PILImageResampling.BICUBIC, + do_rescale: bool = True, + rescale_factor: float = 1 / 255.0, + do_normalize: bool = True, + image_mean: Optional[Union[float, list[float]]] = None, + image_std: Optional[Union[float, list[float]]] = None, + patch_size: Optional[int] = None, + temporal_patch_size: Optional[int] = None, + merge_size: Optional[int] = None, + return_tensors: Optional[Union[str, TensorType]] = None, + **kwargs, + ): + grouped_videos, grouped_videos_index = group_videos_by_shape(videos) + resized_videos_grouped = {} + + for shape, stacked_videos in grouped_videos.items(): + B, T, C, H, W = stacked_videos.shape + num_frames, height, width = T, H, W + if do_resize: + resized_height, resized_width = smart_resize( + num_frames=num_frames, + height=height, + width=width, + temporal_factor=temporal_patch_size, + factor=patch_size * merge_size, + min_pixels=size.shortest_edge, + max_pixels=size.longest_edge, + ) + stacked_videos = stacked_videos.view(B * T, C, H, W) # [B*T,C,H,W] + stacked_videos = self.resize( + stacked_videos, + size=SizeDict(height=resized_height, width=resized_width), + interpolation=interpolation, + ) # [B*T,C,resized_height,resized_width] + stacked_videos = stacked_videos.view( + B, T, C, resized_height, resized_width + ) # [B,T,C,resized_height,resized_width] + resized_videos_grouped[shape] = stacked_videos + resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index) + + # Group videos by size for further processing + # Needed in case do_resize is False, or resize returns videos with different sizes + grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos) + processed_videos_grouped = {} + processed_grids = {} + for shape, stacked_videos in grouped_videos.items(): + resized_height, resized_width = get_image_size(stacked_videos[0], channel_dim=ChannelDimension.FIRST) + + # Fused rescale and normalize + stacked_videos = self.rescale_and_normalize( + stacked_videos, do_rescale, rescale_factor, do_normalize, image_mean, image_std + ) + patches = stacked_videos + + # Check that videos have `num_frames` divisible by `temporal_patch_size` + if patches.shape[1] % temporal_patch_size != 0: + repeats = patches[:, -1:].repeat(1, temporal_patch_size - 1, 1, 1, 1) # [B,temporal_patch_size-1,C,H,W] + patches = torch.cat([patches, repeats], dim=1) # [B,T_padded,C,H,W] + batch_size, grid_t, channel = patches.shape[:3] + grid_t = grid_t // temporal_patch_size + grid_h, grid_w = resized_height // patch_size, resized_width // patch_size + + patches = patches.view( + batch_size, + grid_t, + temporal_patch_size, + channel, + grid_h // merge_size, + merge_size, + patch_size, + grid_w // merge_size, + merge_size, + patch_size, + ) # [B,grid_t,temporal_patch_size,C,grid_h//merge_size,merge_size,patch_size,grid_w//merge_size,merge_size,patch_size] + patches = patches.permute( + 0, 1, 4, 7, 5, 8, 3, 2, 6, 9 + ) # [B,grid_t,grid_h//merge_size,grid_w//merge_size,merge_size,merge_size,C,temporal_patch_size,patch_size,patch_size] + flatten_patches = patches.reshape( + batch_size, + grid_t * grid_h * grid_w, + channel * temporal_patch_size * patch_size * patch_size, + ) # [B,grid_t*grid_h*grid_w,C*temporal_patch_size*patch_size*patch_size] + + processed_videos_grouped[shape] = flatten_patches + processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size + + processed_videos = reorder_videos(processed_videos_grouped, grouped_videos_index) + processed_grids = reorder_videos(processed_grids, grouped_videos_index) + pixel_values_videos = torch.cat( + processed_videos, dim=0 + ) # [total_videos,N_tokens,C*temporal_patch_size*patch_size*patch_size] + video_grid_thw = torch.tensor(processed_grids) # [total_videos,3] + data = { + "pixel_values_videos": pixel_values_videos, + "video_grid_thw": video_grid_thw, + } + + return BatchFeature(data=data, tensor_type=return_tensors) + + +__all__ = ["Qwen3VLVideoProcessor"] diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json new file mode 100644 index 00000000..f1933484 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json @@ -0,0 +1,68 @@ +{ + "architectures": [ + "Qwen3VLMoeForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl_moe", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "decoder_sparse_step": 1, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 4096, + "initializer_range": 0.02, + "intermediate_size": 12288, + "max_position_embeddings": 262144, + "mlp_only_layers": [], + "model_type": "qwen3_vl_moe_text", + "moe_intermediate_size": 1536, + "norm_topk_prob": true, + "num_attention_heads": 64, + "num_experts": 128, + "num_experts_per_tok": 8, + "num_hidden_layers": 94, + "num_key_value_heads": 4, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl_moe", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 4096, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json new file mode 100644 index 00000000..23665bac --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json @@ -0,0 +1,68 @@ +{ + "architectures": [ + "Qwen3VLMoeForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl_moe", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "decoder_sparse_step": 1, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2048, + "initializer_range": 0.02, + "intermediate_size": 6144, + "max_position_embeddings": 262144, + "mlp_only_layers": [], + "model_type": "qwen3_vl_moe_text", + "moe_intermediate_size": 768, + "norm_topk_prob": true, + "num_attention_heads": 32, + "num_experts": 128, + "num_experts_per_tok": 8, + "num_hidden_layers": 48, + "num_key_value_heads": 4, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl_moe", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 2048, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py new file mode 100644 index 00000000..e5d834a5 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py @@ -0,0 +1,330 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_rope_utils import rope_config_validation + + +class Qwen3VLMoeTextConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLMoeTextModel`]. It is used to instantiate a + Qwen3-VL-MOE model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-30B-A3B-Instruct [Qwen/Qwen3-VL-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + Args: + vocab_size (`int`, *optional*, defaults to 151936): + Vocabulary size of the Qwen2MoE model. Defines the number of different tokens that can be represented by the + `inputs_ids` passed when calling [`Qwen2MoeModel`] + hidden_size (`int`, *optional*, defaults to 2048): + Dimension of the hidden representations. + intermediate_size (`int`, *optional*, defaults to 5632): + Dimension of the MLP representations. + num_hidden_layers (`int`, *optional*, defaults to 24): + Number of hidden layers in the Transformer encoder. + num_attention_heads (`int`, *optional*, defaults to 16): + Number of attention heads for each attention layer in the Transformer encoder. + num_key_value_heads (`int`, *optional*, defaults to 16): + This is the number of key_value heads that should be used to implement Grouped Query Attention. If + `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if + `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When + converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed + by meanpooling all the original heads within that group. For more details checkout [this + paper](https://arxiv.org/pdf/2305.13245.pdf). If it is not specified, will default to `32`. + hidden_act (`str` or `function`, *optional*, defaults to `"silu"`): + The non-linear activation function (function or string) in the decoder. + max_position_embeddings (`int`, *optional*, defaults to 128000): + The maximum sequence length that this model might ever be used with. + initializer_range (`float`, *optional*, defaults to 0.02): + The standard deviation of the truncated_normal_initializer for initializing all weight matrices. + rms_norm_eps (`float`, *optional*, defaults to 1e-06): + The epsilon used by the rms normalization layers. + use_cache (`bool`, *optional*, defaults to `True`): + Whether or not the model should return the last key/values attentions (not used by all models). Only + relevant if `config.is_decoder=True`. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether the model's input and output word embeddings should be tied. + rope_theta (`float`, *optional*, defaults to 5000000.0): + The base period of the RoPE embeddings. + attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`): + Whether to use a bias in the query, key, value and output projection layers during self-attention. + attention_dropout (`float`, *optional*, defaults to 0.0): + The dropout ratio for the attention probabilities. + decoder_sparse_step (`int`, *optional*, defaults to 1): + The frequency of the MoE layer. + moe_intermediate_size (`int`, *optional*, defaults to 1408): + Intermediate size of the routed expert. + num_experts_per_tok (`int`, *optional*, defaults to 4): + Number of selected experts. + num_experts (`int`, *optional*, defaults to 60): + Number of routed experts. + norm_topk_prob (`bool`, *optional*, defaults to `True`): + Whether to normalize the topk probabilities. + router_aux_loss_coef (`float`, *optional*, defaults to 0.001): + The aux loss factor for the total loss. + mlp_only_layers (`List[int]`, *optional*, defaults to `[]`): + Indicate which layers use Qwen3VLMoeMLP rather than Qwen3VLMoeSparseMoeBlock + The list contains layer index, from 0 to num_layers-1 if we have num_layers layers + If `mlp_only_layers` is empty, `decoder_sparse_step` is used to determine the sparsity. + rope_scaling (`Dict`, *optional*): + Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type + and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value + accordingly. + Expected contents: + `rope_type` (`str`): + The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope', + 'llama3'], with 'default' being the original RoPE implementation. + `factor` (`float`, *optional*): + Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In + most scaling types, a `factor` of x will enable the model to handle sequences of length x * + original maximum pre-trained length. + `original_max_position_embeddings` (`int`, *optional*): + Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during + pretraining. + `attention_factor` (`float`, *optional*): + Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention + computation. If unspecified, it defaults to value recommended by the implementation, using the + `factor` field to infer the suggested value. + `beta_fast` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear + ramp function. If unspecified, it defaults to 32. + `beta_slow` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear + ramp function. If unspecified, it defaults to 1. + `short_factor` (`List[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to short contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `long_factor` (`List[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to long contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `low_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE + `high_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE + head_dim (`int`, *optional*): + The dimension of the head. If not specified, will default to `hidden_size // num_attention_heads`. + + ```python + >>> from transformers import Qwen3VLMoeForConditionalGeneration, Qwen3VLMoeConfig + + >>> # Initializing a Qwen3VLMoe style configuration + >>> configuration = Qwen3VLMoeConfig() + + >>> # Initializing a model from the Qwen3-VL-30B-A3B style configuration + >>> model = Qwen3VLMoeForConditionalGeneration(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl_moe_text" + base_config_key = "text_config" + keys_to_ignore_at_inference = ["past_key_values"] + # Default tensor parallel plan for base model `Qwen3VLMoe` + base_model_tp_plan = { + "layers.*.self_attn.q_proj": "colwise", + "layers.*.self_attn.k_proj": "colwise", + "layers.*.self_attn.v_proj": "colwise", + "layers.*.self_attn.o_proj": "rowwise", + "layers.*.mlp.gate_proj": "colwise", + "layers.*.mlp.up_proj": "colwise", + "layers.*.mlp.down_proj": "rowwise", + } + base_model_pp_plan = { + "embed_tokens": (["input_ids"], ["inputs_embeds"]), + "layers": (["hidden_states", "attention_mask"], ["hidden_states"]), + "norm": (["hidden_states"], ["hidden_states"]), + } + + def __init__( + self, + vocab_size=151936, + hidden_size=2048, + intermediate_size=5632, + num_hidden_layers=24, + num_attention_heads=16, + num_key_value_heads=16, + hidden_act="silu", + max_position_embeddings=128000, + initializer_range=0.02, + rms_norm_eps=1e-6, + use_cache=True, + tie_word_embeddings=False, + rope_theta=5000000.0, + attention_bias=False, + attention_dropout=0.0, + decoder_sparse_step=1, + moe_intermediate_size=1408, + num_experts_per_tok=4, + num_experts=60, + norm_topk_prob=True, + router_aux_loss_coef=0.001, + mlp_only_layers=None, + rope_scaling=None, + head_dim=None, + **kwargs, + ): + self.vocab_size = vocab_size + self.max_position_embeddings = max_position_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + + # for backward compatibility + if num_key_value_heads is None: + num_key_value_heads = num_attention_heads + + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.attention_bias = attention_bias + self.attention_dropout = attention_dropout + self.rope_scaling = rope_scaling + self.head_dim = head_dim or hidden_size // num_attention_heads + + rope_config_validation(self, ignore_keys={"mrope_section", "mrope_interleaved"}) + + # MoE arguments + self.decoder_sparse_step = decoder_sparse_step + self.moe_intermediate_size = moe_intermediate_size + self.num_experts_per_tok = num_experts_per_tok + self.num_experts = num_experts + self.norm_topk_prob = norm_topk_prob + self.router_aux_loss_coef = router_aux_loss_coef + self.mlp_only_layers = [] if mlp_only_layers is None else mlp_only_layers + + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + +class Qwen3VLMoeVisionConfig(PretrainedConfig): + model_type = "qwen3_vl_moe" + base_config_key = "vision_config" + + def __init__( + self, + depth=27, + hidden_size=1152, + hidden_act="gelu_pytorch_tanh", + intermediate_size=4304, + num_heads=16, + in_channels=3, + patch_size=16, + spatial_merge_size=2, + temporal_patch_size=2, + out_hidden_size=3584, + num_position_embeddings=2304, + deepstack_visual_indexes=[8, 16, 24], + initializer_range=0.02, + **kwargs, + ): + super().__init__(**kwargs) + + self.depth = depth + self.hidden_size = hidden_size + self.hidden_act = hidden_act + self.intermediate_size = intermediate_size + self.num_heads = num_heads + self.in_channels = in_channels + self.patch_size = patch_size + self.spatial_merge_size = spatial_merge_size + self.temporal_patch_size = temporal_patch_size + self.out_hidden_size = out_hidden_size + self.num_position_embeddings = num_position_embeddings + self.initializer_range = initializer_range + self.deepstack_visual_indexes = deepstack_visual_indexes + + +class Qwen3VLMoeConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLMoeModel`]. It is used to instantiate a + Qwen3-VL-MOE model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-30B-A3B-Instruct [Qwen/Qwen3-VL-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + + Args: + text_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLMoeTextConfig`): + The config object or dictionary of the text backbone. + vision_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLMoeVisionConfig`): + The config object or dictionary of the vision backbone. + image_token_id (`int`, *optional*, defaults to 151655): + The image token index to encode the image prompt. + video_token_id (`int`, *optional*, defaults to 151656): + The video token index to encode the image prompt. + vision_start_token_id (`int`, *optional*, defaults to 151652): + The start token index to encode the image prompt. + vision_end_token_id (`int`, *optional*, defaults to 151653): + The end token index to encode the image prompt. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether to tie the word embeddings. + + ```python + >>> from transformers import Qwen3VLMoeForConditionalGeneration, Qwen3VLMoeConfig + + >>> # Initializing a Qwen3-VL-MOE style configuration + >>> configuration = Qwen3VLMoeConfig() + + >>> # Initializing a model from the Qwen3-VL-30B-A3B style configuration + >>> model = Qwen3VLMoeForConditionalGeneration(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl_moe" + sub_configs = {"vision_config": Qwen3VLMoeVisionConfig, "text_config": Qwen3VLMoeTextConfig} + keys_to_ignore_at_inference = ["past_key_values"] + + def __init__( + self, + text_config=None, + vision_config=None, + image_token_id=151655, + video_token_id=151656, + vision_start_token_id=151652, + vision_end_token_id=151653, + tie_word_embeddings=False, + **kwargs, + ): + if isinstance(vision_config, dict): + self.vision_config = self.sub_configs["vision_config"](**vision_config) + elif vision_config is None: + self.vision_config = self.sub_configs["vision_config"]() + + if isinstance(text_config, dict): + self.text_config = self.sub_configs["text_config"](**text_config) + elif text_config is None: + self.text_config = self.sub_configs["text_config"]() + + self.image_token_id = image_token_id + self.video_token_id = video_token_id + self.vision_start_token_id = vision_start_token_id + self.vision_end_token_id = vision_end_token_id + super().__init__(**kwargs, tie_word_embeddings=tie_word_embeddings) + + +__all__ = ["Qwen3VLMoeConfig", "Qwen3VLMoeTextConfig"] diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py new file mode 100644 index 00000000..f8c25d32 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py @@ -0,0 +1,261 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Callable + +import torch +import torch.nn as nn +from transformers.activations import ACT2FN + +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeTextConfig, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_kernels import ( + TOKEN_GROUP_ALIGN_SIZE_M, + _generate_permute_indices, +) + + +def _run_experts_grouped_mm( + gate_up_proj: torch.Tensor, # [num_experts,hidden_size,2*moe_intermediate_size] + down_proj: torch.Tensor, # [num_experts,moe_intermediate_size,hidden_size] + act_fn: Callable[[torch.Tensor], torch.Tensor], + x: torch.Tensor, # [num_tokens,hidden_size] (tokens sorted by expert) + num_tokens_per_expert: torch.Tensor, # [num_experts] + scores: torch.Tensor, # [padded_len] +) -> torch.Tensor: # [num_tokens,hidden_size] + """ + This function runs the gate/up/down projection in a grouped matrix multiplication fashion. + + Args: + gate_up_proj (torch.Tensor): (num_experts, hidden_size, 2 * moe_intermediate_size) + down_proj (torch.Tensor): (num_experts, moe_intermediate_size, hidden_size) + x (torch.Tensor): (batch_size * seq_len, hidden_size) + num_tokens_per_expert (torch.Tensor): (num_experts,) + scores (torch.Tensor): (num_tokens,) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + offsets = torch.cumsum(num_tokens_per_expert, dim=0, dtype=torch.int32) # [num_experts] + h = torch._grouped_mm(x, gate_up_proj, offs=offsets) # [num_tokens,2*moe_intermediate_size] + h = torch.chunk(h, chunks=2, dim=-1) # 2x [num_tokens,moe_intermediate_size] + h = act_fn(h[0]) * h[1] * scores.unsqueeze(-1) # [num_tokens,moe_intermediate_size] + return torch._grouped_mm(h, down_proj, offs=offsets) # [num_tokens,hidden_size] + + +class Qwen3VLMoeTextExpertsGroupedMm(nn.Module): + def __init__(self, config): + super().__init__() + self.gate_up_proj = nn.Parameter( + torch.empty(config.num_experts, config.hidden_size, 2 * config.moe_intermediate_size) + ) + self.down_proj = nn.Parameter(torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)) + self.act_fn = ACT2FN[config.hidden_act] + + self.num_experts = config.num_experts + self.moe_intermediate_size = config.moe_intermediate_size + self.hidden_size = config.hidden_size + self.top_k = config.num_experts_per_tok + + def forward( + self, + hidden_states: torch.Tensor, # [num_tokens,hidden_size] + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + num_tokens_per_expert: torch.Tensor, # [num_experts] + ) -> torch.Tensor: # [num_tokens,hidden_size] + """ + This module obtains the output of the experts by routing the tokens + to the experts and then performing a weighted sum of the output of the experts. + + Args: + hidden_states (torch.Tensor): (batch_size * seq_len, hidden_size) + topk_scores (torch.Tensor): (batch_size * seq_len, top_k) + expert_indices (torch.Tensor): (batch_size * seq_len, top_k) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + num_tokens, dim = hidden_states.shape + topk_scores_sorted, token_indices_sorted = self._reorder_tokens( + topk_scores, + expert_indices, + ) + # topk_scores_sorted: [num_tokens*top_k] + # token_indices_sorted: [num_tokens*top_k] + + # Build padded permutation indices + num_experts = num_tokens_per_expert.shape[0] + alignment = TOKEN_GROUP_ALIGN_SIZE_M + padded_size = num_tokens * self.top_k + num_experts * alignment + padded_size = ((padded_size + alignment - 1) // alignment) * alignment + + permuted_indices, padded_num_tokens_per_expert = _generate_permute_indices( + num_tokens_per_expert, + num_experts, + padded_size, + alignment, + ) + + # Compose: permuted_indices indexes into sorted order, + # token_indices_sorted maps sorted→original. Compose them: + sentinel = torch.tensor([num_tokens], device=hidden_states.device) # for padding slots + token_indices_ext = torch.cat([token_indices_sorted, sentinel]) + combined_indices = token_indices_ext[permuted_indices.long()] + combined_indices = combined_indices.unsqueeze(-1).expand(-1, dim) + + # Pad scores with a zero sentinel so padding slots contribute nothing + scores_ext = torch.cat([topk_scores_sorted, topk_scores_sorted.new_zeros(1)]) + combined_scores = scores_ext[permuted_indices.long()] # [padded_len] + + # Single gather (with a zero-padded sentinel row) + input_padded = torch.cat([hidden_states, hidden_states.new_zeros(1, dim)]) + routed_input = input_padded.gather(dim=0, index=combined_indices) + + # Run experts + routed_output = _run_experts_grouped_mm( + self.gate_up_proj, + self.down_proj, + self.act_fn, + routed_input, + padded_num_tokens_per_expert, + combined_scores, + ) + + output_padded = torch.zeros_like(input_padded) + output_padded.scatter_add_(dim=0, index=combined_indices, src=routed_output) + return output_padded[:-1] + + def _reorder_tokens( + self, + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + ) -> tuple[torch.Tensor, torch.Tensor]: + """Reorder tokens into expert-grouped order via argsort. + + Returns: + topk_scores_sorted: [num_tokens*top_k] scores in expert-grouped order. + token_indices_sorted: [num_tokens*top_k] original token indices in + expert-grouped order. + """ + token_indices_sorted = torch.argsort(expert_indices.view(-1), stable=True) # [num_tokens*top_k] + topk_scores_sorted = topk_scores.view(-1)[token_indices_sorted] # [num_tokens*top_k] + token_indices_sorted = token_indices_sorted // self.top_k # [num_tokens*top_k] + return topk_scores_sorted, token_indices_sorted + + def init_weights(self, buffer_device: torch.device): + nn.init.normal_(self.gate_up_proj, mean=0.0, std=0.02) + nn.init.normal_(self.down_proj, mean=0.0, std=0.02) + + +class Qwen3VLMoeTextExpertsNaive(nn.Module): + def __init__(self, config): + super().__init__() + self.gate_up_proj = nn.Parameter( + torch.empty(config.num_experts, config.hidden_size, 2 * config.moe_intermediate_size) + ) + self.down_proj = nn.Parameter(torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)) + self.act_fn = ACT2FN[config.hidden_act] + + self.num_experts = config.num_experts + self.moe_intermediate_size = config.moe_intermediate_size + self.hidden_size = config.hidden_size + + def forward( + self, + hidden_states: torch.Tensor, # [num_tokens,hidden_size] + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + num_tokens_per_expert: torch.Tensor, # [num_experts] + ) -> torch.Tensor: # [num_tokens,hidden_size] + """ + When training it is more efficient to just loop over the experts and compute the output for each expert + as otherwise the memory would explode. + + For inference we can sacrifice some memory and compute the output for all experts at once. By repeating the inputs. + + Args: + hidden_states (torch.Tensor): (batch_size * token_num, hidden_size) + routing_weights (torch.Tensor): (batch_size * token_num, top_k) + expert_indices (torch.Tensor): (batch_size * token_num, top_k) + num_tokens_per_expert (torch.Tensor): (num_experts,) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + del num_tokens_per_expert + assert hidden_states.ndim == 2, "hidden_states must be of shape (batch_size * seq_len, hidden_size)" + assert hidden_states.shape[1] == self.hidden_size, ( + "hidden_states must be of shape (batch_size * seq_len, hidden_size)" + ) + routing_weights = torch.zeros( + hidden_states.shape[0], + self.num_experts, + dtype=hidden_states.dtype, + device=hidden_states.device, + ) # [num_tokens,num_experts] + routing_weights = routing_weights.scatter_(1, expert_indices, topk_scores) # [num_tokens,num_experts] + + if self.training: + next_states = torch.zeros_like(hidden_states) # [num_tokens,hidden_size] + with torch.no_grad(): + expert_mask = torch.nn.functional.one_hot( + expert_indices, num_classes=self.num_experts + ) # [num_tokens,top_k,num_experts] + expert_mask = expert_mask.permute(2, 1, 0) # [num_experts,top_k,num_tokens] + # we sum on the top_k and on the sequence length to get which experts + # are hit this time around + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + for expert_idx in expert_hit[:]: + with torch.no_grad(): + _, token_idx = torch.where(expert_mask[expert_idx[0]]) + current_state = hidden_states[token_idx] # [num_expert_tokens,hidden_size] + gate_up = current_state @ self.gate_up_proj[expert_idx] # [num_expert_tokens,2*moe_intermediate_size] + gate, up = gate_up.chunk(2, dim=-1) # 2x [num_expert_tokens,moe_intermediate_size] + gated_output = up * self.act_fn(gate) # [num_expert_tokens,moe_intermediate_size] + out = gated_output @ self.down_proj[expert_idx] # [num_expert_tokens,hidden_size] + weighted_output = out[0] * routing_weights[token_idx, expert_idx, None] + assert weighted_output.dtype == hidden_states.dtype + next_states.index_add_(0, token_idx, weighted_output) + else: + hidden_states = hidden_states.repeat(self.num_experts, 1) # [num_experts*num_tokens,hidden_size] + hidden_states = hidden_states.view( + self.num_experts, -1, self.hidden_size + ) # [num_experts,num_tokens,hidden_size] + gate_up = torch.bmm(hidden_states, self.gate_up_proj) # [num_experts,num_tokens,2*moe_intermediate_size] + gate, up = gate_up.chunk( + 2, dim=-1 + ) # not supported for DTensors # 2x [num_experts,num_tokens,moe_intermediate_size] + next_states = torch.bmm((up * self.act_fn(gate)), self.down_proj) # [num_experts,num_tokens,hidden_size] + next_states = next_states * routing_weights.transpose(0, 1).unsqueeze( + dim=-1 + ) # [num_experts,num_tokens,hidden_size] + next_states = next_states.sum(dim=0) # [num_tokens,hidden_size] + return next_states + + def init_weights(self, buffer_device: torch.device): + nn.init.normal_(self.gate_up_proj, mean=0.0, std=0.02) + nn.init.normal_(self.down_proj, mean=0.0, std=0.02) + + +def create_text_experts(config: Qwen3VLMoeTextConfig, implementation_type: str = "naive") -> nn.Module: + if implementation_type == "naive": + return Qwen3VLMoeTextExpertsNaive(config) + elif implementation_type == "grouped_mm": + return Qwen3VLMoeTextExpertsGroupedMm(config) + else: + raise ValueError(f"Invalid implementation: {implementation_type}") diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py new file mode 100644 index 00000000..ff36b535 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py @@ -0,0 +1,455 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Speed benchmark for Qwen3VLMoeTextExpertsGroupedMm. + +Usage: + # Default benchmark (forward only, compiled, bf16): + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench + + # Forward + backward: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --backward + + # Compare grouped_mm vs naive: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --compare + + # Disable torch.compile: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --no-compile + + # Custom sweep: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --backward \ + --hidden-size 4096 \ + --moe-intermediate-size 1536 + + # Capture a torch profiler trace (Chrome trace JSON): + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --profile + + # Profile to a custom directory: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --profile --profile-dir ./my_traces + + # All options: + python -m cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe_bench --help +""" + +import itertools +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal + +import torch +import tyro + +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeTextConfig, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe import ( + Qwen3VLMoeTextExpertsGroupedMm, + Qwen3VLMoeTextExpertsNaive, +) + + +@dataclass +class BenchConfig: + """Benchmark Qwen3VLMoeTextExpertsGroupedMm.""" + + num_tokens: list[int] = field(default_factory=lambda: [16384, 32768]) + num_experts: list[int] = field(default_factory=lambda: [128]) + top_k: list[int] = field(default_factory=lambda: [8]) + hidden_size: list[int] = field(default_factory=lambda: [2048]) + moe_intermediate_size: list[int] = field(default_factory=lambda: [768]) + num_warmup: int = 10 + num_iters: int = 100 + backward: bool = False + """Also benchmark backward pass.""" + compare: bool = False + """Compare grouped_mm vs naive.""" + compile: bool = True + """Wrap module with torch.compile before benchmarking.""" + profile: bool = False + """Capture a torch profiler trace after benchmarking.""" + profile_dir: str = "./profiles" + """Directory to write Chrome trace JSON files.""" + dtype: Literal["bf16", "fp32"] = "bf16" + + +@dataclass +class BenchResult: + num_tokens: int + num_experts: int + top_k: int + hidden_size: int + moe_intermediate_size: int + fwd_ms: float + bwd_ms: float + peak_mem_mb: float + + +def _make_inputs( + num_tokens: int, + config: Qwen3VLMoeTextConfig, + device: torch.device, + dtype: torch.dtype, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + hidden_states = torch.randn(num_tokens, config.hidden_size, device=device, dtype=dtype) + + expert_indices = torch.stack( + [torch.randperm(config.num_experts, device=device)[: config.num_experts_per_tok] for _ in range(num_tokens)] + ).to(torch.int64) + + topk_scores = torch.rand(num_tokens, config.num_experts_per_tok, device=device, dtype=dtype) + topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True) + + num_tokens_per_expert = torch.zeros(config.num_experts, dtype=torch.int32, device=device) + for idx in expert_indices.view(-1): + num_tokens_per_expert[idx] += 1 + + return hidden_states, topk_scores, expert_indices, num_tokens_per_expert + + +def bench_forward( + module: torch.nn.Module, + hidden_states: torch.Tensor, + topk_scores: torch.Tensor, + expert_indices: torch.Tensor, + num_tokens_per_expert: torch.Tensor, + num_warmup: int = 20, + num_iters: int = 100, +) -> float: + for _ in range(num_warmup): + with torch.no_grad(): + module(hidden_states, topk_scores, expert_indices, num_tokens_per_expert) + torch.cuda.synchronize() + + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + + start.record() + for _ in range(num_iters): + with torch.no_grad(): + module(hidden_states, topk_scores, expert_indices, num_tokens_per_expert) + end.record() + torch.cuda.synchronize() + + return start.elapsed_time(end) / num_iters + + +def bench_backward( + module: torch.nn.Module, + hidden_states: torch.Tensor, + topk_scores: torch.Tensor, + expert_indices: torch.Tensor, + num_tokens_per_expert: torch.Tensor, + num_warmup: int = 20, + num_iters: int = 100, +) -> float: + for _ in range(num_warmup): + h = hidden_states.detach().requires_grad_(True) + out = module(h, topk_scores, expert_indices, num_tokens_per_expert) + out.sum().backward() + torch.cuda.synchronize() + + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + + start.record() + for _ in range(num_iters): + h = hidden_states.detach().requires_grad_(True) + out = module(h, topk_scores, expert_indices, num_tokens_per_expert) + out.sum().backward() + end.record() + torch.cuda.synchronize() + + return start.elapsed_time(end) / num_iters + + +def profile_run( + module: torch.nn.Module, + hidden_states: torch.Tensor, + topk_scores: torch.Tensor, + expert_indices: torch.Tensor, + num_tokens_per_expert: torch.Tensor, + output_path: str, + include_backward: bool = False, + num_warmup: int = 5, + num_active: int = 3, +) -> None: + """Run a few iterations under the torch profiler and export a Chrome trace.""" + + def _step() -> None: + if include_backward: + h = hidden_states.detach().requires_grad_(True) + out = module(h, topk_scores, expert_indices, num_tokens_per_expert) + out.sum().backward() + else: + with torch.no_grad(): + module(hidden_states, topk_scores, expert_indices, num_tokens_per_expert) + + for _ in range(num_warmup): + _step() + torch.cuda.synchronize() + + with torch.profiler.profile( + activities=[ + torch.profiler.ProfilerActivity.CPU, + torch.profiler.ProfilerActivity.CUDA, + ], + record_shapes=True, + with_stack=True, + ) as prof: + for _ in range(num_active): + _step() + torch.cuda.synchronize() + + prof.export_chrome_trace(output_path) + print(f"\nProfile trace saved to {output_path}") + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=25)) + + +def run_single( + num_tokens: int, + num_experts: int, + top_k: int, + hidden_size: int, + moe_intermediate_size: int, + include_backward: bool, + num_warmup: int, + num_iters: int, + use_compile: bool, + dtype: torch.dtype = torch.bfloat16, + trace_path: str | None = None, +) -> BenchResult: + device = torch.device("cuda") + config = Qwen3VLMoeTextConfig( + hidden_size=hidden_size, + moe_intermediate_size=moe_intermediate_size, + num_experts=num_experts, + num_experts_per_tok=top_k, + hidden_act="silu", + ) + module = Qwen3VLMoeTextExpertsGroupedMm(config).to(device=device, dtype=dtype) + module.init_weights(device) + if use_compile: + module = torch.compile(module, fullgraph=True, dynamic=True) + + hidden_states, topk_scores, expert_indices, num_tokens_per_expert = _make_inputs(num_tokens, config, device, dtype) + + torch.cuda.reset_peak_memory_stats(device) + + fwd_ms = bench_forward( + module, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + num_warmup=num_warmup, + num_iters=num_iters, + ) + + bwd_ms = 0.0 + if include_backward: + bwd_ms = bench_backward( + module, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + num_warmup=num_warmup, + num_iters=num_iters, + ) + + peak_mem_mb = torch.cuda.max_memory_allocated(device) / (1024 * 1024) + + if trace_path is not None: + profile_run( + module, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + output_path=trace_path, + include_backward=include_backward, + ) + + return BenchResult( + num_tokens=num_tokens, + num_experts=num_experts, + top_k=top_k, + hidden_size=hidden_size, + moe_intermediate_size=moe_intermediate_size, + fwd_ms=fwd_ms, + bwd_ms=bwd_ms, + peak_mem_mb=peak_mem_mb, + ) + + +def run_comparison( + num_tokens: int, + config: Qwen3VLMoeTextConfig, + num_warmup: int, + num_iters: int, + use_compile: bool, + dtype: torch.dtype = torch.bfloat16, + trace_dir: str | None = None, +) -> None: + """Run grouped_mm vs naive side-by-side and report speedup.""" + device = torch.device("cuda") + + naive = Qwen3VLMoeTextExpertsNaive(config).to(device=device, dtype=dtype) + grouped = Qwen3VLMoeTextExpertsGroupedMm(config).to(device=device, dtype=dtype) + naive.init_weights(device) + grouped.load_state_dict(naive.state_dict()) + if use_compile: + grouped = torch.compile(grouped, fullgraph=True, dynamic=False) + + hidden_states, topk_scores, expert_indices, num_tokens_per_expert = _make_inputs(num_tokens, config, device, dtype) + + naive_ms = bench_forward( + naive, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + num_warmup=num_warmup, + num_iters=num_iters, + ) + grouped_ms = bench_forward( + grouped, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + num_warmup=num_warmup, + num_iters=num_iters, + ) + + with torch.no_grad(): + out_naive = naive(hidden_states, topk_scores, expert_indices, num_tokens_per_expert) + out_grouped = grouped(hidden_states, topk_scores, expert_indices, num_tokens_per_expert) + rel_err = (out_naive - out_grouped).norm() / out_naive.norm() + + print(f" naive: {naive_ms:8.3f} ms") + print(f" grouped: {grouped_ms:8.3f} ms") + print(f" speedup: {naive_ms / grouped_ms:8.2f}x") + print(f" rel error: {rel_err.item():.6e}") + + if trace_dir is not None: + tag = ( + f"T{num_tokens}_E{config.num_experts}_K{config.num_experts_per_tok}" + f"_H{config.hidden_size}_I{config.moe_intermediate_size}" + ) + for name, mod in [("naive", naive), ("grouped", grouped)]: + path = os.path.join(trace_dir, f"compare_{name}_{tag}.json") + profile_run( + mod, + hidden_states, + topk_scores, + expert_indices, + num_tokens_per_expert, + output_path=path, + ) + + +def main(args: BenchConfig) -> None: + dtype_map = {"bf16": torch.bfloat16, "fp32": torch.float32} + dtype = dtype_map[args.dtype] + + profile_dir: str | None = None + if args.profile: + profile_dir = args.profile_dir + Path(profile_dir).mkdir(parents=True, exist_ok=True) + + gpu_name = torch.cuda.get_device_name(0) + print(f"GPU: {gpu_name}") + print(f"dtype: {args.dtype}, compile: {args.compile}") + print(f"warmup: {args.num_warmup}, iters: {args.num_iters}") + if profile_dir: + print(f"profile dir: {profile_dir}") + print() + + if args.compare: + for num_tokens, num_experts, top_k, hidden_size, moe_intermediate_size in itertools.product( + args.num_tokens, + args.num_experts, + args.top_k, + args.hidden_size, + args.moe_intermediate_size, + ): + config = Qwen3VLMoeTextConfig( + hidden_size=hidden_size, + moe_intermediate_size=moe_intermediate_size, + num_experts=num_experts, + num_experts_per_tok=top_k, + hidden_act="silu", + ) + header = ( + f"tokens={num_tokens} experts={num_experts} top_k={top_k} " + f"hidden={hidden_size} intermediate={moe_intermediate_size}" + ) + print(header) + run_comparison( + num_tokens=num_tokens, + config=config, + num_warmup=args.num_warmup, + num_iters=args.num_iters, + use_compile=args.compile, + dtype=dtype, + trace_dir=profile_dir, + ) + print() + return + + header = ( + f"{'tokens':>8} {'experts':>7} {'top_k':>5} {'hidden':>6} " + f"{'interm':>6} {'fwd_ms':>8} {'bwd_ms':>8} {'peak_MB':>9}" + ) + print(header) + print("-" * len(header)) + + for num_tokens, num_experts, top_k, hidden_size, moe_intermediate_size in itertools.product( + args.num_tokens, + args.num_experts, + args.top_k, + args.hidden_size, + args.moe_intermediate_size, + ): + trace_path = None + if profile_dir is not None: + mode = "fwd_bwd" if args.backward else "fwd" + tag = f"T{num_tokens}_E{num_experts}_K{top_k}_H{hidden_size}_I{moe_intermediate_size}" + trace_path = os.path.join(profile_dir, f"{mode}_{tag}.json") + result = run_single( + num_tokens=num_tokens, + num_experts=num_experts, + top_k=top_k, + hidden_size=hidden_size, + moe_intermediate_size=moe_intermediate_size, + include_backward=args.backward, + num_warmup=args.num_warmup, + num_iters=args.num_iters, + use_compile=args.compile, + dtype=dtype, + trace_path=trace_path, + ) + bwd_str = f"{result.bwd_ms:8.3f}" if args.backward else " N/A" + print( + f"{result.num_tokens:>8} {result.num_experts:>7} {result.top_k:>5} " + f"{result.hidden_size:>6} {result.moe_intermediate_size:>6} " + f"{result.fwd_ms:>8.3f} {bwd_str} {result.peak_mem_mb:>9.1f}" + ) + + +if __name__ == "__main__": + main(tyro.cli(BenchConfig)) diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py new file mode 100644 index 00000000..98841b35 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py @@ -0,0 +1,216 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Literal + +import torch +import triton +import triton.language as tl + +# Set the token group alignment size for experts in MoE. This is implemented by +# padding each expert size to the next multiple of TOKEN_GROUP_ALIGN_SIZE_M. + +# Valid values are: 8, 16, or 32. +# Different values are needed for different cases: + +# * For bf16, 8 is enough (16 byte alignment / 2 bytes per elem = 8 elements). +# * For fp8, 16 byte alignment / 1 byte per elem = 16 elements. +# * For mxfp8, we need 32 (or block_size) because scaling block size is (1 x 32), +# so when doing per-token-group quantization on each logically distinct subtensor, +# we need to ensure the contracting dim is divisible by block_size. +# In the backward pass, grad_weight = (grad_output_t @ input).t() has gemm dims +# of (N, M) @ (M, K) so M is the contracting dim, and group offsets are along M, +# so we need 32 element alignment. +TOKEN_GROUP_ALIGN_SIZE_M = 16 +ValidTokenGroupAlignmentSize = Literal[8, 16, 32] + + +def _permute( + x: torch.Tensor, + num_tokens_per_expert: int, + num_experts: int, + alignment: int = TOKEN_GROUP_ALIGN_SIZE_M, +): + x_padded_size = x.shape[0] + num_experts * alignment + padded_max_len = ((x_padded_size + alignment - 1) // alignment) * alignment + + with torch.no_grad(): + ( + permuted_indices, + padded_num_tokens_per_expert, + ) = _generate_permute_indices( + num_tokens_per_expert=num_tokens_per_expert, + num_experts=num_experts, + max_len=padded_max_len, + alignment=alignment, + ) + + x = torch.vstack((x, x.new_zeros(x.shape[-1]))) + input_shape = x.shape + x = x[permuted_indices, :] + + return input_shape, x, permuted_indices, padded_num_tokens_per_expert + + +def _unpermute(out, input_shape, permuted_indices): + out_unpermuted = out.new_empty(input_shape) + out_unpermuted[permuted_indices, :] = out + return out_unpermuted[:-1] + + +def indices_padding_wrapper(func: Callable) -> Callable: + """ + In order to use torch._grouped_mm, we need to make sure the number of + tokens each expert gets is a multiple of TOKEN_GROUP_ALIGN_SIZE_M. The + generate_permute_indices kernel also helps achieve this via padding, + without incurring synchronization between device and host. + """ + + def wrapper( + gate_up_proj: torch.Tensor, + down_proj: torch.Tensor, + act_fn: Callable[[torch.Tensor], torch.Tensor], + x: torch.Tensor, + num_tokens_per_expert: torch.Tensor, + ) -> torch.Tensor: + num_experts = num_tokens_per_expert.shape[0] + + input_shape, x, permuted_indices, padded_num_tokens_per_expert = _permute(x, num_tokens_per_expert, num_experts) + + out = func(gate_up_proj, down_proj, act_fn, x, padded_num_tokens_per_expert) + + out = _unpermute(out, input_shape, permuted_indices) + return out + + return wrapper + + +@triton.jit +def _fill_indices_kernel( + num_tokens_per_expert_ptr, + start_index_values_ptr, + write_offsets_ptr, + output_ptr, + num_experts: tl.constexpr, + BLOCK_SIZE: tl.constexpr, # Number of threads per block +): + pid = tl.program_id(axis=0) + num_programs = tl.num_programs(axis=0) + + # map programs (blocks) to the experts and loop (grid stride) if needed + for expert_id in range(pid, num_experts, num_programs): + # read this experts write offset + write_offset = tl.load(write_offsets_ptr + expert_id) + + # load number of tokens for this expert + start_index = tl.load(start_index_values_ptr + expert_id) + length = tl.load(num_tokens_per_expert_ptr + expert_id) + + # each thread in block processes tokens in parallel + offsets = tl.arange(0, BLOCK_SIZE) + + # tokens are processed in chunks of BLOCK_SIZE + for chunk_start in range(0, length, BLOCK_SIZE): + chunk_offsets = chunk_start + offsets + + # mask valid indices + mask = chunk_offsets < length + + values = start_index + chunk_offsets + + # destination + dest_indices = write_offset + chunk_offsets + + # store + tl.store(output_ptr + dest_indices, values, mask=mask) + + +def _fill_indices_wrapper( + num_tokens_per_expert: torch.Tensor, + start_index_values: torch.Tensor, + write_offsets: torch.Tensor, + num_experts: int, + max_len: int, + block_size: int = 128, + max_blocks: int = 1024, # cap on total number of blocks to launch +): + # preallocate output + permuted_indices = torch.full((max_len,), -1, dtype=torch.int32, device=num_tokens_per_expert.device) + + # write offsets is per local expert... + num_blocks = min(num_experts, max_blocks) + # grid = one block per expert unless capped and then we loop... + grid = (num_blocks,) + + # launch kernel + _fill_indices_kernel[grid]( + num_tokens_per_expert, + start_index_values, + write_offsets, + permuted_indices, + num_experts, + BLOCK_SIZE=block_size, + ) + return permuted_indices + + +def _generate_permute_indices( + num_tokens_per_expert: torch.Tensor, + num_experts: int, + max_len: int, + alignment: int, +): + """ + Prepare permutation indices and the number of tokens for each expert. + + Args: + num_tokens_per_expert: number of tokens for each expert. + num_experts: number of experts. + max_len: maximum length of the output index vector. + alignment: alignment for each returned element in `m_sizes` and padding min for zero token experts. + + Returns: + permuted_indices: Tensor of indices that map original token order to the expert-grouped order. + m_sizes: aligned number of tokens for each expert (padded to alignment boundary). + m_offsets: Cumulative sum of m_sizes. The exclusive ending position for each expert's tokens. + + Explanatory details: + `tokens_per_expert_group` is of shape (num_ranks * experts_per_rank,), for example: + From: | rank 0 | rank 1 | + To: | E0 | E1 | E2 | E3 | E0 | E1 | E2 | E3 | + | 4 | 2 | 1 | 3 | 1 | 2 | 3 | 4 | + """ + start_index_values = torch.cumsum(num_tokens_per_expert, dim=0) - num_tokens_per_expert + + # pad out empty experts to alignment requirement + m_sizes = torch.clamp_min(num_tokens_per_expert, alignment) + + # align the chunk sizes (cdiv) + m_sizes = (m_sizes.to(torch.int32) + alignment - 1) // alignment * alignment + + # additional prefix sum to get write offset of each expert in permuted_indices + # write offsets is per local expert, not global + write_offsets = torch.cumsum(m_sizes, dim=0) - m_sizes + + # Select the implementation to use + permuted_indices = _fill_indices_wrapper( + num_tokens_per_expert=num_tokens_per_expert, + start_index_values=start_index_values, + write_offsets=write_offsets, + num_experts=num_experts, + max_len=max_len, + ) + + return permuted_indices, m_sizes diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py new file mode 100644 index 00000000..714eb9c9 --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py @@ -0,0 +1,2041 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from dataclasses import dataclass +from typing import Any, Callable, NamedTuple, Optional, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache, DynamicCache +from transformers.generation import GenerationMixin +from transformers.integrations import use_kernel_forward_from_hub +from transformers.masking_utils import create_causal_mask +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_layers import GradientCheckpointingLayer +from transformers.modeling_outputs import BaseModelOutputWithPast, ModelOutput +from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.processing_utils import Unpack +from transformers.utils import TransformersKwargs, is_torchdynamo_compiling +from transformers.utils.deprecation import deprecate_kwarg + +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeConfig, + Qwen3VLMoeTextConfig, + Qwen3VLMoeVisionConfig, +) +from cosmos3._src.vfm.models.vlm.qwen3_vl_moe.moe import ( + create_text_experts, +) + +# Small additive constant to prevent log(0) in router entropy computation. +ENTROPY_EPSILON = 1e-9 + + +# Avoid torch.combinations here: during FSDP/lazy init this module can be built +# under a meta-device context, and torch.combinations internally calls +# masked_select, which does not have a meta kernel. +def _make_coactivation_pairs(top_k: int, device: torch.device | str | None = None) -> torch.Tensor: + target_device = torch.device(device) if device is not None else torch.device("cpu") + if target_device.type == "meta": + target_device = torch.device("cpu") + + pairs = [(i, j) for i in range(top_k) for j in range(i + 1, top_k)] + if not pairs: + return torch.empty((0, 2), dtype=torch.long, device=target_device) + return torch.tensor(pairs, dtype=torch.long, device=target_device) + + +# We need to use namedtuple instead of dataclass because it is picklable. +class LBLMetadata(NamedTuple): + """Metadata for load balancing loss computation.""" + + # The number of tokens routed to each expert for this rank. + num_tokens_per_expert: torch.Tensor + + # The total number of tokens in the batch. + num_tokens: torch.Tensor + + # The average probability of routing to each expert for this rank. + mean_router_prob_per_expert: torch.Tensor + + +@use_kernel_forward_from_hub("RMSNorm") +class Qwen3VLMoeTextRMSNorm(nn.Module): + def __init__(self, hidden_size, eps=1e-6): + """ + Qwen3VLMoeTextRMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states): + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + def extra_repr(self): + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Qwen3VLMoeTextSparseMoeBlock(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.num_experts = config.num_experts + self.top_k = config.num_experts_per_tok + self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False) + self.experts = create_text_experts(config, implementation_type="grouped_mm") + + # ── Heatmap tracking ────────────────────────────────────────────────────── + # Token counts read and reset by ExpertHeatmap on its own schedule. + # persistent=False so these are never saved to checkpoints. + self.register_buffer( + "total_tokens_per_expert", + torch.zeros(config.num_experts, dtype=torch.int64), + persistent=False, + ) + self.register_buffer( + "total_tokens", + torch.zeros(1, dtype=torch.int64), + persistent=False, + ) + + # ── Stability tracking ─────────────────────────────────────────────────── + # Separate token-count buffers owned and reset by MoEStabilityCallback, + # so it is fully independent of ExpertHeatmap's reset cycle. + self.register_buffer( + "stability_tokens_per_expert", + torch.zeros(config.num_experts, dtype=torch.int64), + persistent=False, + ) + self.register_buffer( + "stability_total_tokens", + torch.zeros(1, dtype=torch.int64), + persistent=False, + ) + # Sum of per-token router entropy H = -sum(p_i * log p_i) across all tokens + # seen since the last reset. Divided by stability_total_tokens in the + # callback to get the mean entropy, then normalized by log(N) for [0, 1]. + # float64 to avoid precision loss when accumulating over many steps. + self.register_buffer( + "sum_token_entropy", + torch.zeros(1, dtype=torch.float64), + persistent=False, + ) + + # ── Specialization tracking ─────────────────────────────────────────────── + # N×N symmetric matrix counting how often each expert pair (i, j) appears + # together in the top-K selection for the same token. Only the upper triangle + # (i < j) is written; read and reset by MoESpecializationCallback. + self.register_buffer( + "coactivation_counts", + torch.zeros(config.num_experts, config.num_experts, dtype=torch.int64), + persistent=False, + ) + # Precomputed C(top_k, 2) slot-index pairs used by the co-activation counting + # kernel in forward(). Registered as a buffer so it moves to the correct device + # with the module; persistent=False since it's derived from config constants. + self.register_buffer( + "_coact_pairs", + _make_coactivation_pairs(config.num_experts_per_tok), + persistent=False, + ) + + def _update_moe_callback_stats( + self, + num_tokens_per_expert: torch.Tensor, + num_tokens: torch.Tensor, + routing_weights: torch.Tensor, + expert_indices: torch.Tensor, + ) -> None: + # ── Heatmap + stability buffers ────────────────────────────────────── + # Accumulate into both buffer sets so each callback can reset independently. + self.total_tokens_per_expert.add_(num_tokens_per_expert) + self.total_tokens.add_(num_tokens) + self.stability_tokens_per_expert.add_(num_tokens_per_expert) + self.stability_total_tokens.add_(num_tokens) + + # Per-token router entropy H_t = -sum_i p_i * log(p_i). + # Summed (not meaned) so the callback can normalize by any window length. + # 1e-9 prevents log(0) for near-zero probabilities. + token_entropy = -torch.sum( + routing_weights * torch.log(routing_weights + ENTROPY_EPSILON), dim=-1 + ) # [num_tokens] + self.sum_token_entropy.add_(token_entropy.sum().to(torch.float64)) + + # ── Co-activation counting ──────────────────────────────────────────── + # For every ordered pair (k1, k2) of top-K slots with k1 < k2, find the + # expert assigned to each slot and increment coactivation_counts[i, j] + # where i = min(expert_k1, expert_k2), j = max(...) to keep counts in the + # upper triangle only (avoids double-counting the pair). + # Vectorized over all C(K,2) pairs in one scatter_add_ call to avoid + # C(K,2) separate kernel launches (28 for top_k=8). + # _coact_pairs: [C(K,2), 2] — precomputed slot index pairs (k1, k2) with k1 < k2 + e1 = expert_indices[:, self._coact_pairs[:, 0]] # [num_tokens, C(K,2)] + e2 = expert_indices[:, self._coact_pairs[:, 1]] # [num_tokens, C(K,2)] + lo = torch.minimum(e1, e2) + hi = torch.maximum(e1, e2) + flat_idx = (lo * self.num_experts + hi).to(torch.int64) # [num_tokens, C(K,2)] + flat_counts = torch.zeros( + self.num_experts * self.num_experts, + dtype=self.coactivation_counts.dtype, + device=self.coactivation_counts.device, + ) + flat_idx = flat_idx.reshape(-1) + flat_counts.scatter_add_(0, flat_idx, torch.ones_like(flat_idx, dtype=flat_counts.dtype)) + self.coactivation_counts.view(-1).add_(flat_counts) + + def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, LBLMetadata]: + """ + This function performs the MoE computation, including routing, dispatch, GEMMs and combine. + + Args: + hidden_states (torch.Tensor): (num_tokens, hidden_size) + + Returns: + torch.Tensor: (num_tokens, hidden_size) + - routed_out: Output of the MoE computation. + LBLMetadata: Load balancing loss metadata. + """ + assert hidden_states.ndim == 2, "hidden_states must be of shape (num_tokens, hidden_size)" + num_tokens = hidden_states.shape[0] + + router_logits = self.gate(hidden_states) # [num_tokens,num_experts] + routing_weights = torch.nn.functional.softmax( + router_logits, dim=-1, dtype=torch.float32 + ) # [num_tokens,num_experts] + expert_weights, expert_indices = torch.topk(routing_weights, self.top_k, dim=-1) + # expert_weights: [num_tokens,top_k], expert_indices: [num_tokens,top_k] + + expert_weights = expert_weights / expert_weights.sum(dim=-1, keepdim=True) # [num_tokens,top_k] + expert_weights = expert_weights.to(hidden_states.dtype) # [num_tokens,top_k] + + num_tokens_per_expert = torch.histc( + expert_indices.to(dtype=torch.int32).view(-1), + bins=self.num_experts, + min=0, + max=self.num_experts - 1, + ) # [num_experts] + + routed_out = self.experts( + hidden_states=hidden_states, + topk_scores=expert_weights, + expert_indices=expert_indices, + num_tokens_per_expert=num_tokens_per_expert, + ) # [num_tokens,hidden_size] + + num_tokens_per_expert = num_tokens_per_expert.to(dtype=torch.int64) # [num_experts] + num_tokens = torch.tensor( + [num_tokens], + dtype=torch.int64, + device=num_tokens_per_expert.device, + ) # [1] + + # Compute the average probability of routing to these experts. + # Summing over all experts should be equal to 1. + mean_router_prob_per_expert = torch.mean(routing_weights, dim=0) # [num_experts] + + lbl_metadata = LBLMetadata( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + mean_router_prob_per_expert=mean_router_prob_per_expert, + ) + + with torch.no_grad(): + self._update_moe_callback_stats( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + routing_weights=routing_weights, + expert_indices=expert_indices, + ) + + return routed_out, lbl_metadata + + def get_total_tokens_per_expert(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + total_tokens = self.total_tokens_per_expert.detach().clone() + if reset: + self.total_tokens_per_expert.zero_() + return total_tokens + + def get_total_tokens(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + total_tokens = self.total_tokens.detach().clone() + if reset: + self.total_tokens.zero_() + return total_tokens + + def get_stability_tokens_per_expert(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.stability_tokens_per_expert.detach().clone() + if reset: + self.stability_tokens_per_expert.zero_() + return val + + def get_stability_total_tokens(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.stability_total_tokens.detach().clone() + if reset: + self.stability_total_tokens.zero_() + return val + + def get_sum_token_entropy(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.sum_token_entropy.detach().clone() + if reset: + self.sum_token_entropy.zero_() + return val + + def get_coactivation_counts(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.coactivation_counts.detach().clone() + if reset: + self.coactivation_counts.zero_() + return val + + def init_weights(self, buffer_device: torch.device | None = None): + self.register_buffer( + "total_tokens_per_expert", + torch.zeros(self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "total_tokens", + torch.zeros(1, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "stability_tokens_per_expert", + torch.zeros(self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "stability_total_tokens", + torch.zeros(1, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "sum_token_entropy", + torch.zeros(1, dtype=torch.float64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "coactivation_counts", + torch.zeros(self.num_experts, self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "_coact_pairs", + _make_coactivation_pairs(self.top_k, device=buffer_device), + persistent=False, + ) + + if hasattr(self.config, "initializer_range"): + std = self.config.initializer_range + else: + std = getattr(self.config.get_text_config(), "initializer_range", 0.02) + + nn.init.normal_(self.gate.weight, mean=0.0, std=std) + nn.init.normal_(self.experts.gate_up_proj, mean=0.0, std=std) + nn.init.normal_(self.experts.down_proj, mean=0.0, std=std) + + +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) # [B,num_kv_heads,n_rep,N,head_dim] + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) # [B,num_heads,N,head_dim] + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, # [B,num_heads,N,head_dim] + key: torch.Tensor, # [B,num_kv_heads,N,head_dim] + value: torch.Tensor, # [B,num_kv_heads,N,head_dim] + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) # [B,num_heads,N,head_dim] + value_states = repeat_kv(value, module.num_key_value_groups) # [B,num_heads,N,head_dim] + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling # [B,num_heads,N,N] + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] + attn_weights = attn_weights + causal_mask # [B,num_heads,N,N] + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) # [B,num_heads,N,N] + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value_states) # [B,num_heads,N,head_dim] + attn_output = attn_output.transpose(1, 2).contiguous() # [B,N,num_heads,head_dim] + + return attn_output, attn_weights + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + position_ids (`torch.Tensor`, *optional*): + Deprecated and unused. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + sin = sin.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [B,num_heads,N,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [B,num_kv_heads,N,head_dim] + return q_embed, k_embed + + +class Qwen3VLMoeTextAttention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, config: Qwen3VLMoeTextConfig, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = True + + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias + ) + self.q_norm = Qwen3VLMoeTextRMSNorm( + self.head_dim, eps=config.rms_norm_eps + ) # unlike olmo, only on the head dim! + self.k_norm = Qwen3VLMoeTextRMSNorm( + self.head_dim, eps=config.rms_norm_eps + ) # thus post q_norm does not need reshape + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_heads,N,head_dim] + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_kv_heads,N,head_dim] + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) # [B,num_kv_heads,N,head_dim] + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + # query_states: [B,num_heads,N,head_dim], key_states: [B,num_kv_heads,N,head_dim] + + if past_key_values is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs) + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + # attn_output: [B,N,num_heads,head_dim] + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() # [B,N,hidden_size] + attn_output = self.o_proj(attn_output) # [B,N,hidden_size] + return attn_output, attn_weights + + +class Qwen3VLMoeTextMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj + + +class Qwen3VLMoeTextDecoderLayer(GradientCheckpointingLayer): + def __init__(self, config: Qwen3VLMoeTextConfig, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + + self.self_attn = Qwen3VLMoeTextAttention(config, layer_idx) + + if (layer_idx not in config.mlp_only_layers) and ( + config.num_experts > 0 and (layer_idx + 1) % config.decoder_sparse_step == 0 + ): + self.mlp = Qwen3VLMoeTextSparseMoeBlock(config) + else: + self.mlp = Qwen3VLMoeTextMLP(config) + + self.input_layernorm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> torch.FloatTensor: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch * seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`, *optional*): attention mask of size + `(batch, sequence_length)` where padding elements are indicated by 0. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + output_router_logits (`bool`, *optional*): + Whether or not to return the logits of all the routers. They are useful for computing the router loss, + and should not be returned during inference. + use_cache (`bool`, *optional*): + If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding + (see `past_key_values`). + past_key_values (`Cache`, *optional*): cached past key and value projection states + cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*): + Indices depicting the position of the input sequence tokens in the sequence. + position_embeddings (`tuple[torch.FloatTensor, torch.FloatTensor]`, *optional*): + Tuple containing the cosine and sine positional embeddings of shape `(batch_size, seq_len, head_dim)`, + with `head_dim` being the embedding dimension of each attention head. + kwargs (`dict`, *optional*): + Arbitrary kwargs to be ignored, used for FSDP and other methods that injects code + into the model + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + residual = hidden_states + + hidden_states = self.input_layernorm(hidden_states) + + # Self Attention + hidden_states, _ = self.self_attn( + hidden_states=hidden_states, + position_embeddings=position_embeddings, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + **kwargs, + ) + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + return hidden_states + + +class Qwen3VLMoePreTrainedModel(PreTrainedModel): + config: Qwen3VLMoeConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer", "Qwen3VLMoeVisionBlock"] + _skip_keys_device_placement = ["past_key_values"] + _supports_flash_attn = True + _supports_sdpa = True + _supports_flex_attn = True + _can_compile_fullgraph = False # MoE models don't work with torch.compile (`torch.where(condition)` not supported) + _supports_attention_backend = True + _can_record_outputs = { + "hidden_states": Qwen3VLMoeTextDecoderLayer, + "attentions": Qwen3VLMoeTextAttention, + } + + def _init_weights(self, module: nn.Module, buffer_device: torch.device | None): + """Initialize the weights.""" + super()._init_weights(module) + + if isinstance(module, Qwen3VLMoeTextSparseMoeBlock): + module.init_weights(buffer_device=buffer_device) + elif isinstance(module, Qwen3VLMoeTextRotaryEmbedding): + module.init_weights(buffer_device=buffer_device) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + self.apply(functools.partial(self._init_weights, buffer_device=buffer_device)) + + +class Qwen3VLMoeVisionMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.linear_fc1 = nn.Linear(self.hidden_size, self.intermediate_size, bias=True) + self.linear_fc2 = nn.Linear(self.intermediate_size, self.hidden_size, bias=True) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, hidden_state): + return self.linear_fc2(self.act_fn(self.linear_fc1(hidden_state))) + + +class Qwen3VLMoeVisionPatchEmbed(nn.Module): + def __init__(self, config) -> None: + super().__init__() + self.patch_size = config.patch_size + self.temporal_patch_size = config.temporal_patch_size + self.in_channels = config.in_channels + self.embed_dim = config.hidden_size + + kernel_size = [self.temporal_patch_size, self.patch_size, self.patch_size] + self.proj = nn.Conv3d(self.in_channels, self.embed_dim, kernel_size=kernel_size, stride=kernel_size, bias=True) + + def forward( + self, hidden_states: torch.Tensor + ) -> torch.Tensor: # hidden_states: [N_patches,in_channels*temporal_patch_size*patch_size*patch_size] + target_dtype = self.proj.weight.dtype + hidden_states = hidden_states.view( + -1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size + ) # [N_patches,in_channels,temporal_patch_size,patch_size,patch_size] + hidden_states = self.proj(hidden_states.to(dtype=target_dtype)).view( + -1, self.embed_dim + ) # [N_patches,embed_dim] + return hidden_states + + +class Qwen3VLMoeVisionRotaryEmbedding(nn.Module): + def __init__(self, dim: int, theta: float = 10000.0) -> None: + super().__init__() + inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim)) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def forward(self, seqlen: int) -> torch.Tensor: + seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype) # [seqlen] + freqs = torch.outer(seq, self.inv_freq) # [seqlen,dim//2] + return freqs # [seqlen,dim//2] + + +class Qwen3VLMoeVisionPatchMerger(nn.Module): + def __init__(self, config: Qwen3VLMoeVisionConfig, use_postshuffle_norm=False) -> None: + super().__init__() + self.hidden_size = config.hidden_size * (config.spatial_merge_size**2) + self.use_postshuffle_norm = use_postshuffle_norm + self.norm = nn.LayerNorm(self.hidden_size if use_postshuffle_norm else config.hidden_size, eps=1e-6) + self.linear_fc1 = nn.Linear(self.hidden_size, self.hidden_size) + self.act_fn = nn.GELU() + self.linear_fc2 = nn.Linear(self.hidden_size, config.out_hidden_size) + + def forward(self, x: torch.Tensor) -> torch.Tensor: # x: [N_patches,hidden_size] (before merge) + x = self.norm(x.view(-1, self.hidden_size) if self.use_postshuffle_norm else x).view( + -1, self.hidden_size + ) # [N_merged,merged_hidden_size] + x = self.linear_fc2(self.act_fn(self.linear_fc1(x))) # [N_merged,out_hidden_size] + return x + + +def apply_rotary_pos_emb_vision( + q: torch.Tensor, # [N,num_heads,head_dim] + k: torch.Tensor, # [N,num_heads,head_dim] + cos: torch.Tensor, # [N,head_dim] + sin: torch.Tensor, # [N,head_dim] +) -> tuple[torch.Tensor, torch.Tensor]: + orig_q_dtype = q.dtype + orig_k_dtype = k.dtype + q, k = q.float(), k.float() + cos, sin = cos.unsqueeze(-2).float(), sin.unsqueeze(-2).float() # [N,1,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [N,num_heads,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [N,num_heads,head_dim] + q_embed = q_embed.to(orig_q_dtype) + k_embed = k_embed.to(orig_k_dtype) + return q_embed, k_embed + + +class Qwen3VLMoeVisionAttention(nn.Module): + def __init__(self, config: Qwen3VLMoeVisionConfig) -> None: + super().__init__() + self.dim = config.hidden_size + self.num_heads = config.num_heads + self.head_dim = self.dim // self.num_heads + self.num_key_value_groups = 1 # needed for eager attention + self.qkv = nn.Linear(self.dim, self.dim * 3, bias=True) + self.proj = nn.Linear(self.dim, self.dim) + self.scaling = self.head_dim**-0.5 + self.config = config + self.attention_dropout = 0.0 + self.is_causal = False + + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: + seq_length = hidden_states.shape[0] + query_states, key_states, value_states = ( + self.qkv(hidden_states).reshape(seq_length, 3, self.num_heads, -1).permute(1, 0, 2, 3).unbind(0) + ) + # query_states, key_states, value_states: [N,num_heads,head_dim] + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb_vision(query_states, key_states, cos, sin) + # query_states, key_states: [N,num_heads,head_dim] + + query_states = query_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + key_states = key_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + value_states = value_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + if self.config._attn_implementation == "flash_attention_2": + # Flash Attention 2: Use cu_seqlens for variable length attention + max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max() + attn_output, _ = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + cu_seq_lens_q=cu_seqlens, + cu_seq_lens_k=cu_seqlens, + max_length_q=max_seqlen, + max_length_k=max_seqlen, + is_causal=False, + **kwargs, + ) + else: + # Other implementations: Process each chunk separately + lengths = cu_seqlens[1:] - cu_seqlens[:-1] + splits = [ + torch.split(tensor, lengths.tolist(), dim=2) for tensor in (query_states, key_states, value_states) + ] + + attn_outputs = [ + attention_interface( + self, + q, + k, + v, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + is_causal=False, + **kwargs, + )[0] + for q, k, v in zip(*splits) + ] + attn_output = torch.cat(attn_outputs, dim=1) # [1,N,num_heads,head_dim] + + attn_output = attn_output.reshape(seq_length, -1).contiguous() # [N,hidden_size] + attn_output = self.proj(attn_output) # [N,hidden_size] + return attn_output + + +class Qwen3VLMoeVisionBlock(GradientCheckpointingLayer): + def __init__(self, config, attn_implementation: str = "sdpa") -> None: + super().__init__() + self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.attn = Qwen3VLMoeVisionAttention(config=config) + self.mlp = Qwen3VLMoeVisionMLP(config=config) + + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: + hidden_states = hidden_states + self.attn( + self.norm1(hidden_states), + cu_seqlens=cu_seqlens, + rotary_pos_emb=rotary_pos_emb, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = hidden_states + self.mlp(self.norm2(hidden_states)) + return hidden_states + + +class Qwen3VLMoeVisionModel(Qwen3VLMoePreTrainedModel): + config: Qwen3VLMoeVisionConfig + _no_split_modules = ["Qwen3VLMoeVisionBlock"] + + def __init__(self, config, *inputs, **kwargs) -> None: + super().__init__(config, *inputs, **kwargs) + self.spatial_merge_size = config.spatial_merge_size + self.patch_size = config.patch_size + self.spatial_merge_unit = self.spatial_merge_size * self.spatial_merge_size + + self.patch_embed = Qwen3VLMoeVisionPatchEmbed( + config=config, + ) + + self.pos_embed = nn.Embedding(config.num_position_embeddings, config.hidden_size) + self.num_grid_per_side = int(config.num_position_embeddings**0.5) + + head_dim = config.hidden_size // config.num_heads + self.rotary_pos_emb = Qwen3VLMoeVisionRotaryEmbedding(head_dim // 2) + + self.blocks = nn.ModuleList([Qwen3VLMoeVisionBlock(config) for _ in range(config.depth)]) + self.merger = Qwen3VLMoeVisionPatchMerger( + config=config, + use_postshuffle_norm=False, + ) + + self.deepstack_visual_indexes = config.deepstack_visual_indexes + self.deepstack_merger_list = nn.ModuleList( + [ + Qwen3VLMoeVisionPatchMerger( + config=config, + use_postshuffle_norm=True, + ) + for _ in range(len(config.deepstack_visual_indexes)) + ] + ) + + self.gradient_checkpointing = False + + def rot_pos_emb(self, grid_thw: torch.Tensor) -> torch.Tensor: + merge_size = self.spatial_merge_size + + max_hw = int(grid_thw[:, 1:].max().item()) + freq_table = self.rotary_pos_emb(max_hw) # [max_hw,head_dim//4] + device = freq_table.device + + total_tokens = int(torch.prod(grid_thw, dim=1).sum().item()) + pos_ids = torch.empty((total_tokens, 2), dtype=torch.long, device=device) # [total_tokens,2] + + offset = 0 + for num_frames, height, width in grid_thw: + merged_h, merged_w = height // merge_size, width // merge_size + + block_rows = torch.arange(merged_h, device=device) # block row indices + block_cols = torch.arange(merged_w, device=device) # block col indices + intra_row = torch.arange(merge_size, device=device) # intra-block row offsets + intra_col = torch.arange(merge_size, device=device) # intra-block col offsets + + # Compute full-resolution positions + row_idx = block_rows[:, None, None, None] * merge_size + intra_row[None, None, :, None] + col_idx = block_cols[None, :, None, None] * merge_size + intra_col[None, None, None, :] + + row_idx = row_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + col_idx = col_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + + coords = torch.stack((row_idx, col_idx), dim=-1) # [H*W,2] + + if num_frames > 1: + coords = coords.repeat(num_frames, 1) # [T*H*W,2] + + num_tokens = coords.shape[0] + pos_ids[offset : offset + num_tokens] = coords + offset += num_tokens + + embeddings = freq_table[pos_ids] # [total_tokens,2,head_dim//4] + embeddings = embeddings.flatten(1) # [total_tokens,head_dim//2] + return embeddings + + def fast_pos_embed_interpolate(self, grid_thw): + grid_ts, grid_hs, grid_ws = grid_thw[:, 0], grid_thw[:, 1], grid_thw[:, 2] + + idx_list = [[] for _ in range(4)] + weight_list = [[] for _ in range(4)] + + for t, h, w in zip(grid_ts, grid_hs, grid_ws): + h_idxs = torch.linspace(0, self.num_grid_per_side - 1, h) + w_idxs = torch.linspace(0, self.num_grid_per_side - 1, w) + + h_idxs_floor = h_idxs.int() + w_idxs_floor = w_idxs.int() + h_idxs_ceil = (h_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + w_idxs_ceil = (w_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + + dh = h_idxs - h_idxs_floor + dw = w_idxs - w_idxs_floor + + base_h = h_idxs_floor * self.num_grid_per_side + base_h_ceil = h_idxs_ceil * self.num_grid_per_side + + indices = [ + (base_h[None].T + w_idxs_floor[None]).flatten(), + (base_h[None].T + w_idxs_ceil[None]).flatten(), + (base_h_ceil[None].T + w_idxs_floor[None]).flatten(), + (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(), + ] + + weights = [ + ((1 - dh)[None].T * (1 - dw)[None]).flatten(), + ((1 - dh)[None].T * dw[None]).flatten(), + (dh[None].T * (1 - dw)[None]).flatten(), + (dh[None].T * dw[None]).flatten(), + ] + + for i in range(4): + idx_list[i].extend(indices[i].tolist()) + weight_list[i].extend(weights[i].tolist()) + + idx_tensor = torch.tensor(idx_list, dtype=torch.long, device=self.pos_embed.weight.device) # [4,total_patches] + weight_tensor = torch.tensor( + weight_list, dtype=self.pos_embed.weight.dtype, device=self.pos_embed.weight.device + ) # [4,total_patches] + pos_embeds = self.pos_embed(idx_tensor) * weight_tensor[:, :, None] # [4,total_patches,hidden_size] + patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3] # [total_patches,hidden_size] + + patch_pos_embeds = patch_pos_embeds.split([h * w for h, w in zip(grid_hs, grid_ws)]) + + patch_pos_embeds_permute = [] + merge_size = self.config.spatial_merge_size + for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws): + pos_embed = pos_embed.repeat(t, 1) # [T*H*W,hidden_size] + pos_embed = ( + pos_embed.view(t, h // merge_size, merge_size, w // merge_size, merge_size, -1) + # [T,H//merge,merge,W//merge,merge,hidden_size] + .permute(0, 1, 3, 2, 4, 5) + # [T,H//merge,W//merge,merge,merge,hidden_size] + .flatten(0, 4) + # [T*H//merge*W//merge*merge*merge,hidden_size] = [T*H*W,hidden_size] + ) + patch_pos_embeds_permute.append(pos_embed) + patch_pos_embeds = torch.cat(patch_pos_embeds_permute) # [total_patches,hidden_size] + return patch_pos_embeds + + def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, **kwargs) -> torch.Tensor: + """ + Args: + hidden_states (`torch.Tensor` of shape `(seq_len, hidden_size)`): + The final hidden states of the model. + grid_thw (`torch.Tensor` of shape `(num_images_or_videos, 3)`): + The temporal, height and width of feature shape of each image in LLM. + + Returns: + `torch.Tensor`: hidden_states. + """ + hidden_states = self.patch_embed(hidden_states) # [total_patches,embed_dim] + + pos_embeds = self.fast_pos_embed_interpolate(grid_thw) # [total_patches,hidden_size] + hidden_states = hidden_states + pos_embeds # [total_patches,hidden_size] + + rotary_pos_emb = self.rot_pos_emb(grid_thw) # [total_patches,head_dim//2] + + seq_len, _ = hidden_states.size() + hidden_states = hidden_states.reshape(seq_len, -1) # [total_patches,hidden_size] + rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1) # [total_patches,head_dim//2] + emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1) # [total_patches,head_dim] + position_embeddings = (emb.cos(), emb.sin()) # 2x [total_patches,head_dim] + + cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum( + dim=0, + # Select dtype based on the following factors: + # - FA2 requires that cu_seqlens_q must have dtype int32 + # - torch.onnx.export requires that cu_seqlens_q must have same dtype as grid_thw + # See https://github.com/huggingface/transformers/pull/34852 for more information + dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32, + ) + cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0) + + deepstack_feature_lists = [] + for layer_num, blk in enumerate(self.blocks): + hidden_states = blk( + hidden_states, + cu_seqlens=cu_seqlens, + position_embeddings=position_embeddings, + **kwargs, + ) + if layer_num in self.deepstack_visual_indexes: + deepstack_feature = self.deepstack_merger_list[self.deepstack_visual_indexes.index(layer_num)]( + hidden_states + ) + deepstack_feature_lists.append(deepstack_feature) + + hidden_states = self.merger(hidden_states) + + return hidden_states, deepstack_feature_lists + + +class Qwen3VLMoeTextRotaryEmbedding(nn.Module): + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__() + if hasattr(config, "rope_scaling") and config.rope_scaling is not None: + self.rope_type = config.rope_scaling.get("rope_type", "default") + else: + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + self.mrope_section = config.rope_scaling.get("mrope_section", [24, 20, 20]) + + self.config = config + self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + inv_freq, self.attention_scaling = self.rope_init_fn(self.config, buffer_device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def apply_interleaved_mrope(self, freqs, mrope_section): + """Apply interleaved MRoPE to 3D rotary embeddings. + Reorganizes frequency layout from chunked [TTT...HHH...WWW] to + interleaved [THTHWHTHW...TT], preserving frequency continuity. + args: + x: (3, bs, seq_len, head_dim // 2) + mrope_section: (3,) + returns: + x_t: (bs, seq_len, head_dim // 2) + """ + freqs_t = freqs[0] # just overwrite the first dimension T + for dim, offset in enumerate((1, 2), start=1): # H, W + length = mrope_section[dim] * 3 + idx = slice(offset, length, 3) + freqs_t[..., idx] = freqs[dim, ..., idx] + return freqs_t + + @torch.no_grad() + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward(self, x, position_ids): + assert self.inv_freq.dtype == torch.float32, f"inv_freq must be float32, but got {self.inv_freq.dtype}" + + # In contrast to other models, Qwen3VLMoe has different position ids for the grids + # So we expand the inv_freq to shape (3, ...) + if position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) # [3,B,N] + inv_freq_expanded = ( + self.inv_freq[None, None, :, None].float().expand(3, position_ids.shape[1], -1, 1) + ) # [3,B,head_dim//2,1] + position_ids_expanded = position_ids[:, :, None, :].float() # [3,B,1,N] + + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3) # [3,B,N,head_dim//2] + freqs = self.apply_interleaved_mrope(freqs, self.mrope_section) # [B,N,head_dim//2] + emb = torch.cat((freqs, freqs), dim=-1) # [B,N,head_dim] + cos = emb.cos() * self.attention_scaling # [B,N,head_dim] + sin = emb.sin() * self.attention_scaling # [B,N,head_dim] + + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + +class Qwen3VLMoeTextModel(Qwen3VLMoePreTrainedModel): + config: Qwen3VLMoeTextConfig + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer"] + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + self.layers = nn.ModuleList( + [Qwen3VLMoeTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] + ) + self.norm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3VLMoeTextRotaryEmbedding(config=config) + self.gradient_checkpointing = False + + # Initialize weights and apply final processing + self.post_init() + + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + use_cache: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + # args for deepstack + visual_pos_masks: Optional[torch.Tensor] = None, + deepstack_visual_embeds: Optional[list[torch.Tensor]] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Union[tuple, BaseModelOutputWithPast]: + r""" + visual_pos_masks (`torch.Tensor` of shape `(batch_size, seqlen)`, *optional*): + The mask of the visual positions. + deepstack_visual_embeds (`list[torch.Tensor]`, *optional*): + The deepstack visual embeddings. The shape is (num_layers, visual_seqlen, embed_dim). + The feature is extracted from the different visual encoder layers, and fed to the decoder + hidden states. It's from the paper DeepStack(https://arxiv.org/abs/2406.04334). + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + # torch.jit.trace() doesn't support cache objects in the output + if use_cache and past_key_values is None and not torch.jit.is_tracing(): + past_key_values = DynamicCache(config=self.config) + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) + + if cache_position is None: + past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0 + cache_position = torch.arange( + past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device + ) + + # the hard coded `3` is for temporal, height and width. + if position_ids is None: + position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1) + elif position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) + + if position_ids.ndim == 3 and position_ids.shape[0] == 4: + text_position_ids = position_ids[0] + position_ids = position_ids[1:] + else: + text_position_ids = position_ids[0] + + attention_mask = create_causal_mask( + config=self.config, + input_embeds=inputs_embeds, + attention_mask=attention_mask, + cache_position=cache_position, + past_key_values=past_key_values, + position_ids=text_position_ids, + ) + + hidden_states = inputs_embeds + + # create position embeddings to be shared across the decoder layers + position_embeddings = self.rotary_emb(hidden_states, position_ids) + + # decoder layers + for layer_idx, decoder_layer in enumerate(self.layers): + layer_outputs = decoder_layer( + hidden_states, + attention_mask=attention_mask, + position_ids=text_position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = layer_outputs + + # add visual features to the hidden states of first several layers + if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)): + hidden_states = self._deepstack_process( + hidden_states, + visual_pos_masks, + deepstack_visual_embeds[layer_idx], + ) + + hidden_states = self.norm(hidden_states) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values, + ) + + def _deepstack_process( + self, hidden_states: torch.Tensor, visual_pos_masks: torch.Tensor, visual_embeds: torch.Tensor + ): + visual_pos_masks = visual_pos_masks.to(hidden_states.device) + visual_embeds = visual_embeds.to(hidden_states.device, hidden_states.dtype) + local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds + hidden_states[visual_pos_masks, :] = local_this + return hidden_states + + +@dataclass +class Qwen3VLMoeCausalLMOutputWithPast(ModelOutput): + r""" + loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided): + Language modeling loss (for next-token prediction). + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.vocab_size)`): + Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax). + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + loss: Optional[torch.FloatTensor] = None + logits: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + aux_loss: Optional[torch.FloatTensor] = None + + +@dataclass +class Qwen3VLMoeModelOutputWithPast(ModelOutput): + r""" + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + last_hidden_state: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + + +class Qwen3VLMoeModel(Qwen3VLMoePreTrainedModel): + base_model_prefix = "" + _checkpoint_conversion_mapping = {} + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLMoeConfig + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer", "Qwen3VLMoeVisionBlock"] + + def __init__(self, config): + super().__init__(config) + self.visual = Qwen3VLMoeVisionModel._from_config(config.vision_config) + self.language_model = Qwen3VLMoeTextModel._from_config(config.text_config) + self.rope_deltas = None # cache rope_deltas here + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.language_model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.language_model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.language_model = decoder + + def get_decoder(self): + return self.language_model + + def get_rope_index( + self, + input_ids: Optional[torch.LongTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Different from the original implementation, Qwen3VLMoe use timestamps rather than absolute time position ids.""" + + # Since we use timestamps to seperate videos, like , the video_grid_thw should also be split + if video_grid_thw is not None: + video_grid_thw = torch.repeat_interleave(video_grid_thw, video_grid_thw[:, 0], dim=0) + video_grid_thw[:, 0] = 1 + + spatial_merge_size = self.config.vision_config.spatial_merge_size + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + mrope_position_deltas = [] + if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None): + total_input_ids = input_ids + if attention_mask is None: + attention_mask = torch.ones_like(total_input_ids) + position_ids = torch.ones( + 3, + input_ids.shape[0], + input_ids.shape[1], + dtype=input_ids.dtype, + device=input_ids.device, + ) # [3,B,N] + image_index, video_index = 0, 0 + attention_mask = attention_mask.to(total_input_ids.device) + for i, input_ids in enumerate(total_input_ids): + input_ids = input_ids[attention_mask[i] == 1] + image_nums, video_nums = 0, 0 + vision_start_indices = torch.argwhere(input_ids == vision_start_token_id).squeeze(1) + vision_tokens = input_ids[vision_start_indices + 1] + image_nums = (vision_tokens == image_token_id).sum() + video_nums = (vision_tokens == video_token_id).sum() + input_tokens = input_ids.tolist() + llm_pos_ids_list: list = [] + st = 0 + remain_images, remain_videos = image_nums, video_nums + for _ in range(image_nums + video_nums): + if image_token_id in input_tokens and remain_images > 0: + ed_image = input_tokens.index(image_token_id, st) + else: + ed_image = len(input_tokens) + 1 + if video_token_id in input_tokens and remain_videos > 0: + ed_video = input_tokens.index(video_token_id, st) + else: + ed_video = len(input_tokens) + 1 + if ed_image < ed_video: + t, h, w = ( + image_grid_thw[image_index][0], + image_grid_thw[image_index][1], + image_grid_thw[image_index][2], + ) + image_index += 1 + remain_images -= 1 + ed = ed_image + + else: + t, h, w = ( + video_grid_thw[video_index][0], + video_grid_thw[video_index][1], + video_grid_thw[video_index][2], + ) + video_index += 1 + remain_videos -= 1 + ed = ed_video + llm_grid_t, llm_grid_h, llm_grid_w = ( + t.item(), + h.item() // spatial_merge_size, + w.item() // spatial_merge_size, + ) + text_len = ed - st + + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) # [3,text_len] + + # t_index is always 0 because llm_grid_t is always 1 (we use timestamps to encode the temporal information for videos) + t_index = ( + torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten() + ) # [T*H*W] + h_index = ( + torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten() + ) # [T*H*W] + w_index = ( + torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten() + ) # [T*H*W] + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx) # [3,T*H*W] + st = ed + llm_grid_t * llm_grid_h * llm_grid_w + + if st < len(input_tokens): + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + text_len = len(input_tokens) - st + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) # [3,N] + position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device) + mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i])) + mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1) # [B,1] + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + else: + if attention_mask is not None: + position_ids = attention_mask.long().cumsum(-1) - 1 # [B,N] + position_ids.masked_fill_(attention_mask == 0, 1) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device) # [3,B,N] + max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0] # [B,1] + mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1] # [B,1] + else: + position_ids = ( + torch.arange(input_ids.shape[1], device=input_ids.device) + .view(1, 1, -1) + .expand(3, input_ids.shape[0], -1) + ) # [3,B,N] + mrope_position_deltas = torch.zeros( + [input_ids.shape[0], 1], + device=input_ids.device, + dtype=input_ids.dtype, + ) # [B,1] + + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + """ + Encodes videos into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values_videos (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input videos. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + # Same implementation as for images + return self.get_image_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + """ + Encodes images into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input images. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + """ + pixel_values = pixel_values.type(self.visual.dtype) + image_embeds, deepstack_image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) + split_sizes = (image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist() + image_embeds = torch.split(image_embeds, split_sizes) + return image_embeds, deepstack_image_embeds + + def get_placeholder_mask( + self, + input_ids: torch.LongTensor, + inputs_embeds: torch.FloatTensor, + image_features: Optional[torch.FloatTensor] = None, + video_features: Optional[torch.FloatTensor] = None, + ): + """ + Obtains multimodal placeholder mask from `input_ids` or `inputs_embeds`, and checks that the placeholder token count is + equal to the length of multimodal features. If the lengths are different, an error is raised. + """ + if input_ids is None: + special_image_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_image_mask = special_image_mask.all(-1) + special_video_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_video_mask = special_video_mask.all(-1) + else: + special_image_mask = input_ids == self.config.image_token_id + special_video_mask = input_ids == self.config.video_token_id + + n_image_tokens = special_image_mask.sum() + special_image_mask = special_image_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + if image_features is not None and inputs_embeds[special_image_mask].numel() != image_features.numel(): + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {image_features.shape[0]}" + ) + + n_video_tokens = special_video_mask.sum() + special_video_mask = special_video_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + if video_features is not None and inputs_embeds[special_video_mask].numel() != video_features.numel(): + raise ValueError( + f"Videos features and video tokens do not match: tokens: {n_video_tokens}, features {video_features.shape[0]}" + ) + + return special_image_mask, special_video_mask + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLMoeModelOutputWithPast]: + r""" + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + image_mask = None + video_mask = None + + if pixel_values is not None: + image_embeds, deepstack_image_embeds = self.get_image_features(pixel_values, image_grid_thw) + image_embeds = torch.cat(image_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype) + image_mask, _ = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) + + if pixel_values_videos is not None: + video_embeds, deepstack_video_embeds = self.get_video_features(pixel_values_videos, video_grid_thw) + video_embeds = torch.cat(video_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype) + _, video_mask = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, video_features=video_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) + + visual_pos_masks = None + deepstack_visual_embeds = None + if image_mask is not None and video_mask is not None: + # aggregate visual_pos_masks and deepstack_visual_embeds + image_mask = image_mask[..., 0] + video_mask = video_mask[..., 0] + visual_pos_masks = image_mask | video_mask + deepstack_visual_embeds = [] + image_mask_joint = image_mask[visual_pos_masks] + video_mask_joint = video_mask[visual_pos_masks] + for img_embed, vid_embed in zip(deepstack_image_embeds, deepstack_video_embeds): + embed_joint = img_embed.new_zeros(visual_pos_masks.sum(), img_embed.shape[-1]).to(img_embed.device) + embed_joint[image_mask_joint, :] = img_embed + embed_joint[video_mask_joint, :] = vid_embed + deepstack_visual_embeds.append(embed_joint) + elif image_mask is not None: + image_mask = image_mask[..., 0] + visual_pos_masks = image_mask + deepstack_visual_embeds = deepstack_image_embeds + elif video_mask is not None: + video_mask = video_mask[..., 0] + visual_pos_masks = video_mask + deepstack_visual_embeds = deepstack_video_embeds + + if position_ids is None: + attention_mask_tensor = ( + attention_mask if not isinstance(attention_mask, dict) else attention_mask["full_attention"] + ) + if attention_mask_tensor is not None and attention_mask_tensor.ndim == 4: + attention_mask_tensor = torch.diagonal(attention_mask_tensor[:, 0], dim1=1, dim2=2) + # Only apply conversion for floating point tensors (inverted masks) + if attention_mask_tensor.dtype.is_floating_point: + attention_mask_tensor = attention_mask_tensor / torch.finfo(attention_mask_tensor.dtype).min + attention_mask_tensor = (1.0 - attention_mask_tensor).int() + + # Calculate RoPE index once per generation in the pre-fill stage only. + # When compiling, we can't check tensor values thus we check only input length + # It is safe to assume that `length!=1` means we're in pre-fill because compiled + # models currently cannot do asssisted decoding + prefill_compiled_stage = is_torchdynamo_compiling() and ( + (input_ids is not None and input_ids.shape[1] != 1) + or (inputs_embeds is not None and inputs_embeds.shape[1] != 1) + ) + prefill_noncompiled_stage = not is_torchdynamo_compiling() and ( + (cache_position is not None and cache_position[0] == 0) + or (past_key_values is None or past_key_values.get_seq_length() == 0) + ) + if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None: + position_ids, rope_deltas = self.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + attention_mask=attention_mask_tensor, + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) if cache_position is not None else 0 + ) + position_ids = torch.arange(seq_length, device=inputs_embeds.device) # [N] + position_ids = position_ids.view(1, -1).expand(batch_size, -1) # [B,N] + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) + position_ids = position_ids.add(delta) # [B,N] + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # [3,B,N] + + outputs = self.language_model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + visual_pos_masks=visual_pos_masks, + deepstack_visual_embeds=deepstack_visual_embeds, + **kwargs, + ) + + return Qwen3VLMoeModelOutputWithPast( + last_hidden_state=outputs.last_hidden_state, + past_key_values=outputs.past_key_values, + rope_deltas=self.rope_deltas, + ) + + +def load_balancing_loss_func( + gate_logits: Union[torch.Tensor, tuple[torch.Tensor], None], + num_experts: Optional[int] = None, + top_k=2, + attention_mask: Optional[torch.Tensor] = None, +) -> Union[torch.Tensor, int]: + r""" + Computes auxiliary load balancing loss as in Switch Transformer - implemented in Pytorch. + + See Switch Transformer (https://huggingface.co/papers/2101.03961) for more details. This function implements the loss + function presented in equations (4) - (6) of the paper. It aims at penalizing cases where the routing between + experts is too unbalanced. + + Args: + gate_logits: + Logits from the `gate`, should be a tuple of model.config.num_hidden_layers tensors of + shape [batch_size X sequence_length, num_experts]. + num_experts: + Number of experts + top_k: + The number of experts to route per-token, can be also interpreted as the `top-k` routing + parameter. + attention_mask (`torch.Tensor`, *optional*): + The attention_mask used in forward function + shape [batch_size X sequence_length] if not None. + + Returns: + The auxiliary loss. + """ + if gate_logits is None or not isinstance(gate_logits, tuple): + return 0 + + if isinstance(gate_logits, tuple): + compute_device = gate_logits[0].device + concatenated_gate_logits = torch.cat([layer_gate.to(compute_device) for layer_gate in gate_logits], dim=0) + # concatenated_gate_logits: [num_layers*B*N,num_experts] + + routing_weights = torch.nn.functional.softmax(concatenated_gate_logits, dim=-1) # [num_layers*B*N,num_experts] + + _, selected_experts = torch.topk(routing_weights, top_k, dim=-1) # [num_layers*B*N,top_k] + + expert_mask = torch.nn.functional.one_hot(selected_experts, num_experts) # [num_layers*B*N,top_k,num_experts] + + if attention_mask is None: + # Compute the percentage of tokens routed to each experts + tokens_per_expert = torch.mean(expert_mask.float(), dim=0) # [top_k,num_experts] + + # Compute the average probability of routing to these experts + router_prob_per_expert = torch.mean(routing_weights, dim=0) # [num_experts] + else: + batch_size, sequence_length = attention_mask.shape + num_hidden_layers = concatenated_gate_logits.shape[0] // (batch_size * sequence_length) + + # Compute the mask that masks all padding tokens as 0 with the same shape of expert_mask + expert_attention_mask = ( + attention_mask[None, :, :, None, None] + .expand((num_hidden_layers, batch_size, sequence_length, top_k, num_experts)) + .reshape(-1, top_k, num_experts) + .to(compute_device) + ) # [num_layers*B*N,top_k,num_experts] + + # Compute the percentage of tokens routed to each experts + tokens_per_expert = torch.sum(expert_mask.float() * expert_attention_mask, dim=0) / torch.sum( + expert_attention_mask, dim=0 + ) # [top_k,num_experts] + + # Compute the mask that masks all padding tokens as 0 with the same shape of tokens_per_expert + router_per_expert_attention_mask = ( + attention_mask[None, :, :, None] + .expand((num_hidden_layers, batch_size, sequence_length, num_experts)) + .reshape(-1, num_experts) + .to(compute_device) + ) # [num_layers*B*N,num_experts] + + # Compute the average probability of routing to these experts + router_prob_per_expert = torch.sum(routing_weights * router_per_expert_attention_mask, dim=0) / torch.sum( + router_per_expert_attention_mask, dim=0 + ) # [num_experts] + + overall_loss = torch.sum(tokens_per_expert * router_prob_per_expert.unsqueeze(0)) + return overall_loss * num_experts + + +class Qwen3VLMoeForConditionalGeneration(Qwen3VLMoePreTrainedModel, GenerationMixin): + _checkpoint_conversion_mapping = {} + _tied_weights_keys = ["lm_head.weight"] + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLMoeConfig + + def __init__(self, config): + super().__init__(config) + self.model = Qwen3VLMoeModel(config) + self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False) + + self.post_init() + + def get_input_embeddings(self): + return self.model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.model.set_decoder(decoder) + + def get_decoder(self): + return self.model.get_decoder() + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + return self.model.get_video_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + return self.model.get_image_features(pixel_values, image_grid_thw) + + # Make modules available through conditional class for BC + @property + def language_model(self): + return self.model.language_model + + @property + def visual(self): + return self.model.visual + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLMoeCausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + + Example: + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Qwen3VLMoeForConditionalGeneration + + >>> model = Qwen3VLMoeForConditionalGeneration.from_pretrained("Qwen/Qwen3-VL-30B-A3B-Instruct", dtype="auto", device_map="auto") + >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen3-VL-30B-A3B-Instruct") + + >>> messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "image": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg", + }, + {"type": "text", "text": "Describe this image in short."}, + ], + } + ] + + >>> # Preparation for inference + >>> inputs = processor.apply_chat_template( + messages, + tokenize=True, + add_generation_prompt=True, + return_dict=True, + return_tensors="pt" + ) + >>> inputs = inputs.to(model.device) + + >>> # Generate + >>> generated_ids = model.generate(**inputs, max_new_tokens=128) + >>> generated_ids_trimmed = [ + out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) + ] + >>> processor.batch_decode(generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "A woman in a plaid shirt sits on a sandy beach at sunset, smiling as she gives a high-five to a yellow Labrador Retriever wearing a harness. The ocean waves roll in the background." + ```""" + + outputs = self.model( + input_ids=input_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] # [B,N,hidden_size] + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep + logits = self.lm_head(hidden_states[:, slice_indices, :]) # [B,N_kept,vocab_size] + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size) + + aux_loss = None + if kwargs.get("output_router_logits", False): + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.config.text_config.num_experts, + self.config.text_config.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.config.text_config.router_aux_loss_coef * aux_loss.to( + loss.device + ) # make sure to reside in the same device + + return Qwen3VLMoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + rope_deltas=outputs.rope_deltas, + ) + + def prepare_inputs_for_generation( + self, + input_ids, + past_key_values=None, + attention_mask=None, + inputs_embeds=None, + cache_position=None, + position_ids=None, + use_cache=True, + pixel_values=None, + pixel_values_videos=None, + image_grid_thw=None, + video_grid_thw=None, + **kwargs, + ): + # Overwritten -- in specific circumstances we don't want to forward image inputs to the model + + model_inputs = super().prepare_inputs_for_generation( + input_ids, + past_key_values=past_key_values, + attention_mask=attention_mask, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + position_ids=position_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + use_cache=use_cache, + **kwargs, + ) + + # Qwen3VLMoe position_ids are prepareed with rope_deltas in forward + model_inputs["position_ids"] = None + + if cache_position[0] != 0: + model_inputs["pixel_values"] = None + model_inputs["pixel_values_videos"] = None + + return model_inputs + + def _get_image_nums_and_video_nums( + self, + input_ids: Optional[torch.LongTensor], + inputs_embeds: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Get the number of images and videos for each sample to calculate the separation length of the sample tensor. + These parameters are not passed through the processor to avoid unpredictable impacts from interface modifications. + + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. + + Returns: + image_nums (`torch.LongTensor` of shape `(batch_size, num_images_sample)`) + video_nums (`torch.LongTensor` of shape `(batch_size, num_videos_sample)`) + """ + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + + if inputs_embeds is not None: + vision_start_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(vision_start_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + image_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + video_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + else: + vision_start_mask = input_ids == vision_start_token_id + image_mask = input_ids == image_token_id + video_mask = input_ids == video_token_id + + vision_first_mask = torch.roll(vision_start_mask, shifts=1, dims=1) + image_nums = torch.sum(vision_first_mask & image_mask, dim=1) + video_nums = torch.sum(vision_first_mask & video_mask, dim=1) + + return image_nums, video_nums + + def _expand_inputs_for_generation( + self, + expand_size: int = 1, + is_encoder_decoder: bool = False, + input_ids: Optional[torch.LongTensor] = None, + **model_kwargs, + ) -> tuple[torch.LongTensor, dict[str, Any]]: + # Overwritten -- Support for expanding tensors without a batch size dimension + # e.g., pixel_values, image_grid_thw, pixel_values_videos, video_grid_thw, second_per_grid_t + # pixel_values.shape[0] is sum(seqlen_images for samples) + # image_grid_thw.shape[0] is sum(num_images for samples) + + if expand_size == 1: + return input_ids, model_kwargs + + visual_keys = ["pixel_values", "image_grid_thw", "pixel_values_videos", "video_grid_thw", "second_per_grid_ts"] + + def _expand_dict_for_generation_visual(dict_to_expand): + image_grid_thw = model_kwargs.get("image_grid_thw", None) + video_grid_thw = model_kwargs.get("video_grid_thw", None) + image_nums, video_nums = self._get_image_nums_and_video_nums( + input_ids, inputs_embeds=model_kwargs.get("inputs_embeds", None) + ) + + def _repeat_interleave_samples(x, lengths, repeat_times): + samples = torch.split(x, lengths) + repeat_args = [repeat_times] + [1] * (x.dim() - 1) + result = torch.cat([sample.repeat(*repeat_args) for sample in samples], dim=0) + return result + + for key in dict_to_expand: + if key == "pixel_values": + # split images into samples + samples = torch.split(image_grid_thw, list(image_nums)) + # compute the sequence length of images for each sample + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "image_grid_thw": + # get the num of images for each sample + lengths = list(image_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "pixel_values_videos": + samples = torch.split(video_grid_thw, list(video_nums)) + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "video_grid_thw": + lengths = list(video_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "second_per_grid_ts": + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=list(video_nums), repeat_times=expand_size + ) + return dict_to_expand + + def _expand_dict_for_generation(dict_to_expand): + for key in dict_to_expand: + if ( + key != "cache_position" + and dict_to_expand[key] is not None + and isinstance(dict_to_expand[key], torch.Tensor) + and key not in visual_keys + ): + dict_to_expand[key] = dict_to_expand[key].repeat_interleave(expand_size, dim=0) + return dict_to_expand + + model_kwargs = _expand_dict_for_generation_visual(model_kwargs) + + if input_ids is not None: + input_ids = input_ids.repeat_interleave(expand_size, dim=0) + + model_kwargs = _expand_dict_for_generation(model_kwargs) + + if is_encoder_decoder: + if model_kwargs.get("encoder_outputs") is None: + raise ValueError("If `is_encoder_decoder` is True, make sure that `encoder_outputs` is defined.") + model_kwargs["encoder_outputs"] = _expand_dict_for_generation(model_kwargs["encoder_outputs"]) + + return input_ids, model_kwargs + + +__all__ = [ + "Qwen3VLMoeVisionModel", + "Qwen3VLMoeForConditionalGeneration", + "Qwen3VLMoeModel", + "Qwen3VLMoePreTrainedModel", + "Qwen3VLMoeTextModel", +] diff --git a/cosmos3/_src/vfm/models/vlm_model.py b/cosmos3/_src/vfm/models/vlm_model.py new file mode 100644 index 00000000..e8b1c55a --- /dev/null +++ b/cosmos3/_src/vfm/models/vlm_model.py @@ -0,0 +1,552 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""VLMModel: config-instantiable ImaginaireModel for VLM training. + +Config usage (in vfm/configs/base/vlm/config.py): + config.model = LazyCall(VLMModel)( + policy=config.policy, checkpoint=config.checkpoint, train=config.train + ) + +Phase 0 — bootstrap via the legacy VLM init path, ParallelDims, and async_safe_ce. +Phase 1 — ParallelDims switches to vfm/utils/parallelism.py. +Phase 2 — legacy init replaced by direct HFModel path (_init_vlm); async_safe_ce + replaced by vfm/utils/loss.py::cross_entropy_loss. +Phase 3 — init_flash_attn_meta ported to vfm/utils/flash_attn.py; + config unified under vfm/configs/base/vlm/config.py. +""" + +import os +import re +from collections.abc import Callable +from functools import partial + +import torch +import torch.nn as nn + +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.configs.base.defaults.model_config import VLMModelConfig +from cosmos3._src.vfm.models.hf_model import HFModel +from cosmos3._src.vfm.models.parallelize_vlm import parallelize +from cosmos3._src.vfm.utils.loss import cross_entropy_loss +from cosmos3._src.vfm.utils.parallelism import ParallelDims +from projects.cosmos3.vlm.utils.constant import IGNORE_INDEX +from projects.cosmos3.vlm.utils.create_position_ids import get_position_ids + +# Model-type dispatch sets. Using hf_config.model_type (stable HF-defined string) +# rather than model_name_or_path avoids the brittleness of substring-matching a local +# filesystem path that VLMModel._init_vlm has already rewritten (see _init_vlm: the +# downloader returns a local cache path, so the configured model name is lost). +# +# ``qwen3_vl_moe`` is listed here as forward-compat — MoE dispatch in every family +# helper below is already wired for the 30B-A3B / 235B-A22B variants. End-to-end +# training still fails earlier at load_vlm_model's MoE precheck +# (safetensors_loader.py _is_moe_vlm / NotImplementedError) because sharded MoE +# weight loading is unimplemented; see spec §2.2. Removing ``qwen3_vl_moe`` here +# would regress the family helpers the moment MoE load support lands. +_QWEN_VL_TYPES = {"qwen2_5_vl", "qwen3_vl", "qwen3_vl_moe"} +# InternVL variants register both "internvl" and "internvl_chat" as model_type +# in the upstream InternVL HF policy registry. +_INTERNVL_TYPES = {"internvl", "internvl_chat"} + + +def _get_overlay_config(model_type: str) -> tuple[list[str], Callable[[str], bool]]: + """Return ``(skip_patterns, is_lm_key)`` for the pretrain_weights_path_llm overlay. + + ``skip_patterns`` are regex patterns for resolved model keys that are expected to + be absent from the LLM overlay checkpoint (visual encoder + projector); they are + passed as ``extra_skip_patterns`` to ``load_vlm_model`` so Phase-6 completeness + check tolerates them. Every OTHER missing model key still raises. + + ``is_lm_key`` is a predicate that decides whether a key returned in ``keys_loaded`` + counts as a "language-model parameter" for VLMModel's post-overlay sanity check. + Implemented as the inverse of ``skip_patterns`` — a loaded key counts as an LM key + iff it does NOT match any of the visual/projector skip regexes. This mirrors + exactly what ``load_vlm_model``'s Phase-5 skip logic does, so the two checks can + never disagree under HF state-dict layout variations (e.g. ``model.model.*`` + vs. ``model.language_model.*``). + + Family-specific because non-LM params differ across VLM families (projectors may + live outside ``model.visual.*``). Raises ``NotImplementedError`` for unsupported + families — safer than silently mis-skipping. Add a new entry when onboarding a + new VLM family. + + MoE note: ``qwen3_vl_moe`` is accepted here but end-to-end MoE training still + fails earlier at ``load_vlm_model``'s MoE precheck (see module docstring on + ``_QWEN_VL_TYPES``). + """ + if model_type in _QWEN_VL_TYPES: + # Qwen2.5-VL / Qwen3-VL dense + MoE: the visual encoder AND merger/projector + # both live under a ``visual.*`` subtree (merger is a submodule of visual — + # see Qwen3VLForConditionalGeneration / Qwen2_5_VLForConditionalGeneration). + # Every non-visual resolved key counts as an LM key (language_model layers, + # norm, embed_tokens, top-level lm_head). + # + # The ``(?:model\.)*`` prefix makes both the loader-side Phase-5 skip AND + # the VLMModel-side LM predicate tolerate three layouts uniformly: + # + # 1. Bare (Qwen2.5-VL official HF class) — ``visual.merger.*``, + # ``model.embed_tokens.*`` / ``lm_head.weight``. See + # projects/cosmos3/vlm/scripts/convert_qwenvl_ckpt.py:101-118 which + # inspects ``state_dict()`` for keys starting with ``visual.merger``. + # 2. One wrapper (Qwen3-VL official HF class) — ``model.visual.*``, + # ``model.language_model.*`` / ``lm_head.weight``. + # 3. Two+ wrappers (HFModel-shim-wrapped callers) — ``model.model.visual + # .*`` etc., e.g. hf_model_test.py::test_vlm_load_hf_native_keys:644. + # + # A narrower regex (e.g. requiring a leading ``model.``) would either + # reject valid Qwen2.5 visual keys in Phase-6 completeness OR misclassify + # wrapper-layout visual keys as LM keys in the post-overlay safety check. + skip_patterns = [r"^(?:model\.)*visual\..*"] + compiled_skips = [re.compile(p) for p in skip_patterns] + return ( + skip_patterns, + lambda k: not any(r.match(k) for r in compiled_skips), + ) + # Nemotron / InternVL / etc: projectors live outside ``model.visual.*`` + # (e.g. ``model.multi_modal_projector.*``, ``model.projector.*``), and lm_head + # may be nested (``model.lm_head.weight``). The Qwen-shaped skip list would fail + # Phase-6 completeness on those families; the Qwen-shaped predicate would misreport + # a successful overlay as "0 language-model parameters". Fail loudly rather than + # silently. + raise NotImplementedError( + f"VLMModel: pretrain_weights_path_llm overlay not yet supported for " + f"model_type={model_type!r}. Supported types: {sorted(_QWEN_VL_TYPES)}. " + f"Add a new entry in _get_overlay_config() when onboarding a new VLM family " + f"(see docs/superpowers/specs/2026-04-20-vlm-pretrain-weights-path-llm-design.md §7)." + ) + + +def _get_vision_encoder_modules(model: nn.Module, model_type: str) -> list: + if model_type in _QWEN_VL_TYPES: + + # which returns only [patch_embed, blocks]. Qwen3-VL adds a learnable `pos_embed` + # (nn.Embedding — see qwen3_vl.py Qwen3VLVisionModel); leaving it trainable while + # freezing the rest of the vision encoder contradicts the intent of + # freeze_vision_encoder=True. `hasattr` gate preserves Qwen2.5-VL compatibility + # (no pos_embed there). + mods = [model.visual.patch_embed, model.visual.blocks] + if hasattr(model.visual, "pos_embed"): + mods.append(model.visual.pos_embed) + return mods + elif model_type in _INTERNVL_TYPES: + return [model.vision_model] + raise ValueError(f"freeze_vision_encoder not supported for model_type={model_type!r}") + + +def _get_mm_projector_modules(model: nn.Module, model_type: str) -> list: + if model_type == "qwen2_5_vl": + return [model.visual.merger] + elif model_type in {"qwen3_vl", "qwen3_vl_moe"}: + mods = [model.visual.merger] + if hasattr(model.visual, "deepstack_merger_list"): + mods.append(model.visual.deepstack_merger_list) + return mods + elif model_type in _INTERNVL_TYPES: + # Legacy InternVL helper used `model.model.model.multi_modal_projector` + # because it operated on a wrapped HFModel (ImaginaireModel -> HFModel -> + # raw HF InternVL). We receive the raw HF model directly + # (hf_model._model), so drop the two wrapper hops. Best-effort until L1 + # GPU validation on a real InternVL3_5 checkpoint. + return [model.model.multi_modal_projector] + raise ValueError(f"freeze_mm_projector not supported for model_type={model_type!r}") + + +def _get_llm_modules(model: nn.Module, model_type: str) -> list: + if model_type in _QWEN_VL_TYPES: + # model.language_model is a @property on Qwen3VLForConditionalGeneration / + # Qwen2_5_VLForConditionalGeneration that delegates to self.model.language_model + # — avoids accidentally freezing `visual` which also lives inside self.model. + # model.lm_head is a top-level submodule on the conditional-generation class. + return [model.language_model, model.lm_head] + elif model_type in _INTERNVL_TYPES: + # Legacy InternVL helper returned `[model.language_model, model.model.lm_head]` + # for the wrapped HFModel. Same raw-HF adjustment as mm_projector above: + # the raw HF InternVL class exposes `.language_model` at the top level but + # its `lm_head` lives one level deeper under `.model`. Best-effort until + # L1 validation. + return [model.language_model, model.model.lm_head] + raise ValueError(f"freeze_llm not supported for model_type={model_type!r}") + + +def _apply_freeze_config(model: nn.Module, model_type: str, cfg) -> int: + """Apply freeze config in-place. Returns trainable parameter-tensor count. + + ``cfg`` can be either a concrete ``OptimizerConfig`` instance (direct-constructor + path, e.g. unit tests) or a ``DictConfig`` / LazyCall-backed config (runtime path, + where the attrs ``__attrs_post_init__`` has not yet fired). The mutual-exclusivity + guard below duplicates the attrs validator so both paths fail loudly before any + parameter is frozen. + """ + # Extract optional VFM-only fields — OptimizerConfig lives in vlm/ and does not + # carry trainable_params / frozen_params, so always use getattr with None default. + trainable_params = getattr(cfg, "trainable_params", None) + frozen_params = getattr(cfg, "frozen_params", None) + + # Defensive mutual-exclusivity guard — runs BEFORE any freeze, even on LazyCall path + if trainable_params is not None and frozen_params is not None: + raise ValueError("OptimizerConfig: set at most one of trainable_params or frozen_params, not both.") + + # Step 1 — legacy named flags via module-probing + if cfg.freeze_vision_encoder: + for m in _get_vision_encoder_modules(model, model_type): + for p in m.parameters(): + p.requires_grad = False + + if cfg.freeze_mm_projector: + for m in _get_mm_projector_modules(model, model_type): + for p in m.parameters(): + p.requires_grad = False + + if cfg.freeze_llm: + for m in _get_llm_modules(model, model_type): + for p in m.parameters(): + p.requires_grad = False + + # Step 2 — regex override (mutually exclusive; already validated above). + # + # `remove_duplicate=False` is required for tied weights. Qwen3 configs set + # `tie_word_embeddings=True`, so `hf_model.tie_embeddings()` makes + # `lm_head.weight` and `model.embed_tokens.weight` the same tensor. The default + # `named_parameters()` dedups by tensor id and keeps only the first traversed + # name (`model.embed_tokens.weight`); a regex aimed at `lm_head` would silently + # match nothing and user intent would be lost. Iterating with duplicates + # preserves both names so either can trigger a match. + if trainable_params is not None: + # OR-semantics across tied names: first freeze everything, then unfreeze + # any tensor whose *any* registered name matches. Cannot write + # `requires_grad = any(...)` directly because a second visit could flip + # True back to False on the same shared tensor. + for p in model.parameters(): + p.requires_grad = False + for param_name, p in model.named_parameters(remove_duplicate=False): + if any(re.search(pat, param_name) for pat in trainable_params): + p.requires_grad = True + elif frozen_params is not None: + for param_name, p in model.named_parameters(remove_duplicate=False): + if any(re.search(pat, param_name) for pat in frozen_params): + p.requires_grad = False + + n = sum(p.requires_grad for p in model.parameters()) + if not any([cfg.freeze_vision_encoder, cfg.freeze_mm_projector, cfg.freeze_llm, trainable_params, frozen_params]): + log.warning("freeze config: no freeze mechanism set — all parameters are trainable (full fine-tune)") + assert n > 0, "freeze config left 0 trainable parameters — check patterns" + return n + + +class VLMModel(ImaginaireModel): + """Config-instantiable ImaginaireModel for VLM training. + + Args: + config: VLMModelConfig (policy, train, ema). + checkpoint: root CheckpointConfig (load_path, load_from_object_store). + """ + + def __init__(self, config: VLMModelConfig, checkpoint): + super().__init__() + from cosmos3._src.vfm.utils.flash_attn import init_flash_attn_meta + + self.config = config + init_flash_attn_meta(config.train.deterministic) + self._init_vlm(config.policy, checkpoint, config.train) + + dp_group = None + cp_group = None + if self.parallel_dims is not None: + if self.parallel_dims.dp_shard_enabled: + dp_group = self.parallel_dims.dp_shard_mesh.get_group() + if self.parallel_dims.cp_enabled: + cp_group = self.parallel_dims.cp_mesh.get_group() + + self._loss_fn = partial( + cross_entropy_loss, + loss_scaling_factor=1.0, + dp_group=dp_group, + cp_group=cp_group, + ignore_index=IGNORE_INDEX, + ) + + def _init_vlm(self, policy, checkpoint, train) -> None: + """Initialize VLM without the legacy ModelRegistry (Phase 2+). + + Sequence (ordering is critical — do not reorder): + a. Download HF weights from S3 to local cache. + b. Meta-init HFModel (params on meta, buffers on CPU via include_buffers=False; + c. Build ParallelDims + device mesh. + d. Apply FSDP2 via parallelize() — meta tensors are NOT auto-materialized. + e. Explicitly materialize meta tensors; move CPU buffers to CUDA. + f. Tie output embedding → input embedding if tie_word_embeddings=True. + g. Load pretrain weights into sharded CUDA tensors. + h. Apply gradient checkpointing if configured. + """ + from projects.cosmos3.vlm.utils.pretrained_models_downloader import ( + maybe_download_hf_model_from_s3, + ) + + load_pretrain_weights = checkpoint.load_path == "" + log.info(f"checkpoint.load_path: {checkpoint.load_path!r} | load_pretrain_weights: {load_pretrain_weights}") + + # ── a. Download HF model files (config + tokenizer; weights only if no ckpt) ── + local_path = maybe_download_hf_model_from_s3( + policy.model_name_or_path, + checkpoint.load_from_object_store.credentials, + checkpoint.load_from_object_store.bucket, + include_model_weights=load_pretrain_weights, + ) + # local_path is exposed below as self.model_name_or_path; the (frozen) policy + # config is not mutated. + + # ── b. Meta-init HFModel ── + hf_model = HFModel( + model_name_or_path=local_path, + dtype=train.master_torch_dtype, + attn_implementation="flash_attention_2", + ) + + # ── b.1. Early family-gate for pretrain_weights_path_llm ── + # Fail-fast on unsupported VLM families BEFORE any expensive work + # (parallelize, materialize, base-weight load, overlay download). + # ``hf_config.model_type`` is populated by HFModel's meta-init; no + # weights touched yet. Uses ``getattr`` default so the probe itself + # matches the later overlay guard at step g.2 exactly. + if getattr(policy, "pretrain_weights_path_llm", ""): + _get_overlay_config(hf_model.hf_config.model_type) + + # ── c. Build ParallelDims + device mesh ── + # Overlay-mesh design (see vfm/utils/parallelism.py): cp/cfgp do NOT + # consume FSDP rank slots, so dp_replicate * dp_shard == world_size + # alone. The VLM HFModel doesn't have a CP-aware attention path. + world_size = int(os.environ.get("WORLD_SIZE", 1)) + _tp = policy.parallelism.tp_size + _ep = policy.parallelism.ep_size + _pp = policy.parallelism.pp_size + if _tp != 1 or _ep != 1 or _pp != 1: + raise NotImplementedError( + "VLMModel: tp/ep/pp parallelism is not supported in the unified ParallelDims " + f"design (got tp={_tp}, ep={_ep}, pp={_pp}). Set policy.parallelism.tp_size=1, " + f"ep_size=1, pp_size=1, and use dp_shard_size + dp_replicate_size for FSDP/HSDP." + ) + _dp_shard = policy.parallelism.dp_shard_size + _dp_replicate = policy.parallelism.dp_replicate_size + # Single-process run: force dp_replicate=1 so ParallelDims doesn't + # auto-infer it to world_size (which would equal 1 anyway, but guards + # against environments where WORLD_SIZE is unset/inconsistent). + if not torch.distributed.is_initialized(): + _dp_replicate = 1 + + parallel_dims = ParallelDims( + world_size=world_size, + dp_shard=_dp_shard, + dp_replicate=_dp_replicate, + cp=getattr(policy.parallelism, "cp_size", 1), + enable_inference_mode=False, + ) + + # VLM does not currently support cp or cfgp. CP needs a CP-aware + # attention path (see ``vfm/models/mot/context_parallel_utils.py``) that + # is not wired into the VLM HFModel; CFGP is inference-only. + assert parallel_dims.cp == 1, f"VLM does not support CP (got cp={parallel_dims.cp})" + assert parallel_dims.cfgp == 1, f"VLM does not support CFGP (got cfgp={parallel_dims.cfgp})" + + if torch.distributed.is_initialized(): + parallel_dims.build_meshes(device_type="cuda") + + # Replicate-only (DDP) is not implemented in Phase 2's parallelize(). + # Raise early rather than running with no gradient synchronization and + # silently producing wrong training results. + if parallel_dims.dp_replicate_enabled and not parallel_dims.dp_shard_enabled: + raise NotImplementedError( + "VLMModel Phase 2 does not support replicate-only DDP " + "(dp_replicate > 1, dp_shard == 1). " + "Use dp_shard > 1 for FSDP2. DDP support is planned for Phase 3." + ) + + # ── d. Apply FSDP2 ── + if torch.distributed.is_initialized(): + parallelize(hf_model, parallel_dims, train) + + # ── e. Materialize meta tensors ── + # FSDP2 fully_shard does NOT auto-materialize meta tensors. We must + # explicitly allocate empty tensors so that load_weights() can copy + # into them via local_view.data.copy_(). + # - Normal (no offload): materialize to CUDA directly. + # - CPUOffloadPolicy: FSDP2 keeps params on CPU between fwd/bwd and + # moves them to GPU during forward. Materialize to CPU here so that + # load_weights() can write into a real (non-meta) CPU tensor; FSDP + # will move them to GPU on first forward. + # Reference: vlm/train.py:188-192 (same guard; offload handled by post_to_empty_hook). + if getattr(train, "fsdp_offload", False): + hf_model._model._apply( + lambda t: torch.empty_like(t, device="cpu") if t.device.type == "meta" else t, + recurse=True, + ) + else: + hf_model._model._apply( + lambda t: torch.empty_like(t, device="cuda") if t.device.type == "meta" else t.to("cuda"), + recurse=True, + ) + + # ── f. Tie embeddings (replaces the legacy post_to_empty_hook) ── + hf_model.tie_embeddings() + + # ── g. Load pretrain weights ── + if load_pretrain_weights: + hf_model.load_weights( + checkpoint_path=local_path, + credential_path=None, # local path after download + parallel_dims=parallel_dims if torch.distributed.is_initialized() else None, + ) + + # ── g.2. Optional LLM overlay (pretrain_weights_path_llm) ── + # Overlay the language tower with a separate LLM checkpoint. + # Visual + projector params are preserved from the VLM load above + # (skipped via extra_skip_patterns). The existing name converter + # in load_vlm_model tail-matches raw LLM keys into + # model.language_model.*, so no temp-dir remap is needed. + # Mirrors legacy vlm/train.py:221-233 semantics. + llm_path = getattr(policy, "pretrain_weights_path_llm", "") + if llm_path: + overlay_skip_patterns, is_lm_key = _get_overlay_config(hf_model.hf_config.model_type) + llm_local_path = maybe_download_hf_model_from_s3( + llm_path, + checkpoint.load_from_object_store.credentials, + checkpoint.load_from_object_store.bucket, + include_model_weights=True, + require_s3_exists=True, + ) + keys_loaded = hf_model.load_weights( + checkpoint_path=llm_local_path, + credential_path=None, + parallel_dims=parallel_dims if torch.distributed.is_initialized() else None, + extra_skip_patterns=overlay_skip_patterns, + ) + lm_loaded = {k for k in keys_loaded if is_lm_key(k)} + if not lm_loaded: + raise RuntimeError( + f"VLMModel overlay: loaded 0 language-model parameters from " + f"{llm_path!r} (local path: {llm_local_path!r}). The LLM " + "checkpoint did not match any language_model.* key in the " + "VLM; check model-family / layer-count compatibility." + ) + log.info(f"VLMModel: overlaid {len(lm_loaded)} language-model params from {llm_path}") + + # ── i. Gradient checkpointing ── + if policy.model_gradient_checkpointing: + hf_model.apply_gradient_checkpointing() + + self.model = hf_model + self.parallel_dims = parallel_dims + self.model_name_or_path = local_path + self.hf_config = hf_model.hf_config + + def on_train_start(self, memory_format) -> None: + """Called by trainer after model.to("cuda"). No device move needed here.""" + + def on_after_backward(self, iteration: int = 0) -> None: + """No-op — FSDP handles gradient synchronization internally.""" + + def init_optimizer_scheduler(self, optimizer_config, scheduler_config): + """Apply freeze config then build optimizer + scheduler. + + Freeze order: legacy flags (module-probing, dispatched on hf_config.model_type) + → regex override (trainable_params or frozen_params). All mechanisms are + optional — if none set, all params train. + + Dispatch uses ``self.hf_config.model_type`` (a stable HF string like + ``"qwen3_vl"``, ``"qwen2_5_vl"``, ``"internvl_chat"``) rather than + ``self.model_name_or_path`` because ``_init_vlm`` rewrites the latter with a + resolved local filesystem path that may not contain a recognizable + architecture substring. + + Names emitted by ``self.model._model.named_parameters()`` do NOT carry a + ``_model.`` prefix; they start directly with the submodule name + (e.g. ``model.visual.patch_embed.weight`` on Qwen3-VL). + """ + cfg = optimizer_config.config + n_trainable = _apply_freeze_config(self.model._model, self.hf_config.model_type, cfg) + log.info( + f"freeze config applied (model_type={self.hf_config.model_type}): {n_trainable} trainable parameter tensors" + ) + + # Single-part optimizer. The freeze config already selected the right subset of + # parameters (via legacy flags, trainable_params, or frozen_params); per-component + # LR multipliers (lr_multiplier={"vision_encoder": 0.1, "mm_projector": 1.0, ...}) + # are NOT restored by this change — that requires a separate multi-part optimizer + # rework on the VFM-unified path. + optimizer_config.model_parts = [self.model] + optimizer_config.model_part_names = ["llm"] + optimizer = instantiate(optimizer_config) + + # Build scheduler. + scheduler_config.optimizers = optimizer + scheduler = instantiate(scheduler_config) + + return optimizer, scheduler + + def training_step(self, data: dict, iteration: int) -> tuple[dict, torch.Tensor]: + """position_ids → forward → CE loss.""" + position_ids = get_position_ids( + self.hf_config, + input_ids=data["input_ids"], + image_grid_thw=data.get("image_grid_thw"), + video_grid_thw=data.get("video_grid_thw"), + attention_mask=data.get("attention_mask"), + ) + if position_ids is not None: + data["position_ids"] = position_ids + + labels = data.pop("labels") + data.pop("attention_mask", None) + logits = self.model(**data) + loss = self._loss_fn(logits, labels) + + # loss_avg: DP-averaged loss for logging (matches cosmos-rl ReduceOp.AVG). + # Does not affect the backward scalar. Pick the same 1-D sub-mesh the + # legacy single-mesh ``ParallelDims.dp_mesh`` returned — dp_shard if + # sharding is on, else dp_replicate — so the reduction group is + # byte-identical to pre-merge behavior. + loss_avg = loss.detach().clone() + pd = getattr(self, "parallel_dims", None) + dp_mesh = pd.dp_mesh if pd is not None else None + if torch.distributed.is_initialized() and dp_mesh is not None: + sub_dim = "dp_shard" if pd.dp_shard_enabled else "dp_replicate" + torch.distributed.all_reduce( + loss_avg, op=torch.distributed.ReduceOp.AVG, group=dp_mesh[sub_dim].get_group() + ) + if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0: + log.info(f"train/loss_avg: {loss_avg.item():.5f} (iteration {iteration})") + + return {"loss": loss, "loss_avg": loss_avg, "labels": labels}, loss + + @torch.no_grad() + def validation_step(self, data: dict, iteration: int) -> tuple[dict, torch.Tensor]: + """Required: VLM experiments enable validation by default (pre_exp01x.py:607). + ImaginaireTrainer.validate() calls this — must not raise NotImplementedError.""" + position_ids = get_position_ids( + self.hf_config, + input_ids=data["input_ids"], + image_grid_thw=data.get("image_grid_thw"), + video_grid_thw=data.get("video_grid_thw"), + attention_mask=data.get("attention_mask"), + ) + if position_ids is not None: + data["position_ids"] = position_ids + + labels = data.pop("labels") + data.pop("attention_mask", None) + logits = self.model(**data) + loss = self._loss_fn(logits, labels) + return {"loss": loss, "labels": labels}, loss diff --git a/cosmos3/_src/vfm/tokenizers/__init__.py b/cosmos3/_src/vfm/tokenizers/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/tokenizers/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/tokenizers/interface.py b/cosmos3/_src/vfm/tokenizers/interface.py new file mode 100644 index 00000000..23b2960b --- /dev/null +++ b/cosmos3/_src/vfm/tokenizers/interface.py @@ -0,0 +1,236 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import Optional + +import torch + +from cosmos3._src.imaginaire.utils.env_parsers.cred_env_parser import CRED_ENVS + + +class VideoTokenizerInterface(ABC): + def __init__(self, object_store_credential_path_pretrained: Optional[str] = None): + assert object_store_credential_path_pretrained is None or isinstance( + object_store_credential_path_pretrained, str + ) + if object_store_credential_path_pretrained is None: + self.backend_args = None + elif os.path.exists(object_store_credential_path_pretrained) or CRED_ENVS.APP_ENV in ["prod", "dev", "stg"]: + self.backend_args = { + "backend": "s3", + "path_mapping": None, + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + raise FileNotFoundError( + f"Invalid object_store_credential_path_pretrained: {object_store_credential_path_pretrained} and APP_ENV is not prod/dev/stg" + ) + + @abstractmethod + def reset_dtype(self): + """ + Reset the dtype of the model to the dtype its weights were trained with or quantized to. + """ + pass + + @abstractmethod + def encode(self, state: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def decode(self, latent: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def get_latent_num_frames(self, num_pixel_frames: int) -> int: + pass + + @abstractmethod + def get_pixel_num_frames(self, num_latent_frames: int) -> int: + pass + + @property + @abstractmethod + def spatial_compression_factor(self) -> int: + pass + + @property + @abstractmethod + def temporal_compression_factor(self) -> int: + pass + + @property + @abstractmethod + def spatial_resolution(self) -> int: + pass + + @property + @abstractmethod + def pixel_chunk_duration(self): + pass + + @property + @abstractmethod + def latent_chunk_duration(self): + pass + + @property + @abstractmethod + def latent_ch(self) -> int: + pass + + def compile_encode( + self, + warmup_resolutions: Sequence[str], + output_dir: str, + aspect_ratio: str | None = None, + ) -> None: + """AOT-compile the tokenizer for the given resolutions. + + Subclasses that support AOT compilation should override this method. + The default raises ``NotImplementedError``. + + Args: + warmup_resolutions: Resolution keys to compile for. + output_dir: Root directory where compiled artifacts are stored + (typically ``config.job.path_local``). + aspect_ratio: If given, only compile this single aspect ratio. + """ + raise NotImplementedError(f"{type(self).__name__} does not support compilation") + + @property + def is_chunk_overlap(self): + return False + + @property + def is_causal(self): + return True + + +class AudioTokenizerInterface(ABC): + """Abstract interface for audio tokenizers.""" + + def __init__(self, object_store_credential_path_pretrained: Optional[str] = None): + assert object_store_credential_path_pretrained is None or isinstance( + object_store_credential_path_pretrained, str + ) + if object_store_credential_path_pretrained is None: + self.backend_args = None + elif os.path.exists(object_store_credential_path_pretrained) or CRED_ENVS.APP_ENV in ["prod", "dev", "stg"]: + self.backend_args = { + "backend": "s3", + "path_mapping": None, + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + raise FileNotFoundError( + f"Invalid object_store_credential_path_pretrained: {object_store_credential_path_pretrained} and APP_ENV is not prod/dev/stg" + ) + + @abstractmethod + def reset_dtype(self): + """ + Reset the dtype of the model to the dtype its weights were trained with or quantized to. + """ + pass + + @abstractmethod + def encode(self, audio: torch.Tensor, force_pad: bool = False) -> torch.Tensor: + """ + Encode audio waveform to latent representation. + + Args: + audio: Input audio tensor of shape [B, C, T] where: + B = batch size, C = audio channels, T = time samples + force_pad: Whether to force padding to match compression factor + + Returns: + Latent tensor of shape [B, latent_ch, T'] + """ + pass + + @abstractmethod + def decode(self, latent: torch.Tensor) -> torch.Tensor: + """ + Decode latent representation to audio waveform. + + Args: + latent: Latent tensor of shape [B, latent_ch, T'] + + Returns: + Audio tensor of shape [B, C, T] + """ + pass + + @abstractmethod + def get_latent_num_samples(self, num_audio_samples: int) -> int: + """ + Calculate the number of latent time samples from audio samples. + + Args: + num_audio_samples: Number of audio samples + + Returns: + Number of latent time samples + """ + pass + + @abstractmethod + def get_audio_num_samples(self, num_latent_samples: int) -> int: + """ + Calculate the number of audio samples from latent samples. + + Args: + num_latent_samples: Number of latent time samples + + Returns: + Number of audio samples + """ + pass + + @property + @abstractmethod + def temporal_compression_factor(self) -> int: + """ + Temporal compression factor (downsampling ratio). + audio_samples = latent_samples * temporal_compression_factor + """ + pass + + @property + @abstractmethod + def sample_rate(self) -> int: + """Audio sample rate in Hz.""" + pass + + @property + @abstractmethod + def audio_channels(self) -> int: + """Number of audio channels (e.g., 1 for mono, 2 for stereo).""" + pass + + @property + @abstractmethod + def latent_ch(self) -> int: + """Number of latent channels.""" + pass + + @property + def is_causal(self) -> bool: + """Whether the model is causal (for streaming).""" + return False diff --git a/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py b/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py new file mode 100644 index 00000000..a14cb1de --- /dev/null +++ b/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Source Repository: https://github.com/ByteDance-Seed/Bagel +# This is adapted from modeling/qwen2/tokenization_qwen2.py +# Commit Hash: 7026cfa0a4df274460d0b0b990117398a4ec6fca + +"""Tokenization classes for Qwen2.""" + +import json +import os +import unicodedata +from functools import lru_cache +from typing import Optional, Tuple + +import regex as re +from transformers.tokenization_utils import AddedToken, PreTrainedTokenizer +from transformers.utils import logging + +logger = logging.get_logger(__name__) + +VOCAB_FILES_NAMES = { + "vocab_file": "vocab.json", + "merges_file": "merges.txt", +} + + +MAX_MODEL_INPUT_SIZES = {"qwen/qwen-tokenizer": 32768} + +PRETOKENIZE_REGEX = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" + + +@lru_cache() +# Copied from transformers.models.gpt2.tokenization_gpt2.bytes_to_unicode +def bytes_to_unicode(): + """ + Returns list of utf-8 byte and a mapping to unicode strings. We specifically avoids mapping to whitespace/control + characters the bpe code barfs on. + + The reversible bpe codes work on unicode strings. This means you need a large # of unicode characters in your vocab + if you want to avoid UNKs. When you're at something like a 10B token dataset you end up needing around 5K for + decent coverage. This is a significant percentage of your normal, say, 32K bpe vocab. To avoid that, we want lookup + tables between utf-8 bytes and unicode strings. + """ + bs = list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1)) + list(range(ord("®"), ord("ÿ") + 1)) + cs = bs[:] + n = 0 + for b in range(2**8): + if b not in bs: + bs.append(b) + cs.append(2**8 + n) + n += 1 + cs = [chr(n) for n in cs] + return dict(zip(bs, cs)) + + +# Copied from transformers.models.gpt2.tokenization_gpt2.get_pairs +def get_pairs(word): + """ + Return set of symbol pairs in a word. + + Word is represented as tuple of symbols (symbols being variable-length strings). + """ + pairs = set() + prev_char = word[0] + for char in word[1:]: + pairs.add((prev_char, char)) + prev_char = char + return pairs + + +class Qwen2Tokenizer(PreTrainedTokenizer): + """ + Construct a Qwen2 tokenizer. Based on byte-level Byte-Pair-Encoding. + + Same with GPT2Tokenizer, this tokenizer has been trained to treat spaces like parts of the tokens so a word will + be encoded differently whether it is at the beginning of the sentence (without space) or not: + + ```python + >>> from transformers import Qwen2Tokenizer + + >>> tokenizer = Qwen2Tokenizer.from_pretrained("Qwen/Qwen-tokenizer") + >>> tokenizer("Hello world")["input_ids"] + [9707, 1879] + + >>> tokenizer(" Hello world")["input_ids"] + [21927, 1879] + ``` + This is expected. + + You should not use GPT2Tokenizer instead, because of the different pretokenization rules. + + This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to + this superclass for more information regarding those methods. + + Args: + vocab_file (`str`): + Path to the vocabulary file. + merges_file (`str`): + Path to the merges file. + errors (`str`, *optional*, defaults to `"replace"`): + Paradigm to follow when decoding bytes to UTF-8. See + [bytes.decode](https://docs.python.org/3/library/stdtypes.html#bytes.decode) for more information. + unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this + token instead. + bos_token (`str`, *optional*): + The beginning of sequence token. Not applicable for this tokenizer. + eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The end of sequence token. + pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The token used for padding, for example when batching sequences of different lengths. + clean_up_tokenization_spaces (`bool`, *optional*, defaults to `False`): + Whether or not the model should cleanup the spaces that were added when splitting the input text during the + tokenization process. Not applicable to this tokenizer, since tokenization does not add spaces. + split_special_tokens (`bool`, *optional*, defaults to `False`): + Whether or not the special tokens should be split during the tokenization process. The default behavior is + to not split special tokens. This means that if `<|endoftext|>` is the `eos_token`, then `tokenizer.tokenize("<|endoftext|>") = + ['<|endoftext|>`]. Otherwise, if `split_special_tokens=True`, then `tokenizer.tokenize("<|endoftext|>")` will be give `['<', + '|', 'endo', 'ft', 'ext', '|', '>']`. This argument is only supported for `slow` tokenizers for the moment. + """ + + vocab_files_names = VOCAB_FILES_NAMES + model_input_names = ["input_ids", "attention_mask"] + + def __init__( + self, + vocab_file, + merges_file, + errors="replace", + unk_token="<|endoftext|>", + bos_token=None, + eos_token="<|endoftext|>", + pad_token="<|endoftext|>", + clean_up_tokenization_spaces=False, + split_special_tokens=False, + **kwargs, + ): + # Qwen vocab does not contain control tokens; added tokens need to be special + bos_token = ( + AddedToken(bos_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(bos_token, str) + else bos_token + ) + eos_token = ( + AddedToken(eos_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(eos_token, str) + else eos_token + ) + unk_token = ( + AddedToken(unk_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(unk_token, str) + else unk_token + ) + pad_token = ( + AddedToken(pad_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(pad_token, str) + else pad_token + ) + + with open(vocab_file, encoding="utf-8") as vocab_handle: + self.encoder = json.load(vocab_handle) + self.decoder = {v: k for k, v in self.encoder.items()} + self.errors = errors # how to handle errors in decoding + self.byte_encoder = bytes_to_unicode() + self.byte_decoder = {v: k for k, v in self.byte_encoder.items()} + bpe_merges = [] + with open(merges_file, encoding="utf-8") as merges_handle: + for i, line in enumerate(merges_handle): + line = line.strip() + if (i == 0 and line.startswith("#version:")) or not line: + continue + bpe_merges.append(tuple(line.split())) + self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges)))) + + # (esp. for texts of language that do not use space between word, e.g. Chinese); technically + # not a memory leak but appears as one. + # GPT2Tokenizer has the same problem, so let's be consistent. + self.cache = {} + + self.pat = re.compile(PRETOKENIZE_REGEX) + + if kwargs.get("add_prefix_space", False): + logger.warning_once( + f"{self.__class__.__name} does not support `add_prefix_space`, setting it to True has no effect." + ) + + super().__init__( + errors=errors, + bos_token=bos_token, + eos_token=eos_token, + pad_token=pad_token, + unk_token=unk_token, + clean_up_tokenization_spaces=clean_up_tokenization_spaces, + split_special_tokens=split_special_tokens, + **kwargs, + ) + + @property + def vocab_size(self) -> int: + return len(self.encoder) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.get_vocab + def get_vocab(self): + return dict(self.encoder, **self.added_tokens_encoder) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.bpe + def bpe(self, token): + if token in self.cache: + return self.cache[token] + word = tuple(token) + pairs = get_pairs(word) + + if not pairs: + return token + + while True: + bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float("inf"))) + if bigram not in self.bpe_ranks: + break + first, second = bigram + new_word = [] + i = 0 + while i < len(word): + try: + j = word.index(first, i) + except ValueError: + new_word.extend(word[i:]) + break + else: + new_word.extend(word[i:j]) + i = j + + if word[i] == first and i < len(word) - 1 and word[i + 1] == second: + new_word.append(first + second) + i += 2 + else: + new_word.append(word[i]) + i += 1 + new_word = tuple(new_word) + word = new_word + if len(word) == 1: + break + else: + pairs = get_pairs(word) + word = " ".join(word) + self.cache[token] = word + return word + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._tokenize + def _tokenize(self, text): + """Tokenize a string.""" + bpe_tokens = [] + for token in re.findall(self.pat, text): + token = "".join( + self.byte_encoder[b] for b in token.encode("utf-8") + ) # Maps all our bytes to unicode strings, avoiding control tokens of the BPE (spaces in our case) + bpe_tokens.extend(bpe_token for bpe_token in self.bpe(token).split(" ")) + return bpe_tokens + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._convert_token_to_id + def _convert_token_to_id(self, token): + """Converts a token (str) in an id using the vocab.""" + return self.encoder.get(token, self.encoder.get(self.unk_token)) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._convert_id_to_token + def _convert_id_to_token(self, index): + """Converts an index (integer) in a token (str) using the vocab.""" + return self.decoder.get(index) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.convert_tokens_to_string + def convert_tokens_to_string(self, tokens): + """Converts a sequence of tokens (string) in a single string.""" + text = "".join(tokens) + text = bytearray([self.byte_decoder[c] for c in text]).decode("utf-8", errors=self.errors) + return text + + def decode( + self, + token_ids, + skip_special_tokens: bool = False, + clean_up_tokenization_spaces: Optional[bool] = False, + spaces_between_special_tokens: bool = False, + **kwargs, + ) -> str: + # `spaces_between_special_tokens` defaults to True for _decode in slow tokenizers + # and cannot be configured elsewhere, but it should default to False for Qwen2Tokenizer + return super().decode( + token_ids, + skip_special_tokens=skip_special_tokens, + clean_up_tokenization_spaces=clean_up_tokenization_spaces, + spaces_between_special_tokens=spaces_between_special_tokens, + **kwargs, + ) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.save_vocabulary + def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]: + if not os.path.isdir(save_directory): + logger.error(f"Vocabulary path ({save_directory}) should be a directory") + return + vocab_file = os.path.join( + save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab_file"] + ) + merge_file = os.path.join( + save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["merges_file"] + ) + + with open(vocab_file, "w", encoding="utf-8") as f: + f.write(json.dumps(self.encoder, indent=2, sort_keys=True, ensure_ascii=False) + "\n") + + index = 0 + with open(merge_file, "w", encoding="utf-8") as writer: + writer.write("#version: 0.2\n") + for bpe_tokens, token_index in sorted(self.bpe_ranks.items(), key=lambda kv: kv[1]): + if index != token_index: + logger.warning( + f"Saving vocabulary to {merge_file}: BPE merge indices are not consecutive." + " Please check that the tokenizer is not corrupted!" + ) + index = token_index + writer.write(" ".join(bpe_tokens) + "\n") + index += 1 + + return vocab_file, merge_file + + def prepare_for_tokenization(self, text, **kwargs): + text = unicodedata.normalize("NFC", text) + return (text, kwargs) diff --git a/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py b/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py new file mode 100644 index 00000000..a6b8c01d --- /dev/null +++ b/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py @@ -0,0 +1,1680 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved. + +import os +import time +from collections.abc import Callable, Generator, Mapping, Sequence +from contextlib import contextmanager, nullcontext + +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange + +from cosmos3._src.imaginaire.flags import DEVICE, INTERNAL, TRAINING +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.distributed import get_rank, sync_model_states +from cosmos3._src.imaginaire.utils.easy_io import easy_io +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO +from cosmos3._src.vfm.tokenizers.interface import VideoTokenizerInterface +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution + +# For sequential decoding, CACHE_T is the number of frames to cache. +CACHE_T = 2 + + +def _contiguous_clone(t: torch.Tensor) -> torch.Tensor: + """Return a contiguous copy of *t* using exactly one allocation. + + When *t* is already contiguous, ``.contiguous()`` would be a no-op that + returns the *same* tensor (sharing storage), so we need ``.clone()``. + When *t* is non-contiguous, ``.contiguous()`` already allocates a fresh + tensor with independent storage — no extra ``.clone()`` needed. + """ + if t.is_contiguous(): + return t.clone() + return t.contiguous() + + +def _update_cache_and_apply( + x: torch.Tensor, + layer: "CausalConv3d", + feat_cache: list, + feat_idx: list[int], +) -> torch.Tensor: + """Apply a CausalConv3d with temporal cache management. + + Saves the last CACHE_T frames of ``x`` as the new cache entry and, + when the current chunk has fewer than 2 frames, prepends the last + cached frame so the cache always spans 2 frames. + + Note that feat_idx is a list with a single element, which stores + the index of the current CausalConv3d layer. List is used here so + feat_idx can be mutated in place, and the caller can pass in a reference + to the list. + """ + idx = feat_idx[0] + cache_x = _contiguous_clone(x[:, :, -CACHE_T:, :, :]) + if cache_x.shape[2] < 2 and feat_cache[idx] is not None: + cache_x = torch.cat( + [ + feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), + cache_x, + ], + dim=2, + ) # [B,C,2,H,W] + x = layer(x, feat_cache[idx]) + feat_cache[idx] = cache_x + feat_idx[0] += 1 + return x + + +class CausalConv3d(nn.Conv3d): + """ + Causal 3d convolution. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._padding = ( + self.padding[2], + self.padding[2], + self.padding[1], + self.padding[1], + 2 * self.padding[0], + 0, + ) + self.padding = (0, 0, 0) + + def forward(self, x, cache_x=None): # x: [B,C,T,H,W] + padding = list(self._padding) + if cache_x is not None and self._padding[4] > 0: + cache_x = cache_x.to(x.device) + x = torch.cat([cache_x, x], dim=2) # [B,C,T+cache_T,H,W] + padding[4] -= cache_x.shape[2] + x = F.pad(x, padding) # [B,C,T_padded,H_padded,W_padded] + + return super().forward(x) # [B,out_C,T_out,H_out,W_out] + + +class RMS_norm(nn.Module): + def __init__(self, dim, channel_first=True, images=True, bias=False): + super().__init__() + broadcastable_dims = (1, 1, 1) if not images else (1, 1) + shape = (dim, *broadcastable_dims) if channel_first else (dim,) + + self.channel_first = channel_first + self.scale = dim**0.5 + self.gamma = nn.Parameter(torch.ones(shape)) + self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0 + + def forward(self, x): + return F.normalize(x, dim=(1 if self.channel_first else -1)) * self.scale * self.gamma + self.bias + + +class Upsample(nn.Upsample): + def forward(self, x): + """ + Fix bfloat16 support for nearest neighbor interpolation. + """ + return super().forward(x.float()).type_as(x) + + +class Resample(nn.Module): + def __init__(self, dim, mode): + assert mode in ( + "none", + "upsample2d", + "upsample3d", + "downsample2d", + "downsample3d", + ) + super().__init__() + self.dim = dim + self.mode = mode + + # layers + if mode == "upsample2d": + self.resample = nn.Sequential( + Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"), + nn.Conv2d(dim, dim, 3, padding=1), + ) + elif mode == "upsample3d": + self.resample = nn.Sequential( + Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"), + nn.Conv2d(dim, dim, 3, padding=1), + ) + self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0)) + elif mode == "downsample2d": + self.resample = nn.Sequential(nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))) + elif mode == "downsample3d": + self.resample = nn.Sequential(nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))) + self.time_conv = CausalConv3d(dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)) + else: + self.resample = nn.Identity() + + def forward(self, x, feat_cache=None, feat_idx=[0]): # x: [B,C,T,H,W] + b, c, t, h, w = x.size() + if self.mode == "upsample3d": + if feat_cache is not None: + idx = feat_idx[0] + if feat_cache[idx] is None: + # First frame: skip time_conv, seed cache with zeros so the next call sees a real tensor + feat_cache[idx] = torch.zeros(b, c, CACHE_T, h, w, device=x.device, dtype=x.dtype) # [B,C,2,H,W] + feat_idx[0] += 1 + else: + cache_x = _contiguous_clone(x[:, :, -CACHE_T:, :, :]) # [B,C,<=2,H,W] + if cache_x.shape[2] < 2: + cache_x = torch.cat( + [ + feat_cache[idx][:, :, -1, :, :].unsqueeze(2), + cache_x, + ], + dim=2, + ) # [B,C,2,H,W] + x = self.time_conv(x, feat_cache[idx]) # [B,C*2,T,H,W] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + x = x.reshape(b, 2, c, t, h, w) # [B,2,C,T,H,W] + x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3) # [B,C,T,2,H,W] + x = x.reshape(b, c, t * 2, h, w) # [B,C,T*2,H,W] + t = x.shape[2] + x = rearrange(x, "b c t h w -> (b t) c h w") # [B*T,C,H,W] + x = self.resample(x) # [B*T,C_out,H_out,W_out] + x = rearrange(x, "(b t) c h w -> b c t h w", t=t) # [B,C_out,T,H_out,W_out] + + if self.mode == "downsample3d": + # Important for torch.compile: when we're *not* doing streaming/cache-based inference + # (feat_cache is None), we still need to apply the temporal downsample conv. + if feat_cache is None: + # `time_conv` has kernel (3,1,1), stride 2 in time, and no internal temporal padding. + # In the streaming path, we effectively provide left temporal context via cached frames. + # For the non-streaming path, pad 2 frames on the left so: + # - the conv is always valid (T>=3) + # - the output temporal length matches the shortcut path's ceil(T/2) behavior + x = F.pad(x, (0, 0, 0, 0, 2, 0)) # [B,C,T+2,H_out,W_out] + x = self.time_conv(x) # [B,C,T//2+1,H_out,W_out] + else: + idx = feat_idx[0] + if feat_cache[idx] is None: + # First call for this layer in a streaming/windowed pass. + # The baseline streaming path primes caches with a single-frame chunk (T==1), + # where skipping time_conv preserves both correctness and shape alignment. + # + # If this is ever called with T>1 (non-standard chunking), fall back to a padded + # time_conv so the main path stays compatible with the shortcut path. + if x.shape[2] == 1: + feat_cache[idx] = _contiguous_clone(x) + else: + cache_x = _contiguous_clone(x[:, :, -1:, :, :]) # [B,C,1,H_out,W_out] + x_in = F.pad(x, (0, 0, 0, 0, 2, 0)) # [B,C,T+2,H_out,W_out] + x = self.time_conv(x_in) # [B,C,T//2+1,H_out,W_out] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + else: + cache_x = _contiguous_clone(x[:, :, -1:, :, :]) # [B,C,1,H_out,W_out] + x_cat = torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2) # [B,C,T+1,H_out,W_out] + t_cat = x_cat.shape[2] + if t_cat < 3: + x_cat = F.pad(x_cat, (0, 0, 0, 0, 3 - t_cat, 0)) # [B,C,3,H_out,W_out] + x = self.time_conv(x_cat) # [B,C,T//2+1,H_out,W_out] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + return x + + +class ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout=0.0): + super().__init__() + self.in_dim = in_dim + self.out_dim = out_dim + + # layers + self.residual = nn.Sequential( + RMS_norm(in_dim, images=False), + nn.SiLU(), + CausalConv3d(in_dim, out_dim, 3, padding=1), + RMS_norm(out_dim, images=False), + nn.SiLU(), + nn.Dropout(dropout), + CausalConv3d(out_dim, out_dim, 3, padding=1), + ) + self.shortcut = CausalConv3d(in_dim, out_dim, 1) if in_dim != out_dim else nn.Identity() + + def forward(self, x, feat_cache=None, feat_idx=[0]): + h = self.shortcut(x) + for layer in self.residual: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + return x + h + + +class AttentionBlock(nn.Module): + """ + Causal self-attention with a single head. + """ + + def __init__(self, dim): + super().__init__() + self.dim = dim + + # layers + self.norm = RMS_norm(dim) + self.to_qkv = nn.Conv2d(dim, dim * 3, 1) + self.proj = nn.Conv2d(dim, dim, 1) + + # zero out the last layer params + nn.init.zeros_(self.proj.weight) + + def forward(self, x): # x: [B,C,T,H,W] + identity = x + b, c, t, h, w = x.size() + x = rearrange(x, "b c t h w -> (b t) c h w") # [B*T,C,H,W] + x = self.norm(x) # [B*T,C,H,W] + # compute query, key, value + q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3, -1).permute(0, 1, 3, 2).contiguous().chunk(3, dim=-1) + # q,k,v: [B*T,1,H*W,C] + + # apply attention + x = F.scaled_dot_product_attention( + q, + k, + v, + ) # [B*T,1,H*W,C] + x = x.squeeze(1).permute(0, 2, 1).contiguous().reshape(b * t, c, h, w) # [B*T,C,H,W] + + # output + x = self.proj(x) # [B*T,C,H,W] + x = rearrange(x, "(b t) c h w-> b c t h w", t=t) # [B,C,T,H,W] + return x + identity # [B,C,T,H,W] + + +def patchify(x, patch_size): # x: [B,C,H,W] or [B,C,T,H,W] -> [B,C*p^2,H//p,W//p] or [B,C*p^2,T,H//p,W//p] + if patch_size == 1: + return x + # Fast path: patch_size==2 is the only one used in this tokenizer. + # Implement it with pure view/permute/reshape to be maximally torch.compile-friendly. + if patch_size == 2: + if x.dim() == 4: + b, c, h, w = x.shape + x = x.view(b, c, h // 2, 2, w // 2, 2) # [B,C,H//2,2,W//2,2] + x = x.permute(0, 1, 5, 3, 2, 4).contiguous() # [B,C,2,2,H//2,W//2] + return x.view(b, c * 4, h // 2, w // 2) # [B,C*4,H//2,W//2] + if x.dim() == 5: + b, c, f, h, w = x.shape + x = x.view(b, c, f, h // 2, 2, w // 2, 2) # [B,C,T,H//2,2,W//2,2] + x = x.permute(0, 1, 6, 4, 2, 3, 5).contiguous() # [B,C,2,2,T,H//2,W//2] + return x.view(b, c * 4, f, h // 2, w // 2) # [B,C*4,T,H//2,W//2] + if x.dim() == 4: + x = rearrange(x, "b c (h q) (w r) -> b (c r q) h w", q=patch_size, r=patch_size) # [B,C*p^2,H//p,W//p] + elif x.dim() == 5: + x = rearrange( + x, + "b c f (h q) (w r) -> b (c r q) f h w", + q=patch_size, + r=patch_size, + ) # [B,C*p^2,T,H//p,W//p] + else: + raise ValueError(f"Invalid input shape: {x.shape}") + + return x + + +def unpatchify(x, patch_size): # x: [B,C*p^2,H,W] or [B,C*p^2,T,H,W] -> [B,C,H*p,W*p] or [B,C,T,H*p,W*p] + if patch_size == 1: + return x + + if x.dim() == 4: + x = rearrange(x, "b (c r q) h w -> b c (h q) (w r)", q=patch_size, r=patch_size) # [B,C,H*p,W*p] + elif x.dim() == 5: + x = rearrange( + x, + "b (c r q) f h w -> b c f (h q) (w r)", + q=patch_size, + r=patch_size, + ) # [B,C,T,H*p,W*p] + return x + + +class AvgDown3D(nn.Module): + def __init__( + self, + in_channels, + out_channels, + factor_t, + factor_s=1, + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.factor_t = factor_t + self.factor_s = factor_s + self.factor = self.factor_t * self.factor_s * self.factor_s + + assert in_channels * self.factor % out_channels == 0 + self.group_size = in_channels * self.factor // out_channels + + def forward( + self, x: torch.Tensor + ) -> torch.Tensor: # x: [B,C,T,H,W] -> [B,out_channels,T//factor_t,H//factor_s,W//factor_s] + pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t + pad = (0, 0, 0, 0, pad_t, 0) + x = F.pad(x, pad) # [B,C,T_padded,H,W] + B, C, T, H, W = x.shape + x = x.view( + B, + C, + T // self.factor_t, + self.factor_t, + H // self.factor_s, + self.factor_s, + W // self.factor_s, + self.factor_s, + ) # [B,C,T//ft,ft,H//fs,fs,W//fs,fs] + x = x.permute(0, 1, 3, 5, 7, 2, 4, 6).contiguous() # [B,C,ft,fs,fs,T//ft,H//fs,W//fs] + x = x.view( + B, + C * self.factor, + T // self.factor_t, + H // self.factor_s, + W // self.factor_s, + ) # [B,C*factor,T//ft,H//fs,W//fs] + x = x.view( + B, + self.out_channels, + self.group_size, + T // self.factor_t, + H // self.factor_s, + W // self.factor_s, + ) # [B,out_channels,group_size,T//ft,H//fs,W//fs] + x = x.mean(dim=2) # [B,out_channels,T//ft,H//fs,W//fs] + return x + + +class DupUp3D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + factor_t, + factor_s=1, + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + + self.factor_t = factor_t + self.factor_s = factor_s + self.factor = self.factor_t * self.factor_s * self.factor_s + + assert out_channels * self.factor % in_channels == 0 + self.repeats = out_channels * self.factor // in_channels + + def forward( + self, x: torch.Tensor, first_chunk=False + ) -> torch.Tensor: # x: [B,in_channels,T,H,W] -> [B,out_channels,T*factor_t,H*factor_s,W*factor_s] + x = x.repeat_interleave(self.repeats, dim=1) # [B,in_channels*repeats,T,H,W] + x = x.view( + x.size(0), + self.out_channels, + self.factor_t, + self.factor_s, + self.factor_s, + x.size(2), + x.size(3), + x.size(4), + ) # [B,out_channels,ft,fs,fs,T,H,W] + x = x.permute(0, 1, 5, 2, 6, 3, 7, 4).contiguous() # [B,out_channels,T,ft,H,fs,W,fs] + x = x.view( + x.size(0), + self.out_channels, + x.size(2) * self.factor_t, + x.size(4) * self.factor_s, + x.size(6) * self.factor_s, + ) # [B,out_channels,T*ft,H*fs,W*fs] + if first_chunk: + x = x[:, :, self.factor_t - 1 :, :, :] # [B,out_channels,T*ft-ft+1,H*fs,W*fs] + return x + + +class Down_ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout, mult, temperal_downsample=False, down_flag=False): + super().__init__() + + # Shortcut path with downsample + self.avg_shortcut = AvgDown3D( + in_dim, + out_dim, + factor_t=2 if temperal_downsample else 1, + factor_s=2 if down_flag else 1, + ) + + # Main path with residual blocks and downsample + downsamples = [] + for _ in range(mult): + downsamples.append(ResidualBlock(in_dim, out_dim, dropout)) + in_dim = out_dim + + # Add the final downsample block + if down_flag: + mode = "downsample3d" if temperal_downsample else "downsample2d" + downsamples.append(Resample(out_dim, mode=mode)) + + self.downsamples = nn.Sequential(*downsamples) + + def forward(self, x, feat_cache=None, feat_idx=[0]): + # Avoid cloning the full activation (extra kernel + bandwidth). + # None of the downstream modules are in-place, so taking the shortcut first is safe. + x_shortcut = self.avg_shortcut(x) + for module in self.downsamples: + x = module(x, feat_cache, feat_idx) + + return x + x_shortcut + + +class Up_ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout, mult, temperal_upsample=False, up_flag=False): + super().__init__() + # Shortcut path with upsample + if up_flag: + self.avg_shortcut = DupUp3D( + in_dim, + out_dim, + factor_t=2 if temperal_upsample else 1, + factor_s=2 if up_flag else 1, + ) + else: + self.avg_shortcut = None + + # Main path with residual blocks and upsample + upsamples = [] + for _ in range(mult): + upsamples.append(ResidualBlock(in_dim, out_dim, dropout)) + in_dim = out_dim + + # Add the final upsample block + if up_flag: + mode = "upsample3d" if temperal_upsample else "upsample2d" + upsamples.append(Resample(out_dim, mode=mode)) + + self.upsamples = nn.Sequential(*upsamples) + + def forward(self, x, feat_cache=None, feat_idx=[0], first_chunk=False): + x_shortcut = self.avg_shortcut(x, first_chunk) if self.avg_shortcut is not None else None + for module in self.upsamples: + x = module(x, feat_cache, feat_idx) + if x_shortcut is not None: + return x + x_shortcut + return x + + +class Encoder3d(nn.Module): + def __init__( + self, + dim=128, + z_dim=4, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_downsample=[False, True, True], + dropout=0.0, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_downsample = temperal_downsample + + # dimensions + dims = [dim * u for u in [1] + dim_mult] + scale = 1.0 + + # init block + self.conv1 = CausalConv3d(12, dims[0], 3, padding=1) + + # downsample blocks + downsamples = [] + for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): + t_down_flag = temperal_downsample[i] if i < len(temperal_downsample) else False + downsamples.append( + Down_ResidualBlock( + in_dim=in_dim, + out_dim=out_dim, + dropout=dropout, + mult=num_res_blocks, + temperal_downsample=t_down_flag, + down_flag=i != len(dim_mult) - 1, + ) + ) + scale /= 2.0 + self.downsamples = nn.Sequential(*downsamples) + + # middle blocks + self.middle = nn.Sequential( + ResidualBlock(out_dim, out_dim, dropout), + AttentionBlock(out_dim), + ResidualBlock(out_dim, out_dim, dropout), + ) + + # output blocks + self.head = nn.Sequential( + RMS_norm(out_dim, images=False), + nn.SiLU(), + CausalConv3d(out_dim, z_dim, 3, padding=1), + ) + + def forward(self, x, feat_cache=None): # x: [B,12,T,H//2,W//2] -> [B,z_dim,T//4,H//16,W//16] + feat_idx = [0] + + if feat_cache is not None: + x = _update_cache_and_apply(x, self.conv1, feat_cache, feat_idx) # [B,dim,T,H//2,W//2] + else: + x = self.conv1(x) # [B,dim,T,H//2,W//2] + + # downsamples + for layer in self.downsamples: + if feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T//4,H//16,W//16] + + # middle + for layer in self.middle: + if isinstance(layer, ResidualBlock) and feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T//4,H//16,W//16] + + # head + for layer in self.head: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + + return x # [B,z_dim,T//4,H//16,W//16] + + +class Decoder3d(nn.Module): + def __init__( + self, + dim=128, + z_dim=4, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_upsample=[True, True, False], + dropout=0.0, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_upsample = temperal_upsample + + # dimensions + dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]] + + # init block + self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1) + + # middle blocks + self.middle = nn.Sequential( + ResidualBlock(dims[0], dims[0], dropout), + AttentionBlock(dims[0]), + ResidualBlock(dims[0], dims[0], dropout), + ) + + # upsample blocks + upsamples = [] + for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): + t_up_flag = temperal_upsample[i] if i < len(temperal_upsample) else False + upsamples.append( + Up_ResidualBlock( + in_dim=in_dim, + out_dim=out_dim, + dropout=dropout, + mult=num_res_blocks + 1, + temperal_upsample=t_up_flag, + up_flag=i != len(dim_mult) - 1, + ) + ) + self.upsamples = nn.Sequential(*upsamples) + + # output blocks + self.head = nn.Sequential( + RMS_norm(out_dim, images=False), + nn.SiLU(), + CausalConv3d(out_dim, 12, 3, padding=1), + ) + + def forward(self, x, feat_cache=None, first_chunk=False): # x: [B,z_dim,T,H,W] -> [B,12,T*4,H*8,W*8] + feat_idx = [0] + + if feat_cache is not None: + x = _update_cache_and_apply(x, self.conv1, feat_cache, feat_idx) # [B,dim*dim_mult[-1],T,H,W] + else: + x = self.conv1(x) # [B,dim*dim_mult[-1],T,H,W] + + for layer in self.middle: + if isinstance(layer, ResidualBlock) and feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T,H,W] + + # upsamples + for layer in self.upsamples: + if feat_cache is not None: + x = layer(x, feat_cache, feat_idx, first_chunk) + else: + x = layer(x) + # x: [B,dec_dim,T*4,H*8,W*8] + + # head + for layer in self.head: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + return x # [B,12,T*4,H*8,W*8] + + +def count_conv3d(model: nn.Module) -> int: + return sum(1 for m in model.modules() if isinstance(m, CausalConv3d)) + + +class WanVAE_(nn.Module): + def __init__( + self, + dim=160, + dec_dim=256, + z_dim=48, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_downsample=[False, True, True], + dropout=0.0, + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_downsample = temperal_downsample + self.temperal_upsample = temperal_downsample[::-1] + self.temporal_window = temporal_window + self._encode_exact_durations: set[int] = set(encode_exact_durations or []) + + # modules + self.encoder = Encoder3d( + dim, + z_dim * 2, + dim_mult, + num_res_blocks, + attn_scales, + self.temperal_downsample, + dropout, + ) + self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1) + self.conv2 = CausalConv3d(z_dim, z_dim, 1) + self.decoder = Decoder3d( + dec_dim, + z_dim, + dim_mult, + num_res_blocks, + attn_scales, + self.temperal_upsample, + dropout, + ) + + self._enc_conv_num = count_conv3d(self.encoder) + self._dec_conv_num = count_conv3d(self.decoder) + self._dec_cache: list[torch.Tensor | None] = self._new_dec_cache() + + def _new_enc_cache(self) -> list: + """Fresh per-layer cache for the encoder (one slot per CausalConv3d).""" + return [None] * self._enc_conv_num + + def _new_dec_cache(self) -> list: + """Fresh per-layer cache for the decoder (one slot per CausalConv3d).""" + return [None] * self._dec_conv_num + + def forward(self, x, scale): + mu = self.encode(x, scale) + x_recon = self.decode(mu, scale, clear_decoder_cache=True) + return x_recon, mu + + def _normalize_latent(self, z: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Normalize the latent.""" + assert len(scale) == 2, "scale must be a tuple with two tensors" + s0 = scale[0].view(1, self.z_dim, 1, 1, 1) + s1 = scale[1].view(1, self.z_dim, 1, 1, 1) + return (z - s0) * s1 # [B,z_dim,T,H,W] + + def _denormalize_latent(self, z: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Invert the normalization applied by _encode_features_to_mu.""" + assert len(scale) == 2, "scale must be a tuple with two tensors" + s0 = scale[0].view(1, self.z_dim, 1, 1, 1) + s1 = scale[1].view(1, self.z_dim, 1, 1, 1) + return z / s1 + s0 # [B,z_dim,T,H,W] + + def _encode_chunk_impl( + self, + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + scale: tuple[torch.Tensor, torch.Tensor], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + """Run the encoder on one temporal chunk and normalize the output. + + Defined as an instance method (not a closure inside ``encode``) so + that ``_ChunkEncodeForAOT`` can wrap it for ``torch.export``. + + Note: Since ``feat_cache`` is mutated in-place by the encoder (each + ``CausalConv3d`` layer overwrites its slot), we pass a shallow copy + to preserve the original cache for compilation. + """ + feat_cache = list(feat_cache) + + assert all(c is None or c.is_contiguous() for c in feat_cache) + assert x_chunk.is_contiguous() + + out = self.encoder(x_chunk, feat_cache=feat_cache) + + assert out.is_contiguous() + assert all(c is None or c.is_contiguous() for c in feat_cache) + + # Project encoder features through conv1, split to mu/log_var, and normalize. + mu, _log_var = self.conv1(out).chunk(2, dim=1) + return self._normalize_latent(mu, scale), feat_cache + + def encode(self, x: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Chunked causal encoding that converts pixel-space video to latent space. + + The Wan 2.2 VAE encoder uses causal 3D convolutions, which means each + ``CausalConv3d`` layer needs temporal context from previous frames. For + long videos, running the full sequence at once would create intermediate + tensors of shape ``(B*T, C, H, W)`` that can exceed Triton's int32 + indexing limit. Instead, we split the video into fixed-size temporal + chunks and maintain a per-layer feature cache (``feat_cache``) so each + chunk can access the causal context from previous chunks. + + **Encoding strategy:** + + 1. The first frame is encoded alone (the "key-frame prime") to seed + the causal caches with initial state. + 2. Subsequent frames are processed in chunks of ``temporal_window`` (e.g. + 68 frames). Each chunk reads from and writes to the shared + ``feat_cache`` list, which stores the last ``CACHE_T=2`` frames of + activations per ``CausalConv3d`` layer. + 3. The per-chunk latents are concatenated along the temporal axis. + + **AOT compilation:** + + When ``_aot_chunk_fns`` has been installed (via + :meth:`Wan2pt2VAEInterface.compile_encode`), + each chunk is dispatched to a pre-compiled ``.pt2`` function keyed by + ``(T_chunk, H_patch, W_patch, cache_t)``. Padding ensures every chunk + (except possibly the last, handled by ``should_pad``) has exactly + ``temporal_window`` frames, keeping the set of compiled input shapes small. + + Args: + x: Pixel-space video tensor of shape ``[B, 3, T, H, W]``. + ``T`` must satisfy ``T == 1`` or ``(T - 1) % 4 == 0`` (the + 4x temporal compression constraint). Use ``pad_video_batch`` + to pad to a valid length before calling. + scale: Tuple of ``(mean, inv_std)`` tensors, each of shape + ``[z_dim]``. Used to normalize the latent: ``(z - mean) * inv_std``. + + Returns: + Normalized latent tensor of shape ``[B, z_dim, T_latent, H//16, W//16]`` + where ``T_latent = 1 + (T - 1) // 4``. Always corresponds to the + *original* (unpadded) input length, even when internal padding was applied. + """ + T, H, W = x.shape[2], x.shape[3], x.shape[4] + + # ``temporal_window`` can be a per-resolution mapping (e.g. + # {"256": 68, "480": 32, "720": 16}) from a Hydra/OmegaConf config, + # which arrives as a ``DictConfig`` (not a plain ``dict``). + # Using ``Mapping`` catches both. + if isinstance(self.temporal_window, Mapping): + resolution = get_vision_data_resolution((H, W)) + temporal_window = self.temporal_window[resolution] + else: + temporal_window = self.temporal_window + + assert T == 1 or (T - 1) % 4 == 0, ( + f"Input temporal length must be 4n+1 (got {T}). " + "Use pad_video_batch to pad before encoding, check wan2pt2_vae_4x16x16_test on how to use it." + ) + + # The 4x temporal compression maps T pixel frames → ceil-like latent frames. + # For T=1 (single image), latent_T=1. For T=4n+1, latent_T=n+1. + latent_T = 1 + (T - 1) // 4 + + # Certain short-clip durations (e.g. robotics datasets with T=17) can be + # encoded at their exact length, avoiding the overhead of padding to the + # next chunk boundary. All other lengths are padded so that each chunk + # has exactly ``temporal_window`` frames, giving the compiled function a + # fixed input shape per {resolution, aspect_ratio} bucket. + should_pad = T not in self._encode_exact_durations + + if should_pad: + # Pad T to ``1 + k * temporal_window`` so that after removing the 1-frame + # prime, the remaining frames divide evenly into ``temporal_window``-sized chunks. + T = 1 + ((T - 1 + temporal_window - 1) // temporal_window) * temporal_window + x = F.pad(x, (0, 0, 0, 0, 0, T - x.shape[2])) + + # One cache slot per CausalConv3d layer in the encoder, initially all None. + enc_cache = self._new_enc_cache() + + # Patchify merges each 2×2 spatial patch into the channel dim: + # [B, 3, T, H, W] → [B, 12, T, H//2, W//2]. + x = patchify(x, patch_size=2) + + aot_chunk_fns: dict | None = getattr(self, "_aot_chunk_fns", None) + H_patch, W_patch = x.shape[3], x.shape[4] + + def _run_chunk( + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + """Encode one chunk, through the AOT path or eager fallback. + + If AOT-compiled chunk functions are installed, the function is + looked up by ``(T_chunk, H_patch, W_patch, cache_t)`` where + ``cache_t`` is the minimum temporal extent of the cache tensors + (0 = all-None prime, 1 = post-prime, 2 = steady state). + + Both full-size and remainder chunks (from ``encode_exact_durations``) + are dispatched through the AOT path when a matching compiled + function exists; uncompiled shapes fall back to eager. + """ + is_prime = feat_cache[0] is None + + # Ensure contiguity so AOT-compiled functions receive tensors + # with deterministic strides regardless of the source slice. + x_chunk = x_chunk.contiguous() + + if aot_chunk_fns is not None: + cache_t = 0 if is_prime else feat_cache[0].shape[2] + aot_key = (x_chunk.shape[2], H_patch, W_patch, cache_t) + aot_fn = aot_chunk_fns.get(aot_key) + + if aot_fn is not None: + return aot_fn(x_chunk, feat_cache) + + return self._encode_chunk_impl(x_chunk, feat_cache, scale) + + # --- Chunked encoding loop --- + # Chunk 0: single-frame "key-frame prime" to seed all causal caches. + out, enc_cache = _run_chunk(x[:, :, :1], feat_cache=enc_cache) + outs = [out] + + # Chunks 1..N: process the remaining frames in fixed-size windows. + for start in range(1, T, temporal_window): + x_chunk = x[:, :, start : start + temporal_window] + out, enc_cache = _run_chunk(x_chunk, feat_cache=enc_cache) + outs.append(out) + + final_out = torch.cat(outs, dim=2) if len(outs) > 1 else outs[0] + + # If we padded the input, trim the latent back to the original length. + if should_pad: + final_out = final_out[:, :, :latent_T] + return final_out + + def decode( + self, + z, + scale, + clear_decoder_cache: bool, + ): # z: [B,z_dim,T_latent,H_latent,W_latent] -> [B,3,T,H,W] + if clear_decoder_cache: + self._dec_cache = self._new_dec_cache() + + z = self._denormalize_latent(z, scale) + x = self.conv2(z) # [B,z_dim,T_latent,H_latent,W_latent] + + parts = [] + for i in range(x.shape[2]): + first_chunk = (i == 0) and all(c is None for c in self._dec_cache) + parts.append( + self.decoder( + x[:, :, i : i + 1], + feat_cache=self._dec_cache, + first_chunk=first_chunk, + ) + ) + + if clear_decoder_cache: + self._dec_cache = self._new_dec_cache() + + decoded = unpatchify(torch.cat(parts, dim=2), patch_size=2) # [B,3,T,H,W] + return decoded # [B,3,T,H,W] + + def clear_decoder_cache(self) -> None: + self._dec_cache = self._new_dec_cache() + + +def _video_vae( + pretrained_path=None, + device="cpu", + object_store_credential_path_pretrained="", + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, +): + """ + Autoencoder3d adapted from Wan 2.2. + """ + # init model + with torch.device("meta"): + model = WanVAE_( + temporal_window=temporal_window, + encode_exact_durations=encode_exact_durations, + ) + if not TRAINING: + model.to_empty(device=device) + + if pretrained_path is None: + model.to_empty(device=device) + else: + if get_rank() == 0: + if not INTERNAL: + from cosmos3._src.imaginaire.utils.checkpoint_db import download_checkpoint_v2 + + pretrained_path = download_checkpoint_v2(pretrained_path) + if pretrained_path.startswith("s3://"): + backend_args = { + "backend": "s3", + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + backend_args = None + + ckpt = easy_io.load( + pretrained_path, + backend_args=backend_args, + map_location=device, + ) + + # load checkpoint + log.info(f"loading {pretrained_path}") + model.load_state_dict(ckpt, assign=TRAINING) + else: + model.to_empty(device=device) + sync_model_states(model) + + return model + + +class WanVAE: + def __init__( + self, + z_dim=48, + vae_pth="", + object_store_credential_path_pretrained="", + dtype=torch.bfloat16, + device=DEVICE, + is_amp=True, + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, + ): + self.dtype = dtype + self.device = device + + # Wan 2.2 mean and std values (48 dimensions) + mean = [ + -0.2289, + -0.0052, + -0.1323, + -0.2339, + -0.2799, + 0.0174, + 0.1838, + 0.1557, + -0.1382, + 0.0542, + 0.2813, + 0.0891, + 0.1570, + -0.0098, + 0.0375, + -0.1825, + -0.2246, + -0.1207, + -0.0698, + 0.5109, + 0.2665, + -0.2108, + -0.2158, + 0.2502, + -0.2055, + -0.0322, + 0.1109, + 0.1567, + -0.0729, + 0.0899, + -0.2799, + -0.1230, + -0.0313, + -0.1649, + 0.0117, + 0.0723, + -0.2839, + -0.2083, + -0.0520, + 0.3748, + 0.0152, + 0.1957, + 0.1433, + -0.2944, + 0.3573, + -0.0548, + -0.1681, + -0.0667, + ] + std = [ + 0.4765, + 1.0364, + 0.4514, + 1.1677, + 0.5313, + 0.4990, + 0.4818, + 0.5013, + 0.8158, + 1.0344, + 0.5894, + 1.0901, + 0.6885, + 0.6165, + 0.8454, + 0.4978, + 0.5759, + 0.3523, + 0.7135, + 0.6804, + 0.5833, + 1.4146, + 0.8986, + 0.5659, + 0.7069, + 0.5338, + 0.4889, + 0.4917, + 0.4069, + 0.4999, + 0.6866, + 0.4093, + 0.5709, + 0.6065, + 0.6415, + 0.4944, + 0.5726, + 1.2042, + 0.5458, + 1.6887, + 0.3971, + 1.0600, + 0.3943, + 0.5537, + 0.5444, + 0.4089, + 0.7468, + 0.7744, + ] + + mean = torch.tensor(mean, dtype=dtype, device=device) # [z_dim] + std = torch.tensor(std, dtype=dtype, device=device) # [z_dim] + self.scale = (mean, 1.0 / std) + + # init model + self.model = _video_vae( + pretrained_path=vae_pth, + object_store_credential_path_pretrained=object_store_credential_path_pretrained, + device=device, + temporal_window=temporal_window, + encode_exact_durations=encode_exact_durations, + ) + + self.model = self.model.eval().requires_grad_(False) + self.is_amp = is_amp + if not is_amp: + self.model = self.model.to(dtype=dtype) + self.context = nullcontext() + else: + self.context = torch.amp.autocast("cuda", dtype=dtype) + + def count_param(self) -> int: + return sum(p.numel() for p in self.model.parameters()) + + @torch.no_grad() + def encode(self, videos: torch.Tensor) -> torch.Tensor: + """Encode a batch of videos. + + AOT-compiled chunk functions (if installed on ``self.model`` via + :meth:`Wan2pt2VAEInterface.compile_encode`) + are dispatched from inside ``WanVAE_.encode`` at the per-chunk level, + preserving the chunked encoding loop and temporal padding logic. + + Args: + videos: Tensor of shape ``[B, C, T, H, W]``. + + Returns: + Tensor of shape ``[B, z_dim, T//4, H//16, W//16]``. + """ + in_dtype = videos.dtype + with self.context: + if not self.is_amp: + videos = videos.to(self.dtype) + latent = self.model.encode(videos, self.scale) + latent = latent.to(in_dtype) + return latent + + @torch.no_grad() + def decode(self, zs: torch.Tensor, clear_decoder_cache: bool = True) -> torch.Tensor: + """Decode a batch of latent tensors. + + Args: + zs: Tensor of shape ``[B, z_dim, T, H, W]``. + clear_decoder_cache: Whether to clear the decoder cache between decode calls. + + Returns: + Tensor of shape ``[B, C, T, H, W]``. + """ + in_dtype = zs.dtype + with self.context: + if not self.is_amp: + zs = zs.to(self.dtype) + video_recon = self.model.decode(zs, self.scale, clear_decoder_cache) + video_recon = video_recon.to(in_dtype) + return video_recon + + +# --------------------------------------------------------------------------- +# AOT compilation helpers +# --------------------------------------------------------------------------- + +_ShapeInfo = tuple[int, int, int] # (chunk_frames, H_patch, W_patch) +_AOTChunkKey = tuple[int, int, int, int] # (T_chunk, H_patch, W_patch, cache_t) + + +class _ChunkEncodeForAOT(torch.nn.Module): + """Wrapper around ``WanVAE_._encode_chunk_impl`` for ``torch.export``. + + Absorbs the ``(mean, inv_std)`` scale as registered buffers so the + exported signature is just ``(x_chunk, feat_cache)``. A separate + wrapper instance (and export) is created per ``cache_t`` because the + ``None``-pattern in ``feat_cache`` differs between ``cache_t=0`` + (all ``None``) and ``cache_t>=1`` (some tensors, some ``None``). + """ + + def __init__( + self, + vae_model: torch.nn.Module, + scale_mean: torch.Tensor, + scale_inv_std: torch.Tensor, + ) -> None: + super().__init__() + self.vae = vae_model + self.register_buffer("scale_mean", scale_mean.clone()) + self.register_buffer("scale_inv_std", scale_inv_std.clone()) + + def forward( + self, + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + return self.vae._encode_chunk_impl( + x_chunk, + feat_cache, + (self.scale_mean, self.scale_inv_std), + ) + + +def _collect_warmup_shapes( + tokenizer: "Wan2pt2VAEInterface", + warmup_resolutions: Sequence[str], + aspect_ratio: str | None = None, +) -> list[_ShapeInfo]: + """Return ``[(chunk_frames, H_patch, W_patch), ...]`` for all warmup shapes. + + Expands *warmup_resolutions* into concrete spatial shapes using + ``VIDEO_RES_SIZE_INFO``. Each resolution may have multiple aspect + ratios (e.g. ``"16,9"``, ``"9,16"``, ``"1,1"``); optionally filtered + to a single ratio via *aspect_ratio*. ``chunk_frames`` is looked up + from the tokenizer (scalar or per-resolution dict). Spatial + dimensions are halved to account for patchify (``patch_size=2``). + """ + all_shapes: list[_ShapeInfo] = [] + for res_key in warmup_resolutions: + if res_key not in VIDEO_RES_SIZE_INFO: + raise ValueError(f"Resolution {res_key} not found in VIDEO_RES_SIZE_INFO") + + if isinstance(tokenizer.encode_chunk_frames, Mapping): + if res_key not in tokenizer.encode_chunk_frames: + raise ValueError(f"Resolution {res_key} not found in tokenizer.encode_chunk_frames") + + res_dict = VIDEO_RES_SIZE_INFO[res_key] + if aspect_ratio is not None: + if aspect_ratio not in res_dict: + raise ValueError(f"Aspect ratio {aspect_ratio} not found in resolution {res_key}") + res_dict = {aspect_ratio: res_dict[aspect_ratio]} + + for H, W in res_dict.values(): + if isinstance(tokenizer.encode_chunk_frames, Mapping): + chunk_frames = tokenizer.encode_chunk_frames[res_key] + else: + chunk_frames = tokenizer.encode_chunk_frames + + H_patch, W_patch = H // 2, W // 2 + all_shapes.append((chunk_frames, H_patch, W_patch)) + return all_shapes + + +class Wan2pt2VAEInterface(VideoTokenizerInterface): + def __init__( + self, + bucket_name: str = "", + object_store_credential_path_pretrained: str = "", + vae_path: str = "", + chunk_duration: int = 93, + keep_decoder_cache: bool = False, + use_streaming_encode: bool = False, + # Granularity of the encoding chunks. Larger values result in higher TensorCore utilization, + # and lower values result in lower memory usage. To optimize for speed and memory usage, + # use a dictionary of chunk frames, one for each resolution. If a single integer is provided, + # it will be used for all resolutions. + encode_chunk_frames: int | Mapping[str, int] = 4, + # Exact frame durations that get encoded without padding. Useful for short-clip datasets + # (e.g. robotics) where the standard bucketing would inflate the input by many multiples + # (e.g. 17 frames → 69 with encode_chunk_frames=68). Must be a list of integers. + encode_exact_durations: list[int] | None = None, + # Compression factors for spatial and temporal dimensions (4x16x16 tokenizer). + spatial_compression_factor: int = 16, + temporal_compression_factor: int = 4, + # Deprecated arguments. This is kept for backwards compatibility + # with older configurations. + temporal_window: int | None = None, + encode_bucket_multiple: int | None = None, + ): + # Remove temporal_window and encode_bucket_multiple once they have been + # removed from the uploaded HuggingFace checkpoint. + if temporal_window is not None: + log.warning("temporal_window is deprecated; remove it.") + del temporal_window + + if encode_bucket_multiple is not None: + log.warning("encode_bucket_multiple is deprecated; remove it.") + del encode_bucket_multiple + + # Remove special handling for encode_chunk_frames once the uploaded + # HuggingFace checkpoint has been updated to use a dictionary of chunk frames, + # one for each resolution. + if isinstance(encode_chunk_frames, int): + encode_chunk_frames = {"256": 68, "480": 24, "720": 12} + assert isinstance(encode_chunk_frames, Mapping) + + assert all(c % 4 == 0 for c in encode_chunk_frames.values()), "encode_chunk_frames must be a multiple of 4" + + self.chunk_duration = chunk_duration + + vae_path_full = f"s3://{bucket_name}/{vae_path}" + self.model = WanVAE( + dtype=torch.bfloat16, + is_amp=False, + vae_pth=vae_path_full, + object_store_credential_path_pretrained=object_store_credential_path_pretrained, + temporal_window=encode_chunk_frames, + encode_exact_durations=encode_exact_durations, + ) + self.encode_chunk_frames = encode_chunk_frames + self.encode_exact_durations = encode_exact_durations + + # When True, keep the decoder cache between decode calls. + self._keep_decoder_cache = keep_decoder_cache + + # When True, always use the streaming/chunked encode path (correct but typically slower). + self.use_streaming_encode = use_streaming_encode + + self._spatial_compression_factor = spatial_compression_factor + self._temporal_compression_factor = temporal_compression_factor + + @property + def dtype(self) -> torch.dtype: + return self.model.dtype + + def reset_dtype(self) -> None: + pass + + @contextmanager + def use_cached_decoder(self) -> Generator[None, None, None]: + """Enable decoder-cache reuse for sequential decode calls.""" + self.model.model.clear_decoder_cache() + self._keep_decoder_cache = True + try: + yield + finally: + self.model.model.clear_decoder_cache() + self._keep_decoder_cache = False + + def encode(self, state: torch.Tensor) -> torch.Tensor: + """Encode a batch of videos. + + Args: + state: Tensor of shape ``[B, C, T, H, W]``. + + Returns: + Tensor of shape ``[B, z_dim, T//4, H//16, W//16]``. + """ + return self.model.encode(state) + + def decode(self, latent: torch.Tensor) -> torch.Tensor: + """Decode a batch of latent tensors. + + Args: + latent: Tensor of shape ``[B, z_dim, T, H, W]``. + + Returns: + Tensor of shape ``[B, C, T, H, W]``. + """ + return self.model.decode( + latent, + clear_decoder_cache=not self._keep_decoder_cache, + ) # [B,3,T,H,W] + + @torch.no_grad() + def compile_encode( + self, + warmup_resolutions: Sequence[str], + output_dir: str, + aspect_ratio: str | None = None, + ) -> None: + """AOT-compile the tokenizer's chunk-level encode for every resolution. + + Compiles ``WanVAE_._encode_chunk_impl`` for each + ``(resolution, aspect_ratio, cache_t)`` variant, producing ``.pt2`` + packages that are loaded on all ranks for zero-overhead dispatch + during training. + + **Variant enumeration** — for each resolution, three standard + ``cache_t`` variants (prime, post-prime, steady-state) are compiled. + When ``encode_exact_durations`` is configured, additional remainder + variants are appended. + + **Distribution** — individual variants are assigned round-robin + across ranks. Reference caches are built lazily. + + **Shared weights** — packages are compiled with + ``package_constants_in_so=False``. After loading, each runner + receives the same encoder weights via + ``load_constants(user_managed=True)``. + + Compiled functions are installed as ``self.model.model._aot_chunk_fns`` + for dispatch by ``_run_chunk`` inside ``WanVAE_.encode``. + + Args: + warmup_resolutions: Resolution keys (e.g. ``["256", "480", "720"]``). + output_dir: Root directory under which compiled ``.pt2`` packages + are written (an ``aot_tokenizer/`` subdirectory will be + created). Typically the job's local output path + (``config.job.path_local``). + aspect_ratio: If given, only compile this single aspect ratio per + resolution instead of all available ratios. + """ + import torch._inductor + import torch.distributed as dist + + log.info(f"AOT chunk-level warmup for resolutions: {warmup_resolutions}", rank0_only=False) + start_time = time.time() + + save_dir = os.path.join(output_dir, "aot_tokenizer") + + all_shapes = _collect_warmup_shapes(self, warmup_resolutions, aspect_ratio) + + is_distributed = dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1 + rank = dist.get_rank() if is_distributed else 0 + world_size = dist.get_world_size() if is_distributed else 1 + + wanvae = self.model # WanVAE (plain class) + wanvae_model = wanvae.model # WanVAE_ (nn.Module) + scale = wanvae.scale # (mean, 1/std) + n_cache_slots = wanvae_model._enc_conv_num + + if rank == 0: + log.info(f"Saving AOT compiled packages to {save_dir}") + os.makedirs(save_dir, exist_ok=True) + if is_distributed: + dist.barrier() + + # -- Helper functions -------------------------------------------------- + + def _rand_cache(cache: list[torch.Tensor | None]) -> list[torch.Tensor | None]: + return [torch.rand_like(c) if c is not None else None for c in cache] + + def _rand_input(t: int, h: int, w: int) -> torch.Tensor: + return torch.rand((1, 12, t, h, w), dtype=torch.bfloat16, device="cuda") + + def _compile_variant( + wrapper: _ChunkEncodeForAOT, + aot_key: _AOTChunkKey, + ref_cache: list[torch.Tensor | None], + ) -> str | None: + """Export + compile one variant, returning the .pt2 path or None.""" + t_chunk, H_patch, W_patch, cache_t = aot_key + pkg_name = f"chunk_ct{cache_t}_{t_chunk}f_{H_patch}x{W_patch}.pt2" + pkg_path = os.path.join(save_dir, pkg_name) + + if os.path.exists(pkg_path): + log.info(f"Rank {rank}: reusing cached {pkg_name}", rank0_only=False) + return pkg_path + + t0 = time.time() + try: + exported = torch.export.export( + wrapper, + (_rand_input(t_chunk, H_patch, W_patch), _rand_cache(ref_cache)), + strict=False, + ) + torch._inductor.aoti_compile_and_package( + exported, + package_path=pkg_path, + inductor_configs={"aot_inductor.package_constants_in_so": False}, + ) + log.info( + f"Rank {rank}: AOT compiled cache_t={cache_t} " + f"{t_chunk}f {H_patch}x{W_patch} in {time.time() - t0:.1f}s", + rank0_only=False, + ) + return pkg_path + except Exception as e: + log.warning( + f"Rank {rank}: AOT compile failed for cache_t={cache_t} {t_chunk}f {H_patch}x{W_patch}: {e}", + rank0_only=False, + ) + return None + + # -- Enumerate all variant keys and distribute across ranks ------------ + + all_variant_keys: list[tuple[_AOTChunkKey, _ShapeInfo]] = [] + seen_keys: set[_AOTChunkKey] = set() + for chunk_frames, H_patch, W_patch in all_shapes: + for cache_t in (0, 1, 2): + t_chunk = 1 if cache_t == 0 else chunk_frames + aot_key: _AOTChunkKey = (t_chunk, H_patch, W_patch, cache_t) + + assert aot_key not in seen_keys, f"Duplicate AOT key: {aot_key}" + seen_keys.add(aot_key) + all_variant_keys.append((aot_key, (chunk_frames, H_patch, W_patch))) + + for T in sorted(self.encode_exact_durations or []): + remaining = T - 1 + if remaining <= 0: + continue + remainder = remaining % chunk_frames + if remainder == 0: + continue + n_full = remaining // chunk_frames + cache_t = 1 if n_full == 0 else 2 + aot_key = (remainder, H_patch, W_patch, cache_t) + + if aot_key not in seen_keys: + seen_keys.add(aot_key) + all_variant_keys.append((aot_key, (chunk_frames, H_patch, W_patch))) + + my_variant_keys = [v for i, v in enumerate(all_variant_keys) if i % world_size == rank] + log.info( + f"Rank {rank}: assigned {len(my_variant_keys)}/{len(all_variant_keys)} variants (world_size={world_size})", + rank0_only=False, + ) + + # -- Build reference caches lazily, only for this rank's shapes -------- + + wrapper = _ChunkEncodeForAOT(wanvae_model, scale[0], scale[1]) + wrapper.eval() + + def _get_ref_caches( + chunk_frames: int, + H_patch: int, + W_patch: int, + ) -> dict[int, list[torch.Tensor | None]]: + cache_ct0: list[torch.Tensor | None] = [None] * n_cache_slots + _, cache_ct1 = wanvae_model._encode_chunk_impl( + _rand_input(1, H_patch, W_patch), + list(cache_ct0), + scale, + ) + _, cache_ct2 = wanvae_model._encode_chunk_impl( + _rand_input(chunk_frames, H_patch, W_patch), + list(cache_ct1), + scale, + ) + return {0: cache_ct0, 1: cache_ct1, 2: cache_ct2} + + ref_cache_map: dict[_ShapeInfo, dict[int, list[torch.Tensor | None]]] = {} + + my_pkg_paths: dict[_AOTChunkKey, str] = {} + for aot_key, shape_info in my_variant_keys: + cache_t = aot_key[3] + if shape_info not in ref_cache_map: + ref_cache_map[shape_info] = _get_ref_caches(*shape_info) + ref_cache = ref_cache_map[shape_info][cache_t] + pkg_path = _compile_variant(wrapper, aot_key, ref_cache) + if pkg_path is not None: + my_pkg_paths[aot_key] = pkg_path + + # -- Gather .pt2 paths from every rank so all ranks can load all variants. + if is_distributed: + gathered: list[dict[_AOTChunkKey, str] | None] = [None] * world_size + dist.all_gather_object(gathered, my_pkg_paths) + pkg_paths: dict[_AOTChunkKey, str] = {} + for rank_paths in gathered: + if rank_paths: + pkg_paths.update(rank_paths) + dist.barrier() + else: + pkg_paths = my_pkg_paths + + # -- Load every .pt2 package and bind to the existing encoder weights. -- + device_index = torch.cuda.current_device() + state_dict = wrapper.state_dict() + + loaded_fns: dict[_AOTChunkKey, Callable] = {} + for key, pkg_path in pkg_paths.items(): + try: + fn = torch._inductor.aoti_load_package(pkg_path, device_index=device_index) + + required_keys = set(fn.get_constant_fqns()) + constants_map = {k: v for k, v in state_dict.items() if k in required_keys} + fn.load_constants(constants_map, check_full_update=True, user_managed=True) + + loaded_fns[key] = fn + except Exception as e: + log.warning( + f"Rank {rank}: failed to load {pkg_path}: {e}", + rank0_only=False, + ) + + wanvae_model._aot_chunk_fns = loaded_fns + + log.info( + f"Rank {rank}: AOT compiled {len(my_pkg_paths)}, " + f"loaded {len(loaded_fns)}/{len(all_variant_keys)} chunk variants, " + f"time: {time.time() - start_time:.2f}s", + rank0_only=False, + ) + + # Clean up .pt2 files so stale packages don't persist across restarts. + if is_distributed: + dist.barrier() + if rank == 0: + import shutil + + try: + shutil.rmtree(save_dir) + log.info(f"Cleaned up AOT cache dir: {save_dir}") + except OSError as e: + log.warning(f"Failed to clean AOT cache dir {save_dir}: {e}") + + if not loaded_fns: + raise RuntimeError("AOT compilation produced no loadable functions") + + def get_latent_num_frames(self, num_pixel_frames: int) -> int: + return 1 + (num_pixel_frames - 1) // 4 + + def get_pixel_num_frames(self, num_latent_frames: int) -> int: + return (num_latent_frames - 1) * 4 + 1 + + @property + def spatial_compression_factor(self) -> int: + return self._spatial_compression_factor + + @property + def temporal_compression_factor(self) -> int: + return self._temporal_compression_factor + + @property + def pixel_chunk_duration(self) -> int: + return self.chunk_duration + + @property + def latent_chunk_duration(self) -> int: + return self.get_latent_num_frames(self.chunk_duration) + + @property + def latent_ch(self) -> int: + return 48 + + @property + def spatial_resolution(self) -> int: + return 512 + + @property + def name(self) -> str: + return "wan2pt2_tokenizer" diff --git a/cosmos3/_src/vfm/utils/__init__.py b/cosmos3/_src/vfm/utils/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/utils/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/utils/context_parallel.py b/cosmos3/_src/vfm/utils/context_parallel.py new file mode 100644 index 00000000..b687bd58 --- /dev/null +++ b/cosmos3/_src/vfm/utils/context_parallel.py @@ -0,0 +1,193 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +from torch import Tensor +from torch.distributed import ProcessGroup, all_gather, broadcast_object_list, get_process_group_ranks, get_world_size +from torch.distributed.utils import _verify_param_shape_across_processes + +from cosmos3._src.imaginaire.utils import distributed + + +def split_inputs_cp(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Split input tensor along the sequence dimension for checkpoint parallelism. + + This function divides the input tensor into equal parts along the specified + sequence dimension, based on the number of ranks in the checkpoint parallelism group. + It then selects the part corresponding to the current rank. + + Args: + x: Input tensor to be split. + seq_dim: The dimension along which to split the input (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A slice of the input tensor corresponding to the current rank. + + Raises: + AssertionError: If the sequence dimension is not divisible by the number of ranks. + """ + cp_ranks = get_process_group_ranks(cp_group) + cp_size = len(cp_ranks) + + assert x.shape[seq_dim] % cp_size == 0, f"{x.shape[seq_dim]} cannot divide cp_size {cp_size}" + x = x.view( + *x.shape[:seq_dim], cp_size, x.shape[seq_dim] // cp_size, *x.shape[(seq_dim + 1) :] + ) # [...,cp_size,S/cp_size,...] + seq_idx = torch.tensor([cp_group.rank()], device=x.device) # [1] + x = x.index_select(seq_dim, seq_idx) # [...,1,S/cp_size,...] + # Note that the new sequence length is the original sequence length / cp_size + x = x.view(*x.shape[:seq_dim], -1, *x.shape[(seq_dim + 2) :]) # [...,S/cp_size,...] + return x + + +def cat_outputs_cp(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Concatenate outputs from different ranks in the checkpoint parallelism group. + + This function gathers tensors from all ranks in the checkpoint parallelism group + and concatenates them along the specified sequence dimension. + + Args: + x: Input tensor to be concatenated. + seq_dim: The dimension along which to concatenate the tensors (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A tensor that is the concatenation of tensors from all ranks in the cp_group. + + Raises: + RuntimeError: If the gather operation fails. + """ + # Get the world size (number of processes in the group) + world_size = get_world_size(cp_group) + + # Create a list to store tensors from all ranks + gathered_tensors = [torch.zeros_like(x) for _ in range(world_size)] + + # Gather tensors from all ranks + try: + all_gather(gathered_tensors, x, group=cp_group) + except RuntimeError as e: + raise RuntimeError(f"Failed to gather tensors: {e}") + + # Concatenate the gathered tensors along the specified dimension + return torch.cat(gathered_tensors, dim=seq_dim) # [...,S*cp_size,...] + + +def cat_outputs_cp_with_grad(x: Tensor, seq_dim: int, cp_group: ProcessGroup) -> Tensor: + """ + Concatenate outputs from different ranks in the context parallelism group. + + This function gathers tensors from all ranks in the checkpoint parallelism group + and concatenates them along the specified sequence dimension. + + It retains computational graph locally for each rank by replacing the portion of the tensor with original output. + + Args: + x: Input tensor to be concatenated. + seq_dim: The dimension along which to concatenate the tensors (sequence dimension). + cp_group: The process group for checkpoint parallelism. + + Returns: + A tensor that is the concatenation of tensors from all ranks in the cp_group. + + Raises: + RuntimeError: If the gather operation fails. + """ + # Get the world size (number of processes in the group) + cp_size = cp_group.size() + assert cp_size > 0, "cp_size should be greater than 0" + + # Create a list to store tensors from all ranks + gathered_tensors = [torch.zeros_like(x) for _ in range(cp_size)] + + # Gather tensors from all ranks + try: + all_gather(gathered_tensors, x, group=cp_group) + except RuntimeError as e: + raise RuntimeError(f"Failed to gather tensors: {e}") + + rank = cp_group.rank() + gathered_tensors[rank] = x + # Concatenate the gathered tensors along the specified dimension + return torch.cat(gathered_tensors, dim=seq_dim) # [...,S*cp_size,...] + + +def robust_broadcast(tensor: torch.Tensor, src: int, pg: ProcessGroup, is_check_shape: bool = False) -> torch.Tensor: + """ + Perform a robust broadcast operation that works regardless of tensor shapes on different ranks. + + Args: + tensor (torch.Tensor): The tensor to broadcast (on src rank) or receive (on other ranks). + src (int): The source rank for the broadcast. Defaults to 0. + + Returns: + torch.Tensor: The broadcasted tensor on all ranks. + """ + # First, broadcast the shape of the tensor + if distributed.get_rank() == src: + shape = torch.tensor(tensor.shape, dtype=torch.long).cuda() # [ndim] + else: + shape = torch.empty(tensor.dim(), dtype=torch.long).cuda() # [ndim] + if is_check_shape: + _verify_param_shape_across_processes(pg, [shape]) + torch.distributed.broadcast(shape, src, group=pg) # [ndim] + + # Resize the tensor on non-src ranks if necessary + if distributed.get_rank() != src: + tensor = tensor.new_empty(shape.tolist()).type_as(tensor) + + # Now broadcast the tensor data + torch.distributed.broadcast(tensor, src, group=pg) + + return tensor + + +def broadcast( + item: torch.Tensor | str | None, process_group: Optional[ProcessGroup] = None +) -> torch.Tensor | str | None: + """ + Broadcast the item from the minimum rank in the specified group(s). + """ + if process_group is None: + return item + + min_rank = min(get_process_group_ranks(process_group)) + if isinstance(item, torch.Tensor): # assume the device is cuda + item = robust_broadcast(item, min_rank, process_group) + elif item is not None: + broadcastable_list = [item] + broadcast_object_list(broadcastable_list, min_rank, group=process_group) + item = broadcastable_list[0] + return item + + +def broadcast_split_tensor( + tensor: torch.Tensor, + seq_dim: int, + process_group: Optional[ProcessGroup] = None, +) -> torch.Tensor: + """ + Broadcast the tensor from the minimum rank in the specified group(s). + """ + if tensor is None: + return tensor + min_rank = min(get_process_group_ranks(process_group)) + tensor = robust_broadcast(tensor, min_rank, process_group) + return split_inputs_cp(tensor, seq_dim, process_group) diff --git a/cosmos3/_src/vfm/utils/data_utils.py b/cosmos3/_src/vfm/utils/data_utils.py new file mode 100644 index 00000000..6360378c --- /dev/null +++ b/cosmos3/_src/vfm/utils/data_utils.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Iterable +from typing import Any + +import torch + + +def get_vision_data_resolution(spatial_shape: tuple[int, int]) -> str: + """Determine the resolution string from spatial dimensions. + + Maps the spatial shape (height, width) to a resolution string based on the + minimum dimension. This is used for resolution-dependent shift lookup when + using dict-based shift configuration. + + Args: + spatial_shape: Tuple of (height, width) in pixels. + + Returns: + Resolution string: "256", "480", or "720" based on the minimum dimension. + + Raises: + ValueError: If the minimum dimension exceeds 960 pixels (unsupported resolution). + + Note: + See VIDEO_RES_SIZE_INFO for more details on resolution definitions. + For the current definition of resolution, these conditions are satisfied. + """ + min_dim = min(spatial_shape[0], spatial_shape[1]) + if min_dim <= 256: + return "256" + elif min_dim <= 640: + return "480" + elif min_dim <= 960: + return "720" + else: + raise ValueError(f"Unsupported resolution: {spatial_shape}") + + +def slice_data_batch( + data_batch: dict[str, Any], + start: int, + limit: int, + multi_item_fields: Iterable[str] = ("image", "video", "image_size"), +) -> dict[str, Any]: + """Slice a data batch based on the start and limit indices. + + For most fields, the slice ``[start:limit]`` is applied directly along the + sample dimension. For fields listed in ``multi_item_fields`` (e.g. ``image`` + and ``video``), each sample may contribute multiple visual items that are + concatenated in flat order. In that case, when + ``num_vision_items_per_sample`` is present in ``data_batch``, the slice is + expanded to cover all visual items belonging to the requested samples. + + Example: + ``num_vision_items_per_sample = [2, 2]`` and + ``video = [v1_s1, v2_s1, v1_s2, v2_s2]``. Slicing with + ``start=0, limit=1`` returns ``video = [v1_s1, v2_s1]``. + + Args: + data_batch: The data batch to slice. + start: The start sample index (inclusive). + limit: The end sample index (exclusive). + multi_item_fields: Field names whose values store multiple visual + items per sample concatenated in flat order. Only used when + ``data_batch`` contains ``num_vision_items_per_sample``. + + Returns: + The sliced data batch. + """ + assert start >= 0 and limit > 0, "Start and limit must be positive" + assert start < limit, "Start must be less than limit" + + num_items = data_batch.get("num_vision_items_per_sample") + if num_items is not None: + if isinstance(num_items, torch.Tensor): + num_items_list = num_items.tolist() + else: + num_items_list = list(num_items) + flat_start = sum(num_items_list[:start]) + flat_limit = sum(num_items_list[:limit]) + else: + flat_start, flat_limit = start, limit + + multi_item_fields = set(multi_item_fields) + + sliced_batch = {} + for key, value in data_batch.items(): + if key in multi_item_fields and num_items is not None: + s, e = flat_start, flat_limit + else: + s, e = start, limit + if isinstance(value, torch.Tensor): + sliced_batch[key] = value[s:e] + elif isinstance(value, list): + sliced_batch[key] = value[s:e] + else: + sliced_batch[key] = value + return sliced_batch diff --git a/cosmos3/_src/vfm/utils/dtensor_helper.py b/cosmos3/_src/vfm/utils/dtensor_helper.py new file mode 100644 index 00000000..ff137030 --- /dev/null +++ b/cosmos3/_src/vfm/utils/dtensor_helper.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Any + +import torch + +from cosmos3._src.imaginaire.utils.misc import get_local_tensor_if_DTensor as dt2lt + + +class DTensorFastEmaModelUpdater: + """ + Similar as FastEmaModelUpdater + """ + + def __init__(self): + # Flag to indicate whether the cache is taken or not. Useful to avoid cache overwrite + self.is_cached = False + + def copy_to(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module) -> None: + with torch.no_grad(): + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + dt2lt(tgt_params).data.copy_(dt2lt(src_params).data) + + @torch.no_grad() + def update_average(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module, beta: float = 0.9999) -> None: + target_list = [] + source_list = [] + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + assert tgt_params.dtype == torch.float32, ( + f"EMA model only works in FP32 dtype, got {tgt_params.dtype} instead." + ) + target_list.append(dt2lt(tgt_params)) + source_list.append(dt2lt(src_params).data) + torch._foreach_mul_(target_list, beta) + torch._foreach_add_(target_list, source_list, alpha=1.0 - beta) + + @torch.no_grad() + def cache(self, parameters: Any, is_cpu: bool = False) -> None: + assert self.is_cached is False, "EMA cache is already taken. Did you forget to restore it?" + device = "cpu" if is_cpu else "cuda" + self.collected_params = [dt2lt(param).clone().to(device) for param in parameters] + self.is_cached = True + + @torch.no_grad() + def restore(self, parameters: Any) -> None: + assert self.is_cached, "EMA cache is not taken yet." + for c_param, param in zip(self.collected_params, parameters, strict=False): + dt2lt(param).copy_(c_param.data.type_as(param.data)) + self.collected_params = [] + # Release the cache after we call restore + self.is_cached = False diff --git a/cosmos3/_src/vfm/utils/flash_attn.py b/cosmos3/_src/vfm/utils/flash_attn.py new file mode 100644 index 00000000..541a0cc2 --- /dev/null +++ b/cosmos3/_src/vfm/utils/flash_attn.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Flash attention initialization for the vfm/ unified VLM training path. + +This module replaces `cosmos_rl.policy.kernel.modeling_utils.init_flash_attn_meta` +in VLMModel.__init__, removing the last cosmos_rl import from cosmos3/_src/vfm/. + +Why a stub for the FlashAttnMeta singleton part? + HFModel uses HuggingFace's flash_attention_2 implementation + (attn_implementation="flash_attention_2" in AutoModel.from_config), which calls + flash_attn_func directly via the transformers.modeling_flash_attention_utils path. + This is entirely independent of the cosmos_rl FlashAttnMeta singleton. + Initializing that singleton has no effect on VLM training: the GPU Phase 2 smoke + test (Qwen3-VL-8B-Instruct, 4-GPU FSDP2, 10 iters, loss 1.15→1.11) confirmed + correct gradient flow without the singleton. + +The deterministic flag IS honored here: it sets the standard PyTorch and CuDNN +determinism knobs, which is equivalent to what the old VLM trainer's +configure_training() did. VLMModel.__init__ calls this before _init_vlm(), so +determinism is configured before any compute happens. +""" + +import os + + +def init_flash_attn_meta(deterministic: bool = False) -> None: + """Initialize flash attention for the HFModel (VLM) training path. + + Replaces the cosmos_rl FlashAttnMeta singleton initialization (which is only + relevant to the cosmos_rl model path, not HFModel). Also applies determinism + settings when deterministic=True, matching what the old VLM trainer's + configure_training() did. + + Args: + deterministic: If True, enables PyTorch and CuDNN deterministic modes. + HF flash_attention_2 respects torch.backends.cudnn.deterministic + and torch.use_deterministic_algorithms(). + """ + if deterministic: + import torch + + torch.backends.cudnn.deterministic = True + torch.use_deterministic_algorithms(True, warn_only=True) + # Required for deterministic CuBLAS on CUDA >= 10.2 + os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8") diff --git a/cosmos3/_src/vfm/utils/fused_adam.py b/cosmos3/_src/vfm/utils/fused_adam.py new file mode 100644 index 00000000..cf709399 --- /dev/null +++ b/cosmos3/_src/vfm/utils/fused_adam.py @@ -0,0 +1,385 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import transformer_engine as te +import transformer_engine_torch as tex + +from cosmos3._src.imaginaire.utils import distributed, log +from cosmos3._src.imaginaire.utils.misc import get_local_tensor_if_DTensor + + +class FusedAdam(torch.optim.Optimizer): + """Implements Adam algorithm. + + + This version of fused Adam implements 2 fusions. + + * Fusion of the Adam update's elementwise operations + * A multi-tensor apply launch that batches the elementwise updates applied to all the model's parameters + into one or a few kernel launches. + + :class:`FusedAdam` may be used as a drop-in replacement for ``torch.optim.AdamW``, + or ``torch.optim.Adam`` with ``adam_w_mode=False``:: + + opt = FusedAdam(model.parameters(), lr = ....) + ... + opt.step() + + + .. warning:: + A previous version of :class:`FusedAdam` allowed a number of additional arguments to ``step``. + These additional arguments are now deprecated and unnecessary. + + Adam was been proposed in `Adam: A Method for Stochastic Optimization`_. + + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups. + lr (float, optional): learning rate. (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square. (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability. (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + (default: False) NOT SUPPORTED in FusedAdam! + adam_w_mode (boolean, optional): Apply L2 regularization or weight decay + True for decoupled weight decay(also known as AdamW) (default: True) + capturable (bool, optional): whether to use the version of the optimizer + that can be used with CUDA Graphs. (default: False) + master_weights (bool, optional): whether to maintain FP32 master weights + in the optimizer with FP16 mixed precision training, currently can + only be used with capturable set to True. (default: False) + + .. _Adam - A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__( + self, + params, + lr=1e-3, + bias_correction=True, + betas=(0.9, 0.999), + eps=1e-8, + adam_w_mode=True, + weight_decay=0.0, + amsgrad=False, + capturable=False, + master_weights=False, + ): + if amsgrad: + raise RuntimeError("FusedAdam does not support the AMSGrad variant.") + if master_weights and not capturable: + raise RuntimeError("Master weights is currently only supported with the capturable version.") + + # If the optimizer is capturable then LR should be a tensor (on GPU) + log.info(f"FusedAdam master_weights: {master_weights} capturable: {capturable}") + + lr = torch.tensor(lr, dtype=torch.float32) if capturable else lr + defaults = dict(lr=lr, bias_correction=bias_correction, betas=betas, eps=eps, weight_decay=weight_decay) + super(FusedAdam, self).__init__(params, defaults) + self.adam_w_mode = 1 if adam_w_mode else 0 + + self.capturable = capturable + self.master_weights = master_weights + + self.param_groups_master = None + + if capturable: + for idx, group in enumerate(self.param_groups): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + for item in ["lr"]: + if isinstance(group[item], float): + group[item] = torch.tensor(group[item], dtype=torch.float32) + self.param_groups[idx][item] = group[item].to(device=device) + + self._step_supports_amp_scaling = True + + self._dummy_overflow_buf = torch.tensor([0], dtype=torch.int, device="cuda") + self.multi_tensor_adam = tex.multi_tensor_adam + self.multi_tensor_adam_capturable = tex.multi_tensor_adam_capturable + self.multi_tensor_adam_capturable_master = tex.multi_tensor_adam_capturable_master + + def step(self, closure=None, grads=None, output_params=None, scale=None, grad_norms=None, grad_scaler=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + + The remaining arguments are deprecated, and are only retained (for the moment) for error-checking purposes. + """ + if any(p is not None for p in [grads, output_params, scale, grad_norms]): + raise RuntimeError( + "FusedAdam has been updated. " + "Simply initialize it identically to torch.optim.Adam, and call step() with no arguments." + ) + loss = None + if closure is not None: + loss = closure() + + if self.param_groups_master is None: + # Create full precision master weights + self.param_groups_master = [] + for i, pg in enumerate(self.param_groups): + param_list = pg["params"] + self.param_groups_master.append( + { + # Change related to master weights + "params": [p.clone().detach().float() if self.master_weights else None for p in param_list], + } + ) + + for group, group_master in zip(self.param_groups, self.param_groups_master): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + bias_correction = 1 if "bias_correction" in group and group["bias_correction"] else 0 + beta1, beta2 = group["betas"] + + # assume same step across group now to simplify things + # per parameter step can be easily support by making it tensor, or pass list into kernel + if "step" in group: + if self.capturable: + group["step"] = ( + group["step"].to(device=device) + if isinstance(group["step"], torch.Tensor) + else torch.tensor(group["step"], dtype=torch.int32, device=device) + ) + group["step"] += (self._dummy_overflow_buf != 1).to(torch.int) + else: + group["step"] += 1 + else: + group["step"] = 1 if not self.capturable else torch.tensor([1], dtype=torch.int, device=device) + + if self.capturable: + group["lr"] = ( + group["lr"].to(device=device) + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32, device=device) + ) + + # create lists for multi-tensor apply + g_16, p_16, m_16, v_16 = [], [], [], [] + g_bf, p_bf, m_bf, v_bf = [], [], [], [] + g_32, p_32, m_32, v_32 = [], [], [], [] + p_16_master = [] + p_32_master = [] + bf16_master = [] + + for p, p_master in zip(group["params"], group_master["params"]): + if p.grad is None: + continue + if p.grad.data.is_sparse: + raise RuntimeError( + "FusedAdam does not support sparse gradients, please consider SparseAdam instead" + ) + + state = self.state[p] + # State initialization + if len(state) == 0: + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like(p).float() + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like(p).float() + + if p.dtype == torch.float16: + if self.master_weights: + p_16_master.append(get_local_tensor_if_DTensor(p_master)) + g_16.append(get_local_tensor_if_DTensor(p.grad)) + p_16.append(get_local_tensor_if_DTensor(p)) + m_16.append(get_local_tensor_if_DTensor(state["exp_avg"])) + v_16.append(get_local_tensor_if_DTensor(state["exp_avg_sq"])) + elif p.dtype == torch.bfloat16: + if self.master_weights: + bf16_master.append(get_local_tensor_if_DTensor(p_master)) + g_bf.append(get_local_tensor_if_DTensor(p.grad)) + p_bf.append(get_local_tensor_if_DTensor(p)) + m_bf.append(get_local_tensor_if_DTensor(state["exp_avg"])) + v_bf.append(get_local_tensor_if_DTensor(state["exp_avg_sq"])) + elif p.dtype == torch.float32: + if self.master_weights: + p_32_master.append(get_local_tensor_if_DTensor(p_master)) + g_32.append(get_local_tensor_if_DTensor(p.grad)) + p_32.append(get_local_tensor_if_DTensor(p)) + m_32.append(get_local_tensor_if_DTensor(state["exp_avg"])) + v_32.append(get_local_tensor_if_DTensor(state["exp_avg_sq"])) + else: + raise RuntimeError("FusedAdam only support fp16, bf16 and fp32.") + + # If the optimizer is capturable, then if there's a grad scaler it works + # on the GPU + a different multi_tensor_applier should be called + if self.capturable: + # overflow check of gradients + found_inf = ( + grad_scaler._check_inf_per_device(self)[device] + if grad_scaler is not None + else torch.zeros((1,), device=device) + ) + self._dummy_overflow_buf.copy_(found_inf) + + # get unscale scale factor + scale, inv_scale = None, None + if grad_scaler: + scale = grad_scaler._get_scale_async() + inv_scale = scale.double().reciprocal().float() + else: + scale = torch.ones((1,), device=device, dtype=torch.float32) + inv_scale = torch.ones((1,), device=device, dtype=torch.float32) + + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16, p_16_master] if self.master_weights else [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf, bf16_master] if self.master_weights else [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32, p_32_master] if self.master_weights else [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + else: + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + return loss + + def load_state_dict(self, state_dict): + super().load_state_dict(state_dict) + for group in self.param_groups: + if self.capturable: + group["lr"] = ( + group["lr"].cuda() + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32).cuda() + ) + + if "step" in group: + if self.capturable: + if distributed.get_rank() == 0: + step = ( + group["step"].cuda() + if isinstance(group["step"], torch.Tensor) + else torch.tensor([group["step"]], dtype=torch.int32).cuda() + ) + else: + step = torch.zeros(1, dtype=torch.int32).cuda() + # make it compatible with FSDP optimizer + distributed.broadcast(step, 0) + group["step"] = step + elif isinstance(group["step"], torch.Tensor): + group["step"] = group["step"].item() + for p in group["params"]: + state = self.state[p] + if "exp_avg" in state: + state["exp_avg"] = state["exp_avg"].float() + state["exp_avg_sq"] = state["exp_avg_sq"].float() diff --git a/cosmos3/_src/vfm/utils/loss.py b/cosmos3/_src/vfm/utils/loss.py new file mode 100644 index 00000000..bf1dba75 --- /dev/null +++ b/cosmos3/_src/vfm/utils/loss.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""CE loss for VLM training. + +Ported from cosmos_rl.policy.trainer.llm_trainer.sft_trainer.async_safe_ce +(packages/cosmos-rl/cosmos_rl/policy/trainer/llm_trainer/sft_trainer.py). + +The reduction formula must match async_safe_ce exactly to preserve loss parity +between Phase 0 (cosmos-rl path) and Phase 2 (this module). + +Two reduction paths — determined by cp_group presence: + + CP enabled (cp_group.size() > 1): + Per-rank mean CE loss × loss_scaling_factor. + Rationale: each CP rank sees a different segment of the sequence; computing + a weighted-mean here would require knowing each rank's valid-token count, + which is expensive. The simpler per-rank mean × scaling is consistent with + cosmos-rl's implementation. + + CP disabled (cp_group is None or cp_group.size() == 1): + Sum CE loss / (global_n_valid_tokens + 1e-8) × (num_dp_workers × scaling). + The ×num_dp_workers compensates for FSDP's gradient averaging across DP + ranks, ensuring the effective gradient equals the gradient of the global + mean loss even with unbalanced per-rank token counts. + Reference: async_safe_ce:97-109 in the source file above. +""" + +from __future__ import annotations + +from typing import Optional + +import torch +import torch.distributed as dist +import torch.nn.functional as F + + +def cross_entropy_loss( + logits: torch.Tensor, + labels: torch.Tensor, + loss_scaling_factor: float = 1.0, + dp_group: Optional[dist.ProcessGroup] = None, + cp_group: Optional[dist.ProcessGroup] = None, + ignore_index: int = -100, +) -> torch.Tensor: + """Next-token-prediction CE loss with DP/CP group reduction. + + Matches the behavior of cosmos_rl.policy.trainer.llm_trainer.sft_trainer.async_safe_ce + with the TORCH_CROSS_ENTROPY backend (F.cross_entropy with float32 cast). + + Args: + logits: (B, T, V) float tensor — raw model output before softmax. + labels: (B, T) long tensor — ground-truth token ids. + Positions equal to ignore_index are excluded from the loss. + loss_scaling_factor: scalar multiplied into the returned loss. + dp_group: FSDP data-parallel shard group for loss normalization. + None = no DP reduction (single-GPU or replicate-only). + cp_group: Context-parallel group. If size > 1, use per-rank mean. + None = no CP reduction. + ignore_index: label value to exclude (default -100). + + Returns: + Scalar loss tensor. + """ + # Shift for next-token prediction: predict token[t+1] using hidden state[t]. + # logits[:, :-1] aligns with labels[:, 1:]. + # Reference: async_safe_ce:63-73 (output[:, :-1], target[:, 1:]) + shifted_logits = logits[:, :-1].contiguous().view(-1, logits.size(-1)) + shifted_labels = labels[:, 1:].contiguous().view(-1) + + if cp_group is not None and cp_group.size() > 1: + # CP path: each rank sees a different sequence segment. + # Use simple mean reduction; nan_to_num handles fully-ignored batches. + # Reference: async_safe_ce:74-88 + loss = F.cross_entropy( + shifted_logits.float(), + shifted_labels, + ignore_index=ignore_index, + reduction="mean", + ) + loss = torch.nan_to_num(loss, nan=0.0) + return loss * loss_scaling_factor + + # No-CP path: per-token loss, then normalize over the global valid-token count. + # Reference: async_safe_ce:89-109 + per_token_loss = F.cross_entropy( + shifted_logits.float(), + shifted_labels, + ignore_index=ignore_index, + reduction="none", + ) + n_valid_tokens = (shifted_labels != ignore_index).sum() + num_dp_workers = 1 + if dp_group is not None: + dist.all_reduce(n_valid_tokens, op=dist.ReduceOp.SUM, group=dp_group) + num_dp_workers = dist.get_world_size(group=dp_group) + + loss = per_token_loss.sum() / (n_valid_tokens + 1e-8) * (num_dp_workers * loss_scaling_factor) + return loss diff --git a/cosmos3/_src/vfm/utils/misc.py b/cosmos3/_src/vfm/utils/misc.py new file mode 100644 index 00000000..682718cf --- /dev/null +++ b/cosmos3/_src/vfm/utils/misc.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import lru_cache +from typing import Any, Tuple + +import torch +import torch.nn.functional as F + +from cosmos3._src.imaginaire.utils.s3_utils import ( + download_from_s3_with_cache as download_from_s3_with_cache, +) +from cosmos3._src.imaginaire.utils.s3_utils import ( + load_from_s3_with_cache as load_from_s3_with_cache, +) + + +def disabled_train(self: Any, mode: bool = True) -> Any: + """Overwrite model.train with this function to make sure train/eval mode + does not change anymore.""" + return self + + +def expand_dims_like(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + while x.dim() != y.dim(): + x = x.unsqueeze(-1) # broadcast-compatible shape matching y.dim() + return x + + +@lru_cache(maxsize=10) +def load_negative_prompt_t5(t5_lenght: int = 512) -> Tuple[str, torch.Tensor, torch.Tensor]: + """ + Loads and returns the raw caption and corresponding T5 embeddings for a negative prompt, + along with a mask tensor from an S3 bucket cache. + + The T5 embeddings are truncated or padded to match the specified T5 length. The function + utilizes LRU (Least Recently Used) cache to store up to 10 recent calls for efficiency. + + Parameters: + t5_length (int): The desired length for the T5 embeddings. Defaults to 512. + + Returns: + Tuple[str, torch.Tensor, torch.Tensor]: A tuple containing the raw caption (str), + the T5 embeddings (torch.Tensor) truncated or padded to the specified length, and + a mask tensor (torch.Tensor) indicating the valid positions in the T5 embeddings. + + The embeddings tensor is of shape [L, D] where L is the sequence length up to `t5_length` + and D is the embeddings dimension. The mask is a LongTensor of shape [t5_length] with 1s + at indices corresponding to actual token positions and 0s elsewhere. + """ + negative_prompt_t5 = load_from_s3_with_cache("s3://bucket/edify_image_v4/test_data_batch/negative_prompt.pt") + raw_caption: str = negative_prompt_t5["raw_captions"] + t5_emb_LD: torch.Tensor = negative_prompt_t5["t5_text_embeddings"] # [L,D] + length = t5_emb_LD.shape[0] + mask = torch.LongTensor(t5_lenght).zero_() # [t5_length] + mask[0:length] = 1 + if length < t5_lenght: + t5_emb_LD = F.pad(t5_emb_LD, (0, 0, 0, t5_lenght - length), value=0) # [t5_length,D] + t5_emb_LD = t5_emb_LD[:t5_lenght] # [t5_length,D] + return raw_caption, t5_emb_LD, mask diff --git a/cosmos3/_src/vfm/utils/model_loader.py b/cosmos3/_src/vfm/utils/model_loader.py new file mode 100644 index 00000000..24f17ea0 --- /dev/null +++ b/cosmos3/_src/vfm/utils/model_loader.py @@ -0,0 +1,226 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import os +import os.path as osp +import time +from typing import Any, Optional + +import torch +import torch.distributed.checkpoint as dcp +from torch.distributed.checkpoint.filesystem import FileSystemReader, FileSystemWriter + +from cosmos3._src.imaginaire.checkpointer.s3_filesystem import S3StorageReader +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log, misc +from cosmos3._src.imaginaire.utils.config_helper import get_config_module, override +from cosmos3._src.vfm.checkpointer.dcp import CustomLoadPlanner, CustomSavePlanner, ModelWrapper + +################################################### +# below are the load_model function for inference # +################################################### +# Thus these load_model functions are designed with less dependency. + + +def checkpoint_path_to_cached_path(path: str, cache_rootdir: Optional[str] = None) -> str: + if cache_rootdir is None: + homedir = os.getenv("HOME") or "" + cache_rootdir = osp.join(homedir, ".cache/imaginaire4/checkpoints/") + + if path.startswith("s3://"): + return osp.join(cache_rootdir, path.removeprefix("s3://").split("/", maxsplit=1)[1]) + else: + return path + + +def _load_model( + model: torch.nn.Module, + checkpoint_path: str, + credential_path: str | None, + enable_gcs_patch_in_boto3: bool = False, + load_ema_to_reg: bool = False, + keys_to_skip_loading: list[str] | None = None, +) -> None: + """ + Args: + model: The model to load weights into + checkpoint_path: Path to checkpoint (can be s3 or local path) + credential_path: Path to S3 credentials (can be none if load local) + enable_gcs_patch_in_boto3: Whether to enable GCS patch in boto3 for DCP loading from GCS + load_ema_to_reg: Whether to load EMA weights into the regular (non-EMA) model parameters. + keys_to_skip_loading: List of key substrings to skip when loading from checkpoint. + Useful for loading pretrained checkpoints that are missing certain keys (e.g. action heads). + """ + + log.info(f"Loading model from {checkpoint_path}") + start_time = time.time() + + state_dict = ModelWrapper(model).state_dict() + + if checkpoint_path.startswith("s3://"): + storage_reader = S3StorageReader( + credential_path=credential_path or "", + path=checkpoint_path, + enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3, + ) + else: + storage_reader = FileSystemReader(checkpoint_path) + + load_planner = CustomLoadPlanner( + load_ema_to_reg=load_ema_to_reg, + keys_to_skip_loading=keys_to_skip_loading or [], + ) + + dcp.load( + state_dict=state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + + log.info(f"Successfully loaded model from {checkpoint_path}") + log.info(f"Time taken to load model: {time.time() - start_time:.2f} seconds") + + +def _save_model( + model: torch.nn.Module, + checkpoint_path: str, + save_reg_to_ema: bool = False, +) -> None: + """ + Args: + model: The model to load weights into + checkpoint_path: Path to cached checkpoint (can be s3 or local path) + save_reg_to_ema: Whether to save regular (non-EMA) model parameters to EMA model parameters. + """ + + log.info(f"Saving model to {checkpoint_path}") + start_time = time.time() + + state_dict = ModelWrapper(model).state_dict() + + assert not checkpoint_path.startswith("s3://"), "Cached checkpoint path must be local path" + storage_writer = FileSystemWriter(checkpoint_path) + + save_planner = CustomSavePlanner(save_reg_to_ema=save_reg_to_ema, dedup_save_to_lowest_rank=True) + + dcp.save( + state_dict=state_dict, + storage_writer=storage_writer, + planner=save_planner, + ) + + log.info(f"Successfully saved model to {checkpoint_path}") + log.info(f"Time taken to save model: {time.time() - start_time:.2f} seconds") + + +def load_model_from_checkpoint( + experiment_name: str, + checkpoint_path: Optional[str] = None, + credential_path: Optional[str] = None, + enable_gcs_patch_in_boto3: bool = False, + config_file: str = "cosmos3/_src/vfm/configs/base/config.py", + load_ema_to_reg: bool = False, + parallelism_config: dict[str, Any] = {}, + seed: int = 0, + experiment_opts: list[str] = [], + use_cache_checkpoint: bool = False, + cache_checkpoint_rootdir: Optional[str] = None, + disable_torch_compile: bool = False, + keys_to_skip_loading: list[str] | None = None, +) -> tuple[torch.nn.Module, Any]: + """ + Args: + experiment_name: Experiment name. + checkpoint_path: Path to the checkpoint (local path or s3 URI). + credential_path: Path to credentials file (if required for remote storage). Optional. + enable_gcs_patch_in_boto3: Whether to enable the boto3 patch for GCS S3-compatibility. + config_file: Path to the config file used to construct the experiment/model. + load_ema_to_reg: If True, load EMA weights into the regular (non-EMA) model parameters. + parallelism_config: Dictionary of parallelism configuration options. + seed: Random seed used for initialization (if applicable). + experiment_opts: Extra experiment/config override options. + use_cache_checkpoint: If True, locally save & read remote checkpoints to speed up repeated loads. + Be aware, the default cache path is $HOME/.cache/imaginaire4/checkpoints/ + cache_checkpoint_rootdir: Customizable root directory for checkpoint cache. Optional. + disable_torch_compile: If True, do not use torch.compile even if the experiment enables it. + keys_to_skip_loading: List of key substrings to skip when loading from checkpoint. + Useful for loading pretrained checkpoints that are missing certain keys (e.g. action heads). + + Returns: + The loaded model and config + """ + + # Ensure checkpoint_path is provided + if checkpoint_path is None: + raise ValueError("'checkpoint_path' must be provided.") + + if not checkpoint_path.strip("/").endswith("model"): + checkpoint_path = os.path.join(checkpoint_path, "model") + + config_module = get_config_module(config_file) + config = importlib.import_module(config_module).make_config() + config = override(config, ["--", f"experiment={experiment_name}"] + experiment_opts) + + if parallelism_config is not None: + for key, value in parallelism_config.items(): + if hasattr(config.model.config.parallelism, key): + setattr(config.model.config.parallelism, key, value) + else: + raise ValueError(f"Key {key} not found in config.model.config.parallelism") + + if disable_torch_compile: + config.model.config.parallelism.use_torch_compile = False + + config.model.config.ema.enabled = False + + config.validate() + config.freeze() # type: ignore + + misc.set_random_seed(seed=seed, by_rank=True) + + torch.backends.cudnn.deterministic = config.trainer.cudnn.deterministic + torch.backends.cudnn.benchmark = config.trainer.cudnn.benchmark + + with misc.timer("instantiate model"): + model = instantiate(config.model).cuda() # type: ignore + model.on_train_start() + + checkpoint_cache_path = None + if use_cache_checkpoint: + checkpoint_cache_path = checkpoint_path_to_cached_path(checkpoint_path, cache_checkpoint_rootdir) + + if checkpoint_cache_path is not None and osp.exists(checkpoint_cache_path): + checkpoint_load_path = checkpoint_cache_path + else: + checkpoint_load_path = checkpoint_path + + _load_model( + model, + checkpoint_path=checkpoint_load_path, + credential_path=credential_path, + enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3, + load_ema_to_reg=load_ema_to_reg, + keys_to_skip_loading=keys_to_skip_loading, + ) + + if checkpoint_cache_path is not None and not osp.exists(checkpoint_cache_path): + _save_model( + model, + checkpoint_path=checkpoint_cache_path, + save_reg_to_ema=load_ema_to_reg, + ) + + return model, config diff --git a/cosmos3/_src/vfm/utils/model_weights_stats.py b/cosmos3/_src/vfm/utils/model_weights_stats.py new file mode 100644 index 00000000..4b5c2669 --- /dev/null +++ b/cosmos3/_src/vfm/utils/model_weights_stats.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +import torch +from torch import nn + + +@dataclass +class TrainingStats: + """Data class to hold training statistics.""" + + video_samples: int = 0 + image_samples: int = 0 + iterations: int = 0 + training_hours: float = 0.0 + + +class WeightTrainingStat(nn.Module, ABC): + """Abstract base class for tracking training statistics.""" + + def __init__(self) -> None: + super().__init__() + self._initialize_tracking_buffers() + + def _initialize_tracking_buffers(self) -> None: + """Initialize tracking buffers with default values.""" + tracking_buffers = { + "accum_video_sample_counter": torch.tensor(0, dtype=torch.int64), + "accum_image_sample_counter": torch.tensor(0, dtype=torch.int64), + "accum_iteration": torch.tensor(0, dtype=torch.int64), + "accum_train_in_hours": torch.tensor(0.0, dtype=torch.float32), + } + + for name, tensor in tracking_buffers.items(): + self.register_buffer(name, tensor) + + def get_training_stats(self) -> TrainingStats: + """Return current training statistics.""" + return TrainingStats( + video_samples=self.accum_video_sample_counter.item(), + image_samples=self.accum_image_sample_counter.item(), + iterations=self.accum_iteration.item(), + training_hours=self.accum_train_in_hours.item(), + ) + + @abstractmethod + def forward(self, *args, **kwargs) -> Any: + pass diff --git a/cosmos3/_src/vfm/utils/monkey_patch.py b/cosmos3/_src/vfm/utils/monkey_patch.py new file mode 100644 index 00000000..b94d1e68 --- /dev/null +++ b/cosmos3/_src/vfm/utils/monkey_patch.py @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import importlib + +import torch +import transformers +from transformers.cache_utils import Cache +from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLModel +from transformers.utils.import_utils import is_torchdynamo_compiling + +from cosmos3._src.imaginaire.utils import log + +_EXPECTED_TRANSFORMERS_VERSION = "4.57.1" + + +def patch_qwen3_vl_forward(model): + """Monkey-patch a ``Qwen3VLModel`` **instance's** forward: + **Dummy visual forward for pure-text batches**: Under FSDP, every rank + must call ``self.visual(...)`` each forward step so that collective + all-gather operations stay in sync. When a batch contains only text, + a lightweight dummy image (16x16 zeros) is pushed through the full + ViT -> merger -> deepstack pipeline, then outputs are sliced to ``[0:0]`` + so they carry ``grad_fn`` but contribute no features. + Args: + model: The ``Qwen3VLModel`` instance (i.e. ``model.model.model`` when + the outer model is ``HFModel``). + """ + if transformers.__version__ != _EXPECTED_TRANSFORMERS_VERSION: + raise ValueError(f"monkey patching transformers version {transformers.__version__} is not supported") + + if not isinstance(model, Qwen3VLModel): + raise ValueError(f"Trying to monkey patch a model that is not a Qwen3VLModel instance: {type(model)}") + + # Resolve the output dataclass from the actual runtime module + model_module = importlib.import_module(type(model).__module__) + Qwen3VLModelOutputWithPast = getattr(model_module, "Qwen3VLModelOutputWithPast") + + # Replaces Qwen3VLModel.forward from: + # transformers.models.qwen3_vl.modeling_qwen3_vl (transformers v4.57.1) + def patched_forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: torch.Tensor | None = None, + position_ids: torch.LongTensor | None = None, + past_key_values: Cache | None = None, + inputs_embeds: torch.FloatTensor | None = None, + pixel_values: torch.Tensor | None = None, + pixel_values_videos: torch.FloatTensor | None = None, + image_grid_thw: torch.LongTensor | None = None, + video_grid_thw: torch.LongTensor | None = None, + cache_position: torch.LongTensor | None = None, + **kwargs, + ): + r""" + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + image_mask = None + video_mask = None + + if pixel_values is not None: + image_embeds, deepstack_image_embeds = self.get_image_features( + pixel_values, image_grid_thw + ) # list of (T, C) tensors per batch element + image_embeds = torch.cat(image_embeds, dim=0).to( + inputs_embeds.device, inputs_embeds.dtype + ) # concat along token dim across the batch + image_mask, _ = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) + + if pixel_values_videos is not None: + video_embeds, deepstack_video_embeds = self.get_video_features(pixel_values_videos, video_grid_thw) + video_embeds = torch.cat(video_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype) + _, video_mask = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, video_features=video_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) + + # Dummy visual forward for text-only data + if pixel_values is None and pixel_values_videos is None: + dummy_h, dummy_w = 16, 16 + dummy_pixels = torch.zeros( + dummy_h * dummy_w, + self.visual.config.temporal_patch_size * self.visual.config.patch_size**2 * 3, + device=inputs_embeds.device, + dtype=self.visual.dtype, + ) + dummy_thw = torch.tensor([[1, dummy_h, dummy_w]], device=inputs_embeds.device) + image_embeds, deepstack_image_embeds = self.get_image_features(dummy_pixels, dummy_thw) + image_embeds = [e[0:0] for e in image_embeds] + deepstack_image_embeds = [e[0:0] for e in deepstack_image_embeds] + + # no-op to mask scatter empty embeddings into inputs to preserve computation graph + image_embeds = torch.cat(image_embeds, dim=0).to( + inputs_embeds.device, inputs_embeds.dtype + ) # concat along token dim across the batch + image_mask, _ = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) + + visual_pos_masks = None + deepstack_visual_embeds = None + if image_mask is not None and video_mask is not None: + # aggregate visual_pos_masks and deepstack_visual_embeds + image_mask = image_mask[..., 0] + video_mask = video_mask[..., 0] + visual_pos_masks = image_mask | video_mask + deepstack_visual_embeds = [] + image_mask_joint = image_mask[visual_pos_masks] + video_mask_joint = video_mask[visual_pos_masks] + for img_embed, vid_embed in zip(deepstack_image_embeds, deepstack_video_embeds): + embed_joint = img_embed.new_zeros(visual_pos_masks.sum(), img_embed.shape[-1]).to(img_embed.device) + embed_joint[image_mask_joint, :] = img_embed + embed_joint[video_mask_joint, :] = vid_embed + deepstack_visual_embeds.append(embed_joint) + elif image_mask is not None: + image_mask = image_mask[..., 0] + visual_pos_masks = image_mask + deepstack_visual_embeds = deepstack_image_embeds + elif video_mask is not None: + video_mask = video_mask[..., 0] + visual_pos_masks = video_mask + deepstack_visual_embeds = deepstack_video_embeds + + if position_ids is None: + attention_mask_tensor = ( + attention_mask if not isinstance(attention_mask, dict) else attention_mask["full_attention"] + ) + if attention_mask_tensor is not None and attention_mask_tensor.ndim == 4: + attention_mask_tensor = torch.diagonal(attention_mask_tensor[:, 0], dim1=1, dim2=2) + # Only apply conversion for floating point tensors (inverted masks) + if attention_mask_tensor.dtype.is_floating_point: + attention_mask_tensor = attention_mask_tensor / torch.finfo(attention_mask_tensor.dtype).min + attention_mask_tensor = (1.0 - attention_mask_tensor).int() + + # Calculate RoPE index once per generation in the pre-fill stage only. + # When compiling, we can't check tensor values thus we check only input length + # It is safe to assume that `length!=1` means we're in pre-fill because compiled + # models currently cannot do asssisted decoding + prefill_compiled_stage = is_torchdynamo_compiling() and ( + (input_ids is not None and input_ids.shape[1] != 1) + or (inputs_embeds is not None and inputs_embeds.shape[1] != 1) + ) + prefill_noncompiled_stage = not is_torchdynamo_compiling() and ( + (cache_position is not None and cache_position[0] == 0) + or (past_key_values is None or past_key_values.get_seq_length() == 0) + ) + if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None: + position_ids, rope_deltas = self.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + attention_mask=attention_mask_tensor, + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) if cache_position is not None else 0 + ) + position_ids = torch.arange(seq_length, device=inputs_embeds.device) + position_ids = position_ids.view(1, -1).expand(batch_size, -1) + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) + position_ids = position_ids.add(delta) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) + + outputs = self.language_model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + visual_pos_masks=visual_pos_masks, + deepstack_visual_embeds=deepstack_visual_embeds, + **kwargs, + ) + + return Qwen3VLModelOutputWithPast( + last_hidden_state=outputs.last_hidden_state, + past_key_values=outputs.past_key_values, + rope_deltas=self.rope_deltas, + ) + + # Replace the forward method + model.forward = patched_forward.__get__(model, type(model)) + log.critical(f"Patched {type(model).__name__} instance forward with pure-text dummy forward") diff --git a/cosmos3/_src/vfm/utils/optimizer.py b/cosmos3/_src/vfm/utils/optimizer.py new file mode 100644 index 00000000..2642fafc --- /dev/null +++ b/cosmos3/_src/vfm/utils/optimizer.py @@ -0,0 +1,426 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import functools +import itertools +import math +from typing import Any, Optional + +import torch +import torch.nn as nn +from omegaconf import DictConfig, ListConfig, OmegaConf +from torch.distributed.checkpoint.state_dict import StateDictOptions, get_optimizer_state_dict, set_optimizer_state_dict +from torch.distributed.checkpoint.stateful import Stateful +from torch.optim.lr_scheduler import LambdaLR + +from cosmos3._src.imaginaire.utils import log + + +def _convert_omegaconf_to_python(obj: Any) -> Any: + """Convert OmegaConf types to plain Python types. + + This is needed because PyTorch's checkpoint utilities don't handle + OmegaConf types like ListConfig and DictConfig. + """ + if isinstance(obj, (ListConfig, DictConfig)): + return OmegaConf.to_container(obj, resolve=True) + return obj + + +def _optimizer_cls( + params: list[nn.Parameter] | list[dict[str, Any]], + optimizer_type: str, + **optimizer_kwargs: Any, +) -> torch.optim.Optimizer: + if optimizer_type.lower() == "adam": + optimizer = torch.optim.Adam(params, **optimizer_kwargs) + elif optimizer_type.lower() == "adamw": + optimizer = torch.optim.AdamW(params, **optimizer_kwargs) + elif optimizer_type.lower() == "fusedadam": + from cosmos3._src.vfm.utils.fused_adam import FusedAdam + + optimizer = FusedAdam(params, capturable=True, master_weights=True, **optimizer_kwargs) + else: + raise NotImplementedError(f"Optimizer {optimizer_type} not found.") + return optimizer + + +def _filter_params(net: nn.Module, keys_to_select: list[str] = []) -> list[nn.Parameter]: + """ + Filter the parameters of the network based on the keys to select. + For the parameters that are not in the keys_to_select, we set requires_grad to False. + """ + total_params = sum(1 for _, _ in net.named_parameters()) + param_dict = {pn: p for pn, p in net.named_parameters() if p.requires_grad} + params_filtered = [] + + if len(keys_to_select) > 0: + for pn, p in param_dict.items(): + if any([key_to_select in pn for key_to_select in keys_to_select]): + params_filtered.append(p) + else: + p.requires_grad = False + else: + params_filtered = list(param_dict.values()) + + log.info( + f"Total parameters: {total_params}, " + f"trainable parameters: {len(params_filtered)}, " + f"frozen parameters: {total_params - len(params_filtered)}, " + f"unselected parameters: {len(param_dict) - len(params_filtered)}" + ) + return params_filtered + + +def _build_optimizer_param_groups( + net: nn.Module, + keys_to_select: list[str], + lr_multipliers: dict[str, float], + base_lr: float, + disable_weight_decay_for_1d_params: bool, +) -> list[dict[str, Any]]: + """Build optimizer parameter groups after applying selection and grouping rules. + + Parameters not matching ``keys_to_select`` are frozen by setting + ``requires_grad=False``. Selected parameters are grouped by matching + ``lr_multipliers`` pattern and each group receives ``base_lr * multiplier``. + If ``disable_weight_decay_for_1d_params`` is true, each LR group is split + into weight-decay and no-weight-decay groups; one-dimensional parameters, + such as norm weights and biases, are assigned ``weight_decay=0.0``. + """ + total_params = sum(1 for _ in net.parameters()) + param_dict = {pn: p for pn, p in net.named_parameters() if p.requires_grad} + + multiplier_groups: dict[float, list[nn.Parameter]] = collections.defaultdict(list) + + for pn, p in param_dict.items(): + if len(keys_to_select) > 0 and not any(key in pn for key in keys_to_select): + p.requires_grad = False + continue + matched_mult = 1.0 + for pattern, mult in lr_multipliers.items(): + if pattern in pn: + matched_mult = mult + break + multiplier_groups[matched_mult].append(p) + + trainable = sum(len(g) for g in multiplier_groups.values()) + log.info( + f"Total parameters: {total_params}, " + f"trainable parameters: {trainable}, " + f"frozen parameters: {total_params - trainable}, " + f"unselected parameters: {len(param_dict) - trainable}" + ) + + optimizer_param_groups: list[dict[str, Any]] = [] + for mult, params in sorted(multiplier_groups.items()): + if not params: + continue + + lr = base_lr * mult + if disable_weight_decay_for_1d_params: + decay_params = [p for p in params if p.dim() >= 2] + no_decay_params = [p for p in params if p.dim() < 2] + param_groups: list[dict[str, Any]] = [] + if decay_params: + param_groups.append({"params": decay_params, "lr": lr}) + if no_decay_params: + param_groups.append({"params": no_decay_params, "lr": lr, "weight_decay": 0.0}) + else: + param_groups = [{"params": params, "lr": lr}] + + optimizer_param_groups.extend(param_groups) + num_tensors_with_weight_decay = sum( + len(group["params"]) for group in param_groups if group.get("weight_decay") != 0.0 + ) + num_tensors_without_weight_decay = sum( + len(group["params"]) for group in param_groups if group.get("weight_decay") == 0.0 + ) + log.info( + f"Param group (lr_mult={mult}x): " + f"{len(params)} tensors, " + f"{sum(p.numel() for p in params)} parameters, " + f"lr={lr}, " + f"num_tensors_with_weight_decay={num_tensors_with_weight_decay}, " + f"num_tensors_without_weight_decay={num_tensors_without_weight_decay}, " + f"disable_weight_decay_for_1d_params={disable_weight_decay_for_1d_params}" + ) + + return optimizer_param_groups + + +def build_optimizer( + model: nn.Module, + optimizer_type: str, + **optimizer_kwargs: Any, +) -> torch.optim.Optimizer: + """Build an optimizer for a model. + + Args: + model: The model to build an optimizer for. + optimizer_type: The type of optimizer to build. + **optimizer_kwargs: Additional keyword arguments to pass to the optimizer. + lr_multipliers: Optional dict mapping parameter name patterns to LR + multipliers. E.g. ``{"sound2llm": 5.0, "llm2sound": 5.0}`` gives those + params 5x the base LR. Unmatched selected params use multiplier 1.0. + disable_weight_decay_for_1d_params: If true, one-dimensional parameters + such as norm weights and biases use weight_decay=0.0. Defaults to + false to preserve the historical optimizer behavior. + + Returns: + A torch.optim.Optimizer. + """ + # Convert OmegaConf types to plain Python types to avoid issues with checkpoint saving. + # PyTorch's _copy_state_dict doesn't handle OmegaConf types like ListConfig. + optimizer_kwargs = {k: _convert_omegaconf_to_python(v) for k, v in optimizer_kwargs.items()} + + fused = optimizer_kwargs.pop("fused", False) + assert fused, "Optimizers with fused=False are not supported." + keys_to_select = optimizer_kwargs.pop("keys_to_select", []) + lr_multipliers: dict[str, float] = optimizer_kwargs.pop("lr_multipliers", {}) + disable_weight_decay_for_1d_params = optimizer_kwargs.pop("disable_weight_decay_for_1d_params", False) + + base_lr = optimizer_kwargs["lr"] + optimizer_param_groups = _build_optimizer_param_groups( + model, + keys_to_select, + lr_multipliers, + base_lr, + disable_weight_decay_for_1d_params, + ) + return _optimizer_cls(optimizer_param_groups, optimizer_type, **optimizer_kwargs) + + +class OptimizersContainer(Stateful): + """Util for calling step/zero_grad on multiple optimizers needed for virtual pipeline stages + and saving/loading optimizer state_dict at checkpoint. + """ + + def __init__( + self, + model_parts: list[nn.Module], + optimizer_type: str, + **optimizer_kwargs: Any, + ) -> None: + self.model_parts = model_parts + self.optimizers = [[] for _ in self.model_parts] + + fused = optimizer_kwargs.pop("fused", False) + keys_to_select = optimizer_kwargs.pop("keys_to_select", []) + + for model_id, model in enumerate(self.model_parts): + filtered_params = _filter_params(model, keys_to_select) + + if fused: + # Group the parameters by device mesh to do optimizer fusion. + parameters_by_mesh = collections.defaultdict(list) + for p in filtered_params: + device_mesh = p.device_mesh if hasattr(p, "device_mesh") else "default" + parameters_by_mesh[device_mesh].append(p) + for params in parameters_by_mesh.values(): + optimizer = _optimizer_cls(params, optimizer_type, **optimizer_kwargs) + self.optimizers[model_id].append(optimizer) + else: + for p in filtered_params: + optimizer = _optimizer_cls([p], optimizer_type, **optimizer_kwargs) + self.optimizers[model_id].append(optimizer) + + def __iter__(self) -> torch.optim.Optimizer: + return iter(itertools.chain(*self.optimizers)) + + def step(self) -> None: + for optimizer in itertools.chain(*self.optimizers): + optimizer.step() + + def zero_grad(self, set_to_none: bool = False) -> None: + for optimizer in itertools.chain(*self.optimizers): + optimizer.zero_grad(set_to_none=set_to_none) + + def state_dict(self) -> dict[str, Any]: + sd = {} + for model, optimizers in zip(self.model_parts, self.optimizers): + sd.update( + get_optimizer_state_dict( + model=model, + optimizers=optimizers, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + ) + return sd + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + for model, optimizers in zip(self.model_parts, self.optimizers): + set_optimizer_state_dict( + model=model, + optimizers=optimizers, + optim_state_dict=state_dict, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + + +# consider split between PP and non-PP +def build_optimizers( + model_parts: list[nn.Module], + optimizer_type: str, + **optimizer_kwargs: dict[str, Any], +) -> OptimizersContainer: + """Wrap one optimizer per model part in an OptimizersContainer which provides a single + step() and zero_grad() method for all the child optimizers. + """ + return OptimizersContainer(model_parts, optimizer_type, **optimizer_kwargs) + + +class SchedulersContainer(Stateful): + """Util for calling step on multiple learning rate schedulers needed for virtual pipeline stages""" + + def __init__(self, optimizers: OptimizersContainer, lr_lambda) -> None: + self.schedulers = [] + for optimizer in optimizers: + self.schedulers.append(LambdaLR(optimizer, lr_lambda=lr_lambda)) + + def step(self) -> None: + for scheduler in self.schedulers: + scheduler.step() + + def state_dict(self) -> dict[str, Any]: + # Currently, we have one scheduler per optimizer. However, when using MultiSchedule PP or optimizer-in-backward, + # there are multiple optimizers and schedulers, but the scheduler state_dict remains the same for all. + # Therefore, we only save the first one and later load it for all. + assert len(self.schedulers) > 0, "Must have at least one scheduler to save state_dict" + return self.schedulers[0].state_dict() + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + # Load the same state_dict for all schedulers. The key value we're concerned with in scheduler.state_dict() is `last_epoch`, + # which is an integer that will be automatically copied. As long as `training.steps` and `training.warmup_iters` remain + # unchanged when resuming from a checkpoint, this approach is safe. We call `.copy()` here to ensure extra safety. + last_epoch = state_dict["last_epoch"] # Extract last known epoch + _step_count = state_dict["_step_count"] + log.info(f"Resuming schedulers by stepping them to last_epoch: {last_epoch}; _step_count: {_step_count}") + + # Manually step all schedulers to match the saved state -- this is a workaround for the inherited issue in the state dict saving (only saved the first scheduler) + # But we have different learning rate for each scheduler, so we need to step them separately instead of loading the state dict + # The benefit of this approach is that we can resume from a checkpoint even if the learning rate is changed + for idx, scheduler in enumerate(self.schedulers): + for step in range(_step_count): + scheduler.step() # Step forward to match previous training state + log.info(f"Scheduler {idx + 1}/{len(self.schedulers)} stepped {_step_count} times.") + log.info(f"Updated learning rate: {scheduler.get_last_lr()}") + + def get_last_lr(self) -> list[float]: + return [scheduler.get_last_lr() for scheduler in self.schedulers] + + +def linear_warmup_linear_decay(warmup_iters: int, decay_steps: int, current_step: int) -> float: + """Computes linear warmup followed by linear decay. + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + """ + if current_step < warmup_iters: + # linear warmup + # 0-indexed step, hence + 1 adjustments + current_step += 1 + curr_adjustment = float(current_step / (warmup_iters + 1)) + + else: + # linear decay + normalized_step = decay_steps - (current_step - warmup_iters) + curr_adjustment = 1 - (decay_steps - normalized_step) / decay_steps + + return curr_adjustment + + +def linear_warmup(warmup_iters: int, current_step: int) -> float: + """Computes linear warmup only + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + """ + if current_step < warmup_iters: + # linear warmup + # 0-indexed step, hence + 1 adjustments + current_step += 1 + curr_adjustment = float(current_step / (warmup_iters + 1)) + else: + curr_adjustment = 1 + + return curr_adjustment + + +def linear_warmup_cosine_cooldown( + warmup_iters: int, cooldown_steps: int, current_step: int, base_lr: float, init_lr: float, end_lr: float +) -> float: + """This scheduler will warmup the learning rate from init_lr to base_lr for warmup_iters, + then decay the learning rate from base_lr to end_lr for cooldown_steps. After cooldown_steps + warmup_iters, + the learning rate will be set to end_lr. + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + + Args: + warmup_iters (int): The number of steps to warmup the learning rate. + cooldown_steps (int): The number of steps to decay the learning rate. + current_step (int): The current step. + base_lr (float): The base learning rate. + init_lr (float): The initial learning rate before warmup. + end_lr (float): The final learning rate after cooldown. + + Returns: + float: The multiplicative factor to adjust the learning rate. + """ + total_steps = warmup_iters + cooldown_steps + + # Normalize + init_multiplier = init_lr / base_lr + end_multiplier = end_lr / base_lr + if current_step <= warmup_iters: + progress = float(current_step / warmup_iters) + return init_multiplier + (1.0 - init_multiplier) * progress + elif current_step <= total_steps: + progress = (current_step - warmup_iters) / cooldown_steps + return end_multiplier + 0.5 * (1.0 - end_multiplier) * (1 + math.cos(math.pi * progress)) + else: + return end_multiplier + + +def build_lr_schedulers( + optimizers: OptimizersContainer, + name: str, + lr: float, + warmup_iters: int, + lr_decay_iters: Optional[int] = None, + init_lr: Optional[float] = None, + end_lr: Optional[float] = None, +) -> SchedulersContainer: + decay_steps = float(max(1, lr_decay_iters - warmup_iters)) if lr_decay_iters is not None else None + if name == "warmup_cosine_lr": + assert init_lr is not None and end_lr is not None, "init_lr and end_lr must be provided for warmup_cosine_lr" + assert lr_decay_iters is not None, "lr_decay_iters must be provided for warmup_cosine_lr" + lr_lambda = functools.partial( + linear_warmup_cosine_cooldown, + warmup_iters, + decay_steps, + base_lr=lr, + init_lr=init_lr, + end_lr=end_lr, + ) + elif name == "lambdalinear": + assert lr_decay_iters is not None, "lr_decay_iters must be provided for lambdalinear" + lr_lambda = functools.partial(linear_warmup_linear_decay, warmup_iters, decay_steps) + else: + lr_lambda = functools.partial(linear_warmup, warmup_iters) + + return SchedulersContainer(optimizers, lr_lambda) diff --git a/cosmos3/_src/vfm/utils/parallelism.py b/cosmos3/_src/vfm/utils/parallelism.py new file mode 100644 index 00000000..a512037e --- /dev/null +++ b/cosmos3/_src/vfm/utils/parallelism.py @@ -0,0 +1,273 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unified ParallelDims for Cosmos3 VFM and VLM (multi-mesh, overlay design). + +Topology +-------- +- ``dp_replicate * dp_shard == world_size`` — the dp dims partition all ranks. +- ``cp`` (context parallel) and ``cfgp`` (CFG parallel) are *overlay* axes: + they do NOT consume FSDP rank slots. ``cfgp * cp`` must divide + ``world_size`` so the overlay grid is well-formed, but the same rank may + appear in both a dp group AND a cp/cfgp group. + +Three meshes are built (any subset, depending on which axes are >1): + +================ =========================================================== +Mesh Shape / dims +================ =========================================================== +``dp_mesh`` 2-D ``(dp_replicate, dp_shard)`` for FSDP/HSDP +``cp_mesh`` 1-D, size ``cp`` (context parallelism) +``cfgp_mesh`` 1-D, size ``cfgp`` (CFG parallelism, inference-only) +================ =========================================================== + +Use cases +--------- +- VLM training — ``dp_shard`` (+ optional ``dp_replicate``); cp=cfgp=1. +- VFM training — ``dp_shard`` (+ optional ``dp_replicate``) + optional cp. +- VFM inference — ``dp_shard`` + cfgp/cp overlays; replicate forced to 1. + +FSDP wrapping for VLM ``HFModel`` instances lives in +``projects.cosmos3.vfm.models.parallelize_vlm``; MoT wrapping lives in +``projects.cosmos3.vfm.models.mot.parallelize_unified_mot``. Both consume +``ParallelDims`` from this module. +""" + +import math +from dataclasses import dataclass, field + +from torch.distributed.device_mesh import DeviceMesh, init_device_mesh + +from cosmos3._src.imaginaire.utils import log + +_MAX_CP = 8 + + +@dataclass +class ParallelDims: + """Unified multi-mesh parallel dimensions descriptor. + + Construct, then call :meth:`build_meshes` to allocate the underlying + DeviceMeshes. ``cp`` and ``cfgp`` are overlay axes that share rank slots + with dp; the invariant ``dp_replicate * dp_shard == world_size`` always + holds, regardless of cp/cfgp. + + Args: + world_size: Total number of ranks (typically WORLD_SIZE env var). + dp_shard: FSDP shard size. Pass ``-1`` to auto-infer to + ``world_size`` (overlay semantics: cp/cfgp do NOT + consume the dp budget). + dp_replicate: HSDP replicate size. Pass ``-1`` to auto-infer to + ``world_size // dp_shard``. + cp: Context parallel size in ``[1, 8]``. Overlay axis. + cfgp: CFG parallel size in ``(1, 2)``. Overlay axis, + inference-only (rejected at construction time + unless ``enable_inference_mode`` is True). Also is + used for only VFM to parallelize the conditional and + unconditional guidance. + enable_inference_mode: Selects inference-time semantics — ``cfgp`` may + be >1 and ``dp_enabled`` ignores ``dp_replicate`` + (matches the legacy VFM inference path). + """ + + world_size: int + dp_shard: int = -1 + dp_replicate: int = -1 + cp: int = 1 + cfgp: int = 1 + enable_inference_mode: bool = False + _meshes: dict = field(default_factory=dict, init=False, repr=False) + + def __post_init__(self) -> None: + self._validate() + + def _validate(self) -> None: + # --- overlay range checks (run before division below) --- + if self.cp < 1 or self.cp > _MAX_CP: + raise ValueError(f"CP (Context Parallelism) must be in [1, {_MAX_CP}]. got {self.cp}") + if self.cfgp not in (1, 2): + raise ValueError(f"CFGP (CFG Parallelism) must be 1 or 2. got {self.cfgp}") + if not self.enable_inference_mode and self.cfgp > 1: + raise ValueError( + f"CFG (Guidance Parallelism) must be 1 when enable_inference_mode is False. got {self.cfgp}" + ) + + # --- dp_shard auto-infer / clamp --- + # Overlay semantics: cp/cfgp do NOT consume FSDP rank slots, so the full + # world is available to dp_shard. Without auto-inference here, the + # pre-unification call form derives a negative dp_replicate, flips + # dp_enabled off, and silently disables FSDP for + # data_parallel_shard_degree=-1 runs (e.g. test_smoke.py). + if self.dp_shard <= 0: + self.dp_shard = self.world_size + log.info(f"dp_shard auto-inferred to world_size = {self.world_size}") + elif self.dp_shard > self.world_size: + # Clamp + warn rather than fail-fast: a mis-sized launch (e.g. an 8-way + # FSDP config on a 4-GPU smoke) will silently run a different topology + # than requested. Emit a loud warning so the regression is visible in + # logs; future work should fail-fast at the call site instead. + log.warning( + f"dp_shard ({self.dp_shard}) > world_size ({self.world_size}); clamping dp_shard to world_size." + ) + self.dp_shard = self.world_size + + # --- dp_replicate auto-infer --- + assert self.dp_replicate == -1 or self.dp_replicate >= 1, "dp_replicate must be -1 or >=1." + if self.dp_replicate < 0: + log.info( + "dp_replicate is set to -1, will be automatically determined based on " + f"world_size {self.world_size} // dp_shard {self.dp_shard}." + ) + self.dp_replicate = self.world_size // self.dp_shard + log.info(f"dp_replicate is set to {self.dp_replicate}.") + + # --- partition checks --- + rest = self.world_size // (self.cfgp * self.cp) + if rest * self.cfgp * self.cp != self.world_size: + raise ValueError( + f"Invalid parallel dims: rest({rest}) * cfgp({self.cfgp}) * cp({self.cp}) " + f"!= WORLD_SIZE({self.world_size})" + ) + if self.dp_replicate * self.dp_shard != self.world_size: + raise ValueError( + f"Invalid parallel dims: dp_replicate({self.dp_replicate}) * " + f"dp_shard({self.dp_shard}) != WORLD_SIZE({self.world_size})" + ) + + # --- mesh construction -------------------------------------------------- + + def _build_mesh(self, device_type: str, dims: list[int], names: list[str]) -> "DeviceMesh": + if len(dims) != len(names): + raise ValueError("Dimensions and names must have the same length.") + if any(d <= 0 for d in dims): + raise ValueError(f"All mesh dimensions must be > 0. got dims: {dims}, names: {names}.") + if math.prod(dims) != self.world_size: + raise ValueError(f"Invalid parallel dims: prod({dims}) != WORLD_SIZE({self.world_size})") + + log.info(f"Building {len(dims)}-D device mesh with {names}, {dims}") + return init_device_mesh(device_type, tuple(dims), mesh_dim_names=tuple(names)) + + def build_meshes(self, device_type: str = "cuda") -> None: + """Build the dp / cp / cfgp meshes. + + cp + cfgp are bundled into a single 3-D overlay mesh + ``(rest, cfgp, cp)`` so they share the same backing process group; + the dp mesh is a separate 2-D ``(dp_replicate, dp_shard)``. + + After this call, :attr:`mesh` may contain any subset of the keys + ``'dp'``, ``'dp_shard'``, ``'dp_replicate'``, ``'cp'``, ``'cfgp'`` + depending on which axes are enabled. + """ + self._meshes = {} + + if self.cfgp_enabled or self.cp_enabled: + overlay_mesh = self._build_mesh( + device_type, + dims=[self.world_size // (self.cfgp * self.cp), self.cfgp, self.cp], + names=["rest", "cfgp", "cp"], + ) + if self.cfgp_enabled: + self._meshes["cfgp"] = overlay_mesh["cfgp"] + if self.cp_enabled: + self._meshes["cp"] = overlay_mesh["cp"] + + if self.dp_enabled: + self._meshes["dp"] = self._build_mesh( + device_type, + dims=[self.dp_replicate, self.dp_shard], + names=["dp_replicate", "dp_shard"], + ) + if self.dp_shard_enabled: + self._meshes["dp_shard"] = self._meshes["dp"]["dp_shard"] + if self.dp_replicate_enabled: + self._meshes["dp_replicate"] = self._meshes["dp"]["dp_replicate"] + + # --- mesh accessors ----------------------------------------------------- + + @property + def mesh(self) -> dict: + """Read-only view of all built meshes. + + Empty until :meth:`build_meshes` is called. After that, may contain + any subset of ``'dp'``, ``'dp_shard'``, ``'dp_replicate'``, ``'cp'``, + ``'cfgp'`` depending on which axes are enabled. Prefer the named + accessors (:attr:`dp_mesh`, :attr:`dp_shard_mesh`, …) over keying + into this dict directly. + """ + return self._meshes + + @property + def dp_mesh(self) -> "DeviceMesh | None": + """2-D ``(dp_replicate, dp_shard)`` mesh, or None if dp is not enabled.""" + return self._meshes.get("dp") + + @property + def dp_shard_mesh(self) -> "DeviceMesh | None": + """1-D ``dp_shard`` mesh, or None if dp_shard is not enabled.""" + return self._meshes.get("dp_shard") + + @property + def dp_replicate_mesh(self) -> "DeviceMesh | None": + """1-D ``dp_replicate`` mesh, or None if dp_replicate is not enabled.""" + return self._meshes.get("dp_replicate") + + @property + def cp_mesh(self) -> "DeviceMesh | None": + return self._meshes.get("cp") + + @property + def cfgp_mesh(self) -> "DeviceMesh | None": + return self._meshes.get("cfgp") + + # --- boolean flags ------------------------------------------------------ + + @property + def dp_enabled(self) -> bool: + if self.enable_inference_mode: + return self.dp_shard > 1 + return self.dp_replicate > 1 or self.dp_shard > 1 + + @property + def dp_shard_enabled(self) -> bool: + return self.dp_shard > 1 + + @property + def dp_replicate_enabled(self) -> bool: + return self.dp_replicate > 1 + + @property + def cp_enabled(self) -> bool: + return self.cp > 1 + + @property + def cfgp_enabled(self) -> bool: + return self.cfgp > 1 + + # --- rank/size helpers -------------------------------------------------- + + @property + def cp_rank(self) -> int: + return self._meshes["cp"].get_local_rank() if self.cp_enabled else 0 + + @property + def cp_size(self) -> int: + return self._meshes["cp"].size() if self.cp_enabled else 1 + + @property + def cfgp_rank(self) -> int: + return self._meshes["cfgp"].get_local_rank() if self.cfgp_enabled else 0 + + @property + def cfgp_size(self) -> int: + return self._meshes["cfgp"].size() if self.cfgp_enabled else 1 diff --git a/cosmos3/_src/vfm/utils/rand_state.py b/cosmos3/_src/vfm/utils/rand_state.py new file mode 100644 index 00000000..b1fe5784 --- /dev/null +++ b/cosmos3/_src/vfm/utils/rand_state.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import random + +import numpy as np +import torch + + +def get_rand_state_dict() -> dict[str, torch.Tensor]: + """ + Get the random state dictionary. used to save the random state to a checkpoint. + """ + numpy_packed_len, numpy_packed_bytes = pack_numpy_state(np.random.get_state()) + random_packed_len, random_packed_bytes = pack_random_state(random.getstate()) + + return { + "torch": torch.get_rng_state(), + "torch_cuda": torch.cuda.get_rng_state(), + "numpy_packed_len": torch.tensor(numpy_packed_len, dtype=torch.long), # [] + "numpy_packed_bytes": torch.frombuffer(numpy_packed_bytes, dtype=torch.uint8), # [MAX_PACKED_LEN] + "random_packed_len": torch.tensor(random_packed_len, dtype=torch.long), # [] + "random_packed_bytes": torch.frombuffer(random_packed_bytes, dtype=torch.uint8), # [MAX_PACKED_LEN] + } + + +def set_rand_state_dict(state_dict: dict[str, torch.Tensor]) -> None: + """ + Set the random state dictionary. used to restore the random state from a checkpoint. + """ + torch.set_rng_state(state_dict["torch"]) + torch.cuda.set_rng_state(state_dict["torch_cuda"]) + np.random.set_state( + unpack_numpy_state(state_dict["numpy_packed_len"].item(), bytes(state_dict["numpy_packed_bytes"].tolist())) + ) + random.setstate( + unpack_random_state(state_dict["random_packed_len"].item(), bytes(state_dict["random_packed_bytes"].tolist())) + ) + + +# MAX padding length for the random state +# numpy and python random state are based on Mersenne Twister algorithm and state has aprox 625 integers. +# When we convert this data through pickle generated output can be of variable size. +# Pad the output to a fixed size to ensure that the size of the random state is always the same. +# Fixed size buffer is required for DCP checkpoint functions as it uses pinned preallocated memory to copy the checkpoint data. +MAX_PACKED_LEN = 4096 + + +def pad_packed_bytes(packed_bytes: bytes) -> bytearray: + padded = packed_bytes.ljust(MAX_PACKED_LEN, b"\0") + + return bytearray(padded) + + +def pack_numpy_state(state: tuple[int, ...]) -> tuple[int, bytearray]: + packed_bytes = pickle.dumps(state) + return len(packed_bytes), pad_packed_bytes(packed_bytes) + + +def pack_random_state(state: tuple[int, ...]) -> tuple[int, bytearray]: + packed_bytes = pickle.dumps(state) + return len(packed_bytes), pad_packed_bytes(packed_bytes) + + +def unpack_numpy_state(packed_len: int, packed_bytes: bytearray) -> tuple[int, ...]: + packed_bytes = packed_bytes[:packed_len] + # unpickle the state + return pickle.loads(packed_bytes) + + +def unpack_random_state(packed_len: int, packed_bytes: bytearray) -> tuple[int, ...]: + packed_bytes = packed_bytes[:packed_len] + # unpickle the state + return pickle.loads(packed_bytes) + + +if __name__ == "__main__": + print("--- Initializing Seeds ---") + torch.manual_seed(42) + torch.cuda.manual_seed_all(42) + np.random.seed(42) + random.seed(42) + + # random operations to advance the state from the initial seed + for _ in range(10): + _ = torch.randn(1) + _ = np.random.standard_normal() + _ = random.gauss(0, 1) + + print("Capturing state...") + state_dict = get_rand_state_dict() + + # Ensure tensors are actually padded to fixed size + assert state_dict["numpy_packed_bytes"].numel() == MAX_PACKED_LEN, ( + f"Numpy buffer size mismatch! Expected {MAX_PACKED_LEN}, got {state_dict['numpy_packed_bytes'].numel()}" + ) + assert state_dict["random_packed_bytes"].numel() == MAX_PACKED_LEN, ( + f"Random buffer size mismatch! Expected {MAX_PACKED_LEN}, got {state_dict['random_packed_bytes'].numel()}" + ) + print(f"State captured. Buffers verified at {MAX_PACKED_LEN} bytes.") + + # generate a sequence of random numbers immediately after saving. + print("Generating Ground Truth Sequence (A)...") + seq_a = [] + for _ in range(5): + vals = ( + torch.randn(1).item(), + np.random.rand(), + np.random.uniform(), + random.random(), + ) + seq_a.append(vals) + + # modify the state by generating random numbers + print("Modifying state (Generating junk)...") + for _ in range(20): + _ = torch.rand(1) + _ = np.random.rand() + _ = random.random() + + # restore the state + print("Restoring random state...") + set_rand_state_dict(state_dict) + + # If restore works, this must match Sequence A exactly. + print("Generating Post-Restore Sequence (B)...") + seq_b = [] + for _ in range(5): + vals = ( + torch.randn(1).item(), + np.random.rand(), + np.random.uniform(), + random.random(), + ) + seq_b.append(vals) + + print("\n--- Verification Results ---") + all_match = True + for i, (truth, actual) in enumerate(zip(seq_a, seq_b)): + # Compare all values in the tuple with small tolerance for float precision + row_match = all(abs(t - a) < 1e-9 for t, a in zip(truth, actual)) + print(f" Row {i}: {truth} vs {actual} - {row_match}") + if row_match: + print(f"Step {i}: MATCH ") + else: + print(f"Step {i}: MISMATCH ") + print(f" Expected: {truth}") + print(f" Actual: {actual}") + all_match = False + + if all_match: + print("\n All random states (Torch, Numpy, Python) restored successfully.") + else: + raise RuntimeError("Test Failed: Random states did not match after restore.") diff --git a/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py b/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py new file mode 100644 index 00000000..5ea400b8 --- /dev/null +++ b/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass + + +@dataclass +class BenchmarkTimes: + """ + Class used to store times computed during tokenizer benchmarking. + All times are in seconds. + """ + + model_invocation: float = 0.0 + # Model's invocation time + overhead + total: float = 0.0 + + @property + def overhead(self) -> float: + return self.total - self.model_invocation + + def __repr__(self) -> str: + return f"BenchmarkTimes(model_invocation={self.model_invocation}, overhead={self.overhead}, total={self.total})" diff --git a/cosmos3/_src/vfm/utils/vlm/__init__.py b/cosmos3/_src/vfm/utils/vlm/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/_src/vfm/utils/vlm/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/_src/vfm/utils/vlm/optimizer.py b/cosmos3/_src/vfm/utils/vlm/optimizer.py new file mode 100644 index 00000000..bf787140 --- /dev/null +++ b/cosmos3/_src/vfm/utils/vlm/optimizer.py @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import functools +import itertools +import math +from copy import deepcopy +from typing import Any, Optional + +import attrs +import torch +import torch.nn as nn +from torch.distributed.checkpoint.state_dict import StateDictOptions, get_optimizer_state_dict, set_optimizer_state_dict +from torch.distributed.checkpoint.stateful import Stateful +from torch.optim.lr_scheduler import LambdaLR + +from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.imaginaire.lazy_config import LazyDict +from cosmos3._src.imaginaire.utils import log +from projects.cosmos3.vlm.utils.fused_adam import FusedAdam + + +@make_freezable +@attrs.define(slots=False) +class OptimizerConfig: + name: str = "FusedAdam" + lr: float = 2e-6 + init_lr: float = 1e-7 + end_lr: float = 1e-6 + fused: bool = False + weight_decay: float = 0.1 + betas: tuple[float, float] = (0.9, 0.95) + lr_multiplier: LazyDict = LazyDict(dict(vision_encoder=0.1, mm_projector=1.0, llm=1.0)) + + # Legacy named freeze flags — VFM-unified VLMModel and cosmos-rl sft_trainer both + # honor these. Supported archs: Qwen2.5-VL, Qwen3-VL (dense + MoE), InternVL3_5. + freeze_vision_encoder: bool = False + freeze_mm_projector: bool = False + freeze_llm: bool = False + + # Regex-based freeze — mutually exclusive (enforced in __attrs_post_init__ below and + # again at the top of _apply_freeze_config for the LazyCall/DictConfig runtime path). + # trainable_params: whitelist — only matching params are trainable (full override). + # frozen_params: blacklist — matching params get frozen, additive on top of legacy. + + # reads only the legacy booleans (sft_trainer_cosmos_rl.py:200); trainable_params / + # frozen_params are silently ignored there. + trainable_params: Optional[list[str]] = None + frozen_params: Optional[list[str]] = None + + def __attrs_post_init__(self) -> None: + if self.trainable_params is not None and self.frozen_params is not None: + raise ValueError("OptimizerConfig: set at most one of trainable_params or frozen_params, not both.") + + +def _optimizer_cls(params: list[nn.Parameter], optimizer_kwargs: dict[str, Any], name: str): + if name.lower() == "adam": + + optimizer = torch.optim.Adam(params, **optimizer_kwargs) + elif name.lower() == "adamw": + optimizer = torch.optim.AdamW(params, **optimizer_kwargs) + elif name.lower() == "fusedadam": + optimizer = FusedAdam( + params, + lr=optimizer_kwargs["lr"], + weight_decay=optimizer_kwargs["weight_decay"], + betas=optimizer_kwargs["betas"], + capturable=True, + master_weights=True, + ) + else: + raise NotImplementedError(f"Optimizer {name} not added.") + return optimizer + + +class OptimizersContainer(Stateful): + """Util for calling step/zero_grad on multiple optimizers needed for virtual pipeline stages + and saving/loading optimizer state_dict at checkpoint. + """ + + def __init__( + self, + model_parts: list[nn.Module], + optimizer_kwargs: dict[str, Any], + name: str, + lr_multiplier: dict[str, float], + model_part_names: list[str], + ) -> None: + assert len(model_parts) == len(model_part_names), "model_parts and model_part_names must have the same length" + self.model_parts = model_parts + self.optimizers = [[] for _ in self.model_parts] + self.model_part_names = model_part_names + for model_id, model in enumerate(self.model_parts): + optimizer_kwargs_copy = deepcopy(optimizer_kwargs) + optimizer_kwargs_copy["lr"] *= lr_multiplier[model_part_names[model_id]] + log.info( + f"model_id: {model_id} | model_part_names: {model_part_names[model_id]} | lr_multiplier: {lr_multiplier[model_part_names[model_id]]} | lr: {optimizer_kwargs_copy['lr']}" + ) + + if optimizer_kwargs_copy["fused"]: + # Group the parameters by device mesh to do optimizer fusion. + parameters_by_mesh = collections.defaultdict(list) + for p in model.parameters(): + if p.requires_grad: + device_mesh = p.device_mesh if hasattr(p, "device_mesh") else "default" + parameters_by_mesh[device_mesh].append(p) + for params in parameters_by_mesh.values(): + optimizer = _optimizer_cls(params, optimizer_kwargs_copy, name) + self.optimizers[model_id].append(optimizer) + else: + for p in model.parameters(): + if p.requires_grad: + optimizer = _optimizer_cls([p], optimizer_kwargs_copy, name) + self.optimizers[model_id].append(optimizer) + + def __iter__(self) -> torch.optim.Optimizer: + return iter(itertools.chain(*self.optimizers)) + + def step(self) -> None: + for optimizer in itertools.chain(*self.optimizers): + optimizer.step() + + def zero_grad(self, set_to_none: bool = False) -> None: + for optimizer in itertools.chain(*self.optimizers): + optimizer.zero_grad(set_to_none=set_to_none) + + def state_dict(self) -> dict[str, Any]: + result = {} + for idx, (model_part, optimizer) in enumerate(zip(self.model_parts, self.optimizers)): + sd = get_optimizer_state_dict( + model=model_part, + optimizers=optimizer, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + prefix = f"optimizer_{idx}/" + result.update({f"{prefix}{k}": v for k, v in sd.items()}) + return result + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + for idx, (model, optimizers) in enumerate(zip(self.model_parts, self.optimizers)): + prefix = f"optimizer_{idx}/" + optimizer_state = { + k[len(prefix) :]: v # Remove prefix + for k, v in state_dict.items() + if k.startswith(prefix) + } + + set_optimizer_state_dict( + model=model, + optimizers=optimizers, + optim_state_dict=optimizer_state, + options=StateDictOptions(flatten_optimizer_state_dict=True), + ) + + +# consider split between PP and non-PP +def build_optimizers( + model_parts: list[nn.Module], + config: OptimizerConfig, + model_part_names: list[str], +) -> OptimizersContainer: + """Wrap one optimizer per model part in an OptimizersContainer which provides a single + step() and zero_grad() method for all the child optimizers. + """ + lr_multiplier = config.lr_multiplier + for part_name in model_part_names: + assert part_name in lr_multiplier, f"lr_multiplier must have the key {part_name}" + + name = config.name + lr = config.lr + fused = config.fused + optimizer_kwargs = { + "lr": lr, + "betas": tuple(config.betas), + "weight_decay": config.weight_decay, + "fused": fused, + "foreach": not fused, + } + + return OptimizersContainer(model_parts, optimizer_kwargs, name, lr_multiplier, model_part_names) + + +class SchedulersContainer(Stateful): + """Util for calling step on multiple learning rate schedulers needed for virtual pipeline stages""" + + def __init__(self, optimizers: OptimizersContainer, lr_lambda) -> None: + self.schedulers = [] + for optimizer in optimizers: + self.schedulers.append(LambdaLR(optimizer, lr_lambda=lr_lambda)) + + def step(self) -> None: + for id, scheduler in enumerate(self.schedulers): + scheduler.step() + + def state_dict(self) -> dict[str, Any]: + # Currently, we have one scheduler per optimizer. However, when using MultiSchedule PP or optimizer-in-backward, + # there are multiple optimizers and schedulers, but the scheduler state_dict remains the same for all. + # Therefore, we only save the first one and later load it for all. + assert len(self.schedulers) > 0, "Must have at least one scheduler to save state_dict" + return self.schedulers[0].state_dict() + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + # Load the same state_dict for all schedulers. The key value we're concerned with in scheduler.state_dict() is `last_epoch`, + # which is an integer that will be automatically copied. As long as `training.steps` and `training.warmup_iters` remain + # unchanged when resuming from a checkpoint, this approach is safe. We call `.copy()` here to ensure extra safety. + last_epoch = state_dict["last_epoch"] # Extract last known epoch + _step_count = state_dict["_step_count"] + log.info(f"Resuming schedulers by stepping them to last_epoch: {last_epoch}; _step_count: {_step_count}") + + # Manually step all schedulers to match the saved state -- this is a workaround for the inherited issue in the state dict saving (only saved the first scheduler) + # But we have different learning rate for each scheduler, so we need to step them separately instead of loading the state dict + # The benefit of this approach is that we can resume from a checkpoint even if the learning rate is changed + for idx, scheduler in enumerate(self.schedulers): + for step in range(_step_count): + scheduler.step() # Step forward to match previous training state + log.info(f"Scheduler {idx + 1}/{len(self.schedulers)} stepped {_step_count} times.") + log.info(f"Updated learning rate: {scheduler.get_last_lr()}") + + def get_last_lr(self) -> list[float]: + return [scheduler.get_last_lr() for scheduler in self.schedulers] + + +def linear_warmup_linear_decay(warmup_iters: int, decay_steps: int, current_step: int) -> float: + """Computes linear warmup followed by linear decay. + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + """ + if current_step < warmup_iters: + # linear warmup + # 0-indexed step, hence + 1 adjustments + current_step += 1 + curr_adjustment = float(current_step / (warmup_iters + 1)) + + else: + # linear decay + normalized_step = decay_steps - (current_step - warmup_iters) + curr_adjustment = 1 - (decay_steps - normalized_step) / decay_steps + + return curr_adjustment + + +def linear_warmup(warmup_iters: int, current_step: int) -> float: + """Computes linear warmup only + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + """ + if current_step < warmup_iters: + # linear warmup + # 0-indexed step, hence + 1 adjustments + current_step += 1 + curr_adjustment = float(current_step / (warmup_iters + 1)) + else: + curr_adjustment = 1 + + return curr_adjustment + + +def linear_warmup_cosine_cooldown( + warmup_iters: int, cooldown_steps: int, current_step: int, base_lr: float, init_lr: float, end_lr: float +) -> float: + """This scheduler will warmup the learning rate from init_lr to base_lr for warmup_iters, + then decay the learning rate from base_lr to end_lr for cooldown_steps. After cooldown_steps + warmup_iters, + the learning rate will be set to end_lr. + Per LambdaLR requirement, this is accomplished by returning + a multiplicative factor to adjust the learning rate to + create the desired schedule. + + Args: + warmup_iters (int): The number of steps to warmup the learning rate. + cooldown_steps (int): The number of steps to decay the learning rate. + current_step (int): The current step. + base_lr (float): The base learning rate. + init_lr (float): The initial learning rate before warmup. + end_lr (float): The final learning rate after cooldown. + + Returns: + float: The multiplicative factor to adjust the learning rate. + """ + total_steps = warmup_iters + cooldown_steps + + # Normalize + init_multiplier = init_lr / base_lr + end_multiplier = end_lr / base_lr + if current_step <= warmup_iters: + progress = float(current_step / warmup_iters) + return init_multiplier + (1.0 - init_multiplier) * progress + elif current_step <= total_steps: + progress = (current_step - warmup_iters) / cooldown_steps + return end_multiplier + 0.5 * (1.0 - end_multiplier) * (1 + math.cos(math.pi * progress)) + else: + return end_multiplier + + +def build_lr_schedulers( + optimizers: OptimizersContainer, + name: str, + lr: float, + warmup_iters: int, + lr_decay_iters: Optional[int] = None, + init_lr: Optional[float] = None, + end_lr: Optional[float] = None, +) -> SchedulersContainer: + decay_steps = float(max(1, lr_decay_iters - warmup_iters)) if lr_decay_iters is not None else None + if name == "warmup_cosine_lr": + assert init_lr is not None and end_lr is not None, "init_lr and end_lr must be provided for warmup_cosine_lr" + assert lr_decay_iters is not None, "lr_decay_iters must be provided for warmup_cosine_lr" + lr_lambda = functools.partial( + linear_warmup_cosine_cooldown, + warmup_iters, + decay_steps, + base_lr=lr, + init_lr=init_lr, + end_lr=end_lr, + ) + elif name == "lambdalinear": + assert lr_decay_iters is not None, "lr_decay_iters must be provided for lambdalinear" + lr_lambda = functools.partial(linear_warmup_linear_decay, warmup_iters, decay_steps) + else: + lr_lambda = functools.partial(linear_warmup, warmup_iters) + + return SchedulersContainer(optimizers, lr_lambda) diff --git a/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py b/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py new file mode 100644 index 00000000..3af82579 --- /dev/null +++ b/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py @@ -0,0 +1,248 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import boto3 +import filelock +from boto3.s3.transfer import TransferConfig +from loguru import logger as log + +_LOCK_TIMEOUT_SECONDS = 1800 # 30 minutes + + +def parallel_download_s3_prefix_to_dir( + bucket: str, + prefix: str, + dest_dir: str, + credential_path: str, + max_workers: int = 4, + skip_if_exists: bool = True, + exclude_list: list[str] = [], +) -> list[str]: + """ + Parallel download of all objects under s3_uri (prefix) to dest_dir, + preserving relative paths. Returns list of downloaded (or skipped) local paths. + Example of exclude_list: [".safetensors"] + """ + os.makedirs(dest_dir, exist_ok=True) + + s3 = boto3.client("s3", **json.load(open(credential_path, "r"))) + + # List all objects under prefix (paginated) + paginator = s3.get_paginator("list_objects_v2") + pages = paginator.paginate(Bucket=bucket, Prefix=prefix) + + # Collect (key, size) for real objects (exclude "folders") + objects: list[tuple[str, int]] = [] + for page in pages: + for obj in page.get("Contents", []): + key = obj["Key"] + # Skip "directory placeholders" + if key.endswith("/"): + continue + if any(exclude in key for exclude in exclude_list): + log.info(f"Skipping {key} because it matches exclude_list {exclude_list}") + continue + objects.append((key, obj["Size"])) + + # Nothing to do + if not objects: + return [] + + # Prepare download tasks + tasks = [] + results: list[str] = [] + + transfer_cfg = TransferConfig( + multipart_threshold=64 * 1024 * 1024, # 64MB threshold + multipart_chunksize=64 * 1024 * 1024, # 64MB parts + max_concurrency=max_workers, + use_threads=True, + ) + + def submit_download(executor, key, size): + rel_path = os.path.relpath(key, start=prefix) if prefix else key + local_path = os.path.join(dest_dir, rel_path) + os.makedirs(os.path.dirname(local_path), exist_ok=True) + + # Skip if exists and size matches + if skip_if_exists and os.path.exists(local_path): + try: + if os.path.getsize(local_path) == size: + results.append(local_path) + return None # do not submit + except OSError: + pass + log.info(f"Downloading s3://{bucket}/{key} to {local_path}") + fut = executor.submit( + s3.download_file, + bucket, + key, + local_path, + ExtraArgs={}, # you can add {"RequestPayer": "requester"} if needed + Config=transfer_cfg, + ) + fut._local_path = local_path # type: ignore[attr-defined] + return fut + + # Dispatch + with ThreadPoolExecutor(max_workers=max_workers) as ex: + for key, size in objects: + f = submit_download(ex, key, size) + if f is not None: + tasks.append(f) + + for f in as_completed(tasks): + # propagate any exceptions immediately + _ = f.result() + results.append(getattr(f, "_local_path")) + + return results + + +def has_model_weights(cache_dir: str) -> bool: + import glob + + return len(glob.glob(os.path.join(cache_dir, "*.safetensors"))) > 0 + + +def s3_dir_exists(bucket, prefix, credentials): + """ + Check whether a given prefix (directory) exists in an S3 bucket. + + Args: + bucket (str): The name of the S3 bucket. + prefix (str): The prefix (directory path) to check. + + Returns: + bool: True if the prefix exists, False otherwise. + """ + s3 = boto3.client("s3", **json.load(open(credentials, "r"))) + # Make sure prefix ends with "/" to represent a "directory" + if not prefix.endswith("/"): + prefix += "/" + + resp = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, MaxKeys=1) + return "Contents" in resp + + +def _download_from_hf_hub(model_name_or_path: str, include_model_weights: bool = True) -> str: + """Download a model from HuggingFace Hub when no S3 credentials/bucket are configured. + + Mirrors the logic in cosmos_rl resolve_model_path: detects safetensors to set + ignore_patterns, then calls snapshot_download into HF_HOME/hub. + """ + from huggingface_hub import HfFileSystem, snapshot_download + + if os.path.isdir(model_name_or_path): + return model_name_or_path + + hf_token = os.environ.get("HF_TOKEN", None) + hf_home = os.environ.get("HF_HOME", "") + hf_cache_dir = os.path.join(hf_home, "hub") if hf_home else os.path.expanduser("~/.cache/huggingface/hub") + + ignore_patterns: list[str] = [] + try: + hf_fs = HfFileSystem(token=hf_token) + files = hf_fs.ls(model_name_or_path, detail=False) + has_safetensors = any( + f.endswith("model.safetensors.index.json") or f.endswith("model.safetensors") for f in files + ) + if has_safetensors: + ignore_patterns += ["*pytorch_model*", "*consolidated*"] + except Exception as e: + log.warning(f"Could not list HuggingFace repo {model_name_or_path}: {e}") + + if not include_model_weights: + ignore_patterns.append("*.safetensors") + + log.info(f"Downloading {model_name_or_path} from HuggingFace Hub (ignore={ignore_patterns})") + local_path = snapshot_download( + model_name_or_path, + token=hf_token, + cache_dir=hf_cache_dir, + ignore_patterns=ignore_patterns or None, + ) + log.info(f"Downloaded {model_name_or_path} to {local_path}") + return local_path + + +def maybe_download_hf_model_from_s3( + model_name_or_path: str, + credentials: str, + bucket: str, + include_model_weights: bool = False, + cache_dir: str = None, + s3_prefix: str = "cosmos_reason2/hf_models", + require_s3_exists: bool = False, +) -> str: + exclude_list = [".safetensors"] if not include_model_weights else [] + s3_prefix = os.path.join(s3_prefix, model_name_or_path) + # download the model from s3 to local cache + if cache_dir is None: + cache_dir = os.path.expanduser(os.getenv("IMAGINAIRE_CACHE_DIR", "~/.cache/imaginaire")) + + cache_dir = os.path.join(cache_dir, s3_prefix) + + if not credentials or not bucket: + log.warning( + f"No S3 credentials/bucket configured, trying to download from HuggingFace Hub for {model_name_or_path}" + ) + return _download_from_hf_hub(model_name_or_path, include_model_weights) + + if not s3_dir_exists(bucket, s3_prefix, credentials): + if require_s3_exists: + raise FileNotFoundError(f"Model {model_name_or_path} not found in s3://{bucket}/{s3_prefix}") + else: + log.critical( + f"Model {model_name_or_path} not found in s3://{bucket}/{s3_prefix} with credentials {credentials}", + rank0_only=False, + ) + return model_name_or_path + + lock_path = os.path.join(cache_dir, "lock.lock") + lock = filelock.FileLock(lock_path, timeout=_LOCK_TIMEOUT_SECONDS) # 1 minute timeout for download + with lock: + if ( + os.path.exists(cache_dir) + and not include_model_weights + and os.path.exists(os.path.join(cache_dir, "vocab.json")) + ): + return cache_dir + elif os.path.exists(cache_dir) and include_model_weights and has_model_weights(cache_dir): + return cache_dir + else: + os.makedirs(cache_dir, exist_ok=True) + tic = time.time() + parallel_download_s3_prefix_to_dir(bucket, s3_prefix, cache_dir, credentials, exclude_list=exclude_list) + toc = time.time() + print(f"Time taken to download model {model_name_or_path}: {toc - tic:.3f} seconds") + + return cache_dir + + +if __name__ == "__main__": + """ + Usage: + PYTHONPATH=. python3 projects/cosmos3/vlm/utils/pretrained_models_downloader.py + """ + cache_dir = maybe_download_model( # noqa: F821 + "eagle_er_qwen3_1p7b_siglip_400m", "credentials/s3_training.secret", "bucket" + ) + print(f"Downloaded to {cache_dir}") diff --git a/cosmos3/_test.py b/cosmos3/_test.py new file mode 100644 index 00000000..3a6dfaa2 --- /dev/null +++ b/cosmos3/_test.py @@ -0,0 +1,268 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path +from typing import get_args + +import pytest +import safetensors.torch +import torch +import torchvision.transforms.functional + +from cosmos3.args import ( + IMAGE_ONLY_RESOLUTIONS, + ActionMode, + AspectRatio, + InferenceResolution, + OmniSampleArgs, + OmniSampleOverrides, + _load_modality_defaults, +) +from cosmos3.common.args import SampleOutputs +from cosmos3.fixtures.args import MAX_GPUS +from cosmos3.fixtures.script import INPUT_DIR, ScriptConfig, ScriptRunner, script_test +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + +_CURRENT_DIR = Path(__file__).parent.absolute() +_TEST_DIR = _CURRENT_DIR / "_test" + +_TEMPORAL_COMPRESSION = 4 # wan2pt2_vae_4x16x16 +_T2V_DEFAULTS = _load_modality_defaults("text2video") + + +def _vae_output_frames(num_frames: int) -> int: + """Round up to the nearest valid output frame count (t*k + 1).""" + t = _TEMPORAL_COMPRESSION + return ((num_frames - 1 + t - 1) // t) * t + 1 + + +# Excluded from the sweep: Cosmos-Guardrail1 resolution-sensitive FP on 720p t2v. +_GUARDRAIL_BLOCKED_RESOLUTIONS: frozenset[str] = frozenset({"720"}) + +# Image-only resolutions can't run in the t2v sweep (num_frames > 1 is rejected). +_T2V_BLOCKED_RESOLUTIONS: frozenset[str] = _GUARDRAIL_BLOCKED_RESOLUTIONS | IMAGE_ONLY_RESOLUTIONS + +_SWEEP_CASES: list[tuple[str, dict]] = [ + *((f"res_{r}", {"resolution": r}) for r in get_args(InferenceResolution) if r not in _T2V_BLOCKED_RESOLUTIONS), + *((f"ar_{ar}", {"resolution": "256", "aspect_ratio": ar}) for ar in get_args(AspectRatio)), + *((f"nframes_{n}", {"resolution": "256", "num_frames": n}) for n in (None, 189)), +] + +_OMNI_SUPER_MODALITIES: list[str] = ["t2v", "t2i", "i2v", "i2i", "v2v"] + + +def test_assets(): + overrides_list = OmniSampleOverrides.from_files([INPUT_DIR / "omni/*.json*"]) + assert overrides_list + + +def _check_inference_output(input_files: list[Path], output_dir: Path) -> list[SampleOutputs]: + sample_args_list = OmniSampleArgs.from_files([output_dir / "*/sample_args.json"]) + assert sample_args_list + sample_outputs_list: list[SampleOutputs] = [] + for sample_args in sample_args_list: + (sample_outputs,) = SampleOutputs.from_files([sample_args.output_dir / "sample_outputs.json"]) + sample_outputs_list.append(sample_outputs) + + assert len(sample_outputs.outputs) == sample_args.num_outputs + for output in sample_outputs.outputs: + for file in output.files: + assert file.is_file() + + vision_files = [f for f in output.files if f.stem == "vision"] + assert len(vision_files) == 1 + return sample_outputs_list + + +def _get_video_dims(path: Path) -> tuple[int, int, int]: + """Return (width, height, frame_count) of a video file.""" + import av + + with av.open(str(path), mode="r") as container: + stream = container.streams.video[0] + frame_count = int(stream.frames) + if frame_count == 0: + frame_count = sum(1 for _ in container.decode(video=0)) + return int(stream.width), int(stream.height), frame_count + + +def _omni_after_script(runner: ScriptRunner) -> None: + inference_dir = runner.output_dir / "inference" + sample_outputs_list = _check_inference_output([runner.input_dir / "omni/*json"], inference_dir) + # Skip golden PSNR/MSE at L0 — SMOKE mode runs a degenerate model that won't hit real thresholds. + if runner.level == 0: + return + failures: list[str] = [] + for sample_outputs in sample_outputs_list: + failures.extend(_check_action_golden(sample_outputs, inference_dir / sample_outputs.name)) + assert not failures, "Golden checks failed:\n " + "\n ".join(failures) + + +def _omni_super_before_script(runner: ScriptRunner) -> None: + """Stage only the omni modalities supported by Cosmos3-Super (32B).""" + src_dir = runner.input_dir / "omni" + dst_dir = runner.tmp_input_dir / "omni" + dst_dir.mkdir(parents=True, exist_ok=True) + for src in sorted(src_dir.glob("*.json")): + if not any(src.stem == m or src.stem.startswith(f"{m}_") for m in _OMNI_SUPER_MODALITIES): + continue + dst = dst_dir / src.name + dst.unlink(missing_ok=True) + dst.symlink_to(src) + + +def _omni_param_before_script(runner: ScriptRunner) -> None: + """Stage t2v-based parameter-sweep cases into a temp input root.""" + input_root = runner.tmp_input_dir + input_dir = input_root / "omni" + base_case = json.loads((runner.input_dir / "omni" / "t2v.json").read_text()) + + input_dir.mkdir(parents=True, exist_ok=True) + + prompt_file = runner.input_dir / "t2v_prompt.txt" + prompt_target = input_root / prompt_file.name + prompt_target.unlink(missing_ok=True) + prompt_target.symlink_to(prompt_file) + + for name, payload in _SWEEP_CASES: + (input_dir / f"{name}.json").write_text(json.dumps({**base_case, "name": name, **payload}, indent=4) + "\n") + + +def _omni_param_after_script(runner: ScriptRunner) -> None: + """Validate parameter-sweep outputs.""" + inference_dir = runner.output_dir / "inference" + for name, payload in _SWEEP_CASES: + resolution = payload["resolution"] + aspect_ratio = payload.get("aspect_ratio", _T2V_DEFAULTS["aspect_ratio"]) + num_frames = payload.get("num_frames") or _T2V_DEFAULTS["num_frames"] + expected_frames = _vae_output_frames(num_frames) + + vision_path = inference_dir / name / "vision.mp4" + assert vision_path.is_file(), f"{name}: missing {vision_path.relative_to(inference_dir)}" + width, height, frame_count = _get_video_dims(vision_path) + expected_width, expected_height = VIDEO_RES_SIZE_INFO[resolution][aspect_ratio] + assert (width, height) == (expected_width, expected_height), ( + f"{name}: {width}x{height} != {expected_width}x{expected_height}" + ) + assert frame_count == expected_frames, f"{name}: {frame_count} frames != {expected_frames}" + + +def _dcp_checkpoint_after_script(runner: ScriptRunner) -> None: + _check_inference_output([runner.input_dir / "omni/t2v.json"], runner.output_dir / "inference") + + +def _action_after_script(runner: ScriptRunner) -> None: + st_files = list(runner.output_dir.rglob("output.safetensors")) + assert st_files, f"No output.safetensors found under {runner.output_dir}" + for f in st_files: + tensors = safetensors.torch.load_file(f) + if "action" in tensors: + action = tensors["action"] + assert action.ndim >= 2, ( + f"{f}: expected action to have >= 2 dims (T, D), got {action.ndim} dims: {tuple(action.shape)}" + ) + print(f"PASS: '{f.relative_to(runner.output_dir)}' action shape={tuple(action.shape)}") + + +def _load_canonical_gt_video(sample_dir: Path) -> torch.Tensor: + """Load the raw GT video and resize it to the canonical (unpadded) shape.""" + from cosmos3.vision import read_media_frames + + image_size = safetensors.torch.load_file(sample_dir / "sample_data.safetensors")["image_size"][0] + orig_h, orig_w = int(image_size[2].item()), int(image_size[3].item()) + raw, _ = read_media_frames(sample_dir / "inputs" / "vision.mp4", max_frames=1024) + return torchvision.transforms.functional.resize( + raw, + [orig_h, orig_w], + interpolation=torchvision.transforms.functional.InterpolationMode.BICUBIC, + antialias=True, + ) + + +def _load_gt_action(url_or_path: str, sample_dir: Path) -> torch.Tensor: + """Download (if URL) and load the GT action JSON as a float32 tensor.""" + from cosmos3.common.args import download_file + + path = download_file(url_or_path, sample_dir, "golden_action") + return torch.tensor(json.loads(Path(path).read_text()), dtype=torch.float32) + + +def _check_action_golden(sample_outputs: SampleOutputs, sample_dir: Path) -> list[str]: + """Run PSNR/MSE checks against thresholds in the sample's `extra` block.""" + extra = sample_outputs.args.get("extra") or {} + psnr_min: float | None = extra.get("golden_psnr_min") + mse_max: float | None = extra.get("golden_mse_max") + if psnr_min is None and mse_max is None: + return [] + + from cosmos3.scripts.eval_utils import compute_sample_metrics + + mode = ActionMode(sample_outputs.args["action_mode"]) + gt_video = ( + _load_canonical_gt_video(sample_dir) if mode in (ActionMode.FORWARD_DYNAMICS, ActionMode.POLICY) else None + ) + gt_action = _load_gt_action(extra["golden_action_path"], sample_dir) if mse_max is not None else None + # `compute_sample_metrics` parses mode from `name.split("/")[-2]`. + metrics = compute_sample_metrics( + f"{mode.value}/{sample_dir.name}", gt_video, gt_action, sample_outputs, sample_dir, ".mp4" + ) + + failures: list[str] = [] + if psnr_min is not None and "psnr" in metrics: + psnr = metrics["psnr"] + print(f"[{sample_dir.name}] PSNR={psnr:.2f} dB (min {psnr_min:.2f})") + if psnr < psnr_min: + failures.append(f"{sample_dir.name}: PSNR {psnr:.2f} < {psnr_min:.2f}") + if mse_max is not None and "action_mse" in metrics: + mse = metrics["action_mse"] + print(f"[{sample_dir.name}] MSE={mse:.4f} (max {mse_max:.4f})") + if mse > mse_max: + failures.append(f"{sample_dir.name}: MSE {mse:.4f} > {mse_max:.4f}") + return failures + + +_script_configs = [ + ScriptConfig( + script=_TEST_DIR / "omni.sh", + levels=(0, 1, 2), + gpus=(1, MAX_GPUS, MAX_GPUS), + after_script=_omni_after_script, + ), + ScriptConfig( + script=_TEST_DIR / "latency.sh", + levels=(1, 2), + gpus=(0, MAX_GPUS, MAX_GPUS), + after_script=_omni_after_script, + ), + ScriptConfig( + script=_TEST_DIR / "omni-super.sh", + use_tmp_input_dir=True, + levels=(1, 2), + gpus=(1, MAX_GPUS, MAX_GPUS), + before_script=_omni_super_before_script, + after_script=_omni_after_script, + ), + ScriptConfig( + script=_TEST_DIR / "sft.sh", + training=True, + levels=(0, 1, 2), + gpus=(1, MAX_GPUS, MAX_GPUS), + ), +] + + +@script_test(_script_configs) +class TestScript: ... diff --git a/cosmos3/_test/autoregressive.sh b/cosmos3/_test/autoregressive.sh new file mode 100644 index 00000000..9b36d4c1 --- /dev/null +++ b/cosmos3/_test/autoregressive.sh @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/autoregressive/*_test.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path 9d47ff88-cb43-4bf0-8f9b-3d74e2bd9a67 \ + --use_cuda_graphs \ + $INFERENCE_ARGS diff --git a/cosmos3/_test/distilled.sh b/cosmos3/_test/distilled.sh new file mode 100644 index 00000000..e32b3c7f --- /dev/null +++ b/cosmos3/_test/distilled.sh @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/interactive/*.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path eae30a62-7633-466c-976f-47f5a90c843f \ + $INFERENCE_ARGS diff --git a/cosmos3/_test/latency.sh b/cosmos3/_test/latency.sh new file mode 100644 index 00000000..59d32c14 --- /dev/null +++ b/cosmos3/_test/latency.sh @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/omni/t2v.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path Cosmos3-Nano \ + --parallelism-preset=latency \ + $INFERENCE_ARGS diff --git a/cosmos3/_test/omni-super.sh b/cosmos3/_test/omni-super.sh new file mode 100644 index 00000000..af2a1189 --- /dev/null +++ b/cosmos3/_test/omni-super.sh @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/omni/*.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path Cosmos3-Super \ + --parallelism-preset=latency \ + $INFERENCE_ARGS diff --git a/cosmos3/_test/omni.sh b/cosmos3/_test/omni.sh new file mode 100644 index 00000000..2c937407 --- /dev/null +++ b/cosmos3/_test/omni.sh @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/omni/*.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path Cosmos3-Nano \ + --parallelism-preset=throughput \ + $INFERENCE_ARGS diff --git a/cosmos3/_test/omni_param.sh b/cosmos3/_test/omni_param.sh new file mode 100644 index 00000000..b95e69d0 --- /dev/null +++ b/cosmos3/_test/omni_param.sh @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source "$(dirname "$0")/omni.sh" diff --git a/cosmos3/_test/sft.sh b/cosmos3/_test/sft.sh new file mode 100644 index 00000000..756f16e6 --- /dev/null +++ b/cosmos3/_test/sft.sh @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DATASET_PATH=$(uvx hf@latest download --repo-type dataset nvidia/bridge-v2-subset-synthetic-captions --include "sft_dataset_bridge/*" --quiet)/sft_dataset_bridge + +# HF -> DCP +# Use temporary directory, since output is large. +python -m cosmos3.scripts.convert_model_to_dcp \ + --checkpoint-path Cosmos3-Nano \ + -o $TMP_DIR/checkpoint_base + +# Train +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.train \ + -o $OUTPUT_DIR/train \ + --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml \ + $TRAIN_ARGS \ + --config-overrides \ + "checkpoint.load_path=$TMP_DIR/checkpoint_base" \ + "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl" \ + $TRAIN_OVERRIDES + +CHECKPOINT_ITER=$(cat $OUTPUT_DIR/train/job/checkpoints/latest_checkpoint.txt) +CHECKPOINT_PATH=$OUTPUT_DIR/train/job/checkpoints/$CHECKPOINT_ITER + +# Inference +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + --parallelism-preset=latency \ + -i $DATASET_PATH/val/inference_prompt/episode_049683_clip000.json \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path $CHECKPOINT_PATH \ + --config-file $OUTPUT_DIR/train/config.yaml \ + $INFERENCE_ARGS diff --git a/cosmos3/action.py b/cosmos3/action.py new file mode 100644 index 00000000..3f86c69c --- /dev/null +++ b/cosmos3/action.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import torch +from typing_extensions import assert_never + +from cosmos3.args import ActionMode +from cosmos3.vision import read_media_frames +from cosmos3._src.vfm.datasets.action.domain_utils import get_domain_id +from cosmos3._src.vfm.datasets.action.transforms import ( + build_sequence_plan_from_mode, + find_closest_target_size, + pad_action_to_max_dim, + reflection_pad_to_target, +) +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution + + +def _load_actions( + action_path: Path | str | None, + action_mode: ActionMode, + action_chunk_size: int, + max_action_dim: int, + raw_action_dim: int | None, +) -> tuple[torch.Tensor, int]: + """Load actions from JSON (or zeros for policy mode and inverse dynamics mode). Returns ``(padded_action, raw_dim)``.""" + match action_mode: + case ActionMode.FORWARD_DYNAMICS: + assert action_path is not None, "action_path is required for forward_dynamics mode" + p = Path(str(action_path)) + raw = torch.tensor(json.loads(p.read_text()), dtype=torch.float32) + raw_dim = raw.shape[-1] + return pad_action_to_max_dim(raw, max_action_dim), raw_dim + case ActionMode.POLICY | ActionMode.INVERSE_DYNAMICS: + assert raw_action_dim is not None, "raw_action_dim is required for policy and inverse_dynamics modes" + return torch.zeros(action_chunk_size, max_action_dim, dtype=torch.float32), raw_action_dim + case ActionMode.IMAGE2VIDEO: + assert False + case _: + assert_never(action_mode) + + +def build_action_batch( + *, + video: torch.Tensor, + action: torch.Tensor, + raw_action_dim: int, + prompt: str, + domain_name: str, + action_mode: ActionMode, + action_chunk_size: int, + fps: int, + resolution: str | None = None, + input_video_key: str, + duration_template: str | None = None, + resolution_template: str | None = None, + batch_size: int = 1, + device: Any = "cuda", +) -> dict: + """Build an Action data batch from pre-loaded video and action tensors.""" + target_frames = action_chunk_size + 1 + _, num_frames, h, w = video.shape + + if num_frames < target_frames: + pad = video[:, -1:].repeat(1, target_frames - num_frames, 1, 1) + video = torch.cat([video, pad], dim=1) + elif num_frames > target_frames: + video = video[:, :target_frames] + + if resolution is None: + resolution = get_vision_data_resolution((h, w)) + + target_w, target_h = find_closest_target_size(h, w, resolution) + pad_dict: dict[str, Any] = {"video": video} + reflection_pad_to_target(pad_dict, ["video"], keep_aspect_ratio=True, target_w=target_w, target_h=target_h) + video_padded = pad_dict["video"] + padded_image_size = pad_dict["image_size"] + + sequence_plan = build_sequence_plan_from_mode( + mode=action_mode, + video_length=target_frames, + action_length=action_chunk_size, + has_text=True, + ) + + duration_seconds = int(num_frames / fps) if fps > 0 else 0 + ai_caption = prompt.strip() + if duration_template: + ai_caption += duration_template.format(duration=duration_seconds, fps=fps) + if resolution_template: + ai_caption += resolution_template.format(height=target_h, width=target_w) + + return { + input_video_key: [[video_padded]] * batch_size, + "action": [[action]] * batch_size, + "raw_action_dim": [torch.tensor(raw_action_dim, dtype=torch.long)] * batch_size, + "mode": [action_mode.value] * batch_size, + "ai_caption": [ai_caption] * batch_size, + "prompt": [prompt] * batch_size, + "conditioning_fps": [torch.tensor(fps, dtype=torch.long)] * batch_size, + "image_size": padded_image_size.unsqueeze(0).to(device=device), + "domain_id": [torch.tensor(get_domain_id(domain_name), dtype=torch.long)] * batch_size, + "sequence_plan": [sequence_plan] * batch_size, + } + + +def get_action_sample_data( + model_config: Any, + *, + batch_size: int, + prompt: str, + vision_path: Path, + action_mode: ActionMode, + action_path: Path | None, + domain_name: str, + resolution: str, + aspect_ratio: str | None = None, + action_chunk_size: int, + max_action_dim: int, + raw_action_dim: int | None, + duration_template: str | None = None, + resolution_template: str | None = None, + fps: int, + device: Any, +) -> dict: + """Load observation image/video + optional actions and build an Action inference batch.""" + frames, _ = read_media_frames(Path(vision_path), max_frames=action_chunk_size + 1) + assert action_path is not None or raw_action_dim is not None, ( + "Either action_path or raw_action_dim must be provided" + ) + action, raw_action_dim = _load_actions(action_path, action_mode, action_chunk_size, max_action_dim, raw_action_dim) + + return build_action_batch( + video=frames, + action=action, + raw_action_dim=raw_action_dim, + prompt=prompt, + domain_name=domain_name, + action_mode=action_mode, + action_chunk_size=action_chunk_size, + fps=fps, + resolution=resolution, + input_video_key=model_config.input_video_key, + duration_template=duration_template, + resolution_template=resolution_template, + batch_size=batch_size, + device=device, + ) diff --git a/cosmos3/args.py b/cosmos3/args.py new file mode 100644 index 00000000..0df882e7 --- /dev/null +++ b/cosmos3/args.py @@ -0,0 +1,832 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import math +import os +from functools import cache +from pathlib import Path +from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, Self, cast, override + +import pydantic +import pynvml +from typing_extensions import assert_never +from tyro.conf import Suppress + +from cosmos3.common.args import ( + IMAGE_EXTENSIONS, + VIDEO_EXTENSIONS, + ArgsBase, + CfgpSize, + CheckpointConfig, + OverridesBase, + ResolvedFilePath, + ResolvedFilePathOrUrl, + SampleArgs, + SampleOverrides, + SetupArgs, + SetupOverrides, + StrEnum, + Training, + _deep_merge, + download_file, +) +from cosmos3.flags import EARLY_ACCESS +from cosmos3._src.imaginaire.flags import SMOKE, TRAINING +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.checkpoint_db import CheckpointDirHf + +if TYPE_CHECKING: + from cosmos3.common.inference import Inference + from cosmos3._src.vfm.configs.base.defaults.model_config import OmniMoTModelConfig + +_PACKAGE_DIR = Path(__file__).parent + + +@cache +def _load_modality_defaults(model_mode: str) -> dict[str, Any]: + default_file = _PACKAGE_DIR / f"defaults/{model_mode}/sample_args.json" + if not default_file.exists(): + raise FileNotFoundError(f"Missing modality defaults: {default_file}") + return json.loads(default_file.read_text()) + + +Guidance = Annotated[float, pydantic.Field(ge=0, le=7)] +GuidanceInterval = tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat] + + +class SamplingArgs(ArgsBase): + num_steps: pydantic.PositiveInt + guidance: Guidance + guidance_interval: GuidanceInterval | None + normalize_cfg: bool + shift: float + sigma_max: float + + +class SamplingOverrides(OverridesBase): + """Sampling arguments for 'OmniMoTModel.generate_samples'.""" + + num_steps: Training[pydantic.PositiveInt | None] = None + """Number of steps for the diffusion model.""" + guidance: Training[Guidance | None] = None + """Guidance scale for the diffusion model.""" + guidance_interval: Training[GuidanceInterval | None] = None + """Guidance interval for the diffusion model.""" + normalize_cfg: Training[bool | None] = None + """If True, normalize the CFG output.""" + shift: Training[float | None] = None + """Shift in the UniPC sampler. Ignored when sampler='edm'.""" + sigma_max: Training[float | None] = None + """Maximum sigma for the EDM sampler. Ignored when sampler='unipc'.""" + + def _build_sampling(self, model_config: "OmniMoTModelConfig", sample_meta: "SampleMeta"): + assert self.num_steps is not None + if SMOKE: + self.num_steps = min(self.num_steps, 1) + + +InferenceResolution = Literal["256", "480", "720", "1080"] +if TRAINING: + Resolution = Literal["256", "480", "704", "720", "1080"] +else: + Resolution = InferenceResolution +AspectRatio = Literal["1,1", "4,3", "3,4", "16,9", "9,16"] + +# Resolutions that only support image generation (num_frames == 1). Video +# generation at these resolutions is rejected by ``_build_vision_data`` because +# the model wasn't trained on temporal data above 720p and ``MAX_NUM_FRAMES`` +# has no entry for them. +IMAGE_ONLY_RESOLUTIONS: frozenset[str] = frozenset({"1080"}) + +MIN_NUM_FRAMES = 24 +MAX_NUM_FRAMES: dict[Resolution, int] = { + "256": 400, + "480": 300, + "704": 200, + "720": 200, +} + + +class ModelVariant(StrEnum): + VFM = "vfm" + ACTION = "action" + + +ModelSize = Literal["0.6B", "2B", "8B", "30B-A3B", "32B", "235B-A22B"] +PromptUpsamplerVariant = Literal["8B", "32B"] + + +class ModelMode(StrEnum): + TEXT2IMAGE = "text2image" + TEXT2VIDEO = "text2video" + IMAGE2IMAGE = "image2image" + IMAGE2VIDEO = "image2video" + VIDEO2VIDEO = "video2video" + + # Action + FORWARD_DYNAMICS = "forward_dynamics" + INVERSE_DYNAMICS = "inverse_dynamics" + POLICY = "policy" + + +class VisionMode(StrEnum): + IMAGE = "image" + VIDEO = "video" + + +class ConditionVisionMode(StrEnum): + IMAGE = "image" + VIDEO = "video" + + +class ActionMode(StrEnum): + POLICY = "policy" + FORWARD_DYNAMICS = "forward_dynamics" + INVERSE_DYNAMICS = "inverse_dynamics" + IMAGE2VIDEO = "image2video" + + +class NegativeMetadataMode(StrEnum): + NONE = "none" + SAME = "same" + INVERSE = "inverse" + + +class TransferHintKey(StrEnum): + EDGE = "edge" + BLUR = "blur" + DEPTH = "depth" + SEG = "seg" + WSM = "wsm" + + +class PresetEdgeThreshold(StrEnum): + VERY_LOW = "very_low" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + VERY_HIGH = "very_high" + + +class PresetBlurStrength(StrEnum): + NONE = "none" + VERY_LOW = "very_low" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + VERY_HIGH = "very_high" + + +class TransferArgs(ArgsBase): + """Resolved transfer inference arguments for a single control hint.""" + + control_path: ResolvedFilePathOrUrl | None = None + + +class EdgeTransferArgs(TransferArgs): + preset_edge_threshold: PresetEdgeThreshold = PresetEdgeThreshold.MEDIUM + + +class BlurTransferArgs(TransferArgs): + preset_blur_strength: PresetBlurStrength = PresetBlurStrength.MEDIUM + + +class TransferOverrides(OverridesBase): + """Transfer inference overrides for a single control hint (all optional).""" + + control_path: ResolvedFilePathOrUrl | None = None + """Path or URL to pre-computed control input.""" + + def download(self, output_dir: Path): + if self.control_path is not None: + self.control_path = download_file(self.control_path, output_dir, "transfer_control") + + +class EdgeTransferOverrides(TransferOverrides): + preset_edge_threshold: PresetEdgeThreshold | None = None + """Edge detection threshold preset.""" + + +class BlurTransferOverrides(TransferOverrides): + preset_blur_strength: PresetBlurStrength | None = None + """Blur strength preset.""" + + +class SampleMeta(pydantic.BaseModel): + model_variant: ModelVariant + model_mode: ModelMode + vision_mode: VisionMode + condition_vision_mode: ConditionVisionMode | None + action_mode: ActionMode | None = None + + +RESOLUTION_ADAPTER = pydantic.TypeAdapter(Resolution) +ASPECT_RATIO_ADAPTER = pydantic.TypeAdapter(AspectRatio) + +DEFAULT_CONDITION_FRAME_INDEXES_VISION: dict[ConditionVisionMode, list[int]] = { + ConditionVisionMode.IMAGE: [0], + ConditionVisionMode.VIDEO: [0, 1], +} + + +class TextDataArgs(ArgsBase): + prompt: str + + negative_prompt: str | None + + duration_template: str | None + resolution_template: str | None + negative_metadata_mode: NegativeMetadataMode + inverse_duration_template: str + inverse_resolution_template: str + negative_prompt_keep_metadata: bool + + +class TextDataOverrides(OverridesBase): + prompt_path: ResolvedFilePath | None = None + """Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided.""" + prompt: str | None = None + """Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided.""" + + negative_prompt: str | None = None + """Negative prompt - describing what you don't want in the generated video.""" + + duration_template: Training[str | None] = None + """Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders.""" + resolution_template: Training[str | None] = None + """Template string for appending resolution to prompt. Use {height} and {width} placeholders.""" + negative_metadata_mode: Training[NegativeMetadataMode | None] = None + """Negative prompt metadata mode: 'none', 'same', or 'inverse'.""" + inverse_duration_template: Training[str | None] = None + """Inverse template for duration/fps metadata in the negative prompt.""" + inverse_resolution_template: Training[str | None] = None + """Inverse template for resolution metadata in the negative prompt.""" + negative_prompt_keep_metadata: Training[bool | None] = None + """Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.""" + + def _build_text_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): + if self.prompt_path is not None: + self.prompt = self.prompt_path.read_text().strip() + if self.prompt is None: + self.prompt = "" + + if self.negative_prompt_keep_metadata and self.negative_metadata_mode == NegativeMetadataMode.NONE: + self.negative_metadata_mode = NegativeMetadataMode.SAME + + +Fps = Annotated[int, pydantic.Field(ge=1)] +VideoSaveQuality = Annotated[int, pydantic.Field(ge=0, le=10)] +ImageSaveQuality = Annotated[int, pydantic.Field(ge=0, le=100)] + + +class _VisionDataBase: + @property + def vision_mode(self) -> VisionMode: + self = cast(VisionDataOverrides, self) + return VisionMode.IMAGE if self.num_frames == 1 else VisionMode.VIDEO + + @property + def condition_vision_mode(self) -> ConditionVisionMode | None: + self = cast(VisionDataOverrides, self) + if self.vision_path is not None: + vision_ext = Path(self.vision_path).suffix.lower() + if vision_ext in IMAGE_EXTENSIONS: + return ConditionVisionMode.IMAGE + elif vision_ext in VIDEO_EXTENSIONS: + return ConditionVisionMode.VIDEO + else: + raise ValueError(f"Invalid vision extension: {vision_ext}") + else: + return None + + +class VisionDataArgs(ArgsBase, _VisionDataBase): + vision_path: ResolvedFilePath | None + condition_frame_indexes_vision: list[int] + + resolution: Resolution | None + aspect_ratio: AspectRatio | None + fps: pydantic.PositiveInt + num_frames: pydantic.PositiveInt + video_save_quality: VideoSaveQuality + image_save_quality: ImageSaveQuality + + @property + def duration(self) -> float: + return self.num_frames / self.fps + + @property + def vision_size(self) -> tuple[int, int]: + """Vision size (width, height) in pixels.""" + from cosmos3._src.vfm.datasets.utils import IMAGE_RES_SIZE_INFO, VIDEO_RES_SIZE_INFO + + assert self.resolution + assert self.aspect_ratio + if self.num_frames == 1: + return IMAGE_RES_SIZE_INFO[self.resolution][self.aspect_ratio] + else: + return VIDEO_RES_SIZE_INFO[self.resolution][self.aspect_ratio] + + @property + def vision_extension(self) -> str: + match self.vision_mode: + case VisionMode.IMAGE: + return ".jpg" + case VisionMode.VIDEO: + return ".mp4" + case _: + assert_never(self.vision_mode) + + +class VisionDataOverrides(OverridesBase, _VisionDataBase): + # Vision condition fields + vision_path: ResolvedFilePathOrUrl | None = None + """Path or URL to conditioning image/video.""" + condition_frame_indexes_vision: Training[list[int] | None] = None + """Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video.""" + + # Vision fields + resolution: Resolution | None = None + """Vision resolution. + + Defaults to model config resolution. + """ + aspect_ratio: AspectRatio | None = None + """Vision aspect ratio. When None, image_edit preserves the input image's native + aspect ratio; all other modes default to 16:9.""" + fps: Fps | None = None + """Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range.""" + num_frames: pydantic.PositiveInt | None = None + """Number of vision frames. + + Range by resolution: 256p: [24, 400], 480p: [24, 300], 720p: [24, 200]. + Image-only resolutions (e.g. 1080p) require num_frames=1. + """ + video_save_quality: Training[VideoSaveQuality | None] = None + """Quality of the saved video (0-10).""" + image_save_quality: Training[ImageSaveQuality | None] = None + """Quality of the saved image (0-100).""" + + @override + def download(self, output_dir: Path): + super().download(output_dir) + self.vision_path = download_file(self.vision_path, output_dir, "vision") + + def _build_vision_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): + """Finalize and validate in-place.""" + if self.vision_path and "://" in self.vision_path: + raise ValueError("Must call `download()` before building vision data") + + if self.condition_frame_indexes_vision is None: + if sample_meta.condition_vision_mode: + self.condition_frame_indexes_vision = DEFAULT_CONDITION_FRAME_INDEXES_VISION[ + sample_meta.condition_vision_mode + ] + else: + self.condition_frame_indexes_vision = [] + + # Image edit defaults to input image size + if sample_meta.model_mode != ModelMode.IMAGE2IMAGE: + if self.resolution is None: + self.resolution = RESOLUTION_ADAPTER.validate_python(model_config.resolution) + + assert self.num_frames is not None + if self.fps is not None and (self.fps < 10 or self.fps > 30): + log.warning(f"FPS {self.fps} is outside the recommended range [10, 30]. Quality may be degraded.") + if self.num_frames > 1: + assert self.resolution is not None + if self.resolution in IMAGE_ONLY_RESOLUTIONS: + raise ValueError( + f"Resolution {self.resolution!r} only supports image generation (num_frames=1). " + f"For video, use one of: {sorted(MAX_NUM_FRAMES)}" + ) + if self.num_frames < MIN_NUM_FRAMES or self.num_frames > MAX_NUM_FRAMES[self.resolution]: + log.warning( + f"Number of frames {self.num_frames} is outside the recommended range [{MIN_NUM_FRAMES}, {MAX_NUM_FRAMES[self.resolution]}]. Quality may be degraded." + ) + if SMOKE: + self.num_frames = min(self.num_frames, 2) + temporal_compression_factor = model_config.tokenizer.temporal_compression_factor + self.num_frames = ( + math.ceil((self.num_frames - 1) / temporal_compression_factor) * temporal_compression_factor + 1 + ) + + +class SoundDataArgs(ArgsBase): + enable_sound: bool = False + + +class SoundDataOverrides(OverridesBase): + """Sound data overrides.""" + + + +class ActionDataArgs(ArgsBase): + action_mode: ActionMode | None = None + action_path: ResolvedFilePath | None = None + domain_name: str = "" + image_size: pydantic.PositiveInt = 256 + action_chunk_size: pydantic.PositiveInt = 16 + raw_action_dim: int | None = None + + +class ActionDataOverrides(OverridesBase): + """Action data overrides.""" + + action_mode: Training[ActionMode | None] = None + """Action mode. When set, activates Action batch construction.""" + action_path: Training[ResolvedFilePathOrUrl | None] = None + """Path to action JSON file. Required for forward_dynamics mode.""" + domain_name: Training[str | None] = None + """Action domain name passed to get_domain_id().""" + image_size: Training[pydantic.PositiveInt | None] = None + """Target image height in pixels (aspect-ratio-preserving resize).""" + action_chunk_size: Training[pydantic.PositiveInt | None] = None + """Number of action steps to predict.""" + raw_action_dim: Training[pydantic.PositiveInt | None] = None + """Dimension of the raw action data. Required when action_path is not provided.""" + + @override + def download(self, output_dir: Path): + super().download(output_dir) + self.action_path = download_file(self.action_path, output_dir, "action") + + def _build_action_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): + if self.action_mode is not None: + match self.action_mode: + case ActionMode.IMAGE2VIDEO: + pass + case ActionMode.FORWARD_DYNAMICS: + if self.action_path is None: + raise ValueError("'action_path' is required") + case ActionMode.INVERSE_DYNAMICS | ActionMode.POLICY: + if self.raw_action_dim is None: + raise ValueError("'raw_action_dim' is required") + case _: + assert_never(self.action_mode) + if self.action_path and "://" in self.action_path: + raise ValueError("Must call `download()` before building action data") + + if self.domain_name is None: + self.domain_name = "" + if self.image_size is None: + self.image_size = 256 + if self.action_chunk_size is None: + self.action_chunk_size = 16 + + + + +class _SampleDataBase: + @property + def model_variant(self) -> ModelVariant: + self = cast(SampleDataOverrides, self) + if self.action_mode is not None: + return ModelVariant.ACTION + return ModelVariant.VFM + + @property + def model_mode(self) -> ModelMode: + self = cast(SampleDataOverrides, self) + match self.model_variant: + case ModelVariant.VFM: + input_mode = self.condition_vision_mode or "text" + output_mode = self.vision_mode + return ModelMode(f"{input_mode}2{output_mode}") + case ModelVariant.ACTION: + assert self.action_mode + return ModelMode(self.action_mode.value) + case _: + assert_never(self.model_variant) + + @property + def sample_meta(self) -> SampleMeta: + self = cast(SampleDataOverrides, self) + return SampleMeta( + model_variant=self.model_variant, + model_mode=self.model_mode, + vision_mode=self.vision_mode, + condition_vision_mode=self.condition_vision_mode, + action_mode=self.action_mode, + ) + + +class SampleDataArgs( + _SampleDataBase, + TextDataArgs, + VisionDataArgs, + SoundDataArgs, + ActionDataArgs, +): ... + + +class SampleDataOverrides( + _SampleDataBase, + TextDataOverrides, + VisionDataOverrides, + SoundDataOverrides, + ActionDataOverrides, +): + """Sample data arguments for 'OmniMoTModel.generate_samples'.""" + + +class OmniSampleArgs(SampleArgs, SamplingArgs, SampleDataArgs): ... + + +class OmniSampleOverrides(SampleOverrides, SamplingOverrides, SampleDataOverrides): + defaults_file: ResolvedFilePath | None = None + """Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults.""" + + + _VLM_MODEL_SIZE: ClassVar[dict[str, ModelSize]] = { + "Qwen/Qwen3-0.6B": "0.6B", + "Qwen/Qwen3-VL-2B-Instruct": "2B", + "Qwen/Qwen3-VL-8B-Instruct": "8B", + "Qwen/Qwen3-VL-32B-Instruct": "32B", + "Qwen/Qwen3-VL-30B-A3B-Instruct": "30B-A3B", + "Qwen/Qwen3-VL-235B-A22B-Instruct": "235B-A22B", + } + + _RESOLUTION_SHIFT_DEFAULTS: ClassVar[dict[(ModelSize, Resolution), float]] = { + ("8B", "256"): 3.0, + ("8B", "480"): 5.0, + ("8B", "720"): 10.0, + ("32B", "256"): 5.0, + ("32B", "480"): 5.0, + ("32B", "720"): 5.0, + } + + @override + def build_sample(self, *, model_config: Any) -> OmniSampleArgs: + model_config = cast("OmniMoTModelConfig", model_config) + sample_meta = self.sample_meta + + # Apply per-modality defaults from JSON config files. + # User-provided values take precedence over JSON defaults. + if self.defaults_file is not None: + defaults = json.loads(self.defaults_file.read_text()) + else: + defaults = _load_modality_defaults(sample_meta.model_mode) + overrides = self.model_dump(exclude_none=True) + user_specified_shift = "shift" in overrides + merged_data = _deep_merge(defaults, overrides) + merged_data = {k: v for k, v in merged_data.items() if k in type(self).model_fields} + merged = type(self).model_validate(merged_data) + + self.__dict__.update(merged.__dict__) + + self._build_sample() + self._build_sampling(model_config=model_config, sample_meta=sample_meta) + self._build_text_data(model_config=model_config, sample_meta=sample_meta) + self._build_vision_data(model_config=model_config, sample_meta=sample_meta) + + + + self._build_action_data(model_config=model_config, sample_meta=sample_meta) + + + if not user_specified_shift: + model_size = self._VLM_MODEL_SIZE[model_config.vlm_config.model_name] + key = (model_size, self.resolution) + if key in self._RESOLUTION_SHIFT_DEFAULTS: + self.shift = self._RESOLUTION_SHIFT_DEFAULTS[key] + + return self._build(OmniSampleArgs) + + + +_MODEL_MEMORY_FACTOR: int = int(1e9) * 2 * 2 # 1B params/tower * 2 bytes/param (bfloat16) * 2 towers +MODEL_MEMORY_BYTES_BY_SIZE: dict[ModelSize, int] = { + "0.6B": round(0.6 * _MODEL_MEMORY_FACTOR), + "2B": 2 * _MODEL_MEMORY_FACTOR, + "8B": 8 * _MODEL_MEMORY_FACTOR, + "30B-A3B": 30 * _MODEL_MEMORY_FACTOR, + "32B": 32 * _MODEL_MEMORY_FACTOR, + "235B-A22B": 235 * _MODEL_MEMORY_FACTOR, +} + +_CHECKPOINTS_EXPERIMENTAL: dict[str, CheckpointConfig] = { + # 0.6B + "5d561d7d-080f-45cb-a455-920d444e40cc": CheckpointConfig( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["0.6B"], + s3_uri="s3://bucket/cosmos3_vfm/t2w_mot_0p6b_qwen3_vl_runs/t2w_mot_dryrun_exp200_001_qwen3_vl_0p6b_480res_qwen3_captions_mrope_v2/checkpoints/iter_000079500/", + hf=CheckpointDirHf( + repository="nvidia/Cosmos3-Experimental", + subdirectory="5d561d7d-080f-45cb-a455-920d444e40cc", + revision="844eb561ec6a8d6a917aec463464cdd594d5e965", + ), + ), + # 8B + "5fabe660-7021-4286-96ec-e1858d194b82": CheckpointConfig( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + s3_uri="s3://bucket/cosmos3_vfm/t2w_mot_8b_qwen3_vl_runs/t2w_mot_exp506_000_qwen3_vl_8b_multires_recipe_midtraining_v1/checkpoints/iter_000040000/", + hf=CheckpointDirHf( + repository="nvidia/Cosmos3-Nano-Internal", + revision="dd232bc1219d8dd724f54027a6f5c987b91f0623", + ), + ), + # 32B + "7415f3d4-91e5-4df4-baaa-f09ff4dafd5e": CheckpointConfig( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + s3_uri="s3://bucket/cosmos3_vfm/t2w_mot_32b_qwen3_vl_runs/t2w_mot_exp305_000_qwen3_vl_32b_multires_v7/checkpoints/iter_000085000/", + hf=CheckpointDirHf( + repository="nvidia/Cosmos3-Super-Internal", + revision="0cb696a450e36e2be48402d5815fdcca2d11050d", + ), + ), +} +_CHECKPOINTS_EA = { + "5fabe660-7021-4286-96ec-e1858d194b82": CheckpointConfig( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + s3_uri="s3://bucket/cosmos3_vfm/t2w_mot_8b_qwen3_vl_runs/t2w_mot_exp506_000_qwen3_vl_8b_multires_recipe_midtraining_v1/checkpoints/iter_000040000/", + hf=CheckpointDirHf( + repository="nvidia-cosmos-ea/Cosmos3-Nano", + revision="ebbe03fe665661d0eab87130a341184efecca365", + ), + ), + "7415f3d4-91e5-4df4-baaa-f09ff4dafd5e": CheckpointConfig( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + s3_uri="s3://bucket/cosmos3_vfm/t2w_mot_32b_qwen3_vl_runs/t2w_mot_exp305_000_qwen3_vl_32b_multires_v7/checkpoints/iter_000085000/", + hf=CheckpointDirHf( + repository="nvidia-cosmos-ea/Cosmos3-Super", + revision="824198211d88e556645ec1c38bd37c3581775268", + ), + ), +} +_CHECKPOINTS = _CHECKPOINTS_EXPERIMENTAL.copy() +if EARLY_ACCESS: + _CHECKPOINTS.update(_CHECKPOINTS_EA) +_CHECKPOINTS["Cosmos3-Test"] = _CHECKPOINTS["Cosmos3-0.6B"] = _CHECKPOINTS["5d561d7d-080f-45cb-a455-920d444e40cc"] +_CHECKPOINTS["Cosmos3-Nano"] = _CHECKPOINTS["Cosmos3-8B"] = _CHECKPOINTS["5fabe660-7021-4286-96ec-e1858d194b82"] +_CHECKPOINTS["Cosmos3-Super"] = _CHECKPOINTS["Cosmos3-32B"] = _CHECKPOINTS["7415f3d4-91e5-4df4-baaa-f09ff4dafd5e"] +DEFAULT_CHECKPOINT_NAME = "Cosmos3-Test" if SMOKE else "Cosmos3-Nano" +DEFAULT_CHECKPOINT = _CHECKPOINTS[DEFAULT_CHECKPOINT_NAME] + +# CP size cannot exceed number of KV heads (8) +MAX_CP_SIZE = 8 +CpSize = Annotated[int, pydantic.Field(ge=1, le=MAX_CP_SIZE)] + + +class OmniSetupArgs(SetupArgs): + variant: Suppress[Literal["omni"]] = "omni" + """Discriminator.""" + + # pyrefly: ignore[bad-override] + sample_overrides: OmniSampleOverrides + + sampler: Literal["unipc", "edm"] + + # Override defaults + cp_size: CpSize + + @override + @classmethod + def get_sample_overrides_cls(cls) -> type[SampleOverrides]: + return OmniSampleOverrides + + @override + @classmethod + def get_sample_args_cls(cls) -> type[SampleArgs]: + return OmniSampleArgs + + @override + @classmethod + def get_inference_cls(cls) -> type["Inference"]: + from cosmos3.inference import OmniInference + + return OmniInference + + @pydantic.model_validator(mode="after") + def _validate_parallelism(self) -> Self: + world_size = int(os.environ.get("WORLD_SIZE", "0")) + + if world_size: + if self.dp_shard_size * self.dp_replicate_size > world_size: + raise ValueError( + f"dp_shard_size({self.dp_shard_size}) * dp_replicate_size({self.dp_replicate_size}) must be <= WORLD_SIZE({world_size})" + ) + if world_size % (self.dp_shard_size * self.dp_replicate_size) != 0: + raise ValueError( + f"dp_shard_size({self.dp_shard_size}) * dp_replicate_size({self.dp_replicate_size}) must divide WORLD_SIZE({world_size})" + ) + + if world_size: + if self.cp_size * self.cfgp_size > world_size: + raise ValueError( + f"cp_size({self.cp_size}) * cfgp_size({self.cfgp_size}) must be <= WORLD_SIZE({world_size})" + ) + if world_size % (self.cp_size * self.cfgp_size) != 0: + raise ValueError( + f"cp_size({self.cp_size}) * cfgp_size({self.cfgp_size}) must divide WORLD_SIZE({world_size})" + ) + + return self + + +class OmniSetupOverrides(SetupOverrides): + variant: Suppress[Literal["omni"]] = "omni" + """Discriminator.""" + + CHECKPOINTS: ClassVar[dict[str, CheckpointConfig]] = _CHECKPOINTS + + sample_overrides: OmniSampleOverrides = OmniSampleOverrides() + + model_size: Training[ModelSize | None] = None + + sampler: Literal["unipc", "edm"] = "unipc" + + # Override defaults + dp_replicate_size: pydantic.NonNegativeInt = 0 + dp_shard_size: pydantic.NonNegativeInt = 0 + cp_size: CpSize | Literal[0] = 0 + cfgp_size: CfgpSize | Literal[0] = 0 + + use_cuda_graphs: bool = False + + compiled_region: Literal["all", "language"] = "all" + # Unsupported + tp_size: Suppress[pydantic.NonNegativeInt] = 1 + + def _build_model_parallelism(self, world_size: int, device_memory_bytes: int): + if not self.dp_shard_size: + if self.model_memory_bytes: + self.dp_shard_size = _get_dp_shard_size( + model_memory_bytes=self.model_memory_bytes, + device_memory_bytes=device_memory_bytes, + device_memory_utilization=self.device_memory_utilization, + ) + else: + self.dp_shard_size = 1 + if not self.dp_replicate_size: + self.dp_replicate_size = max(1, world_size // self.dp_shard_size) + + def _build_context_parallelism(self, world_size: int): + if not self.cfgp_size: + match self.parallelism_preset: + case "throughput": + self.cfgp_size = 1 + case "latency": + self.cfgp_size = max(1, min(2, world_size)) + case _: + assert_never(self.parallelism_preset) + if not self.cp_size: + match self.parallelism_preset: + case "throughput": + self.cp_size = 1 + case "latency": + self.cp_size = max(1, min(MAX_CP_SIZE, world_size // self.cfgp_size)) + case _: + assert_never(self.parallelism_preset) + + @override + def _build_parallelism(self, world_size: int | None, local_world_size: int | None, device_memory_bytes: int | None): + if world_size is None: + world_size = int(os.environ.get("WORLD_SIZE", "0")) + if local_world_size is None: + local_world_size = int(os.environ.get("LOCAL_WORLD_SIZE", str(world_size))) + if device_memory_bytes is None: + device_memory_bytes = _get_device_memory_bytes() + + if self.model_memory_bytes is None and self.model_size is not None: + self.model_memory_bytes = MODEL_MEMORY_BYTES_BY_SIZE[self.model_size] + self._build_model_parallelism(world_size=world_size, device_memory_bytes=device_memory_bytes) + self._build_context_parallelism(world_size=world_size) + + @override + def build_setup( + self, world_size: int | None = None, local_world_size: int | None = None, device_memory_bytes: int | None = None + ) -> OmniSetupArgs: + self._build_setup() + self._build_checkpoint(checkpoints=self.CHECKPOINTS) + self._build_parallelism( + world_size=world_size, local_world_size=local_world_size, device_memory_bytes=device_memory_bytes + ) + return self._build(OmniSetupArgs) + + +def _get_dp_shard_size( + model_memory_bytes: int, device_memory_bytes: int, device_memory_utilization: float = 0.75 +) -> int: + return math.ceil(model_memory_bytes / device_memory_bytes / device_memory_utilization) + + +@cache +def _get_device_memory_bytes() -> int: + pynvml.nvmlInit() + handle = pynvml.nvmlDeviceGetHandleByIndex(0) + info = pynvml.nvmlDeviceGetMemoryInfo(handle) + pynvml.nvmlShutdown() + return info.total diff --git a/cosmos3/args_test.py b/cosmos3/args_test.py new file mode 100644 index 00000000..62a76166 --- /dev/null +++ b/cosmos3/args_test.py @@ -0,0 +1,212 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path + +import omegaconf +import pytest +from typing_extensions import TYPE_CHECKING + +from cosmos3.args import ( + _CHECKPOINTS_EA, + _CHECKPOINTS_EXPERIMENTAL, + DEFAULT_CHECKPOINT, + DEFAULT_CHECKPOINT_NAME, + MODEL_MEMORY_BYTES_BY_SIZE, + OmniSampleOverrides, + OmniSetupOverrides, + _get_dp_shard_size, +) +from cosmos3.common.args import ParallelismOverrides +from cosmos3.common.config import structure_config +from cosmos3.model import Cosmos3OmniConfig + +if TYPE_CHECKING: + from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + +_H100_MEMORY_BYTES = 80 * 1024**3 +_GB200_MEMORY_BYTES = 192 * 1024**3 + + +def test_dp_shard_size(): + device_memory_utilization = ParallelismOverrides().device_memory_utilization + assert ( + _get_dp_shard_size( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + device_memory_bytes=_H100_MEMORY_BYTES, + device_memory_utilization=device_memory_utilization, + ) + == 1 + ) + assert ( + _get_dp_shard_size( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + device_memory_bytes=_H100_MEMORY_BYTES, + device_memory_utilization=device_memory_utilization, + ) + == 2 + ) + assert ( + _get_dp_shard_size( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + device_memory_bytes=_GB200_MEMORY_BYTES, + device_memory_utilization=device_memory_utilization, + ) + == 1 + ) + assert ( + _get_dp_shard_size( + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + device_memory_bytes=_GB200_MEMORY_BYTES, + device_memory_utilization=device_memory_utilization, + ) + == 1 + ) + + +def test_build_parallelism(monkeypatch: pytest.MonkeyPatch): + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + parallelism_preset="latency", + ).build_parallelism(world_size=16, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.dp_shard_size == 1 + assert parallelism_args.dp_replicate_size == 16 + assert parallelism_args.cp_size == 8 + assert parallelism_args.cfgp_size == 2 + + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + parallelism_preset="throughput", + ).build_parallelism(world_size=16, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.dp_shard_size == 1 + assert parallelism_args.dp_replicate_size == 16 + assert parallelism_args.cp_size == 1 + assert parallelism_args.cfgp_size == 1 + + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + parallelism_preset="latency", + ).build_parallelism(world_size=16, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.dp_shard_size == 2 + assert parallelism_args.dp_replicate_size == 8 + assert parallelism_args.cp_size == 8 + assert parallelism_args.cfgp_size == 2 + + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + parallelism_preset="throughput", + ).build_parallelism(world_size=16, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.dp_shard_size == 2 + assert parallelism_args.dp_replicate_size == 8 + assert parallelism_args.cp_size == 1 + assert parallelism_args.cfgp_size == 1 + + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["32B"], + parallelism_preset="latency", + ).build_parallelism(world_size=0, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.dp_shard_size == 2 + assert parallelism_args.dp_replicate_size == 1 + assert parallelism_args.cp_size == 1 + assert parallelism_args.cfgp_size == 1 + + parallelism_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir="outputs", + model_memory_bytes=MODEL_MEMORY_BYTES_BY_SIZE["8B"], + parallelism_preset="latency", + compile_dynamic=False, + ).build_parallelism(world_size=16, device_memory_bytes=_H100_MEMORY_BYTES) + assert parallelism_args.compile_dynamic is False + + +def _normalize_s3_uri(uri: str) -> str: + # Format '{project}/{group}/{name}/checkpoints/iter_{iter}/model' + uri = uri.rstrip("/").removesuffix("/model") + parts = Path(uri).parts + assert len(parts) >= 5 + return "/".join(parts[-5:]) + + +def test_checkpoints(): + assert set(_CHECKPOINTS_EA).issubset(_CHECKPOINTS_EXPERIMENTAL) + for name, ckpt in _CHECKPOINTS_EXPERIMENTAL.items(): + + if name in ["Cosmos3-2B-Action-Libero"]: + continue + + assert ckpt.hf.repository.split("/")[0] == "nvidia" + + # Download a file to ensure that the repository/revision is valid + ckpt_hf = ckpt.hf.model_copy(update=dict(include=("checkpoint.json",))) + cfg = json.loads((Path(ckpt_hf.download()) / "checkpoint.json").read_text()) + s3_uri = cfg["checkpoint_path"] + assert _normalize_s3_uri(ckpt.s3_uri) == _normalize_s3_uri(s3_uri) + + if name in _CHECKPOINTS_EA: + ckpt_ea = _CHECKPOINTS_EA[name] + assert _normalize_s3_uri(ckpt_ea.s3_uri) == _normalize_s3_uri(s3_uri) + assert ckpt_ea.hf.repository.split("/")[0] == "nvidia-cosmos-ea" + + # Download a file to ensure that the repository/revision is valid + ckpt_ea_hf = ckpt_ea.hf.model_copy(update=dict(include=("checkpoint.json",))) + # Check that checkpoints are identical + cfg_ea = json.loads((Path(ckpt_ea_hf.download()) / "checkpoint.json").read_text()) + assert cfg_ea == cfg + + +def test_setup_args(tmp_path: Path): + overrides = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir=tmp_path / "outputs", + ) + args = overrides.build_setup() + + # Check idempotent + assert overrides.build_setup() == args + assert OmniSetupOverrides.model_validate(args.model_dump()).build_setup() == args + + +def test_sample_args(tmp_path: Path): + hf_config = Cosmos3OmniConfig.from_pretrained( + **DEFAULT_CHECKPOINT.pretrained_kwargs, + ) + model_dict: "OmniMoTModel" = structure_config(hf_config.model, omegaconf.DictConfig) + + # Check that all fields are optional + for name, field in OmniSampleOverrides.model_fields.items(): + assert field.default is None, name + + overrides = OmniSampleOverrides( + name="test", + ) + overrides.output_dir = tmp_path / "inputs" + args = overrides.build_sample(model_config=model_dict.config) + + # Check idempotent + assert overrides.build_sample(model_config=model_dict.config) == args + overrides_dump = {k: v for k, v in args.model_dump().items() if k in OmniSampleOverrides.model_fields} + assert OmniSampleOverrides.model_validate(overrides_dump).build_sample(model_config=model_dict.config) == args diff --git a/cosmos3/common/__init__.py b/cosmos3/common/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/common/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/common/args.py b/cosmos3/common/args.py new file mode 100644 index 00000000..eb3809c9 --- /dev/null +++ b/cosmos3/common/args.py @@ -0,0 +1,862 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import glob +import itertools +import json +import os +import re +import tempfile +from abc import ABC, abstractmethod +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + Literal, + NoReturn, + TypeVar, + overload, + override, +) + +import pydantic +import tyro +import yaml +from typing_extensions import Self, assert_never +from tyro.conf import Suppress + +from cosmos3.common.config import deserialize_config_dict, unstructure_config +from cosmos3.common.init import is_rank0 as is_rank0 +from cosmos3._src.imaginaire.flags import TRAINING, StrEnum +from cosmos3._src.imaginaire.utils.checkpoint_db import CheckpointDirHf + +if TYPE_CHECKING: + from cosmos3.common.inference import Inference + from cosmos3._src.imaginaire.config import Config + +if TRAINING or TYPE_CHECKING: + T = TypeVar("T") + Training = Annotated[T, None] +else: + Training = Suppress + + +IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp"] +VIDEO_EXTENSIONS = [".mp4"] +MEDIA_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS + + +def _download_file_url(url: str, path: Path): + if "huggingface.co" in url: + _download_file_hf(url, path) + else: + import obstore + + base_url, file_name = url.rsplit("/", 1) + store = obstore.store.from_url(base_url) + result = obstore.get(store, file_name) + with path.open("wb") as f: + for chunk in iter(result): + f.write(chunk) + + +def _download_file_hf(url: str, path: Path): + """Download from HuggingFace with token auth.""" + import urllib.request + + url = url.replace("/blob/", "/resolve/") + headers: dict[str, str] = {} + token = os.environ.get("HF_TOKEN") + if not token: + token_path = Path.home() / ".cache" / "huggingface" / "token" + if token_path.is_file(): + token = token_path.read_text().strip() + if token: + headers["Authorization"] = f"Bearer {token}" + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req) as resp, path.open("wb") as f: + while chunk := resp.read(8192): + f.write(chunk) + + +def _download_file(url: str, path: Path): + if "://" not in url and Path(url).resolve() == path.resolve(): + return + meta_path = Path(f"{path}.meta") + if path.exists() and meta_path.exists(): + if json.loads(meta_path.read_text())["url"] == url: + return + + if "://" in url: + # Download to a temporary directory and symlink to the final path. + # This keeps the output directory small. + local_path = Path(tempfile.TemporaryDirectory(delete=False).name) / path.name + _download_file_url(url, local_path) + else: + local_path = Path(url) + + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + path.unlink() + path.symlink_to(local_path) + meta_path.write_text(json.dumps({"url": url})) + + +@overload +def download_file(url: str, output_dir: Path, output_name: str) -> str: ... + + +@overload +def download_file(url: None, output_dir: Path, output_name: str) -> None: ... + + +def download_file(url, output_dir, output_name): + """Download a file from a URL to a local path. + + * Skip if the file already exists. + * Only download on rank 0. + """ + if not url or "://" not in url: + return url + ext = Path(url).suffix.lower() + download_path = output_dir / f"{output_name}{ext}" + if is_rank0(): + _download_file(url, download_path) + return str(download_path) + + +@overload +def path_to_str(v: Path | str) -> str: ... + + +@overload +def path_to_str(v: None) -> None: ... + + +def path_to_str(v): + """Convert optional path to optional string.""" + if v is None: + return None + return str(v) + + +@overload +def str_to_path(v: Path | str) -> Path: ... + + +@overload +def str_to_path(v: None) -> None: ... + + +def str_to_path(v): + if v is None: + return None + return Path(v) + + +_PydanticModelT = TypeVar("_PydanticModelT", bound=pydantic.BaseModel) + + +def _get_root_exception(exception: BaseException) -> BaseException: + if exception.__cause__ is not None: + return _get_root_exception(exception.__cause__) + if exception.__context__ is not None: + return _get_root_exception(exception.__context__) + return exception + + +def handle_tyro_exception(exception: Exception) -> NoReturn: + root_exception = _get_root_exception(exception) + if isinstance(root_exception, pydantic.ValidationError): + raise root_exception from None + raise exception + + +_T = TypeVar("_T") + + +def tyro_cli(cls: type[_T], **kwargs) -> _T: + kwargs.setdefault("console_outputs", is_rank0()) + try: + return tyro.cli(cls, **kwargs) + except Exception as e: + handle_tyro_exception(e) + + +def _resolve_path(v: Path) -> Path: + """Resolve path to absolute.""" + return v.expanduser().absolute() + + +def _resolve_file_or_url(v: str) -> str: + """Validate a file path or URL. URLs pass through; local paths must exist and are resolved to absolute.""" + if v.startswith(("http://", "https://", "s3://")): + return v + p = Path(v).expanduser().absolute() + if not p.is_file(): + raise ValueError(f"Path does not point to a file: {v}") + return str(p) + + +ResolvedPath = Annotated[Path, pydantic.AfterValidator(_resolve_path)] +ResolvedFilePath = Annotated[pydantic.FilePath, pydantic.AfterValidator(_resolve_path)] +ResolvedFilePathOrUrl = Annotated[str, pydantic.AfterValidator(_resolve_file_or_url)] +ResolvedDirectoryPath = Annotated[pydantic.DirectoryPath, pydantic.AfterValidator(_resolve_path)] + + +class ArgsBase(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid", use_attribute_docstrings=True) + + @classmethod + def from_files(cls, paths: list[Path]) -> list[Self]: + return from_files(cls, paths) + + +_ArgsT = TypeVar("_ArgsT", bound=ArgsBase) + + +class OverridesBase(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid", use_attribute_docstrings=True) + + @classmethod + def from_files(cls, paths: list[Path], *, overrides: pydantic.BaseModel | None = None) -> list[Self]: + return from_files(cls, paths, overrides=overrides) + + def download(self, output_dir: Path): + """Download all urls.""" + pass + + def _build(self, target: type[_ArgsT], **kwargs) -> _ArgsT: + """Build arguments from overrides.""" + return target.model_validate(self.model_dump() | kwargs, extra="ignore") + + +_OverridesT = TypeVar("_OverridesT", bound=OverridesBase) + + +class ConfigFileType(StrEnum): + """Config file type.""" + + MODULE = "module" + """Hydra config store module.""" + YAML = "yaml" + """Hydra config yaml.""" + JSON = "json" + """Hugging Face model json.""" + + @classmethod + def from_path(cls, path: str) -> Self: + suffix = Path(path).suffix.lower() + if suffix == ".py": + return cls("module") + if suffix in [".yaml", ".yml"]: + return cls("yaml") + if suffix == ".json": + return cls("json") + raise ValueError(f"Invalid config file: {path}") + + +def _validate_config_file(v: str) -> str: + config_file_type = ConfigFileType.from_path(v) + if config_file_type == ConfigFileType.MODULE: + # Relative module path + return v + + # Absolute file path + p = Path(v).expanduser().absolute() + if not p.is_file(): + raise ValueError(f"Config file does not exist: {v}") + return str(p) + + +ConfigFilePath = Annotated[str, pydantic.AfterValidator(_validate_config_file)] + + +class ConfigArgs(ArgsBase): + config_file: ConfigFilePath + config_file_type: ConfigFileType + experiment: str + experiment_overrides: list[str] + + def load_config(self) -> "Config": + """Load Hydra config.""" + from cosmos3.common.config import load_config + + if self.config_file_type != ConfigFileType.MODULE: + raise ValueError(f"Invalid config file type: {self.config_file_type}") + return load_config(self.config_file, self.experiment, overrides=self.experiment_overrides) + + def load_model_config_dict(self) -> dict: + """Load model config dict.""" + match self.config_file_type: + case ConfigFileType.MODULE: + return unstructure_config(self.load_config().model) + case ConfigFileType.YAML | ConfigFileType.JSON: + return deserialize_config_dict(Path(self.config_file))["model"] + case _: + assert_never(self.config_file_type) + + +DEFAULT_CONFIG_FILE = "cosmos3/_src/vfm/configs/base/config.py" + + +class ConfigOverrides(OverridesBase): + """Hydra config arguments.""" + + config_file: Training[ConfigFilePath] = DEFAULT_CONFIG_FILE + """Hydra config store module, Hydra config yaml file, or Hugging Face model json file.""" + config_file_type: Suppress[ConfigFileType | None] = None + """Hydra config file type.""" + experiment: Training[str] = "" + """Hydra experiment name.""" + experiment_overrides: Training[list[str]] = pydantic.Field(default_factory=list) + """Hydra experiment overrides.""" + + def _build_config(self) -> None: + self.config_file_type = ConfigFileType.from_path(self.config_file) + + def build_config(self) -> ConfigArgs: + self._build_config() + return self._build(ConfigArgs) + + +class CheckpointType(StrEnum): + """Checkpoint type.""" + + HF = "hf" + """Hugging Face checkpoint.""" + DCP = "dcp" + """DCP checkpoint.""" + + @classmethod + def from_path(cls, path: Path) -> Self: + if any(path.glob("*.safetensors")): + if not (path / "config.json").exists(): + raise ValueError(f"Invalid Hugging Face checkpoint: {path}") + return cls("hf") + if any(path.glob("*.distcp")): + if not (path / ".metadata").exists(): + raise ValueError(f"Invalid DCP checkpoint: {path}") + return cls("dcp") + raise ValueError(f"Unknown checkpoint type: {path}") + + +class CheckpointConfig(pydantic.BaseModel): + """Checkpoint config.""" + + model_config = pydantic.ConfigDict(extra="forbid", frozen=True) + + model_memory_bytes: int | None = None + """Approximate model size in bytes. + + Used for automatic sharding. + """ + + s3_uri: str + """Checkpoint S3 URI.""" + hf: CheckpointDirHf + """Config for checkpoint on Hugging Face.""" + + def download(self) -> str: + return self.hf.download() + + @property + def pretrained_kwargs(self) -> dict[str, Any]: + return dict( + pretrained_model_name_or_path=self.hf.repository, + subfolder=self.hf.subdirectory, + revision=self.hf.revision, + ) + + +class CheckpointArgs(ConfigArgs): + checkpoint_path: str + + checkpoint_type: CheckpointType + model_memory_bytes: int | None + + checkpoint_hf: CheckpointDirHf | None + + credential_path: str + use_ema_weights: bool + checkpoint_cache_dir: Path | None + + def download_checkpoint(self) -> Path: + if self.checkpoint_hf is not None: + return Path(self.checkpoint_hf.download()) + if "://" in self.checkpoint_path: + raise ValueError(f"Invalid checkpoint path: {self.checkpoint_path}") + return Path(self.checkpoint_path) + + @pydantic.model_validator(mode="after") + def _validate_checkpoint(self) -> Self: + if self.checkpoint_type == CheckpointType.DCP: + if not self.config_file: + raise ValueError("'config_file' is required") + if self.config_file_type == ConfigFileType.MODULE and not self.experiment: + raise ValueError("'experiment' is required") + return self + + +class CheckpointOverrides(ConfigOverrides): + """Checkpoint arguments.""" + + checkpoint_path: str + """Model name or path. + + * Model name: Cosmos3-Nano + * Local path: /path/to/checkpoint + """ + + checkpoint_type: Suppress[CheckpointType | None] = None + """Checkpoint type.""" + model_memory_bytes: Suppress[int | None] = None + """Approximate model size in bytes.""" + + checkpoint_hf: Suppress[CheckpointDirHf | None] = None + """Hugging Face checkpoint directory.""" + + credential_path: Training[str] = "credentials/gcp_checkpoint.secret" + """Path to S3 credentials file for remote checkpoint loading.""" + use_ema_weights: Training[bool] = True + """If True, use EMA weights. Otherwise, use regular weights.""" + checkpoint_cache_dir: Training[Path | None] = None + """Directory for caching S3 checkpoints.""" + + def _build_checkpoint(self, checkpoints: dict[str, CheckpointConfig]): + # Detect checkpoint type + if self.checkpoint_path in checkpoints: + self.checkpoint_type = CheckpointType.HF + checkpoint = checkpoints[self.checkpoint_path] + if not self.model_memory_bytes: + self.model_memory_bytes = checkpoint.model_memory_bytes + self.checkpoint_hf = checkpoint.hf + elif self.checkpoint_path.startswith("s3://"): + self.checkpoint_type = CheckpointType.DCP + self.checkpoint_path = self.checkpoint_path.rstrip("/") + # Strip '/model' suffix, since it isn't included in checkpoint_db. + # Automatically added during checkpoint load by + # 'cosmos3._src.vfm.utils.model_loader.load_model_from_checkpoint'. + if not self.checkpoint_path.endswith("/model"): + self.checkpoint_path = self.checkpoint_path + "/model" + else: + checkpoint_dir = Path(self.checkpoint_path).expanduser().absolute() + if not checkpoint_dir.is_dir(): + raise ValueError(f"Checkpoint directory does not exist: {checkpoint_dir}") + if (checkpoint_dir / "model").is_dir(): + checkpoint_dir = checkpoint_dir / "model" + self.checkpoint_path = str(checkpoint_dir) + self.checkpoint_type = CheckpointType.from_path(checkpoint_dir) + if (checkpoint_dir / "config.json").is_file(): + self.config_file = str(checkpoint_dir / "config.json") + self.config_file_type = ConfigFileType.from_path(self.config_file) + + if self.checkpoint_type == CheckpointType.DCP and self.config_file_type == ConfigFileType.MODULE: + # Infer missing values from checkpoint path + if not self.experiment: + pattern = r"/(?P[\w-]+)/checkpoints/iter_(?P\d+)/" + match = re.search(pattern, f"/{self.checkpoint_path}/") + if match is None: + raise ValueError(f"Could not infer experiment from checkpoint path: {self.checkpoint_path}") + if not self.experiment: + self.experiment = match.group("experiment") + + self.experiment_overrides = [ + f"checkpoint.load_from_object_store.enabled={self.checkpoint_path.startswith('s3://')}", + # Pretrained weights are only needed for training. + # See 'cosmos3._src.vfm.configs.base.config.Config._set_skip_pretrained_if_checkpoint_exists()'. + "model.config.vlm_config.load_pretrained=False", + "model.config.diffusion_expert_config.load_weights_from_pretrained=False", + *self.experiment_overrides, + ] + + def build_checkpoint(self, *, checkpoints: dict[str, CheckpointConfig]) -> CheckpointArgs: + self._build_checkpoint(checkpoints=checkpoints) + self._build_config() + return self._build(CheckpointArgs) + + +ParallelismPreset = Literal["throughput", "latency"] +CfgpSize = Annotated[int, pydantic.Field(ge=1, le=2)] +CompiledRegion = Literal["all", "language"] + + +class ParallelismArgs(ArgsBase): + """Parallelism arguments.""" + + dp_replicate_size: pydantic.PositiveInt + dp_shard_size: pydantic.PositiveInt + tp_size: pydantic.PositiveInt + cp_size: pydantic.PositiveInt + cfgp_size: CfgpSize + use_torch_compile: bool + use_cuda_graphs: bool + compiled_region: CompiledRegion + compile_dynamic: bool + use_separate_pipeline_vision_decode_gpu: bool + + @property + def world_size(self) -> int: + return max( + self.dp_replicate_size * self.dp_shard_size, + self.cp_size * self.cfgp_size, + ) + + +class ParallelismOverrides(OverridesBase): + parallelism_preset: ParallelismPreset = "latency" + """Preset for automatic sharding.""" + device_memory_utilization: Training[float] = pydantic.Field(default=0.75, ge=0.0, le=1.0) + """Fraction of device memory to use for model weights. + + Used for automatic sharding. + """ + + dp_replicate_size: pydantic.NonNegativeInt = 1 + """Data parallel size.""" + dp_shard_size: pydantic.NonNegativeInt = 1 + """FSDP size.""" + tp_size: pydantic.NonNegativeInt = 1 + """Tensor parallel size.""" + cp_size: pydantic.NonNegativeInt = 1 + """Context parallel size.""" + cfgp_size: CfgpSize | Literal[0] = 1 + """CFG (Classifier Free Guidance) parallel size. + + If set to 1, runs conditional and unconditional guidance on the same GPU. + If set to 2, parallelizes conditional and unconditional guidance onto two GPUs. + """ + + use_torch_compile: bool = True + """Whether to use torch compile.""" + use_cuda_graphs: bool = True + """Whether to use CUDA graphs.""" + compiled_region: CompiledRegion = "all" + """Torch compile region.""" + compile_dynamic: bool = True + """Compile with symbolic-shape kernels (maps to ``torch.compile(dynamic=...)``). + + Defaults to ``True`` for backward compatibility with training, which can + see varying shapes across batches. Setting to ``False`` produces faster + kernels when the shapes are stable (e.g. single-prompt AR inference), at + the cost of a recompile on shape change. + """ + use_separate_pipeline_vision_decode_gpu: bool = False + """Whether to place pipeline vision decode on a spare local GPU when one is available.""" + + def _build_parallelism(self, world_size: int | None, local_world_size: int | None, device_memory_bytes: int | None): + if not self.dp_replicate_size: + self.dp_replicate_size = 1 + if not self.dp_shard_size: + self.dp_shard_size = 1 + if not self.tp_size: + self.tp_size = 1 + if not self.cp_size: + self.cp_size = 1 + if not self.cfgp_size: + self.cfgp_size = 1 + + def build_parallelism( + self, world_size: int | None = None, local_world_size: int | None = None, device_memory_bytes: int | None = None + ) -> ParallelismArgs: + self._build_parallelism( + world_size=world_size, local_world_size=local_world_size, device_memory_bytes=device_memory_bytes + ) + return self._build(ParallelismArgs) + + +class GuardrailArgs(ArgsBase): + """Guardrail arguments.""" + + guardrails: bool + offload_guardrail_models: bool + + +class GuardrailOverrides(OverridesBase): + guardrails: bool = False + """Enable guardrails.""" + offload_guardrail_models: bool = False + """Offload guardrail models to CPU.""" + + +class SetupArgs(ABC, CheckpointArgs, ParallelismArgs, GuardrailArgs): + output_dir: ResolvedPath + keep_going: bool + debug: bool + profile: bool + benchmark: bool + warmup: pydantic.NonNegativeInt + max_model_len: pydantic.PositiveInt | None + max_num_seqs: pydantic.PositiveInt | None + + # Subclass must implement these fields/methods + # ------------------------------------------------------------ + sample_overrides: pydantic.BaseModel + + @classmethod + @abstractmethod + def get_sample_overrides_cls(cls) -> type["SampleOverrides"]: + """Get sample overrides class.""" + + @classmethod + @abstractmethod + def get_sample_args_cls(cls) -> type["SampleArgs"]: + """Get sample arguments class.""" + + @classmethod + @abstractmethod + def get_inference_cls(cls) -> type["Inference"]: + """Get inference class.""" + + @classmethod + def get_variant(cls) -> str: + return cls.model_fields["variant"].default + + +class SetupOverrides(ABC, CheckpointOverrides, ParallelismOverrides, GuardrailOverrides): + """Inference setup arguments.""" + + output_dir: Annotated[ResolvedPath | None, tyro.conf.arg(aliases=("-o",))] = None + """Output directory.""" + keep_going: bool = False + """If True, catch and log errors instead of raising them.""" + debug: bool = False + """If True, enable debug outputs.""" + profile: bool = False + """Run profiler and save report to output directory.""" + benchmark: bool = False + """If set, measures and reports inference runtime (disables tqdm).""" + warmup: pydantic.NonNegativeInt = 0 + """Number of warmup generations before each sample.""" + max_model_len: pydantic.PositiveInt | None = None + """Maximum total tokens per batch. When set, samples are packed into + batches by token count.""" + max_num_seqs: pydantic.PositiveInt | None = 1 + """Maximum number of sequences per batch. When set, samples are packed into + batches by number of sequences.""" + + def _build_setup(self): + pass + + @abstractmethod + def build_setup(self) -> SetupArgs: + """Build setup arguments.""" + + +class SampleArgs(ArgsBase): + """Inference sample arguments.""" + + output_dir: ResolvedPath + model: str + extra: dict + + name: str + num_outputs: pydantic.PositiveInt + seed: int | None + tensors_file: ResolvedFilePath | None + pickle_file: ResolvedFilePath | None + + def get_data(self, *, device: str | int = "cpu") -> dict[str, Any]: + import pickle + + import safetensors.torch + + data: dict[str, Any] = {} + if self.tensors_file is not None: + data |= safetensors.torch.load_file(self.tensors_file, device=device) + if self.pickle_file is not None: + with self.pickle_file.open("rb") as f: + data |= dict(pickle.load(f)) + return data + + +class SampleOverrides(OverridesBase): + """Inference sample arguments.""" + + output_dir: Suppress[ResolvedPath | None] = None + """Output directory.""" + model: str | None = None + """Model name.""" + extra: Suppress[dict | None] = None + """Extra arguments.""" + + name: Suppress[str | None] = None + """Name of the sample.""" + num_outputs: Training[Annotated[pydantic.PositiveInt | None, tyro.conf.arg(aliases=("-n",))]] = None + """Number of outputs to generate per sample.""" + seed: int | None = None + """Seed for the random number generator.""" + + tensors_file: ResolvedFilePath | None = None + """Path to data tensors file.""" + pickle_file: ResolvedFilePath | None = None + """Path to data pickle file.""" + + @override + @classmethod + def from_files(cls, paths: list[Path], *, overrides: pydantic.BaseModel | None = None) -> list[Self]: + objs_per_file = _from_files(cls, paths, overrides=overrides) + + # Check names + all_objs: list[Self] = [] + names: set[str] = set() + for path, objs in objs_per_file.items(): + for line, obj in enumerate(objs): + if not obj.name: + if path.suffix.lower() == ".jsonl": + obj.name = f"{path.stem}_{line}" + else: + obj.name = path.stem + if obj.name in names: + raise ValueError(f"Duplicate name: '{obj.name}'") + all_objs.append(obj) + return all_objs + + def _build_sample(self): + if self.model is None: + self.model = "" + if self.extra is None: + self.extra = {} + + if self.num_outputs is None: + self.num_outputs = 1 + + @abstractmethod + def build_sample(self, *, model_config: Any) -> SampleArgs: + """Build sample arguments.""" + + +def _deep_merge(base: dict, overrides: dict) -> dict: + """Recursively merge *overrides* into *base*, merging nested dicts instead of replacing them.""" + merged = base.copy() + for key, value in overrides.items(): + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + merged[key] = _deep_merge(merged[key], value) + else: + merged[key] = value + return merged + + +def _from_file(cls: type[_PydanticModelT], path: Path, override_data: dict[str, Any]) -> list[_PydanticModelT]: + """Load arguments from a json/jsonl/yaml file. + + Returns a list of arguments. + """ + # Load data from file + if path.suffix in [".json"]: + data_list = [json.loads(path.read_text())] + elif path.suffix in [".jsonl"]: + data_list = [json.loads(line) for line in path.read_text().splitlines() if line] + elif path.suffix in [".yaml", ".yml"]: + data_list = [yaml.safe_load(path.read_text())] + else: + raise ValueError(f"Unsupported file extension: {path}") + + # Validate data + # Input paths are relative to the file path + path = path.expanduser().absolute() + with contextlib.chdir(path.parent): + objs: list[_PydanticModelT] = [] + for i, data in enumerate(data_list): + data = _deep_merge(data, override_data) + try: + objs.append(cls.model_validate(data)) + except pydantic.ValidationError as e: + raise ValueError( + f"Error validating parameters from '{path}' at sample {i}\nParameters: {data}\n{e}" + ) from e + + return objs + + +def _from_files( + cls: type[_PydanticModelT], paths: list[Path], *, overrides: pydantic.BaseModel | None = None +) -> dict[Path, list[_PydanticModelT]]: + """Load arguments from a list of json/jsonl/yaml files. + + Returns a list of arguments per file. + """ + if not paths: + raise ValueError("No inference parameter files") + + if overrides is None: + override_data = {} + else: + override_data = overrides.model_dump(exclude_none=True) + + # Expand glob patterns + expanded_paths: list[Path] = [] + for path in paths: + pattern = str(path) + if "*" in pattern: + expanded_paths.extend(Path(g) for g in glob.glob(pattern, recursive=True)) + else: + expanded_paths.append(path) + paths = sorted(set(expanded_paths)) + + # Load arguments from files + objs_per_file: dict[Path, list[_PydanticModelT]] = {} + for path in paths: + objs_per_file[path] = _from_file(cls, path, override_data) + return objs_per_file + + +def from_files( + cls: type[_PydanticModelT], paths: list[Path], *, overrides: pydantic.BaseModel | None = None +) -> list[_PydanticModelT]: + return list(itertools.chain.from_iterable(_from_files(cls, paths, overrides=overrides).values())) + + +class SampleOutput(ArgsBase): + content: dict = pydantic.Field(default_factory=dict) + """Output json.""" + files: list[Path] = pydantic.Field(default_factory=list) + """List of output file paths.""" + + def map_files(self, func: Callable[[Path], Path]) -> Self: + return self.model_copy(update={"files": [func(p) for p in self.files]}) + + +class SampleOutputs(ArgsBase): + """Inference sample outputs.""" + + args: dict + """Sample arguments.""" + + status: Literal["success", "error"] = "success" + """Generation status.""" + message: str | None = None + """Generation error message.""" + stack_trace: str | None = None + """Generation error stack trace.""" + + outputs: list[SampleOutput] = pydantic.Field(default_factory=list) + """List of sample outputs.""" + + @pydantic.model_validator(mode="after") + def _validate_name(self) -> Self: + if "name" not in self.args: + raise ValueError("'name' is required") + return self + + @property + def name(self) -> str: + return self.args["name"] + + def map_files(self, func: Callable[[Path], Path]) -> Self: + return self.model_copy(update={"outputs": [output.map_files(func) for output in self.outputs]}) diff --git a/cosmos3/common/args_test.py b/cosmos3/common/args_test.py new file mode 100644 index 00000000..1525961b --- /dev/null +++ b/cosmos3/common/args_test.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from pathlib import Path + +from cosmos3.args import DEFAULT_CHECKPOINT, DEFAULT_CHECKPOINT_NAME +from cosmos3.common.args import CheckpointConfig, CheckpointOverrides, download_file + +CHECKPOINTS: dict[str, CheckpointConfig] = { + DEFAULT_CHECKPOINT_NAME: DEFAULT_CHECKPOINT, +} + + +def test_download_file(tmp_path: Path): + download_url_1 = ( + "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg" + ) + file_size_1 = 279410 + + download_url_2 = "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/bus_terminal.jpg" + file_size_2 = 1283715 + + # Download file + download_path = Path( + download_file( + download_url_1, + tmp_path, + "robot_welding", + ) + ) + assert download_path.stat().st_size == file_size_1 + meta_path = Path(f"{download_path}.meta") + assert json.loads(meta_path.read_text()) == { + "url": download_url_1, + } + cache_path = download_path.resolve() + + # Same file should be noop + download_path = Path(download_file(str(download_path), tmp_path, "robot_welding")) + assert download_path.resolve() == cache_path + + # Copy file + copy_path = Path(download_file(str(download_path), tmp_path, "robot_welding_copy")) + assert copy_path.stat().st_size == file_size_1 + assert copy_path.resolve() == cache_path + + # Re-download should be noop + copy_path = Path(download_file(str(download_path), tmp_path, "robot_welding_copy")) + assert copy_path.resolve() == cache_path + + # Force re-download + os.remove(meta_path) + download_path = Path(download_file(download_url_1, tmp_path, "robot_welding")) + assert download_path.resolve() != cache_path + assert download_path.stat().st_size == file_size_1 + + # Different file should overwrite + download_path = Path( + download_file( + download_url_2, + tmp_path, + "robot_welding", + ) + ) + assert download_path.stat().st_size == file_size_2 + assert json.loads(Path(f"{download_path}.meta").read_text()) == { + "url": download_url_2, + } + + +def test_parse_checkpoint_path(tmp_path: Path): + # Named checkpoint + args = CheckpointOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + ).build_checkpoint(checkpoints=CHECKPOINTS) + assert args.checkpoint_type == "hf" + assert args.experiment == "" + + # Local HF checkpoint + hf_path = tmp_path / "hf" + hf_path.mkdir(parents=True, exist_ok=True) + (hf_path / "config.json").touch() + (hf_path / "model.safetensors").touch() + args = CheckpointOverrides( + checkpoint_path=str(hf_path), + ).build_checkpoint(checkpoints=CHECKPOINTS) + assert args.checkpoint_type == "hf" + assert args.experiment == "" + + # Local DCP checkpoint + dcp_path = ( + tmp_path + / "cosmos3_vfm/t2i_mot_0p6b_qwen3_vl_ablations/t2i_mot_exp000_015_qwen3_0p6b_256res_frozen_llm_lr_4e4_large_seq_no_llm_qknorm_gcp_bs8k_baseline_long_run_1e4/checkpoints/iter_000700000/model" + ) + dcp_path.mkdir(parents=True, exist_ok=True) + (dcp_path / ".metadata").touch() + (dcp_path / "__0_0.distcp").touch() + args = CheckpointOverrides( + checkpoint_path=str(dcp_path), + ).build_checkpoint(checkpoints=CHECKPOINTS) + assert args.checkpoint_type == "dcp" + assert ( + args.experiment + == "t2i_mot_exp000_015_qwen3_0p6b_256res_frozen_llm_lr_4e4_large_seq_no_llm_qknorm_gcp_bs8k_baseline_long_run_1e4" + ) + + # S3 DCP checkpoint + args = CheckpointOverrides( + checkpoint_path=f"s3://bucket/cosmos3_vfm/t2i_mot_0p6b_qwen3_vl_ablations/t2i_mot_exp000_015_qwen3_0p6b_256res_frozen_llm_lr_4e4_large_seq_no_llm_qknorm_gcp_bs8k_baseline_long_run_1e4/checkpoints/iter_000700000/model", + ).build_checkpoint(checkpoints=CHECKPOINTS) + assert args.checkpoint_type == "dcp" + assert ( + args.experiment + == "t2i_mot_exp000_015_qwen3_0p6b_256res_frozen_llm_lr_4e4_large_seq_no_llm_qknorm_gcp_bs8k_baseline_long_run_1e4" + ) + assert args.config_file == "cosmos3/_src/vfm/configs/base/config.py" diff --git a/cosmos3/common/checkpoints.py b/cosmos3/common/checkpoints.py new file mode 100644 index 00000000..52833ea9 --- /dev/null +++ b/cosmos3/common/checkpoints.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from uuid import uuid4 + +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.utils.checkpoint_db import ( + CheckpointConfig, + CheckpointDirHf, + CheckpointDirS3, + CheckpointFileHf, + CheckpointFileS3, + register_checkpoint, +) + + +@functools.cache +def register_checkpoints(): + for repository, revision in [ + ("Qwen/Qwen3-0.6B", "c1899de289a04d12100db370d81485cdf75e47ca"), + ("Qwen/Qwen3-VL-2B-Instruct", "89644892e4d85e24eaac8bacfd4f463576704203"), + ("Qwen/Qwen3-VL-8B-Instruct", "0c351dd01ed87e9c1b53cbc748cba10e6187ff3b"), + ("Qwen/Qwen3-VL-32B-Instruct", "0cfaf48183f594c314753d30a4c4974bc75f3ccb"), + ]: + # Used by 'cosmos3/_src/vfm/configs/base/defaults/vlm.py::download_tokenizer_files'. + register_checkpoint( + CheckpointConfig( + uuid=uuid4().hex, + name=repository, + s3=CheckpointDirS3( + uri=f"s3://bucket/cosmos3/pretrained/huggingface/{repository}", + ), + hf=CheckpointDirHf( + repository=repository, + revision=revision, + include=() if TRAINING else ("*.json", "*.txt"), + ), + ), + ) + + register_checkpoint( + CheckpointConfig( + uuid=uuid4().hex, + name="Wan2.1/vae", + s3=CheckpointFileS3( + uri="s3://bucket/pretrained/tokenizers/video/wan2pt1/Wan2.1_VAE.pth", + ), + hf=CheckpointFileHf( + repository="Wan-AI/Wan2.1-T2V-14B", + revision="a064a6c71f5be440641209c07bf2a5ce7a2ff5e4", + filename="Wan2.1_VAE.pth", + ), + ), + ) + + register_checkpoint( + CheckpointConfig( + uuid=uuid4().hex, + name="Wan2.2/vae", + s3=CheckpointFileS3( + uri="s3://bucket/pretrained/tokenizers/video/wan2pt2/Wan2.2_VAE.pth", + ), + hf=CheckpointFileHf( + repository="Wan-AI/Wan2.2-TI2V-5B", + revision="921dbaf3f1674a56f47e83fb80a34bac8a8f203e", + filename="Wan2.2_VAE.pth", + ), + ), + ) diff --git a/cosmos3/common/config.py b/cosmos3/common/config.py new file mode 100644 index 00000000..ec4da2ab --- /dev/null +++ b/cosmos3/common/config.py @@ -0,0 +1,460 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import functools +import importlib +import json +import re +import typing +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +import attrs +import cattrs +import cattrs.preconf.json +import omegaconf +import pydantic +import tomllib +import torch +import yaml +from typing_extensions import assert_never + +from cosmos3.common.init import is_rank0 +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.lazy_config.registry import convert_target_to_string, locate +from cosmos3._src.imaginaire.utils import log + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.config import Config + +ROOT_DIR = Path(__file__).parents[2].absolute() +PACKAGE_DIR = ROOT_DIR / "cosmos3" +CONFIG_DIR = PACKAGE_DIR / "configs" + + +def load_config( + config_file: str, + experiment: str, + *, + overrides: list[str] = [], +) -> "Config": + """Load config from config store.""" + assert TRAINING + from cosmos3._src.imaginaire.utils import config_helper + + config_module = importlib.import_module(config_helper.get_config_module(config_file)) + config = config_module.make_config() + config = config_helper.override(config, ["--", f"experiment={experiment}", *overrides]) + return config + + +def save_config(config: Any, output_dir: Path) -> None: + """Save config to output directory for debugging.""" + if not is_rank0(): + return + output_dir.mkdir(parents=True, exist_ok=True) + config_file = output_dir / "config.yaml" + serialize_config(config, config_file, invalid="ignore") + log.info(f"Saved config to {config_file}") + + +InvalidMode = Literal["error", "warn", "ignore"] +"""How to handle invalid fields. + +* error: raise an error. +* warn: log a warning. +* ignore: + * When unstructuring, convert to string. + * When structuring, leave unstructured. +""" +_InvalidMode = InvalidMode # For backward compatibility. +_InvalidModeValidator = pydantic.TypeAdapter(_InvalidMode) + + +@attrs.define +class _UnstructureOptions: + converter: cattrs.Converter + + invalid: _InvalidMode + + +@attrs.define +class _FixOptions: + converter: cattrs.Converter + + invalid: _InvalidMode + add_defaults: bool + + +def unstructure_config(config: Any, *, invalid: _InvalidMode = "error") -> dict: + """Unstructure config to primitive types.""" + _InvalidModeValidator.validate_python(invalid) + options = _UnstructureOptions(converter=config_converter, invalid=invalid) + return _unstructure(config, prefix=(), options=options) + + +def structure_config(config_dict: Any, target: type | str, /, *, invalid: _InvalidMode = "error") -> Any: + """Structure config from primitive types.""" + if isinstance(target, str): + target = locate(target) + config_dict = fix_config_dict(config_dict, invalid=invalid, add_defaults=True) + return config_converter.structure(config_dict, target) + + +def serialize_config_dict(config_dict: dict, config_file: Path) -> None: + """Serialize config dict to a file.""" + match config_file.suffix.lower(): + case ".yaml" | ".yml": + config_str = yaml.safe_dump(config_dict) + case ".json": + config_str = json.dumps(config_dict, indent=2) + case _: + raise ValueError(f"Unsupported file extension '{config_file.suffix}'") + config_str = apply_config_replacements(config_str) + config_file.parent.mkdir(parents=True, exist_ok=True) + config_file.write_text(config_str) + + +def serialize_config(config: Any, config_file: Path, /, *, invalid: _InvalidMode = "error") -> None: + """Serialize config to a file.""" + config_dict = unstructure_config(config, invalid=invalid) + serialize_config_dict(config_dict, config_file) + + +def deserialize_config_dict(config_file: Path) -> dict: + """Deserialize config dict from a file.""" + config_str = config_file.read_text() + config_str = undo_config_replacements(config_str) + match config_file.suffix.lower(): + case ".yaml" | ".yml": + config_dict = yaml.safe_load(config_str) + case ".json": + config_dict = json.loads(config_str) + case _: + raise ValueError(f"Unsupported file extension '{config_file.suffix}'") + config_dict = fix_config_dict(config_dict) + return config_dict + + +def deserialize_config(config_file: Path, target: type | str, /, *, invalid: _InvalidMode = "error") -> Any: + """Deserialize config from a file.""" + config_dict = deserialize_config_dict(config_file) + return structure_config(config_dict, target, invalid=invalid) + + +def fix_config_dict(config_dict: Any, *, invalid: _InvalidMode = "error", add_defaults: bool = False) -> dict: + """Fix config dict. + + * Unstructure. + * Fix legacy fields. + * Optional: Add missing default fields. + """ + _InvalidModeValidator.validate_python(invalid) + config_dict = unstructure_config(config_dict) + return _fix( + config_dict, + prefix=(), + options=_FixOptions(converter=config_converter, invalid=invalid, add_defaults=add_defaults), + ) + + +TYPE_KEY = "_type" +TARGET_KEY = "_target_" + +# For backward compatibility. +_TYPE_KEY = TYPE_KEY +_TARGET_KEY = TARGET_KEY + + +def _join_prefix(prefix: tuple[Any, ...]) -> str: + return ".".join(str(p) for p in prefix) + + +def get_default_params(tp: type) -> dict: + from cosmos3._src.imaginaire.lazy_config.lazy_call import get_default_params + + return {k: v for k, v in get_default_params(tp).items() if v is not attrs.NOTHING} + + +def _fix(obj: Any, prefix: tuple[Any, ...], options: _FixOptions) -> Any: + # Handle primitive types. + if isinstance(obj, (int, float, str, bool)) or obj is None: + return obj + if isinstance(obj, list): + return [_fix(item, prefix=(*prefix, i), options=options) for i, item in enumerate(obj)] + + # Handle objects. + if isinstance(obj, dict): + # Fix metadata. + metadata = obj.pop("_metadata", {}) + tp_str = obj.pop(_TYPE_KEY, None) + tp_str = metadata.pop("object_type", None) or tp_str + if tp_str is not None and tp_str.startswith("omegaconf."): + tp_str = None + if tp_str is not None: + obj[_TYPE_KEY] = tp_str + + # Recurse. + data = {k: _fix(v, prefix=(*prefix, k), options=options) for k, v in obj.items()} + + # Add missing default fields. + target_str = obj.get(_TARGET_KEY) or obj.get(_TYPE_KEY) + if options.add_defaults and target_str is not None: + target = locate(target_str) + data = get_default_params(target) | data + + return data + raise ValueError(f"Unsupported type: {type(obj)}") + + +def _unstructure(obj: Any, prefix: tuple[Any, ...], options: _UnstructureOptions) -> Any: + """Wrapper around cattrs 'unstructure' to handle missing type annotations. + + For values missing type annotations, the type of the object is included in + the unstructured data. + """ + # Handle primitive types. + if isinstance(obj, (int, float, str, bool)) or obj is None: + return obj + if isinstance(obj, list): + return [_unstructure(item, prefix=(*prefix, i), options=options) for i, item in enumerate(obj)] + if isinstance(obj, dict): + return {k: _unstructure(v, prefix=(*prefix, k), options=options) for k, v in obj.items()} + + # Handle objects. + try: + # Use cattrs to unstructure the object. + data = options.converter.unstructure(obj) + if data is obj: + raise ValueError(f"Unsupported type: {type(obj)}") + if isinstance(data, dict) and not isinstance(obj, omegaconf.DictConfig): + if _TYPE_KEY in data: + raise ValueError(f"Already has a {_TYPE_KEY} field: {data[_TYPE_KEY]}") + data[_TYPE_KEY] = convert_target_to_string(type(obj)) + except Exception as e: + if options.invalid == "ignore": + return str(obj) + msg = f"Invalid value '{_join_prefix(prefix)}': {e}" + if options.invalid == "warn": + log.warning(msg) + return str(obj) + if options.invalid == "error": + raise ValueError(msg) from e + assert_never(options.invalid) + # Recursively unstructure the data. + return _unstructure(data, prefix=prefix, options=options) + + +config_converter = cattrs.preconf.json.make_converter() + + +# type +def _is_type_cls(cls: Any) -> bool: + return cls in [type, abc.ABCMeta] or typing.get_origin(cls) is type + + +def _unstructure_type(cls: Any | None) -> str | None: + if cls is None: + return None + return convert_target_to_string(cls) + + +def _structure_type(data: Any, _cls: Any) -> type: + assert isinstance(data, str) + return locate(data) + + +config_converter.register_unstructure_hook_func(_is_type_cls, _unstructure_type) +config_converter.register_structure_hook_func(_is_type_cls, _structure_type) + + +# functools.partial +def _unstructure_functools_partial(obj: functools.partial | None) -> dict | None: + if obj is None: + return None + + # Convert to omegaconf: https://hydra.cc/docs/advanced/instantiate_objects/overview/ + return { + _TARGET_KEY: convert_target_to_string(obj.func), + "_args_": obj.args, + **obj.keywords, + } + + +def _structure_functools_partial(data: Any, _cls: Any) -> functools.partial: + assert isinstance(data, dict) + func = locate(data.pop(_TARGET_KEY)) + args = data.pop("_args_") + return functools.partial(func, *args, **data) + + +config_converter.register_unstructure_hook(functools.partial, _unstructure_functools_partial) +config_converter.register_structure_hook(functools.partial, _structure_functools_partial) + +# We need allow objects, because we add default fields to the config. +_OMEGACONF_FLAGS = dict(allow_objects=True) + + +# omegaconf.DictConfig +def _is_omegaconf_dict_cls(cls: Any) -> bool: + return isinstance(cls, type) and issubclass(cls, omegaconf.DictConfig) + + +def _unstructure_omegaconf_dict(obj: omegaconf.DictConfig | None) -> dict | None: + if obj is None or obj._is_none(): + return None + + # Create a shallow copy without recursion or resolution. + data = dict(obj.items_ex(resolve=False)) + + target_str = data.get(_TARGET_KEY) + if target_str is not None and not isinstance(target_str, str): + data[_TARGET_KEY] = convert_target_to_string(target_str) + if obj._metadata.object_type not in [None, dict]: + data[_TYPE_KEY] = convert_target_to_string(obj._metadata.object_type) + + return data + + +def _structure_omegaconf_dict(data: Any, _cls: Any) -> omegaconf.DictConfig | None: + if data is None: + return None + assert isinstance(data, dict), type(data) + # Ideally, we should use omegaconf.structured if _TYPE_KEY is present. + return omegaconf.OmegaConf.create(data, flags=_OMEGACONF_FLAGS) + + +config_converter.register_unstructure_hook_func(_is_omegaconf_dict_cls, _unstructure_omegaconf_dict) +config_converter.register_structure_hook_func(_is_omegaconf_dict_cls, _structure_omegaconf_dict) + + +# omegaconf.ListConfig +def _is_omegaconf_list_cls(obj: Any) -> bool: + return isinstance(obj, type) and issubclass(obj, omegaconf.ListConfig) + + +def _unstructure_omegaconf_list(obj: omegaconf.ListConfig | None) -> list | None: + if obj is None or obj._is_none(): + return None + # Create a shallow copy without recursion or resolution. + return list(obj._iter_ex(resolve=False)) + + +def _structure_omegaconf_list(data: Any, _: Any) -> omegaconf.ListConfig | None: + if data is None: + return None + assert isinstance(data, list), type(data) + return omegaconf.OmegaConf.create(data, flags=_OMEGACONF_FLAGS) + + +config_converter.register_unstructure_hook_func(_is_omegaconf_list_cls, _unstructure_omegaconf_list) +config_converter.register_structure_hook_func(_is_omegaconf_list_cls, _structure_omegaconf_list) + + +# torch types +def _unstructure_torch_type(obj: Any | None) -> str | None: + if obj is None: + return None + return str(obj).removeprefix("torch.") + + +def _structure_torch_type(data: Any, _cls: Any) -> Any | None: + if data is None: + return None + assert isinstance(data, str), type(data) + return getattr(torch, data) + + +for _torch_type in [ + torch.dtype, + torch.layout, + torch.memory_format, +]: + config_converter.register_unstructure_hook(_torch_type, _unstructure_torch_type) + config_converter.register_structure_hook(_torch_type, _structure_torch_type) + + +# torch.device +def _unstructure_torch_device(obj: torch.device | None) -> str | None: + if obj is None: + return None + return str(obj) + + +def _structure_torch_device(data: Any, _cls: Any) -> torch.device | None: + if data is None: + return None + assert isinstance(data, str), type(data) + return torch.device(data) + + +config_converter.register_unstructure_hook(torch.device, _unstructure_torch_device) +config_converter.register_structure_hook(torch.device, _structure_torch_device) + + +# torch.Tensor +def _unstructure_torch_tensor(obj: torch.Tensor | None) -> list | None: + if obj is None: + return None + return obj.detach().cpu().tolist() + + +def _structure_torch_tensor(data: Any, _cls: Any) -> torch.Tensor | None: + if data is None: + return None + assert isinstance(data, list), type(data) + return torch.tensor(data) + + +config_converter.register_unstructure_hook(torch.Tensor, _unstructure_torch_tensor) +config_converter.register_structure_hook(torch.Tensor, _structure_torch_tensor) + + + +def replace_case_preserving(text: str, old: str, new: str) -> str: + """Similar to `str.replace()`, but preserves the case of the matched text.""" + + def replace_func(match: re.Match[str]) -> str: + original = match.group() + if original.isupper(): + return new.upper() + if original.islower(): + return new.lower() + if original == old.capitalize(): + return new.capitalize() + if original[0].isupper(): + return new.upper() + return new.lower() + + pattern = re.compile(re.escape(old), re.IGNORECASE) + return pattern.sub(replace_func, text) + + +def apply_config_replacements(config_str: str) -> str: + """Apply config replacements to a config string.""" + return config_str + + +def undo_config_replacements(config_str: str) -> str: + """Undo config replacements to a config string.""" + return config_str + + +def undo_config_dict_replacements(config_dict: dict) -> dict: + """Undo config replacements to a config dict.""" + config_str = json.dumps(config_dict) + config_str = undo_config_replacements(config_str) + return json.loads(config_str) diff --git a/cosmos3/common/config_test.py b/cosmos3/common/config_test.py new file mode 100644 index 00000000..4f6c9d9e --- /dev/null +++ b/cosmos3/common/config_test.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +from pathlib import Path +from typing import Type + +import attrs +import omegaconf +import pytest +import torch + +from cosmos3.common.args import DEFAULT_CONFIG_FILE +from cosmos3.common.config import ( + _is_type_cls, + apply_config_replacements, + config_converter, + deserialize_config, + load_config, + serialize_config, + structure_config, + undo_config_replacements, + unstructure_config, +) +from cosmos3._src.imaginaire.flags import TRAINING +from cosmos3._src.imaginaire.lazy_config import LazyCall as L +from cosmos3._src.imaginaire.lazy_config.registry import convert_target_to_string + + +def test_is_type(): + assert not _is_type_cls(int) + assert _is_type_cls(type[int]) + assert _is_type_cls(Type[int]) + + +@attrs.define +class Config: + tp: type[int] + + list_config: omegaconf.ListConfig + + x: int + + device: torch.device + dtype: torch.dtype + layout: torch.layout + memory_format: torch.memory_format + + +@attrs.define +class Cls: + x: int = 5 + + +def test_config_converter(): + def round_trip(obj): + return config_converter.structure(config_converter.unstructure(obj), type(obj)) + + tensor = torch.Tensor([1, 2, 3]) + assert torch.equal(round_trip(tensor), tensor) + + config = Config( + tp=int, + list_config=omegaconf.ListConfig( + [ + omegaconf.OmegaConf.structured(Cls(x=1)), + L(Cls)(x=2), + ] + ), + x=1, + device=torch.device("cuda"), + dtype=torch.float32, + layout=torch.strided, + memory_format=torch.preserve_format, + ) + + config_dict = unstructure_config(config) + assert config_dict == { + "_type": convert_target_to_string(Config), + "tp": convert_target_to_string(int), + "list_config": [ + { + "_type": convert_target_to_string(Cls), + "x": 1, + }, + { + "_target_": convert_target_to_string(Cls), + "x": 2, + }, + ], + "x": 1, + "device": "cuda", + "dtype": "float32", + "layout": "strided", + "memory_format": "preserve_format", + } + + structured_config = attrs.evolve( + config, + list_config=omegaconf.ListConfig( + [ + dict(_type=convert_target_to_string(Cls), x=1), + dict(_target_=convert_target_to_string(Cls), x=2), + ] + ), + ) + assert structure_config(config_dict, Config) == structured_config + + # Test missing fields are populated with defaults + for i in range(2): + del config_dict["list_config"][i]["x"] + structured_config.list_config[i].x = 5 + assert structure_config(config_dict, Config) == structured_config + + + +if TRAINING: + + @pytest.mark.parametrize("config_file", sorted(set([DEFAULT_CONFIG_FILE]))) + def test_make_config(config_file: str): + from cosmos3._src.imaginaire.utils import config_helper + + config_module = importlib.import_module(config_helper.get_config_module(config_file)) + config_module.make_config() + + def test_serialize_config(tmp_path: Path): + config = load_config( + config_file="cosmos3/_src/vfm/configs/base/config.py", + experiment="t2w_mot_dryrun_exp100_006_qwen3_0p6b_256res_resume_from_t2i", + ) + + for suffix in [".yaml", ".json"]: + config_file = tmp_path / f"config{suffix}" + serialize_config(config, config_file) + deserialize_config(config_file, type(config)) diff --git a/cosmos3/common/inference.py b/cosmos3/common/inference.py new file mode 100644 index 00000000..c052db0c --- /dev/null +++ b/cosmos3/common/inference.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import dataclasses +import traceback +from abc import ABC, abstractmethod +from collections.abc import Iterator +from contextlib import contextmanager, nullcontext +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, ContextManager, Self, Sequence, final + +import torch +import torch.profiler + +from cosmos3.common.args import GuardrailArgs, SampleArgs, SampleOutputs, SetupArgs +from cosmos3.common.init import is_rank0 +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.misc import TrainingTimer + +if TYPE_CHECKING: + from cosmos3._src.imaginaire.auxiliary.guardrail.common.core import GuardrailRunner + + +@contextlib.contextmanager +def sync_distributed_errors(): + """Catches local exceptions and synchronizes the error state across all distributed ranks. + + Raises a DistributedError on all ranks if ANY rank encountered an exception. + """ + error_flag = torch.zeros(1, dtype=torch.int32, device="cuda") # [1] + local_error: Exception | None = None + + try: + yield + except Exception as e: + error_flag += 1 + local_error = e + + if torch.distributed.is_initialized(): + # Sync the error count across all GPUs + torch.distributed.all_reduce(error_flag, op=torch.distributed.ReduceOp.SUM) + + if error_flag.item() > 0: + # If we got here, somebody failed. + # Ranks that failed will raise their actual error. + # Ranks that succeeded will raise a generic error so they gracefully abort too. + err_to_raise = local_error if local_error else RuntimeError("A different GPU rank failed.") + raise err_to_raise + + +@dataclass +class GuardrailRunners: + text: "GuardrailRunner" + video: "GuardrailRunner" + + @classmethod + def create(cls, args: GuardrailArgs, /) -> Self: + from cosmos3._src.imaginaire.auxiliary.guardrail.common import presets + + return cls( + text=presets.create_text_guardrail_runner(offload_model_to_cpu=args.offload_guardrail_models), + video=presets.create_video_guardrail_runner(offload_model_to_cpu=args.offload_guardrail_models), + ) + + +@dataclass(kw_only=True) +class Inference(ABC): + """Inference pipeline base class.""" + + setup_args: SetupArgs + model: torch.nn.Module + guardrails: GuardrailRunners | None + + _timer: TrainingTimer | None + _timer_context: list[str] = dataclasses.field(default_factory=list) + + @property + @abstractmethod + def model_config(self) -> Any: + """Get model config.""" + + @classmethod + @abstractmethod + def _create(cls, setup_args: SetupArgs, /, **kwargs: Any) -> Self: + """Create instance.""" + + @abstractmethod + def create_batches( + self, sample_args_list: Sequence[SampleArgs] + ) -> Iterator[tuple[list[SampleArgs], dict[str, Any]]]: + """Create batches of sample data.""" + + @abstractmethod + def generate_batch( + self, sample_args_list: Sequence[SampleArgs], data_batch: dict[str, Any], *, warmup: bool = False + ) -> list[SampleOutputs]: + """Generate a batch of samples.""" + + @final + @classmethod + def create(cls, setup_args: SetupArgs, /) -> Self: + """Create instance.""" + timer = TrainingTimer() if setup_args.benchmark else None + guardrails = GuardrailRunners.create(setup_args) if setup_args.guardrails else None + return cls._create(setup_args, guardrails=guardrails, _timer=timer) + + @torch.no_grad() + @final + def generate(self, sample_args_list: list[SampleArgs]) -> list[SampleOutputs]: + """Generate a list of samples.""" + # Create batches + try: + with sync_distributed_errors(): + batches = self.create_batches(sample_args_list) + except Exception as e: + return [self._handle_sample_exception(sample_args, e) for sample_args in sample_args_list] + + # Generate batches + sample_outputs: list[SampleOutputs] = [] + for i_batch, (sample_args_batch, data_batch) in enumerate(batches): + log.debug(f"[{i_batch + 1}] Processing batch", rank0_only=False) + + if self.setup_args.profile: + profiler = torch.profiler.profile( + activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA], + record_shapes=True, + profile_memory=True, + with_stack=True, + ) + else: + profiler = contextlib.nullcontext() + + with self._get_timer_context("warmup"): + for _ in range(self.setup_args.warmup): + with self._get_timer(f"{self.__class__.__name__}.generate_batch"): + self.generate_batch(sample_args_batch, data_batch, warmup=True) + with self._get_timer(f"{self.__class__.__name__}.generate_batch"), profiler: + sample_outputs.extend(self.generate_batch(sample_args_batch, data_batch)) + + if self.setup_args.profile and is_rank0(): + assert isinstance(profiler, torch.profiler.profile) + sample_args = sample_args_batch[0] + profile_file = sample_args.output_dir / "profile.json.gz" + profiler.export_chrome_trace(str(profile_file)) + log.success(f"Saved profile to '{profile_file}'") + + return sample_outputs + + def _get_timer(self, func_name: str) -> ContextManager: + if self._timer is None: + return nullcontext() + if self._timer_context: + context = ".".join(self._timer_context) + func_name = f"[{context}] {func_name}" + return self._timer(func_name) + + @contextmanager + def _get_timer_context(self, func_name: str): + self._timer_context.append(func_name) + try: + yield + finally: + self._timer_context.pop() + + def get_timer_results(self) -> dict | None: + if self._timer is None: + return None + return { + "all": self._timer.results, + "average": self._timer.compute_average_results(), + } + + def _handle_sample_exception(self, sample_args: SampleArgs, e: Exception) -> SampleOutputs: + msg = f"Error generating sample '{sample_args.name}': {e}" + if not self.setup_args.keep_going: + raise ValueError(msg) from e + log.error(msg) + return SampleOutputs( + args=sample_args.model_dump(mode="json"), status="error", message=msg, stack_trace=traceback.format_exc() + ) + + @final + def _run_text_guardrail(self, name: str, prompt: str) -> None: + """Run guardrail checks on the prompt.""" + if self.guardrails is None: + return + + from cosmos3._src.imaginaire.auxiliary.guardrail.common import presets + + if not presets.run_text_guardrail(prompt, self.guardrails.text): + raise ValueError(f"Guardrail blocked prompt '{name}': '{prompt}'") + + @final + def _run_video_guardrail(self, name: str, video_cthw: torch.Tensor) -> torch.Tensor: + """Run guardrail checks on the video and apply face blur.""" + if self.guardrails is None: + return video_cthw + processed_video_cthw, message = _run_video_guardrail(self.guardrails.video, video_cthw) + if processed_video_cthw is None: + raise ValueError(f"Guardrail blocked video '{name}': {message}") + return processed_video_cthw + + +def _run_video_guardrail( + video_guardrail_runner: "GuardrailRunner", video_cthw: torch.Tensor +) -> tuple[torch.Tensor | None, str]: + """Run video guardrail and apply face blur. + + Returns a ``(video_or_none, message)`` tuple. When the guardrail blocks + the video, ``video_or_none`` is ``None`` and ``message`` contains the + underlying reason (unsafe frame ratio, categories, etc.) as produced by + :class:`GuardrailRunner.run_safety_check`. + """ + if video_cthw.ndim != 4: + raise ValueError(f"Video tensor must have 4 dimensions, got {video_cthw.shape}") + frames_thwc = ( + (video_cthw * 255.0).clamp(0.0, 255.0).to(torch.uint8).permute(1, 2, 3, 0).detach().cpu().numpy() + ) # [T,H,W,C] + + # Inline of presets.run_video_guardrail so we can forward `message` (the helper drops it). + is_safe, message = video_guardrail_runner.run_safety_check(frames_thwc) + if not is_safe: + log.critical(f"GUARDRAIL BLOCKED: {message}") + return None, message + + frames_thwc = video_guardrail_runner.postprocess(frames_thwc) + video_cthw = (torch.from_numpy(frames_thwc).float().permute(3, 0, 1, 2) / 255.0).to( # [C,T,H,W] + video_cthw.device, dtype=video_cthw.dtype + ) + return video_cthw, message diff --git a/cosmos3/common/inference_test.py b/cosmos3/common/inference_test.py new file mode 100644 index 00000000..cc349a07 --- /dev/null +++ b/cosmos3/common/inference_test.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from cosmos3.common.args import GuardrailArgs +from cosmos3.common.inference import GuardrailRunners +from cosmos3._src.imaginaire.auxiliary.guardrail.common import presets + + +def test_guardrail_runners(): + guardrail_args = GuardrailArgs(guardrails=True, offload_guardrail_models=False) + runners = GuardrailRunners.create(guardrail_args) + assert runners.text is not None + assert runners.video is not None + + assert presets.run_text_guardrail("test", runners.text) + assert not presets.run_text_guardrail("Tesla Cybertruck", runners.text) + + frames_thwc = np.random.randint(0, 255, (1, 16, 16, 3), dtype=np.uint8) + clean_frames_thwc = presets.run_video_guardrail(frames_thwc, runners.video) + assert clean_frames_thwc is not None + np.testing.assert_allclose(frames_thwc, clean_frames_thwc) diff --git a/cosmos3/common/init.py b/cosmos3/common/init.py new file mode 100644 index 00000000..a06f59d2 --- /dev/null +++ b/cosmos3/common/init.py @@ -0,0 +1,259 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import fnmatch +import logging +import os +import socket +import sys +import time +import warnings +from pathlib import Path + +import loguru +import torch +from torch.distributed.elastic.multiprocessing.errors import get_error_handler + +"""Script initialization.""" + + +def get_rank() -> int: + return int(os.environ.get("RANK", "0")) + + +def get_world_size() -> int: + return int(os.environ.get("WORLD_SIZE", "1")) + + +def get_local_rank() -> int: + return int(os.environ.get("LOCAL_RANK", "0")) + + +def get_local_world_size() -> int: + return int(os.environ.get("LOCAL_WORLD_SIZE", "1")) + + +def enable_distributed() -> bool: + return get_world_size() > 1 + + +def is_rank0() -> bool: + return get_rank() == 0 + + +def _get_logger_format() -> str: + from cosmos3._src.imaginaire.utils import log + + return f"{log.get_datetime_format()}{log.get_machine_format()}{log.get_message_format()}" + + +_LOGGER_INCLUDE = [ + "cosmos3._src.imaginaire.attention", + "cosmos3._src.imaginaire.utils.checkpoint_db", + "cosmos3._src.imaginaire.trainer", + "cosmos3._src.vfm.utils.model_loader", + "*.callbacks.*", +] +_LOGGER_EXCLUDE = [ + "*._*", + "projects.*", + "cosmos3._src.imaginaire.*", +] + + +def _console_filter(record: dict) -> bool: + from cosmos3._src.imaginaire.utils import log + + # Not sure why but critical messages need a special case to be filtered + if record["level"].name == "CRITICAL": + module_name: str = record["name"] + for pat in _LOGGER_INCLUDE: + if fnmatch.fnmatch(module_name, pat): + return True + for pat in _LOGGER_EXCLUDE: + if fnmatch.fnmatch(module_name, pat): + return False + return True + + if not log._rank0_only_filter(record): + return False + module_name: str = record["name"] + for pat in _LOGGER_INCLUDE: + if fnmatch.fnmatch(module_name, pat): + return True + for pat in _LOGGER_EXCLUDE: + if fnmatch.fnmatch(module_name, pat): + return False + return True + + +def _init_log_console(*, verbose: bool | None = None): + from cosmos3._src.imaginaire.flags import VERBOSE + from cosmos3._src.imaginaire.utils import log + + if verbose is None: + verbose = VERBOSE + + log.logger.remove() + log.logger.add( + sys.stdout, + level="DEBUG" if verbose else "INFO", + format=_get_logger_format(), + filter=log._rank0_only_filter if verbose else _console_filter, + catch=False, + ) + if not verbose: + logging.basicConfig( + level=logging.ERROR, + ) + loguru.logger.remove() + warnings.filterwarnings("ignore") + + +def _init_log_files(output_dir: Path): + from cosmos3._src.imaginaire.utils import log + + console_path = output_dir / "console.log" + debug_path = output_dir / "debug.log" + log.info(f"Console log saved to {console_path}") + log.info(f"Debug log saved to {debug_path}") + logger_format = _get_logger_format() + log.logger.add( + console_path, + mode="w", + level="INFO", + format=logger_format, + filter=_console_filter, + enqueue=True, + catch=False, + ) + log.logger.add( + debug_path, + mode="w", + level="DEBUG", + format=logger_format, + filter=log._rank0_only_filter, + enqueue=True, + catch=False, + ) + + +def get_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] + + +def _init_distributed(): + from cosmos3._src.imaginaire.utils import distributed + + distributed.init() + + +def _cleanup_distributed(): + if torch.distributed.is_initialized(): + torch.distributed.destroy_process_group() + + +_error_handler = get_error_handler() + + +def _distributed_excepthook(exc_type, value, traceback): + from cosmos3._src.imaginaire.utils import log + + if isinstance(value, Exception): + _error_handler.record_exception(value) + + log.logger.complete() + sys.stderr.flush() + sys.stdout.flush() + + if not is_rank0(): + # Wait for rank0 to throw the exception + time.sleep(10) + + sys.__excepthook__(exc_type, value, traceback) + + +def _init_script(training: bool = False, env: dict[str, str] | None = None, default_env: dict[str, str] | None = None): + """Initialize script.""" + if "cosmos3._src.imaginaire" in sys.modules: + raise RuntimeError("'init_script' must be called first.") + if default_env: + for k, v in default_env.items(): + os.environ.setdefault(k, v) + os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") + if env: + for k, v in env.items(): + os.environ[k] = v + + _error_handler.initialize() + sys.excepthook = _distributed_excepthook + + import torch + + if not training: + torch.set_grad_enabled(False) + + _init_log_console() + # Initialize distributed early so that: + # 1. torch.cuda.set_device(local_rank) runs before any CUDA allocations, + # ensuring each rank places tensors on its own GPU (not all on cuda:0). + # 2. sync_model_states in tokenizer / model init is not a silent no-op. + if enable_distributed(): + _init_distributed() + set_seed(0) + + if torch.cuda.is_available(): + device_memory_fraction = float(os.environ.get("DEVICE_MEMORY_FRACTION", "1")) + if device_memory_fraction < 1: + torch.cuda.set_per_process_memory_fraction(device_memory_fraction) + + +def _cleanup_script(): + """Clean up script.""" + if sys.exc_info()[1] is not None: + # Skip cleanup if an exception was raised + return + if enable_distributed(): + _cleanup_distributed() + + +def init_script( + *, training: bool = False, env: dict[str, str] | None = None, default_env: dict[str, str] | None = None +): + _init_script(training=training, env=env, default_env=default_env) + atexit.register(_cleanup_script) + + +def init_output_dir(output_dir: Path): + """Initialize output directory.""" + from cosmos3._src.imaginaire.flags import FLAGS + from cosmos3._src.imaginaire.utils import log + + output_dir.mkdir(parents=True, exist_ok=True) + if not is_rank0(): + return + + _init_log_files(output_dir) + log.debug(f"{FLAGS}") + + +def set_seed(seed: int): + """Set seed for random number generator.""" + from transformers import set_seed + + set_seed(seed) diff --git a/cosmos3/configs/experiment/action_policy_sft_8b.yaml b/cosmos3/configs/experiment/action_policy_sft_8b.yaml new file mode 100644 index 00000000..b5f92556 --- /dev/null +++ b/cosmos3/configs/experiment/action_policy_sft_8b.yaml @@ -0,0 +1,591 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_type: cosmos3._src.vfm.configs.base.config.Config +checkpoint: + broadcast_via_filesystem: false + dcp_async_mode_enabled: true + enable_gcs_patch_in_boto3: true + hf_export: + enabled: false + export_every_n: 1 + hf_repo_id: null + upload_to_object_store: + bucket: '' + credentials: '' + enabled: false + jit: + device: cuda + dtype: bfloat16 + enabled: false + input_shape: null + strict: true + keys_not_to_resume: [] + keys_to_skip_loading: + - net_ema. + - action2llm + - llm2action + - action_modality_embed + - action_pos_embed + load_ema_to_reg: false + load_from_object_store: + bucket: '' + credentials: '' + enabled: false + load_path: outputs/checkpoints/action_policy_sft_8b + load_training_state: false + only_load_scheduler_state: false + save_iter: 500 + save_to_object_store: + bucket: '' + credentials: '' + enabled: false + strict_resume: true + type: + _target_: cosmos3._src.vfm.checkpointer.dcp.DistributedCheckpointer + callbacks: null + disable_async: false + verbose: true +data_setting: + qwen_max_video_token_length: 8192 +dataloader_train: + _target_: cosmos3._src.vfm.datasets.joint_dataloader.IterativeJointDataLoader + audio_sample_rate: 48000 + dataloaders: + action_data: + dataloader: + _target_: cosmos3._src.vfm.datasets.action.dataloaders.InfiniteDataLoader + batch_size: 256 + dataset: + _target_: cosmos3._src.vfm.datasets.action.unified_dataset.wrap_dataset + action_channel_masking: true + append_duration_fps_timestamps: true + append_idle_frames: false + append_resolution_info: true + caption_key: ai_caption + cfg_dropout_rate: 0.1 + format_prompt_as_json: false + idle_frames_dropout: 0.05 + keep_aspect_ratio: true + list_of_datasets: + - _target_: cosmos3._src.vfm.datasets.action.unified_dataset.dataset_entry + dataset: + _target_: cosmos3._src.vfm.datasets.action.libero_dataset.LIBERODataset + action_normalization: quantile_rot + action_space: frame_wise_relative + action_stats_path: cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json + camera_mode: concat_view + chunk_length: 16 + download_videos: false + embodiment_type: libero + force_cache_sync: false + fps: 20 + image_size: 256 + mode: policy + pose_coordinate_frame: native + repo_id: + - libero_10 + - libero_object + - libero_spatial + - libero_goal + root: + - outputs/libero_datasets/libero_10 + - outputs/libero_datasets/libero_object + - outputs/libero_datasets/libero_spatial + - outputs/libero_datasets/libero_goal + rotation_space: 6d + seed: 0 + skip_video_loading: false + split: train + tolerance_s: 0.0001 + val_ratio: 0.01 + video_backend: torchcodec + name: libero + ratio: 1.0 + resolution: null + max_action_dim: 64 + pad_keys: null + resolution: null + shard_across_workers: true + text_token_key: text_token_ids + tokenizer_config: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_qwen2_tokenizer_with_download + config_variant: gcp + pretrained_model_name: Qwen/Qwen3-VL-8B-Instruct + video_temporal_downsample: 4 + in_order: false + multiprocessing_context: spawn + num_workers: 4 + pin_memory: true + seed: 42 + use_deterministic_seed: true + ratio: 1 + max_samples_per_batch: 256 + max_sequence_length: null + patch_spatial: 2 + seed: 42 + sound_latent_fps: 0 + tokenizer_spatial_compression_factor: 16 + tokenizer_temporal_compression_factor: 4 +dataloader_val: null +defaults: +- _self_ +- model: mot_fsdp +- data_train: null +- data_val: null +- optimizer: adamw +- scheduler: warmup_cosine_lr +- checkpoint: s3 +- callbacks: + - basic + - optimization + - job_monitor + - generation +- ema: power +- tokenizer: wan2pt2_tokenizer +- sound_tokenizer: null +- cluster: null +- vlm_config: null +- ckpt_type: dcp +- experiment: null +job: + cluster: + _type: cosmos3._src.vfm.configs.base.defaults.cluster.ClusterConfig + object_store_bucket_checkpoint: '' + object_store_bucket_data: '' + object_store_bucket_pretrained: '' + object_store_credential_checkpoint: '' + object_store_credential_data: '' + object_store_credential_pretrained: '' + group: action_libero + name: action_policy_sft_8b + project: cosmos3_action_libero + wandb_mode: online +model: + _recursive_: false + _target_: cosmos3._src.vfm.models.omni_mot_model.OmniMoTModel + config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.OmniMoTModelConfig + action_gen: true + causal_training_strategy: none + diffusion_expert_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.DiffusionExpertConfig + base_fps: 24 + enable_fps_modulation: true + load_weights_from_pretrained: false + max_vae_latent_side_after_patchify: 20 + patch_spatial: 2 + position_embedding_type: unified_3d_mrope + rope_h_extrapolation_ratio: 1.0 + rope_t_extrapolation_ratio: 1.0 + rope_w_extrapolation_ratio: 1.0 + timestep_range: 1.0 + unified_3d_mrope_reset_spatial_ids: true + unified_3d_mrope_temporal_modality_margin: 15000 + ema: + _type: cosmos3._src.vfm.configs.base.defaults.ema.EMAConfig + enabled: true + iteration_shift: 0 + rate: 0.1 + fixed_step_sampler_config: null + input_caption_key: ai_caption + input_image_key: images + input_video_key: video + joint_attn_implementation: two_way + latent_downsample_factor: 16 + lbl: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.LBLConfig + coeff_gen: null + coeff_und: null + method: local + log_enc_time_every_n: 100 + max_action_dim: 64 + max_num_tokens_after_packing: 45056 + natten_parameter_list: null + net: null + num_embodiment_domains: 32 + parallelism: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.ParallelismConfig + cfg_parallel_shard_degree: 1 + compile_dynamic: true + compiled_region: language + context_parallel_shard_degree: 1 + coordinate_descent_tuning: false + data_parallel_shard_degree: 8 + enable_inference_mode: false + max_autotune_pointwise: false + precision: bfloat16 + use_activation_checkpointing: true + use_cuda_graphs: false + use_torch_compile: true + rectified_flow_inference_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.RectifiedFlowInferenceConfig + num_train_timesteps: 1000 + scheduler_type: unipc + shift: 1 + use_dynamic_shifting: false + rectified_flow_training_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.RectifiedFlowTrainingConfig + action_loss_weight: 10.0 + high_sigma_ratio: 0.05 + high_sigma_timesteps_max: 1000 + high_sigma_timesteps_min: 995 + image_loss_scale: null + independent_action_schedule: false + loss_scale: 10.0 + normalize_loss_by_active: false + shift: + '256': 3 + '480': 5 + '720': 10 + shift_action: null + sound_loss_scale: null + train_time_action_distribution: logitnormal + train_time_image_distribution: logitnormal + train_time_sound_distribution: logitnormal + train_time_video_distribution: waver + train_time_weight: uniform + use_discrete_rf: false + use_dynamic_shift: false + use_high_sigma_strategy: false + use_high_sigma_strategy_action: false + resolution: '720' + sound_dim: null + sound_gen: false + sound_latent_fps: 25 + sound_tokenizer: null + state_ch: 48 + state_t: 300 + tokenizer: + _target_: cosmos3._src.vfm.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface + bucket_name: bucket + chunk_duration: 93 + encode_bucket_multiple: null + encode_chunk_frames: + '256': 68 + '480': 24 + '720': 12 + encode_exact_durations: + - 17 + - 61 + - 73 + keep_decoder_cache: false + object_store_credential_path_pretrained: '' + spatial_compression_factor: 16 + temporal_compression_factor: 4 + temporal_window: null + use_streaming_encode: false + vae_path: pretrained/tokenizers/video/wan2pt2/Wan2.2_VAE.pth + video_temporal_causal: false + vision_gen: true + vlm_config: + _type: cosmos3._src.vfm.configs.base.defaults.vlm.VLMConfig + checkpoint_path: s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/ + credential_path: '' + enable_gcs_patch_in_boto3: true + layer_module: Qwen2MoTDecoderLayer + load_pretrained: true + model_instance: + _target_: cosmos3._src.vfm.models.mot.unified_mot.Qwen3VLTextForCausalLM + config: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_vlm_config + base_config: + _target_: cosmos3._src.vfm.models.mot.unified_mot.Qwen3VLTextConfig.from_json_file + json_file: cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json + freeze_und: false + layer_module: MoTDecoderLayer + qk_norm_for_diffusion: true + qk_norm_for_text: true + tie_word_embeddings: true + model_name: Qwen/Qwen3-VL-8B-Instruct + qk_norm_for_diffusion: true + qk_norm_for_text: false + tie_word_embeddings: false + tokenizer: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_qwen2_tokenizer_with_download + config_variant: gcp + pretrained_model_name: Qwen/Qwen3-VL-8B-Instruct + use_system_prompt: false + vlm_checkpoint_format: null +model_parallel: + _cpu_offloading_context: null + async_tensor_model_parallel_allreduce: false + autocast_dtype: float32 + barrier_with_L1_time: true + batch_p2p_comm: true + batch_p2p_sync: true + bf16: false + context_parallel_size: 1 + cpu_offloading: false + cpu_offloading_activations: true + cpu_offloading_double_buffering: false + cpu_offloading_num_layers: 0 + cpu_offloading_weights: false + cross_entropy_fusion_impl: native + cross_entropy_loss_fusion: false + deallocate_pipeline_outputs: false + defer_embedding_wgrad_compute: false + delay_wgrad_compute: false + deterministic_mode: false + enable_autocast: false + ep_overlap_early_attn_memory_release: false + expert_model_parallel_size: 1 + expert_tensor_parallel_size: 1 + finalize_model_grads_func: null + fp16: false + grad_scale_func: null + grad_sync_func: null + gradient_accumulation_fusion: false + hierarchical_context_parallel_sizes: null + microbatch_group_size_per_vp_stage: 1 + moe_extended_tp: false + no_sync_func: null + num_microbatches_with_partial_activation_checkpoints: null + overlap_moe_expert_parallel_comm: false + overlap_p2p_comm: false + overlap_p2p_comm_warmup_flush: false + param_sync_func: null + params_dtype: float32 + perform_initialization: true + pipeline_dtype: null + pipeline_model_parallel_comm_backend: null + pipeline_model_parallel_size: 1 + sequence_parallel: false + tensor_model_parallel_size: 1 + timers: null + tp_comm_atomic_ag: false + tp_comm_atomic_rs: false + tp_comm_bootstrap_backend: nccl + tp_comm_bulk_dgrad: true + tp_comm_bulk_wgrad: true + tp_comm_overlap: false + tp_comm_overlap_ag: true + tp_comm_overlap_disable_fc1: false + tp_comm_overlap_disable_qkv: false + tp_comm_overlap_rs: true + tp_comm_overlap_rs_dgrad: false + tp_comm_split_ag: true + tp_comm_split_rs: true + use_cpu_initialization: false + use_ring_exchange_p2p: false + use_te_rng_tracker: false + variable_seq_lengths: false + virtual_pipeline_model_parallel_size: null + wgrad_deferral_limit: 0 +optimizer: + _target_: cosmos3._src.vfm.utils.optimizer.build_optimizer + betas: + - 0.9 + - 0.99 + eps: 1.0e-08 + fused: true + keys_to_select: + - moe_gen + - time_embedder + - vae2llm + - llm2vae + - action2llm + - llm2action + - action_modality_embed + lr: 5.0e-05 + lr_multipliers: + action2llm: 5.0 + action_modality_embed: 5.0 + llm2action: 5.0 + model: null + optimizer_type: FusedAdam + weight_decay: 0.05 +scheduler: + _target_: cosmos3._src.imaginaire.functional.lr_scheduler.LambdaLinearScheduler + cycle_lengths: + - 20000 + f_max: + - 1.0 + f_min: + - 0.0 + f_start: + - 1.0e-06 + verbosity_interval: 0 + warm_up_steps: + - 500 +trainer: + callbacks: + compile_tokenizer: + _target_: cosmos3._src.vfm.callbacks.compile_tokenizer.CompileTokenizer + compile_after_iterations: 3 + enabled: true + warmup_resolutions: + - '256' + - '480' + - '720' + dataloader_speed: + _target_: cosmos3._src.vfm.callbacks.dataloading_monitor.DetailedDataLoadingSpeedMonitor + every_n: 100 + save_s3: false + step_size: 1 + device_monitor: + _target_: cosmos3._src.vfm.callbacks.device_monitor.DeviceMonitor + every_n: 200 + log_memory_detail: true + save_s3: false + step_size: 1 + upload_every_n_mul: 5 + expert_heatmap: + _target_: cosmos3._src.vfm.callbacks.expert_heatmap.ExpertHeatmap + every_n: 1000 + grad_clip: + _target_: cosmos3._src.vfm.callbacks.grad_clip.GradClip + clip_norm: 1.0 + force_finite: true + heart_beat: + _target_: cosmos3._src.vfm.callbacks.heart_beat.HeartBeat + every_n: 200 + save_s3: false + step_size: 1 + update_interval_in_minute: 20 + iter_speed: + _target_: cosmos3._src.vfm.callbacks.iter_speed.IterSpeed + every_n: 100 + hit_thres: 50 + save_s3: false + save_s3_every_log_n: 500 + load_pretrained: + _target_: cosmos3._src.vfm.callbacks.load_pretrained.LoadPretrained + low_precision: + _target_: cosmos3._src.vfm.callbacks.low_precision.LowPrecisionCallback + config: null + trainer: null + update_iter: 1 + manual_gc: + _target_: cosmos3._src.imaginaire.callbacks.manual_gc.ManualGarbageCollection + every_n: 200 + gc_level: 1 + warm_up: 5 + mfu: + _target_: cosmos3._src.vfm.callbacks.mfu.MFUCallback + backwardpass_ratio: 2.0 + every_n: 100 + grad_accum_iter: 1 + hit_thres: 5 + include_padding: true + include_vae_encoder: true + moe_specialization: + _target_: cosmos3._src.vfm.callbacks.moe_specialization_callback.MoESpecializationCallback + every_n: 250 + moe_stability: + _target_: cosmos3._src.vfm.callbacks.moe_stability_callback.MoEStabilityCallback + every_n: 250 + norm_monitor: + _target_: cosmos3._src.vfm.callbacks.norm_monitor.NormMonitor + every_n: 100 + layer_norm_only: false + log_stat_wandb: true + model_key: null + save_s3: false + step_size: 1 + track_activations: true + ofu: + _target_: cosmos3._src.vfm.callbacks.ofu.OFUCallback + every_n: 100 + hit_thres: 5 + param_count: + _target_: cosmos3._src.vfm.callbacks.param_count.ParamCount + save_s3: false + sequence_packing_padding: + _target_: cosmos3._src.vfm.callbacks.sequence_packing_padding.SequencePackingPadding + every_n: 100 + sigma_loss_analysis: + _target_: cosmos3._src.vfm.callbacks.sigma_loss_analysis.SigmaLossAnalysis + every_n: 500 + every_n_viz: 500 + save_s3: false + skip_nan_step: + _target_: cosmos3._src.vfm.callbacks.skip_nan_step.SkipNaNStep + max_consecutive_nan: 100 + straggler_detection: + enabled: true + report_freq: 50 + training_stats: + _target_: cosmos3._src.vfm.callbacks.training_stats.TrainingStatsCallback + log_freq: 100 + wandb: + _target_: cosmos3._src.imaginaire.utils.callback.WandBCallback + config: null + trainer: null + wandb_2x: + _target_: cosmos3._src.vfm.callbacks.wandb_log.WandbCallback + logging_iter_multipler: 2 + save_logging_iter_multipler: 1 + save_s3: false + wandb_val: + _target_: cosmos3._src.vfm.callbacks.wandb_log_eval.WandbCallback + save_s3: false + compile_config: + recompile_limit: 100 + use_duck_shape: false + cudnn: + benchmark: true + deterministic: false + ddp: + broadcast_buffers: true + find_unused_parameters: false + static_graph: true + distributed_parallelism: fsdp + grad_accum_iter: 1 + grad_scaler_args: + enabled: false + logging_iter: 100 + max_iter: 16000 + max_val_iter: null + memory_format: preserve_format + profiling: + enable_memory_snapshot: false + enable_nsys: false + enable_profiling: false + profile_freq: 1 + profile_memory: false + profile_warmup: 3 + record_shape: false + save_s3: false + target_ranks: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + with_modules: true + with_stack: true + run_validation: false + run_validation_on_start: false + save_zero_checkpoint: false + seed: 0 + straggler_detection: + analyze_backward: true + analyze_dataloading: true + analyze_forward: true + analyze_optimizer: true + enabled: false + max_diff: 2.0 + profile_freq: 1 + raise_error: true + report_freq: 100 + save_s3: false + timeout_period: 999999999 + type: cosmos3._src.imaginaire.trainer.ImaginaireTrainer + validation_iter: 100 +upload_reproducible_setup: false diff --git a/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml b/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml new file mode 100644 index 00000000..b7b6705a --- /dev/null +++ b/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml @@ -0,0 +1,561 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_type: cosmos3._src.vfm.configs.base.config.Config +checkpoint: + broadcast_via_filesystem: false + dcp_async_mode_enabled: false + enable_gcs_patch_in_boto3: true + hf_export: + enabled: false + export_every_n: 1 + hf_repo_id: null + upload_to_object_store: + bucket: '' + credentials: '' + enabled: false + jit: + device: cuda + dtype: bfloat16 + enabled: false + input_shape: null + strict: true + keys_not_to_resume: [] + keys_to_skip_loading: + - net_ema. + load_ema_to_reg: false + load_from_object_store: + bucket: '' + credentials: '' + enabled: false + load_path: ??? + load_training_state: false + only_load_scheduler_state: false + save_iter: 100 + save_to_object_store: + bucket: '' + credentials: '' + enabled: false + strict_resume: true + type: + _target_: cosmos3._src.vfm.checkpointer.dcp.DistributedCheckpointer + callbacks: null + disable_async: false + verbose: true +data_setting: + qwen_max_video_token_length: 8192 +dataloader_train: + _target_: cosmos3._src.vfm.datasets.joint_dataloader.PackingDataLoader + audio_sample_rate: 48000 + dataloader: + _target_: cosmos3._src.vfm.datasets.joint_dataloader.RankPartitionedDataLoader + batch_size: 1 + datasets: + video: + dataset: + _target_: cosmos3._src.vfm.datasets.local_datasets.sft_dataset.get_sft_dataset + append_duration_fps_timestamps: true + append_resolution_info: true + caption_suffix: '' + cfg_dropout_keep_metadata: false + cfg_dropout_rate: 0.1 + conditioning_config: # 70% T2V, 20% I2V (first frame), 10% V2V (first 5 frames / 2 latent frames) + 0: 0.7 + 1: 0.2 + 2: 0.1 + conditioning_fps: -1 + conditioning_fps_noise_std: 0.0 + frame_selection_mode: first + jsonl_paths: + - ??? + min_short_edge: 0 + num_video_frames: -1 + resolution: '256' + sample_by_window: false + temporal_compression_factor: 4 + temporal_interval_mode: max_30fps + tokenizer_config: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_qwen2_tokenizer_with_download + config_variant: gcp + pretrained_model_name: Qwen/Qwen3-VL-8B-Instruct + use_system_prompt: false + ratio: 1 + in_order: true + num_workers: 4 + persistent_workers: true + pin_memory: true + prefetch_factor: 4 + sampler: null + dataset_name: default + max_samples_per_batch: null + max_sequence_length: 45056 + patch_spatial: 2 + sound_latent_fps: 0 + tokenizer_spatial_compression_factor: 16 + tokenizer_temporal_compression_factor: 4 +dataloader_val: null +defaults: +- _self_ +- model: mot_fsdp +- data_train: null +- data_val: null +- optimizer: adamw +- scheduler: warmup_cosine_lr +- checkpoint: s3 +- callbacks: + - basic + - optimization + - job_monitor + - generation +- ema: power +- tokenizer: wan2pt2_tokenizer +- sound_tokenizer: null +- cluster: gcp_iad_gb200 +- vlm_config: null +- ckpt_type: dcp +- experiment: null +job: + cluster: + _type: cosmos3._src.vfm.configs.base.defaults.cluster.ClusterConfig + object_store_bucket_checkpoint: '' + object_store_bucket_data: '' + object_store_bucket_pretrained: '' + object_store_credential_checkpoint: '' + object_store_credential_data: '' + object_store_credential_pretrained: '' + group: sft + name: mixed_modality_sft_8b + project: cosmos3 + wandb_mode: disabled +model: + _recursive_: false + _target_: cosmos3._src.vfm.models.omni_mot_model.OmniMoTModel + config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.OmniMoTModelConfig + action_gen: true + causal_training_strategy: none + diffusion_expert_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.DiffusionExpertConfig + base_fps: 24 + enable_fps_modulation: true + load_weights_from_pretrained: true + max_vae_latent_side_after_patchify: 20 + patch_spatial: 2 + position_embedding_type: unified_3d_mrope + rope_h_extrapolation_ratio: 1.0 + rope_t_extrapolation_ratio: 1.0 + rope_w_extrapolation_ratio: 1.0 + timestep_range: 1.0 + unified_3d_mrope_reset_spatial_ids: true + unified_3d_mrope_temporal_modality_margin: 15000 + ema: + _type: cosmos3._src.vfm.configs.base.defaults.ema.EMAConfig + enabled: true + iteration_shift: 0 + rate: 0.1 + fixed_step_sampler_config: null + input_caption_key: ai_caption + input_image_key: images + input_video_key: video + joint_attn_implementation: two_way + latent_downsample_factor: 16 + lbl: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.LBLConfig + coeff_gen: null + coeff_und: null + method: local + log_enc_time_every_n: 100 + max_action_dim: 64 + max_num_tokens_after_packing: 45056 + natten_parameter_list: null + net: null + num_embodiment_domains: 32 + parallelism: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.ParallelismConfig + cfg_parallel_shard_degree: 1 + compile_dynamic: true + compiled_region: language + context_parallel_shard_degree: 1 + coordinate_descent_tuning: false + data_parallel_shard_degree: 8 + enable_inference_mode: false + max_autotune_pointwise: false + precision: bfloat16 + use_activation_checkpointing: true + use_cuda_graphs: false + use_torch_compile: true + rectified_flow_inference_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.RectifiedFlowInferenceConfig + num_train_timesteps: 1000 + scheduler_type: unipc + shift: 1 + use_dynamic_shifting: false + rectified_flow_training_config: + _type: cosmos3._src.vfm.configs.base.defaults.model_config.RectifiedFlowTrainingConfig + action_loss_weight: 10.0 + high_sigma_ratio: 0.05 + high_sigma_timesteps_max: 1000 + high_sigma_timesteps_min: 995 + image_loss_scale: 1.0 + independent_action_schedule: false + loss_scale: 1.0 + normalize_loss_by_active: false + shift: + '256': 3 + '480': 5 + '720': 10 + shift_action: null + sound_loss_scale: null + train_time_action_distribution: logitnormal + train_time_image_distribution: logitnormal + train_time_sound_distribution: logitnormal + train_time_video_distribution: waver + train_time_weight: uniform + use_discrete_rf: false + use_dynamic_shift: false + use_high_sigma_strategy: false + use_high_sigma_strategy_action: false + resolution: '720' + sound_dim: null + sound_gen: false + sound_latent_fps: 25 + sound_tokenizer: null + state_ch: 48 + state_t: 300 + tokenizer: + _target_: cosmos3._src.vfm.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface + bucket_name: bucket + chunk_duration: 93 + encode_bucket_multiple: null + encode_chunk_frames: + '256': 68 + '480': 24 + '720': 12 + encode_exact_durations: null + keep_decoder_cache: false + object_store_credential_path_pretrained: credentials/gcp_checkpoint.secret + spatial_compression_factor: 16 + temporal_compression_factor: 4 + temporal_window: null + use_streaming_encode: false + vae_path: pretrained/tokenizers/video/wan2pt2/Wan2.2_VAE.pth + video_temporal_causal: false + vision_gen: true + vlm_config: + _type: cosmos3._src.vfm.configs.base.defaults.vlm.VLMConfig + checkpoint_path: s3://bucket/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/ + credential_path: credentials/gcp_checkpoint.secret + enable_gcs_patch_in_boto3: true + layer_module: Qwen2MoTDecoderLayer + load_pretrained: false + model_instance: + _target_: cosmos3._src.vfm.models.mot.unified_mot.Qwen3VLTextForCausalLM + config: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_vlm_config + base_config: + _target_: cosmos3._src.vfm.models.mot.unified_mot.Qwen3VLTextConfig.from_json_file + json_file: cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json + freeze_und: false + layer_module: MoTDecoderLayer + qk_norm_for_diffusion: true + qk_norm_for_text: true + tie_word_embeddings: true + model_name: Qwen/Qwen3-VL-8B-Instruct + qk_norm_for_diffusion: true + qk_norm_for_text: false + tie_word_embeddings: false + tokenizer: + _target_: cosmos3._src.vfm.configs.base.defaults.vlm.create_qwen2_tokenizer_with_download + config_variant: gcp + pretrained_model_name: Qwen/Qwen3-VL-8B-Instruct + use_system_prompt: false + vlm_checkpoint_format: null +model_parallel: + _cpu_offloading_context: null + async_tensor_model_parallel_allreduce: false + autocast_dtype: float32 + barrier_with_L1_time: true + batch_p2p_comm: true + batch_p2p_sync: true + bf16: false + context_parallel_size: 1 + cpu_offloading: false + cpu_offloading_activations: true + cpu_offloading_double_buffering: false + cpu_offloading_num_layers: 0 + cpu_offloading_weights: false + cross_entropy_fusion_impl: native + cross_entropy_loss_fusion: false + deallocate_pipeline_outputs: false + defer_embedding_wgrad_compute: false + delay_wgrad_compute: false + deterministic_mode: false + enable_autocast: false + ep_overlap_early_attn_memory_release: false + expert_model_parallel_size: 1 + expert_tensor_parallel_size: 1 + finalize_model_grads_func: null + fp16: false + grad_scale_func: null + grad_sync_func: null + gradient_accumulation_fusion: false + hierarchical_context_parallel_sizes: null + microbatch_group_size_per_vp_stage: 1 + moe_extended_tp: false + no_sync_func: null + num_microbatches_with_partial_activation_checkpoints: null + overlap_moe_expert_parallel_comm: false + overlap_p2p_comm: false + overlap_p2p_comm_warmup_flush: false + param_sync_func: null + params_dtype: float32 + perform_initialization: true + pipeline_dtype: null + pipeline_model_parallel_comm_backend: null + pipeline_model_parallel_size: 1 + sequence_parallel: false + tensor_model_parallel_size: 1 + timers: null + tp_comm_atomic_ag: false + tp_comm_atomic_rs: false + tp_comm_bootstrap_backend: nccl + tp_comm_bulk_dgrad: true + tp_comm_bulk_wgrad: true + tp_comm_overlap: false + tp_comm_overlap_ag: true + tp_comm_overlap_disable_fc1: false + tp_comm_overlap_disable_qkv: false + tp_comm_overlap_rs: true + tp_comm_overlap_rs_dgrad: false + tp_comm_split_ag: true + tp_comm_split_rs: true + use_cpu_initialization: false + use_ring_exchange_p2p: false + use_te_rng_tracker: false + variable_seq_lengths: false + virtual_pipeline_model_parallel_size: null + wgrad_deferral_limit: 0 +optimizer: + _target_: cosmos3._src.vfm.utils.optimizer.build_optimizer + betas: + - 0.9 + - 0.95 + eps: 1.0e-06 + fused: true + keys_to_select: + - moe_gen + - time_embedder + - vae2llm + - llm2vae + lr: 2.0e-05 + lr_multipliers: {} + model: null + optimizer_type: AdamW + weight_decay: 0 +scheduler: + _target_: cosmos3._src.imaginaire.functional.lr_scheduler.LambdaWarmUpCosineScheduler + cycle_lengths: + - 1000 + f_max: + - 1.0 + f_min: + - 0.0 + f_start: + - 0.0 + verbosity_interval: 0 + warm_up_steps: + - 50 +trainer: + callbacks: + compile_tokenizer: + _target_: cosmos3._src.vfm.callbacks.compile_tokenizer.CompileTokenizer + compile_after_iterations: 3 + enabled: false + warmup_resolutions: null + dataloader_speed: + _target_: cosmos3._src.vfm.callbacks.dataloading_monitor.DetailedDataLoadingSpeedMonitor + every_n: 100 + save_s3: false + step_size: 1 + device_monitor: + _target_: cosmos3._src.vfm.callbacks.device_monitor.DeviceMonitor + every_n: 200 + log_memory_detail: true + save_s3: false + step_size: 1 + upload_every_n_mul: 5 + every_n_sample_ema: + _target_: cosmos3._src.vfm.callbacks.every_n_draw_sample.EveryNDrawSample + do_x0_prediction: false + every_n: 999999 + fps: 16 + guidance: + - 0.0 + - 3.0 + - 7.0 + is_ema: true + n_sample_to_save: 128 + n_sigmas_for_x0_prediction: 4 + n_viz_sample: 2 + num_sampling_step: 35 + prompt_type: t5_xxl + run_at_start: false + save_local: false + save_s3: false + step_size: 1 + use_negative_prompt: false + every_n_sample_reg: + _target_: cosmos3._src.vfm.callbacks.every_n_draw_sample.EveryNDrawSample + do_x0_prediction: false + every_n: 999999 + fps: 16 + guidance: + - 0.0 + - 3.0 + - 7.0 + is_ema: false + n_sample_to_save: 128 + n_sigmas_for_x0_prediction: 4 + n_viz_sample: 2 + num_sampling_step: 35 + prompt_type: t5_xxl + run_at_start: false + save_local: false + save_s3: false + step_size: 1 + use_negative_prompt: false + expert_heatmap: + _target_: cosmos3._src.vfm.callbacks.expert_heatmap.ExpertHeatmap + every_n: 1000 + grad_clip: + _target_: cosmos3._src.vfm.callbacks.grad_clip.GradClip + clip_norm: 0.1 + force_finite: true + heart_beat: + _target_: cosmos3._src.vfm.callbacks.heart_beat.HeartBeat + every_n: 200 + save_s3: false + step_size: 1 + update_interval_in_minute: 20 + iter_speed: + _target_: cosmos3._src.vfm.callbacks.iter_speed.IterSpeed + every_n: 1 + hit_thres: 50 + save_s3: false + save_s3_every_log_n: 500 + load_pretrained: + _target_: cosmos3._src.vfm.callbacks.load_pretrained.LoadPretrained + low_precision: + _target_: cosmos3._src.vfm.callbacks.low_precision.LowPrecisionCallback + config: null + trainer: null + update_iter: 1 + manual_gc: + _target_: cosmos3._src.imaginaire.callbacks.manual_gc.ManualGarbageCollection + every_n: 5 + gc_level: 1 + warm_up: 1 + norm_monitor: + _target_: cosmos3._src.vfm.callbacks.norm_monitor.NormMonitor + every_n: 100 + layer_norm_only: false + log_stat_wandb: true + model_key: null + save_s3: false + step_size: 1 + track_activations: true + param_count: + _target_: cosmos3._src.vfm.callbacks.param_count.ParamCount + save_s3: false + sequence_packing_padding: + _target_: cosmos3._src.vfm.callbacks.sequence_packing_padding.SequencePackingPadding + every_n: 1 + sigma_loss_analysis: + _target_: cosmos3._src.vfm.callbacks.sigma_loss_analysis.SigmaLossAnalysis + every_n: 500 + every_n_viz: 500 + save_s3: false + skip_nan_step: + _target_: cosmos3._src.vfm.callbacks.skip_nan_step.SkipNaNStep + max_consecutive_nan: 100 + wandb: + _target_: cosmos3._src.imaginaire.utils.callback.WandBCallback + config: null + trainer: null + wandb_2x: + _target_: cosmos3._src.vfm.callbacks.wandb_log.WandbCallback + logging_iter_multipler: 1 + save_logging_iter_multipler: 1 + save_s3: false + wandb_val: + _target_: cosmos3._src.vfm.callbacks.wandb_log_eval.WandbCallback + save_s3: false + compile_config: + recompile_limit: 8 + use_duck_shape: false + cudnn: + benchmark: true + deterministic: false + ddp: + broadcast_buffers: true + find_unused_parameters: false + static_graph: true + distributed_parallelism: fsdp + grad_accum_iter: 2 + grad_scaler_args: + enabled: false + logging_iter: 1 + max_iter: 500 + max_val_iter: null + memory_format: preserve_format + profiling: + enable_memory_snapshot: false + enable_nsys: false + enable_profiling: false + profile_freq: 1 + profile_memory: false + profile_warmup: 3 + record_shape: false + save_s3: false + target_ranks: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + with_modules: true + with_stack: true + run_validation: false + run_validation_on_start: false + save_zero_checkpoint: false + seed: 42 + straggler_detection: + analyze_backward: true + analyze_dataloading: true + analyze_forward: true + analyze_optimizer: true + enabled: false + max_diff: 2.0 + profile_freq: 1 + raise_error: true + report_freq: 100 + save_s3: false + timeout_period: 999999999 + type: cosmos3._src.imaginaire.trainer.ImaginaireTrainer + validation_iter: 100 +upload_reproducible_setup: false diff --git a/cosmos3/dataset_samples.py b/cosmos3/dataset_samples.py new file mode 100644 index 00000000..356fe3bf --- /dev/null +++ b/cosmos3/dataset_samples.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Lazy dataset sample iterators for map-style and iterable-style datasets.""" + +import itertools +from collections.abc import Iterable, Iterator +from typing import Any, Callable + +import torch +from loguru import logger +from torch.utils.data import Dataset, IterableDataset +from torch.utils.data.dataloader import default_collate + +from cosmos3.args import OmniSampleOverrides +from cosmos3.scripts.dataset_utils import set_dataset_mode +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution + +Sample = tuple[OmniSampleOverrides, dict[str, Any]] + + +def _collate_sample(sample: dict) -> dict: + """Collate a single sample dict, adding a batch dim to tensors.""" + result: dict[str, Any] = {} + for key, val in sample.items(): + if isinstance(val, torch.Tensor): + result[key] = val.unsqueeze(0) + else: + try: + result[key] = default_collate([val]) + except TypeError: + result[key] = [val] + return result + + +class _BaseSamples(Iterable[Sample]): + """Base iterator yielding ``(OmniSampleOverrides, data_batch)`` pairs. + + Iterates over every (mode, sample_id) combination, applying an optional + transform to each raw item. Subclasses implement ``__iter__`` for + map-style and iterable-style datasets respectively. + """ + + def __init__( + self, + dataset: Dataset | IterableDataset, + modes: list[str], # model modes to iterate (e.g. ["joint", "forward_dynamics", etc.]) + sample_ids: list[int], # indices into dataset to yield + transform: Callable | None, # UVA transform pipeline applied per item, or None + resolution: str | None, # global resolution override; inferred from video shape if None + dataset_name: str, # name of the dataset" + sample_overrides_data: dict[str, Any] | None = None, # additional overrides to apply to every sample + ) -> None: + self._dataset = dataset + self._modes = modes + self._sample_ids = sample_ids + self._transform = transform + self._resolution = resolution + self._dataset_name = dataset_name + self._sample_overrides_data = sample_overrides_data + + def __len__(self) -> int: + return len(self._modes) * len(self._sample_ids) + + def _make_sample_from_raw(self, raw_sample: Any, sample_idx: int, mode: str) -> Sample: + """Apply transform, collate, and wrap a raw dataset item into a ``Sample``.""" + resolution = self._resolution + if resolution is None: + video = raw_sample.get("video") + if video is not None: + resolution = get_vision_data_resolution(video.shape[-2:]) + + if self._transform is not None: + raw_sample = self._transform(raw_sample, resolution=resolution) + sample_data = _collate_sample(raw_sample) + + sample_name = f"{self._dataset_name}/{mode}/{sample_idx}" if self._dataset_name else f"{mode}/{sample_idx}" + sample_args = OmniSampleOverrides( + name=sample_name, + prompt=sample_data.get("ai_caption", [""])[0], + resolution=resolution, # type: ignore + raw_action_dim=sample_data.get("raw_action_dim", [None])[0], + ) + # Apply any additional sample overrides specified in the setup config (e.g. num_steps, guidance, etc.) + sample_args = sample_args.model_copy(update=self._sample_overrides_data) + return sample_args, sample_data + + +class MapDatasetSamples(_BaseSamples): + """Iterator for map-style datasets (``Dataset``), accessed via ``__getitem__``. + + Iterates modes in order, indexing each sample directly by its — enabling + random access. + """ + + def __iter__(self) -> Iterator[Sample]: + for mode in self._modes: + set_dataset_mode(self._dataset, mode) + for sample_idx in self._sample_ids: + raw_sample = self._dataset[sample_idx] # type: ignore[index] + yield self._make_sample_from_raw(raw_sample, sample_idx, mode) + + +class IterableDatasetSamples(_BaseSamples): + """Iterator for iterable-style datasets (``IterableDataset``), accessed via ``__iter__``. + + Since random access is unavailable, advances the underlying iterator using + ``islice`` to reach each target sample index in order — requires + ``sample_ids`` to be sorted ascending. + """ + + def __iter__(self) -> Iterator[Sample]: + for mode in self._modes: + set_dataset_mode(self._dataset, mode) + dataset = iter(getattr(self._dataset, "dataset", self._dataset)) + cur_ix = 0 + for sample_idx in sorted(self._sample_ids): + try: + raw_sample = next(itertools.islice(dataset, sample_idx - cur_ix, None)) + except StopIteration: + # Dataset exhausted early (inaccurate __len__); move on to next mode. + logger.warning( + f"Dataset {self._dataset_name!r} exhausted early while iterating mode={mode!r}: " + f"tried to reach sample_idx={sample_idx}, expected __len__={len(self._dataset)}. " # type: ignore[arg-type] + "Moving on to next mode." + ) + break + cur_ix = sample_idx + 1 + yield self._make_sample_from_raw(raw_sample, sample_idx, mode) + + +DatasetSamples = MapDatasetSamples | IterableDatasetSamples diff --git a/cosmos3/defaults/forward_dynamics/sample_args.json b/cosmos3/defaults/forward_dynamics/sample_args.json new file mode 100644 index 00000000..7afb4149 --- /dev/null +++ b/cosmos3/defaults/forward_dynamics/sample_args.json @@ -0,0 +1,24 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 5.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false, + "domain_name": "", + "image_size": 256, + "action_chunk_size": 16 +} diff --git a/cosmos3/defaults/image2video/sample_args.json b/cosmos3/defaults/image2video/sample_args.json new file mode 100644 index 00000000..e61e5be0 --- /dev/null +++ b/cosmos3/defaults/image2video/sample_args.json @@ -0,0 +1,21 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 10.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "The video captures a series of frames showing macroblocking artifacts, chromatic aberration, high-frequency noise, and rolling shutter distortion. It includes static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, bit-depth compression artifacts, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Avoid moiré patterns, edge halos, and temporal aliasing. Furthermore, the content defies common sense, generating illogical scenarios, nonsensical entities, absurd character behaviors, and conceptual paradoxes that violate basic human reasoning and everyday reality. The video looks like a surreal or glitchy hallucination. Overall, the video is of poor quality.", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false +} diff --git a/cosmos3/defaults/inverse_dynamics/sample_args.json b/cosmos3/defaults/inverse_dynamics/sample_args.json new file mode 100644 index 00000000..7afb4149 --- /dev/null +++ b/cosmos3/defaults/inverse_dynamics/sample_args.json @@ -0,0 +1,24 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 5.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false, + "domain_name": "", + "image_size": 256, + "action_chunk_size": 16 +} diff --git a/cosmos3/defaults/policy/sample_args.json b/cosmos3/defaults/policy/sample_args.json new file mode 100644 index 00000000..7afb4149 --- /dev/null +++ b/cosmos3/defaults/policy/sample_args.json @@ -0,0 +1,24 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 5.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false, + "domain_name": "", + "image_size": 256, + "action_chunk_size": 16 +} diff --git a/cosmos3/defaults/prompt_upsampler.txt b/cosmos3/defaults/prompt_upsampler.txt new file mode 100644 index 00000000..d4842c9b --- /dev/null +++ b/cosmos3/defaults/prompt_upsampler.txt @@ -0,0 +1,69 @@ +You are an expert prompt engineer for a text-to-video model. Your task is to take a short, basic user prompt and upsample it into a dense, multi-sentence narrative caption. + +To ensure the final caption contains the correct density of visual, cinematic, and temporal details, you must complete this task in two phases. + +--- +### PHASE 1: SCENE DESIGN (JSON DRAFT) +First, you must invent the missing details of the user's prompt by filling out a strict JSON schema. Brainstorm plausible, visually rich details for every field. Output this JSON inside `` XML tags. + +JSON Schema to complete: +{ + "short_description": "Concise summary of subjects, actions, and setting.", + "subjects": [ + { + "description": "Detailed visual description, posture, colors", + "appearance_details": "Accessories, markings, logos", + "relationship": "Relation to other subjects", + "location": "Placement (e.g., center, left foreground)", + "relative_size": "e.g., small, large within frame", + "orientation": "e.g., facing left, profile", + "pose": "Body position", + "action": "Main action", + "clothing": "Attire, colors, footwear (if human)", + "expression": "Facial expression (if human)", + "gender": "Apparent gender presentation (if human)", + "age": "Apparent age group (if human)", + "skin_tone_and_texture": "Apparent skin tone (if human)" + } + ], + "background_setting": "Environment type, key structures, scenery, furniture.", + "lighting": { + "conditions": "e.g., bright daylight, dim indoor", + "direction": "e.g., front-lit, backlit", + "shadows": "Presence and quality of shadows" + }, + "aesthetics": { + "composition": "e.g., rule of thirds, centered", + "color_scheme": "Dominant colors, contrasts", + "mood_atmosphere": "Visual mood" + }, + "cinematography": { + "camera_motion": "e.g., static, slow pan left, tracking", + "framing": "e.g., close-up, wide shot", + "camera_angle": "e.g., eye-level, low angle", + "depth_of_field": "e.g., shallow, deep", + "focus": "Where the focus lies", + "lens_focal_length": "e.g., wide-angle, telephoto" + }, + "style_medium": "e.g., live-action video, 3D animation", + "artistic_style": "e.g., minimalist, highly realistic", + "context": "e.g., vlog, product demo, cinematic shot", + "actions": ["Array of visually observable events in chronological order"], + "text_renders": ["Array of visible text, location, size, color, font"], + "temporal_structure": "Organization over time (e.g., single continuous shot, loop)" +} + +--- +### PHASE 2: DENSE NARRATIVE REWRITE +After drafting the JSON, generate the final output. This must be a dense, multi-sentence caption derived STRICTLY from your JSON draft. Put this final output inside `` XML tags. + +Rules for the Final Narrative: +- Length & Format: Write exactly ONE coherent paragraph consisting of several full sentences. Do not use bullet points or lists. +- Content: Cover the main subjects, setting, chronological actions, background elements, lighting, cinematography, and temporal structure defined in your JSON. +- Taboo Words: DO NOT refer to the video itself. Avoid phrases like "the video...", "the scene...", "the clip...", "the frame...". +- Perspective: Describe human body sides/movements from the subject's own perspective (e.g., "her right hand" means the subject's right hand). +- Spatial Phrasing: Avoid camera-centric phrases like "enters the frame" or "comes into view". Describe motion using spatial relationships (e.g., "approaches from the left", "steps from behind the wall"). +- Tone: Use clear, neutral, objective language without opinions or value judgments. No inferred thoughts or hidden motivations. + +USER PROMPT: +"{caption}" diff --git a/cosmos3/defaults/text2image/sample_args.json b/cosmos3/defaults/text2image/sample_args.json new file mode 100644 index 00000000..7ba35c2c --- /dev/null +++ b/cosmos3/defaults/text2image/sample_args.json @@ -0,0 +1,21 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 10.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This image is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 1, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false +} diff --git a/cosmos3/defaults/text2video/sample_args.json b/cosmos3/defaults/text2video/sample_args.json new file mode 100644 index 00000000..ba699d84 --- /dev/null +++ b/cosmos3/defaults/text2video/sample_args.json @@ -0,0 +1,21 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 10.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality.", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false +} diff --git a/cosmos3/defaults/video2video/sample_args.json b/cosmos3/defaults/video2video/sample_args.json new file mode 100644 index 00000000..80699c38 --- /dev/null +++ b/cosmos3/defaults/video2video/sample_args.json @@ -0,0 +1,21 @@ +{ + "num_steps": 35, + "guidance": 6.0, + "shift": 10.0, + "sigma_max": 80.0, + "normalize_cfg": false, + "autoregressive": false, + "negative_prompt": "", + "duration_template": "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS.", + "resolution_template": "This video is of {height}x{width} resolution.", + "negative_metadata_mode": "none", + "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", + "inverse_resolution_template": "This video is not of {height}x{width} resolution.", + "negative_prompt_keep_metadata": true, + "aspect_ratio": "16,9", + "fps": 24, + "num_frames": 189, + "video_save_quality": 10, + "image_save_quality": 95, + "enable_sound": false +} diff --git a/cosmos3/defaults/video_captioner.txt b/cosmos3/defaults/video_captioner.txt new file mode 100644 index 00000000..fcefbe13 --- /dev/null +++ b/cosmos3/defaults/video_captioner.txt @@ -0,0 +1,67 @@ +You are an expert video captioner for a text-to-video model. Your task is to watch the provided video frames and produce a dense, multi-sentence narrative caption that faithfully describes what you see. + +To ensure the final caption contains the correct density of visual, cinematic, and temporal details, you must complete this task in two phases. + +--- +### PHASE 1: SCENE ANALYSIS (JSON DRAFT) +First, analyze the video frames and fill out a strict JSON schema describing what you observe. Output this JSON inside `` XML tags. + +JSON Schema to complete: +{ + "short_description": "Concise summary of subjects, actions, and setting.", + "subjects": [ + { + "description": "Detailed visual description, posture, colors", + "appearance_details": "Accessories, markings, logos", + "relationship": "Relation to other subjects", + "location": "Placement (e.g., center, left foreground)", + "relative_size": "e.g., small, large within frame", + "orientation": "e.g., facing left, profile", + "pose": "Body position", + "action": "Main action", + "clothing": "Attire, colors, footwear (if human)", + "expression": "Facial expression (if human)", + "gender": "Apparent gender presentation (if human)", + "age": "Apparent age group (if human)", + "skin_tone_and_texture": "Apparent skin tone (if human)" + } + ], + "background_setting": "Environment type, key structures, scenery, furniture.", + "lighting": { + "conditions": "e.g., bright daylight, dim indoor", + "direction": "e.g., front-lit, backlit", + "shadows": "Presence and quality of shadows" + }, + "aesthetics": { + "composition": "e.g., rule of thirds, centered", + "color_scheme": "Dominant colors, contrasts", + "mood_atmosphere": "Visual mood" + }, + "cinematography": { + "camera_motion": "e.g., static, slow pan left, tracking", + "framing": "e.g., close-up, wide shot", + "camera_angle": "e.g., eye-level, low angle", + "depth_of_field": "e.g., shallow, deep", + "focus": "Where the focus lies", + "lens_focal_length": "e.g., wide-angle, telephoto" + }, + "style_medium": "e.g., live-action video, 3D animation", + "artistic_style": "e.g., minimalist, highly realistic", + "context": "e.g., vlog, product demo, cinematic shot", + "actions": ["Array of visually observable events in chronological order"], + "text_renders": ["Array of visible text, location, size, color, font"], + "temporal_structure": "Organization over time (e.g., single continuous shot, loop)" +} + +--- +### PHASE 2: DENSE NARRATIVE REWRITE +After drafting the JSON, generate the final output. This must be a dense, multi-sentence caption derived STRICTLY from your scene analysis. Put this final output inside `` XML tags. + +Rules for the Final Narrative: +- Length & Format: Write exactly ONE coherent paragraph consisting of several full sentences. Do not use bullet points or lists. +- Content: Cover the main subjects, setting, chronological actions, background elements, lighting, cinematography, and temporal structure observed in the video frames. +- Taboo Words: DO NOT refer to the video itself. Avoid phrases like "the video...", "the scene...", "the clip...", "the frame...". +- Perspective: Describe human body sides/movements from the subject's own perspective (e.g., "her right hand" means the subject's right hand). +- Spatial Phrasing: Avoid camera-centric phrases like "enters the frame" or "comes into view". Describe motion using spatial relationships (e.g., "approaches from the left", "steps from behind the wall"). +- Tone: Use clear, neutral, objective language without opinions or value judgments. No inferred thoughts or hidden motivations. +- Faithfulness: Only describe what is actually visible in the provided frames. Do not hallucinate or invent details not present in the video. diff --git a/cosmos3/fixtures/__init__.py b/cosmos3/fixtures/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/fixtures/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/fixtures/args.py b/cosmos3/fixtures/args.py new file mode 100644 index 00000000..0c7e1539 --- /dev/null +++ b/cosmos3/fixtures/args.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +import pytest +from typing_extensions import Self + +MAX_GPUS = int(os.environ.get("TEST_MAX_GPUS", "8")) +"""Maximum number of GPUs.""" +ALL_LEVELS = (0, 1, 2) +ALL_NUM_GPUS = (0, 1, MAX_GPUS) +ALLOWED_GPUS_BY_LEVEL: dict[int, tuple[int, ...]] = { + 0: (0, 1), + 1: (0, 1, MAX_GPUS), + 2: (0, 1, MAX_GPUS), +} +"""Allowed number of GPUs by level.""" + +if TYPE_CHECKING: + Level = int + NumGpus = int +else: + Level = Literal[tuple(ALL_LEVELS)] + NumGpus = Literal[tuple(ALL_NUM_GPUS)] + + +@dataclass(frozen=True) +class Args: + worker_id: str + worker_index: int + worker_count: int + + enable_manual: bool + num_gpus: int | None + levels: set[int] | None + + @classmethod + def from_config(cls, config: pytest.Config) -> Self: + worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master") + if worker_id == "master": + worker_index = 0 + else: + worker_index = int(worker_id.removeprefix("gw")) + worker_count = int(os.environ.get("PYTEST_XDIST_WORKER_COUNT", "1")) + + if config.option.levels is not None: + levels = set(map(int, config.option.levels.split(","))) + if levels.difference(ALL_LEVELS): + raise ValueError(f"Invalid levels: {levels}") + else: + levels = None + + return cls( + worker_id=worker_id, + worker_index=worker_index, + worker_count=worker_count, + enable_manual=config.option.manual, + num_gpus=config.option.num_gpus, + levels=levels, + ) + + +_ARGS: Args | None = None + + +def init_args(args: Args) -> None: + global _ARGS + if _ARGS is not None: + raise ValueError("Args already initialized") + _ARGS = args + + +def get_args() -> Args: + global _ARGS + if _ARGS is None: + raise ValueError("Args not initialized") + return _ARGS diff --git a/cosmos3/fixtures/script.py b/cosmos3/fixtures/script.py new file mode 100644 index 00000000..c581e7db --- /dev/null +++ b/cosmos3/fixtures/script.py @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inference/training script test fixtures. + +Used by 'tests/scripts_test.py'. +""" + +import os +import re +import shutil +import subprocess +import warnings +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path +from typing import Callable + +import numpy as np +import pydantic +import pytest + +from cosmos3.common.args import MEDIA_EXTENSIONS, ResolvedFilePath +from cosmos3.common.init import get_free_port +from cosmos3.fixtures.args import Level, NumGpus +from cosmos3._src.imaginaire.utils.checkpoint_db import HF_VERSION +from cosmos3._src.imaginaire.utils.easy_io import easy_io + +INPUT_DIR = Path("inputs").absolute() +OUTPUT_DIR = Path("outputs").absolute() + + +class ScriptConfig(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid") + + script: ResolvedFilePath + """Script path.""" + use_tmp_input_dir: bool = False + """If set, use a per-test temp directory for INPUT_DIR.""" + levels: tuple[Level, ...] = (0,) + """Test levels.""" + gpus: tuple[NumGpus, NumGpus, NumGpus] = (0, 1, 1) + """Number of GPUs for each level.""" + marks: tuple[pytest.MarkDecorator | pytest.Mark, ...] = () + """Additional pytest marks.""" + + golden_psnr: pydantic.PositiveFloat = 14.0 + """Golden comparison PSNR threshold in dB.""" + + training: bool = False + """Enable training features.""" + internal: bool = False + """Enable internal (nvidia-only) features.""" + + before_script: Callable[["ScriptRunner"], None] | None = None + """Function to run before the script.""" + after_script: Callable[["ScriptRunner"], None] | None = None + """Function to run after the script.""" + + @property + def name(self) -> str: + return self.script.stem + + def get_marks(self, level: int) -> list[pytest.MarkDecorator | pytest.Mark]: + marks = list(self.marks) + if level not in self.levels: + marks.append(pytest.mark.manual) + marks.append(pytest.mark.gpus(self.gpus[level])) + return marks + + +@dataclass(kw_only=True, frozen=True) +class ScriptRunner: + request: pytest.FixtureRequest + tmp_path_factory: pytest.TempPathFactory + tmp_path: Path + level: int = 0 + + @cached_property + def output_name(self) -> str: + test_name = self.request.node.name + if "[" in test_name and "]" in test_name: + base_part, param_part = test_name.split("[", 1) + param_part = param_part.rstrip("]").replace("/", "_").replace("-", "_") + sanitized_name = f"{base_part}_{param_part}" + else: + sanitized_name = test_name.replace("/", "_").replace("-", "_") + return sanitized_name + + @cached_property + def input_dir(self) -> Path: + return INPUT_DIR + + @cached_property + def tmp_input_dir(self) -> Path: + return self.tmp_path / "inputs" + + @cached_property + def output_dir(self) -> Path: + return OUTPUT_DIR / "pytest" / self.output_name + + @cached_property + def golden_dir(self) -> Path: + return INPUT_DIR / "outputs/pytest" / self.output_name + + def _get_env( + self, + cfg: ScriptConfig, + *, + torchrun_args: list[str] | None = None, + inference_args: list[str] | None = None, + train_args: list[str] | None = None, + train_overrides: list[str] | None = None, + ) -> dict[str, str]: + if torchrun_args is None: + torchrun_args = [] + if inference_args is None: + inference_args = [] + if train_args is None: + train_args = [] + if train_overrides is None: + train_overrides = [] + + num_gpus = os.environ["NUM_GPUS"] + master_port = get_free_port() + env = dict(os.environ) + env |= { + "COSMOS_INTERNAL": "1" if cfg.internal else "0", + "COSMOS_TRAINING": "1" if cfg.training or cfg.internal else "0", + "INPUT_DIR": f"{self.tmp_input_dir if cfg.use_tmp_input_dir else self.input_dir}", + "OUTPUT_DIR": f"{self.output_dir}", + "TMP_DIR": f"{self.tmp_path}/tmp", + "MASTER_PORT": str(master_port), + "HF_VERSION": HF_VERSION, + "TORCHRUN_ARGS": " ".join( + [ + f"--nproc_per_node={num_gpus}", + f"--master_port={master_port}", + *torchrun_args, + ] + ), + "INFERENCE_ARGS": " ".join( + [ + "--seed=0", + "--debug", + *inference_args, + ] + ), + "TRAIN_ARGS": " ".join( + [ + *train_args, + ] + ), + "TRAIN_OVERRIDES": " ".join( + [ + "job.wandb_mode=disabled", + f"model.config.parallelism.data_parallel_shard_degree={num_gpus}", + *train_overrides, + ] + ), + } + if not cfg.internal: + # Disable S3 checkpoints + env |= { + "IMAGINAIRE_CACHE_DIR": "/invalid", + } + return env + + def get_env(self, cfg: ScriptConfig, level: int) -> dict[str, str]: + match level: + case 0: + return self._get_env(cfg) | {"COSMOS_SMOKE": "1"} + case 1: + return self._get_env( + cfg, + inference_args=[ + "--no-guardrails", + ], + train_overrides=[ + "trainer.max_iter=5", + ], + ) + case 2: + return self._get_env( + cfg, + inference_args=[ + "--guardrails", + ], + train_overrides=[ + "trainer.max_iter=20", + ], + ) + case _: + assert False, "unreachable" + + def run(self, cfg: ScriptConfig, level: int): + object.__setattr__(self, "level", level) # frozen dataclass, but level is set per call + shutil.rmtree(self.output_dir, ignore_errors=True) + if cfg.before_script: + cfg.before_script(self) + subprocess.check_call( + ["bash", "-euxo", "pipefail", str(cfg.script)], + cwd=self.request.config.rootpath, + env=self.get_env(cfg, level), + ) + if cfg.after_script: + cfg.after_script(self) + + if False: + _check_golden_dir(self.output_dir, self.golden_dir, min_psnr=cfg.golden_psnr) + + +def script_test(configs: list[ScriptConfig]) -> Callable[[type], type]: + names = set() + for cfg in configs: + if cfg.name in names: + raise ValueError(f"Duplicate script name: {cfg.name}") + names.add(cfg.name) + + def decorator(cls: type) -> type: + @pytest.fixture + def script_runner( + self, request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, tmp_path: Path + ) -> ScriptRunner: + return ScriptRunner(request=request, tmp_path_factory=tmp_path_factory, tmp_path=tmp_path) + + setattr(cls, "script_runner", script_runner) + + @pytest.mark.level(0) + @pytest.mark.parametrize("cfg", [pytest.param(cfg, id=cfg.name, marks=cfg.get_marks(0)) for cfg in configs]) + def test_level_0(self, cfg: ScriptConfig, script_runner: ScriptRunner): + script_runner.run(cfg, 0) + + setattr(cls, "test_level_0", test_level_0) + + @pytest.mark.level(1) + @pytest.mark.parametrize("cfg", [pytest.param(cfg, id=cfg.name, marks=cfg.get_marks(1)) for cfg in configs]) + def test_level_1(self, cfg: ScriptConfig, script_runner: ScriptRunner): + script_runner.run(cfg, 1) + + setattr(cls, "test_level_1", test_level_1) + + @pytest.mark.level(2) + @pytest.mark.parametrize("cfg", [pytest.param(cfg, id=cfg.name, marks=cfg.get_marks(2)) for cfg in configs]) + def test_level_2(self, cfg: ScriptConfig, script_runner: ScriptRunner): + script_runner.run(cfg, 2) + + setattr(cls, "test_level_2", test_level_2) + return cls + + return decorator + + +def _extract_bash_commands(md_file: Path) -> list[str]: + content = md_file.read_text() + pattern = r"```(bash|shell)([^\n]*)\n(.*?)```" + matches = re.findall(pattern, content, re.DOTALL) + scripts = [] + for lang, attrs, block_content in matches: + if "exclude=true" in attrs.lower(): + continue + + lines = [] + for line in block_content.strip().split("\n"): + if line.strip() and not line.strip().startswith("#"): + line = line.split("#")[0].rstrip() + # Replace --nproc_per_node with dynamic NUM_GPUS value + line = re.sub(r"--nproc_per_node=\d+", "--nproc_per_node=$NUM_GPUS", line) + line = re.sub(r"--master_port=\d+", "--master_port=$MASTER_PORT", line) + if line: + lines.append(line) + + if lines: + script = "\n".join(lines) + scripts.append(script) + + return scripts + + +def _array_to_float(array: np.ndarray) -> np.ndarray: + if np.issubdtype(array.dtype, np.floating): + assert np.min(array) >= 0.0 and np.max(array) <= 1.0 + return array + if array.dtype == np.uint8: + return array / 255.0 + raise NotImplementedError(f"Unsupported dtype: {array.dtype}") + + +def _compute_psnr(array1: np.ndarray, array2: np.ndarray) -> float: + """Compare PSNR between two arrays.""" + array1 = _array_to_float(array1) + array2 = _array_to_float(array2) + overall_mse = ((array1 - array2) ** 2).mean() + return 10 * np.log10(1.0 / overall_mse) if overall_mse > 0 else float("inf") + + +def _check_golden_file(output_path: Path, golden_path: Path, /, min_psnr: float) -> None: + output_array, _output_meta = easy_io.load(output_path) + assert isinstance(output_array, np.ndarray) + golden_array, _golden_meta = easy_io.load(golden_path) + assert isinstance(golden_array, np.ndarray) + psnr = _compute_psnr(output_array, golden_array) + if psnr < min_psnr: + warnings.warn( + f"FAIL: Golden PSNR {psnr:.2f} dB is less than minimum {min_psnr:.2f} dB for file '{output_path}'" + ) + else: + print(f"PASS: Golden PSNR {psnr:.2f} dB is greater than minimum {min_psnr:.2f} dB for file '{output_path}'") + + +def _check_golden_dir(output_dir: Path, golden_dir: Path, /, min_psnr: float) -> None: + if not golden_dir.exists(): + warnings.warn(f"Golden directory '{golden_dir}' does not exist") + return + for dirpath, _dirnames, filenames in os.walk(golden_dir): + for filename in filenames: + golden_path = Path(dirpath) / filename + output_path = output_dir / golden_path.relative_to(golden_dir) + if output_path.suffix not in MEDIA_EXTENSIONS: + continue + if not output_path.exists(): + warnings.warn(f"File '{output_path}' missing in output directory") + continue + _check_golden_file(output_path, golden_path, min_psnr=min_psnr) diff --git a/cosmos3/flags.py b/cosmos3/flags.py new file mode 100644 index 00000000..b95b8a39 --- /dev/null +++ b/cosmos3/flags.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Final + +from cosmos3._src.imaginaire.flags import Flags as _Flags +from cosmos3._src.imaginaire.flags import _get_bool + +EARLY_ACCESS: Final[bool] = _get_bool("COSMOS_EARLY_ACCESS", False) +"""Whether to use early access checkpoints.""" + + +@dataclass +class Flags(_Flags): + early_access: bool = EARLY_ACCESS + + +FLAGS = Flags() diff --git a/cosmos3/inference.py b/cosmos3/inference.py new file mode 100644 index 00000000..a0f4b78c --- /dev/null +++ b/cosmos3/inference.py @@ -0,0 +1,1129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import pickle +import random +from collections.abc import Generator, Iterable +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Sequence, cast, override + +import attrs +import cattrs +import cattrs.preconf.json +import safetensors.torch +import torch +from PIL import Image +from torch.utils._pytree import tree_map_only +from torch.utils.data import Dataset +from typing_extensions import Self, assert_never + +from cosmos3.args import ( + ActionMode, + ModelMode, + NegativeMetadataMode, + OmniSampleArgs, + OmniSetupArgs, +) +from cosmos3.common.args import ( + CheckpointType, + ConfigFileType, + ParallelismArgs, + SampleArgs, + SampleOutput, + SampleOutputs, + SetupArgs, +) +from cosmos3.common.inference import Inference, sync_distributed_errors +from cosmos3.common.init import get_rank, get_world_size +from cosmos3.model import Cosmos3OmniConfig, Cosmos3OmniModel +from cosmos3.vision import ( + build_conditioned_video_batch, + build_image_edit_batch, + load_conditioning_image, + load_conditioning_video, + pil_to_conditioning_frames, + resize_pil_image, +) +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.visualize.video import save_img_or_video +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel +from cosmos3._src.vfm.models.vlm.qwen3_vl.utils import _SYSTEM_PROMPT_IMAGE_EDITING + +if TYPE_CHECKING: + from cosmos3._src.vfm.configs.base.defaults.model_config import OmniMoTModelConfig + + +def _iter_batch_ranges( + sampler_indices: list[int], + sample_args_list: Sequence[OmniSampleArgs], + model: OmniMoTModel, + max_model_len: int | None, + max_num_seqs: int | None, +) -> Generator[tuple[int, int]]: + """Yield ``(start, end)`` position pairs into *sampler_indices* for each batch. + + The boundary logic accounts for either a token budget (``max_model_len``) + or a sequence-count budget (``max_num_seqs``). Exactly one must be set. + """ + batch_start = 0 + running_tokens = 0 + running_seqs = 0 + + for pos, sample_idx in enumerate(sampler_indices): + sa = sample_args_list[sample_idx] + assert isinstance(sa, OmniSampleArgs) + assert sa.num_outputs == 1, "num_outputs must be 1" + + if max_model_len is not None: + num_tokens = _compute_num_tokens_for_sample(sa, model) + if running_tokens > 0 and running_tokens + num_tokens > max_model_len: + yield (batch_start, pos) + batch_start = pos + running_tokens = 0 + running_tokens += num_tokens + + elif max_num_seqs is not None: + if running_seqs > 0 and running_seqs + 1 > max_num_seqs: + yield (batch_start, pos) + batch_start = pos + running_seqs = 0 + running_seqs += 1 + + else: + raise ValueError("Either max_model_len or max_num_seqs must be set") + + if running_tokens > 0 or running_seqs > 0: + yield (batch_start, len(sampler_indices)) + + +def _compute_num_tokens_for_sample(sample_args: OmniSampleArgs, model: OmniMoTModel) -> int: + """Estimate the number of tokens for a single inference sample. + + Follows the counting logic in + ``JointDataLoader._compute_num_tokens_per_sample`` (vision + text + EOS). + """ + w, h = sample_args.vision_size + T = sample_args.num_frames + + spatial_cf = cast(int, model.tokenizer_vision_gen.spatial_compression_factor) + temporal_cf = cast(int, model.tokenizer_vision_gen.temporal_compression_factor) + patch_spatial: int = model.config.diffusion_expert_config.patch_spatial + + vae_spatial_downsample = spatial_cf * patch_spatial + vae_temporal_downsample = temporal_cf + + latent_h = h // vae_spatial_downsample + latent_w = w // vae_spatial_downsample + latent_t = 1 + (T - 1) // vae_temporal_downsample + num_vision_tokens = latent_h * latent_w * latent_t + + + # small compared to vision tokens, so we can ignore them for now. + + return num_vision_tokens + + +def _format_prompt_with_template( + prompt: str, + *, + fps: int, + num_frames: int, + duration_template: str | None, + resolution_template: str | None, + h: int, + w: int, + force_duration_template: bool = False, +) -> str: + """Append duration/fps and resolution metadata to a prompt.""" + prompt = prompt.strip() + if duration_template is not None and (num_frames > 1 or force_duration_template): + duration = num_frames / fps + dur_text = duration_template.format(duration=duration, fps=fps) + prompt = prompt.rstrip(".") + ". " + dur_text + + prompt = prompt.strip() + if resolution_template is not None: + res_text = resolution_template.format(height=h, width=w) + prompt = prompt.rstrip(".") + ". " + res_text + + return prompt + + +def _parse_json_object_prompt(prompt: str) -> dict | None: + """Return the parsed dict iff ``prompt`` is a JSON object string; else ``None``. + + JSON arrays / numbers / strings / nulls are NOT considered "JSON-object + prompts" and return ``None`` so they continue down the plain-text path. + """ + try: + obj = json.loads(prompt) + except (json.JSONDecodeError, TypeError, ValueError): + return None + return obj if isinstance(obj, dict) else None + + +def _format_json_prompt_with_template( + prompt_obj: dict, + *, + fps: int, + num_frames: int, + aspect_ratio: str | None, + h: int, + w: int, +) -> str: + """JSON-prompt counterpart to ``_format_prompt_with_template``. + + Injects structured metadata fields directly into the parsed prompt object, + matching the training-time augmentors so the tokenizer sees the exact + schema the model was trained on: + + - ``ResolutionTextInfo`` -> ``resolution: {"H": int, "W": int}``, ``aspect_ratio: str`` + - ``DurationFPSTextTimeStamps`` -> ``duration: "s"``, ``fps: float`` + + Always overwrites existing values for these keys, mirroring the augmentors' + ``dict.update(...)`` semantics: the actual generation specs are the source + of truth, regardless of what the input prompt may have specified. + """ + duration_seconds = int(num_frames / fps) if fps > 0 else 0 + metadata: dict[str, Any] = { + "duration": f"{duration_seconds}s", + "fps": float(fps), + "resolution": {"H": int(h), "W": int(w)}, + } + if aspect_ratio is not None: + metadata["aspect_ratio"] = aspect_ratio + + prompt_obj.update(metadata) + log.debug(f"Injected JSON-prompt metadata fields: {sorted(metadata.keys())}") + + return json.dumps(prompt_obj) + + +def _get_prompt_sample_data(sample_args: OmniSampleArgs, model: OmniMoTModel, *, h: int, w: int, device: Any) -> dict: + duration_template = sample_args.duration_template + inverse_duration_template = sample_args.inverse_duration_template + prompt_obj = _parse_json_object_prompt(sample_args.prompt) + prompt_is_json = prompt_obj is not None + if not prompt_is_json: + prompt = _format_prompt_with_template( + sample_args.prompt, + fps=sample_args.fps, + num_frames=sample_args.num_frames, + duration_template=duration_template, + resolution_template=sample_args.resolution_template, + h=h, + w=w, + ) + else: + assert prompt_obj is not None # type-narrowing + prompt = _format_json_prompt_with_template( + prompt_obj, + fps=sample_args.fps, + num_frames=sample_args.num_frames, + aspect_ratio=sample_args.aspect_ratio, + h=h, + w=w, + ) + out = { + model.input_caption_key: [prompt] * sample_args.num_outputs, + } + + negative_prompt = sample_args.negative_prompt + if sample_args.negative_metadata_mode == NegativeMetadataMode.SAME: + negative_prompt = ( + _format_prompt_with_template( + negative_prompt if negative_prompt is not None else "", + fps=sample_args.fps, + num_frames=sample_args.num_frames, + duration_template=duration_template, + resolution_template=sample_args.resolution_template, + h=h, + w=w, + ) + .lstrip(".") + .strip() + ) + elif sample_args.negative_metadata_mode == NegativeMetadataMode.INVERSE: + negative_prompt = ( + _format_prompt_with_template( + negative_prompt if negative_prompt is not None else "", + fps=sample_args.fps, + num_frames=sample_args.num_frames, + duration_template=inverse_duration_template, + resolution_template=sample_args.inverse_resolution_template, + h=h, + w=w, + force_duration_template=True, + ) + .lstrip(".") + .strip() + ) + + if negative_prompt: + neg_key = "neg_" + model.input_caption_key + out[neg_key] = [negative_prompt] * sample_args.num_outputs + + return out + + +def _get_image_edit_sample_data( + sample_args: OmniSampleArgs, + model: OmniMoTModel, + *, + device: Any, +) -> dict: + """Create a sample batch for image-edit generation.""" + assert sample_args.vision_path is not None + if sample_args.resolution and sample_args.aspect_ratio: + w, h = sample_args.vision_size + conditioning_frames = load_conditioning_image(Path(sample_args.vision_path), target_h=h, target_w=w) + else: + pil_img = Image.open(sample_args.vision_path).convert("RGB") + pil_img = resize_pil_image(pil_img, max_size=512, padding_constant=32) + conditioning_frames, h, w = pil_to_conditioning_frames(pil_img) + + conditioning_frames = conditioning_frames.to(device=device) + batch = build_image_edit_batch(conditioning_frames, h=h, w=w, batch_size=sample_args.num_outputs) + batch["system_prompt"] = _SYSTEM_PROMPT_IMAGE_EDITING + batch |= _get_prompt_sample_data(sample_args, model, h=h, w=w, device=device) + return batch + + +def get_sample_data( + sample_args: OmniSampleArgs, + model: OmniMoTModel, + *, + device: Any = "cuda", +) -> dict: + """Create a sample batch for generation.""" + + match sample_args.action_mode: + case ActionMode.FORWARD_DYNAMICS | ActionMode.POLICY | ActionMode.INVERSE_DYNAMICS: + from cosmos3.action import get_action_sample_data + + assert sample_args.vision_path is not None + return get_action_sample_data( + model_config=model, + batch_size=sample_args.num_outputs, + prompt=sample_args.prompt, + vision_path=sample_args.vision_path, + action_mode=sample_args.action_mode, + action_path=sample_args.action_path, + domain_name=sample_args.domain_name, + resolution=str(sample_args.image_size), + aspect_ratio=sample_args.aspect_ratio, + action_chunk_size=sample_args.action_chunk_size, + max_action_dim=model.config.max_action_dim, + raw_action_dim=sample_args.raw_action_dim, + duration_template=sample_args.duration_template, + resolution_template=sample_args.resolution_template, + fps=sample_args.fps, + device=device, + ) + case ActionMode.IMAGE2VIDEO | None: + pass + case _: + assert_never(sample_args.action_mode) + + + if sample_args.model_mode == ModelMode.IMAGE2IMAGE: + return _get_image_edit_sample_data(sample_args, model, device=device) + + w, h = sample_args.vision_size + if sample_args.num_frames == 1: + input_vision_key = model.input_image_key + else: + input_vision_key = model.input_video_key + + with torch.device(device): + match sample_args.condition_vision_mode: + case "image": + assert sample_args.vision_path is not None + conditioning_frames = load_conditioning_image(Path(sample_args.vision_path), target_h=h, target_w=w) + case "video": + assert sample_args.vision_path is not None + assert sample_args.condition_frame_indexes_vision is not None + temporal_compression_factor = model.config.tokenizer.temporal_compression_factor + max_frames = (max(sample_args.condition_frame_indexes_vision) + 1) * temporal_compression_factor + conditioning_frames = load_conditioning_video( + Path(sample_args.vision_path), target_h=h, target_w=w, max_frames=max_frames + ) + case _: + conditioning_frames = None + + if conditioning_frames is not None: + assert sample_args.condition_frame_indexes_vision is not None + conditioned = build_conditioned_video_batch( + conditioning_frames, + condition_frames_vision=sample_args.condition_frame_indexes_vision, + w=w, + h=h, + num_frames=sample_args.num_frames, + fps=sample_args.fps, + batch_size=sample_args.num_outputs, + ) + video_tensor = torch.cat(conditioned["video"], dim=0).to(device=device) # [1,3,T,H,W] + sequence_plan = conditioned["sequence_plan"] + else: + video_tensor = [ + torch.zeros(1, 3, sample_args.num_frames, h, w) for _ in range(sample_args.num_outputs) + ] # list of [1,3,T,H,W] + sequence_plan = None + + out: dict = { + input_vision_key: video_tensor, + "image_size": [ + torch.tensor([[h, w, h, w]], dtype=torch.float32) for _ in range(sample_args.num_outputs) + ], # list of [1,4] + "t5_text_embeddings": torch.randn(sample_args.num_outputs, 512, 1024, dtype=torch.bfloat16), # [B,512,1024] + "fps": torch.full((sample_args.num_outputs,), float(sample_args.fps)), # [B] + "conditioning_fps": torch.full((sample_args.num_outputs,), float(sample_args.fps)), # [B] + "num_frames": torch.full((sample_args.num_outputs,), sample_args.num_frames), # [B] + "is_preprocessed": True, + } + if sequence_plan is not None: + out["sequence_plan"] = sequence_plan + + out |= _get_prompt_sample_data(sample_args, model, w=w, h=h, device=device) + + + return out + + +def _merge_data_batches(batches: list[dict[str, Any]]) -> dict[str, Any]: + """Merge multiple single-sample data dicts into one batched dict. + + Values that are lists are concatenated. Tensors with a batch dimension are + concatenated along dim 0. Scalar/bool values are taken from the first batch. + + Args: + batches (list[dict[str, Any]]): List of data batches to merge. + + Returns: + dict[str, Any]: Merged data batch. + + Raises: + ValueError: If the batches have different keys. + ValueError: If the scalar/bool values are not the same across all batches. + """ + if len(batches) == 1: + return batches[0] + + # First ensure that all batches have the same keys. + reference_keys = set(batches[0].keys()) + for i, batch in enumerate(batches[1:], start=1): + if set(batch.keys()) != reference_keys: + raise ValueError(f"Batch {i} keys {set(batch.keys())} differ from batch 0 keys {reference_keys}") + + # Then merge the batches. + merged: dict[str, Any] = {} + keys = batches[0].keys() + for key in keys: + values = [b[key] for b in batches if key in b] + first = values[0] + if isinstance(first, list): + merged[key] = [item for v in values for item in v] + elif isinstance(first, torch.Tensor): + assert first.ndim > 0, "Tensor must have at least one (batch) dimension" + merged[key] = torch.cat(values, dim=0) + else: + if not all(v == values[0] for v in values): + raise ValueError(f"Key {key} values are not the same: {values}") + merged[key] = first + return merged + + +def _finalize_sample_args_list(sample_args_list: Sequence[OmniSampleArgs], model: OmniMoTModel) -> list[OmniSampleArgs]: + """ + Finalize sample arguments by checking and validating. + Also, expand the sample arguments to multiple outputs if num_outputs > 1. + """ + if all(sample_args.num_outputs == 1 for sample_args in sample_args_list): + return list(sample_args_list) + + finalized_sample_args_list = [] + + for sample_args in sample_args_list: + seed = sample_args.seed + num_outputs = sample_args.num_outputs + output_dir = sample_args.output_dir + + for i in range(num_outputs): + sample_args_i = sample_args.model_copy(deep=True) + sample_args_i.seed = (seed + i) if seed is not None else None + sample_args_i.num_outputs = 1 + sample_args_i.output_dir = output_dir / f"{i}" + finalized_sample_args_list.append(sample_args_i) + + return finalized_sample_args_list + + +def create_batches_from_dataset( + samples: Iterable[tuple[OmniSampleArgs, dict[str, Any]]], + model: OmniMoTModel, + *, + max_num_seqs: int | None = None, + max_model_len: int | None = None, +) -> Generator[tuple[list[OmniSampleArgs], dict[str, Any], list[dict[str, Any]]]]: + """Create batches from pre-loaded (sample_args, data_batch) pairs. + + Reuses the same token-count / sample-count batching logic as + ``OmniInference.create_batches``, but works with dataset iterators that + already provide data. Samples with ``num_outputs > 1`` are multi-seed + expanded via ``_finalize_sample_args_list``; callers that want only a + subset of samples expanded should set ``num_outputs`` accordingly before + yielding each sample. + + Args: + samples: Iterable of ``(OmniSampleArgs, data_batch)`` pairs. + model: The model, used for token counting and seed expansion. + max_num_seqs: Maximum number of sequences per batch. + max_model_len: Maximum total tokens per batch. + Exactly one of ``max_num_seqs`` or ``max_model_len`` must be set. + + Yields: + ``(sample_args_list, merged_data_batch, per_sample_data_batches)`` tuples. + ``per_sample_data_batches`` is the list of individual data dicts before + merging, useful when callers need per-sample post-processing. + """ + assert max_model_len is not None or max_num_seqs is not None, "Either max_model_len or max_num_seqs must be set" + assert max_model_len is None or max_num_seqs is None, "Either max_model_len or max_num_seqs must be set, not both" + + def _prepare_for_merge(db: dict[str, Any]) -> dict[str, Any]: + """Reshape a per-sample data dict so ``_merge_data_batches`` can combine it. + + Returns a shallow copy of ``db`` with two key-specific rewrites; all + other values are passed through by reference. + + - **0-dim tensors** are promoted to 1-D via ``unsqueeze(0)``. Without + this, ``_merge_data_batches`` would route them to ``torch.stack`` + (adding a new batch dim), inconsistent with the ``torch.cat`` path + taken by every other tensor key. + - **``"video"``** is converted from a ``[1, C, T, H, W]`` tensor (the + shape produced by ``_collate_sample``'s ``unsqueeze(0)``) into a + length-1 ``list[Tensor]`` of shape ``[C, T, H, W]``. + ``_merge_data_batches`` then flattens these per-sample lists across + the batch instead of calling ``torch.cat`` on the tensors, which + would fail when samples in the same chunk have different aspect + ratios or spatial dims (e.g. 544x736 vs 640x640). + + Args: + db: Single sample's data dict, typically straight out of + ``_collate_sample``. + + Returns: + A new dict with the rewrites applied; the original ``db`` is not + mutated. + """ + updated_db: dict[str, Any] = {} + for key, value in db.items(): + if isinstance(value, torch.Tensor) and value.ndim == 0: + updated_db[key] = value.unsqueeze(0) + elif key == "video" and isinstance(value, torch.Tensor): + updated_db[key] = [value.squeeze(0)] + else: + updated_db[key] = value + return updated_db + + # Materialize all samples and apply seed expansion. + all_args: list[OmniSampleArgs] = [] + all_data: list[dict[str, Any]] = [] + for sa, db in samples: + db = _prepare_for_merge(db) + if sa.num_outputs > 1: + expanded = _finalize_sample_args_list([sa], model) + else: + expanded = [sa] + for exp_sa in expanded: + all_args.append(exp_sa) + all_data.append(db) + + if not all_args: + return + + # Reuse _iter_batch_ranges for smart batching. + indices = list(range(len(all_args))) + for batch_start, batch_end in _iter_batch_ranges(indices, all_args, model, max_model_len, max_num_seqs): + chunk_args = [all_args[indices[pos]] for pos in range(batch_start, batch_end)] + chunk_data = [all_data[indices[pos]] for pos in range(batch_start, batch_end)] + yield chunk_args, _merge_data_batches(chunk_data), chunk_data + + +def _finalize_data_batch(data_batch: dict[str, Any], batch_size: int, model: OmniMoTModel): + """Finalize and validate data batch in-place.""" + for old_key, new_key in [ + ("video", model.input_video_key), + ("images", model.input_image_key), + ("ai_caption", model.input_caption_key), + ]: + if old_key in data_batch and new_key != old_key: + if new_key in data_batch: + raise ValueError(f"Conflicting keys: '{old_key}' and '{new_key}'") + data_batch[new_key] = data_batch.pop(old_key) + + # Unstack variable length tensors + _multi_item_keys = { + "text_token_ids", + "action", + model.input_video_key, + model.input_image_key, + } + for key in _multi_item_keys: + if key in data_batch and isinstance(data_batch[key], torch.Tensor): + if key == model.input_image_key: + data_batch[key] = [ + t.unsqueeze(0).squeeze(2) for t in torch.unbind(data_batch[key]) + ] # list of [1,C,H,W] + elif key == model.input_video_key: + if data_batch.get("is_preprocessed", False): + data_batch[key] = [t.unsqueeze(0) for t in torch.unbind(data_batch[key])] # list of [1,C,T,H,W] + else: + data_batch[key] = list(torch.unbind(data_batch[key])) + else: + data_batch[key] = [[t] for t in torch.unbind(data_batch[key])] + + # Validate + if len(data_batch[model.input_caption_key]) != batch_size: + raise ValueError( + f"Data batch length ({len(data_batch[model.input_caption_key])}) does not match batch size ({batch_size})" + ) + + +class SampleDataset(Dataset): + """PyTorch map-style dataset over inference sample args. + + Each item is a ``(SampleArgs, data_dict)`` tuple where the data dict is + lazily prepared on access via ``__getitem__``. + """ + + def __init__(self, sample_args_list: Sequence[SampleArgs], model: OmniMoTModel) -> None: + self._sample_args_list = list(sample_args_list) + self._model = model + + def __len__(self) -> int: + return len(self._sample_args_list) + + def __getitem__(self, idx: int) -> tuple[SampleArgs, dict[str, Any]]: + sample_args = self._sample_args_list[idx] + assert isinstance(sample_args, OmniSampleArgs) + assert sample_args.output_dir is not None + data_batch = sample_args.get_data(device="cuda") + if not data_batch: + data_batch = get_sample_data(sample_args=sample_args, model=self._model) + return sample_args, data_batch + + +@dataclass +class OmniInference(Inference): + # pyrefly: ignore[bad-override] + model: OmniMoTModel + vae_decode_stream: torch.cuda.Stream | None = None + + @property + def model_config(self) -> "OmniMoTModelConfig": + return self.model.config + + @classmethod + def _get_parallelism_config(cls, setup_args: ParallelismArgs) -> ParallelismConfig: + return ParallelismConfig( + enable_inference_mode=True, + data_parallel_shard_degree=setup_args.dp_shard_size, + context_parallel_shard_degree=setup_args.cp_size, + cfg_parallel_shard_degree=setup_args.cfgp_size, + use_torch_compile=setup_args.use_torch_compile, + use_cuda_graphs=setup_args.use_cuda_graphs + and setup_args.dp_shard_size * setup_args.cp_size * setup_args.cfgp_size == 1, + compiled_region=setup_args.compiled_region, + compile_dynamic=setup_args.compile_dynamic, + use_activation_checkpointing=False, + ) + + @override + @classmethod + def _create(cls, setup_args: SetupArgs, **kwargs: Any) -> Self: + assert isinstance(setup_args, OmniSetupArgs) + assert setup_args.output_dir is not None + + sampler_override = setup_args.sampler + parallelism_config = cls._get_parallelism_config(setup_args) + if setup_args.checkpoint_type == CheckpointType.DCP and setup_args.config_file_type == ConfigFileType.MODULE: + from cosmos3.common.config import save_config + from cosmos3._src.vfm.utils.model_loader import load_model_from_checkpoint + + if not setup_args.experiment: + raise ValueError("'experiment' is required") + if not setup_args.config_file: + raise ValueError("'config_file' is required") + + Cosmos3OmniModel.before_load_model() + model, config = load_model_from_checkpoint( + experiment_name=setup_args.experiment, + config_file=setup_args.config_file, + checkpoint_path=setup_args.checkpoint_path, + credential_path=setup_args.credential_path or None, + parallelism_config=attrs.asdict(parallelism_config), + load_ema_to_reg=setup_args.use_ema_weights, + experiment_opts=[ + *setup_args.experiment_overrides, + f"model.config.rectified_flow_inference_config.scheduler_type={sampler_override}", + ], + use_cache_checkpoint=setup_args.checkpoint_cache_dir is not None, + cache_checkpoint_rootdir=str(setup_args.checkpoint_cache_dir or ""), + ) + model = cast("OmniMoTModel", model) + Cosmos3OmniModel.after_load_model(model) + save_config(config, setup_args.output_dir) + else: + checkpoint_path = setup_args.download_checkpoint() + if setup_args.config_file_type == ConfigFileType.MODULE: + config = None + else: + model_dict = setup_args.load_model_config_dict() + config = Cosmos3OmniConfig(model=model_dict) + model = Cosmos3OmniModel.from_pretrained_dcp( + checkpoint_path, config=config, parallelism_config=parallelism_config + ).model + if model.config.rectified_flow_inference_config.scheduler_type != sampler_override: + model.config.rectified_flow_inference_config.scheduler_type = sampler_override + model.set_up_scheduler_and_sampler() + log.debug(f"Sampler overridden to: {sampler_override}") + + vae_decode_stream: torch.cuda.Stream | None = None + if setup_args.use_separate_pipeline_vision_decode_gpu: + # The CP/CFGP ranks are partitioned into replica-local groups of size + # cp_size * cfgp_size. Only the first rank in each group owns separate-VAE + # decode work. For example, with cp_size=2 and cfgp_size=1, ranks [0,1] + # form one replica and only rank 0 returns True here. + replica_size = setup_args.cp_size * setup_args.cfgp_size + + is_vae_output_rank = (replica_size <= 1) or (get_rank() % replica_size == 0) + + vae_device_index = setup_args.cp_size * setup_args.cfgp_size + if torch.cuda.device_count() <= vae_device_index: + raise RuntimeError( + "--use-separate-pipeline-vision-decode-gpu requires a spare visible local GPU on the " + "same node as the decode-owning rank, but the configured local decode GPU index " + f"{vae_device_index} is unavailable with only {torch.cuda.device_count()} visible local GPUs." + ) + if is_vae_output_rank: + vae_device = torch.device("cuda", vae_device_index) + inference_device = torch.device("cuda", torch.cuda.current_device()) + vae_decode_stream = torch.cuda.Stream(device=vae_device) + vae = model.tokenizer_vision_gen.model + vae.device = str(vae_device) + vae.model = vae.model.to(device=vae_device) + vae.scale = tree_map_only(torch.Tensor, lambda tensor: tensor.to(device=vae_device), vae.scale) + + original_encode = model.encode + original_decode = model.decode + + def encode_on_vae(state: torch.Tensor) -> torch.Tensor: + return original_encode(state.to(device=vae_device, non_blocking=True)).to( + device=inference_device, non_blocking=True + ) + + def decode_on_vae(latent: torch.Tensor) -> torch.Tensor: + return original_decode(latent.to(device=vae_device, non_blocking=True)) + + model.encode = encode_on_vae + model.decode = decode_on_vae + log.info( + f"Configured vision VAE on device '{vae_device}' while inference remains on '{inference_device}'", + rank0_only=False, + ) + + return cls(setup_args=setup_args, model=model, vae_decode_stream=vae_decode_stream, **kwargs) + + @classmethod + def save_data( + cls, + data: dict[str, Any], + *, + output_dir: Path, + output_name: str, + truncate_action_dim: bool = True, + ) -> list[Path]: + """Save data to disk in multiple formats. + + Tensors are saved as ``.safetensors``, non-tensor values as + ``.pickle``. If ``truncate_action_dim`` is True and both ``action`` + and ``raw_action_dim`` are present in ``data``, the action tensor's last dimension + is truncated to ``raw_action_dim`` before saving. + + Returns a list of paths to all files written. + """ + files: list[Path] = [] + data_tensors: dict[str, torch.Tensor] = {} + data_pickle: dict[str, Any] = {} + for k, v in data.items(): + if isinstance(v, list) and len(v) > 0 and isinstance(v[0], torch.Tensor): + for i, x in enumerate(v): + data_tensors[f"{k}[{i}]"] = x + elif isinstance(v, torch.Tensor): + data_tensors[k] = v + else: + data_pickle[k] = v + + # Truncate `action` tensor's last dimension to `raw_action_dim` if available; + # otherwise use the full action tensor as-is. + if truncate_action_dim and "action" in data_tensors and "raw_action_dim" in data_tensors: + raw_action_dim = data_tensors["raw_action_dim"][0] + action = data_tensors["action"][..., :raw_action_dim] + data_tensors["action"] = action + log.debug(f"Truncated 'action' tensor to shape={action.shape}") + + if data_tensors: + tensors_file = output_dir / f"{output_name}.safetensors" + safetensors.torch.save_file( + {k: v.detach().cpu().contiguous() for k, v in data_tensors.items()}, tensors_file + ) + files.append(tensors_file) + + if data_pickle: + pickle_file = output_dir / f"{output_name}.pickle" + with pickle_file.open("wb") as f: + pickle.dump(data_pickle, f) + files.append(pickle_file) + + return files + + @override + def create_batches( + self, sample_args_list: Sequence[SampleArgs] + ) -> Generator[tuple[list[SampleArgs], dict[str, Any]]]: + assert isinstance(self.setup_args, OmniSetupArgs) + max_model_len = self.setup_args.max_model_len + max_num_seqs = self.setup_args.max_num_seqs + assert max_model_len is not None or max_num_seqs is not None, "Either max_model_len or max_num_seqs must be set" + assert max_model_len is None or max_num_seqs is None, ( + "Either max_model_len or max_num_seqs must be set, not both" + ) + + sample_args_list = _finalize_sample_args_list(cast(Sequence[OmniSampleArgs], sample_args_list), self.model) + dataset = SampleDataset(sample_args_list, self.model) + + # Mod-shard the dataset indices across replicas. + sampler_indices = list(range(self.replica_id, len(dataset), self.num_replicas)) + + # --- Phase 1: pre-compute batch boundaries (cheap, no data prep) --- + batch_ranges = list( + _iter_batch_ranges(sampler_indices, sample_args_list, self.model, max_model_len, max_num_seqs) + ) + num_local_batches = len(batch_ranges) + + log.debug(f"Number of local batches: {num_local_batches}", rank0_only=False) + + # --- Phase 2: synchronize batch count across replicas --- + # All ranks within a replica share the same replica_id and therefore + # the same local batch count, so a global MAX all-reduce is sufficient + # to align all replicas. + if torch.distributed.is_initialized() and self.num_replicas > 1: + count_tensor = torch.tensor([num_local_batches], dtype=torch.long, device="cuda") + torch.distributed.all_reduce(count_tensor, op=torch.distributed.ReduceOp.MAX) + global_max_batches = int(count_tensor.item()) + else: + global_max_batches = num_local_batches + + log.debug(f"Number of global batches: {global_max_batches}") + log.debug(f"Number of padding batches: {global_max_batches - num_local_batches}", rank0_only=False) + + # --- Phase 3: yield real batches (lazily prepare data) --- + batches_yielded = 0 + + for batch_start, batch_end in batch_ranges: + chunk_args: list[SampleArgs] = [] + chunk_data: list[dict[str, Any]] = [] + + for pos in range(batch_start, batch_end): + sample_idx = sampler_indices[pos] + sample_args, data_batch = dataset[sample_idx] + + if self.setup_args.debug and self.should_process_sample(sample_args): + assert sample_args.output_dir is not None + sample_args.output_dir.mkdir(parents=True, exist_ok=True) + self.save_data( + data_batch, + output_dir=sample_args.output_dir, + output_name="sample_data", + ) + + chunk_args.append(sample_args) + chunk_data.append(data_batch) + + yield chunk_args, _merge_data_batches(chunk_data) + batches_yielded += 1 + + assert batches_yielded == num_local_batches + + # --- Phase 4: pad with dummy batches so every replica calls + # generate_batch the same number of times (prevents collective + # deadlocks in context-parallel / CFG-parallel communication). --- + dummy_sa = sample_args_list[0].model_copy(update={"output_dir": None, "name": "padding"}) + dummy_data = dataset[0][1] + while batches_yielded < global_max_batches: + yield [dummy_sa], dummy_data + batches_yielded += 1 + assert batches_yielded == global_max_batches + + @torch.no_grad() + @override + def generate_batch( + self, sample_args_list: Sequence[SampleArgs], data_batch: dict[str, Any], *, warmup: bool = False + ) -> list[SampleOutputs]: + assert all(isinstance(sa, OmniSampleArgs) for sa in sample_args_list) + + + # Process inputs + try: + with sync_distributed_errors(): + for sample_args in sample_args_list: + if self.should_process_sample(sample_args) and not warmup: + log.debug(f"{sample_args.__class__.__name__}({sample_args})") + assert sample_args.output_dir is not None + sample_args.output_dir.mkdir(parents=True, exist_ok=True) + sample_args_file = sample_args.output_dir / "sample_args.json" + sample_args_file.write_text(sample_args.model_dump_json()) + log.info(f"Saved sample args to '{sample_args_file}'", rank0_only=False) + + assert all(sa.num_outputs == 1 for sa in sample_args_list), "num_outputs must be 1" + _finalize_data_batch(data_batch=data_batch, batch_size=len(sample_args_list), model=self.model) + except Exception as e: + return [ + self._handle_sample_exception(args, e) + for args in sample_args_list + if self.should_process_sample(args) and not warmup + ] + + # Generate samples + # + # Can't catch exceptions here. This code contains collective operations + # that will hang if any rank fails. If a rank fails, we must restart + # the entire distributed environment. + # + # Use the first sample's sampling parameters for the whole batch. + # All samples in a batch share guidance, num_steps, shift, etc. + def _getattr(sample_args_list: Sequence[OmniSampleArgs], attr: str) -> Any: + attr_values = [getattr(sa, attr) for sa in sample_args_list] + if all(v == attr_values[0] for v in attr_values): + return attr_values[0] + else: + raise ValueError(f"Attribute '{attr}' is not the same for all samples: {attr_values}") + + is_distilled = self.model.config.fixed_step_sampler_config is not None + if is_distilled: + sampler = self.model.fixed_step_sampler + guidance = 1.0 + else: + sampler = None + guidance = _getattr(sample_args_list, "guidance") + + should_decode_outputs = self.should_process_sample(sample_args_list[0]) + + def decode_vision(vision_latent: torch.Tensor) -> torch.Tensor: + """ + Handles decoding of vision latents, either on the inference device or on a separate VAE device if configured. + """ + if not should_decode_outputs: + tokenizer_vision_gen = self.model.tokenizer_vision_gen + return vision_latent.new_zeros( + ( + vision_latent.shape[0], + 3, + tokenizer_vision_gen.get_pixel_num_frames(int(vision_latent.shape[2])), + int(vision_latent.shape[3]) * tokenizer_vision_gen.spatial_compression_factor, + int(vision_latent.shape[4]) * tokenizer_vision_gen.spatial_compression_factor, + ) + ) + if self.vae_decode_stream is None: + # We are not using a separate GPU for VAE decoding, so decode directly on the inference device + vision = self.model.decode(vision_latent) # [B,C,T,H,W] + return ((1.0 + vision) / 2).clamp(0, 1) # [B,C,T,H,W] + # We are using a separate GPU for VAE decoding, so we need to issue decode on the VAE device + vision_ready = torch.cuda.Event() + torch.cuda.current_stream(device=vision_latent.device).record_event(vision_ready) + self.vae_decode_stream.wait_event(vision_ready) + with torch.cuda.stream(self.vae_decode_stream): + vision = self.model.decode(vision_latent) # [B,C,T,H,W] + return ((1.0 + vision) / 2).clamp(0, 1) # [B,C,T,H,W] + + seed = [sa.seed if sa.seed is not None else random.randint(0, 10000) for sa in sample_args_list] + outputs: dict[str, Any] | None = None + + if outputs is None: + assert all(sa.num_outputs == 1 for sa in sample_args_list), "num_outputs must be 1" + n_sample = sum(cast(OmniSampleArgs, sa).num_outputs for sa in sample_args_list) + neg_key = "neg_" + self.model.input_caption_key + + with self._get_timer(f"{self.model.__class__.__name__}.generate_samples_from_batch"): + outputs = self.model.generate_samples_from_batch( + data_batch, + sampler=sampler, + guidance=guidance, + guidance_interval=_getattr(sample_args_list, "guidance_interval"), + seed=seed, + num_steps=_getattr(sample_args_list, "num_steps"), + shift=_getattr(sample_args_list, "shift"), + sigma_max=_getattr(sample_args_list, "sigma_max"), + has_negative_prompt=neg_key in data_batch, + n_sample=n_sample, + normalize_cfg=_getattr(sample_args_list, "normalize_cfg"), + ) + + with self._get_timer(f"{self.model.__class__.__name__}.decode"): + output_vision = outputs.pop("vision") + decoded_vision = [decode_vision(vision) for vision in output_vision] + outputs["vision"] = [cast(torch.Tensor, vision) for vision in decoded_vision] + if self.vae_decode_stream is not None: + # If we are using a separate GPU for VAE decoding, wait for results to be ready + torch.cuda.current_stream(device=outputs["vision"][0].device).wait_stream(self.vae_decode_stream) + for k, v in outputs.items(): + if len(v) != len(sample_args_list): + raise ValueError(f"Output key '{k}' has length {len(v)} but expected {len(sample_args_list)}") + + if "sound" in outputs: + with self._get_timer(f"{self.model.__class__.__name__}.decode_sound"): + outputs["sound"] = [self.model.decode_sound(sound) for sound in outputs.pop("sound")] + + if warmup: + return [] + + # Save outputs + sample_outputs: list[SampleOutputs] = [] + try: + with sync_distributed_errors(): + for sample_idx, sample_args in enumerate(sample_args_list): + if self.should_process_sample(sample_args): + assert isinstance(sample_args, OmniSampleArgs) + assert sample_args.output_dir is not None + assert sample_args.num_outputs == 1 + output = {k: v[sample_idx].squeeze(0) for k, v in outputs.items()} + vision_cthw = output.pop("vision") + + # Run guardrails + self._run_text_guardrail( + str(sample_args.output_dir), data_batch[self.model.input_caption_key][sample_idx] + ) + vision_cthw = self._run_video_guardrail(str(sample_args.output_dir), vision_cthw) + output["vision"] = vision_cthw + + content: dict[str, Any] = {} + files: list[Path] = [] + + # Save debug + if self.setup_args.debug: + files.extend( + self.save_data(output, output_dir=sample_args.output_dir, output_name="output") + ) + + # Save vision + if vision_cthw.shape[1] == 1: + quality = sample_args.image_save_quality + else: + quality = sample_args.video_save_quality + vision_file = sample_args.output_dir / f"vision{sample_args.vision_extension}" + output_fps = sample_args.fps + save_img_or_video( + vision_cthw, str(vision_file.with_suffix("")), fps=output_fps, quality=quality + ) + assert vision_file.is_file(), vision_file + files.append(vision_file) + + if "sound" in output: + from cosmos3.sound import ( + get_audio_tokenizer_info, + save_sound, + ) + + audio_info = get_audio_tokenizer_info(self.model) + decoded_audio = output["sound"] + sound_file = sample_args.output_dir / "sound.wav" + save_sound(decoded_audio, sound_file, audio_info.sample_rate) + files.append(sound_file) + + if "action" in output: + pred_action = output["action"] + if sample_args.raw_action_dim is not None: + raw_action_dim = int(sample_args.raw_action_dim) + + assert pred_action.shape[-1] >= raw_action_dim, ( + f"invalid raw_action_dim={raw_action_dim} for action with shape {pred_action.shape}" + ) + pred_action = pred_action[..., :raw_action_dim] + content["action"] = pred_action.detach().cpu().tolist() + + sample_output = SampleOutputs( + args=sample_args.model_dump(mode="json"), + outputs=[SampleOutput(content=content, files=files)], + ) + sample_outputs_file = sample_args.output_dir / "sample_outputs.json" + sample_outputs_file.write_text(sample_output.model_dump_json()) + log.success(f"Saved sample outputs to '{sample_outputs_file}'", rank0_only=False) + + sample_outputs.append(sample_output) + + except Exception as e: + return [ + self._handle_sample_exception(sample_args, e) + for sample_args in sample_args_list + if self.should_process_sample(sample_args) + ] + + return sample_outputs + + + @property + def replica_size(self) -> int: + """ + The ranks are divided into computation replicas. The replica size is + the product of the context parallelism and CFG parallelism sizes. + """ + if not hasattr(self.model, "parallel_dims") or self.model.parallel_dims is None: + return 1 + else: + return self.model.parallel_dims.cp_size * self.model.parallel_dims.cfgp_size + + @property + def num_replicas(self) -> int: + assert get_world_size() % self.replica_size == 0 + return get_world_size() // self.replica_size + + @property + def replica_id(self) -> int: + return get_rank() // self.replica_size + + @property + def index_in_replica(self) -> int: + return get_rank() % self.replica_size + + def should_process_sample(self, sample_args: SampleArgs) -> bool: + """Whether the sample should be processed by the current rank.""" + return sample_args.output_dir is not None and self.index_in_replica == 0 + + +_data_converter = cattrs.preconf.json.make_converter() + + +# torch.Tensor +@_data_converter.register_unstructure_hook +def _unstructure_torch_tensor(obj: torch.Tensor) -> Any: + return { + "shape": obj.shape, + "dtype": str(obj.dtype), + "device": str(obj.device), + "values": obj.detach().flatten()[:5].cpu().tolist(), + } diff --git a/cosmos3/model.py b/cosmos3/model.py new file mode 100644 index 00000000..1a662804 --- /dev/null +++ b/cosmos3/model.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +from pathlib import Path + +import attrs +import diffusers +import hydra +import omegaconf +import torch.distributed.checkpoint as dcp +import transformers +from torch.distributed.checkpoint.filesystem import FileSystemReader +from torch.distributed.checkpoint.hf_storage import HuggingFaceStorageReader +from torch.distributed.checkpoint.state_dict import get_model_state_dict +from typing_extensions import TYPE_CHECKING, assert_never + +from cosmos3.common.args import CheckpointType +from cosmos3.common.checkpoints import register_checkpoints +from cosmos3.common.config import structure_config, undo_config_dict_replacements, unstructure_config +from cosmos3._src.imaginaire.flags import SMOKE +from cosmos3._src.imaginaire.lazy_config.lazy_call import LazyCall +from cosmos3._src.imaginaire.utils import misc +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig + +if TYPE_CHECKING: + from cosmos3._src.vfm.models.mot.cosmos3_vfm_network import Cosmos3VFMNetwork + from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + from cosmos3._src.vfm.tokenizers.wan2pt2_vae_4x16x16 import WanVAE_ + + +_ROOT_DIR = Path(__file__).parents[1].absolute() + + +class Cosmos3OmniConfig(transformers.PretrainedConfig): + model_type = "cosmos3_omni" + + def __init__(self, model: dict | None = None, **kwargs): + if model is not None: + model = undo_config_dict_replacements(model) + self.model = model or {} + + super().__init__(**kwargs) + + self.auto_map = { + "AutoConfig": "cosmos3.model.Cosmos3OmniConfig", + "AutoModel": "cosmos3.model.Cosmos3OmniModel", + } + + @property + def parallelism(self) -> dict: + return self.model.get("config", {}).get("parallelism", {}) + + @parallelism.setter + def parallelism(self, value: dict | None): + if value is None: + return + self.model.setdefault("config", {})["parallelism"] = unstructure_config(LazyCall(ParallelismConfig)(**value)) + + +class Cosmos3OmniModel(transformers.PreTrainedModel): + config_class = Cosmos3OmniConfig # type: ignore + + def __init__(self, config: Cosmos3OmniConfig, *args, **kwargs): + super().__init__(config, *args, **kwargs) + + self.before_load_model() + model_dict: "OmniMoTModel" = structure_config(config.model, omegaconf.DictConfig) + + + if SMOKE: + vlm_dict = model_dict.config.vlm_config.model_instance + assert vlm_dict is not None + with omegaconf.open_dict(vlm_dict.config): + vlm_dict.config.num_hidden_layers = vlm_dict.config.num_window_layers = 2 + + # The model loads some files by relative path 'cosmos3/...' + with contextlib.chdir(_ROOT_DIR): + self.model: "OmniMoTModel" = hydra.utils.instantiate(model_dict) + self.after_load_model(self.model) + + @classmethod + def from_pretrained_dcp( + cls, + checkpoint_path: Path, + config: Cosmos3OmniConfig | None = None, + parallelism_config: ParallelismConfig | None = None, + ): + if parallelism_config is None: + parallelism_config = ParallelismConfig() + if config is None: + config = Cosmos3OmniConfig.from_pretrained(checkpoint_path, parallelism=attrs.asdict(parallelism_config)) + model = cls(config) + checkpoint_type = CheckpointType.from_path(checkpoint_path) + match checkpoint_type: + case CheckpointType.DCP: + state_dict = get_model_state_dict(model.model) + storage_reader = FileSystemReader(str(checkpoint_path)) + case CheckpointType.HF: + is_diffusers = next(checkpoint_path.glob("diffusion_pytorch_model.*"), None) is not None + if is_diffusers: + state_dict = get_model_state_dict(model.model) + else: + state_dict = get_model_state_dict(model) + storage_reader = HuggingFaceStorageReader(str(checkpoint_path)) + case _: + assert_never(checkpoint_type) + dcp.load(state_dict=state_dict, storage_reader=storage_reader) + return model + + @classmethod + def before_load_model(cls): + # Disable duck shapes, which triggers recompile. + misc.set_torch_compile_options(use_duck_shape=False) + + register_checkpoints() + + @classmethod + def after_load_model(cls, model: "OmniMoTModel"): + pass + + +class Cosmos3OmniTransformer(diffusers.pipelines.pipeline_utils.ModelMixin, diffusers.ConfigMixin): + @diffusers.configuration_utils.register_to_config + def __init__(self, model: dict): + super().__init__() + + self.net: "Cosmos3VFMNetwork | None" = None + + def forward(self, *args, **kwargs): + pass + + +class Cosmos3OmniVisionTokenizer(diffusers.pipelines.pipeline_utils.ModelMixin, diffusers.ConfigMixin): + @diffusers.configuration_utils.register_to_config + def __init__(self, model: dict): + super().__init__() + + self.model: "WanVAE_ | None" = None + + def forward(self, *args, **kwargs): + pass + + +class Cosmos3OmniDiffusersPipeline(diffusers.pipelines.pipeline_utils.DiffusionPipeline): + def __init__( + self, + transformer: Cosmos3OmniTransformer, + text_tokenizer: transformers.PreTrainedTokenizerBase, + vision_tokenizer: Cosmos3OmniVisionTokenizer, + ): + super().__init__() + self.register_modules(transformer=transformer, text_tokenizer=text_tokenizer, vision_tokenizer=vision_tokenizer) + + def __call__(self): + pass diff --git a/cosmos3/model_test.py b/cosmos3/model_test.py new file mode 100644 index 00000000..3e51bbfb --- /dev/null +++ b/cosmos3/model_test.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import attrs +import hydra + +from cosmos3.args import DEFAULT_CHECKPOINT +from cosmos3.common.config import structure_config +from cosmos3.model import Cosmos3OmniConfig +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig + + +def test_config(): + parallelism = ParallelismConfig( + data_parallel_shard_degree=2, + context_parallel_shard_degree=2, + cfg_parallel_shard_degree=2, + use_torch_compile=True, + use_cuda_graphs=True, + ) + parallelism_kwargs = attrs.asdict(parallelism) + checkpoint_path = DEFAULT_CHECKPOINT.download() + config = Cosmos3OmniConfig.from_pretrained(checkpoint_path, parallelism=parallelism_kwargs) + assert ( + hydra.utils.instantiate(structure_config(config.model["config"]["parallelism"], ParallelismConfig)) + == parallelism + ) diff --git a/cosmos3/ray/__init__.py b/cosmos3/ray/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/ray/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/ray/configs/latency.yaml b/cosmos3/ray/configs/latency.yaml new file mode 100644 index 00000000..571326ab --- /dev/null +++ b/cosmos3/ray/configs/latency.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +proxy_location: EveryNode +applications: + - name: cosmos3_omni + route_prefix: / + import_path: cosmos3.ray.serve:router_app_builder + args: + app: + output_dir: "outputs/ray_serve" + models: + Cosmos3-Nano: + setup: + checkpoint_path: "Cosmos3-Nano" + guardrails: False + parallelism_preset: latency diff --git a/cosmos3/ray/configs/throughput.yaml b/cosmos3/ray/configs/throughput.yaml new file mode 100644 index 00000000..f1825f71 --- /dev/null +++ b/cosmos3/ray/configs/throughput.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +proxy_location: EveryNode +applications: + - name: cosmos3_omni + route_prefix: / + import_path: cosmos3.ray.serve:router_app_builder + args: + app: + output_dir: "outputs/ray_serve" + models: + Cosmos3-Nano: + setup: + checkpoint_path: "Cosmos3-Nano" + guardrails: False + parallelism_preset: throughput diff --git a/cosmos3/ray/gradio.py b/cosmos3/ray/gradio.py new file mode 100644 index 00000000..b65a8b39 --- /dev/null +++ b/cosmos3/ray/gradio.py @@ -0,0 +1,274 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import time +import typing +from functools import partial +from pathlib import Path +from typing import Any, Literal, Union, cast + +import gradio as gr +import httpx +import pydantic + +from cosmos3.args import OmniSampleOverrides +from cosmos3.common.args import ( + MEDIA_EXTENSIONS, + ResolvedPath, + SampleOutputs, + tyro_cli, +) + +INPUTS_DIR = Path(__file__).parents[2] / "inputs" + + +class Args(pydantic.BaseModel): + host: str = "localhost" + """The hostname to bind the Gradio server to.""" + port: int = 8080 + """The port to bind the Gradio server to. If None, a random port will be used.""" + server_host: str = "localhost" + """The hostname of the Ray Serve endpoint.""" + server_port: int = 8000 + """The port of the Ray Serve endpoint.""" + server_output_dir: ResolvedPath = Path("outputs/ray_serve") + """Server output directory.""" + + timeout: int = 600 + """The timeout for the request in seconds.""" + + @property + def server(self) -> str: + return f"http://{self.server_host}:{self.server_port}" + + +def get_info(args: Args) -> dict[str, Any]: + response = httpx.get(f"{args.server}/info", timeout=args.timeout) + response.raise_for_status() + return response.json() + + +def build_components( + model: type[pydantic.BaseModel], include: dict[str, dict[str, Any] | None] +) -> dict[str, gr.components.Component]: + components: dict[str, gr.components.Component] = {} + for name, kwargs in include.items(): + field = model.model_fields[name] + + default_val = field.get_default(call_default_factory=True) + + min_val, max_val = None, None + for meta in field.metadata: + if hasattr(meta, "ge"): + min_val = meta.ge + if hasattr(meta, "gt"): + min_val = meta.gt + 1 + if hasattr(meta, "le"): + max_val = meta.le + if hasattr(meta, "lt"): + max_val = meta.lt - 1 + + kwargs = dict( + value=default_val, + label=name.replace("_", " ").title(), + ) | (kwargs or {}) + kwargs = cast(dict[str, Any], kwargs) + + annotation = field.annotation + is_optional = False + if typing.get_origin(annotation) is Union: + args = typing.get_args(annotation) + is_optional = type(None) in args + args = [arg for arg in args if arg is not type(None)] + if len(args) == 1: + annotation = args[0] + if typing.get_origin(annotation) is Literal: + choices = list(typing.get_args(annotation)) + if is_optional: + choices.insert(0, None) + kwargs.setdefault("choices", choices) + components[name] = gr.Dropdown(**kwargs) + elif annotation in (int, float): + if min_val is not None: + kwargs.setdefault("minimum", min_val) + if max_val is not None: + kwargs.setdefault("maximum", max_val) + if "minimum" in kwargs and "maximum" in kwargs: + components[name] = gr.Slider(**kwargs) + else: + components[name] = gr.Number(**kwargs) + elif annotation == bool: + components[name] = gr.Checkbox(**kwargs) + else: + components[name] = gr.Textbox(**kwargs) + return components + + +COMPONENTS: dict[str, dict[str, Any] | None] = { + "prompt": dict( + lines=3, + ), + "resolution": None, + "aspect_ratio": None, + "fps": None, + "num_frames": None, + "seed": None, +} +EXCLUDE_FIELDS = [ + "model", + "name", + "output_dir", + "prompt_path", + "tensors_file", + "pickle_file", +] + + +def _load_sample_outputs(args: Args, sample_outputs: SampleOutputs): + if sample_outputs.status == "success": + media_output = [ + (f"{args.server_output_dir}/{p}", p.name) + for p in sample_outputs.outputs[0].files + if p.suffix.lower() in MEDIA_EXTENSIONS + ] + else: + media_output = [] + return media_output + + +async def generate(*inputs, args: Args): + try: + model, *values, extra_json = inputs + request_dict = {k: v for k, v in zip(COMPONENTS, values, strict=True) if v} + name = str(round(time.time())) + request_dict = request_dict | json.loads(extra_json) | {"model": model, "name": name} + sample_args = OmniSampleOverrides.model_validate(request_dict) + + async with httpx.AsyncClient(timeout=args.timeout) as http_client: + response = await http_client.post( + f"{args.server}/generate", + json=sample_args.model_dump(mode="json"), + ) + response.raise_for_status() + + response_dict = response.json() + sample_outputs = SampleOutputs.model_validate(response_dict) + media_output = _load_sample_outputs(args, sample_outputs) + return media_output, request_dict, response_dict + except Exception as e: + raise gr.Error(f"Error generating: {str(e)}") + + +def load_input(input_name: str, examples: dict[str, Path]): + if input_name: + input_file = examples[input_name] + sample_args_list = OmniSampleOverrides.from_files([input_file]) + assert len(sample_args_list) == 1 + sample_args = sample_args_list[0] + if sample_args.prompt_path: + sample_args.prompt = Path(sample_args.prompt_path).read_text().strip() + else: + sample_args = OmniSampleOverrides(name="") + + updates = [] + updates.append(sample_args.model or gr.update()) + for comp_name in COMPONENTS: + updates.append(getattr(sample_args, comp_name)) + extra_data = sample_args.model_dump_json(indent=2, exclude={*COMPONENTS, *EXCLUDE_FIELDS}) + updates.append(extra_data) + return tuple(updates) + + +def ui_builder(args: Args) -> gr.Blocks: + info = get_info(args) + available_models = info["models"] + if len(available_models) == 0: + raise ValueError("No models available") + + default_example = "t2i" + examples: dict[str, Path] = {} + for p in INPUTS_DIR.rglob("*.json"): + if "internal" in p.parts: + continue + if p.stem in examples: + raise ValueError(f"Duplicate example file: {p}") + examples[p.stem] = p + assert default_example in examples + + with gr.Blocks(title="Cosmos3 Omni Generator") as ui: + gr.Markdown("# Cosmos3 Omni Generator") + with gr.Accordion("Environment", open=False): + gr.JSON(value=info["environment"]) + + example_dropdown = gr.Dropdown( + choices=["", *sorted(examples.keys())], + value=default_example, + label="Input", + ) + + with gr.Row(): + with gr.Column(scale=1): + model_input = gr.Dropdown(value=available_models[0], choices=available_models, label="Model") + + generate_btn = gr.Button("Generate", variant="primary") + + components = build_components(OmniSampleOverrides, COMPONENTS) + + with gr.Accordion("Extra Arguments", open=False): + extra_json = OmniSampleOverrides(name="").model_dump_json( + indent=2, + exclude={*COMPONENTS, *EXCLUDE_FIELDS}, + ) + extra_input = gr.Code( + extra_json, + language="json", + lines=10, + ) + + with gr.Column(scale=1): + media_output = gr.Gallery(label="Media", allow_preview=True) + + with gr.Accordion("Request", open=False): + request_output = gr.JSON() + + with gr.Accordion("Response", open=False): + response_output = gr.JSON() + + load_input_kwargs = dict( + fn=partial(load_input, examples=examples), + inputs=[example_dropdown], + outputs=[model_input, *components.values(), extra_input], + ) + example_dropdown.change(**load_input_kwargs) + ui.load(**load_input_kwargs) + + generate_btn.click( + fn=partial(generate, args=args), + inputs=[model_input, *components.values(), extra_input], + outputs=[media_output, request_output, response_output], + ) + + return ui + + +def main(): + args = tyro_cli(Args, description=__doc__) + ui = ui_builder(args) + ui.launch(server_name=args.host, server_port=args.port, allowed_paths=[str(args.server_output_dir)]) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/ray/serve.py b/cosmos3/ray/serve.py new file mode 100644 index 00000000..eaa97cc3 --- /dev/null +++ b/cosmos3/ray/serve.py @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import importlib.metadata +import os +import random +import socket +import subprocess +import uuid +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Annotated, Any, Sequence, cast + +import fastapi +import pydantic +import ray +import ray.serve +import ray.serve.handle +import torch +import tyro +from fastapi.staticfiles import StaticFiles +from ray.serve.config import AutoscalingConfig + +from cosmos3.args import OmniSampleOverrides, OmniSetupArgs, OmniSetupOverrides +from cosmos3.common.args import ResolvedPath, SampleOutputs, tyro_cli +from cosmos3._src.imaginaire.utils import log + +if TYPE_CHECKING: + from ray.actor import ActorClass + from ray.serve.batching import _LazyBatchQueueWrapper + +torch.set_grad_enabled(False) + +fastapi_app = fastapi.FastAPI() + +DEFAULT_MAX_BATCH_SIZE = 1 +DEFAULT_BATCH_WAIT_TIMEOUT_S = 0.01 +DEFAULT_AUTOSCALING_CONFIG = AutoscalingConfig() + + +def _get_environment_info() -> dict[str, Any]: + try: + commit_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + except Exception: + commit_sha = None + return { + "cosmos3_version": importlib.metadata.version("cosmos3"), + "commit_sha": commit_sha, + } + + +class OmniAppArgs(pydantic.BaseModel): + output_dir: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + + +class OmniModelDeploymentUserConfig(pydantic.BaseModel): + max_batch_size: int = DEFAULT_MAX_BATCH_SIZE + batch_wait_timeout_s: float = DEFAULT_BATCH_WAIT_TIMEOUT_S + + +class OmniModelDeploymentArgs(pydantic.BaseModel): + setup: OmniSetupOverrides + world_size: int | None = None + + user_config: OmniModelDeploymentUserConfig = pydantic.Field(default_factory=OmniModelDeploymentUserConfig) + autoscaling_config: dict[str, Any] = pydantic.Field(default_factory=lambda: DEFAULT_AUTOSCALING_CONFIG.dict()) + + +class OmniRouterDeploymentArgs(pydantic.BaseModel): + app: OmniAppArgs + models: dict[str, OmniModelDeploymentArgs] = pydantic.Field(min_length=1) + + +@ray.remote(num_gpus=1) +class OmniModelWorker: + def __init__(self, setup_args: OmniSetupArgs, *, rank: int, world_size: int): + self.setup_args = setup_args + + # Ray sets CUDA_VISIBLE_DEVICES, so LOCAL_WORLD_SIZE is always 1. + local_rank = 0 + local_world_size = 1 + + os.environ["RANK"] = str(rank) + os.environ["WORLD_SIZE"] = str(world_size) + os.environ["LOCAL_RANK"] = str(local_rank) + os.environ["LOCAL_WORLD_SIZE"] = str(local_world_size) + + def get_address_and_port(self) -> tuple[str, int]: + ip = ray.util.get_node_ip_address() + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + port = s.getsockname()[1] + return ip, port + + def setup_distributed(self, master_addr: str, master_port: int) -> bool: + from cosmos3.inference import OmniInference + from cosmos3._src.imaginaire.utils import distributed + + os.environ["MASTER_ADDR"] = master_addr + os.environ["MASTER_PORT"] = str(master_port) + distributed.init() + + self.pipe = OmniInference.create(self.setup_args) + return True + + def generate_batch(self, sample_overrides_list: Sequence[OmniSampleOverrides]) -> list[SampleOutputs]: + # Create batches + sample_args_list = [args.build_sample(model_config=self.pipe.model.config) for args in sample_overrides_list] + batches = self.pipe.create_batches(sample_args_list) + + # Generate batches + sample_outputs_list: list[SampleOutputs] = [] + for sample_args_list, data_batch in batches: + sample_outputs_list.extend(self.pipe.generate_batch(sample_args_list, data_batch)) + return sample_outputs_list + + +@ray.serve.deployment( + health_check_timeout_s=5 * 60, # Model loading can take a while + autoscaling_config=DEFAULT_AUTOSCALING_CONFIG, +) +class OmniModelDeployment: + def __init__(self, setup_args: OmniSetupArgs): + self.setup_args = setup_args + world_size = setup_args.world_size + + self.pg = ray.util.placement_group([{"GPU": 1, "CPU": 1} for _ in range(world_size)]) + ray.get(self.pg.ready()) + + # Spawn the workers + self.workers = [ + cast("ActorClass", OmniModelWorker) + .options(placement_group=self.pg) + .remote( + rank=i, + world_size=world_size, + setup_args=setup_args, + ) + for i in range(world_size) + ] + + master_addr, master_port = ray.get(self.workers[0].get_address_and_port.remote()) + ray.get([w.setup_distributed.remote(master_addr, master_port) for w in self.workers]) + + def reconfigure(self, user_config_dict: dict[str, Any]): + user_config = OmniModelDeploymentUserConfig.model_validate(user_config_dict) + + _generate_batch = cast("_LazyBatchQueueWrapper", self._generate_batch) + _generate_batch.set_max_batch_size(user_config.max_batch_size) + _generate_batch.set_batch_wait_timeout_s(user_config.batch_wait_timeout_s) + + async def generate(self, sample_overrides: OmniSampleOverrides) -> SampleOutputs: + result = await self._generate_batch(sample_overrides) + return result + + @ray.serve.batch( + max_batch_size=DEFAULT_MAX_BATCH_SIZE, + batch_wait_timeout_s=DEFAULT_BATCH_WAIT_TIMEOUT_S, + ) + async def _generate_batch(self, sample_overrides_list: list[OmniSampleOverrides]) -> list[SampleOutputs]: + tasks = [worker.generate_batch.remote(sample_overrides_list) for worker in self.workers] + # Wait for all workers to complete. + results = await asyncio.wait_for( + asyncio.gather(*tasks), # type: ignore + timeout=300.0, + ) + + sample_outputs_list = cast(list[SampleOutputs], results[0]) + sample_outputs_list = [ + obj.map_files(lambda p: p.relative_to(self.setup_args.output_dir)) for obj in sample_outputs_list + ] + return sample_outputs_list + + +@ray.serve.deployment(num_replicas=1) +@ray.serve.ingress(fastapi_app) +class OmniRouterDeployment: + def __init__(self, models: dict[str, ray.serve.handle.DeploymentHandle], output_dir: Path): + output_dir.mkdir(parents=True, exist_ok=True) + self.output_dir = output_dir + self.models = models + fastapi_app.mount("/outputs", StaticFiles(directory=output_dir), name="outputs") + + @fastapi_app.get("/models") + async def list_models(self) -> list[str]: + return list(self.models.keys()) + + @fastapi_app.get("/info") + async def info(self) -> dict[str, Any]: + return { + "environment": _get_environment_info(), + "models": list(self.models.keys()), + "output_dir": str(self.output_dir), + } + + @fastapi_app.post("/generate") + async def generate(self, sample_args: OmniSampleOverrides) -> SampleOutputs: + if sample_args.model: + if sample_args.model not in self.models: + raise fastapi.HTTPException( + status_code=404, + detail=f"Model '{sample_args.model}' not found. Available models: {list(self.models.keys())}", + ) + handle = self.models[sample_args.model] + else: + handle = next(iter(self.models.values())) + sample_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + sample_uuid = uuid.uuid4().hex + sample_args.output_dir = self.output_dir / f"generate_{sample_timestamp}_{sample_uuid[:4]}" + if sample_args.seed is None: + sample_args.seed = random.randint(0, 10000) + sample_args.download(sample_args.output_dir / "inputs") + + result: SampleOutputs = await handle.generate.remote(sample_args) + return result + + +def router_app_builder(router_args_dict: dict) -> ray.serve.Application: + router_args = OmniRouterDeploymentArgs.model_validate(router_args_dict) + + # Build models + models = {} + for model_name, model_args in router_args.models.items(): + model_args.setup.output_dir = router_args.app.output_dir + setup_args = model_args.setup.build_setup( + world_size=model_args.world_size, + ) + log.info(f"{model_name}: {setup_args.__class__.__name__}({setup_args})") + models[model_name] = ( + cast(ray.serve.Deployment, OmniModelDeployment) + .options( + name=model_name, + autoscaling_config=model_args.autoscaling_config, + user_config=model_args.user_config.model_dump(), + ) + .bind(setup_args) + ) + return cast(ray.serve.Deployment, OmniRouterDeployment).bind( + models, + output_dir=router_args.app.output_dir, + ) + + +def main(): + model_args = tyro_cli(OmniModelDeploymentArgs, description=__doc__, config=(tyro.conf.OmitArgPrefixes,)) + if model_args.setup.output_dir is None: + raise ValueError("An output directory is required. Set it with -o or --output-dir .") + + ray.init() # type: ignore + if model_args.world_size is None: + available_resources = ray.available_resources() # type: ignore + available_gpus = int(available_resources.get("GPU", 0)) + model_args.world_size = max(1, available_gpus) + + app_args = OmniAppArgs( + output_dir=model_args.setup.output_dir, + ) + router_args = OmniRouterDeploymentArgs(app=app_args, models={"": model_args}) + router_app = router_app_builder(router_args.model_dump()) + ray.serve.run(router_app, name="cosmos3_omni", blocking=True) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/ray/serve_test.py b/cosmos3/ray/serve_test.py new file mode 100644 index 00000000..24c95d6e --- /dev/null +++ b/cosmos3/ray/serve_test.py @@ -0,0 +1,157 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path +from typing import cast +from unittest.mock import AsyncMock + +import fastapi +import pytest +import ray +import ray.serve +import requests +import yaml +from starlette.testclient import TestClient + +from cosmos3.args import OmniSampleOverrides +from cosmos3.common.args import SampleOutput, SampleOutputs +from cosmos3.common.init import get_free_port +from cosmos3.ray.serve import OmniRouterDeployment, OmniRouterDeploymentArgs +from cosmos3.ray.submit import handle_response + +_CONFIGS_DIR = Path(__file__).parent / "configs" + + +@pytest.mark.parametrize("config_path", sorted(_CONFIGS_DIR.glob("*.yaml")), ids=lambda p: p.stem) +def test_config(monkeypatch: pytest.MonkeyPatch, config_path: Path): + monkeypatch.delenv("WORLD_SIZE", raising=False) + config = yaml.safe_load(config_path.read_text()) + args = OmniRouterDeploymentArgs.model_validate(config["applications"][0]["args"]) + assert "Cosmos3-Nano" in args.models + + +def _mock_response(status_code: int, json_body: dict | None = None) -> requests.Response: + resp = requests.Response() + resp.status_code = status_code + if json_body is not None: + resp._content = json.dumps(json_body).encode() + return resp + + +def test_handle_response_ok(): + handle_response(_mock_response(200)) + + +def test_handle_response_error_with_detail(): + with pytest.raises(ValueError, match="Something went wrong"): + handle_response(_mock_response(400, {"detail": "Something went wrong"})) + + +def test_handle_response_error_without_detail(): + with pytest.raises(requests.exceptions.HTTPError): + handle_response(_mock_response(500)) + + +def _make_generate_app(mock_handle: AsyncMock) -> fastapi.FastAPI: + app = fastapi.FastAPI() + + @app.post("/generate") + async def generate(sample_overrides: OmniSampleOverrides) -> SampleOutputs: + return await mock_handle.generate.remote(sample_overrides) + + return app + + +def test_generate_e2e(): + mock_handle = AsyncMock() + sample_kwargs = {"name": "test", "prompt": "a city"} + mock_handle.generate.remote.return_value = SampleOutputs( + args=sample_kwargs, status="success", outputs=[SampleOutput(content={}, files=[])] + ) + client = TestClient(_make_generate_app(mock_handle)) + response = client.post("/generate", json=sample_kwargs) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["args"] == sample_kwargs + mock_handle.generate.remote.assert_called_once() + + +def test_generate_error_response(): + mock_handle = AsyncMock() + sample_kwargs = {"name": "test", "prompt": "a city"} + mock_handle.generate.remote.return_value = SampleOutputs( + args=sample_kwargs, status="error", message="Out of memory", stack_trace="Traceback ..." + ) + client = TestClient(_make_generate_app(mock_handle)) + response = client.post("/generate", json=sample_kwargs) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + assert data["message"] == "Out of memory" + assert data["stack_trace"] is not None + + +@ray.serve.deployment +class _FakeModelDeployment: + async def generate(self, sample_overrides: OmniSampleOverrides) -> SampleOutputs: + return SampleOutputs( + args=sample_overrides.model_dump(mode="json"), + status="success", + outputs=[SampleOutput(content={}, files=[])], + ) + + + +@pytest.mark.manual +@pytest.mark.level(1) +@pytest.mark.gpus(0) +def test_ray_serve_integration(tmp_path: Path): + ray.init(num_cpus=2, num_gpus=0) # type: ignore + try: + port = get_free_port() + ray.serve.start(http_options={"host": "127.0.0.1", "port": port}) + + app = cast(ray.serve.Deployment, OmniRouterDeployment).bind( + {"test-model": cast(ray.serve.Deployment, _FakeModelDeployment).bind()}, + output_dir=tmp_path, + ) + ray.serve.run(app, name="test") + base_url = f"http://127.0.0.1:{port}" + + response = requests.get(f"{base_url}/models") + assert response.status_code == 200 + assert "test-model" in response.json() + + sample_kwargs = {"name": "test", "prompt": "A city at night"} + response = requests.post( + f"{base_url}/generate", + json=sample_kwargs, + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + for k, v in sample_kwargs.items(): + assert data["args"][k] == v + + response = requests.post( + f"{base_url}/generate", + json={"name": "test", "prompt": "A city", "model": "nonexistent"}, + ) + assert response.status_code == 404 + finally: + ray.serve.shutdown() + ray.shutdown() # type: ignore diff --git a/cosmos3/ray/submit.py b/cosmos3/ray/submit.py new file mode 100644 index 00000000..51cb4fde --- /dev/null +++ b/cosmos3/ray/submit.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Annotated + +import pydantic +import requests +import tyro + +from cosmos3.args import OmniSampleOverrides +from cosmos3.common.args import ResolvedPath, SampleOutputs, tyro_cli +from cosmos3._src.imaginaire.utils import log + + +class Args(pydantic.BaseModel): + output_dir: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + """Output directory.""" + input_files: Annotated[list[ResolvedPath], tyro.conf.arg(aliases=("-i",))] + """Path to the inference parameter file(s). + + If multiple files are provided, the model will be loaded once and all the samples will be run sequentially. + + Accepts glob patterns (e.g. `inputs/*.json`). + """ + overrides: OmniSampleOverrides = pydantic.Field(default_factory=OmniSampleOverrides) + """Overrides for the inference parameters.""" + + host: str = "localhost" + """The hostname of the Ray Serve endpoint.""" + port: int = 8000 + """The port of the Ray Serve endpoint.""" + + timeout: int = 600 + """The timeout for the request in seconds.""" + + @property + def server(self) -> str: + return f"http://{self.host}:{self.port}" + + +def handle_response(response: requests.Response) -> None: + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + try: + detail = response.json().get("detail") + except Exception: + detail = None + if detail: + raise ValueError(f"{detail}") from e + raise e + + +def submit(args: Args): + args.output_dir.mkdir(parents=True, exist_ok=True) + sample_args_list = OmniSampleOverrides.from_files(args.input_files, overrides=args.overrides) + + + for i_sample, sample_args in enumerate(sample_args_list): + assert sample_args.name + log.info(f"[{i_sample + 1}/{len(sample_args_list)}] Submitting sample '{sample_args.name}'") + log.debug(f"{sample_args.__class__.__name__}({sample_args})") + + sample_dir = args.output_dir / sample_args.name + sample_dir.mkdir(parents=True, exist_ok=True) + sample_args_file = sample_dir / "sample_args.json" + sample_args_file.write_text(sample_args.model_dump_json()) + log.info(f"Saved sample args to '{sample_args_file}'") + + response = requests.post( + f"{args.server}/generate", json=sample_args.model_dump(mode="json"), timeout=args.timeout + ) + handle_response(response) + response_dict = response.json() + sample_outputs = SampleOutputs.model_validate(response_dict) + + # Download files + def download_file(remote_file: Path) -> Path: + response = requests.get(f"{args.server}/outputs/{remote_file}", timeout=args.timeout) + handle_response(response) + local_file = sample_dir.joinpath(*remote_file.parts[1:]) + local_file.write_bytes(response.content) + return local_file + + sample_outputs = sample_outputs.map_files(download_file) + + sample_outputs_file = sample_dir / "sample_outputs.json" + sample_outputs_file.write_text(sample_outputs.model_dump_json()) + log.info(f"Saved sample outputs to '{sample_outputs_file}'") + + +def main(): + args = tyro_cli(Args, description=__doc__, config=(tyro.conf.OmitArgPrefixes,)) + submit(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/__init__.py b/cosmos3/scripts/__init__.py new file mode 100644 index 00000000..db6d6eb6 --- /dev/null +++ b/cosmos3/scripts/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/cosmos3/scripts/_test.py b/cosmos3/scripts/_test.py new file mode 100644 index 00000000..2b241f64 --- /dev/null +++ b/cosmos3/scripts/_test.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +from cosmos3.fixtures.script import ScriptConfig, script_test + +_CURRENT_DIR = Path(__file__).parent.absolute() +_TEST_DIR = _CURRENT_DIR / "_test" + +_script_configs: list[ScriptConfig] = [ + ScriptConfig( + script=_TEST_DIR / "convert_model_to_dcp.sh", + training=True, + ), +] + + +@script_test(_script_configs) +class TestScript: ... diff --git a/cosmos3/scripts/_test/convert_model_to_dcp.sh b/cosmos3/scripts/_test/convert_model_to_dcp.sh new file mode 100644 index 00000000..01646cdd --- /dev/null +++ b/cosmos3/scripts/_test/convert_model_to_dcp.sh @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Cosmos3-Test +subdirectory="5d561d7d-080f-45cb-a455-920d444e40cc" +CHECKPOINT_PATH=$(uvx hf@$HF_VERSION download \ + --repo-type model nvidia/Cosmos3-Experimental \ + --revision 844eb561ec6a8d6a917aec463464cdd594d5e965 \ + --include "$subdirectory/*" \ + --quiet)/$subdirectory + +# HF -> DCP +# Use temporary directory, since output is large. +CUDA_VISIBLE_DEVICES= python -m cosmos3.scripts.convert_model_to_dcp \ + -o $TMP_DIR/checkpoint \ + --checkpoint-path $CHECKPOINT_PATH + +# DCP -> HF +# Use temporary directory, since output is large. +CUDA_VISIBLE_DEVICES= python -m cosmos3.scripts.export_model \ + -o $TMP_DIR/model \ + --checkpoint-path $TMP_DIR/checkpoint/model \ + --config-file $TMP_DIR/checkpoint/model/config.json \ + --no-use-ema-weights + +# HF Inference +torchrun $TORCHRUN_ARGS -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/omni/t2i.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path $TMP_DIR/model \ + $INFERENCE_ARGS diff --git a/cosmos3/scripts/_test/convert_model_to_diffusers.sh b/cosmos3/scripts/_test/convert_model_to_diffusers.sh new file mode 100644 index 00000000..329c782b --- /dev/null +++ b/cosmos3/scripts/_test/convert_model_to_diffusers.sh @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Cosmos3-Test +subdirectory="5d561d7d-080f-45cb-a455-920d444e40cc" +CHECKPOINT_PATH=$(uvx hf@$HF_VERSION download \ + --repo-type model nvidia/Cosmos3-Experimental \ + --revision 844eb561ec6a8d6a917aec463464cdd594d5e965 \ + --include "$subdirectory/*" \ + --quiet)/$subdirectory + +# HF -> Diffusers +# Use temporary directory, since output is large. +CUDA_VISIBLE_DEVICES= python -m cosmos3.scripts.convert_model_to_diffusers \ + -o $TMP_DIR/model \ + --checkpoint-path $CHECKPOINT_PATH + +# Inference +python -m cosmos3.scripts.inference \ + -i "$INPUT_DIR/omni/t2i.json" \ + -o $OUTPUT_DIR/inference \ + --checkpoint-path $TMP_DIR/model/transformer \ + $INFERENCE_ARGS diff --git a/cosmos3/scripts/_test/export_config.sh b/cosmos3/scripts/_test/export_config.sh new file mode 100644 index 00000000..fa0499c0 --- /dev/null +++ b/cosmos3/scripts/_test/export_config.sh @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CUDA_VISIBLE_DEVICES= python -m cosmos3.scripts.export_config \ + -o $OUTPUT_DIR/config.yaml \ + --experiment t2w_mot_dryrun_exp100_006_qwen3_0p6b_256res_resume_from_t2i \ + --config_file cosmos3/_src/vfm/configs/base/config.py diff --git a/cosmos3/scripts/_test/export_model.sh b/cosmos3/scripts/_test/export_model.sh new file mode 100644 index 00000000..f8080317 --- /dev/null +++ b/cosmos3/scripts/_test/export_model.sh @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +subdirectory="Cosmos3-Test-DCP/cosmos3_vfm/t2w_mot_0p6b_qwen3_vl_runs/t2w_mot_dryrun_exp200_001_qwen3_vl_0p6b_480res_qwen3_captions_mrope_v2/checkpoints/iter_000079500/model" +CHECKPOINT_PATH=$(uvx hf@$HF_VERSION download \ + --repo-type model nvidia/Cosmos-Experimental \ + --revision cb49fa71add22a060936f2391d1eb4e58a49358a \ + --include "$subdirectory/*" \ + --quiet)/$subdirectory + +# Use temporary directory, since output is large. +CUDA_VISIBLE_DEVICES= python -m cosmos3.scripts.export_model \ + -o $TMP_DIR/model \ + --checkpoint-path $CHECKPOINT_PATH diff --git a/cosmos3/scripts/action_policy_server.py b/cosmos3/scripts/action_policy_server.py new file mode 100644 index 00000000..25426726 --- /dev/null +++ b/cosmos3/scripts/action_policy_server.py @@ -0,0 +1,1324 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""HTTP inference server for Action policy mode using OmniMoTModel. + +The server exposes two endpoints: + +- POST /predict: run policy inference. + - Input: {"image": "", "prompt": "", "domain_name": "", "image_size": } + - Output: {"action": [[a0, a1, ...], ...], "video": ["", ...]} +- GET /info: model / runtime info (run_name, checkpoint, sampling params, ...). + +Model loading uses ``cosmos3.inference.OmniInference.create`` which auto-dispatches +between two checkpoint flows based on ``--config-file``'s extension: + +* ``.py`` — in-tree experiment registry (e.g. + ``projects/cosmos3/vfm/configs/base/config.py``) plus an ``--experiment-name``. +* ``.yaml`` / ``.yml`` / ``.json`` — frozen Hydra config produced by + ``cosmos3.scripts.train`` next to the DCP checkpoint. + +Example (in-tree experiment): + + PYTHONPATH=. python -m cosmos3.scripts.action_policy_server \ + --experiment-name libero_exp_policy \ + --config-file cosmos3/_src/vfm/configs/base/config.py \ + --checkpoint-dir /path/to/checkpoints/iter_000020000 \ + --port 8000 + +Example (frozen Hydra YAML produced by cosmos3.scripts.train): + + PYTHONPATH=. python -m cosmos3.scripts.action_policy_server \ + --config-file /path/to/job/config.yaml \ + --checkpoint-dir /path/to/job/checkpoints/iter_000020000 \ + --port 8000 +""" + +from cosmos3.common.init import init_script, is_rank0 # noqa: F401 (is_rank0 may be useful for logging) + +init_script() + +import base64 +import binascii +import datetime +import io +import json +import os +import socket +import threading +import time +import traceback +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any, Literal + +import numpy as np +import pydantic +import torch +import tyro +from omegaconf import DictConfig +from PIL import Image +from torch import distributed as dist + +from cosmos3.args import OmniSetupArgs, OmniSetupOverrides +from cosmos3.common.args import ConfigFileType, tyro_cli +from cosmos3.common.config import deserialize_config_dict +from cosmos3.common.init import init_output_dir +from cosmos3.inference import OmniInference +from cosmos3._src.imaginaire.lazy_config import instantiate +from cosmos3._src.imaginaire.utils import log + +# Action-specific helpers live in the in-tree project tree. Imports stay as +# `projects.cosmos3.vfm.*` and are auto-rewritten to `cosmos3._src.vfm.*` by the +# Cosmos release script. +from cosmos3._src.vfm.datasets.action.domain_utils import get_domain_id +from cosmos3._src.vfm.datasets.action.transforms import ( + build_sequence_plan_from_mode, + find_closest_target_size, + reflection_pad_to_target, + remove_reflection_padding, +) +from cosmos3._src.vfm.utils.data_utils import get_vision_data_resolution + +_DEFAULT_ACTION_CHUNK_SIZE = 16 +_DEFAULT_FALLBACK_OUTPUT_DIR = Path("/tmp/cosmos3_action_server") +ActionNormalization = Literal["auto", "meanstd", "minmax", "quantile", "quantile_rot"] +ResolvedActionNormalization = Literal["meanstd", "minmax", "quantile", "quantile_rot"] + +_DURATION_FPS_TEMPLATE = "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS." +_RESOLUTION_TEMPLATE = "This video is of {height}x{width} resolution." + + +# --------------------------------------------------------------------------- +# Pre/post processing helpers (copied verbatim from the previous server, with +# minor adjustments so config-introspection helpers also accept plain dicts). +# --------------------------------------------------------------------------- + + +def _augment_prompt_with_metadata( + prompt: str, + *, + t_frames: int, + fps: int, + height: int, + width: int, + append_duration_fps: bool = True, + append_resolution_info: bool = True, +) -> str: + """Append duration/FPS and resolution metadata to match training-time augmentation. + + Mirrors ``DurationFPSTextTimeStamps`` and ``ResolutionTextInfo`` augmentors + from the Action training transform pipeline. Only appends each piece when the + corresponding flag is ``True`` (matching the training config). + """ + if append_duration_fps: + duration = t_frames / fps + sep = " " if prompt.rstrip().endswith(".") else ". " + prompt = prompt + sep + _DURATION_FPS_TEMPLATE.format(duration=duration, fps=fps) + if append_resolution_info: + sep = " " if prompt.rstrip().endswith(".") else ". " + prompt = prompt + sep + _RESOLUTION_TEMPLATE.format(height=height, width=width) + return prompt + + +def _extract_bool_from_config(config: Any, key: str, default: bool) -> bool: + """Recursively search dataloader_train config for a boolean flag.""" + + def _search(obj: Any) -> bool | None: + if isinstance(obj, (DictConfig, dict)): + if key in obj: + val = obj[key] + if isinstance(val, bool): + return val + iterable = obj.values() + for v in iterable: + result = _search(v) + if result is not None: + return result + return None + + try: + if isinstance(config, dict): + dl_train = config.get("dataloader_train") + else: + dl_train = getattr(config, "dataloader_train", None) + if dl_train is not None: + result = _search(dl_train) + if result is not None: + return result + except Exception: + pass + return default + + +def _extract_str_from_config(config: Any, key: str) -> str | None: + """Recursively search dataloader_train config for a string field.""" + + def _search(obj: Any) -> str | None: + if isinstance(obj, (DictConfig, dict)): + if key in obj: + val = obj[key] + if isinstance(val, str): + return val + iterable = obj.values() + for v in iterable: + result = _search(v) + if result is not None: + return result + return None + + try: + if isinstance(config, dict): + dl_train = config.get("dataloader_train") + else: + dl_train = getattr(config, "dataloader_train", None) + if dl_train is not None: + return _search(dl_train) + except Exception: + pass + return None + + +def _extract_chunk_length_from_config(config: Any) -> int | None: + """Try to extract chunk_length from the experiment's dataloader config. + + Recursively searches ``config.dataloader_train`` for ``chunk_length`` or + ``num_action_per_chunk`` to determine the action chunk size the model was + trained with. Returns ``None`` when neither key is found. + """ + + def _search(obj: Any, keys: tuple[str, ...] = ("chunk_length", "num_action_per_chunk")) -> int | None: + if isinstance(obj, (DictConfig, dict)): + for key in keys: + if key in obj: + val = obj[key] + if isinstance(val, int): + return val + for v in obj.values(): + result = _search(v, keys) + if result is not None: + return result + return None + + try: + if isinstance(config, dict): + dl_train = config.get("dataloader_train") + else: + dl_train = getattr(config, "dataloader_train", None) + if dl_train is not None: + return _search(dl_train) + except Exception: + pass + return None + + +def _get_free_local_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +def _get_local_ip() -> str: + """Get the local IP address of this machine.""" + try: + # Connect to an external address to determine the local IP + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + return str(s.getsockname()[0]) + except Exception: + return socket.gethostbyname(socket.gethostname()) + + +def _maybe_init_distributed() -> None: + """ + Initialize a one-rank process group when launched outside torchrun so FSDP + and distributed utilities have the process-group state they expect. + + ``init_script()`` already inits the PG when ``WORLD_SIZE>1`` (under + torchrun); this function fills in the single-process case used by the + standalone server. + """ + if not dist.is_available() or dist.is_initialized(): + return + + world_size_env = os.getenv("WORLD_SIZE") + rank_env = os.getenv("RANK") + local_rank = int(os.getenv("LOCAL_RANK", "0")) + + backend = "nccl" if torch.cuda.is_available() else "gloo" + + if world_size_env is not None and rank_env is not None: + if torch.cuda.is_available(): + torch.cuda.set_device(local_rank) + dist.init_process_group(backend=backend, init_method="env://") + return + + if torch.cuda.is_available(): + torch.cuda.set_device(0) + port = _get_free_local_port() + dist.init_process_group(backend=backend, init_method=f"tcp://127.0.0.1:{port}", rank=0, world_size=1) + + +def _strip_data_url_prefix(b64: str) -> str: + # Accept "data:image/png;base64,...." as well as raw base64. + if "," in b64 and b64[:64].lower().startswith("data:"): + return b64.split(",", 1)[1].strip() + return b64.strip() + + +def _b64decode_loose(b64: str) -> bytes: + """ + Decode base64 permissively. + + The simulator/client may include whitespace/newlines, omit padding, or use urlsafe base64. + """ + s = _strip_data_url_prefix(b64) + s = "".join(s.split()) # remove whitespace/newlines + pad = (-len(s)) % 4 + if pad: + s = s + ("=" * pad) + try: + return base64.b64decode(s, validate=False) + except binascii.Error: + return base64.urlsafe_b64decode(s) + + +def _decode_base64_png_to_rgb_uint8(image_b64: str) -> torch.Tensor: + """ + Returns a tensor with shape (3, H, W), dtype uint8, RGB. + """ + try: + raw = _b64decode_loose(image_b64) + except (binascii.Error, ValueError) as e: + raise ValueError(f"Invalid base64 image: {e}") from e + + with Image.open(io.BytesIO(raw)) as img: + img = img.convert("RGB") + # Pillow can expose a read-only view; make it writable to avoid PyTorch warnings/UB. + arr = np.asarray(img, dtype=np.uint8).copy() + if arr.ndim != 3 or arr.shape[2] != 3: + raise ValueError(f"Expected RGB image, got shape {arr.shape}") + chw = torch.from_numpy(arr).permute(2, 0, 1).contiguous() # [3,H,W] + return chw + + +def _video_tensor_to_pil_images(video_c_t_h_w: torch.Tensor) -> list[Image.Image]: + """ + Convert (C, T, H, W) float tensor in [-1,1] or [0,1] to a list of PIL RGB frames. + """ + if video_c_t_h_w.dim() != 4: + raise ValueError(f"Expected (C,T,H,W), got {tuple(video_c_t_h_w.shape)}") + if int(video_c_t_h_w.shape[0]) != 3: + raise ValueError(f"Expected C=3 RGB, got C={int(video_c_t_h_w.shape[0])}") + + images: list[Image.Image] = [] + t = int(video_c_t_h_w.shape[1]) + for ti in range(t): + frame = video_c_t_h_w[:, ti].detach().cpu().float() + if frame.min().item() < 0.0: + frame = (frame + 1.0) / 2.0 + frame = frame.clamp(0.0, 1.0) + frame_uint8 = (frame * 255.0).round().to(torch.uint8) # [3,H,W] + hwc = frame_uint8.permute(1, 2, 0).numpy() # [H,W,3] + images.append(Image.fromarray(hwc)) + return images + + +def _save_gif(frames: list[Image.Image], path: Path, fps: int) -> None: + if not frames: + return + path.parent.mkdir(parents=True, exist_ok=True) + duration_ms = max(1, int(round(1000.0 / float(fps)))) + frames[0].save( + path, + save_all=True, + append_images=frames[1:], + duration=duration_ms, + loop=0, + optimize=False, + ) + + print(f"Saved gif to {path}") + + +def _save_policy_request_dump( + *, + dump_root: Path, + request_id: int, + request_json: dict[str, Any], + obs_chw_uint8: torch.Tensor, + pred_action: list[list[float]], + pred_video_c_t_h_w: torch.Tensor | None, + fps: int, +) -> None: + """ + Dump input observation, predicted actions, and rollout video for offline debugging. + Creates: + - request.json + - observation.png + - action_output.json + - rollout.gif (if pred_video provided) + - rollout_frames/frame_XXX.png (if pred_video provided) + """ + ts = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%d_%H%M%S_%fZ") + out_dir = dump_root / f"{ts}_req{request_id:06d}" + out_dir.mkdir(parents=True, exist_ok=False) + + # Save request JSON (without the base64 image to save space, image is saved separately) + request_json_copy = {k: v for k, v in request_json.items() if k != "image"} + request_json_copy["image"] = "" + (out_dir / "request.json").write_text(json.dumps(request_json_copy, indent=2), encoding="utf-8") + + # Save observation image + obs_hwc = obs_chw_uint8.permute(1, 2, 0).cpu().numpy() # [H,W,3] + obs_img = Image.fromarray(obs_hwc) + obs_img.save(out_dir / "observation.png") + + # Save predicted actions + action_output = {"action": pred_action} + (out_dir / "action_output.json").write_text(json.dumps(action_output, indent=2), encoding="utf-8") + + if pred_video_c_t_h_w is None: + return + + # Save rollout video + frames = _video_tensor_to_pil_images(pred_video_c_t_h_w) + _save_gif(frames, out_dir / "rollout.gif", fps=fps) + + # Save individual frames + frames_dir = out_dir / "rollout_frames" + frames_dir.mkdir(parents=True, exist_ok=True) + for i, frame in enumerate(frames): + frame.save(frames_dir / f"frame_{i:03d}.png") + + +def _save_failed_request_dump( + *, + dump_root: Path, + request_id: int, + request_json: dict[str, Any], + error: str, +) -> None: + """ + Dump request + error even if inference fails. + Best-effort: will try to decode and save observation image if present. + """ + ts = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%d_%H%M%S_%fZ") + out_dir = dump_root / f"{ts}_req{request_id:06d}_ERROR" + out_dir.mkdir(parents=True, exist_ok=False) + + # Save request JSON (without base64 image) + request_json_copy = {k: v for k, v in request_json.items() if k != "image"} + if "image" in request_json: + request_json_copy["image"] = "" + (out_dir / "request.json").write_text(json.dumps(request_json_copy, indent=2), encoding="utf-8") + (out_dir / "error.txt").write_text(error, encoding="utf-8") + + try: + image_b64 = request_json.get("image") + if isinstance(image_b64, str): + img_chw_uint8 = _decode_base64_png_to_rgb_uint8(image_b64) + obs_hwc = img_chw_uint8.permute(1, 2, 0).cpu().numpy() # [H,W,3] + Image.fromarray(obs_hwc).save(out_dir / "observation.png") + except Exception: + # Ignore any dump failures. + return + + +def _ts() -> str: + return datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +def _load_introspection_config_dict(setup_args: OmniSetupArgs) -> dict: + """Load the full experiment config as a plain dict for prompt-augmentation + introspection. + + For ``MODULE`` (``.py``) configs, ``OmniInference.create`` saves the + structured config to ``setup_args.output_dir / 'config.yaml'`` via + ``cosmos3.common.config.save_config`` immediately after model load. For + ``YAML`` / ``JSON`` configs we just deserialize the source file directly. + """ + if setup_args.config_file_type == ConfigFileType.MODULE: + saved = Path(setup_args.output_dir) / "config.yaml" + if saved.exists(): + return deserialize_config_dict(saved) + # Fallback: re-parse the .py module without instantiating anything. + import importlib + + from cosmos3.common.config import unstructure_config + from cosmos3._src.imaginaire.utils import config_helper + + config_module = importlib.import_module(config_helper.get_config_module(setup_args.config_file)) + config = config_module.make_config() + config = config_helper.override( + config, ["--", f"experiment={setup_args.experiment}", *setup_args.experiment_overrides] + ) + return unstructure_config(config, invalid="ignore") + + return deserialize_config_dict(Path(setup_args.config_file)) + + +# --------------------------------------------------------------------------- +# CLI args +# --------------------------------------------------------------------------- + + +class ActionServerArgs(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid", use_attribute_docstrings=True) + + # We deliberately do NOT expose the full ``OmniSetupOverrides`` (i.e. no + # ``setup: SetupOverrides`` field). Doing so under ``tyro.conf.OmitArgPrefixes`` + # would surface the same flag names twice (once as ``--config-file`` from + # ``setup.config_file`` and once as the action-server shortcut) plus expose + # every ``setup.sample_overrides.*`` knob (``--guidance``, ``--seed``, + # ``--num-steps``, ...) which collides with our own server-level defaults. + # Instead we keep a flat arg model and build ``OmniSetupOverrides`` + # programmatically in ``build_setup_overrides``. + # + # All flags follow the cosmos3 convention: hyphenated CLI names auto-derived + # from the underscored field name (``checkpoint_dir`` -> ``--checkpoint-dir``). + + checkpoint_dir: str = "" + """Path to checkpoint directory (local path or ``s3://`` URI). Maps to + ``OmniSetupOverrides.checkpoint_path``.""" + + config_file: str = "cosmos3/_src/vfm/configs/base/config.py" + """Hydra config: a ``.py`` config-store module, a ``.yaml`` frozen Hydra + config (e.g. the one written next to a ``cosmos3.scripts.train`` checkpoint), + or a Hugging Face ``config.json``. ``OmniInference.create`` auto-dispatches + on the file extension.""" + + experiment_name: str = "" + """Hydra experiment name (only required for ``.py`` configs; ignored when + ``--config-file`` is a ``.yaml``/``.json`` frozen config).""" + + experiment_overrides: list[str] = pydantic.Field(default_factory=list) + """Extra Hydra dotlist overrides to apply to the experiment config.""" + + credential_path: str = "" + """Path to S3 credentials file for remote checkpoint loading.""" + + local_cache_dir: Path | None = None + """Directory for caching S3 checkpoints. Maps to + ``OmniSetupOverrides.checkpoint_cache_dir``.""" + + output_dir: Path | None = None + """Output directory for ``OmniInference`` (saved config.yaml, benchmarks). + Defaults to ``--dump-dir`` if set, else ``/tmp/cosmos3_action_server``.""" + + # ----- single-rank parallelism / sampler ---------------------------------- + sampler: Literal["unipc", "edm"] = "unipc" + """Diffusion sampler used by ``OmniInference``.""" + + # ----- sampling defaults (per-request, used when client doesn't override) -- + seed: int = 0 + """Random seed for ``model.generate_samples_from_batch``.""" + guidance: float = 1.0 + """Guidance scale for denoising.""" + num_steps: int = 30 + """Number of denoising steps.""" + fps: int = 20 + """Frames per second used for both prompt augmentation and rollout encoding.""" + + # ----- action policy parameters ------------------------------------------- + action_chunk_size: int | None = None + """Number of action steps to predict. Defaults to ``chunk_length`` / + ``num_action_per_chunk`` from the experiment config (or 16).""" + max_action_dim: int | None = None + """Maximum action dimension. Defaults to ``model.config.max_action_dim`` + from the experiment config (or 64).""" + raw_action_dim: int | None = None + """Unpadded action dimension used for action-channel masking. Inferred + from action stats when omitted.""" + + # ----- action denormalization --------------------------------------------- + action_stats_path: Path | None = None + """Path to action stats JSON for denormalizing predicted actions.""" + action_normalization: ActionNormalization = "auto" + """Action normalization to invert. ``auto`` reads ``action_normalization`` + from the experiment config (default ``minmax`` if unspecified).""" + + # ----- debug dumps -------------------------------------------------------- + dump_dir: Path | None = None + """If set, dump observations, predicted actions, and rollout videos under + this directory for offline debugging.""" + dump_every: int = 1 + """Dump every N-th request (only used when ``--dump-dir`` is set).""" + + # ----- HTTP server -------------------------------------------------------- + host: str = "0.0.0.0" + """HTTP host to bind.""" + port: int = 8000 + """HTTP port to bind.""" + http_400_on_error: bool = False + """If set, return HTTP 400 on inference errors. Default is HTTP 200 with an + empty action list, matching the legacy simulator client expectations.""" + + # ----- developer utilities ------------------------------------------------ + run_validation: bool = False + """If set, run a one-shot validation/training batch through the model on + startup (developer debugging only).""" + + def build_setup_overrides(self) -> OmniSetupOverrides: + """Build an ``OmniSetupOverrides`` from the flat CLI fields. + + Required fields (``checkpoint_path``) must be present; optional fields + keep their ``OmniSetupOverrides`` defaults when not specified by the + user. + """ + if not self.checkpoint_dir: + raise ValueError("--checkpoint-dir is required") + + output_dir = self.output_dir or self.dump_dir or _DEFAULT_FALLBACK_OUTPUT_DIR + + # ``model_construct`` skips validation; we set fields explicitly so + # downstream ``build_setup`` validators run on a fully-populated object. + base = OmniSetupOverrides.model_construct() + base.checkpoint_path = self.checkpoint_dir + base.config_file = self.config_file + base.experiment = self.experiment_name + base.experiment_overrides = list(self.experiment_overrides) + if self.credential_path: + base.credential_path = self.credential_path + if self.local_cache_dir is not None: + base.checkpoint_cache_dir = self.local_cache_dir + base.output_dir = output_dir + base.sampler = self.sampler + return base + + +# --------------------------------------------------------------------------- +# Service implementation (predict path is a verbatim port from the previous +# evaluation/action/http_inference_server.py). +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ActionServerConfig: + """Internal snapshot of the resolved CLI args, threaded through the service. + + Kept as a plain dataclass (vs. carrying ``ActionServerArgs`` directly) so + request-time code can ``replace(...)`` individual fields after model load. + """ + + seed: int + guidance: float + num_steps: int + fps: int + action_chunk_size: int + max_action_dim: int + raw_action_dim: int | None + dump_dir: Path | None + dump_every: int + http_400_on_error: bool + action_stats_path: Path | None + action_normalization: ActionNormalization + experiment_name: str + checkpoint_dir: str + + +class ActionModelService: + def __init__(self, args: ActionServerArgs) -> None: + if not torch.cuda.is_available(): + raise RuntimeError("CUDA is required for OmniMoTModel inference in this repo.") + + # OmniInference internally calls into FSDP / DTensor parallelize utilities + # that expect a process group; create a single-rank PG when not under + # torchrun (init_script() only inits the PG when WORLD_SIZE>1). + _maybe_init_distributed() + + setup_overrides = args.build_setup_overrides() + setup_args = setup_overrides.build_setup() + init_output_dir(setup_args.output_dir) + + # Surface the resolved max_action_dim into the experiment config when + # the user explicitly overrode it on the CLI; matches the previous + # ``experiment_opts=[f"model.config.max_action_dim={...}"]`` plumbing. + if args.max_action_dim is not None: + setup_args.experiment_overrides = [ + *setup_args.experiment_overrides, + f"model.config.max_action_dim={int(args.max_action_dim)}", + ] + + log.info( + f"[action-server] loading model: config_file='{setup_args.config_file}' " + f"({setup_args.config_file_type}) experiment='{setup_args.experiment}' " + f"checkpoint_path='{setup_args.checkpoint_path}'" + ) + + # OmniInference dispatches between MODULE (.py) and YAML/JSON loaders. + pipe = OmniInference.create(setup_args) + self.pipe: OmniInference = pipe + self.model = pipe.model + self.model.eval() + # OmniInference always uses OmniSetupArgs at runtime, but the base class + # types the attribute as the more general SetupArgs. + assert isinstance(pipe.setup_args, OmniSetupArgs) + self.setup_args: OmniSetupArgs = pipe.setup_args + self.experiment_config: dict = _load_introspection_config_dict(self.setup_args) + + # Resolve action_chunk_size: CLI arg > experiment config > default. + if args.action_chunk_size is not None: + resolved_chunk_size = int(args.action_chunk_size) + else: + config_chunk = _extract_chunk_length_from_config(self.experiment_config) + if config_chunk is not None: + resolved_chunk_size = config_chunk + log.info( + f"[action-server] --action-chunk-size not specified, " + f"using chunk_length={resolved_chunk_size} from experiment config" + ) + else: + resolved_chunk_size = _DEFAULT_ACTION_CHUNK_SIZE + log.info( + f"[action-server] --action-chunk-size not specified and not found in experiment config, " + f"using default={resolved_chunk_size}" + ) + + # Resolve max_action_dim: CLI > model config > default (64). + if args.max_action_dim is not None: + resolved_max_action_dim = int(args.max_action_dim) + else: + model_max_action_dim = getattr(self.model.config, "max_action_dim", None) + if isinstance(model_max_action_dim, int): + resolved_max_action_dim = model_max_action_dim + log.info( + f"[action-server] --max-action-dim not specified, " + f"using max_action_dim={resolved_max_action_dim} from model config" + ) + else: + resolved_max_action_dim = 64 + log.info( + f"[action-server] --max-action-dim not specified and not found in model config, " + f"using default={resolved_max_action_dim}" + ) + + self.cfg = ActionServerConfig( + seed=int(args.seed), + guidance=float(args.guidance), + num_steps=int(args.num_steps), + fps=int(args.fps), + action_chunk_size=resolved_chunk_size, + max_action_dim=resolved_max_action_dim, + raw_action_dim=int(args.raw_action_dim) if args.raw_action_dim is not None else None, + dump_dir=args.dump_dir, + dump_every=int(args.dump_every), + http_400_on_error=bool(args.http_400_on_error), + action_stats_path=args.action_stats_path, + action_normalization=args.action_normalization, + experiment_name=setup_args.experiment or "", + checkpoint_dir=setup_args.checkpoint_path, + ) + + self._lock = threading.Lock() + self._req_id_lock = threading.Lock() + self._req_id = 0 + + self.append_duration_fps = _extract_bool_from_config( + self.experiment_config, "append_duration_fps", default=True + ) + self.append_resolution_info = _extract_bool_from_config( + self.experiment_config, "append_resolution_info", default=True + ) + log.info( + f"[action-server] prompt augmentation: " + f"append_duration_fps={self.append_duration_fps}, append_resolution_info={self.append_resolution_info}" + ) + + # Action denormalization stats. + self.action_min: torch.Tensor | None = None + self.action_range: torch.Tensor | None = None + self.action_mean: torch.Tensor | None = None + self.action_std: torch.Tensor | None = None + self.action_normalization: ResolvedActionNormalization = "minmax" + self.raw_action_dim: int | None = self.cfg.raw_action_dim + self._load_action_normalization_stats() + if self.raw_action_dim is None: + self.raw_action_dim = 7 + + if args.run_validation: + self._run_developer_validation() + + # ------------------------------------------------------------------ + # Action denormalization + # ------------------------------------------------------------------ + + def _load_action_normalization_stats(self) -> None: + """Load action denormalization tensors from ``cfg.action_stats_path``. + + Populates ``self.action_normalization``, the relevant tensor attributes + (``action_mean`` / ``action_std`` for meanstd; ``action_min`` / + ``action_range`` for minmax / quantile / quantile_rot), and infers + ``self.raw_action_dim`` when not explicitly set. + """ + if self.cfg.action_stats_path is None: + return + + self.action_normalization = self._resolve_action_normalization(self.cfg.action_normalization) + stats_path = Path(self.cfg.action_stats_path) + if not stats_path.is_absolute(): + stats_path = Path.cwd() / stats_path + with open(stats_path) as f: + raw_stats = json.load(f) + if not isinstance(raw_stats, dict): + raise ValueError(f"Action stats file must contain a dict: {stats_path}") + stats_key = "global_raw" if self.action_normalization == "quantile_rot" else "global" + stats = raw_stats.get(stats_key, raw_stats) + if not isinstance(stats, dict): + raise ValueError(f"Action stats file must contain a dict or {stats_key} stats dict: {stats_path}") + if self.action_normalization == "meanstd": + if "mean" not in stats or "std" not in stats: + raise ValueError(f"Mean/std action normalization requires 'mean' and 'std' in {stats_path}") + self.action_mean = torch.tensor(stats["mean"], dtype=torch.float32) # [D] + action_std = torch.tensor(stats["std"], dtype=torch.float32) # [D] + self.action_std = torch.clamp(action_std, min=1e-8) # [D] + stats_dim = int(self.action_mean.shape[0]) + stats_summary = f"mean={self.action_mean.tolist()}, std={self.action_std.tolist()}" + elif self.action_normalization in ("quantile", "quantile_rot"): + if "q01" not in stats or "q99" not in stats: + raise ValueError(f"Quantile action normalization requires 'q01' and 'q99' in {stats_path}") + self.action_min = torch.tensor(stats["q01"], dtype=torch.float32) # [D] + action_max = torch.tensor(stats["q99"], dtype=torch.float32) # [D] + action_range = action_max - self.action_min # [D] + self.action_range = torch.clamp(action_range, min=1e-6) # [D] + stats_dim = int(self.action_min.shape[0]) + stats_summary = f"q01={self.action_min.tolist()}, q99={action_max.tolist()}" + else: + if "min" not in stats or "max" not in stats: + raise ValueError(f"Min/max action normalization requires 'min' and 'max' in {stats_path}") + self.action_min = torch.tensor(stats["min"], dtype=torch.float32) # [D] + action_max = torch.tensor(stats["max"], dtype=torch.float32) # [D] + action_range = action_max - self.action_min # [D] + self.action_range = torch.clamp(action_range, min=1e-6) # [D] + stats_dim = int(self.action_min.shape[0]) + stats_summary = f"min={self.action_min.tolist()}, max={action_max.tolist()}" + if self.raw_action_dim is None: + self.raw_action_dim = stats_dim + if stats_dim != self.raw_action_dim: + raise ValueError(f"Action stats dimension {stats_dim} does not match raw_action_dim={self.raw_action_dim}") + log.info( + f"[action-server] Loaded action stats for denormalization from {stats_path}: " + f"normalization={self.action_normalization}, {stats_summary}" + ) + + def _resolve_action_normalization( + self, requested_normalization: ActionNormalization + ) -> ResolvedActionNormalization: + """Resolve auto action normalization from the loaded experiment config.""" + if requested_normalization != "auto": + return requested_normalization + + configured_normalization = _extract_str_from_config(self.experiment_config, "action_normalization") + if configured_normalization is None: + return "minmax" + if configured_normalization in ("meanstd", "minmax", "quantile", "quantile_rot"): + return configured_normalization # type: ignore[return-value] + raise ValueError( + "action_policy_server.py can denormalize action_normalization='minmax', 'meanstd', " + "'quantile', or 'quantile_rot'; " + f"loaded experiment config requested {configured_normalization!r}. " + "Pass --action-normalization explicitly if this checkpoint should use a supported method." + ) + + def _denormalize_action(self, action: torch.Tensor) -> torch.Tensor: + """Invert the configured action normalization.""" + if self.action_normalization == "meanstd": + if self.action_mean is None or self.action_std is None: + return action + action_dim = self.action_mean.shape[0] + normalized = action[..., :action_dim] # [...,D] + action_mean = self.action_mean.to(action.device) # [D] + action_std = self.action_std.to(action.device) # [D] + return normalized * action_std + action_mean # [...,D] + + if self.action_min is None or self.action_range is None: + return action + action_dim = self.action_min.shape[0] + normalized = action[..., :action_dim] # [...,D] + action_min = self.action_min.to(action.device) # [D] + action_range = self.action_range.to(action.device) # [D] + return (normalized + 1.0) / 2.0 * action_range + action_min # [...,D] + + # ------------------------------------------------------------------ + # HTTP plumbing + # ------------------------------------------------------------------ + + def _should_dump(self, request_id: int) -> bool: + """Decide whether to dump this request. + + We always dump the first request (request_id == 1) as a quick sanity + check that dumping is wired up, then dump every N-th request controlled + by ``dump_every``. + """ + if self.cfg.dump_dir is None: + return False + n = int(self.cfg.dump_every) + if n <= 0: + return False + return request_id == 1 or (request_id % n == 0) + + def get_info(self) -> dict[str, Any]: + """Return model / server info for the /info endpoint. + + Includes all runtime-relevant config so clients can record reproducible + params.json without needing to know CLI flags. + """ + return { + "run_name": self.cfg.experiment_name, + "checkpoint": self.cfg.checkpoint_dir, + "config_file": str(self.setup_args.config_file), + "config_file_type": str(self.setup_args.config_file_type), + "guidance": self.cfg.guidance, + "num_steps": self.cfg.num_steps, + "fps": self.cfg.fps, + "seed": self.cfg.seed, + "action_chunk_size": self.cfg.action_chunk_size, + "max_action_dim": self.cfg.max_action_dim, + "raw_action_dim": self.cfg.raw_action_dim, + "action_stats_path": str(self.cfg.action_stats_path) if self.cfg.action_stats_path else None, + } + + # ------------------------------------------------------------------ + # Predict + # ------------------------------------------------------------------ + + def predict_policy(self, req: dict[str, Any]) -> dict[str, Any]: + """ + Run policy inference: given an observation image and prompt, predict actions. + + Input request format: + { + "image": "", + "prompt": "", + "domain_name": "", + "image_size": + } + + Output format: + { + "action": [[a0_0, a0_1, ...], ..., [aN_0, aN_1, ...]], + "video": ["", ...] # List of T base64-encoded PNG frames + } + + All action dimensions are returned. Video is the decoded predicted rollout as base64 PNGs. + """ + t0 = time.monotonic() + + # Get or assign request ID + injected_id = req.get("request_id", None) + if isinstance(injected_id, int) and injected_id > 0: + request_id = int(injected_id) + else: + with self._req_id_lock: + self._req_id += 1 + request_id = int(self._req_id) + + # Validate request + image_b64 = req.get("image") + if not isinstance(image_b64, str): + raise ValueError("'image' must be a base64 string") + + prompt = req.get("prompt") + if not isinstance(prompt, str): + raise ValueError("'prompt' must be a string") + + domain_name = req.get("domain_name") + if not isinstance(domain_name, str): + raise ValueError("'domain_name' must be a string") + + image_size = req.get("image_size") + if not isinstance(image_size, int) or image_size <= 0: + raise ValueError("'image_size' must be a positive integer") + + # Decode image + t_decode0 = time.monotonic() + img_chw_uint8 = _decode_base64_png_to_rgb_uint8(image_b64) + img_h, img_w = img_chw_uint8.shape[-2:] + + # Handle resizing: for multi-view (non-square) images, scale proportionally + # to maintain aspect ratio while matching height to image_size + if img_h != image_size: + # Calculate new width to maintain aspect ratio + scale = image_size / img_h + new_w = int(round(img_w * scale)) + hwc = img_chw_uint8.permute(1, 2, 0).cpu().numpy() # [H,W,3] + resized = Image.fromarray(hwc).resize((new_w, image_size), resample=Image.Resampling.BILINEAR) + arr = np.asarray(resized, dtype=np.uint8).copy() + img_chw_uint8 = torch.from_numpy(arr).permute(2, 0, 1).contiguous() # [3,H,W] # [3,H,W] + t_decode1 = time.monotonic() + + # Construct batch in IterativeJointDataLoader format (list-of-lists for multi-item keys) + t_frames = self.cfg.action_chunk_size + 1 + _, final_h, final_w = img_chw_uint8.shape + video_c_t_h_w_uint8 = img_chw_uint8.unsqueeze(1).repeat(1, t_frames, 1, 1) # [3,T,H,W] + + # Apply reflection padding to match closest predefined resolution + resolution = get_vision_data_resolution((final_h, final_w)) + target_w, target_h = find_closest_target_size(final_h, final_w, resolution) + pad_dict: dict[str, Any] = {"video": video_c_t_h_w_uint8} + reflection_pad_to_target(pad_dict, ["video"], True, target_w, target_h) + video_padded = pad_dict["video"] # (C, T, target_h, target_w) + padded_image_size = pad_dict["image_size"] # (4,) + + # Action: zeros tensor as noise starting point for policy mode + action_t_d = torch.zeros( + (self.cfg.action_chunk_size, self.cfg.max_action_dim), + dtype=torch.float32, + ) # [T,action_dim] + + input_video_key = getattr(self.model, "input_video_key", None) + if input_video_key is None: + input_video_key = getattr(self.model, "config", None).input_video_key # type: ignore[union-attr] + + sequence_plan = build_sequence_plan_from_mode( + mode="policy", + video_length=self.cfg.action_chunk_size + 1, + action_length=self.cfg.action_chunk_size, + has_text=True, + ) + + augmented_prompt = _augment_prompt_with_metadata( + prompt, + t_frames=t_frames, + fps=self.cfg.fps, + height=final_h, + width=final_w, + append_duration_fps=self.append_duration_fps, + append_resolution_info=self.append_resolution_info, + ) + + batch: dict[str, Any] = { + input_video_key: [[video_padded]], + "raw_action_dim": [torch.tensor(self.raw_action_dim, dtype=torch.long)], + "action": [[action_t_d]], + "mode": ["policy"], + "ai_caption": [augmented_prompt], + "prompt": [augmented_prompt], + "conditioning_fps": [torch.tensor(self.cfg.fps, dtype=torch.long)], + "image_size": padded_image_size.unsqueeze(0).to(device="cuda"), + "domain_id": [torch.tensor(get_domain_id(domain_name), dtype=torch.long)], + "sequence_plan": [sequence_plan], + } + + if getattr(self.model, "training", False): + log.warning(f"[action-server] request_id={request_id} WARNING: model.training=True") + + log.info( + f"[action-server] request_id={request_id} mode=policy " + f"prompt={augmented_prompt!r} domain_name={domain_name!r} image_size={image_size} " + f"img={tuple(img_chw_uint8.shape)} steps={self.cfg.num_steps} guidance={self.cfg.guidance}" + ) + + # Run inference + t_inf0 = time.monotonic() + with self._lock: + with torch.inference_mode(): + samples = self.model.generate_samples_from_batch( + batch, + guidance=self.cfg.guidance, + seed=[self.cfg.seed], + num_steps=self.cfg.num_steps, + has_negative_prompt=False, + ) + pred_action = samples["action"][0] # [T,D] or [1,T,D] + + # Decode vision for rollout video (samples["vision"] is a list; take first sample) + pred_video_c_t_h_w = self.model.decode(samples["vision"][0]).squeeze(0) # [C,T,H,W] + + # Remove reflection padding so the reported video matches the original resolution + pred_video_c_t_h_w = remove_reflection_padding(pred_video_c_t_h_w, padded_image_size) + t_inf1 = time.monotonic() + + # Extract actions: return all dimensions — (T, D) or (1, T, D) + pred_action = pred_action.float().squeeze(0) # [T,D] + pred_action = self._denormalize_action(pred_action) + pred_action_np = pred_action.detach().cpu().numpy() # [T,D] + pred_action_list = pred_action_np.tolist() # List of [a0, a1, ..., aD] + + # Convert video to base64-encoded PNG frames + pred_video_frames = _video_tensor_to_pil_images(pred_video_c_t_h_w) + pred_video_b64: list[str] = [] + for frame in pred_video_frames: + buf = io.BytesIO() + frame.save(buf, format="PNG") + pred_video_b64.append(base64.b64encode(buf.getvalue()).decode("ascii")) + + # Optional offline debug dump + if self._should_dump(request_id): + dump_dir = self.cfg.dump_dir + assert dump_dir is not None + dump_root = Path(dump_dir) + dump_root.mkdir(parents=True, exist_ok=True) + try: + log.info(f"[action-server] request_id={request_id} dumping to {str(dump_root)}") + _save_policy_request_dump( + dump_root=dump_root, + request_id=request_id, + request_json=req, + obs_chw_uint8=img_chw_uint8, + pred_action=pred_action_list, + pred_video_c_t_h_w=pred_video_c_t_h_w, + fps=int(self.cfg.fps), + ) + except Exception as e: + # Never fail serving a request due to dump failures + log.error(f"[action-server] dump failed for request_id={request_id}: {e}") + + dt_total_ms = (time.monotonic() - t0) * 1000.0 + dt_decode_ms = (t_decode1 - t_decode0) * 1000.0 + dt_inf_ms = (t_inf1 - t_inf0) * 1000.0 + log.info( + f"[action-server] request_id={request_id} done action_steps={len(pred_action_list)} " + f"video_frames={len(pred_video_b64)} " + f"ms_total={dt_total_ms:.1f} ms_decode={dt_decode_ms:.1f} ms_infer={dt_inf_ms:.1f}" + ) + return {"action": pred_action_list, "video": pred_video_b64} + + # ------------------------------------------------------------------ + # Developer validation (optional, --run-validation) + # ------------------------------------------------------------------ + + def _run_developer_validation(self) -> None: + """Run a single validation/training batch through the model on startup.""" + # Re-instantiate config so we can spin up dataloaders without the + # inference-time freezes applied to the model config. + if self.setup_args.config_file_type != ConfigFileType.MODULE: + log.warning( + "[action-server] --run-validation requires a .py config-file (got " + f"{self.setup_args.config_file_type}); skipping." + ) + return + + try: + config = self.setup_args.load_config() + except Exception as e: + log.warning(f"[action-server] --run-validation could not load config: {e}; skipping.") + return + + try: + val_dataset = instantiate(config.dataloader_val) + train_dataset = instantiate(config.dataloader_train) + except Exception as e: + log.warning(f"[action-server] --run-validation could not instantiate datasets: {e}; skipping.") + return + + val_batch = next(iter(val_dataset)) # pyrefly: ignore[no-matching-overload] + train_batch = next(iter(train_dataset)) # pyrefly: ignore[no-matching-overload] + + with torch.inference_mode(): + self.model.training_step(val_batch, 0) + sample_num = 1 + val_result = self.model.generate_samples_from_batch( + val_batch, + guidance=1.0, + seed=[0 for _ in range(sample_num)], + num_steps=8, + n_sample=sample_num, + ) + video_mse_list = [] + action_mse_list = [] + for i in range(sample_num): + val_video = self.model.decode(val_result["vision"][i]).detach().cpu() + val_video_mse = torch.nn.functional.mse_loss(val_video, val_batch["video"][i].cpu()) + val_action = val_result["action"][i].detach().cpu() + val_action_mse = torch.nn.functional.mse_loss(val_action[:, :6], val_batch["action"][i][0][:, :6].cpu()) + video_mse_list.append(val_video_mse.item()) + action_mse_list.append(val_action_mse.item()) + log.info(f"Val video MSE: {np.mean(video_mse_list)}") + log.info(f"Val action MSE: {np.mean(action_mse_list)}") + + self.model.training_step(train_batch, 0) + train_result = self.model.generate_samples_from_batch( + train_batch, + guidance=1.0, + seed=list(range(20)), + num_steps=8, + n_sample=20, + ) + train_video = self.model.decode(train_result["vision"][0]) + train_action = train_result["action"][0] + train_action_mse = torch.nn.functional.mse_loss(train_action[:, :6], train_batch["action"][0][:, :6]) + train_video_mse = torch.nn.functional.mse_loss(train_video, train_batch["video"][0]) + log.info(f"Train action MSE: {train_action_mse}; Train video MSE: {train_video_mse}") + + +# --------------------------------------------------------------------------- +# HTTP handler +# --------------------------------------------------------------------------- + + +class _ActionHandler(BaseHTTPRequestHandler): + """ + ThreadingHTTPServer handler. + + The service instance is injected via ``server.service``. + """ + + server: ThreadingHTTPServer # type: ignore[assignment] + + def _send_json(self, status_code: int, payload: dict[str, Any]) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status_code) + self.send_header("Content-Type", "application/json") + # Avoid caches/proxies returning stale results. + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + try: + self.wfile.write(body) + except (BrokenPipeError, ConnectionResetError): + # Client closed the connection (often due to request timeout). + # Avoid noisy tracebacks; nothing to do server-side. + return + + def do_GET(self) -> None: # noqa: N802 + if self.path == "/info": + service: ActionModelService = getattr(self.server, "service") # type: ignore[attr-defined] + self._send_json(200, service.get_info()) + elif self.path == "/": + self._send_json(200, {"status": "ok"}) + else: + self._send_json(404, {"error": "Not found"}) + + def do_POST(self) -> None: # noqa: N802 + if self.path not in ("/", "/predict"): + self._send_json(404, {"error": "Not found"}) + return + + content_type = (self.headers.get("Content-Type") or "").split(";", 1)[0].strip().lower() + if content_type != "application/json": + self._send_json(415, {"error": "Content-Type must be application/json"}) + return + + try: + length = int(self.headers.get("Content-Length") or "0") + except ValueError: + self._send_json(400, {"error": "Invalid Content-Length"}) + return + + body = self.rfile.read(max(0, length)) + try: + req = json.loads(body.decode("utf-8")) + except Exception as e: + self._send_json(400, {"error": f"Invalid JSON: {e}"}) + return + + if not isinstance(req, dict): + self._send_json(400, {"error": "JSON body must be an object"}) + return + + service: ActionModelService = getattr(self.server, "service") # type: ignore[attr-defined] + + # Generate a per-request id at the HTTP layer to correlate logs and dumps. + req_id_lock: threading.Lock | None = getattr(self.server, "_req_id_lock", None) # type: ignore[attr-defined] + if req_id_lock is None: + req_id_lock = threading.Lock() + setattr(self.server, "_req_id_lock", req_id_lock) # type: ignore[attr-defined] + setattr(self.server, "_req_id", 0) # type: ignore[attr-defined] + with req_id_lock: + next_id = int(getattr(self.server, "_req_id")) + 1 # type: ignore[attr-defined] + setattr(self.server, "_req_id", next_id) # type: ignore[attr-defined] + req["request_id"] = next_id + + log.info( + f"[action-server] HTTP request_id={next_id} from={self.client_address[0]}:{self.client_address[1]} " + f"path={self.path} bytes={length}" + ) + + try: + out = service.predict_policy(req) + except Exception as e: + err = str(e) + traceback.print_exc() + + payload = {"action": [], "error": err, "request_id": req.get("request_id")} + log.error(f"[action-server] request_id={req.get('request_id')} ERROR: {err}") + + # Dump failed request for offline debugging if enabled. + if service.cfg.dump_dir is not None: + try: + dump_root = Path(service.cfg.dump_dir) + dump_root.mkdir(parents=True, exist_ok=True) + _save_failed_request_dump( + dump_root=dump_root, + request_id=int(req.get("request_id") or 0), + request_json=req, + error=err, + ) + except Exception: + pass + + status = 400 if service.cfg.http_400_on_error else 200 + self._send_json(status, payload) + return + + self._send_json(200, out) + + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 + # Silence default request logging (the simulator can be chatty). + return + + +# --------------------------------------------------------------------------- +# Entrypoint +# --------------------------------------------------------------------------- + + +def serve(args: ActionServerArgs) -> None: + if args.dump_dir is not None: + # Create dump dir up front so it's obvious where outputs will go. + dump_root = Path(args.dump_dir).resolve() + dump_root.mkdir(parents=True, exist_ok=True) + log.info(f"[action-server] dump_root={str(dump_root)} dump_every={args.dump_every}") + + service = ActionModelService(args) + + local_ip = _get_local_ip() + log.info( + f"[action-server] starting host={args.host} port={int(args.port)} " + f"experiment_name={service.cfg.experiment_name!r} " + f"steps={service.cfg.num_steps} guidance={service.cfg.guidance} fps={service.cfg.fps} " + f"action_chunk_size={service.cfg.action_chunk_size} max_action_dim={service.cfg.max_action_dim} " + f"raw_action_dim={service.cfg.raw_action_dim} " + f"dump_dir={service.cfg.dump_dir} dump_every={service.cfg.dump_every} " + f"http_400_on_error={service.cfg.http_400_on_error}" + ) + log.info(f"[action-server] Server accessible at: http://{local_ip}:{int(args.port)}/") + log.info("[action-server] Endpoints:") + log.info(" - GET / : Health check") + log.info(" - GET /info : Model info (run_name, checkpoint, sampling params)") + log.info(" - POST /predict: Policy inference (image + prompt + domain_name + image_size -> action)") + + httpd: ThreadingHTTPServer = ThreadingHTTPServer((args.host, int(args.port)), _ActionHandler) + setattr(httpd, "service", service) + httpd.serve_forever() + + +def main() -> None: + args = tyro_cli( + ActionServerArgs, + description=__doc__, + config=( + tyro.conf.OmitArgPrefixes, + tyro.conf.CascadeSubcommandArgs, + tyro.conf.OmitSubcommandPrefixes, + ), + ) + serve(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/caption_from_video.py b/cosmos3/scripts/caption_from_video.py new file mode 100644 index 00000000..8d04cd49 --- /dev/null +++ b/cosmos3/scripts/caption_from_video.py @@ -0,0 +1,270 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate dense narrative captions from video files using a Vision-Language Model. + +Each video is passed directly to a VLM server via a ``video_url`` content part +using a ``file://`` path. A structured prompt template guides the VLM through +a two-phase captioning process (scene analysis → dense narrative rewrite). + +The VLM server must support the OpenAI chat-completions API with vision and +must be started with ``--allowed-local-media-path`` pointing to the root of +your video storage so that it can read video files by path. Compatible servers +include vLLM serving Qwen2-VL / Qwen3-VL, LLaVA-Next-Video, etc. + +Example usage:: + + # Caption videos listed in a JSONL file (each line has {"name": ..., "vision_path": ...}) + python -m cosmos3.scripts.caption_from_video \ + -i samples.jsonl -o /output/captions \ + --server http://localhost:8000/v1 + + # Caption a single video directly + python -m cosmos3.scripts.caption_from_video \ + --video /path/to/video.mp4 -o /output/captions \ + --server http://localhost:8000/v1 + + # Caption a directory of videos + python -m cosmos3.scripts.caption_from_video \ + --video /path/to/videos/ -o /output/captions \ + --server http://localhost:8000/v1 +""" + +import asyncio +import re +from pathlib import Path +from typing import Annotated + +import openai +import pydantic +import tyro +from tqdm import tqdm + +from cosmos3.args import OmniSampleOverrides +from cosmos3.common.args import VIDEO_EXTENSIONS +from cosmos3._src.imaginaire.utils import log + +_PACKAGE_DIR = Path(__file__).parents[1].absolute() + + +class Args(pydantic.BaseModel): + input_files: Annotated[list[Path] | None, tyro.conf.arg(aliases=("-i",))] = None + """Path to input sample argument files (JSON/JSONL). + Each entry should have at least 'name' and 'vision_path' fields. + Mutually exclusive with --video.""" + + video: Annotated[Path | None, tyro.conf.arg(aliases=("-v",))] = None + """Path to a single video file or a directory of videos. + Mutually exclusive with --input_files.""" + + output_dir: Annotated[Path, tyro.conf.arg(aliases=("-o",))] + """Output directory for generated captions.""" + + server: str = "http://localhost:8000/v1" + """The URL of the OpenAI-compatible VLM API server.""" + model: str | None = None + """The model to use. If not provided, the first model served will be used.""" + + max_workers: int = 16 + """Maximum number of concurrent requests to the API.""" + max_retries: int = 5 + """Maximum number of retries for each request.""" + + prompt_template_path: Path | None = None + """Path to a custom prompt template. Defaults to the built-in video_captioner.txt.""" + + debug: bool = False + """If True, save raw API responses for debugging.""" + + +def _extract_xml_tag(text: str, tag: str) -> str | None: + pattern = rf"<{tag}>\s*(.*?)\s*" + match = re.search(pattern, text, re.DOTALL) + if match: + return match.group(1).strip() + return None + + +def _build_vlm_messages( + video_path: Path, + prompt_template: str, +) -> list[dict]: + """Build an OpenAI-compatible multimodal message with a video file URL + text prompt. + + The vLLM server must be started with ``--allowed-local-media-path`` so it + can read the video directly from the shared filesystem. + """ + return [ + { + "role": "user", + "content": [ + { + "type": "video_url", + "video_url": {"url": f"file://{video_path.absolute()}"}, + }, + {"type": "text", "text": prompt_template}, + ], + } + ] + + +async def _process_single( + args: Args, + client: openai.AsyncOpenAI, + name: str, + video_path: Path, + prompt_template: str, +): + assert args.model + + output_dir = args.output_dir / name + messages = _build_vlm_messages(video_path, prompt_template) + + for i_retry in range(args.max_retries): + try: + response = await client.chat.completions.create( + model=args.model, + messages=messages, + max_tokens=2048, + temperature=0.7, + top_p=0.8, + extra_body={"top_k": 20, "min_p": 0.0}, + ) + except Exception as e: + log.warning(f"[{i_retry + 1}/{args.max_retries}] API Error for {name}: {e}") + await asyncio.sleep(1) + continue + + if args.debug: + retry_dir = output_dir / f"{i_retry}" + retry_dir.mkdir(parents=True, exist_ok=True) + (retry_dir / "response.json").write_text(response.model_dump_json()) + + assert len(response.choices) == 1 + choice = response.choices[0] + if choice.finish_reason != "stop" or not choice.message.content: + log.warning(f"[{i_retry + 1}/{args.max_retries}] Invalid response for {name}") + continue + + text = choice.message.content.strip() + final_prompt = _extract_xml_tag(text, "final_prompt") + if final_prompt is None: + log.warning(f"[{i_retry + 1}/{args.max_retries}] Failed to extract final prompt for {name}") + continue + + output_dir.mkdir(parents=True, exist_ok=True) + + sample_overrides = OmniSampleOverrides( + name=name, + prompt=final_prompt, + vision_path=str(video_path), + output_dir=output_dir, + ) + (output_dir / "sample_args.json").write_text(sample_overrides.model_dump_json()) + + (output_dir / "caption.txt").write_text(final_prompt) + return + + log.warning(f"Failed to get caption for {name}") + + +async def _process_with_semaphore( + args: Args, + client: openai.AsyncOpenAI, + semaphore: asyncio.Semaphore, + name: str, + video_path: Path, + prompt_template: str, +): + async with semaphore: + return await _process_single(args, client, name, video_path, prompt_template) + + +def _collect_video_items(args: Args) -> list[tuple[str, Path]]: + """Return a list of (name, video_path) pairs from the CLI arguments.""" + items: list[tuple[str, Path]] = [] + + if args.input_files: + sample_overrides_list = OmniSampleOverrides.from_files(args.input_files) + for s in sample_overrides_list: + if not s.vision_path: + log.warning(f"Skipping '{s.name}': no vision_path") + continue + vp = Path(s.vision_path) + if vp.suffix.lower() not in VIDEO_EXTENSIONS: + log.warning(f"Skipping '{s.name}': vision_path is not a video ({vp.suffix})") + continue + items.append((s.name or vp.stem, vp)) + + elif args.video: + if args.video.is_dir(): + for vp in sorted(args.video.iterdir()): + if vp.suffix.lower() in VIDEO_EXTENSIONS: + items.append((vp.stem, vp)) + elif args.video.is_file(): + items.append((args.video.stem, args.video)) + else: + raise FileNotFoundError(f"Video path does not exist: {args.video}") + + if not items: + raise ValueError("No video inputs found. Provide --input_files (-i) or --video (-v).") + return items + + +async def caption_from_video(args: Args): + if args.input_files and args.video: + raise ValueError("Provide either --input_files or --video, not both.") + + if args.prompt_template_path: + prompt_template = args.prompt_template_path.read_text() + else: + prompt_template = (_PACKAGE_DIR / "defaults/video_captioner.txt").read_text() + + items = _collect_video_items(args) + + client = openai.AsyncOpenAI( + api_key="EMPTY", + base_url=args.server, + timeout=3600, + ) + if not args.model: + models = await client.models.list() + args.model = models.data[0].id + log.info(f"Using model: {args.model}") + + semaphore = asyncio.Semaphore(args.max_workers) + + tasks = [ + _process_with_semaphore( + args=args, + client=client, + semaphore=semaphore, + name=name, + video_path=video_path, + prompt_template=prompt_template, + ) + for name, video_path in items + ] + for result in tqdm(asyncio.as_completed(tasks), desc="Captioning", total=len(tasks)): + await result + + +def main(): + args = tyro.cli(Args, description=__doc__) + asyncio.run(caption_from_video(args)) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/captions_to_sft_jsonl.py b/cosmos3/scripts/captions_to_sft_jsonl.py new file mode 100644 index 00000000..ad715dca --- /dev/null +++ b/cosmos3/scripts/captions_to_sft_jsonl.py @@ -0,0 +1,197 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Convert caption_from_video.py output directories into the SFT training JSONL format. + +The SFT dataset loader (sft_dataset.py) expects each JSONL line to have: + uuid, duration, width, height, s3_path, t2w_windows + +where t2w_windows is a list of dicts with start_frame, end_frame, and a +caption field. The default key is "caption", which sft_dataset.py +recognises as a generic fallback. Videos longer than 61 s are filtered +by the loader, so they are skipped here with a warning. + +Usage +----- + python -m cosmos3.scripts.captions_to_sft_jsonl \ + --captions-dir outputs/captions \ + --videos-dir outputs/videos \ + -o outputs/my_dataset.jsonl + + # With a custom caption key (default: caption): + python -m cosmos3.scripts.captions_to_sft_jsonl \ + --captions-dir outputs/captions \ + --videos-dir outputs/videos \ + -o outputs/my_dataset.jsonl \ + --caption-key qwen3_235b_dense +""" + +import json +import subprocess +import sys +from pathlib import Path +from typing import Annotated + +import tyro + +_MAX_DURATION = 61.0 # seconds; matches hard-coded limit in sft_dataset.py +_MIN_FRAMES = 61 # matches min_frames=61 in get_sft_dataset() +_VIDEO_EXTENSIONS = (".mp4", ".mov", ".avi", ".mkv", ".webm") + + +def _find_video(videos_dir: Path, name: str) -> Path | None: + for ext in _VIDEO_EXTENSIONS: + candidate = videos_dir / f"{name}{ext}" + if candidate.exists(): + return candidate + return None + + +def _get_video_metadata(video_path: Path) -> dict: + """Return fps, duration, width, height, total_frames via ffprobe.""" + cmd = [ + "ffprobe", + "-v", + "quiet", + "-print_format", + "json", + "-show_streams", + "-show_format", + str(video_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"ffprobe failed for {video_path}: {result.stderr}") + data = json.loads(result.stdout) + + video_stream = next( + (s for s in data["streams"] if s["codec_type"] == "video"), + None, + ) + if video_stream is None: + raise RuntimeError(f"No video stream found in {video_path}") + + fps_str = video_stream.get("avg_frame_rate", "30/1") + fps_num, fps_den = map(int, fps_str.split("/")) + fps = fps_num / max(fps_den, 1) + + duration = float(data["format"]["duration"]) + width = video_stream["width"] + height = video_stream["height"] + + # nb_frames may be absent; fall back to duration * fps + total_frames = int(video_stream.get("nb_frames") or round(duration * fps)) + + return { + "fps": fps, + "duration": duration, + "width": width, + "height": height, + "total_frames": total_frames, + } + + +def main( + captions_dir: Annotated[ + Path, tyro.conf.arg(help="Directory containing per-video caption subdirectories (each with a caption.txt).") + ], + videos_dir: Annotated[Path, tyro.conf.arg(help="Directory containing video files named ..")], + output: Annotated[Path, tyro.conf.arg(aliases=("-o",), help="Output JSONL path.")], + caption_key: str = "caption", +) -> None: + """Build an SFT JSONL from caption.txt files and a videos directory.""" + caption_files = sorted(captions_dir.glob("*/caption.txt")) + if not caption_files: + print(f"No caption.txt files found under {captions_dir}", file=sys.stderr) + sys.exit(1) + + records = [] + skipped = 0 + + for caption_path in caption_files: + name = caption_path.parent.name + caption = caption_path.read_text().strip() + + if not caption: + print(f" SKIP {name}: empty caption.txt") + skipped += 1 + continue + + video_path = _find_video(videos_dir, name) + if video_path is None: + print(f" SKIP {name}: no video found in {videos_dir} for name '{name}'") + skipped += 1 + continue + + try: + meta = _get_video_metadata(video_path) + except Exception as e: + print(f" SKIP {name}: ffprobe error — {e}") + skipped += 1 + continue + + if meta["duration"] > _MAX_DURATION: + print( + f" SKIP {name}: duration {meta['duration']:.1f}s > {_MAX_DURATION}s " + "(sft_dataset.py would filter this out)" + ) + skipped += 1 + continue + + if meta["total_frames"] < _MIN_FRAMES: + print( + f" SKIP {name}: only {meta['total_frames']} frames < {_MIN_FRAMES} " + "(sft_dataset.py would filter this out)" + ) + skipped += 1 + continue + + try: + s3_path = str(video_path.resolve().relative_to(videos_dir.resolve().parent)) + except ValueError: + s3_path = str(video_path) + + record = { + "uuid": name, + "duration": meta["duration"], + "width": meta["width"], + "height": meta["height"], + "s3_path": s3_path, + "t2w_windows": [ + { + "start_frame": 0, + "end_frame": meta["total_frames"] - 1, + "temporal_interval": 1, + caption_key: caption, + } + ], + } + records.append(record) + print(f" OK {name}: {meta['duration']:.1f}s, {meta['total_frames']} frames, {meta['width']}x{meta['height']}") + + output.parent.mkdir(parents=True, exist_ok=True) + with output.open("w") as f: + for record in records: + f.write(json.dumps(record) + "\n") + + print(f"\nWrote {len(records)} records → {output}") + if skipped: + print(f"Skipped {skipped} videos") + if not records: + print("ERROR: No valid records written.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + tyro.cli(main) diff --git a/cosmos3/scripts/convert_model_to_dcp.py b/cosmos3/scripts/convert_model_to_dcp.py new file mode 100644 index 00000000..5c6067a4 --- /dev/null +++ b/cosmos3/scripts/convert_model_to_dcp.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Convert a Hugging Face model to a DCP checkpoint.""" + +from cosmos3.common.init import init_script + +init_script( + env={ + "COSMOS_DEVICE": "cpu", + } +) + +import json +import shutil +from typing import Annotated + +import pydantic +import torch.distributed.checkpoint as dcp +import tyro +from torch.distributed.checkpoint.filesystem import FileSystemWriter +from torch.distributed.checkpoint.state_dict import get_model_state_dict + +from cosmos3.args import OmniSetupOverrides +from cosmos3.common.args import CheckpointOverrides, ResolvedPath +from cosmos3.common.config import fix_config_dict +from cosmos3.model import Cosmos3OmniModel +from cosmos3._src.vfm.checkpointer.dcp import CustomSavePlanner + + +class Args(pydantic.BaseModel): + checkpoint: CheckpointOverrides + """Hugging Face checkpoint.""" + output_path: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + """Output DCP checkpoint directory.""" + + +def convert_model_to_dcp(args: Args): + print("Loading model...") + checkpoint_config = args.checkpoint.build_checkpoint(checkpoints=OmniSetupOverrides.CHECKPOINTS) + hf_path = checkpoint_config.download_checkpoint() + model_dict = json.loads((hf_path / "config.json").read_text())["model"] + model_dict = fix_config_dict(model_dict) + hf_model = Cosmos3OmniModel.from_pretrained_dcp(hf_path) + + print("Saving model...") + storage_writer = FileSystemWriter(args.output_path / "model") + dcp.save( + state_dict=get_model_state_dict(hf_model.model), storage_writer=storage_writer, planner=CustomSavePlanner() + ) + shutil.copy(hf_path / "checkpoint.json", args.output_path / "checkpoint.json") + shutil.copy(hf_path / "config.json", args.output_path / "model/config.json") + + print(f"Saved checkpoint to {args.output_path}") + + +def main(): + args = tyro.cli(Args, description=__doc__, config=(tyro.conf.OmitArgPrefixes,)) + convert_model_to_dcp(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/eval_utils.py b/cosmos3/scripts/eval_utils.py new file mode 100644 index 00000000..80f6c449 --- /dev/null +++ b/cosmos3/scripts/eval_utils.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for `eval.py`: per-sample metric computation and aggregation.""" + +import json +from collections import defaultdict +from pathlib import Path +from typing import Any + +import numpy as np +import torch + +from cosmos3.common.args import SampleOutputs +from cosmos3.vision import read_media_frames +from cosmos3._src.vfm.datasets.action.transforms import remove_reflection_padding +from cosmos3._src.vfm.evaluation.action.metrics import compute_action_mse, compute_psnr + +VIDEO_MODES = {"forward_dynamics"} +ACTION_MODES = {"inverse_dynamics"} +BOTH_MODES = {"policy"} +ALL_MODES = VIDEO_MODES | ACTION_MODES | BOTH_MODES + + +def extract_gt_video(data_batch: dict) -> torch.Tensor | None: + """Snapshot the GT video as (C, T, H, W) uint8, trimmed to its content region if padded. + + Must be called BEFORE the inference pipeline runs — the model normalizes + `data_batch["video"]` in place from uint8 [0, 255] to float [-1, 1]. + """ + video = data_batch.get("video") + if video is None: + return None + gt_video = video[0].detach().clone() + image_size = data_batch.get("image_size") + if image_size is not None: + gt_video = remove_reflection_padding(gt_video, image_size[0]) + return gt_video + + +def extract_gt_action(data_batch: dict) -> torch.Tensor | None: + """Snapshot the GT action as a (T, D) float32 tensor, or None when absent.""" + action = data_batch.get("action", [None])[0] + if action is None: + return None + + raw_action_dim = data_batch.get("raw_action_dim", [None])[0] + if raw_action_dim is not None: + # If raw_action_dim is provided, it indicates that the GT action has been padded to a larger size. + # We trim the action to its original dimension before returning it. + raw_action_dim = int(raw_action_dim.item()) # remove batch dim and convert to int + assert action.shape[-1] >= raw_action_dim, ( + f"invalid raw_action_dim={raw_action_dim} for action with shape {action.shape}" + ) + action = action[..., :raw_action_dim] + + return action.detach().clone().float() + + +def _parse_mode_from_name(name: str) -> str: + parts = name.split("/") + if len(parts) < 2: + raise ValueError(f"unexpected sample name: {name!r}") + mode = parts[-2] + if mode not in ALL_MODES: + raise ValueError(f"unexpected mode {mode!r} in sample name {name!r}; expected one of {sorted(ALL_MODES)}") + return mode + + +def _compute_video_metrics(gt_video_cthw_uint8: torch.Tensor, pred_path: Path) -> dict[str, float]: + # +1 so an over-long prediction surfaces as a shape mismatch instead of silent truncation. + pred, _ = read_media_frames(pred_path, max_frames=gt_video_cthw_uint8.shape[1] + 1) + # Match GT's spatial dims (top-left crop, mirroring remove_reflection_padding's convention) + # so a reflection-padded GT trimmed to its content region can be compared against the + # padded mp4 saved to disk. + pred = pred[..., : gt_video_cthw_uint8.shape[-2], : gt_video_cthw_uint8.shape[-1]] + if pred.shape != gt_video_cthw_uint8.shape: + raise ValueError( + f"video shape mismatch: gt {tuple(gt_video_cthw_uint8.shape)} vs pred {tuple(pred.shape)} ({pred_path})" + ) + return {"psnr": compute_psnr(gt_video_cthw_uint8, pred)} + + +def _compute_action_metrics(gt_action_td: torch.Tensor, pred_action_list: list) -> dict[str, Any]: + pred = torch.tensor(pred_action_list, dtype=torch.float32) + if pred.shape != gt_action_td.shape: + raise ValueError(f"action shape mismatch: gt {tuple(gt_action_td.shape)} vs pred {tuple(pred.shape)}") + return {"action_mse": compute_action_mse(gt_action_td, pred)} + + +def compute_sample_metrics( + name: str, + gt_video_cthw: torch.Tensor | None, + gt_action_td: torch.Tensor | None, + sample_output: SampleOutputs, + sample_dir: Path, + vision_extension: str, +) -> dict[str, Any]: + """Compute metrics for a single sample, dispatched by the mode parsed from `name`.""" + mode = _parse_mode_from_name(name) + out: dict[str, Any] = {"mode": mode, "name": sample_dir.name} + if mode in VIDEO_MODES | BOTH_MODES: + if gt_video_cthw is None: + raise ValueError(f"mode={mode!r} requires GT video but data_batch had none") + out.update(_compute_video_metrics(gt_video_cthw, sample_dir / f"vision{vision_extension}")) + if mode in ACTION_MODES | BOTH_MODES: + pred_action = sample_output.outputs[0].content.get("action") if sample_output.outputs else None + if pred_action is None: + raise ValueError(f"mode={mode!r} requires predicted action but content has none") + if gt_action_td is None: + raise ValueError(f"mode={mode!r} requires GT action but data_batch had none") + out.update(_compute_action_metrics(gt_action_td, pred_action)) + return out + + +def aggregate_metrics(output_dir: Path) -> dict[str, Any]: + """Walk `output_dir` for per-sample `metrics.json` files and average per-mode/metric.""" + totals: dict[str, dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) + for f in output_dir.rglob("metrics.json"): + m = json.loads(f.read_text()) + mode = m.pop("mode", None) + m.pop("name", None) + if mode is None: + continue + for k, v in m.items(): + if isinstance(v, dict): + for sub_k, sub_v in v.items(): + totals[mode][f"{k}/{sub_k}"].append(float(sub_v)) + else: + totals[mode][k].append(float(v)) + return { + mode: {metric: {"mean": float(np.mean(vals)), "count": len(vals)} for metric, vals in metrics.items()} + for mode, metrics in totals.items() + } diff --git a/cosmos3/scripts/export_config.py b/cosmos3/scripts/export_config.py new file mode 100644 index 00000000..60f0ef2b --- /dev/null +++ b/cosmos3/scripts/export_config.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Export config to yaml file.""" + +from cosmos3.common.init import init_script + +init_script( + env={ + "COSMOS_TRAINING": "1", + "COSMOS_DEVICE": "cpu", + } +) + +from typing import Annotated + +import tyro + +from cosmos3.common.args import ConfigOverrides, ResolvedPath, tyro_cli +from cosmos3.common.config import InvalidMode + + +class Args(ConfigOverrides): + output_file: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + + invalid: InvalidMode = "error" + """How to handle unknown field types.""" + config_key: str | None = None + """Config key to export.""" + + +def export_config(args: Args): + config_args = args.build_config() + if args.output_file.suffix not in [".yaml", ".yml"]: + raise ValueError("Output file must have a .yaml or .yml extension") + + from cosmos3.common.config import serialize_config + + config = config_args.load_config() + + if args.config_key: + for k in args.config_key.split("."): + config = getattr(config, k) + + args.output_file.parent.mkdir(parents=True, exist_ok=True) + serialize_config(config, args.output_file, invalid=args.invalid) + print(f"Saved config to {args.output_file}") + + +def main(): + args = tyro_cli(Args, description=__doc__) + export_config(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/export_model.py b/cosmos3/scripts/export_model.py new file mode 100644 index 00000000..d50773d8 --- /dev/null +++ b/cosmos3/scripts/export_model.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Convert DCP checkpoint to Hugging Face model.""" + +from cosmos3.common.init import init_script + +init_script( + env={ + "COSMOS_DEVICE": "cpu", + "COSMOS_TRAINING": "1", + } +) + +import json +from typing import Annotated, Any + +import attrs +import torch.distributed.checkpoint as dcp +import tyro +from torch.distributed.checkpoint.filesystem import FileSystemReader +from torch.distributed.checkpoint.state_dict import StateDictOptions, get_model_state_dict + +from cosmos3.common.args import ( + CheckpointOverrides, + ParallelismOverrides, + ResolvedPath, + tyro_cli, +) +from cosmos3.common.checkpoints import register_checkpoints +from cosmos3.common.config import serialize_config_dict +from cosmos3.common.init import is_rank0 +from cosmos3.model import Cosmos3OmniConfig, Cosmos3OmniModel +from cosmos3._src.imaginaire.checkpointer.s3_filesystem import S3StorageReader +from cosmos3._src.imaginaire.lazy_config.registry import convert_target_to_string +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.checkpointer.dcp import CustomLoadPlanner +from cosmos3._src.vfm.configs.base.defaults.model_config import OmniMoTModelConfig +from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + + +def _coerce_to_base_model(model_dict: dict[str, Any]) -> None: + """For distillation training configs, rewrite the target to the base + OmniMoTModel so the exported checkpoint only contains the student network.""" + target = model_dict.get("_target_", "") + if "OmniMoTModel" in target: + return + + log.info(f"Overriding model target from {target} to OmniMoTModel for export") + model_dict["_target_"] = convert_target_to_string(OmniMoTModel) + + config = model_dict["config"] + base_field_names = {f.name for f in attrs.fields(OmniMoTModelConfig)} + extra_keys = [k for k in config if k not in base_field_names and not k.startswith("_")] + for k in extra_keys: + del config[k] + + metadata = config.get("_metadata", {}) + metadata["object_type"] = convert_target_to_string(OmniMoTModelConfig) + config["_metadata"] = metadata + + +class Args(ParallelismOverrides): + checkpoint: CheckpointOverrides = CheckpointOverrides.model_construct() + output_dir: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + """Output model directory.""" + config_only: bool = False + """If True, only export config.""" + + cache_dir: ResolvedPath | None = None + """Cache directory.""" + + +def export_model(args: Args): + checkpoint_args = args.checkpoint.build_checkpoint(checkpoints={}) + args.output_dir.mkdir(parents=True, exist_ok=True) + + # Load config + log.info("Loading config...") + model_dict = checkpoint_args.load_model_config_dict() + model_dict["config"]["ema"]["enabled"] = False + + # Load model + log.info("Loading model...") + register_checkpoints() + _coerce_to_base_model(model_dict) + hf_config = Cosmos3OmniConfig(model=model_dict) + hf_config.save_pretrained(args.output_dir) + hf_model = Cosmos3OmniModel(hf_config) + + # Save model + log.info("Saving model...") + if not args.config_only: + # Load checkpoint + if checkpoint_args.checkpoint_path.startswith("s3://"): + storage_reader = S3StorageReader( + credential_path=checkpoint_args.credential_path, + path=checkpoint_args.checkpoint_path, + ) + else: + storage_reader = FileSystemReader(checkpoint_args.checkpoint_path) + state_dict = get_model_state_dict(hf_model.model) + dcp.load( + state_dict=state_dict, + storage_reader=storage_reader, + planner=CustomLoadPlanner( + load_ema_to_reg=checkpoint_args.use_ema_weights, + ), + ) + + # Save checkpoint + state_dict = get_model_state_dict( + hf_model, + options=StateDictOptions( + full_state_dict=True, + cpu_offload=True, + ), + ) + if not is_rank0(): + return + hf_model.save_pretrained( + args.output_dir, + state_dict=state_dict, + ) + + # Re-write 'config.json' to apply replacements. + hf_config_file = args.output_dir / "config.json" + hf_config_json = json.loads(hf_config_file.read_text()) + serialize_config_dict(hf_config_json, hf_config_file) + + # Write 'checkpoint.json' last to indicate that the model is complete. + serialize_config_dict(checkpoint_args.model_dump(mode="json"), args.output_dir / "checkpoint.json") + + print(f"Saved model to {args.output_dir}") + + +def main(): + args = tyro_cli(Args, description=__doc__, config=(tyro.conf.OmitArgPrefixes,)) + export_model(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/export_schemas.py b/cosmos3/scripts/export_schemas.py new file mode 100644 index 00000000..61bc018a --- /dev/null +++ b/cosmos3/scripts/export_schemas.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Export config defaults and schemas.""" + +from cosmos3.common.init import init_script + +init_script(default_env={"COSMOS_TRAINING": "0"}) + +import argparse +import json +import pathlib +import textwrap +import typing + +import pydantic +import yaml + +from cosmos3.args import OmniSampleOverrides, OmniSetupOverrides + +MODELS: list[tuple[type[pydantic.BaseModel], dict[str, typing.Any]]] = [ + (OmniSetupOverrides, {}), + (OmniSampleOverrides, {}), +] + + +def _nested_model_cls(field: pydantic.fields.FieldInfo) -> type[pydantic.BaseModel] | None: + """Return the BaseModel subclass backing *field*, if any.""" + ann = field.annotation + if isinstance(ann, type) and issubclass(ann, pydantic.BaseModel): + return ann + for arg in typing.get_args(ann): + if isinstance(arg, type) and issubclass(arg, pydantic.BaseModel): + return arg + return None + + +def _commented_yaml(model_cls: type, data: dict) -> str: + lines: list[str] = [] + for name, field in sorted(model_cls.model_fields.items()): + if name not in data: + continue + if field.description: + for dl in field.description.strip().splitlines(): + s = dl.strip() + lines.append(f"# {s}" if s else "#") + value = data[name] + nested = _nested_model_cls(field) + if nested and isinstance(value, dict): + lines.append(f"{name}:") + lines.append(textwrap.indent(_commented_yaml(nested, value).rstrip("\n"), " ")) + else: + lines.append(yaml.dump({name: value}, default_flow_style=False, sort_keys=False).rstrip()) + return "\n".join(lines) + "\n" + + +def export_schemas(output_dir: pathlib.Path) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + for cls, defaults in MODELS: + data = cls.model_construct(**defaults).model_dump(mode="json") + (output_dir / f"{cls.__name__}.yaml").write_text(_commented_yaml(cls, data)) + (output_dir / f"{cls.__name__}.schema.json").write_text(json.dumps(cls.model_json_schema(), indent=2) + "\n") + print(f"Saved {cls.__name__} -> {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-o", "--output", type=pathlib.Path, default="schemas", help="Output directory") + args = parser.parse_args() + export_schemas(args.output.absolute()) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/export_schemas_test.py b/cosmos3/scripts/export_schemas_test.py new file mode 100644 index 00000000..ac74b780 --- /dev/null +++ b/cosmos3/scripts/export_schemas_test.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import shutil +import subprocess +import sys +from filecmp import dircmp +from pathlib import Path + +from cosmos3.args import OmniSampleOverrides + +SCHEMAS_DIR = Path(__file__).parents[2] / "schemas" + + +def test_schemas_up_to_date(tmp_path: Path): + """Auto-fix like pre-commit: fails in CI, pass on second local run.""" + old = tmp_path / "old" + shutil.rmtree(old, ignore_errors=True) + if SCHEMAS_DIR.exists(): + shutil.copytree(SCHEMAS_DIR, old) + subprocess.check_call( + [sys.executable, "-m", "cosmos3.scripts.export_schemas", "-o", str(SCHEMAS_DIR)], + env={**dict(os.environ), "COSMOS_TRAINING": "0"}, + ) + if old.exists(): + diff = dircmp(old, SCHEMAS_DIR) + stale = diff.diff_files + diff.left_only + diff.right_only + # assert not stale, f"Schemas out of date: {', '.join(stale)}. Commit the updated files." + + +def test_all_sample_args_have_descriptions(): + for name, field in OmniSampleOverrides.model_fields.items(): + pass + assert field.description, f"OmniSampleOverrides.{name} is missing a docstring" diff --git a/cosmos3/scripts/hydra.py b/cosmos3/scripts/hydra.py new file mode 100644 index 00000000..71a231ed --- /dev/null +++ b/cosmos3/scripts/hydra.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Hydra CLI.""" + +import hydra +import omegaconf +from hydra.core.hydra_config import HydraConfig + +from cosmos3.common.config import CONFIG_DIR + + +@hydra.main(version_base=None, config_path=str(CONFIG_DIR), config_name="base_config") +def main(cfg: omegaconf.DictConfig) -> None: + hydra_cfg = HydraConfig.get() + output_dir = hydra_cfg.runtime.output_dir + print(f"Config saved to: '{output_dir}/.hydra/config.yaml'") + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/inference.py b/cosmos3/scripts/inference.py new file mode 100644 index 00000000..7ca018c9 --- /dev/null +++ b/cosmos3/scripts/inference.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3.common.init import init_script, is_rank0 + +init_script() + +import json +from pathlib import Path +from typing import Annotated + +import pydantic +import tyro + +from cosmos3.args import OmniSetupOverrides +from cosmos3.common.args import SetupOverrides, tyro_cli +from cosmos3.common.init import init_output_dir +from cosmos3._src.imaginaire.utils import log + + +class InferenceArgs(pydantic.BaseModel): + input_files: Annotated[list[Path], tyro.conf.arg(aliases=("-i",))] + """Path to the inference parameter file(s). + + If multiple files are provided, the model will be loaded once and all the samples will be run sequentially. + + Accepts glob patterns (e.g. `inputs/*.json`). + """ + + setup: SetupOverrides = OmniSetupOverrides.model_construct() + """Setup arguments.""" + + +def inference(args: InferenceArgs): + from cosmos3.common.inference import sync_distributed_errors + + with sync_distributed_errors(): + if args.setup.output_dir is None: + raise ValueError("'output_dir' is required") + setup_args = args.setup.build_setup() + init_output_dir(setup_args.output_dir) + log.debug(f"{args.__class__.__name__}({args})") + sample_overrides_list = setup_args.get_sample_overrides_cls().from_files( + args.input_files, overrides=setup_args.sample_overrides + ) + log.info(f"Loaded {len(sample_overrides_list)} samples") + for sample_overrides in sample_overrides_list: + assert sample_overrides.name + sample_overrides.output_dir = setup_args.output_dir / sample_overrides.name + sample_overrides.download(sample_overrides.output_dir / "inputs") + + pipe = setup_args.get_inference_cls().create(setup_args) + sample_args_list = [overrides.build_sample(model_config=pipe.model_config) for overrides in sample_overrides_list] + pipe.generate(sample_args_list) + + if setup_args.benchmark and is_rank0(): + benchmark_file = setup_args.output_dir / "benchmark.json" + benchmark_file.write_text(json.dumps(pipe.get_timer_results(), indent=2, sort_keys=True)) + log.success(f"Saved benchmark to '{benchmark_file}'") + + +def main(): + args = tyro_cli( + InferenceArgs, + description=__doc__, + config=( + tyro.conf.OmitArgPrefixes, + tyro.conf.CascadeSubcommandArgs, + tyro.conf.OmitSubcommandPrefixes, + ), + ) + inference(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/prefetch_hf_checkpoints.py b/cosmos3/scripts/prefetch_hf_checkpoints.py new file mode 100644 index 00000000..a7e7e33f --- /dev/null +++ b/cosmos3/scripts/prefetch_hf_checkpoints.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pre-download all HF models used by the cosmos3 CI.""" + +import itertools + +from cosmos3.common.checkpoints import register_checkpoints +from cosmos3._src.imaginaire.utils.checkpoint_db import CheckpointDirHf + + +def prefetch_all() -> None: + register_checkpoints() + + from cosmos3.args import _CHECKPOINTS_EA, _CHECKPOINTS_EXPERIMENTAL + + for cfg in itertools.chain(_CHECKPOINTS_EXPERIMENTAL.values(), _CHECKPOINTS_EA.values()): + cfg.hf.download() + + from cosmos3._src.imaginaire.utils.checkpoint_db import _CHECKPOINTS + + for cfg in _CHECKPOINTS.values(): + cfg.hf.download() + + for repo in [ + # 'cosmos3._src.imaginaire.auxiliary.guardrail.llamaGuard3.llamaGuard3', + "meta-llama/Llama-Guard-3-8B", + # 'cosmos3._src.imaginaire.auxiliary.guardrail.qwen3guard.qwen3guard', + "Qwen/Qwen3Guard-Gen-0.6B", + # 'cosmos3._src.imaginaire.auxiliary.guardrail.video_content_safety_filter.vision_encoder', + "google/siglip-so400m-patch14-384", + ]: + CheckpointDirHf(repository=repo, revision="main").download() + + +def main(): + prefetch_all() + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/train.py b/cosmos3/scripts/train.py new file mode 100644 index 00000000..909c7f49 --- /dev/null +++ b/cosmos3/scripts/train.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos3.common.init import init_script + +init_script( + training=True, + env={"COSMOS_TRAINING": "1"}, + default_env={"COSMOS_VERBOSE": "1"}, +) + +import contextlib +import os +from pathlib import Path +from typing import TYPE_CHECKING, Annotated + +import hydra +import omegaconf +import pydantic +import tyro + +from cosmos3.common.args import ResolvedFilePath, ResolvedPath, tyro_cli +from cosmos3.common.checkpoints import register_checkpoints +from cosmos3.common.config import ( + ROOT_DIR, + TYPE_KEY, + deserialize_config_dict, + serialize_config, + structure_config, +) +from cosmos3.common.init import init_output_dir, is_rank0 +from cosmos3._src.imaginaire.flags import SMOKE +from cosmos3._src.imaginaire.trainer import ImaginaireTrainer +from cosmos3._src.imaginaire.utils import log + +if TYPE_CHECKING: + from torch.utils.data import DataLoader + + from cosmos3._src.imaginaire.config import Config + from cosmos3._src.vfm.models.omni_mot_model import OmniMoTModel + + +def _validate_config_file(v: Path) -> Path: + if v.suffix != ".yaml": + raise ValueError(f"Config file must be a YAML file: {v}") + return v + + +ConfigFilePath = Annotated[ResolvedFilePath, pydantic.AfterValidator(_validate_config_file)] + + +class Args(pydantic.BaseModel): + output_dir: Annotated[ResolvedPath, tyro.conf.arg(aliases=("-o",))] + + config_file: ConfigFilePath + """Hydra config yaml file.""" + config_overrides: list[str] = pydantic.Field(default_factory=list) + """Hydra config overrides.""" + + dry_run: bool = False + """Dry run (no training).""" + + +def _get_config_overrides(args: Args, config_dict: dict) -> list[str]: + model_name = config_dict["model"]["config"]["vlm_config"]["model_name"] + overrides = [ + *args.config_overrides, + ] + if SMOKE: + overrides.extend( + [ + "trainer.max_iter=2", + "trainer.logging_iter=1", + ] + ) + if model_name.startswith("Qwen/Qwen3-VL-"): + overrides.extend( + [ + "model.config.vlm_config.model_instance.config.num_hidden_layers=2", + "model.config.vlm_config.model_instance.config.num_window_layers=2", + ] + ) + return overrides + + +def _get_job_dir(config: "Config") -> Path: + output_root = Path(os.environ.get("IMAGINAIRE_OUTPUT_ROOT", "/tmp/imaginaire4-output")) + return output_root / config.job.project / config.job.group / config.job.name + + +def train(args: Args) -> None: + init_output_dir(args.output_dir) + + # Create config + config_dict = deserialize_config_dict(args.config_file) + overrides = _get_config_overrides(args, config_dict) + log.debug(f"Config overrides: {overrides}") + overrides_omegaconf = omegaconf.OmegaConf.from_dotlist(overrides) + config_omegaconf = omegaconf.OmegaConf.merge(config_dict, overrides_omegaconf) + + # Finalize config + omegaconf.OmegaConf.save(config_omegaconf, args.output_dir / "config_raw.yaml") + omegaconf.OmegaConf.resolve(config_omegaconf) + config: "Config" = structure_config(config_omegaconf, config_omegaconf[TYPE_KEY]) + config.validate() + config.freeze() # type: ignore + serialize_config(config, args.output_dir / "config.yaml") + + # Instantiate + register_checkpoints() + with contextlib.chdir(ROOT_DIR): + # Trainer init sets the rank-local CUDA device before tokenizers allocate weights. + trainer: "ImaginaireTrainer" = config.trainer.type(config) + model: "OmniMoTModel" = hydra.utils.instantiate(config.model) + dataloader_train: "DataLoader" = hydra.utils.instantiate(config.dataloader_train) + dataloader_val: "DataLoader" = hydra.utils.instantiate(config.dataloader_val) + + if is_rank0(): + # Symlink job directory + job_dir = _get_job_dir(config) + job_dir.mkdir(parents=True, exist_ok=True) + if (args.output_dir / "job").exists(): + os.remove(args.output_dir / "job") + os.symlink(job_dir, args.output_dir / "job") + log.info(f"Job directory: {job_dir}") + + if args.dry_run: + return + + # Start training + trainer.train( + model=model, + dataloader_train=dataloader_train, + dataloader_val=dataloader_val, + ) + + +def main() -> None: + args = tyro_cli(Args, description=__doc__, config=(tyro.conf.OmitArgPrefixes,)) + train(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/scripts/upsample_prompts.py b/cosmos3/scripts/upsample_prompts.py new file mode 100644 index 00000000..823fa375 --- /dev/null +++ b/cosmos3/scripts/upsample_prompts.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import re +from pathlib import Path +from typing import Annotated + +import openai +import pydantic +import tyro +from tqdm import tqdm + +from cosmos3.args import OmniSampleOverrides +from cosmos3._src.imaginaire.utils import log + +_PACKAGE_DIR = Path(__file__).parents[1].absolute() + + +class Args(pydantic.BaseModel): + input_files: Annotated[list[Path], tyro.conf.arg(aliases=("-i",))] + """Path to the input sample argument files.""" + output_dir: Annotated[Path, tyro.conf.arg(aliases=("-o",))] + """Output directory.""" + + server: str = "http://localhost:8000/v1" + """The URL of the API server.""" + model: str | None = None + """The model to use. + + If not provided, the first model in the list will be used. + """ + + max_workers: int = 16 + """Maximum number of concurrent requests to the API.""" + max_retries: int = 5 + """Maximum number of retries for each request.""" + + debug: bool = False + """If True, enable debug outputs.""" + + +def _extract_xml_tag(text: str, tag: str) -> str | None: + pattern = rf"<{tag}>\s*(.*?)\s*" + match = re.search(pattern, text, re.DOTALL) + if match: + return match.group(1).strip() + return None + + +async def _process_sample( + args: Args, + client: openai.AsyncOpenAI, + sample_overrides: OmniSampleOverrides, + prompt_template: str, +): + assert args.model + assert sample_overrides.name + assert sample_overrides.prompt + + sample_overrides.output_dir = args.output_dir / sample_overrides.name + + prompt = prompt_template.replace(r"{caption}", sample_overrides.prompt) + + for i_retry in range(args.max_retries): + # Send request + try: + response = await client.chat.completions.create( + model=args.model, + messages=[{"role": "user", "content": prompt}], + max_tokens=2048, + temperature=0.7, + top_p=0.8, + extra_body={"top_k": 20, "min_p": 0.0}, + ) + except Exception as e: + log.warning(f"[{i_retry + 1}/{args.max_retries}] API Error for {sample_overrides.name}: {e}") + await asyncio.sleep(1) # Backoff before retrying + continue + + if args.debug: + retry_dir = sample_overrides.output_dir / f"{i_retry}" + retry_dir.mkdir(parents=True, exist_ok=True) + (retry_dir / "response.json").write_text(response.model_dump_json()) + + assert len(response.choices) == 1 + choice = response.choices[0] + if choice.finish_reason != "stop" or not choice.message.content: + log.warning(f"[{i_retry + 1}/{args.max_retries}] Invalid response for {sample_overrides.name}") + continue + + # Extract final prompt + text = choice.message.content.strip() + final_prompt = _extract_xml_tag(text, "final_prompt") + if final_prompt is None: + log.warning( + f"[{i_retry + 1}/{args.max_retries}] Failed to extract final prompt for {sample_overrides.name}" + ) + continue + + # Save + sample_overrides.prompt = final_prompt + sample_overrides.output_dir.mkdir(parents=True, exist_ok=True) + (sample_overrides.output_dir / "sample_args.json").write_text(sample_overrides.model_dump_json()) + return + log.warning(f"Failed to get response for {sample_overrides.name}") + + +async def process_sample( + args: Args, + client: openai.AsyncOpenAI, + semaphore: asyncio.Semaphore, + sample_overrides: OmniSampleOverrides, + prompt_template: str, +): + async with semaphore: + return await _process_sample(args, client, sample_overrides, prompt_template) + + +async def upsample_prompts(args: Args): + sample_overrides_list = OmniSampleOverrides.from_files(args.input_files) + prompt_template = (_PACKAGE_DIR / "defaults/prompt_upsampler.txt").read_text() + + client = openai.AsyncOpenAI( + api_key="EMPTY", + base_url=args.server, + timeout=3600, + ) + if not args.model: + models = await client.models.list() + args.model = models.data[0].id + log.info(f"Using model: {args.model}") + + semaphore = asyncio.Semaphore(args.max_workers) + + tasks = [ + process_sample( + args=args, + client=client, + semaphore=semaphore, + sample_overrides=sample_overrides, + prompt_template=prompt_template, + ) + for sample_overrides in sample_overrides_list + ] + for result in tqdm(asyncio.as_completed(tasks), desc="Upsampling", total=len(sample_overrides_list)): + await result + + +def main(): + args = tyro.cli(Args, description=__doc__) + asyncio.run(upsample_prompts(args)) + + +if __name__ == "__main__": + main() diff --git a/cosmos3/vision.py b/cosmos3/vision.py new file mode 100644 index 00000000..c7d16459 --- /dev/null +++ b/cosmos3/vision.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from pathlib import Path + +import numpy as np +import torch +import torchvision.io +import torchvision.transforms.functional as TF +from PIL import Image + +from cosmos3._src.vfm.datasets.sequence_packing import SequencePlan +from cosmos3._src.vfm.datasets.utils import VIDEO_RES_SIZE_INFO + + +def resize_pil_image(image: Image.Image, max_size: int, padding_constant: int) -> Image.Image: + """Resize a PIL image so the max side length is at most *max_size* and both + dimensions are divisible by *padding_constant*. + + Args: + image: Input PIL image. + max_size: Maximum allowed side length (longest edge will be at most this). + padding_constant: Both height and width are rounded down to the nearest + multiple of this value. + + Returns: + Resized PIL image. + """ + orig_w, orig_h = image.size + scale = max_size / max(orig_w, orig_h) + new_w = int(orig_w * scale) + new_h = int(orig_h * scale) + new_w = (new_w // padding_constant) * padding_constant + new_h = (new_h // padding_constant) * padding_constant + new_w = max(new_w, padding_constant) + new_h = max(new_h, padding_constant) + return image.resize( + (new_w, new_h), + Image.LANCZOS, # type: ignore + ) + + +def _resize_and_center_crop(frames: torch.Tensor, target_h: int, target_w: int) -> torch.Tensor: + """Aspect-ratio-preserving resize followed by center crop.""" + orig_h, orig_w = frames.shape[2], frames.shape[3] + scaling_ratio = max(target_w / orig_w, target_h / orig_h) + resize_h = int(math.ceil(scaling_ratio * orig_h)) + resize_w = int(math.ceil(scaling_ratio * orig_w)) + frames = TF.resize(frames, [resize_h, resize_w]) # [...,resize_h,resize_w] + frames = TF.center_crop(frames, [target_h, target_w]) # [...,target_h,target_w] + return frames + + +def load_conditioning_image(image_path: Path, target_h: int, target_w: int) -> torch.Tensor: + """Load an image as conditioning frames from local or remote path; returns (3, 1, H, W) in [-1, 1].""" + with image_path.open("rb") as f: + img = Image.open(f).convert("RGB") + img_tensor = torch.from_numpy(np.array(img)).permute(2, 0, 1).float().unsqueeze(0) # [1,3,H,W] + img_tensor = _resize_and_center_crop(img_tensor, target_h, target_w) # [1,3,target_h,target_w] + img_tensor = img_tensor.squeeze(0) / 127.5 - 1.0 # [3,target_h,target_w] + return img_tensor.unsqueeze(1) # [3,1,target_h,target_w] + + +def load_conditioning_video(video_path: Path, target_h: int, target_w: int, max_frames: int) -> torch.Tensor: + """Load video frames for conditioning from local or remote path; returns (3, T, H, W) in [-1, 1].""" + frames, _, _ = torchvision.io.read_video(str(video_path), pts_unit="sec") + frames = frames[:max_frames] # [T,H,W,3] + frames_tchw = frames.permute(0, 3, 1, 2).float() # [T,3,H,W] + frames_resized = _resize_and_center_crop(frames_tchw, target_h, target_w) # [T,3,target_h,target_w] + frames_normalized = frames_resized / 127.5 - 1.0 # [T,3,target_h,target_w] + return frames_normalized.permute(1, 0, 2, 3) # [3,T,target_h,target_w] + + +def pil_to_conditioning_frames(pil_img: Image.Image) -> tuple[torch.Tensor, int, int]: + """Convert a PIL image to a conditioning tensor in [-1, 1] and return (frames, h, w).""" + w, h = pil_img.size + img_tensor = torch.from_numpy(np.array(pil_img)).permute(2, 0, 1).float() # [3,H,W] + return (img_tensor / 127.5 - 1.0).unsqueeze(1), h, w # [3,1,H,W] + + +def build_conditioned_video_batch( + conditioning_frames: torch.Tensor, + condition_frames_vision: list[int], + w: int, + h: int, + num_frames: int, + fps: float, + batch_size: int = 1, +) -> dict: + """Build a data batch with conditioning frames and sequence plans for generation.""" + t_cond = conditioning_frames.shape[1] + video_data = torch.zeros(1, 3, num_frames, h, w, dtype=torch.bfloat16) # [1,3,num_frames,h,w] + t_fill = min(t_cond, num_frames) + video_data[0, :, :t_fill, :, :] = conditioning_frames[:, :t_fill, :, :].to(dtype=torch.bfloat16) # [3,t_fill,h,w] + if t_fill < num_frames: + video_data[0, :, t_fill:, :, :] = video_data[0, :, t_fill - 1 : t_fill, :, :].expand( + -1, num_frames - t_fill, -1, -1 + ) # [3,num_frames-t_fill,h,w] + video_list = [video_data.cuda() for _ in range(batch_size)] # list of [1,3,num_frames,h,w] + image_size = [torch.tensor([[h, w, h, w]], dtype=torch.float32).cuda() for _ in range(batch_size)] # list of [1,4] + sequence_plans = [ + SequencePlan(has_text=True, has_vision=True, condition_frame_indexes_vision=list(condition_frames_vision)) + for _ in range(batch_size) + ] + return { + "dataset_name": "video_data", + "video": video_list, + "image_size": image_size, + "t5_text_embeddings": torch.randn(batch_size, 512, 1024).cuda().to(dtype=torch.bfloat16), # [B,512,1024] + "fps": torch.full((batch_size,), float(fps)).cuda(), # [B] + "conditioning_fps": torch.full((batch_size,), float(fps)).cuda(), # [B] + "num_frames": torch.full((batch_size,), num_frames).cuda(), # [B] + "is_preprocessed": True, + "sequence_plan": sequence_plans, + } + + +def build_image_edit_batch( + conditioning_frames: torch.Tensor, + h: int, + w: int, + batch_size: int = 1, +) -> dict: + """Build a data batch for image-to-image editing.""" + image = conditioning_frames.unsqueeze(0).cuda().to(dtype=torch.bfloat16) # [1,3,1,h,w] + sequence_plans = [ + SequencePlan(has_text=True, has_vision=True, condition_frame_indexes_vision=[]) for _ in range(batch_size) + ] + image_size = torch.tensor([[h, w, h, w]], dtype=torch.float32).cuda() # [1,4] + return { + "dataset_name": "image_data", + "images": [image, image] * batch_size, + "image_size": [image_size, image_size] * batch_size, + "num_frames": [torch.tensor([2], dtype=torch.int64).cuda() for _ in range(batch_size)], # list of [1] + "num_vision_items_per_sample": [2 for _ in range(batch_size)], + "is_preprocessed": True, + "sequence_plan": sequence_plans, + } + + +_VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".webm"} +_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg"} + + +def detect_aspect_ratio(width: int, height: int) -> str: + """Return the closest supported aspect-ratio key for a frame size.""" + aspect_ratios = np.array([16 / 9, 4 / 3, 1, 3 / 4, 9 / 16]) + aspect_ratio_keys = ["16,9", "4,3", "1,1", "3,4", "9,16"] + current = width / height + return aspect_ratio_keys[int(np.argmin((aspect_ratios - current) ** 2))] + + +def read_media_frames(path: Path, max_frames: int) -> tuple[torch.Tensor, float]: + """Read an image or video into a uint8 tensor of shape (C, T, H, W).""" + ext = path.suffix.lower() + if ext in _IMAGE_EXTENSIONS: + with path.open("rb") as f: + image = Image.open(f).convert("RGB") + frames = torch.from_numpy(np.array(image)).permute(2, 0, 1).unsqueeze(1) + return frames, 1.0 + if ext not in _VIDEO_EXTENSIONS: + raise ValueError(f"Unsupported media extension: {ext}") + frames, _, info = torchvision.io.read_video(str(path), pts_unit="sec") + frames = frames[:max_frames].permute(0, 3, 1, 2).permute(1, 0, 2, 3) + fps = float(info.get("video_fps", 24.0)) + return frames, fps + + +def read_and_resize_media( + path: Path, + *, + resolution: str, + aspect_ratio: str | None, + max_frames: int, +) -> tuple[torch.Tensor, float, str, tuple[int, int]]: + """Read an image/video and resize it to the requested resolution bucket.""" + raw_frames, fps = read_media_frames(path, max_frames=max_frames) + original_hw = (raw_frames.shape[2], raw_frames.shape[3]) + detected_aspect_ratio = detect_aspect_ratio(raw_frames.shape[3], raw_frames.shape[2]) + final_aspect_ratio = aspect_ratio or detected_aspect_ratio + width, height = VIDEO_RES_SIZE_INFO[resolution][final_aspect_ratio] + resized = _resize_and_center_crop(raw_frames.permute(1, 0, 2, 3), height, width) + return resized.permute(1, 0, 2, 3), fps, final_aspect_ratio, original_hw + + +def uint8_to_normalized_float(tensor: torch.Tensor, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor: + """Convert uint8 [0, 255] frames into normalized [-1, 1] frames.""" + return tensor.to(dtype=dtype) / 127.5 - 1.0 + + +def pad_temporal_frames(frames: torch.Tensor, target_frames: int) -> torch.Tensor: + """Pad a (C, T, H, W) tensor along time using reflection/repeat behavior.""" + num_frames = frames.shape[1] + if num_frames >= target_frames: + return frames + if num_frames == 0: + raise ValueError("Cannot pad an empty frame tensor.") + padded = frames + while padded.shape[1] < target_frames: + pad_len = min(padded.shape[1] - 1, target_frames - padded.shape[1]) + if pad_len <= 0: + pad_frame = padded[:, -1:].repeat(1, target_frames - padded.shape[1], 1, 1) + padded = torch.cat([padded, pad_frame], dim=1) + break + padded = torch.cat([padded, padded.flip(dims=[1])[:, :pad_len]], dim=1) + return padded diff --git a/docker/ci.Dockerfile b/docker/ci.Dockerfile new file mode 100644 index 00000000..55ffff09 --- /dev/null +++ b/docker/ci.Dockerfile @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This Dockerfile is used in the cosmos3.yml pipeline. +# +# Build: +# docker build --build-arg CUDA_VERSION=12.8.1 -f packages/cosmos3/docker/ci.Dockerfile -t nvcr.io/nvidian/cosmos3-ci:v0.1.0 . +# docker build --build-arg CUDA_VERSION=13.0.2 -f packages/cosmos3/docker/ci.Dockerfile -t nvcr.io/nvidian/cosmos3-ci:v0.1.0_arm . +# +# **Latest container tags:** +# * x86_64: nvcr.io/nvidian/cosmos3-ci:v0.1.0 +# * arm64: nvcr.io/nvidian/cosmos3-ci:v0.1.0_arm + +ARG CUDA_VERSION=13.0.2 +FROM nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu24.04 + +# Keep in sync with .cosmos3-variables in ci_cd/cosmos3.yml +ARG UV_VERSION=0.10.8 +ARG JUST_VERSION=1.46.0 +ARG PYTHON_VERSION=3.13 +ARG PRE_COMMIT_VERSION=4.5.0 + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 + +# Install packages +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + ffmpeg \ + git \ + git-lfs \ + libx11-dev \ + tree \ + wget + +# Install uv: https://docs.astral.sh/uv/getting-started/installation/ +# https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile +COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /usr/local/bin/ +ENV UV_LINK_MODE=copy +# Ensure installed tools can be executed out of the box +ENV UV_TOOL_BIN_DIR=/usr/local/bin + +# Install just: https://just.systems/man/en/pre-built-binaries.html +RUN command -v just || curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin --tag ${JUST_VERSION} + +RUN uv python install ${PYTHON_VERSION} --default + +RUN uv tool install "pre-commit>=${PRE_COMMIT_VERSION}" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..6340e153 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Docker entrypoint script. + +set -e + +uv pip install --no-deps -e . || true + +exec "$@" diff --git a/docker/nightly.Dockerfile b/docker/nightly.Dockerfile new file mode 100644 index 00000000..f483687c --- /dev/null +++ b/docker/nightly.Dockerfile @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG BASE_VERSION=26.02 +ARG BASE_IMAGE=nvcr.io/nvidia/pytorch:${BASE_VERSION}-py3 +FROM ${BASE_IMAGE} + +# Set the DEBIAN_FRONTEND environment variable to avoid interactive prompts during apt operations. +ENV DEBIAN_FRONTEND=noninteractive + +# Install packages +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ffmpeg \ + git-lfs \ + gpg \ + tree \ + wget + +# Overwrite base image uv: https://docs.astral.sh/uv/getting-started/installation/ +# https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile +COPY --from=ghcr.io/astral-sh/uv:0.10.8 /uv /uvx /usr/local/bin/ +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy +# Cache python downloads +ENV UV_SYSTEM_PYTHON=true +ENV UV_BREAK_SYSTEM_PACKAGES=true + +# Install just: https://just.systems/man/en/pre-built-binaries.html +RUN curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin --tag 1.46.0 + +ENV PATH="/root/.local/bin:$PATH" + +# Install into system environment +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv pip install -r pyproject.toml --all-extras + +ENTRYPOINT ["/workspace/docker/entrypoint.sh"] + +CMD ["/bin/bash"] diff --git a/docs/Cosmos3.pdf b/docs/Cosmos3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4bc28271ad8a2f85a4a961af4b1e94184eabe955 GIT binary patch literal 617527 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 z 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

M#9u-rNOW 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 z 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�fC zuCnlvFRnuBs3#A@)Yps4DQ&o(O6v1TZ3!;cVI(InUc5w!=*9kP_QX36ex8_*jN=ro zXY;Z7_SxUQq?UAkvb3wGsot6HV7zxf!c9!@gM;ueHn^|JqcMk{ZWeR8YslT*mTQK< z-0HaOhvz^|65ID}f-X-uQQ7;ZC7eYGH3f9#}(UsT_;57#KfMh4KzTU*P6# zlk1_;^546rWby9gjguFyipMIdHcB zptErp$}i2C#kWT+s{1IQmkFLLPD>&xV11A2daA#i=vAm(M0)oS8Rg8*IM90+TcCCG zx-->G&rWGiBICdZhip}7}7ERLve@Y|6JT*VPX3J#T_;lR<{3l_dkm}>@2K||GT*30;-a1 zy~?H=xPTi3gfzg=3_-CSBLN9F2p~Mw#~|TW<`ULS0gDvq@8U+GyFgq1#D2kf`#$se zYyJJ}x#F37+Ed&8`tPdqX=<`Me~{89m@0t!w>m=~8l{JeS6yXA0lj;Kez<>xMrdfd z>{oD|>op*9^`TgR4E2xtr3cm|L5UVJb}~TOki!K8FXz|<6bJ-}59g3j$K3DyGeMTMCI*cAR%84(|Xn`;W?{Oo2F@mq{WXIFqY%FW=06Ymqne8h0_iNkdC%E@<(mTW zg$m{~@!$Zh8Qjf_66-|tk&=f40~wMZw+;JN*3{|{483(&7eWeSXZT(P0c{Sxr_3=m zk6B6cng)qQ^a0)!SO_ri1urTRJOrR14zPr31p9@mH#-gZkp=P{IFIV?!9jonka|Xx zug4%w@gaP)58?|6njb%-Kp*#yPUIyD0t857fPm76V+9(7{EdtYD@^-aJwGizpaVGa zWpNz<@YS39`^76hISB;V=KMMO<>nWlm08)zCGYX0@`W!a2DT4%w+9Ld?2Zo=1V8}z zB&{L?u=jk+pk=V8Z+Z87SOwE20)Y23j^-odXa4m-59sj2ilgK2H@YB@uL=$7@I&l` z>}%)?@d5hroBCm&_M0`}C;ILe@zsm0OYx&U}_vpc%g+{*>mQ-M_!~JUBoM9IR$TLPP=dNsz3? zgyYka0~#9ogG2DfqbaQJN`V2q??Iq!fcn?YLmNa0Abnd^0YCsWfBdl&Oi&OvcY&X~ zvJb=uXb$Vo$@u|>$Okm%MwtTae%!^_SO?se2nPKCdqeF%V1KLwA)JvQ$^5Dzc6sru z^V@||D5pjddNPFq^gyvtMqPVq63XlkdA%gqIdF2;h&D7YXNWZZXbEe<^36O*FPegm zQrh@2shJ(_FH+8h+4|%U(~;#feWN9zG%aE{;#N0>^KkZ@?eS?0WYQ)$s|giN8dMvH zGiu4W`O7fE*n|ITbHOChEbCBY!~0vgZaH|i57&pM@RpOz8Uct(nzQ^j2-R?GqBZ)0 znT0ThOdP9>)cKqx@290M%)s`Ne61FtI$VJ_7 z`XcVKv?y{~p~87!FCOuSPCFaJIjy#jFWne$v&RZ(Xlx8^TX57Kp2Dd4JS{ZDXoQgU zwj5@ah3y{}$Kgq$UazxjzuPPg=kIp#lJpXYae$YIVM%s8ZChc_+R5`t5yNn1UDRR( zeVDgfU#LySe&=>r^S766cH=rU7-DRgR2wVH>|h!jU&#mui_}19Zj(hCV6Es0XRRBfxmY=6zZ||ZW5#YO2km*lg8V}iaK7?MJk;sxc;;- z77qSHkB0$DkMuXAaQQ(w$anWf4Nd;Nd*$5e2od&c+)b=>(G+}n$~_Xa)#tbQL-}^w zq;6UrB;&NQJ3ULXUc%y#@+)q9P!wL$xD&ca{LH6;j$C(u;j#=VUwcol?tb<^1>0kL zxHI`|Og2$#kEi1o%-BgicJlcWXBZBg>3j;Wu)~saj2o=l7{)WRo9JN(eFaMm(;0g9 z&c4z`&5y?ggq(?l$5!O_i$!KUDOc%SDP9#-Te2z*O-gmm4PP0zQY8MdEdB$Cw3y2k zRaI%TWMv_I$mdOc_3mEDk&5vNKqNP)kiu%ejO$W&W3>9BcaHV9K?4iLpEcUnsP9;h z8_l4}hgn4D@H&6^1eOa4j@!%3qHkVVM^x;;@9k6ZaAOtoWAXCWQ;=ElN=Yk|JU~0b zKE`sLvh^0Pf%T;SVhJLv)<(s?sbwo*B%2R0+Xd(TXayCp#?N{9-ABZ8cy_2p^X$k@jjowyRmPJ*<+U-0_yZJ9TPXzIT-;Y zW73DPi+e=`J%?Qy=Bo(VY4M(|Azl^i=gQ!53wyHCb{ zNvWVns8493r6d9P1Fp_trUpcDs`<+>=QTUK@cH4?uZ`P9-$QP)X_xM*)j$DZ+<0!V z32Y@fAVwl|@JTp!PCvmKd92?5z0*jT%lMs|gl|OX5{{rg9XESzW zFJ=7N2uNihci@^ucK58S1!jHoQ_@7x&A@c{8}X$ zE()t5TH&e=c(X-b1Nf>5OTvE~d6}(4Zr7Cy6yAmG->a?#iH5640+Od1o<`2g zYvTtOy!L5;r!u;=Yfp0MfndIK5qd7C1BrPW$wn%=R{Hq$^f3C2sDfuTwzcJz@fi>(Q}7K)qg1&N$<_|y1Ma?#j9q|m9el%!>R^^V zB&toH#M~4)om9A<(Bz7cmI2kn!#0EFTPj&LIySXi;FTw^4lx!-cTwVOdtT(%d4@3J zUY4`UN-6$KjCfeYIG9XDuY&JtcK|34NR)N08`UCP)~`q8$JV^IXOwDy)}Ds!7@n2- zc54gmS62Dzl&{(M$GaW;Mv%L7p1TTlgYxR5@H6S^=Bb&DXl?owV_qhloqynk_9JZ5 zy?8kf*W2lrBOGi!Kh0%azXQn~eJh@iC*(WSuXQLRAmSqrsoucmLf9=AKaA#Waziab%u_2jkZ^3d!l>X07tQO$1DJ zww%{tXL@;4GItQ_QgsfS1g}*B%SeP#_^bC}lNI>K%I=+6EhOm>%)@*Qf3za_q%1Tc z@^S=`8N6!uZyxTmEW)BOl$JzuQfkQ9aleHEnOQ$7DEkf|ZQsGw-CDrF+4tsph$SD8 z!Hmrpuunh+87z@~e!Co4Z zmcn17Z2M)!ohNQ~t&G@puzD!_E~H`D46Lfh&K@M(ncUjhgbVg}H6rESqvdJbLG3VM z#Uf%FZZHxnxn38q>wcr7tn`k+84waX5$iM_t0xEu+(N%MH9bKZ`x zzUF7TVa(%K+KMPu*06Rr)peuJ3=t(8nCytHySC5y3qfRmKf&e4i!ATFJu577#!W<( zrjV{|Rj$#z^I|RLFXw z4w|&P@VyuZs`Cuk;k>aXOmavO<-^;!N8fZHo(i}*Zi4OHHm8s*5h z65(UoH%i!Kc^j`DX~c^1ot8=}5g!Z2DR$d#E`F7nS-Bg!1v4>rNP5&}Gz%BEytch# zg4FYxClz92?yT^sm$GTR3zH&v;S7?l&pchatY!BkqLf?297>43NCt!xk&wz z7fH*uxreG?;1y6EP%dD095QQw!_Z}2zZngFW?9)3kGAJx372jP+K&;hZe0`J{^7zA}*K$uo;@+HCa6U7zre&OI zu)W^0fR&(yz)kVikswPVN~6=idC`EGvi9m6m!xcdIa_;WEYv`q+e7;{#q@YdNbWwl zP%qE+e0a5CG?H_=&)yPLIMlMQ#_fq`JdgmlQ)i_~Mu1wc=Xh3jFWKMXVG0neun6Lrffi-RVZ9; z5^vWV#yU9Dr?q|&I4&w;u7b>CxIvJz;0cAB=v2{Pc3@I*Loe_fwEaLFEBFv9XgT~AQON|3G>79UIvoB2^!|Pj7L(4UGZ6L0U4r6=O= zs8VyE=N{%4Sx1 zmalEbUKFVdmn$gMvN8^Hr{u2kw{T5ZJ~s`v@W}@ysV04XHCxgJjWbom(>2e3D z)M7qYlOQd8_Ntt(v&_)q69rO-*LSXqhKY0M3JWb{Fnj3iSedaJiR+)9jw$s}kb~?F z`R6c$QTC=sW0hn{7Ejc79Cq@+J17ciB+x&pd(GFl;zV1T$AL3@pma-m zR{LWJRQQqaXMf*9JO*%y!`bpZ2T;Nu@5@qNv0SJapwjmKeNCJ(?s;uLFAlRxZ^;B6 zS}w4gigcOmTsD-wHc|4XT_CXr-zwp_BfkQG{If*w;8VYYDm(gdPHM*dNCA+%fbFEa zNw)r58`}yf?IgQ9sTE~^8#aFfmx9vghtcPRXH={!h$lW6XLOW3^5Y2to0GqF%Fuay zGEkv{$A#N!Yj!k*=qK9wP7>vR-Iat05%cR;EtX3yzbq_)esY7LbM(?Q>5qE)zJyo- zCI*ZVWXd(A_)rw?sBtMFKb<2SX`jfAuCBP^-5;pD@O*a4Zm%eP65?70b);d1sr2yI zsvia9S^HC(DYxW#fO2%cGQU_*{nqxBCc2B{SQf`f&6GJDpeb%DrmYnl`26BZagJ9s z5uqVX;oDAp+w*dV0dY%QdX@p3a`DW@3i0W<7@?8Gk0dqcp`?-vx2c}6VYyo{xt)m= zeO((xJ1vd&piZeoNkRK8M6FIZl#h)m;ZylmE@~w#1>9P9U`O&#E~aB_;y`|6t}RS& zpD{hn^zL520Zd1T>_jud;y6|&qAF{=!;L!lG@d4-IaWK<8+=-MSDDl(0o zN^>X;NwSuYn3QsQOd36yoqt`2oyDK@x_nqKwuzfU{lu2}7i)mjygmIR1uAm7Dgz%`+qEtVX|Tgb#hjk& zK}e?4cJ+G7ckEJqWL6Sf`GvljQ%H(P!@RG97tFMLA$l=Rd}NG8 zRSdTmjJaTgK8MS=jpyM&Q&VgWbo{j9-2&(@T12Ub>4!TI_y)R?y&5uIrvbCN?-bwl zP7)w2(naB|PcO*p8fo*jEX*VnSB)V@1iZVchtCxP&Yn}yVfTU;HQACxfNtXn?OFx7 z&_`+cS?u;}^!+AK_y*XDO|DZR9rLIJT;mIutWwoe|BQT*wiY34@M<&+$k6%}(+4SL z9v6VohR+|vgcnMNn4LBg*XXPmN7JLtI zO;uHk%}N&UiryUfmQ_L$uOnkoeJN)JxEcq9P>TNq=7POfhe4Qa>nq(w-i~MNcVRNH zpw}3u0ri_A6g-dNDoRAwz~4`26{abD)KlVlA_x6D$(pBdbJkR%*}zP zD4FV@p^?ctq6V2@g$JcYl?k92qsL`WlGMDio{;kG4?=7|`z5ZJG0_w3{+t;%w2HC) z*<5~W9wtW5ryi(N3*r;d%-2jeCj5UizcpcDaiAu%clXaQ=^nnestc=gUOLpte_H0j z{KCJf7)t#c`>Hfp*X{>$b52VvOZrakRu@tJCcK*W;%`ZqxT{M`Q~kG{gl&AbTCjq> zG^Z^o>2XcZPN)gcCMhTuekfVfCMyMTZ=QEG;xj@k1~fV4Vg=6H<39E2On!b51`PJ7 z{6nWpOwEb9(xO?_CT#wUKd9Di^E75_n2@~uK4qWT&6wYe1QBz`*dsZ5o`vQn3$IQz zWNcKjD>5Yyga_BM4V~o3Af#Lx+1jh(9A4T9pGP+H6m`UqW<;c0&H@(u2)Wuh1<9oP z3x@2LTZ#>GuTlz-$KYsLMnZLMDm=xETBhP`58fULtHiOQO-8Dyj#r9??pLH|e!_Hrdgz@PdIi0!q*>n; zHb0hP|9QhCvR{$^K&@s$TOG!h_JE64&Q3;!52s6qk?D4V>o&K#R3*W!H+D~bf94_Lz@?NN7vSIy_2x~ko|3f)Z4U`39_MAhEsHN=+zFMd| zsKz#!WhM>nb*_TQtDAXuRwo%(#@|V|-7}rLOk@-c?Yn2j^U*KDb=8l&lk=)B>)3Ot zH^8D+NZVnbd&YDuD%GM_ z{r!VovzY;OnI=C19dK_pJJ}8DWK16jjkdE@B~6-Q_3lwPb@%f!w3Mz~=_C*xgw63}TPM^GCr8NO<=Hz63CTOe<^7_bWd z{lN?SI*8m#SPvb^5psmr);(oa9}_4%KL4*eR)|@aJaZi@t zYiFR+CAu#|PL+3FU`X1r6z*)gq@8~5NI#I3Y4R@kqAb&>v)w^>9rWhyy$;lnoLRJ7 zmB-;&h>#XFksf?mm|9~M^_QXlIt6WWSSm9lSb-j&Et+BGQ#39XidbXJTRVx)WfJ)8 zf_^McKSlw(6mcE{eKAE>)i;B}{NjTy(|6X{X=zhU&&jk{5IApEwlUk#9rhD6MH4FbsPs3`&Qe{+tzQrNw2!hB>W4mmLwo%38p&)^HIj$ z$xrNOu}8YjWrhp8*eacy30@0z1)EIu3}-81|-yBzzlp}iXaroJOg14!{5wGP}@|n z1c)oe>IX7hlExnwCGgH48ddoXyo&&dDq#k5n zh{7Sr0g#4B??^~!FTfEILtO^a(15hhu>mxMC~a~)UP5Yw1aKu(Odxg-s4t+To&@9s z&~)5@mL!l|nmFkxpbQ{Ej)RARk1mXafdED*7`jL)kVx309t5Ed7@l0cUJj-R*=%C8^*CCT>_>63IrCwTLdELkBvGLUL-ak#4%++yh0?RHwB1L za6CUChCD8i&59=gBC6a9`LPrNBOR$UVnL%=BooZMnOCxTRD=jaq_pJ3*A)Wf6zNB8 zz6=~Y#PE^DS{XLJpoh09CQnBCK?qSl)7rQll5o0&z6~G)T)GO_^j0+7o&i9Z6DZ0t za4r-h&rLDqvS`#gZ^`=|GY5JhxYfzUAe+gmv0esrZaZpGg zgE1L0M%El;4wDQuCwpe2$>0IU-hPlhdtZrQ2vlSUuMC7#Z|J*F zaHa&72B3J(qFbMuO_=Ls;h4jCd6LoN6e|j(DsVmyfuX5onK@P*SCj-n4B7T5TbuV3-z`DQ^urzabPzigwDebm*5ykiq?MK#8H_9{MpByR zB^_)Y)uAIyWu^-~tgQA)=neYtCEFeH<~`-^t~(Z33#p}x+clJ~m=d?Gd|#DYDx1S~ za4cuNG;OO`>*ffo3P{M7d-OFg8rOH`cgL4?cC6!gukH z8_SQ?nEkMVld`#$toB=&>E1t*-`N*|So+8@)o9?oOY!7hZZoaJMq`5VHkP74mg3H_ zP)x7-W^vzhRxD2DC7xKE2^BZF;%3_G>ekHJSwP4e7dJjf^irG_Kfe5*s@74-zuqiL z+RG^Gc&8t+>)9POaw#f!M{CVh0*LxM`kBkFEsf_d-6q?AkFPF1%Zwccfpad2aPoM6 zCpYC$N71(4&aymo*4ja-Utc3#nDIG`wHBS3-sor#jusmzTG7Z^yZ!)7Y(9SKUPm_% z=$%N+TpB%7ABD?Cfgf^#xn)0AH*TxWdwz7&HFb}X!Cd2|YR8MSY2lr`Elss^HeM21 z&y!u*oKKlfh2AdJR4YC5bKOY7k3;af*wPbg_~#g=LMe&2HJ{-35=AkYJWaXCtGD3% zY=4vUmNl)~fq$r$lknI)FT&g&RvV1gxozY|yk5SQsNr^uT`%MhnYzXLWP=W6XEjY4 zS5;pa@20lsZ{)8h7E5vF?B%DhSS(*He#6V|@Qnt2chGa8>(L$pd@#*!9rsRano7#4 z#dr)>@O)O%ii@a@+f}7f<8_x$<~SOkC%BJVc0aGUe4cyab2OQMU7Bd#3MrBHzKFjb z$!@j#Y;2*Xp%14$O_XJP9N9Uu$GSHj)(niDm{}`4Odd>AWivM^%LL|vDb(Ef{&ps# zRw&r3kmxW%qPvxY_OYusKmPIYLBp9bv(Z#mr=^m+@UYCFhmAUm-tg&muRi}ySU*bm z_7uNyR|8iZXu7-(-8|un`}w?Qu7m1)^#B+9=^4o8q|>vH1TWZrUSYC&Z<$6%bq?aO zmh9j%BQGLGBL@}lbcyL9iE@c@ujS8C=6@((zKBD8z2WDmOgiSWceHV9f8Jr0hQq~z zsW?AziLVMS+Sd#-N&_p)Yd1g>k9)qzOj+arR~KLzQ5 zvei?|aGEF38#?FyQ*k++$GYceHT>FZHjG;b$jKEy=bClDGmluCB;~ih;D4gcTZ;2> z9VS}F6!|#$a8=QAf-ORpz5iuh~tQu4!4po4ph7TXWJEa9wtS=2mag*!5uQ+_Px9JS($y z0X(7f0+Z)VE!EETt8hLWMtO}z>t_o)e>vU0>iEfj6RkD#ABG;b|Lf4h&h+0?4ifEDC@>Bh71GYPH>Lt&v-3uq`qs zf8{;hy6yT?X?M+^)G~r&eWU8Ux`9HH_~J@ul?x|;28JePrsrQ-oQVa^%*^bckpLST zC^Q9Wab#p^Pi}VvbOdSvAPZP201g06?Vpm70U8TLV0OEAZenS-50uYV@ZN@)Z)a>` zWodm5qR`gX;MT;_0C3mU(E-`T*~P@w0mbx_R15_Iq(kEmz$}eTARr^DDJ`Z5B|uy9 zhD-q349tbO4v?%HV@o3o@Q_L-z->+d+c&iXY4C^-YG7nYTJP)k%qu$8)p+ElS zSMvMJ8V*-si{hZmuqS zeQm?@dmH(i{BZl6E+TT2EV6v|(+2r-om$V_z~0i#3|`*(HCUFAbpDI=bFIeK_$3ei z4WIgZP606ayNDXClTE`5z%>>9YlV2?$NNis{@qKVsOS}^4^ut01pvk-X9fU`jn3_# z6|;HTyO-I{#R+(=^CSDg-|`jrb87|!#OqJyU_JRl=H0JmSyO=8gCyFN67SNq82XQf zrzxQR^R#o8uLY-_j)kMQzg@9^+(nEq1A+m4rFS#7)e?_242T#@UREeM@SJ+DCpAVj z?PJ}xqXMOeJ@8ydxa4Rni$Z8eWT(rtvI>-9S+>^XG;3{&I|-stqEQGt$qi1R*m|gr zj(1gT@l-y?)Y6OK|LMX?(S$}AvN|F}?`3UZ_%7H5!H&`nh8BVf2p49!0Sc?GX2hj6 z;Y_o=u;#%9-rjzoU68zKmy*{om=!?erf%bRG>dn%t%Z5k@bQ60Fm~nBg-6Nlm`LAwvhyu{U z0Ib7?4}WHOS=DfcF@K1ZmCjTmZ4;;KSsFxGyuoRpnC%5|OT` zRq&|h@xH~gFq8irtuU``k;o!)TB8j?uMQc zc+W7lyv*tSwfgL1GDJ0k$nV2dNMr?Tld9|8Sw9$0WeYh=IM%sqf&)s8%YXm#s=f_?t;-Mt{gIE% zADIOiR|am^44~sMR}zXqV!l@c9gf#!^5@Ix^y=2kgIrf)SC79;#@NaPb+?cMV*tN+ zj!(~%Xz@@Pp?y+C)4i&Br1+esmU|1fFLpm6-gW}zwg|TjbA<~#o3w=Q1Vw-OyyxvM zZwvu1m#gl>R%iZ1s=P~Z|cVv>-nrAZg{8>t#f|2%m=1g)~excFkpg>IW{4S zbL#3Ol&Pxuxlldhgix$GuD;Gj_{(&yoIS$HGH+$6duvQ ztd|J^Nuy+MPAm&tny;mKF*h28da~Qo#iY09XmaKFcc`q8ZFpbgbpj@t zNux~^6>0X-D7UwJ-7klSjHWhiIAd21fLMu9ot36vvCY|D@uL`LaeC91P4_x`;4hC5uD@uW%N=rhEQmcLM-thPQ?6J)%>{O4xqw~ zrzCdJ%cXRgS$xcPgb{nT*!~UCbFk2{sXV+!)Jc8c$NfvaE2cXC{4j8V=+j7ErryBT zMp{({?A4-qMD01ySKzR00CB>*q@)(_#oS6Ldsa{bp6a`LM7DD@mzU;nT6U!}bdiZu zA2}38HCoX&V_cge%M@_xMccQnlP0`f%`ud4-Ab z8~3fLBt^*+fssw*4=Httj|hIIfB3Vca60fl@Jq)%*$*`r>i z&3mW&Y11J%?)Gh(nF!?%)cMO01VaJrMj0pgrZ*gw6r^W zh6-IvpM0faJdXQXvWq-ghjIYCbLX%-*e7WOkrg-)2%1fnTGbkH#>z*j+V>e4alB56 z>{vZ4bU`UczGy=jsj1UqieGNyv>o}$R1+jLy9kLZ3=T4^R6RFiQxyqKm4_wA13+-8 z-LBNm-t3A~IuH%w*ao$W@K49ieFje}NS43dc9(GXH(f6KUv$pWPsISm1`&L)tRo-W z-*;8SayxdQoeuMqoO9uFYL*t*Qkiq10PdS!IN5{`KGLh?E~os3gn9PC9V+-DgA5bVNl?YOKw1=3b(a3D9UH3yu5-}(s6ABuWdJBHQa7pZ0IK)B;8g+HD z5m;0DjBA9vCpYG=Zk1*8%_Hd*Z1Gz6NSXc#2oEYXC6^Eab4jR8vuC^_8<_V>5^3Wh zG1ps130qZOSwH33UKT6c>2hwr+9smw?--eI6C4*J@n!5S$iNqYE5Oy{JSiCCqukkKNOw;FvHFk97W?ynYgT z6@Esii*)3x`HQ?{@gAN=WfgQp8ls52^+_vt)7rL?K|<>=_FA?R^;d%7_I^*A_t#DX zFu;(=llyi4YIzAA=kp%;P(D=n&imy<#Q zK!w>oOiA!ERc$8uvJg3rpj_?*qSRtvBf*^p`dSr&GJEwH-Bxs8$rHr$gxcA7^j|f0 zwO_oG`C1DqfR$|Xj89^Lm)b$DriwdoMh!~MZ7SMOGj;1?g6Q9TTQY;y{;CRAa#0hkV;5XwaR~T_!#h*LQ zp1dXzQ~^7vAC#b>B7Q?!F-ne6#2`8Rxt2_5p7r}5#fYVNy?9iwD>$mi7(CUbAh}*r9p&nJGJ;V!*P8GY z-Y5seW3`{fC_0kEpw_}V_N`46pI%7cm29LU5~t*yNb9ieBLYSDa=4>OU%7n{ioOPm z8%Z^ol5|WB@ox-G2oKH;kghu;S4ylKn2}mH!huytVzLCYsvp^u9h5F*ilK}C(cXQc zmchI>2iNgjTu=)CP8&$AlN9^Uw|~hn|Ij~(WA1hZLs7Aoei)1kJZX!w6v6LFQg=Zx z^&OER_-3Cr5z7^vJ^Hx2)80@Fw+ce&Iwer1W@c;?&Hx)hMv8)3IoCzZjz3Kq|tLm5e2!MSo zv+DUBRtltejMUt_ZIo?Zxvc0ZbxJ;yySMOE2w2m3$WHoe?WZL9O_W1NXR4zhEBFWN+EXA}lwm=T&3K+E6=UWd63CdObdjtrL90Z|Mynx}7)P`7 zoSnag)gb9nED_Y3g!NgUy*frFyv*9tAPXuRuQA#U;8{wk(o^q(vF;@Uda#h!yOf(E zyLmo6-;&t4OGB}n;6=bQh|Ai6-56;8)nz4dF5ncD-L5d6=wOe%Pwnj<5Qc`|3#nCFD!N-@cvxf*2dzRgFk;Ho0zz7RCSvD?}Pfo9n)%d@4<%YQe)5{D1u*LmnK_Z4y zV;V6NkVd2STuL3Nvhxvd-nqf;vA8b_yF%H-rMK3?>J=$!z;dy=vHRsIy{hoRh2qdC zg?;hZxi7L~1$^^?y`&`;0j5>sFj+r%5aN`$c2gs^6Gx5AHTQf$xfq?3tAB2-Ntvu; zO~S+j2w*Y%1b-u<6?WtsmRa-ijuOQNmN1jP0roH=O6dVdH%~BlZZc1t_1KxBc|C;> zt8aP%_F`FRWNU=+;YnYA)<^|CgG#lW@OW%sxn?y+)1oMO?g349Q>AM8VIm(MxPx~4 z?ob|8o{?#B@lS#UX~_BAZdf`JzT=}@l1rjnF#6O_P?8Nag7FO+@Vf0VUmzh0pY5}- z1I{JcY%J8z&QwEAmQOn@ed)7r)#Bo;wDgrO;0;RdVckdIf}?INS4Oeu%OW&mSKHMq zY_eC|rh~f;RAp=W#l%V^Sj@4OrsR6{sLX>v3MI`hHqc$$#Poes^ zspsR#%>J;$bJU$#|1F1{EmNJC3!3LVsB3Pwt?cCyy=;}HnQ4tYS_k~MCd`A)s5#2V za9I-pIbf#e*o-jk2J>2t8BoR)FVhM~jOuJX#@7t`M@f9mRaCPjn{G3Fn^0dlO+)~y z0%ec!XDtFW@E_foBYUXW6JRO8=3^aPY=aAt(!YAleI771dP|TG1`O zEVDO~1%ciTp&e?=TlhwN!?3oDB{Et42pe-#J#3%3j1e@+UNiz#h_CUTs26k-YiG#| za+C%80-O-*VI?G8Dp&Z+u_?j`s0#ksOM`>=6l1` z3_|5HbqL^{MRN z&^$SU0ZeaTLI|tVrJl~sf+lqG&ju?fJ*q76&LYWIiUi+QT-9&p6@ zdvz_tx{3mM0lP4#u$^)P#_TbMwXFK9I6&%O?!g`4YbHM*H)p+3sw;IwZ;nMK1t=dt z%ft1DhefTmwKr&|vq18xW$vUU>5yBxwDRnb!+x zp6udf^@?=jC)=zm?0|o0KsI$CD__YVdsPltpt3i0AlnmdvG4=hCqC`q;*9Z@jNvuG za0id5uAW^k!r{_^G;oM1s3T05ga)}Pkgg0qnH*tdT5D&fKj=M^zpHA-d``QaCCby0 zTjI2OiuLG4mv9uO{$|mDYBfR=v`>M1EKYu2@z2~q7~0?j`&z7*dcSFSAOPt)~MA=8B9F+`CJpK5V<;;qOHUwzGZnE(pLj*fH*kEq|07WHZtZ+M0i0r6Fx|iLX=kTX; zWg}?FSw^zj>PXCl6&gOlM(#3lBa)DU7Mpra{J&;2pxWxOQ`o_;$$f01D&jT6H3@qR zX62e6L*frT`PQBmhK2~|q&oR+$<_V~!D_fZqJhVyB+QARiTAhq@^PZ#w zA2PdVzABST9Ll0HESQm}zcz&~23HCm*d3wh>a))< za5vBD1BK+tS1YVe8^K4U?2H$OTmn^7zxBvePC zliBNm6WoXnHIDE!j=QhP6i`oq+Y;nu|Gzm+#3w7KzVJmI`XL(3sJG;3yvQHH1V+MQjU(I)*w;0UI(((BJPK&IAjY0mRAY=A8srR`X;EJP8s<_Qydu#92Q)>iv)Y5{XvWKPYtHmJU=YZrKoG-DaS)mN!o8 z1l&ctHEz>R^wPx`B|#L9ZQj1R6ngKZ_I7EcL^>@a=I+nYBk8tz6?&8v^t~-QQzf=i z*d4hTxq{jA*UhA-JFeTPKu)Gp=U<&5WC?Q;!l#;VK$xkX(ha*%Dr8~l{k@93v%M6P zW}{shN@zX`JWy7PmOv7iWh(i%pQfyKP`_8UVzNV)#wGQ}2Bel9urk3h-Rd#Vy~0E% z_JqX#Ji(dR2H<7R6;h04tIGcZUO=J0<+v+!isW@cX);W7^)>nd8-^e=KNK5@8IMOd z|BTZPoxY~Wloe(Eb=d^POFP&yV_a*10At#>^kpAwB3E3YL$=M&k!1uFs3u>(GAB~x zN}qfRdpdfX^U1E6%0Y{A*N_J>L92f`1z4vXeB?B=X*qLRg<-IuA?z(@^;N#@^?TL7 zg-~$s6mF(!6GWQ!qxjS#Edmy_Yc~ti@h&|W&F&cW|MW`BA(DCZI=fHe6ny?D6j^h? z-Glcokb{yCP|yEVArTt<=gXuuXpQx;8R2RV)^?d}NW9~1w;R_>R$&SCqQ zq20)&GiYb=J!$Ydvmlr$OLqj_x;gdC99c|DnU;tzqLssX5(GSt|D5H;VloMtpMLPL z)kBcab^g$M%p=P|Un)e%d5hb&WTcR+Dtg{@*sT=L2!;eByBTduHrmamf@9#T>L2J* zZoGa7vLZ9zVX=ZHH7;$-|-{JfU_=Rj1)}%5*FEoYA}nE*T{ZcC7<#vakfT)iDGv z8qS4tXj>i4f=uNqGtszRXbP;#NfwJam?Pgd!7U7^jm-rVufN|ng>j*6moiK4Mbt4h zq*A~MsDt)hiUt5adWDeu1{JJ;E2)b(aRPR2O}P?~&ofJZG9%f(nICdy+=6Nh&kFrm zUCcOF7h)GIkVm;|z0kK@>%KxpQVuCgTxF>)MDMP{pQ5XB+MafPbYNUbg-@qeYDKK~Z+NL~`h5>#Mj5PWS9@^N6TG==LG%bvYN zP3uy><6Oad)}}YD|CvsQu*tG_W}f3Zd{kkybc-pY2j39Y_E0;o%M5ET;&!$$1p;7o z!I`~v_4nul>Ok5QlFuykSyEJX(dIb_xB=)uBQ3b*(|t%%`-W%3k-<6EaiAyI;}>5V~D-se8Q9Gdb_ z!lFyW6QA}2)c_|FLpfAg+lQrRNsUT7M+%WNPgzzDCF%FNBw(8|B$JyQQ-h6ku$-F4 zYbSj3rxkicDbwCQf!3)OJ;)V?vnTX0x_=a~PhPn+9!2y0u9Y#7<4NlgqIfx!skAr` zr+)Vt*gCd@;%F}gk=4I3k(=%YBmVvSW0B}r1<8VW{XI;6XGjKuct6n;FfXO^O+uV_ zk63+IsScu+^%NjcI{I9c+yS#rD|SiPyOvDmMWNo?Q>58YZlQ{~Tw%zM`+@6FeWXkn z$7V8IVl8WQz0400p;6h^x4k#r zz8vwx$VoEndI4L|yfIOchCi6)U8=$vY!8RyH3&LSUCGueEuX;DC=vX4Vunf`^YytX z?OEt`jvOEa*Pf(ex&tZx_NME|5rKK#Zh>}DlKt`h#I=6BXvCX_)P$lbW}c~>qn_8 z4zx~B*DV>{*_tLAjp&>VBw4a{Uax*lDycCu( zE~Zk89)!Tt6F*x4N%DE3X3t)Tz>5sDR#}bUZ{{JnKg`y!vs}+~+E$P&_!Q)a*W9_4 zu^L-HjvRlVu%pK7Vmf?92n~U3IurZjRMIw{0&J&(JNI)2;u^_#(Q^3?A25Ip{ zVg(A5nh3>m#S5+*yk5~M8yKUOcsQ3i!0_ zHQ8i|qw0$WcY70#puB`Wt_=f0mKIjH*yaN7VN`i&I)km=>WeYbk)>7^d~sK2>?d*b zy@+^yp4^(azIq)RFKGnb4-A_j(kat-F`EU(8<8ma3*= zXjcVwmk zs_EoAuzQz-7}JPawloI8F*a#0jn#cWq>NsM)B|9X4{e!?+Q=h64u2m1HOpoTaFXj5 zW0ZoE^pLnZ2F8MfRy}OrJa2HZEHL4MXxt7%^SHe__jIudpj+~gb7oT@s&afl)}7V?QRgrrYyTX~G8D-Y7f|lP{OTE>(V{r=#dUSpF$QxDd=DQK zJ-pi63A`_Nm$h&f%WEAp-8|Z)%SLT2le)e6?Nc6}?Z)D6{im{)5>-jypV$~#K-}GTY zY}6VUAAE1Q3!bpZ&cA%}<%6qgq+?wBfGJxMBB4m(QB?K0?F*vcldhR-H7@Uvy)=Xm zdcSoPhoAeYnKV7JJ@?f>oJKk1i}Ra2Ic|x9hU_ zIcX!@36V%G$GmQ9jM+=i2FwDiue=u4dnVYe^g{RJcVORDROh~7xiW)>z%SrMDFw0t zIi0B6I$$ZO$bi_$bA$)c?HWnWAts+!-ckV5F zCse$yustc!kIwu)4K?zSQGtO_hQbHsHdB$%)AKdkDMr({BmgF8{Sl-MkKi5X(vCag3(KPH{Y+XWnJeGkB&5=}p1TVc->1hqw0U!m11sd4lYf1fYQ@aXf9 z>P6~L%DQEB6u;srN8{2K^KeNZVOB`;VC5}opIC~)%ukpU4l=ChY#!`f5@T?a#X5~S z4L39&_JzO@+|*6nkNXLVep;l_~3yM035SxTxnNmiC?+IVEdVM$=%#3 z873;IddV^Vw7gv?P{lzTFeCEzjpspdW^yzKR9~F+TjB73@X96aUl7EcVlw}n1A#7b z>Pz1q3K>ZfD!J5hWM(d+aP{JVgqJuxdO@FqeXXQFlm{?X4G z2vR+&f0DS#NL2NSX!*+oSgTMsR2~a?=FHVc7L6bwvr}KKl5=3~m!aP95w?7+A2B8x z*_>&Yez{Yi3-!#GP~;+AO(sM9Mcl$Gx`eoz6IidR)*g)kUJ=(HZfW9z0CTF1n_P(Hl0MZ5Q!y z>RvPnDiB<_1&zMGAwa95d*KI|PIh8Xe+LKI+O5JYhSn$kwN|~Nfc=lf>cnBcEYuIy zqlWORd9N_e-PB?dk06ze8K5ZF3w+1P>y?&Z3{gp^nq; zVD;+oEa8mpFX=2j+K-}C*`Whd#Z`?OqxS`^BCB)baAKQPC<_E#5t_)IftA5BJ(Y7f z1OD=#0ihm^Kyd&SBP+Q=U3HchbEFiTD~{WwMI)QAHFs>VDAOMaH`HLgp9aHY2s5f8 zA(?NHKs>nZ?u0~l1{wdj%l~9*OuR=7Vc0f24v5AgRX%%%2z-bM-Q$8?(OWl{hQGF1 z16v4gDM`70J#X!-ZZZqtlN^n;eyUt}ODhfBkdr+`@R7p$F^L7}xARGUmMu2j15FsI zXzd93RBJ-7J|Ko_--%)327}8f6zyynD&$iIo#&Km>t)i^I`f}|vVFa!6A!Bs<(j-V zRIx-n@-*X~jp-76tl+Lk`OHK~*w zukzhGO9j?(ppa{tZqK&|*tFu0BG(dvi!0QWImB)lbR!mv+^BtX>1tF?p!9jYE3G2V zQL?{JPk<1HY2dWgpI%ui`t2XBnSv%Kl=&DqcH%TGh2-@@CDM`6!Hx61J$cvyT8}Fi z-*E({(xm*NNpR+TqHpjip8Z%XwMT&=TDibI0-9halv`ANRpaDV>?}OLUUZnH7sSS^ zqdBMArZ(}g$%4sS9d+1E27f)V`1*vn#}6+#hamc;J#Uz7EpJ>`aB?Ra1qVRJ!9{I|=j1NjxVJ~D# zgu5GEurJBGj_dTz2I0h|WHbavj;kBVTNq~g#DFB8Q}<~DBdvkh3D-$7g%%H6TIp5% zWq;-RqRCN6E)7`GW8r^5WzgP(kW9=wCX_G9q{2i zQCLSpI%^Z#t#EleQdVz%fVB-+jSGC=Sn0qD7%9Z-7u=2UI;=xJzg1uNbranOU771h z#*F;Jm_d@!C&pVP9+0;RBC9tO&Wl=3%pT_)+JiR_C>#7mE2L1bLl?qKWOw6ql$^+! z1V5zGF+{xo3NkqLO*~SOe{_-`KCirK6*6=qAKE@SwsWU1zft`W)li*5>;*O*I6NvM zOl58zjVXlm*Z5h+ASb7wrrl!J#=hd$W?7>4c@kfwQ2{`PDl1tEFf*V8csJR71+!zI zeMM5i|07jkgAL95dzo$#YU#oa5%2(ldtjWcmCOsYkH&IOhggk9@vinQHC6|L(*5QBdT4Uqwc1 zx8^B4LC4QF8Krt<*Y{YYUr`r94Z$C(inNr~6rYUib~+c5r*s`~W}~^?E?H?eA?9(1 zPY*e%?|uQN+^U`fmcTax1>`A!mRP+dQum`)C8ipYNRVmYT%__ud!zg7MxP2}T>MSu zb7<~1^_#1W#^r~16-d30jd~)c^?0z}6%3P2!;yFy4z@JxLBGZT@_Ll>a3p>Oi9p16 z+Qo|TTDsu5_yX`v+wWdh`USHKlLG1Jwf;jaDEQ}rMF)1iTcnM+cT)!VFNLySnx+Zi z8}RTyrQ7IbFN*f1MKp~#m5=nmyUjC6a`n-`vY*`+0-h&>VI0V>(0DD+rANJl34T@K zP7CQFcd`p$O)5y*rrzm>onx=8$j?sCqqKF5JKk`7kLc+>CKlyO1liGw?@%BfEND2L`m9qQIbj*sbI`V>u;91bgAc`8W}K5NA=eAD>% zCutRC<%8ggcQs;hw*ftGMlrA_D zDL_oNUk}+z1S^jTiTD8v|D0Cn=DqQ6>r5Qi)RMrAW!^#>seL`%;O1YnM<%$%@EUN6 zp4coM;0{Xd(joh7L^-M|cEP)Hi1ufFf#FHK`Lp{Z$EHM}OsPavtH?7{k2QcK2o?-r?VK9ZDpvIHh*4C=p|iuBgpM266%Ul1f_hLf}b^je}Ed zL3HfDQ{ZG!XOzyGp6Laz4|hXThBOo-9vkh)Q*9eV2SrS!>MWbxID1u$puP&9!{mGE z$U-1FPY*ryL$k76-b#fcI+By<1A^sGqawbSl|M@=e#)-BmU^@46>fS4j8hxR_E@}Q z`EZqVyI5szqT8nG78O+Lkch!2RSe(qI=XW2nb-{G95Q|jVO899WxZxdj4+{T9N&Ni zfcLcQdEe)KV`?-OFX6^xqX;b&^{ zl*!5*8g92X9>Jp;tQ;Sqla&jtm7kR~I5Ox4Rm4a)X=4xr8=R*hc-&WaKin;3V%uX# zHS|M1XA3gb)*_ZRy-Iy9@|M+Z9N~C{cS{tZV0s#)*G+Fhi?26U1OMK@$Oy9fCJ?b8 z`_R%q(W~>yjT9Ww#Rw)vM`L!l(NpXCX~qprnP0uqwJq0=_I~_yde#v}7jI=n+qp#z zJc;O%1`^TJW~ysUzqI}+zb!lJ>J3KQEQlb71dC&`rm4TsEzr7fYx%L4Q}rnWOQ0tL zjoEotbKczUQ>n4GJ1W~8hk@O}F1+9gIf(C|TcNl@a%7DfXX{g|4|=?=y%__wOZ zXKzi+R$tryeY%oX=Hs5Cc`(~|A()~&IExBF*{g6sD6 zPf$)m=a+5+%9?mA3=2j?7mmcBqfZ>bR>ek@KNWRmQWI+C4W!I#YIegJrtTNTmtR*% zK#tGkdMwUjrfg`hc=NILh@2-hp<@G;mo~3yzRiA!dahS_1u3W+%_~9mho1sD{BTMOAySaFr{zH*IFB42KWadu9r$y9PcKd*}&unOEFGa*ZNXt}d~9nv*` zT#XIsJ~r!S#8nfUfQ>qC_d;S>A-vq7BgvrC`-4OstGx5`>24>+nmrXOx;VV-ck7)v z>Z^sLoxxh7I)0z8zAKHs)k8BL>3|fZQD(eo z<_7yS!xacGQM`~T;%qwDCXi(;NO2K)!a4gA5?z!B#?-_aj?x%tmo&*1v<}c%o8TK3 zk=|R{o*eH3qmSRtik`69*=QC?2e9;kyDhcY-Rbg8NexB0p8Dc1(v=6O#!>+6m&#-S;4pGV{z*Bt+KtOr(~)@(yvUF0~1g zTvl2C<&!ySZy6+MLRD!y7MEvmyuQO3gb0&Et73L36N#4c$eo_X37Ps^7xpb4nw;DW z7A0bsmJE6oO(LvaM9rbTt=)TkT>jJ@;RV0+xEF*=T82&ITu@vpyQo);mxCKfFaY70 z)Z;~uUbo32kJaC(i*Q9gm;`THa3h7=a8I^ zp+doFK2lGSq@{>p`=m-eit2m?g#qCafw^<_$#OR0X#`$4Oe){qE|>R1e>)W$i7ZpRuYP5i|{+()N}z!tQSF5sIpp% zA$QHmxJVi|f1VZ~lm489gnjLSS)h;2bfDkve&lXB&j|m(d@5ejSU5;8QV)Zxk;rS{ z{IbQt___-GA+MqRS=D5^(?arz$z>VVZOfDOhQUw(MTJ6hK&_i%+6^zGqvDE6zvZ9l z@!qoSuYs;5s?l`;_TJ>8)ax%Y|2O*34qe=lv_m=R1?wfbX7-@^$NQh21!Z*=zTjsC zZ!G7(Yl^3yhVaC_{OD;5Wu$wnR$_)ZzdvE^$+24 zkz21U{veayaPx$_u9YgH{#n>NeTn&Ga+cT)cD9&-{g*(ybV$LDR2-WwsG+{)vPaqf zkpLUcu)6sfaY?UGl=CKT1J%cc6%BV`LtsQd90mubDpE#JBfChhkbx+W@onkT6-ML`mcbEnK zlER{|3VF1)jF@877+#6|L^MyiLUFTIN(7y;Dv+I^qCI|rkrs68AQNbtCCtn?ho>4s z-Gm1obxrm(-@?A;`g8mnA13G5)XccV<+-lnG>TIZ+dPjAA<1i51a$37QO1Zp)R$(+6zoYlw25P;Vxwod5*u;t zoCh&OJXj<<#Q3I&3?T%IAr$nvRA5L1M6`^NRy}5(=k~^t6 zE>10H_~1TPkpBC|q62Pie_ewkCEgg6^NY2TNEICQZQ90nS*x?dNrC@69g-OzDEi-5 zkC?k1@mBJCxyn*(eO;DY%<0PU#j|^OJ&+2|aZYSK8R!(uTw0ceKy0R8&GBQ8-Vw}t zw3ok7aTocSy`eiJtc$y@@?{qojdg*q`Hnf_6R{_GRd{R6I~b-8UG&;R&4+f8e;A6T z;nRyts7xkXlqHD9;R*k^KbkZZ^ooM25Rjxw^MCeBaD$FoU2nJ!H0G>xkM>n9e0;MU zHswAZL3&bFs*9EVO~{9uXOWv>cO$(Ia)!7x*zWyluOBSTUP>JNcSxX()p%NXP|sy1 zeLnD@tA3vm+h${KGwLARKl5f)Fpy-2W#xoIyjG`)AtlR~4=&JINu1~i{; z080%A=O=c7sbgv|zjK35E}8~>zc>*TyHpLnL`3wOUi1maN`2P0{pRZ-U6|v?zuH5L(&g|#$)+#e-@d7U&DDb& z5-?pebr`}YDHsk2_$@?+O>>|%q<>5F?ttN4dxuq0BQ0tSuBSiNK%Uaf7d%#PJQ$Sf zaoF6!^ts}5h)B884>WZq#4nI@LkRG|l7X)$z0!hDv6?2~hOs+BH z%BJZEa13-4_T$_=`Dr3uzB~hsO84Kt(C^H5^?dq3RF&-cvTY@UrXJu#yfZ-1qnN+b z!zhagy>aR#yu2RjMD9@%@wUsoky*JYzY3q|@tOCf-l~0HC?0}|t^j&bMufKykNTPw zH@3L+wDho0YNuW$mJ-nthcR`P>X}LMczZ5~&XFNRiGOOZ#~|OhG|z7gHEct+FJ+3P z^BF3#AzCZZ54hzv%wfAtDAjj*H#!6Uw9jgPtQ+Gs7{w{M8Tpw#9Xr7>XXZzreOrko zIDZa*Bu7D0TWepBHbG*yg`GEXa9yw}68pN3l~v>WpNDMG95AftKYyZe7%ye&UDBzl zR#6ajq81WR-YUXC)GJ*&+36`GbcA6T7^<_XFt zyhy8=7^zr9U4GSdf>ZVzMaz<;WxN7a2!$pw0udN2e8Eyu{w!htNZN zVH4qd|5{M@x~MSBI-g&^nF94w8dRMJbIYajGgIQk=lXdEn#v>!+60gXThC}R_3?pa zj_O$)iB@0pl2qjU2{qwWa}yoe28t0hjBx zeXi8Z=ePJ*uOnR^3Y453n9wxlUa-Jf0!ZCi(eBo9io5)w8YvB9-P|WV!4YWt)GcVg z2v;j;?)>Ep>QpX$*g{G|ojOUN7~=*9Osty_$B68(u<=rD1zDiM_M!Zt*l(%vluY3- z9h>=*cx9IdaE_p%!e1*%Qn_Z?;+!{@*K{h;dD`T%+uAjC?C5rg$3RJbQH*xGJWfbM zn=?plfn3o)os?29HrFs(WPP-Otmlx}a>>epX^(~inS!?GFh}whg+Jsskb71n>a8(5 z2mBiy*GYL|Oxtg7!P|<#Wj8*+MC}gG>nfsk8}ZLHEY(2!89(3MakYDG7fcIgE3Q<$ zMO79SUu{pn?}3yV$vNL*1V=Hs}-^Y8eR2$n7nLpsMy11iJOs zJZ$x{>)Fq47Cm)!_fSmk(KsC_V>se* z3n09IpJ$S;iT&$xDWtA3mwn*fTm_B2_bq4Pc@q?_0q*9x#j$tCpQ;QMhDlT^2wLoO zVJ@v$n5{QPfnLnv;*xnHTgh~hmzngs>5Y*}vOv%u{Q$Om)aBiVG@FKQ0$OiA0G_1< z?@Orwa$_~@Im;#xVaqn~s}vKqdI7|I52#xz{N6>FMIsNe0+BfZLzPLmrFWUZDohDZ z6O^e<;WteU5qvPg?(H!pb>(Gf_D+PIJT*h7hqj z3pnXtKD6@RIH{9h{iUZB3V?&HDhI379Hb6@ft{lliY;^;wW2^8OpBPLVZ!|H}r`c&;D)XSPsCkvw} zcA+$KjI-Rwh>B7nKDvY@9LG?3J)xj~m1* z>x{euzJz0eQ|M3{45D}OzVt7!x&HzMi#82xvBKTHs9^Kb4hNfNQInYLOK^)WWxtoT zH9@JS>xq|p7+UAeEUL}$IwWj)g>C6!!Lo5iNHP=BzaIhu=L*k)#{lu0!7ngPdV(MJ zHf_}%&%+)jHCaB5{|&>8oVSCg%nR1Fc89QDR?ucBQYYm|ywG)=fkmq9OI{KBu}9`2 z?IRKVeWRZdEH`hGS!aT)-yA%wWr#q$bEW2_NqRKUt9%up%}TflMcLHP*1izV2=6*8 zlO!93R1EKHwV)IvzB-y!K1t+HvehhtPr zMMCB*=Cu~Nz?l%aEch}`W^b$VoM}$Mr}?C~!=NqC^lo*grTDfuMCia?q!7^fcQnC` zEECTJ&*x7Y#eIimpd(`E>CX6NJ~o9}7%pFpwRZ{qdk1CcAj@>5fs3^J08C8cBUJ z1gA?=hH8>m(EUuDjJA|4H`&?HuwCIXjuNQYqTcoim<=Pod~j#_binbX}mO~-~93>yJ9Za=YD0iHj+}W0|RtkF(36={!H%)iws#f z@Q3ZwyGyj^Bmv)r0^S^e3e8mY7EK5f z@v}D}4$(gjs)f-%vTxAg-7luHtmh3csw8GBzoBsd_#_nUw<;FM;i(8g#7&0e#PNCY z;CjLd`LpH;T8}-hEJCwZU9dY6ptU8FJ`MiQ#Hc*as?FA~6Uy6pye|42x-?A{`u3nV z4iIJn+f`K;>6-H6u_F66r+?ey!n2g%hXj*(Lbhv>z7lWS(Pj`(&#{yBpl2_ZkPSx^ zc|2hllO#8irLYZ?zbKb0m@Olx8fpL^I3lZZP2~J=%yk*yD(gltTsU8Bo(x87!2F$X zjL3Ravo7C$E=(l!F%IJlS5B5jq<3KmHcGcEw2p?~REJA9b>cuIPx|e3{wH2yi_xai>J11M}ku30b7`eP=y|a+ceJ^!MB*Y z^jERp4>VIl2KEpkTS`g)8dYv4Kg4At+bO&aaHiU+(qrIT!@|x9_2S#{QT+zXnQ?8@ zPe4&P-ny8DUZn>(j#7foTOId;fVegFb3h_o!z^ZR@o*KsYmR-Baqn!@(w=+lWU=Pb z-wCrzf;gA8f~(Y@zEPp+53-g8e70h>vJNJlZp_WDK|GCX_3%ia<<*d&g|P6R0rrQt zo<_s(xEyv9Jec<3kZjV3T3TN^H~#%i3q-x`hBlT035L@|&oV2<4FV45=^%tL-?i;Eo|Gc%4$~R~BCcW%-BO*5C z_0MZ0n8RP_I*cSE!%hd-Pgl$BJlE-`KscoK&tl3#ien*J=it0d%ijTOZCUrlcsE?( z#_A%#+1Yn~u|cjkEilru5Wy_{{9O!JsunLxK9QrGO1+_B*~NT@LMtg^B`cx6t{BU$ zp%8p%>0KTH1o5z&U!5B%R!Z-M#K<%|R;#Jf@u#L_jLM@|kVL*?c6eP8K^xT}x1qVs zdI}02IbxO_0j0ZgIW6B4*EKup-xYiCsQrL*MgNf#6I-G=i+TZ?VJXGB^VQ89^!T-r z5%-wncA5e}xq|;lG-^{lQ1zlOfkYu&mdQa_tWn&WvxW#<1|pBfTm{8cZe3lG{M_7_gACF!rKp-M?BJs_hB@{sM|;LuT<@6%rk_ zelRk@D_BO@m_^r~Ql(@mlFI~!JMHk|EY+|A=!9y!Zo@fnRHF5KY- zFf$(AWJq*Gg4Nu~`?ZQl=d?!k@edImXu-*ndBi>Eag^ynkx*^AejFslE$U}@elotrL}n2o%0(-d_YjJ7z3t0jT2!bVEsiphK|nW4K3YBONk=$ z=6r`lvIYbtE|h^mguk(r!7exoGT}YS0m7A#zY?%8PbMzK=JXKxVn{s+WgJmw=b_3$ zq%i8}&1WjTwf!ww!IxY74U-tRX8KEV^!aFkEm=%Uk9I4g=lg3wBVeFl=TCu>6$3^_ z{CO}kJ5RKCRJRjHYMm`al;2vX&IwGA`jgtH|1a5;i01j^o7KhSi@;>s_Og5@d2Dsb zZINJ*1Ia65tyH{-(K4F{h>j^UyF;S$tHJ_fdSRNomYk8F!{zC=!D5xJ6YOr5afCub z2qf6E55m(}N?#$O@K2R+w*tAj2q9x)9IepW`|`wA?K^^2JJW|cGH`%P1Jd+bXp{nMorM`XmmRDOopT4-k-3VZ(`Se ztIg_4qe3{CDbdkKIfM64rZF#>5NuqjPR0YGf3W_6HBjXUttCPpULw-o9w=8SRf$G&7C9cE}(+)ml#a%`3@Q?vb@tVY1TB ztRgKPF2$I$Zl2XcAr>$275_=zGNZK`t2;~ta#ov$J45kl#})J}&hSAVMXiIa5g&s5 zg7;h`UVQs*!Rt;99mU)eq>0kp+=+lC#>xw@~0oj^C#%FOADL}FOC#kpnbeTSLg7tV=Qr8a!c5YPAJkdD{KvF&CBlfXz>*^z z(992TQe$N70~Nfxz+=II8c22@)uv)-EbuoEUb!h7mz;CEP{0m6!dWsd)*dhsN2d)| zvapBj08Dl4Zj?8BXnEqmXZ?ENMIcyENo;dpUn&`SYinw~68q!yxqMOu(Dv2`p`>AO zpv$j-i}5T6ge9jG?h4u;BssorerixOi&K#SUS;_O+)SUmDmI42WgqC8;r^I?jna)tOci}6;KNNk$U`7?_nHJvN}~1}HSQJn7B3Hn;x641j09xJ z7~7REjO_7I7!HqHb2$OeTaQ~QbLE@HAy|>w(YIh)v=tlmVjX9zVeKbwjnb>&KESxA zdC7=&Hmpy;{Kclx#DZQuD&keR1uIYGcE`y)FK%0BzB&H&ys{ET)O4@H$<{?|S;w{>Nnc+g7b)pR=8)E-}xehFiQlPs=F=Z~_vhmh#-LH=Jl z0b|qmg|u8E!YFNSj3^u=N303)Dkc=|EV!-p@Rh0V%eBjt=Izlx9RpF?m^e<9ts}Zu z^!cv>tI*nhAQa5f%R}c{u&r}x9{>UTlLhZF>egWMK3I_ctI0h?qXu9O(&aK{p4i0> z&q6%a6|_F9ZAifiOE>($^`^`bz2BPZV&A!D2SLlkHbDlY53O+I(=2YpxlA$5~J2d*R_-;U~BNI zIVkG>b%)5Mx>h9u7?J^X?lV1e^Ni_9y}<%-pRrR*ZMH_AdEhq~j^X{;RNoX<{`8N! z2ACFGWI0RjzU0GeM00;KoQgFiy3|26K>lhev4mEZ_Kko*L``2d9HOC zgZq)IZ}VV&naBzHnML@K`S+96C>~pKx~d}A)nNL931?CL<%tn@a46HGAw z<;e}h%_+wu(rx7?i#Z^*cn3)O>8keeUQ>8I+u3fC8ah}7jpttP70o)l^CjSoN5gnF zw`r8{BeLqBmBB@^rY{Z~>BlamC))ewJo~HQtWX?s#y;8Xz`1;!Qx~wT^FAFnzwMwc zN0z@SGN<@{M5@!4%cagUQ)kt~w3(3RHfOE}4P@yNJafn8X2pQ+44s>u%G;b#Wwu-7 z1nnllpsqg>AI`!^bX`_C?Qhxy0vi$KxON8i&0n{}Jj%(=un5oflrdi`B7-M_IVdY% zl|Ep?z3#8P5b>KS700UKFo|k*%dkV7#yk~K1@l}#C4KB+1 zK9#{Tg-8nM{hOgNC(sJ}3D0!s>*$fy(`*tyc3WoG@L_azI__bZ5ya8LwP9yi1X}p; zHUT;{2zdT$FH!!4Je&8#`pk;Sra?c}RAOOW0|kksAxopbUfu_nJBLf>%{6KS=SZ+a z6UXhAo@aRDRu@K&0;m@kU|e_@a13v>dG^%4QV?MB#dTe$6&^L$_*rcmrG6MeFWLCRdjVPIOvo;(l%p)?8u9hi7YW~b$b0{PO;y{q6X z#NTsOI#W|+&(Iy7kL<`FH};8*;obrSikgfV6O!<(AW*;$9jN*z?!Nk|KEH&EgMj!= ztvh0-7Wd+!phCvz^S^W+6ABoR6iu#C3wW9wM1(EkJ{dt$v`afj zG5t$4ubE4{g24l(Mw~qN;S=*KNh%Xz3Kb>*G(vS(j|979cc%t=4Kd~EH)VD2CQ;GXzj zBvH|YwqzMtNzn=IB4vfA553M=+P4=YB(HXPa-u(ADMp$EGxAb`J@m+vrQu8qK}%`W z)5l9U6*a0N2CW;2=*oA$5Sl)?LTc%VFRG@mFo5imx5qwyS)jPJ*tV%|n@otoNi{$4 z&(Q|6(9~fBIz1!d-V?g&9yZ2XPK8=sle!jVnm9C^!`XFWvM1B*%>VDfqIa9FEnY>y zKQEZ3UZyM~%Y`t-zIJNz+FC}z)0}$w8XfL_S&Yf$Z=M>7HAaGI2U)lp-}`Yxm@Cc_ zF**@76VMmHtJYWL1veh?q*m_)Hfc#vcYn9;o!y)!muPTa%BliQzE`j68VulDH7!6L zx`N^ig+JV?aJpfe2YmVV6fTNzg%^mv(M|TW1Z?XtcOH`A3e0zaH*g@oGAD@C5`Mp1 zMeCsu%GT3Z;R$6VBg0?XAQg|eSjKl@gHU1>RW_;1#Qe$numI~Co$q0GewT(Kie2JP z{g3q{_CytU6$+;px8N!#zbKJ>qnP4=RnCFX}5>1FCJFbt@z?8EuqE>`x1Gq} z9v2|Q(sdaT48`M>(KPiWK&h~bEQj8p{tMeh%^dnGR^~+@&PLuU)Jt&RYOJ{!tdxKW zi7BGU@ts>A+nWIPxKyTL40!zi#c!PaBA*7t#$w$xpDjxiKVlgJPzfLXf)y+D*l2ww zp!>wbB$gCZwvH%i;baA>l|cFg!5sP&*RvaW4Rd7Qn5(#mpXWF}&@Zkdln(IA0bep| z9DCuZYkFwJX39=%w2zOgTN4&C#>ej}3KG(UT$+&Bn5YgM{r9Odxi{{=abFCiS5jj4__^S+wma=Nh#jpoadz>C& z$vYaH`bItX=P_QT@UNq64yDihyKd>Bdh8L7&Za@YJ0w+pLysy`2q_@P@XPduGJ2~U zn`6in<{w4?W1fASL@dv)4hrpk^;aC-(k>p{T?Yv+gAD`0-JOu&Hn13Yxb(%B~R6=-n;vU?yl--%@znOH1{jU6smKk z*~JZK@|kdUpD^-wnx@L;1*?d?gUD{&L6hVQvf-elj;5egcLV5(4+7CO7wjXp~mjeL$w z9E&95=m?%dVLBP8i^q*h7VCS$p50(#0OQfA+5-XN+er4%*l2$ALq=Zn&REhs3fIC5 z&d_{S%5dudy19XRoXRBmJhsWXle8D*Rh|~(iKgIuACy94Z87b}jh&ll-_GsmthV(Q ziVQYI@=cd{HboW}nh^YjilmDdyVTCGVumJC1wMX*m+9mERdmAZB@g0Q4=b55a-Lhz z*)@ZGA_*L$U$ywy4Ty7L3AflOW8`FugeWHR49_9uQtNx_Cw8p}&*9l|juq71H|ah-~&r>3VQ3-eG*%c(u_zj7D&a zrBBn>M#$A(8{NKFt-J!D#l78^EeQyTqg6C%F0aYeXvm|^gj<~Ed$ASQ@GO2F#h17j zJ>~lAIKUTFN*_K5OTn0_uIMGRauJoF+t~#gNkPVo=sCsZ6=Z z-dn`Be|FwI1ngzpp&t18RKk?AKI$RB9EJ>gG)a%ta=i(e42*5#H(w6$fAzbfDT0MYfN7g@B#9CzcbnaP z*tL?vuU4`*I*Ax~AUtxK*EwK7tRenh;SyeFI@wtAGiVY!1G_N*Evhqj5EC^XRI|2` zz*Oq>y!!Z|p^VM34Yw|`&6h=xZ7OY;NNV6U&C+dggL0^EAk=p(Lx`d}5xZiEKW|T+ zS&P{=g3nO-+D&%;ET+ojou3S)a@JWD#V?TI1SnLmaN1(afd1r?Toq$yT~}46n?gOq zV6qZ_0Ylr-9``YY7TX5ZgURH8wK$4KR%ZvW%~5gimc;F9Ut$r-GV^jXOkOkxq4*>2 zBN%v~_H#Nzxi-p9y;yY13M@~ZR|Y*wJ6v+sK>f~IG8xKB_f!{?63bSjQtXFSR_*s{5|a4JV7t&=Uyo4 z!)P1P*<8pPy^UdtYP@RAeL&37gQ5ORak68UF2zxYQyvD*;3J zvU0{*v+>c@JC;I>(0BRhVhjDh6(1p&AwTKUjTqBImjsT`^uOA)oIAYdMM{o8U3x~p zlOkT^^wzT89*O#0dUrQGJ#O%{Qe*Pt^XXecqU?zZ)(DjM)x`7C5*DZ$LX0nbH0L;@ zAHw$d^Ldy(1@Y9>x+v(frxF|}>poaITg4!fd1gnFW+h6dP)Vt`lNILC+F?#e2V+=Q?YAb6(#9nUll8qJs+n-(_i5f49L7)Uw`E12A=r3;RbX?85pHp zl21P!+6tV2SOes6JE<>&*hT~7Dn0bH1$dx=4o5{7)lZJ*J=B12~{6mz> zP^KRR+}>&3L-5Ia#n$KN06LBjB!Y)hZqO@vMBk)e9b?jF4Dr19g>75<(8A&;wBMj` z8e6MiylX|9Z^sdqO_kDaSvn2hkF;kWg9YamuvifohTqWz3dx>6$If#iZ|)F%Ve`Si z{rEP^2rKA88x9nQ-Kjs=Fphnmi)8Dg!O2gpQFr(HR-{mk_ZQ{%6_RMOx5H;u3PF0~ z1L}tMH^ZX-^mW8n)9IZjQp0piv_$?Cu5%;X5I7YLz+T&UJFG${0X_3PI+=#j$a_qmPfIo8?p{Zqqo1+j)bYpHNgeQS!pJ%kj#%ZAcJ&vl%vQfB&#eBJ9|Xh0fBXF;m#R7MGHY+RD*s{=Iu z7xnr3PF6koNu!3$joP8{s8mJW4*+abACGlBvL5V=SBoT71xMs1XE77qbMP;-WD+K1 zn>cPn2G-oTp>yKGYNNqPaNxTF{~ErYu#Dwk0~zuqA&(R}ZbPomt*U*9+3>HU2HS{|=Dd;|I5;SZe_EK@ zyWgHybuvvLd>WpJw%>2MPBsoBV!r&v7#QucpCvGxuu zj$HBVx~)+3Y{Ps}nEYcB)74}x11>&jSduoH=WIGd4Uf?O1$c}g#b;>Z+WzK6dmrB^fAPbHqtEpt zRlqF^$g9B|(wg?S=FX-|bx*IFo5N$(_#YRearIv%T`C!xF>~VEDOxm{UReY_UYtAp z_IrQyB47-F!{Qq@k|kNE9TK5nU|WU#yQHoxLhSU)7MZ{8WxE!oxoIfNE26Gu;jVxDzpFrDKKE>>jNBBA}et%j4_N+0o$ZVfXp>}%bgluy&h^TeTV5^1s zC-gVc*S=LJEFMXMP$uLEIoEgbJO-#tC0A|^+?uVlKjKJPHluV@9K2#x#WbA?!uNbqpK2$fmG9F}H%m6;h+ zA+}m@to}JdVLi=5AHE*=8Cmw)KdQ7~1tDs6_O)F(){qk+rh!aXKM4_BgV@Jg(fsce z*Y972T`)gi`grJjqoO_b4xw#{RAJIP-TT9zu?CRk+}FW#M|aZrY_K!~*W^tkA9OuV z#EDXB#GevI$Wg+~6eYSXaVGnY&B?OUF%ancE0R(>1uFS*q$iWd?Tcbt3+c6G9Rp+tM_2z5`UU=N_7`#FP#$3s* zl}HyK{ibxW_UE7%tsOeymsZP2;aD#xkQ(3PC87(&%)+xvn5mxsF*U_(Np6ZE9a>f# z3$n?eea~>EQi_dON1Ch-cI*m1&n8926*?@h_A#-(_?%={$TdaJEc)7fJ_~+WXo?G< z_o7*yxTX2VGpDb@oo&(l@!6rzA5NU+E8JuP?Ty9`8Z9kXueI{=mLzk9JTNr`VN6;A zQD}nDKDWTD>u55XgEvJW01i+bQr{$JB7UOcy0vDjJ{rZQNb4>LYonUUKKR?8QsHa|#x zOo>|Q+vs3_7@s+8p^uEeP{z8(#~)-FVKHx$;u6{}VikZcTeuXhUogm$s!>>fCN6f< z3Vanq$r()m#;ieWFw>`8Hi0jijZQ-n#W;?(G$yo{eMd64c<_0eUPdSJn&{?2 z!%50-%tdJiZ=$LsFhk3qQ1$2Zr}oM9203N9iNyi=WFPdCg&CKMJZ*pHh#2KG5g!+` z1FkI)SbPd z!&P7ZZph*BHdKUGwQ(j?thH%hhPkaXfNT-Id(Z<~xgwV!ngwuxRiUoL`Dd0hA_vXkqO z+<+;Rh$7gn`CG86FuS)O=B>*_J}+&CzR@O@lM5(1R%zgUHp0=Q0dFC{Ni0-yw1>3b zpM%=jj?{Sh{7;IbS{Ctrly);?P4jPUd(d8%1%`Qr$gkSlDWpDSfWkzIeuC5O-2&Df z5_ug5f8dA3lbQny>`K>otDDTC5`?;KCDMSs5d9i^mlRu6SMgm z_3GY5T)ikpOF0KXk}md`K-j-x`ZAk_dFb-3Xntr@VHcmdJAQy1Vplw!zc9x~-28{i z`+_WlvEK?OH&LRCCnSTNKw5W|L3g%}v2P0$vA(f#M%DYTCIdQgFGg0x_oir~6q7&* zF{vkF7%N#%{`bjW$^)npz8Kh5V+YydX}{gs6+?XH6}@IH*W&v^wm~j#M80p|1Z8iM zspwf-7^o*6gMhdaG~-2kv9b#{a0)3mkqO-8?-5t|V=z_~PC!qN!c+%BKCDhjGtL}zDZ_^1>PI)|GZ&PUO;Mv^lzx-x=k+bJA+WWfYgIS(l za^f38+ci@iq_ncJRMYf)5t)_GcHUpjEBl!{E9a)vPAT%T8A`rtlA4SMPJ`bc@FUP{~N%d>bfH(UwBWrPpAQnRiD!B+f=@zZfw#N~&X1=n{{cv#))? z&UCyULX3obb44PgBSsrFWR*C)#0$PY`BmTp3Qe&UMvl*y4Ay5CgwQp^8`-T^Pk#iUYJGf$yyn*z5pos?%pP?af)2#frio$mha zk1sm{uFX(bA(9H;uI-(B587{`Ql$4`xLiFINbC+rDmkUAhIVcB1@*oCTE0K^-5m_5 zIDLdVEtsfe-r9WVi5WmS^C+22T9|Q#+_E(u(afgDZcdI$=rrRO2bi76&_vFg&3XOI z?J>zEzIuR)+p~OFSZkGln+X4bp0;NH4FjcZ1I@|{`AU`KW|@-$WmgF!I6Jf_H=rO@ zd4M%Fq(qCSgpK0!6TECOhxZy);QXfjvFxYPrdL0?uPs~5Wi9B-9Q)On4+#)BDj=-a zW4IwYVb>=KX_N(P9xTr&zT5`GX_(-dW%cLL2b6i1j&7abN79Zh!6ps0ta*1v*hbPzTwk8M||XTkc$@P$}+%%+}o3sag4+D7wFzOEey=MP7nu!^FAPNk!lC<$*$L zZuHBHT$q@F^97!B2SXlT;rt&2(Ft>$-}NVoRkN)xbG&8?C0lev8*y9a!8<}axZeRo zu(s}`;t*G$t8)Q}_f0O_PUYBziQoc;Q0&;8U+R0ftK_BDTt*`oBDcQ7YJp7R zaO%H+1W;P}4bs~|)0FVb{@Pv82PCT?Lh@M#OS)F&bHqg)iRI+H4blh@qIDkb8}ko@ z02=bl#u$VN@}xP&)!3t_^?=iLiFOvGCV>ENwhZQ8*VVy86aax~*jsPCNI5uCGW)r9 zLhLfNGjCysb(5^2M61Ugzfl=n@7@u#_mBm~_CeBCbd1Rr|0HIF%1a3IaZUuG9da7j%Aqk*kgf#pC_{^ehkOES^%y>+|F)-DZVYa2OEy z`5*)y9X$G~afnWwA6h@n+n03Y$O9-wG60F&G9{Cqhg`5&yN3IiRJtqSJV~`teVEBt zzZK0(w<3RfPd(_HRin4bWiKiG%8`zR-jY85!D+_q%=UdNbiMm{d+Ux!Mke7ETb(yu{;)Te9B~hx3R~wvnx5LnFQU_MFusUJ zE=jaBeASG%jN~4*8`!`DI`Z5s=;tqFNksN}1Dcq`5}zOI3Fc;MvNzI_{jnf z=}Y~V$GsWYPpe<-Ewl$EOPxKG_U#2qL{V&PJ3iqxsgI7foXsRtQh*zdc8lP za2*Y!F;CToTc@G!`rfmLATB+BCmHem!YIl6%-!I;>YL-HgVC*`nLH+A;)0sndhS#) zX@hgB2$m9%C&zB=jHTlTzLtDs8?~E;+K}NylvO%Uug|S?A3GR#^64);zg>I!g1@po z9p9d}b7*2~KFOo1T=MZ7z5^9+v|IGKCC4h-=93=?5jxKLxPP1OGn-6#eAcikuf0(}48TH8UV%W)G(*t*Bo8m@R8Z1##F7@c+zEfIXw#5!wR zosi+kAYZ^h`EYx9PXE>37cMq;V4U~H#98W_R{aI5YM@1f|$Ja-yO)j_z?FU zTgT9Ygxem+CODy@B5H#Ny4E*DjY8}s_Ry6Ly3R*SQv&%nCO=R3gWuzHU43Iy(Vaa# z`X!tBaWQB4#yT(L+@#U0Sa2b1?y-7xsOG>Er8?zH{2cJ0KSY~I#pc07H^Wi4H45j(`XHb!Heco~6FO6C1Gj?iy?ctq^jX|*+A zjVcqD8f9Q+k##S!5A&6J^daZ#`4T{8d{k6N%SpkYxbA%!7pd?gTh1k&tlHV z!39)cXXSK{Bm}KWWks+iv z_#_XZmz~#`3*HH(TVNcXcdsSp^PC6i@|zKIru7C~et7MI-Co#v;f;NWST|0nUO6Aq z`U$q%lsCE9xJeChq=$v9#%>+$77@X9JP;J3-NO)iv1i3 zAu)->k)^-)5tG%Z#>~h@pFN>^1)r2m*r)tNORsC|VR`wLQ z{Bjy#jHNw=&oI%Y_mo{O)5k~Q;*~uXB2l%{D{n=s`B$zTez!^k%b}iE4?l_as_JnH zeC=1I!kMbdMz2L2KM)webnpCjvVHctbnOUL-sVUp`qr9j*A^ z-wW5PbX?cpl@oS6<%?l8=I*S49`{eG_7A7WJO@sH2(**w;>2(6P0Ww~Vm`V2yjf}& zk3)M`TE31nxck!fVl|^tlXX(~_d6Xf$Nqr?G zBq`u=W8!ADP~EsV1)jNu@zj%f3wLx}v)7OCcFAMZd5*qGorHxWt4TD+>dN?3QY_)= z$p!qds&%_0SDu>j8$CLF>r8^2rr5eM~gC%;fk|TmR8^gnIBjYik7(H;z)(es*0thV^T6SXYj>C zu!skfd14B6qBb>IRY_RjP?Z!i#`0%^dVt|lp~&I0TJP8&?2=1|h|3&=_!lLUM?W#@ zpS|_%3#R&*opmJsGpTe4n`KN@#iL|pM=P1?yYtS>Hl?DgzYWxR!aDUmS*6zRRm_cq z%mqT8xPyuN3T45u%(>TFt(*Qk-c@gGvf?i5`$oV_AG3U_zOJHupUJ}Ua*3Pf9+#sY z^(K#<&nAh%Q%%z&Lg2QXf8!>!>spK4fAl!91Qhj5a-Msw$vImI?WT%n5#8fMw$VD`hlWlKs$hB7gpg^1WF^n-8!{^7MGXl<>p?#w zH^_<1Fy~1NiF^1N31I`+X39i91G)1coiXb)wy@ODcI%2M9O%}-HuTBFAG=oi&Q;w{ z)TcXhza(=aM%yBOnFBLUizVh$9O#Q$L@3jWJZ zlyG;mc7g(!SpZy8P>89UjgzCasT%~qEG@(XtARpP8R=vAa!k-H=MHW_V#9`j*bu*ptiM*D**O) zwy=aDvjbj3psuh`0KA;seBAtkJRp8v4jxVr2RA1lkdsFMV8Hw*0A(8w2mlr)zX1!t z&e_z>+7)67@c5@Gh?9?#8y1y1)XBo#{Ew4=fd7pF6KT1d*+R_S{)$)I#?2lA5VnL^ zS^|N5yg(obMg{&*i~oDf0|Wy31^zjQO+e)br$UpcXZWsnk4ifx_ z><|Ah-G5^Ku?79nhvEE%4a4~h^KZMqG5`32+5FJ~{R{8^*nnYvU_Sne_m8hXalyK&)|%Hk*)+o*7fm~0XZuEH521k&iiGNgY55lw_#?aVGG<(G^(_(%xr`wk ziKtBwyaf*l+|qF*o@s(3Jx@zqGG|KDIA@HEHgbCsoD6?b>5DPMdbM}*)H+}7$nMkW zSNaFgh;y-CpGKQpa!F*2BYWU%L2!pyFzDWX3X&paAI8UKjO{8C0FQhSFH(hHcwTn~ zF5u7+U5Gc?*(21`1W7&8L}()qqe5G8_dG{=;MST>2 z(yswc>1BXFqvpB(e=h}q|Nj1)f&XUUPX^Fk-AtiwUa&hEIuADp{pCv;HCgok0g6Wg AlK=n! literal 0 HcmV?d00001 diff --git a/docs/action_policy_closed_loop_eval.md b/docs/action_policy_closed_loop_eval.md new file mode 100644 index 00000000..e4bbdad4 --- /dev/null +++ b/docs/action_policy_closed_loop_eval.md @@ -0,0 +1,409 @@ +# Action Policy Closed-Loop Evaluation on LIBERO + + + +______________________________________________________________________ + +**Table of Contents** + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Set Up LIBERO](#set-up-libero) +- [Start the Action Model Server](#start-the-action-model-server) + - [Server Options](#server-options) + - [HTTP API Reference](#http-api-reference) + - [GET Health Check](#get-health-check) + - [GET Info](#get-info) + - [POST Predict](#post-predict) + - [Client Control Loop](#client-control-loop) +- [Run the LIBERO Evaluation Client](#run-the-libero-evaluation-client) +- [Optional Dataset Action Server](#optional-dataset-action-server) +- [Outputs](#outputs) +- [Common Options](#common-options) +- [Troubleshooting](#troubleshooting) + - [Server Starts but Client Gets Empty Actions](#server-starts-but-client-gets-empty-actions) + - [Success Rate Is Near Zero](#success-rate-is-near-zero) + - [MuJoCo or OpenGL Fails to Initialize](#mujoco-or-opengl-fails-to-initialize) + - [LIBERO Config Is Missing](#libero-config-is-missing) + - [`Numba needs NumPy 2.2 or less. Got NumPy 2.4.`](#numba-needs-numpy-22-or-less-got-numpy-24) + +______________________________________________________________________ + + + +## Overview + +LIBERO closed-loop evaluation runs as two HTTP-connected processes: + +- **Action model server**: loads a Cosmos3 Action policy checkpoint on GPU and serves `POST /predict`. +- **LIBERO evaluation client**: runs the LIBERO simulator, sends rendered observations to the server, executes returned actions, and writes success metrics. + +The client and server can run on the same machine or on separate machines. Use separate machines or virtual environments when your LIBERO simulator dependencies differ from the model-serving environment. + +## Prerequisites + +Start from the root of the released Cosmos3 repository: + +```shell +git clone git@github.com:nvidia-cosmos/cosmos3.git +cd cosmos3 +``` + +Install Cosmos3 with the CUDA/training dependencies needed to load DCP checkpoints, plus the `libero` group for the evaluation client: + +```shell +uv sync --all-extras --group=cu130-train --group=libero +source .venv/bin/activate +export LD_LIBRARY_PATH= +``` + +Use `--group=cu128-train` instead if your environment uses the CUDA 12.8 dependency group. See [Setup](./setup.md) for the full installation matrix. If the model server and LIBERO client need separate environments, omit `--group=libero` from the server env and create a second venv with only `--group=libero` for the client. + +You also need: + +- A LIBERO-compatible Cosmos3 Action policy checkpoint. +- The experiment name and config used by that checkpoint. +- Action normalization stats for the checkpoint, if the policy was trained with normalized actions. +- LIBERO simulator dependencies on the client side. + +## Set Up LIBERO + +LIBERO is installed via the `libero` dependency group from [Prerequisites](#prerequisites). Verify that the evaluation client can import LIBERO and resolve the benchmark paths: + +```shell +python - <<'PY' +from libero.libero import benchmark, get_libero_path +from libero.libero.envs import OffScreenRenderEnv + +benchmark_dict = benchmark.get_benchmark_dict() +task_suite = benchmark_dict["libero_10"]() +task = task_suite.get_task(0) +print(f"Loaded libero_10 task 0: {task.language}") +print(f"BDDL root: {get_libero_path('bddl_files')}") +print(f"Renderer: {OffScreenRenderEnv.__name__}") +PY +``` + +For headless machines, choose a MuJoCo rendering backend before launching the client: + +```shell +export MUJOCO_GL=egl +export PYOPENGL_PLATFORM=egl +``` + +Use `MUJOCO_GL=osmesa` for CPU rendering if EGL is not available. + +## Start the Action Model Server + +Run the server in the Cosmos3 environment on a GPU machine. For a checkpoint trained with the OSS +`cosmos3/configs/experiment/action_policy_sft_8b.yaml` config, point `--checkpoint-dir` at the local DCP +checkpoint and `--config-file` at the finalized training config saved by `cosmos3.scripts.train`. +The training output directory is the `-o` directory passed to `cosmos3.scripts.train`; the checkpoint +directory is under the job directory in `${IMAGINAIRE_OUTPUT_ROOT}`. + +```shell +TRAIN_OUTPUT_DIR=/path/to/train-output +CHECKPOINT_DIR=/path/to/job/checkpoints/iter_000002000 + +python -m cosmos3.scripts.action_policy_server \ + --experiment-name action_policy_sft_8b \ + --checkpoint-dir "${CHECKPOINT_DIR}" \ + --config-file "${TRAIN_OUTPUT_DIR}/config.yaml" \ + --host 0.0.0.0 \ + --port 8000 \ + --action-chunk-size 16 \ + --max-action-dim 64 \ + --raw-action-dim 10 \ + --action-stats-path cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json \ + --action-normalization quantile_rot \ + --guidance 1.0 \ + --num-steps 1 \ + --fps 20 +``` + +Notes: + +- `TRAIN_OUTPUT_DIR` should be the `-o` output directory passed to `cosmos3.scripts.train`. Its `job` symlink points to the checkpoint job directory. +- `--checkpoint-dir` can point to either a local DCP checkpoint directory or a remote checkpoint path supported by the configured checkpoint reader. If you use remote storage, pass `--credential-path`. +- `--config-file` should be the serialized `config.yaml` from the `cosmos3.scripts.train` `-o` output directory. Use `cosmos3/configs/experiment/action_policy_sft_8b.yaml` only when it exactly matches the run. +- `--raw-action-dim 10` matches LIBERO frame-wise relative actions with 6D rotation: `xyz(3) + rot6d(6) + gripper(1)`. +- `--max-action-dim 64`, `--action-chunk-size 16`, `--fps 20`, and `--action-normalization quantile_rot` match the released `action_policy_sft_8b.yaml` config. +- `--action-stats-path` should match the normalization statistics used by the checkpoint. The released package includes the LIBERO frame-wise relative 6D-rotation stats at `cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json`. + +To see all available server arguments: + +```shell +python -m cosmos3.scripts.action_policy_server --help +``` + +Check the server health endpoint: + +```shell +curl http://localhost:8000/ +curl http://localhost:8000/info +``` + +If the client runs on another machine, replace `localhost` with the server host or IP address. + +### Server Options + +| Argument | Default | Description | +| ------------------------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `--experiment-name` | required | Run label. For module-backed configs, this is also the Hydra experiment name. Use `action_policy_sft_8b` for OSS YAML configs. | +| `--checkpoint-dir` | required | Local or remote DCP checkpoint directory. The server appends `/model` when the path does not already end with it. | +| `--config-file` | `cosmos3/_src/vfm/configs/base/config.py` | Finalized training config YAML for OSS-trained checkpoints, or a config module file for registry-backed checkpoints. | +| `--credential-path` | empty | Credential file for remote checkpoint storage. Leave empty for local checkpoints. | +| `--local-cache-dir` | unset | Local cache root for remote checkpoints. | +| `--seed` | `0` | Random seed for model loading and generation. | +| `--guidance` | `1.0` | Classifier-free guidance scale used during denoising. | +| `--num-steps` | `30` | Number of denoising steps per policy request. | +| `--fps` | `20` | FPS metadata appended to the prompt when the checkpoint config enables duration/FPS augmentation. | +| `--action-chunk-size` | inferred, fallback `16` | Number of action steps predicted per request. | +| `--max-action-dim` | inferred, fallback `64` | Padded action width expected by the model. | +| `--raw-action-dim` | inferred from stats, otherwise unset | Unpadded action width returned to the client. Use `10` for LIBERO 6D-rotation actions. | +| `--action-stats-path` | unset | JSON stats file used to denormalize generated actions. | +| `--action-normalization` | `auto` | Normalization to invert: `auto`, `minmax`, `meanstd`, `quantile`, or `quantile_rot`. Use `quantile_rot` for `action_policy_sft_8b.yaml`. | +| `--dump-dir` | unset | Directory for request dumps, generated videos, and predicted actions. | +| `--dump-every` | `1` | Dump every N-th request when `--dump-dir` is set. | +| `--http-400-on-error` | disabled | Return HTTP 400 on request errors instead of HTTP 200 with an empty action list. | +| `--host` | `0.0.0.0` | Host address to bind. | +| `--port` | `8000` | Port to listen on. | + +### HTTP API Reference + +The released LIBERO client uses these endpoints, and custom environment clients can use the same interface. + +#### GET Health Check + +Health check endpoint. + +Response: + +```json +{"status": "ok"} +``` + +#### GET Info + +Returns model and server configuration useful for recording reproducible evaluation metadata. + +Example response: + +```json +{ + "run_name": "", + "checkpoint": "/path/to/checkpoints/iter_000002000", + "guidance": 1.0, + "num_steps": 30, + "fps": 20, + "seed": 0, + "action_chunk_size": 16, + "max_action_dim": 64, + "raw_action_dim": 10, + "action_stats_path": "/path/to/libero_action_stats.json" +} +``` + +#### POST Predict + +Runs policy inference for one observation. + +Request: + +```json +{ + "image": "", + "prompt": "", + "domain_name": "libero", + "image_size": 256 +} +``` + +| Field | Type | Required | Description | +| ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------- | +| `image` | `string` | Yes | Base64-encoded PNG observation. Multi-view clients concatenate resized views horizontally. | +| `prompt` | `string` | Yes | Natural-language task description. | +| `domain_name` | `string` | Yes | Action domain identifier. Use `libero` for LIBERO checkpoints. | +| `image_size` | `int` | Yes | Observation height used by the model input. For multi-view images, width may be a multiple of this value. | + +Response: + +```json +{ + "action": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], + "video": ["", ""] +} +``` + +| Field | Type | Description | +| -------- | --------------------- | ------------------------------------------------------------------------------------------------- | +| `action` | `list[list[float]]` | Predicted action chunk with shape `action_chunk_size x raw_action_dim`. | +| `video` | `list[string]` | Optional base64 PNG rollout frames returned by the model, usually `action_chunk_size + 1` frames. | +| `error` | `string` | Present when request processing fails. | + +Error response: + +```json +{ + "action": [], + "error": "", + "request_id": 1 +} +``` + +### Client Control Loop + +The bundled LIBERO client implements the standard closed-loop pattern: + +1. Wait for `GET /` to report a healthy model server. +2. Reset the simulator and load a LIBERO initial state. +3. Render the configured camera view or horizontally concatenated multi-view observation. +4. Send the PNG observation, task prompt, domain name, and image size to `POST /predict`. +5. Queue the returned action chunk. +6. Execute `--action_horizon` actions in the simulator, or the full chunk when `--action_horizon=0`. +7. Repeat prediction and execution until success, termination, error, or `--max_steps`. +8. Call `POST /next_episode` when using the dataset action server so it advances its per-task episode cursor. + +Use the same loop when adapting the HTTP server to a different simulator. The action post-processing from model action vectors to environment commands is simulator-specific; for LIBERO it is already implemented in `cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval`. + +## Run the LIBERO Evaluation Client + +Run the client in an environment with LIBERO installed. The OSS `action_policy_sft_8b.yaml` config trains on +concatenated `agentview` and wrist observations, so evaluate it with `--camera agentview,wrist`: + +```shell +python -m cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval \ + --server_url http://localhost:8000 \ + --task_suite libero_10 \ + --num_trials_per_task 20 \ + --action_horizon 16 \ + --action_dim 10 \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --domain_name libero \ + --camera agentview,wrist \ + --mujoco_gl auto \ + --output_dir outputs/libero_closed_loop/libero_10_multiview +``` + +For checkpoints trained with a single camera, change only the camera and output directory with flag `--camera agentview` + +`--save_comparison` writes side-by-side GIFs comparing the model-predicted rollout returned by the server with the actual environment rollout. + +To see all available client arguments: + +```shell +python -m cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval --help +``` + +## Optional Dataset Action Server + +Use the dataset action server to validate the LIBERO closed-loop client with ground-truth actions from a LeRobot-format LIBERO dataset. It implements the same HTTP interface as the model server, so the client command stays the same except for `--server_url`. + +Start the dataset server: + +```shell +python -m cosmos3._src.vfm.evaluation.action.libero.dataset_reply_action_server \ + --repo_id libero_10 \ + --root /path/to/libero_10_lerobot \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --pose_coordinate_frame opencv \ + --action_chunk_size 16 \ + --max_action_dim 64 \ + --send_video \ + --camera_mode agentview \ + --host 0.0.0.0 \ + --port 8001 +``` + +Then point the evaluation client at it: + +```shell +python -m cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval \ + --server_url http://localhost:8001 \ + --task_suite libero_10 \ + --num_trials_per_task 3 \ + --task_ids 0 \ + --action_horizon 16 \ + --action_dim 10 \ + --action_space frame_wise_relative \ + --rotation_space 6d \ + --camera agentview \ + --output_dir outputs/libero_closed_loop/dataset_server_smoke +``` + +This is useful for debugging camera orientation, action-space settings, and LIBERO initial states before testing a learned policy. + +## Outputs + +The evaluation client prints per-episode and per-task success rates and writes: + +```text +outputs/libero_closed_loop/libero_10/ ++-- summary.json ++-- actions/ ++-- gifs/ # only when --save_gifs is set ++-- comparisons/ # only when --save_comparison is set +``` + +`summary.json` contains the selected task IDs, number of episodes, per-task success rates, overall success rate, action-space settings, and per-episode errors if any. + +## Common Options + +| Option | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `--task_suite` | One of `libero_spatial`, `libero_object`, `libero_goal`, `libero_10`, `libero_90`. | +| `--task_ids` | Comma-separated task IDs. Omit to run all tasks in the suite. | +| `--num_trials_per_task` | Number of LIBERO initial states to evaluate for each selected task. | +| `--camera` | `agentview`, `wrist`, or comma-separated `agentview,wrist` for concatenated multi-view input. | +| `--action_space` | `frame_wise_relative` for per-step deltas, or `relative` for anchored relative actions. Must match the checkpoint. | +| `--rotation_space` | `3d`, `6d`, `9d`, or `auto`. Must match the action representation returned by the server. | +| `--action_dim` | Unpadded action width returned by the server: usually `7` for axis-angle or `10` for 6D rotation. | +| `--action_horizon` | Number of actions to execute from each server response. `0` executes the full returned chunk. | +| `--mujoco_gl` | `auto`, `egl`, `osmesa`, or `glfw`. Use `egl` for headless GPU rendering and `osmesa` for CPU rendering. | +| `--initial_states_path` | `DEFAULT` uses LIBERO benchmark initial states. Pass a JSON file to use custom initial states. | + +## Troubleshooting + +### Server Starts but Client Gets Empty Actions + +Check the server logs for request errors. For stricter HTTP behavior, launch the server with `--http-400-on-error` so request failures return HTTP 400 instead of an empty action list. + +### Success Rate Is Near Zero + +Confirm that the following settings match the checkpoint: + +- client `--action_space` +- client `--rotation_space` +- client `--action_dim` +- server `--action-chunk-size` +- server `--action-stats-path` +- camera choice and image orientation flags such as `--rotate_180` + +### MuJoCo or OpenGL Fails to Initialize + +Try an explicit backend: + +```shell +MUJOCO_GL=egl PYOPENGL_PLATFORM=egl python -m cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval ... +``` + +If EGL is unavailable, use: + +```shell +MUJOCO_GL=osmesa PYOPENGL_PLATFORM=osmesa python -m cosmos3._src.vfm.evaluation.action.libero.closed_loop_eval ... +``` + +### LIBERO Config Is Missing + +Set `LIBERO_CONFIG_PATH` to a writable directory and rerun the config snippet in [Set Up LIBERO](#set-up-libero). + +### `Numba needs NumPy 2.2 or less. Got NumPy 2.4.` + +LIBERO pulls in `robosuite`, which depends on `numba`; current `numba` releases support only `numpy<2.3`. Cosmos3 caps `numpy<2.3` via `[tool.uv].override-dependencies` whenever the `libero` group is part of a sync. If you see this error, you most likely synced without that override active — re-resolve the lockfile and reinstall NumPy: + +```shell +uv lock --upgrade-package numpy +uv sync --all-extras --group=cu130-train --group=libero --reinstall-package numpy +``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..7a686641 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,224 @@ +# Cosmos3 FAQ + +> **Skills:** `.agents/skills/cosmos3-setup/SKILL.md` · `.agents/skills/cosmos3-inference/SKILL.md` + +A catch-all collection of frequently asked questions, tips, and troubleshooting for the Cosmos3 package. Can't find what you need? Check [setup.md](./setup.md) for installation issues or [inference.md](./inference.md) for inference details. + +To add a new entry, append it under the most relevant section — or under [Miscellaneous](#miscellaneous) if nothing fits. + +--- + +## Table of Contents + +- [Setup and Installation](#setup-and-installation) +- [Configuration and Defaults](#configuration-and-defaults) +- [Inference](#inference) +- [Tips and Tricks](#tips-and-tricks) +- [Miscellaneous](#miscellaneous) + +--- + +## Setup and Installation + +### Q: I get `ImportError: cannot import name '_functionalization' from 'torch._C'` inside an NGC container + +Clear the library path before running anything: + +```shell +export LD_LIBRARY_PATH='' +``` + +This is needed because the NGC PyTorch container ships its own libraries that conflict with the venv-installed versions. See [setup.md#pytorch-import-issue](./setup.md#pytorch-import-issue). + +### Q: `ModuleNotFoundError: No module named 'cosmos3'` + +Make sure you installed the package: + +```shell +uv sync --all-extras --group=cu130 +source .venv/bin/activate +``` + +If already installed, try `--reinstall` to force a clean state. + +### Q: Which CUDA version should I use? + +CUDA 13.0 is recommended. CUDA 12.8 is also supported. The major version must match between your system CUDA and the installed PyTorch wheels. Check with: + +```shell +nvidia-smi # system CUDA +python -c "import torch; print(torch.version.cuda)" # PyTorch CUDA +``` + +### Q: How do I download model checkpoints? + +Checkpoints are downloaded automatically from Hugging Face during inference. You need: + +1. A [Hugging Face token](https://huggingface.co/settings/tokens) with Read permission +2. Accepted [NVIDIA Open Model License Agreement](https://huggingface.co/nvidia/Cosmos-Guardrail1) +3. `HF_TOKEN` environment variable set, or `uvx hf auth login` + +Control the download location with `HF_HOME` (default: `~/.cache/huggingface`). If downloads fail, the commands are printed to the console — run them manually to debug. See [setup.md#downloading-checkpoints](./setup.md#downloading-checkpoints). + +### Q: `fatal error: Python.h: No such file or directory` + +Reinstall uv and the venv from scratch: + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +uv python install --reinstall +rm -rf .venv +uv sync --all-extras --group=cu130 --reinstall +source .venv/bin/activate +``` + +--- + +## Configuration and Defaults + +### Q: Where are the default inference parameters (guidance, shift, num_steps, etc.)? + +Per-modality defaults live in JSON files under `cosmos3/defaults//sample_args.json`: + +| Mode | Default file | +| ------------- | ----------------------------------------------- | +| `text2image` | `cosmos3/defaults/text2image/sample_args.json` | +| `text2video` | `cosmos3/defaults/text2video/sample_args.json` | +| `image2video` | `cosmos3/defaults/image2video/sample_args.json` | + +See [AGENTS.md](../AGENTS.md) for the full config defaults chain. + +### Q: How do I override a default parameter? + +From most temporary to most permanent: + +1. **CLI flag**: `--shift 5.0` (per-run, applies to all samples) +2. **Sample argument file**: set the field in your input JSON (per-sample) +3. **Custom defaults file**: pass `"defaults_file": "my_defaults.json"` in your sample argument file (see [inference.md#custom-defaults](./inference.md#custom-defaults)) +4. **Built-in default**: edit `cosmos3/defaults//sample_args.json` (permanent change) + +Fields set in the sample argument file take precedence over defaults. CLI flags override both. + +### Q: What is the `shift` parameter and which value should I use? + +`shift` controls the time-shift in the UniPC diffusion sampler. Higher values produce more detail but can introduce artifacts. Recommended values: + +| Model | Recommended shift | +| ------------------- | ----------------- | +| Cosmos3-Nano (8B) | `10.0` (default) | +| Cosmos3-Super (32B) | `5.0` | + +### Q: How do I add a new parameter to the inference pipeline? + +1. Add the field to `SamplingArgs` and `SamplingOverrides` in `cosmos3/args.py` +2. Add its default to each `cosmos3/defaults//sample_args.json` +3. Wire it through `OmniSampleOverrides.build_sample()` in `args.py` + +### Q: What does `defaults_file` do? + +It lets you supply a custom JSON file of default values instead of the built-in presets. The format is the same as the files in `cosmos3/defaults/`. Fields in your sample argument file still take precedence over the custom defaults. See [inference.md#custom-defaults](./inference.md#custom-defaults). + +--- + +## Inference + +### Q: How much GPU memory do the models need? + +| Model | GPU Memory | +| ------------------- | ---------- | +| Cosmos3-Nano (8B) | 32 GB | +| Cosmos3-Super (32B) | 128 GB | + +### Q: What is the difference between `latency` and `throughput` parallelism presets? + +| Preset | What it does | When to use | +| ------------ | ----------------------------------- | --------------------------- | +| `latency` | Spreads each sample across all GPUs | Interactive / real-time use | +| `throughput` | One sample per GPU in parallel | Large batch jobs | + +### Q: How do I generate images instead of videos? + +Use a text2image input file: + +```shell +python -m cosmos3.scripts.inference -i inputs/omni/t2i.json -o outputs/ --checkpoint-path Cosmos3-Nano +``` + +The modality is determined by the input JSON (`num_frames=1` for images), not by a separate flag. See `inputs/omni/t2i.json` for the format. + +### Q: How many frames can I generate? + +Depends on resolution: + +| Resolution | Max frames | +| ---------- | ---------- | +| 256p | 400 | +| 480p | 300 | +| 720p | 200 | + +Default is 189 frames at 24 FPS (~7.9 seconds). + +### Q: What input formats does image-to-video support? + +Provide a `vision_path` pointing to an image (`.jpg`, `.jpeg`, `.png`) or a URL. See `inputs/omni/i2v.json` for the format. + +### Q: How do I run online inference with Ray? + +Install serve dependencies and start the server: + +```shell +uv pip install -e ".[serve]" +python -m cosmos3.ray.serve --parallelism-preset=latency -o outputs/ray_serve --checkpoint-path Cosmos3-Nano +``` + +Then submit requests via curl, the submit CLI, or the Gradio UI. See [inference_online.md](./inference_online.md) for details. + +--- + +## Tips and Tricks + +### Seed reproducibility + +Always pass `--seed` when comparing runs. Without it, a random seed is used each time. + +### Prompt upsampling + +Short prompts produce worse results. Use the built-in prompt upsampler with a vLLM-served Qwen3 model: + +```shell +python -m cosmos3.scripts.upsample_prompts -i "inputs/omni/*.json" -o outputs/upsample_prompts +``` + +See [prompting.md#upsampling](./prompting.md#upsampling) for full setup instructions. + +### Batch inference resume + +The inference script automatically skips samples whose output files already exist. If a run is interrupted, re-run the same command to resume. + +### Generate all modalities at once + +```shell +python -m cosmos3.scripts.inference -i "inputs/omni/*.json" -o outputs/ --checkpoint-path Cosmos3-Nano --seed=0 +``` + +### CLI help + +All available flags and their current defaults: + +```shell +python -m cosmos3.scripts.inference --help +``` + +--- + +## Miscellaneous + +*This section is a catch-all for tips that don't fit elsewhere. Add new entries freely.* + +### Q: What are the example scripts in `examples/` for? + +They illustrate how the inference logic works under the hood — `examples/inference.py` shows the low-level model API and `examples/inference_pipeline.py` shows the pipeline API. For production use, prefer `python -m cosmos3.scripts.inference`. + +### Q: Where are the Ray Serve config files? + +`cosmos3/ray/configs/latency.yaml` and `cosmos3/ray/configs/throughput.yaml`. These configure the Ray Serve deployment with different parallelism strategies. diff --git a/docs/gallery.md b/docs/gallery.md new file mode 100644 index 00000000..1b331d81 --- /dev/null +++ b/docs/gallery.md @@ -0,0 +1,67 @@ +# Example Gallery + + + +______________________________________________________________________ + +**Table of Contents** + +- [Text2Video](#text2video) +- [Image2Video](#image2video) +- [Forward Dynamics](#forward-dynamics) + +______________________________________________________________________ + + + +## Text2Video + +

Input prompt + +> The video opens with a view of a well-lit indoor space featuring a wooden display case with compartments filled with various fruits, including bananas, apples, pears, oranges, and carambolas. The bananas are neatly arranged in the middle compartment, while apples are in the left and a mix of pears, oranges, and carambolas are in the right. Two robotic arms with grippers are positioned at the bottom of the frame, with the one on the left remaining stationary, partially obscuring the apples. The robotic arm on the right begins its action, extending towards the right side of the display case. It carefully picks up a pear from the fruit section, placing it into a plastic bag in the shopping cart nearby, which has red handles. After securing the pear, the arm retracts back to its original position. The process repeats as the robotic arm picks up an orange and places it in the bag, followed by a carambola. The final frame captures the robotic arm returning to its initial position, leaving the display case and surrounding area unchanged. The video showcases a seamless and efficient automated fruit-picking process, highlighting the precision and efficiency of modern robotics in a retail setting. + +
+ +
Additional Parameters + +- checkpoint-path=Cosmos3-Nano +- num_steps=50 +- shift=10.0 +- seed=5 + +
+ +| Output Video | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | +| | + +## Image2Video + +
Input prompt + +> The video opens with a view of a testing environment, characterized by a large wooden table at the center. On this table, two robot arms are positioned at opposite ends, with the left arm closer to the camera and the right arm further away. Between the hands lies a dark wooden shelf with a red spherical object on its top rack, likely serving as a platform or obstacle. In the background, various pieces of equipment, including a tripod, a chair, are visible. A person wearing a blue jacket and black pants stands near the center of the room, observing the experiment, with a static hand position throughout. The floor is tiled with a patterned design, and additional items like a small robot figure and some cables can be seen scattered around the space. As the video progresses, the right robotic hand extends outward, moving from its initial position towards the red spherical object on the shelf. The hand then picks up the object and places it on the lowest rack of the shelf, completing a smooth, deliberate manipulation. The left robotic hand remains stationary throughout the sequence. No new objects appear in the video; all existing elements maintain their positions except for the movement of the right robotic hand. The scene concludes with the right robotic hand returning to its initial position, while the left hand continues to rest on the table. The overall environment remains unchanged, with the focus remaining on the interaction between the robotic hands and the wooden block, highlighting precise control during the demonstration. + +
+ +
Additional Parameters + +- checkpoint-path=Cosmos3-Super +- num_steps=50 +- shift=5.0 +- seed=0 + +
+ +| Input Image | Output Video | +| ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| | | + +## Forward Dynamics + +Action inference uses SO(3) pose representations. + +| Conditioning | Output Video | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| Camera conditioned | | +| Camera conditioned | | +| Action conditioned | | diff --git a/docs/inference.md b/docs/inference.md new file mode 100644 index 00000000..1677d847 --- /dev/null +++ b/docs/inference.md @@ -0,0 +1,301 @@ +# Inference + +> **Skill:** `.agents/skills/cosmos3-inference/SKILL.md` + + + +______________________________________________________________________ + +**Table of Contents** + +- [Setup Arguments](#setup-arguments) + - [Parallelism](#parallelism) +- [Sample Arguments](#sample-arguments) + - [Condition](#condition) + - [Text](#text) + - [Vision (Image/Video)](#vision-imagevideo) + - [Action](#action) + - [Generation](#generation) + - [Vision (Image/Video)](#vision-imagevideo-1) + - [Action](#action-1) +- [Action Inference](#action-inference) + - [Action Modes](#action-modes) + - [Action Configuration](#action-configuration) +- [Default Values](#default-values) + - [Custom Defaults](#custom-defaults) +- [Schema Reference](#schema-reference) +- [Troubleshooting](#troubleshooting) + - [Checkpoint Issue](#checkpoint-issue) + - [Torch CUDA Out of Memory Error](#torch-cuda-out-of-memory-error) + - [NCCL Issue](#nccl-issue) + - [NCCL Plugin Issue](#nccl-plugin-issue) +- [Supplementary Examples](#supplementary-examples) + +______________________________________________________________________ + + + +This guide applies to the following: + +- [Offline Batch Inference](../README.md#offline-batch-inference) +- [Online Inference](./inference_online.md) + +## Setup Arguments + +### Parallelism + +| Parallelism | Arguments | Description | +| ----------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Latency | `--parallelism-preset=latency` | Generates each sample as fast as possible by spreading work across GPUs. Use for interactive or real-time use. | +| Throughput | `--parallelism-preset=throughput` | Generates many samples in parallel, one per GPU. Use for large batch jobs. | + +## Sample Arguments + +Each inference run takes one or more **sample argument files** (JSON, JSONL, or YAML) that describe what to generate: + +```shell +python -m cosmos3.scripts.inference \ + -i "inputs/omni/t2i.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +| CLI Argument | Description | +| --------------------- | ---------------------------------------------------------------------------------- | +| `-i`, `--input-files` | Path to the sample argument file(s). Accepts glob patterns (e.g. `inputs/*.json`). | +| `-o`, `--output-dir` | Output directory. | + +General sample arguments: + +| Argument | Description | +| -------- | ---------------------------------------------- | +| `name` | Output subfolder name (inside `--output-dir`). | +| `seed` | Random seed for reproducibility. | + +### Condition + +Condition fields control the inputs to generation. Paths are relative to the input file. + +#### Text + +Provide a text prompt inline via `prompt`, or point to a `.txt` file via `prompt_path`. If both are provided, `prompt` takes precedence. See [inputs/omni/t2v.json](../inputs/omni/t2v.json) for an example. + +| Argument | Description | +| ----------------- | ---------------------------------------------------------------- | +| `prompt` | Inline text prompt. | +| `prompt_path` | Path to a `.txt` file with the prompt (alternative to `prompt`). | +| `negative_prompt` | Describes what to avoid in the output. | + +#### Vision (Image/Video) + +Provide an image or video via `vision_path`. + +| Argument | Description | +| ------------- | --------------------------------------------------- | +| `vision_path` | Path to an image or video file (local path or URL). | + +- Image conditioning: see [inputs/omni/i2v.json](../inputs/omni/i2v.json) + +#### Action + +Action inference is enabled by setting `action_mode` in the sample argument file. The examples live in [`inputs/omni/`](../inputs/omni/). + +| Argument | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `action_mode` | Selects the action task: `forward_dynamics`, `inverse_dynamics`, or `policy`. This also selects the matching default preset. | +| `vision_path` | Observation image or video. URLs are downloaded into the sample output's `inputs/` directory before generation. | +| `prompt` | Text instruction or scene/task description used as the action sample caption. | +| `domain_name` | Domain name passed to the action domain registry, such as `libero` or `av`. Use the domain used by the checkpoint's action training data. | +| `image_size` | Action input resize bucket. The value is passed as the action media resolution bucket; examples use `256` for LIBERO and `480` for AV. | +| `fps` | Conditioning FPS and output video FPS. | +| `action_chunk_size` | Number of action steps in the chunk. The action media loader reads at most `action_chunk_size + 1` observation frames. | +| `action_path` | JSON action sequence. Required for `forward_dynamics`; each row is one action step and each column is one raw action dimension. | +| `raw_action_dim` | Raw action width to return for generated actions. Required for `inverse_dynamics` and `policy`. | + +`action_path` files are plain JSON arrays with shape `action_chunk_size x raw_action_dim`, for example `[[...], [...], ...]`. + +For example, [`inputs/omni/action_forward_dynamics_robot.json`](../inputs/omni/action_forward_dynamics_robot.json) conditions on an observation image, a task prompt, and an action JSON to generate a future rollout. [`inputs/omni/action_inverse_dynamics_av.json`](../inputs/omni/action_inverse_dynamics_av.json) conditions on a video and predicts an action sequence with `raw_action_dim`. + +### Generation + +#### Vision (Image/Video) + +Outputs `vision.jpg` or `vision.mp4` depending on `num_frames`. + +| Argument | Type | Description | +| -------------- | ----------------------------------------------------- | ---------------------------------------------------- | +| `num_frames` | `int` | Number of output frames. `1` = image; `≥24` = video. | +| `fps` | `int` | Frames per second. | +| `resolution` | `"256"` \| `"480"` \| `"720"` | Output resolution (height in pixels). | +| `aspect_ratio` | `"1,1"` \| `"4,3"` \| `"3,4"` \| `"16,9"` \| `"9,16"` | Output aspect ratio. Defaults to `16:9`. | + +#### Action + +Action samples still write the generated visual output as `vision.mp4` in each sample subdirectory. When the model returns predicted actions, `sample_outputs.json` also contains `outputs[0].content.action` as a JSON list of action rows. + +Use `--debug` to save raw generated tensors in `output.safetensors` and non-tensor debug data in `output.pickle`. + +Typical action output layout: + +```text +outputs/omni_nano/action_forward_dynamics_robot/ ++-- inputs/ ++-- sample_args.json ++-- sample_outputs.json ++-- vision.mp4 +``` + +## Action Inference + +Run one action sample: + +```shell +python -m cosmos3.scripts.inference \ + -i inputs/omni/action_forward_dynamics_robot.json \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano\ + --seed=0 +``` + +Run the bundled forward dynamics examples: + +```shell +python -m cosmos3.scripts.inference \ + -i "inputs/omni/action_forward_dynamics_*.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +For standalone `policy` or `inverse_dynamics` runs, include `raw_action_dim` in the sample JSON. + +For S3 or other DCP checkpoints, use the same checkpoint arguments as regular inference, for example `--credential-path`, `--checkpoint-cache-dir`, `--config-file`, and `--experiment` when they are needed by the checkpoint. + +### Action Modes + +| Mode | Inputs | Outputs | Required action fields | +| ------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------- | +| `forward_dynamics` | Observation image/video, text prompt, action sequence | Future visual rollout in `vision.mp4` or `vision.jpg` | `action_path` | +| `inverse_dynamics` | Observation video, text prompt | Predicted action sequence in `sample_outputs.json` | `raw_action_dim` | +| `policy` | Current observation image/video, text prompt | Predicted action sequence, and any visual output returned by the checkpoint | `raw_action_dim` | + +### Action Configuration + +The action sample fields control input preprocessing and action tensor shape. `action_chunk_size` should match the chunk length used by the checkpoint. `image_size` should match the action training/evaluation resolution bucket. `domain_name` must be compatible with the checkpoint's action domain registry. + +Action tensors are padded to `model.config.max_action_dim` before generation. Set it with `--experiment_overrides "[model.config.max_action_dim=]"` when the checkpoint config does not already define the desired padded width. Use a value greater than or equal to the raw action width in `action_path` or `raw_action_dim`. + +## Default Values + +Each modality ships with a built-in preset that supplies sampling parameters (`num_steps`, `guidance`, `shift`, ...), negative prompts, and output settings. These presets are applied automatically. Any field you set in your sample argument file takes precedence over the preset. + +The built-in presets live in the package under `cosmos3/defaults/`: + +| Modality | Preset File | +| -------------- | --------------------------------------------------------------------------------------------------- | +| Text-to-Image | [`cosmos3/defaults/text2image/sample_args.json`](../cosmos3/defaults/text2image/sample_args.json) | +| Text-to-Video | [`cosmos3/defaults/text2video/sample_args.json`](../cosmos3/defaults/text2video/sample_args.json) | +| Image-to-Video | [`cosmos3/defaults/image2video/sample_args.json`](../cosmos3/defaults/image2video/sample_args.json) | + +Action presets use the same sample argument format: + +| Modality | Preset File | +| ---------------- | ------------------------------------------------------------------------------------------------------------- | +| Forward Dynamics | [`cosmos3/defaults/forward_dynamics/sample_args.json`](../cosmos3/defaults/forward_dynamics/sample_args.json) | +| Inverse Dynamics | [`cosmos3/defaults/inverse_dynamics/sample_args.json`](../cosmos3/defaults/inverse_dynamics/sample_args.json) | +| Policy | [`cosmos3/defaults/policy/sample_args.json`](../cosmos3/defaults/policy/sample_args.json) | + +> **Tip:** Only the parameters listed in `python -m cosmos3.scripts.inference --help` are recommended to change. The remaining fields in the preset files are internal and may break generation if altered. + +### Custom Defaults + +To use your own default values instead of the built-in presets, pass a JSON file via the `defaults_file` field in your sample arguments: + +```json +{ + "defaults_file": "my_defaults.json", + "prompt": "..." +} +``` + +The custom defaults file has the same format as the built-in presets. Fields you set explicitly in the sample argument file still take precedence over the custom defaults file. + +## Schema Reference + +The `schemas/` directory contains auto-generated reference files listing every available argument with types, constraints, and descriptions. These files are the authoritative reference for field names and valid values. + +| File | Description | +| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [`schemas/OmniSampleOverrides.yaml`](../schemas/OmniSampleOverrides.yaml) | All sample arguments with default values and inline comments. | +| [`schemas/OmniSampleOverrides.schema.json`](../schemas/OmniSampleOverrides.schema.json) | JSON Schema with types, enums, and validation constraints. | +| [`schemas/OmniSetupOverrides.yaml`](../schemas/OmniSetupOverrides.yaml) | All setup/CLI arguments with default values and inline comments. | +| [`schemas/OmniSetupOverrides.schema.json`](../schemas/OmniSetupOverrides.schema.json) | JSON Schema with types, enums, and validation constraints. | + +## Troubleshooting + +### Checkpoint Issue + +If you encounter failures downloading checkpoints, refer to [Downloading Checkpoints](./setup.md#downloading-checkpoints). + +Checkpoint download commands are printed to the console. You can run them manually to debug issues. + +### Torch CUDA Out of Memory Error + +Error: `torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate X MiB` + +[Optimize memory allocation](https://docs.pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-alloc-conf): + +```shell +export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True +``` + +### NCCL Issue + +Error: + +```shell +[rank0]:[W415 18:57:09.249883195 ProcessGroupNCCL.cpp:5138] Guessing device ID based on global rank. This can cause a hang if rank to GPU mapping is heterogeneous. You can specify device_id in init_process_group() + +Fatal Python error: Segmentation fault +``` + +Re-run with debugging enabled: + +```shell +export NCCL_DEBUG=INFO +export TORCH_DISTRIBUTED_DEBUG=DETAIL +export CUDA_LAUNCH_BLOCKING=1 +``` + +#### NCCL Plugin Issue + +Error: + +```shell +NCCL INFO Failed to initialize NET plugin Libfabric + +Fatal Python error: Segmentation fault +``` + +Fix: + +```shell +export NCCL_NET_PLUGIN=none +``` + +## Supplementary Examples + +The following scripts are provided for learning purposes - they illustrate how the inference logic works under the hood. For production use, prefer `cosmos3.scripts.inference` as shown above. + +Inference [example](../examples/inference.py) (low-level model API): + +```shell +python examples/inference.py +``` + +Inference [example](../examples/inference_pipeline.py) (pipeline API): + +```shell +python examples/inference_pipeline.py +``` diff --git a/docs/inference_online.md b/docs/inference_online.md new file mode 100644 index 00000000..0fd2c469 --- /dev/null +++ b/docs/inference_online.md @@ -0,0 +1,114 @@ +# Online Inference + +> **Skill:** `.agents/skills/cosmos3-inference/SKILL.md` + + + +______________________________________________________________________ + +**Table of Contents** + +- [Setup](#setup) +- [Usage](#usage) + +______________________________________________________________________ + + + +We recommend first reading [Inference](./inference.md). + +Useful resources: + +- [Ray Serve Documentation](https://docs.ray.io/en/latest/serve/index.html). + +## Setup + +Install the serve dependencies: + +```shell +uv pip install -e ".[serve]" +``` + +## Usage + +Start the server using one of the following methods: + +
Single model + +```shell +python -m cosmos3.ray.serve \ + --parallelism-preset=latency \ + --keep-going \ + -o outputs/ray_serve \ + --checkpoint-path Cosmos3-Nano +``` + +To see all available arguments: + +```shell +python -m cosmos3.ray.serve --help +``` + +
+ +
Multiple models + +```shell +serve run cosmos3/ray/configs/latency.yaml +``` + +
+ +To monitor, open the dashboard . + +Wait ~1 min until the server is ready. You can either monitor the dashboard or the log: + +``` +Application 'cosmos3_omni' is ready at http://127.0.0.1:8000/. +``` + +Models are not loaded until a request is submitted. In a separate terminal, submit requests using one of the following methods: + +
Curl/Wget + +```shell +curl -X POST http://localhost:8000/generate \ + -H "Content-Type: application/json" \ + -d '{"model": "", "name": "city", "prompt": "A bustling city street at night"}' \ + -N +``` + +
+ +
Submit CLI + +```shell +python -m cosmos3.ray.submit \ + -i "inputs/omni/*.json" \ + -o outputs/omni_ray \ + --seed=0 +``` + +To see all available arguments: + +```shell +python -m cosmos3.ray.submit --help +``` + +
+ +
Gradio UI + +Launch the gradio frontend at : + +```shell +python -m cosmos3.ray.gradio --port=8080 +``` + +To see all available arguments: + +```shell +python -m cosmos3.ray.gradio --help +``` + +
diff --git a/docs/prompting.md b/docs/prompting.md new file mode 100644 index 00000000..643bd16b --- /dev/null +++ b/docs/prompting.md @@ -0,0 +1,97 @@ +# Prompting + +> **Skill:** `.agents/skills/cosmos3-inference/SKILL.md` + + + +______________________________________________________________________ + +**Table of Contents** + +- [Vision Generation](#vision-generation) + - [Do](#do) + - [Don't](#dont) + - [Upsampling](#upsampling) +- [Action Generation](#action-generation) + - [Do](#do-1) + +______________________________________________________________________ + + + +## Vision Generation + +**Modalities:** Text2Video, Image2Video, Video2Video + +Write your prompt as a **rich, flowing narrative paragraph** that describes the scene exactly as it unfolds. + +Example: + +> The video begins with a view from inside a vehicle, likely captured by a dashboard camera, showing a wide street scene under an overcast sky. The road stretches ahead with multiple lanes, and several vehicles are visible, including a white car directly ahead and a larger bus to the right. The bus has an advertisement for a law firm on its side. On the left side of the street, there's a parking lot filled with cars, and various commercial buildings line either side, featuring signs and storefronts. The environment suggests a suburban or semi-urban area with palm trees scattered along the sidewalks, adding a touch of greenery to the otherwise urban landscape. As the video progresses, the vehicle continues to move forward down the street. The white car directly ahead remains in the same lane, maintaining a steady pace. The bus on the right side of the road begins to look closer to the ego vehicle, as the large size becomes more prominent while moving towards it. The advertisements, including one for a law firm, become clearer as the ego vehicle overtakes a bus stopped ahead. The surrounding environment remains consistent, with the parking lot on the left and commercial buildings on the right under the same cloudy sky. By the final frame, both the white car and the ego vehicle move forward from the bus, making it out of the frame, and the white car slightly turns right, with its right blinker on, continuing to show the same street with the same vehicles and surroundings, maintaining the calm and steady pace of the journey. + +[More examples](../inputs/t2v_long_prompts.jsonl) + +### Do + +1. **Describe actions chronologically**, grounded in spatial landmarks + - *"A woman walks through a wooden doorway"* rather than *"a woman comes into view"* +1. **Be highly specific** about visible details: + - Subject appearance, clothing, and movements + - Lighting and cinematic camera angles + - Overall atmospheric aesthetic +1. **Use the subject's perspective** for left/right directions +1. **Stick to objective, visible elements** — describe only what the camera can see +1. **Be vivid and precise** about the physical scene, environment, and temporal actions + +### Don't + +1. Use meta-phrases like *"generate a video of..."* or *"enters the frame"* +1. Reference invisible backstories or hidden emotions +1. Write abstract or vague descriptions + +> **Key takeaway:** The more vividly and precisely you describe the physical scene, environment, and temporal actions, the better the model can bring your vision to life. + +### Upsampling + +Prompt upsampling is strongly recommended. We provide an [upsampling template](../cosmos3/defaults/prompt_upsampler.txt) that is known to work well with [Qwen/Qwen3-VL-8B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct-FP8). + +To run locally, start a vLLM server. This will take a few minutes. + +```shell +uvx --with nvidia-cuda-runtime-cu12 \ +vllm@0.19.0 serve Qwen/Qwen3-VL-8B-Instruct-FP8 \ +--tensor-parallel-size 1 +``` + +The server is ready when you see `Application startup complete.` + +In a separate terminal, run the following. The outputs will be in the specified path. + +```shell +python -m cosmos3.scripts.upsample_prompts -i "inputs/omni/*.json" -o outputs/upsample_prompts +``` + +## Action Generation + +**Modalities:** Forward Dynamics, Inverse Dynamics, Policy + +Action generation prompts should be concise. Example: + +> Put the pot to the left of the purple item. This video is captured from a first-person perspective looking at the scene. + +### Do + +1. Use precise spatial language +Specify exact directional relationships ("to the left of") rather than vague terms like "near" or "next to." Relative positioning anchors the action clearly in 3D space. +1. Reference objects by distinct attributes +Identify objects by a unique, visible property ("the purple item") rather than just category names. Color, size, and shape help the model disambiguate when multiple similar objects are present. +1. State the action as a direct imperative +Begin with a verb in command form ("Put," "Move," "Place"). This keeps the prompt unambiguous about what should happen versus what is being described. +1. Specify the camera perspective explicitly +Include a perspective clause ("captured from a first-person perspective looking at the scene"). The same action looks very different from a top-down, third-person, or egocentric view, and models benefit from this being stated rather than inferred. +1. Keep the sentence structure simple and linear +One action, one object, one target location. Compound instructions ("pick up X, then put it left of Y, while facing Z") introduce ambiguity and increase failure modes. +1. Ground spatial references to the scene, not the viewer +"Left of the purple item" is scene-anchored. "To your left" or "in front of you" can shift meaning depending on assumed viewer orientation. Scene-anchored references are more stable. +1. Avoid hedging or descriptive filler +Phrases like "try to place" or "it would be nice if" weaken the instruction signal. Be declarative and specific. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 00000000..c84749f5 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,246 @@ +# Setup Guide + +> **Skill:** `.agents/skills/cosmos3-setup/SKILL.md` + + + +______________________________________________________________________ + +**Table of Contents** + +- [System Requirements](#system-requirements) +- [Installation](#installation) + - [Virtual Environment](#virtual-environment) + - [CUDA Variants](#cuda-variants) + - [Docker Container](#docker-container) +- [Downloading Checkpoints](#downloading-checkpoints) +- [Troubleshooting](#troubleshooting) + - [PyTorch Import Issue](#pytorch-import-issue) + - [Dependency Issue](#dependency-issue) + - [Python Issue](#python-issue) + - [CUDA Issue](#cuda-issue) + +______________________________________________________________________ + + + +## System Requirements + +- NVIDIA GPUs with Ampere architecture (RTX 30 Series, A100) or newer +- NVIDIA driver compatible with [CUDA version](https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html) +- [NVIDIA CUDA >=12.8](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/#ubuntu) +- Linux x86-64/aarch64 +- glibc >=2.35 (e.g Ubuntu >=22.04) +- Python >=3.10 + +## Installation + +If you encounter issues, see [Troubleshooting](#troubleshooting). + +Clone the repository: + +```bash +git clone git@github.com:nvidia-cosmos/cosmos3.git +cd cosmos3 +``` + +Install one of the following environments: + +
Virtual Environment + +### Virtual Environment + +Install system dependencies: + +```shell +sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget +``` + +Install [uv](https://docs.astral.sh/uv/getting-started/installation/): + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env +``` + +Install the package using one of the following methods: + +
UV Sync: fully reproducible environment + +```shell +uv sync --all-extras --group=cu130-train +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +
+ +
UV Pip: virtual environment + +```shell +# Create virtual environment (skip if using an existing environment) +uv venv --clear && source .venv/bin/activate && export LD_LIBRARY_PATH= + +uv pip install -r pyproject.toml --all-extras --group=cu130-train +uv pip install -e . +``` + +
+ +
UV Pip: system environment + +```shell +uv pip install --system --break-system-packages -r pyproject.toml --all-extras --group=cu130-train +``` + +
+ +
Advanced: custom torch/cuda versions + +```shell +cuda_name=cu130 +torch_name=torch210 + +# 1. Create and activate the virtual environment +uv venv --clear && source .venv/bin/activate + +# 2. Install the desired torch/cuda versions +uv pip install "torch==2.10.0" "torchvision" --torch-backend=$cuda_name + +# 3. Install the package with desired extras +uv pip install -r pyproject.toml --all-extras + +# 4. Install one of the following attention backends: +# * Blackwell +uv pip install "natten==0.21.6.dev6+$cuda_name.$torch_name" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/natten +# * Hopper +uv pip install "flash-attn-3-nv==1.0.3+$cuda_name.$torch_name" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/flash-attn-3-nv +# * Ada/Ampere +uv pip install "flash-attn==2.7.4.post1+$cuda_name.$torch_name" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/flash-attn +``` + +If there is no attention backend wheel for your torch/cuda versions, you can build one using [cosmos-dependencies](https://github.com/nvidia-cosmos/cosmos-dependencies). + +
+ +Optional package extras: + +- `guardrail`: Guardrails +- `serve`: Online inference (ray, gradio). +- `train`: Training + +#### CUDA Variants + +| CUDA Version | Inference | Training | Notes | +| --------------------------- | --------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| **CUDA 13.0 (recommended)** | `--group=cu130` | `--group=cu130-train` | [NVIDIA Driver](https://docs.nvidia.com/cuda/archive/13.0.0/cuda-toolkit-release-notes/index.html#cuda-toolkit-major-component-versions) | +| CUDA 12.8 | `--group=cu128` | `--group=cu128-train` | [NVIDIA Driver](https://docs.nvidia.com/cuda/archive/12.8.0/cuda-toolkit-release-notes/index.html#cuda-toolkit-major-component-versions) | + +
+ +
Docker Container + +### Docker Container + +Please make sure you have access to Docker on your machine and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) is installed. + +Build the containers: + +```bash +image_tag=$(docker build -f Dockerfile -q .) +``` + +Run the container: + +```bash +docker run -it --runtime=nvidia --ipc=host --rm -v .:/workspace -v /workspace/.venv -v /root/.cache:/root/.cache -e HF_TOKEN="$HF_TOKEN" $image_tag +``` + +Optional arguments: + +- `--ipc=host`: Use host system's shared memory, since parallel torchrun consumes a large amount of shared memory. If not allowed by security policy, increase `--shm-size` ([documentation](https://docs.docker.com/engine/containers/run/#runtime-constraints-on-resources)). +- `-v /root/.cache:/root/.cache`: Mount host cache to avoid re-downloading cache entries. +- `-e HF_TOKEN="$HF_TOKEN"`: Set Hugging Face token to avoid re-authenticating. + +If you get `docker: Error response from daemon: unknown or invalid runtime name: nvidia`, you need to [configure docker](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#configuring-docker): + +```shell +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker +``` + +
+ +## Downloading Checkpoints + +1. Get a [Hugging Face Access Token](https://huggingface.co/settings/tokens) with `Read` permission +2. Login to [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/en/guides/cli): `uvx hf auth login` +3. Accept the [NVIDIA Open Model License Agreement](https://huggingface.co/nvidia/Cosmos-Guardrail1). +4. Test: `uvx hf@latest download --repo-type model nvidia/Cosmos-Guardrail1 --revision d6d4bfa899a71454a700907664f3e88f503950cf --include "README.md"` + +If you encounter issues: + +1. Check that you don't have conflicting [environment variables](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables) (e.g. `HF_TOKEN`): `printenv | grep HF_` +2. Check that your [token](https://huggingface.co/settings/tokens) has sufficient permissions. + +Checkpoints are automatically downloaded during inference and post-training. To modify the checkpoint cache location, set the [`HF_HOME`](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hfhome) environment variable. + +## Troubleshooting + +### PyTorch Import Issue + +Errors: + +- `ImportError: cannot import name '_functionalization' from 'torch._C'` + +Clear the library path in your current shell: + +```shell +export LD_LIBRARY_PATH= +``` + +This applies to the current session only. To persist, add the line to your `Dockerfile` or `~/.bashrc`. + +If this doesn't fix the issue, try [reinstalling venv](#dependency-issue). + +### Dependency Issue + +Errors: + +- `ModuleNotFoundError: No module named ` + +Reinstall venv: + +```shell +uv sync --all-extras --group=cu130-train --reinstall +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +If this doesn't fix the issue, try [reinstalling uv](#python-issue). + +### Python Issue + +Errors: + +- `fatal error: Python.h: No such file or directory` + +Reinstall uv and venv: + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +uv python install --reinstall +rm -rf .venv +uv sync --all-extras --group=cu130-train --reinstall +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +### CUDA Issue + +- `OSError: : cannot open shared object file: No such file or directory` + +Ensure you have [CUDA installed](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/#ubuntu). The major version must match between the system and virtual environment cuda versions. + +```shell +sudo apt-get install -y --no-install-recommends cuda-toolkit- +``` + +Alternatively, use the [Docker container](#docker-container). diff --git a/docs/training.md b/docs/training.md new file mode 100644 index 00000000..7ef398da --- /dev/null +++ b/docs/training.md @@ -0,0 +1,325 @@ +# Post-Training (Supervised Fine-Tuning) + + + +______________________________________________________________________ + +**Table of Contents** + +- [Setup](#setup) +- [Training](#training) + - [Step 1 — Prepare data](#step-1--prepare-data) + - [VFM Bridge dataset](#vfm-bridge-dataset) + - [Action LIBERO dataset](#action-libero-dataset) + - [Step 2 — Prepare checkpoint](#step-2--prepare-checkpoint) + - [Step 3 — Prepare config](#step-3--prepare-config) + - [Step 4 — Run training](#step-4--run-training) + - [VFM Post-Training](#vfm-post-training) + - [Action Policy Post-Training](#action-policy-post-training) +- [Inference](#inference) + - [Run inference with trained checkpoint](#run-inference-with-trained-checkpoint) + - [Result Comparison](#result-comparison) + - [Export checkpoint to Hugging Face safetensors](#export-checkpoint-to-hugging-face-safetensors) +- [Outputs](#outputs) +- [Video Captioning for Training Data Processing](#video-captioning-for-training-data-processing) + - [Server setup](#server-setup) + - [Running Video Captioning](#running-video-captioning) +- [Creating Video Dataset JSONL File for Training](#creating-video-dataset-jsonl-file-for-training) + +______________________________________________________________________ + + + +Fine-tune a pre-trained Cosmos3 model on your own video dataset using supervised fine-tuning (SFT). The workflow has two stages: + +1. **Training** (4 steps): prepare data, prepare the checkpoint, prepare the config, and launch distributed training with `torchrun`. +2. **Inference** (2 steps): run inference with the trained checkpoint, and optionally export it to Hugging Face safetensors. + +This guide was tested on the following environments: + +- 8x H100 (80 GB) + +## Setup + +Install training dependencies. Pick the CUDA group that matches your driver — see [Setup](./setup.md#cuda-variants) to check your driver's max CUDA version. + +```shell +# CUDA 13.0 (recommended; needs newer driver) +uv sync --all-extras --group=cu130-train + +# OR, CUDA 12.8 (use this if your driver does not support CUDA 13.0) +uv sync --all-extras --group=cu128-train + +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +Environment variables: + +- `IMAGINAIRE_OUTPUT_ROOT=/tmp/imaginaire4-output`: Output directory for training DCP checkpoints. We recommend at least 1 TB of free disk space. +- `COSMOS_SMOKE=1`: Enable smoke test. +- `COSMOS_VERBOSE=1`: Enable verbose console output. + +## Training + +### Step 1 — Prepare data + +#### VFM Bridge dataset + +Download example dataset [nvidia/bridge-v2-subset-synthetic-captions](https://huggingface.co/datasets/nvidia/bridge-v2-subset-synthetic-captions/tree/main) (~650 MB) to the [Hugging Face cache](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables#hfhubcache): + +```shell +DATASET_PATH=$(uvx hf@latest download --repo-type dataset nvidia/bridge-v2-subset-synthetic-captions --include "sft_dataset_bridge/*" --quiet)/sft_dataset_bridge +``` + +#### Action LIBERO dataset + +Download the LIBERO LeRobot v3 dataset from [nvidia/LIBERO_LeRobot_v3](https://huggingface.co/datasets/nvidia/LIBERO_LeRobot_v3/) into `outputs/libero_datasets`: + +```shell +mkdir -p outputs/libero_datasets +uvx hf@latest download \ + --repo-type dataset nvidia/LIBERO_LeRobot_v3 \ + --local-dir outputs/libero_datasets +``` + +### Step 2 — Prepare checkpoint + +Convert base checkpoint to [PyTorch Distributed Checkpoint (DCP)](https://pytorch.org/docs/stable/distributed.checkpoint.html) format: + +```shell +BASE_CHECKPOINT_PATH=/tmp/$USER/checkpoints/cosmos3_nano +torchrun -m cosmos3.scripts.convert_model_to_dcp \ + --checkpoint-path Cosmos3-Nano \ + -o $BASE_CHECKPOINT_PATH +``` + +### Step 3 — Prepare config + +> **Quick start:** the provided `cosmos3/configs/experiment/mixed_modality_sft_8b.yaml` works as-is for the example dataset. Skip to [Step 4](#step-4--run-training). + +A config template for the Cosmos3-Nano model is provided at [`mixed_modality_sft_8b.yaml`](../cosmos3/configs/experiment/mixed_modality_sft_8b.yaml). The following fields are commonly updated: + +1. `job` + 1. `project`, `group`, `name`: Job directory path. + 1. `cluster` + 1. `wandb_mode`: Enable Wandb logging (choices: `online`, `disabled`). +1. `model` + 1. `config` + 1. `parallelism` + 1. `data_parallel_shard_degree`: Model FSDP shard size. Should be set to `WORLD_SIZE`. +1. `checkpoint` + 1. `load_path`: Base DCP checkpoint path. + 1. `save_iter`: Save DCP checkpoint every N iterations. +1. `dataloader_train` + 1. `dataloader` + 1. `datasets.*` + 1. `jsonl_paths`: Dataset JSONL file paths. + 1. `num_video_frames`: Number of input video frames to sample (`-1`: all frames). + 1. `num_workers`: Number of dataloader workers. +1. `trainer` + 1. `max_iter`: Total number of training iterations. + 1. `grad_accum_iter`: Number of iterations to accumulate gradients to increase the effective batch size with limited memory. +1. `optimizer` + 1. `lr`: Learning rate. + +### Step 4 — Run training + +#### VFM Post-Training + +Launch distributed training with `torchrun`: + +```shell +torchrun --nproc_per_node=8 -m cosmos3.scripts.train \ + -o outputs/train \ + --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml \ + --config-overrides \ + "checkpoint.load_path=$BASE_CHECKPOINT_PATH" \ + "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl" +``` + +#### Action Policy Post-Training + +Use [`action_policy_sft_8b.yaml`](../cosmos3/configs/experiment/action_policy_sft_8b.yaml) to post-train an action policy on LIBERO. This config trains in policy mode with concatenated `agentview` and wrist camera observations, frame-wise relative actions, 6D rotations, and quantile-rotation action normalization. + +```shell +BASE_CHECKPOINT_PATH=outputs/checkpoints/action_policy_sft_8b + +torchrun --nproc_per_node=8 -m cosmos3.scripts.train \ + -o outputs/libero_sft \ + --config-file cosmos3/configs/experiment/action_policy_sft_8b.yaml \ + --config-overrides \ + "checkpoint.load_path=$BASE_CHECKPOINT_PATH" +``` + +Flags: + +- `-o`: Output directory for checkpoints and logs. +- `--config-file`: Training config YAML from Step 3. +- `--dry-run`: Validate the setup without running training. +- `--config-overrides`: [Hydra overrides](https://hydra.cc/docs/advanced/override_grammar/basic/). See [Config](#step-3--prepare-config). + +## Inference + +### Run inference with trained checkpoint + +Get the path to the latest training checkpoint: + +```shell +CHECKPOINT_ITER=$(cat outputs/train/job/checkpoints/latest_checkpoint.txt) +CHECKPOINT_PATH=outputs/train/job/checkpoints/$CHECKPOINT_ITER +``` + +Run Text2Video (T2V), Image2Video (I2V), and Video2Video (V2V) inference for a single clip with the output checkpoint: + +```shell +torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ + --parallelism-preset=latency \ + -i "$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json" \ + -o outputs/train_inference \ + --checkpoint-path $CHECKPOINT_PATH \ + --config-file outputs/train/config.yaml \ + --seed=0 +``` + +- The ground truth video is in `${DATASET_PATH}/val/videos/`. +- The input image for I2V is in `${DATASET_PATH}/val/images/`. +- The input 5-frame video clip for V2V is in `${DATASET_PATH}/val/videos_5frames/`. + +#### Result Comparison + +Each example below uses the following layout: + +- Row 1 (T2V): ground truth video (left), before SFT (middle), after 500 iterations of SFT (right). +- Row 2 (I2V): input image (left), before SFT (middle), after 500 iterations of SFT (right). +- Row 3 (V2V): 5-frame input clip (left), before SFT (middle), after 500 iterations of SFT (right). + +**episode_049683_clip000** + +
Input prompt + +> A robotic arm with articulated joints and a gripping mechanism is positioned centrally on a wooden kitchen countertop, manipulating a small silver metal object while also interacting with scattered black coffee beans. The arm moves the metal object slightly, adjusting its position before shifting focus to the coffee beans, scattering and repositioning them with precision. The countertop is surrounded by kitchen elements, including a stove on the right, a microwave on the left, and two canned goods labeled "Tomato Juice" and "Baking Soda" in the background. The scene is illuminated by bright, even indoor lighting, casting minimal shadows, and the camera remains static throughout, offering a top-down perspective that emphasizes the robotic arm's movements. The composition centers on the robotic arm and its interaction with the metal object and coffee beans, with a shallow depth of field keeping the focus sharp on these elements while softly blurring the background. The overall atmosphere is technical and functional, highlighting the precision and control of the robotic manipulation within a domestic kitchen setting. + +
+ + + +**episode_009171_clip000** + +
Input prompt + +> A robotic arm with a black and metallic gripper, accented with blue near its base, extends over a white rectangular tray filled with scattered brown almonds, methodically picking up and placing each almond in a precise line across the tray's surface. The arm moves with deliberate, controlled motion, shifting its position to reach different almonds while maintaining a top-down perspective that captures the entire workspace. The background reveals an indoor setting with a wooden table and various kitchen items, including a metal bowl and utensils, subtly visible behind the tray. The lighting is bright and evenly distributed, casting minimal shadows and highlighting the contrast between the white tray, the brown almonds, and the metallic sheen of the robotic arm. The camera remains static throughout, offering a wide-angle view that emphasizes the robotic arm's precision and the systematic rearrangement of the almonds, creating a clean, minimalist aesthetic that underscores the technical nature of the task. The scene unfolds as a continuous, uninterrupted sequence, showcasing the robotic arm's efficiency in organizing the almonds without any cuts or transitions. + +
+ + + +**Note:** For action policy inference and evaluation, please refer to [LIBERO Closed-Loop Evaluation](./action_policy_closed_loop_eval.md). + +### Export checkpoint to Hugging Face safetensors + +Optionally, convert the DCP checkpoint to a Hugging Face model: + +```shell +torchrun -m cosmos3.scripts.export_model \ + --checkpoint-path $CHECKPOINT_PATH \ + --config-file outputs/train/config.yaml \ + -o outputs/train/model +``` + +## Outputs + +The training output directory contains: + +1. `debug.log`, `console.log`: Logs. +1. `config.yaml`: Finalized hydra config. +1. `job/` + 1. `checkpoints/` + 1. `latest_checkpoint.txt`: Text file containing the latest checkpoint iteration. + 1. `iter_/`: DCP checkpoints saved every `{checkpoint.save_iter}` iterations. + 1. `/`: Callback outputs. + +| Artifact | Path | +| ------------------------------------------------------- | ---------------------------------------- | +| Example dataset | `$DATASET_PATH` | +| Base checkpoint (DCP) | `$BASE_CHECKPOINT_PATH` | +| Trained checkpoint (DCP) | `outputs/train/job/checkpoints/iter_` | +| Trained checkpoint (Hugging Face safetensors, optional) | `outputs/train/model` | +| Inference video from trained model | `outputs/train_inference` | + +______________________________________________________________________ + +## Video Captioning for Training Data Processing + +If you have video sources and would like to synthesize caption annotations to build video–text pairs for training, follow this section for data preprocessing. The script sends each video directly to a Vision-Language Model (VLM), which analyzes the visual content and produces a dense narrative caption following a two-phase process (scene analysis → narrative rewrite) — the same format expected by the Cosmos3 training pipeline. + +The captioning prompt template is available at [`cosmos3/defaults/video_captioner.txt`](../cosmos3/defaults/video_captioner.txt). + +### Server setup + +The captioning script passes video files to vLLM via `video_url` content parts using `file://` paths, so the server must be able to read files from the local filesystem. We recommend [Qwen/Qwen3-VL-8B-Instruct-FP8](https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct-FP8) as the VLM. Start the server — this may take a couple of minutes: + +```shell +uvx --with nvidia-cuda-runtime-cu12 \ + vllm@0.19.0 serve Qwen/Qwen3-VL-8B-Instruct-FP8 \ + --tensor-parallel-size 1 \ + --allowed-local-media-path / +``` + +The server is ready when you see `Application startup complete.` + +### Running Video Captioning + +Caption a single video: + +```shell +python -m cosmos3.scripts.caption_from_video \ + --video /path/to/video.mp4 -o outputs/captions \ + --server http://localhost:8000/v1 +``` + +Caption all `.mp4` files in a directory: + +```shell +python -m cosmos3.scripts.caption_from_video \ + --video /path/to/videos/ -o outputs/captions \ + --server http://localhost:8000/v1 +``` + +Caption videos listed in a JSONL manifest (each line must have a `vision_path` field pointing to a video): + +```shell +python -m cosmos3.scripts.caption_from_video \ + -i samples.jsonl -o outputs/captions \ + --server http://localhost:8000/v1 +``` + +Options: + +| Flag | Default | Description | +| ------------------------ | -------- | -------------------------------- | +| `--max_workers` | `16` | Concurrent API requests | +| `--prompt_template_path` | built-in | Path to a custom prompt template | +| `--debug` | `False` | Save raw API responses | + +Each video produces an output directory containing `caption.txt` (the plain-text caption) and `sample_args.json` (metadata). + +## Creating Video Dataset JSONL File for Training + +After generating the captions, you will have videos and captions stored in the following file structure: + +``` +path/to/dataset/ +└── captions/ +└── videos/ +``` + +To create a video dataset JSONL file for post-training, run the following command: + +``` +python -m cosmos3.scripts.captions_to_sft_jsonl \ + --captions-dir outputs/sft_dataset/train/captions \ + --videos-dir outputs/sft_dataset/train/videos \ + -o outputs/sft_dataset/train/video_dataset_file.jsonl +``` + +It will create a dataset JSONL file containing captions and their corresponding paths to video files. diff --git a/examples/_test.py b/examples/_test.py new file mode 100644 index 00000000..20fa3a42 --- /dev/null +++ b/examples/_test.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from cosmos3.fixtures.script import ScriptConfig, script_test + +_CURRENT_DIR = Path(__file__).parent.absolute() +_TEST_DIR = _CURRENT_DIR / "_test" + + +_script_configs = [ + ScriptConfig( + script=_TEST_DIR / "inference.sh", + ), + ScriptConfig( + script=_TEST_DIR / "inference_pipeline.sh", + ), +] + + +@script_test(_script_configs) +class TestScript: ... diff --git a/examples/_test/inference.sh b/examples/_test/inference.sh new file mode 100644 index 00000000..e2162aa1 --- /dev/null +++ b/examples/_test/inference.sh @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +python examples/inference.py diff --git a/examples/_test/inference_pipeline.sh b/examples/_test/inference_pipeline.sh new file mode 100644 index 00000000..06c65380 --- /dev/null +++ b/examples/_test/inference_pipeline.sh @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +python examples/inference_pipeline.py diff --git a/examples/inference.py b/examples/inference.py new file mode 100644 index 00000000..5e8d9a14 --- /dev/null +++ b/examples/inference.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inference example.""" + +from cosmos3.common.init import init_output_dir, init_script + +init_script() + +from pathlib import Path + +import safetensors.torch +import torch + +from cosmos3.args import DEFAULT_CHECKPOINT, OmniSampleOverrides +from cosmos3.inference import get_sample_data +from cosmos3.model import Cosmos3OmniModel +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.visualize.video import save_img_or_video +from cosmos3._src.vfm.configs.base.defaults.model_config import ParallelismConfig + + +def inference(): + name = "inference" + output_dir = Path(f"outputs/{name}").absolute() + init_output_dir(output_dir) + + log.info("Loading model...") + checkpoint_path = DEFAULT_CHECKPOINT.download() + model = Cosmos3OmniModel.from_pretrained_dcp( + Path(checkpoint_path), + parallelism_config=ParallelismConfig( + use_torch_compile=True, + ), + ).model + + # Create batch + sample_args = OmniSampleOverrides( + name=name, + output_dir=output_dir, + prompt="A medium shot of a modern robotics research laboratory with white walls and a gray floor. A robotic arm with a metallic finish is mounted on a clean white workbench, its gripper positioned above a row of small colored objects. A laptop and neatly arranged tools sit beside the robot. A large monitor on the wall behind displays a software interface. The scene is brightly lit by overhead fluorescent lights.", + num_frames=1, + ).build_sample(model_config=model.config) + data_batch = get_sample_data(sample_args, model) + + # Generate samples + log.info("Generating samples...") + outputs = model.generate_samples_from_batch(data_batch, seed=[0]) + + # Decode + def decode_vision(vision_latent: torch.Tensor) -> torch.Tensor: + vision = model.decode(vision_latent) # Decode to pixel space + return (1.0 + vision.clamp(-1, 1)) / 2 # [0, 1] + + outputs["vision"] = [decode_vision(vision) for vision in outputs.pop("vision")] + outputs = {k: torch.cat(v, dim=0) for k, v in outputs.items()} + + # Save outputs + log.info("Saving outputs...") + safetensors.torch.save_file(outputs, output_dir / "outputs.safetensors") + save_img_or_video(outputs["vision"][0], str(output_dir / "vision"), fps=data_batch["fps"][0].item()) + log.success(f"Saved outputs to {output_dir}") + + +def main(): + inference() + + +if __name__ == "__main__": + main() diff --git a/examples/inference_pipeline.py b/examples/inference_pipeline.py new file mode 100644 index 00000000..805987ea --- /dev/null +++ b/examples/inference_pipeline.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inference example using the pipeline.""" + +from cosmos3.common.init import init_output_dir, init_script + +init_script() + +from pathlib import Path + +from cosmos3.args import DEFAULT_CHECKPOINT_NAME, OmniSampleOverrides, OmniSetupOverrides +from cosmos3.inference import OmniInference, get_sample_data +from cosmos3._src.imaginaire.utils import log + + +def inference_pipeline(): + name = "inference_pipeline" + output_dir = Path(f"outputs/{name}").absolute() + init_output_dir(output_dir) + + log.info("Loading model...") + setup_args = OmniSetupOverrides( + checkpoint_path=DEFAULT_CHECKPOINT_NAME, + output_dir=output_dir, + ).build_setup() + pipe = OmniInference.create(setup_args) + + sample_args = OmniSampleOverrides( + name=name, + output_dir=output_dir, + prompt="the quick brown fox is happily jumping over the fence.", + num_frames=1, + ).build_sample(model_config=pipe.model_config) + data_batch = get_sample_data(sample_args, model=pipe.model) + + log.info("Generating samples...") + pipe.generate_batch([sample_args], data_batch) + + +def main(): + inference_pipeline() + + +if __name__ == "__main__": + main() diff --git a/inputs/omni/action_forward_dynamics_camera_0.json b/inputs/omni/action_forward_dynamics_camera_0.json new file mode 100644 index 00000000..ab9f3799 --- /dev/null +++ b/inputs/omni/action_forward_dynamics_camera_0.json @@ -0,0 +1,14 @@ +{ + "action_mode": "forward_dynamics", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/mountain_720.png", + "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/camera_action_44.json", + "image_size": 480, + "fps": 30, + "prompt": "A serene landscape video of a calm body of water in the foreground, leading up to rolling green pastoral hills and a prominent mountain peak partially shrouded in low-hanging fog under a moody, overcast gray sky. This video is captured from a first-person perspective looking at the scene.", + "action_chunk_size": 60, + "num_steps": 30, + "guidance": 1.0, + "shift": 5.0, + "seed": 0, + "domain_name": "camera_pose" +} diff --git a/inputs/omni/action_forward_dynamics_camera_1.json b/inputs/omni/action_forward_dynamics_camera_1.json new file mode 100644 index 00000000..8cac7e4d --- /dev/null +++ b/inputs/omni/action_forward_dynamics_camera_1.json @@ -0,0 +1,14 @@ +{ + "action_mode": "forward_dynamics", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/solar_720.png", + "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/camera_action_44.json", + "image_size": 480, + "fps": 30, + "prompt": "An architectural video of a modern elevated terrace at twilight, characterized by a large, intricate white geometric canopy supporting integrated solar panels, slatted wooden benches, and a unique cylindrical wooden seating pod, overlooking a distant campus. This video is captured from a first-person perspective looking at the scene.", + "action_chunk_size": 60, + "num_steps": 30, + "guidance": 1.0, + "shift": 5.0, + "seed": 0, + "domain_name": "camera_pose" +} diff --git a/inputs/omni/action_forward_dynamics_robot.json b/inputs/omni/action_forward_dynamics_robot.json new file mode 100644 index 00000000..c925f85b --- /dev/null +++ b/inputs/omni/action_forward_dynamics_robot.json @@ -0,0 +1,15 @@ +{ + "action_mode": "forward_dynamics", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.mp4", + "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.json", + "image_size": 480, + "fps": 5, + "prompt": "Put the pot to the left of the purple item. This video is captured from a first-person perspective looking at the scene.", + "num_steps": 30, + "guidance": 1.0, + "shift": 5.0, + "seed": 0, + "action_chunk_size": 16, + "domain_name": "bridge_orig_lerobot", + "extra": {"golden_psnr_min": 14.0} +} diff --git a/inputs/omni/action_inverse_dynamics_av.json b/inputs/omni/action_inverse_dynamics_av.json new file mode 100644 index 00000000..19d9b495 --- /dev/null +++ b/inputs/omni/action_inverse_dynamics_av.json @@ -0,0 +1,18 @@ +{ + "action_mode": "inverse_dynamics", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/av_vision_25_73d01c91-51f0-46cf-9b76-5682a76fb349.mp4", + "image_size": 480, + "fps": 10, + "prompt": "You are an autonomous vehicle planning system. This video is captured from a first-person perspective looking at the scene.", + "num_steps": 30, + "guidance": 1.0, + "shift": 5.0, + "seed": 0, + "raw_action_dim": 9, + "action_chunk_size": 60, + "domain_name": "av", + "extra": { + "golden_mse_max": 0.05, + "golden_action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/av_action_25.json" + } +} diff --git a/inputs/omni/action_policy_robot.json b/inputs/omni/action_policy_robot.json new file mode 100644 index 00000000..7ab7e4bb --- /dev/null +++ b/inputs/omni/action_policy_robot.json @@ -0,0 +1,19 @@ +{ + "action_mode": "policy", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.mp4", + "image_size": 480, + "fps": 5, + "prompt": "Put the pot to the left of the purple item. This video is captured from a first-person perspective looking at the scene.", + "num_steps": 30, + "guidance": 1.0, + "shift": 5.0, + "seed": 0, + "action_chunk_size": 16, + "raw_action_dim": 10, + "domain_name": "bridge_orig_lerobot", + "extra": { + "golden_psnr_min": 14.0, + "golden_mse_max": 0.05, + "golden_action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.json" + } +} diff --git a/inputs/omni/i2v.json b/inputs/omni/i2v.json new file mode 100644 index 00000000..5a5b6884 --- /dev/null +++ b/inputs/omni/i2v.json @@ -0,0 +1,4 @@ +{ + "prompt": "The video opens with a view of a testing environment, characterized by a large wooden table at the center. On this table, two robot arms are positioned at opposite ends, with the left arm closer to the camera and the right arm further away. Between the hands lies a dark wooden shelf with a red spherical object on its top rack, likely serving as a platform or obstacle. In the background, various pieces of equipment, including a tripod, a chair, are visible. A person wearing a blue jacket and black pants stands near the center of the room, observing the experiment, with a static hand position throughout. The floor is tiled with a patterned design, and additional items like a small robot figure and some cables can be seen scattered around the space. As the video progresses, the right robotic hand extends outward, moving from its initial position towards the red spherical object on the shelf. The hand then picks up the object and places it on the lowest rack of the shelf, completing a smooth, deliberate manipulation. The left robotic hand remains stationary throughout the sequence. No new objects appear in the video; all existing elements maintain their positions except for the movement of the right robotic hand. The scene concludes with the right robotic hand returning to its initial position, while the left hand continues to rest on the table. The overall environment remains unchanged, with the focus remaining on the interaction between the robotic hands and the wooden block, highlighting precise control during the demonstration.", + "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/vision/robot_153.jpg" +} diff --git a/inputs/omni/t2i.json b/inputs/omni/t2i.json new file mode 100644 index 00000000..2e9b736d --- /dev/null +++ b/inputs/omni/t2i.json @@ -0,0 +1,4 @@ +{ + "prompt": "A medium shot of a modern robotics research laboratory with white walls and a gray floor. A robotic arm with a metallic finish is mounted on a clean white workbench, its gripper positioned above a row of small colored objects. A laptop and neatly arranged tools sit beside the robot. A large monitor on the wall behind displays a software interface. The scene is brightly lit by overhead fluorescent lights.", + "num_frames": 1 +} diff --git a/inputs/omni/t2v.json b/inputs/omni/t2v.json new file mode 100644 index 00000000..a9d05c9f --- /dev/null +++ b/inputs/omni/t2v.json @@ -0,0 +1,3 @@ +{ + "prompt": "The video opens with a view of a well-lit indoor space featuring a wooden display case with compartments filled with various fruits, including bananas, apples, pears, oranges, and carambolas. The bananas are neatly arranged in the middle compartment, while apples are in the left and a mix of pears, oranges, and carambolas are in the right. Two robotic arms with grippers are positioned at the bottom of the frame, with the one on the left remaining stationary, partially obscuring the apples. The robotic arm on the right begins its action, extending towards the right side of the display case. It carefully picks up a pear from the fruit section, placing it into a plastic bag in the shopping cart nearby, which has red handles. After securing the pear, the arm retracts back to its original position. The process repeats as the robotic arm picks up an orange and places it in the bag, followed by a carambola. The final frame captures the robotic arm returning to its initial position, leaving the display case and surrounding area unchanged. The video showcases a seamless and efficient automated fruit-picking process, highlighting the precision and efficiency of modern robotics in a retail setting." +} diff --git a/inputs/t2v_long_prompts.jsonl b/inputs/t2v_long_prompts.jsonl new file mode 100644 index 00000000..b2236cd5 --- /dev/null +++ b/inputs/t2v_long_prompts.jsonl @@ -0,0 +1,21 @@ +{"prompt": "The video begins with a view from inside a vehicle, likely captured by a dashboard camera, showing a wide street scene under an overcast sky. The road stretches ahead with multiple lanes, and several vehicles are visible, including a white car directly ahead and a larger bus to the right. The bus has an advertisement for a law firm on its side. On the left side of the street, there's a parking lot filled with cars, and various commercial buildings line either side, featuring signs and storefronts. The environment suggests a suburban or semi-urban area with palm trees scattered along the sidewalks, adding a touch of greenery to the otherwise urban landscape. As the video progresses, the vehicle continues to move forward down the street. The white car directly ahead remains in the same lane, maintaining a steady pace. The bus on the right side of the road begins to look closer to the ego vehicle, as the large size becomes more prominent while moving towards it. The advertisements, including one for a law firm, become clearer as the ego vehicle overtakes a bus stopped ahead. The surrounding environment remains consistent, with the parking lot on the left and commercial buildings on the right under the same cloudy sky. By the final frame, both the white car and the ego vehicle move forward from the bus, making it out of the frame, and the white car slightly turns right, with its right blinker on, continuing to show the same street with the same vehicles and surroundings, maintaining the calm and steady pace of the journey."} +{"prompt": "A point-of-view video shot from inside a vehicle driving on a multi-lane highway under a clear blue sky. The dashboard of the car is visible at the bottom of the frame, reflecting the road ahead. The road stretches forward with multiple lanes, bordered by trees on both sides, suggesting a suburban or semi-rural area. On the left side of the highway, there’s a concrete barrier separating the lanes, and beyond it, additional vehicles can be seen traveling in the same direction. The right side of the road features more trees and some greenery. The sun is positioned high in the sky, casting bright light across the scene and creating a lens flare effect that slightly obscures the upper left corner of the frame. As the video progresses, the scene remains consistent, with the vehicle continuing its journey along the multi-lane highway. The dashboard reflects the road ahead and the surrounding environment, including the trees and other vehicles. There are no significant changes in the positions or actions of the objects; the vehicles maintain their steady pace. The sun continues to shine brightly, maintaining the lens flare effect in the upper left corner of the frame. By the end of the video, the ego vehicle gradually changes to the adjacent right lane on the highway, surrounded by the same trees and vehicles, and the sun continues to cast its bright light over the landscape. A steady, forward-moving shot from a first-person perspective."} +{"prompt": "A dashboard camera captures a view from inside a vehicle as it drives through a suburban neighborhood on a partly cloudy day. The street is lined with houses featuring various architectural styles, including brick and siding facades. Well-maintained lawns and trees, including a prominent large tree on the left, surround the houses. Several cars are parked along the sides of the road, including a white van and a silver SUV near the foreground. A white sedan is seen driving away from the camera. Traffic cones are placed on the right side of the road, indicating ongoing construction or maintenance work. As the video progresses, the ego vehicle makes a U-turn on the residential street, and the white van and SUV initially parked on the right side of the road move out of the frame. The traffic cones remain in place, marking their designated area, while the surrounding environment, including the houses, trees, and parked vehicles, stays largely unchanged throughout the sequence. By the end of the video, the scene remains consistent with the initial frame, showing the same suburban street lined with houses, trees, and parked cars under a partly cloudy sky. A medium shot from a slightly elevated perspective."} +{"prompt": "The video opens with a view from inside a car, positioned at an intersection in a suburban neighborhood during early evening. The car's dashboard reflects the street ahead, capturing a serene suburban scene. Across the street, a pedestrian in dark clothing is crossing the road, moving from left to right. The houses lining both sides of the street are two-story structures with neutral-colored exteriors, some featuring white picket fences. Several cars are parked along the curb, including a blue sedan. Palm trees are scattered throughout the neighborhood, adding a touch of greenery. The sky is clear with a gradient of colors suggesting a sunset, enhancing the tranquil suburban atmosphere. As the pedestrian continues their crossing, a white car moves forward towards the frame on the left lane and moves over the \"STOP\" lane marking on the road while the ego car remains stationary. The pedestrian's pace is consistent, and they appear focused on reaching the other side of the road. The surrounding environment remains unchanged, with the same houses, parked cars, and palm trees visible throughout the sequence. By the final frame, the pedestrian has almost reached the far side of the street, with the ego car slightly turned to the left and the overall scene maintaining its serene suburban ambiance. A medium shot from a slightly elevated perspective captures the pedestrian's movement across the road."} +{"prompt": "The video begins with a view from inside a vehicle traveling on a multi-lane highway under a clear blue sky. The road is bordered by dense green trees on both sides, creating a tranquil environment. Several vehicles, including a prominent white semi-truck and various cars, are visible ahead, maintaining a steady pace. The highway features multiple lanes separated by concrete barriers, and the scene is bathed in bright sunlight, indicating a clear day. As the video progresses, a large amount of debris suddenly appears on the lane ahead. With little time to avoid it, the ego vehicle has to drive over the debris and continue moving forward. A noticeable jolt occurs as the ego vehicle passes over the scattered objects. A point-of-view shot from inside the vehicle, capturing the road ahead and the surrounding environment."} +{"prompt": "The video opens with a view from inside a vehicle, looking out through the windshield onto a quiet residential street. The dashboard is visible at the bottom of the frame. A red and white ambulance is driving down the center of the road, moving away from the viewer. On the left side of the road, two white vans are seen passing on the opposite side of the ego vehicle one after another. The street is lined with houses on both sides, featuring different architectural styles and colors, including brick and siding. Trees with green foliage line the sidewalks, and utility poles with wires run along the street. As the ambulance continues to drive down the street, the ego vehicle changes to the adjacent left lane, and the white vans cross in the opposite lane. The ambulance maintains its steady pace, driving straight ahead. The surrounding environment remains unchanged, with the houses, trees, and utility poles continuing to stand still. The scene concludes with the ambulance moving further down the road, and the overall atmosphere remains tranquil and uneventful. A point-of-view shot from inside a vehicle, capturing the ambulance driving down the residential street. The camera remains stationary, focusing on the ambulance as it moves away. The scene transitions smoothly as the ego vehicle changes lanes, providing a dynamic perspective of the street and the passing vehicles. The background features a mix of houses, trees, and utility poles, creating a serene suburban setting."} +{"prompt": "The video begins with a view from inside a vehicle, likely captured by a dashboard camera, as it approaches an intersection in a suburban neighborhood. The road is marked with a \"SLOW SCHOOL XING\" sign painted in yellow on the asphalt, indicating a speed limit or cautionary area. Double yellow lines run continuously along the left side of the ego vehicle. As the vehicle moves into the left-turn-only lane, the left-turn-only pavement marking becomes clearly visible, with a single white line appearing on the right side of the vehicle. The street is lined with mature trees, their branches casting shadows across the road, suggesting a sunny day. On the left side of the road, there's a white car parked near a sidewalk. Across the street, a few more cars are parked along the curb, and a multi-story building with large windows stands in the background. The sky above is clear and blue, adding to the serene suburban atmosphere. As the ego vehicle moves forward, the \"SLOW SCHOOL XING\" sign remains prominently displayed on the road. Two cyclists appear on the left side of the road, riding past the ego vehicle, while a white sedan drives towards the camera from the opposite direction. The cyclists continue down the street, and the sedan passes by, revealing more of the residential area with parked cars and houses. The trees continue to cast shadows, and the clear blue sky persists throughout the scene. A point-of-view shot from within a moving vehicle, capturing the road ahead and the surrounding suburban neighborhood. The camera angle provides a first-person perspective, emphasizing the motion and the environment as the vehicle navigates the intersection and continues down the street. The scene is bathed in natural sunlight, highlighting the details of the road markings, the parked cars, and the lush greenery of the trees. The overall atmosphere is calm and orderly, typical of a quiet suburban setting."} +{"prompt": "A video captured from inside a car, likely using a dashboard camera, shows a two-lane suburban street during late afternoon or early evening. The road is marked with a white dashed line down the center, and several cars are visible ahead, including a silver sedan directly in front and a blue car to its right. Residential houses with varying roof colors and styles line both sides of the street, some with visible driveways and parked vehicles. Trees, some bare, suggest it might be autumn or winter. A utility pole with wires spans across the top of the frame, and a lamppost stands near the curb on the right side. The sky is clear and blue, with the sun low, casting a golden hue over the scene. As the video progresses, the silver sedan maintains its position, while the blue car gradually moves out of view. The ego vehicle overtakes a stopped vehicle ahead, which is waiting to make a left turn, indicated by its left turn signal. The surrounding environment remains consistent, with no significant changes in the positions of other vehicles or objects. The trees and houses remain stationary, and the utility pole and lamppost continue to stand in their respective places. The lighting conditions suggest the time of day is still late afternoon or early evening, with the sun continuing to cast long shadows across the road. By the end of the video, the scene remains largely unchanged, with the silver sedan still in the same position and the blue car no longer visible. The residential houses, trees, utility pole, and lamppost maintain their positions, and the clear blue sky with the low sun continues to illuminate the suburban street. A medium shot from the driver's perspective captures the road and surrounding environment."} +{"prompt": "The video opens with a heavy, rainy nighttime scene viewed from inside a vehicle moving at varying speeds, looking out onto a wet, reflective street. The environment is dimly lit, with only the headlights of approaching cars and streetlights breaking the darkness. The ego vehicle stops due to turning at a T-intersection. The road is slick with rain, creating a glossy surface that mirrors the lights. A leafless tree stands prominently in the center-right of the frame, its branches stark against the gray sky. To the left, several houses are partially visible with a person walking near them, while on the right, a larger, more modern structure with illuminated windows can be seen with the camera at the same angle. Parked cars line both sides of the street, and the overall atmosphere is quiet and somber. As the video progresses, a car approaches from a distance, its headlights becoming more prominent as it gets closer. This car then passes out of view, and the scene remains largely unchanged, maintaining the same wet, reflective road and dimly lit surroundings until the final frame, which captures the same tranquil, rainy street at night. A medium shot from inside a vehicle, capturing the wet, reflective street and the dimly lit surroundings."} +{"prompt": "The video opens with a view from inside a vehicle, looking out onto a multi-lane highway under a partly cloudy sky. The dashboard of the car is visible at the bottom of the frame, indicating the perspective is from the driver's seat. A white sedan is moving on the left side of the road, and a few vehicles are seen traveling in both directions on the highway. On both sides, a tall, light brown wall or barrier runs alongside the road, in front of which a chain-link fence is visible. Beyond the fence, some greenery, including trees and bushes, is present. Overhead, power lines stretch across the sky, supported by tall utility poles. The sky is a mix of blue and white clouds, suggesting a partly sunny day. As the video progresses, the ego vehicle gradually makes a right lane change to enter a faster-moving highway lane, smoothly adjusting its position relative to nearby vehicles. The white sedan on the left continues to move forward, while the vehicles on the highway maintain a steady pace. The sky retains its partly cloudy appearance, and the surrounding environment, including the utility poles and the fence, stays static throughout the frames. There are no new objects appearing, and the actions of the existing objects remain consistent with minimal changes in their states. By the end of the video, the ego vehicle has completed the lane change, while the rest of the scene maintains its original composition. A dynamic driving sequence captured from a first-person perspective, showcasing the transition from a slower to a faster lane on a multi-lane highway. The sky's mix of blue and white clouds adds a serene backdrop to the motion, while the stationary elements like the fence and utility poles provide a sense of stability amidst the vehicle's movement."} +{"prompt": "The video begins with a view from inside a vehicle, looking out onto a wet, overcast street intersection. A silver sedan is positioned directly ahead of the ego vehicle. The rear lights of two leading vehicles are red, indicating they are stationary and waiting at the intersection ahead. To the left, another car is visible, positioned further back in the opposite lane. The traffic light above the intersection is green, allowing vehicles to proceed. The road surface is shiny and reflective, indicating recent rainfall. On the right side of the street, there's a grassy slope leading up to a modern building with glass windows. Trees and shrubs line the roadside, and the sky is filled with dark, cloudy formations, contributing to the dreary weather. As the video progresses, the traffic light remains green, and the vehicles continue to move forward at a slow pace. The silver sedan ahead eventually stops, and the ego car behind it also comes to a halt. The final frame shows the same scene, with the vehicles stopped at the intersection under the same overcast sky, the wet road reflecting the surroundings, the green lights shining, and the grassy slope and modern building still visible. A medium shot from the perspective of the ego vehicle, capturing the intersection and the vehicles ahead."} +{"prompt": "The video opens with a view from inside a vehicle, positioned at a traffic intersection in an urban area under an overcast sky, casting a subdued light over the scene. The intersection is marked with multiple traffic lights, all displaying red and green signals, indicating that vehicles must stop and move. On the right side of the frame, a multi-story apartment building with balconies stands prominently, accompanied by a sign that reads \"Leasing.\" To the left, another building with a blue awning is visible, hinting at a mix of residential and commercial spaces. Several cars are parked along the sides of the road, and a few vehicles are moving while others wait at the traffic lights. The intersection is clearly marked with white lines, and the traffic lights remain red, signaling that vehicles must stop. The reflective surface of the car's hood mirrors the surrounding environment, capturing the stillness of the scene, which remains unchanged throughout the video."} +{"prompt": "The video opens with a view from inside a car, looking out through the windshield onto a suburban street. The car's hood is visible at the bottom of the frame, with a small camera mounted to the windshield. The road ahead is marked with yellow lines, and a portable warning sign \"ROAD WORK AHEAD\" is positioned on the right side of the ego vehicle, indicating ongoing construction activity. The street is lined with large, leafy trees that provide shade, and houses with light-colored exteriors can be seen on both sides. A white van is parked near the curb on the left of the ego vehicle. The sky above is clear and blue, suggesting a sunny day. As the video progresses, the car begins to move forward along the suburban street. The portable warning sign \"ROAD WORK AHEAD\" remains stationary on the right side of the road, and the working zone is enclosed by traffic cones on the front right side of the road. The trees and houses remain static, providing a consistent backdrop as the car advances down the road. By the final frame, the car has moved further down the street, while the white van still remains stationary. The scene maintains its suburban tranquility, with the road markings and construction sign clearly visible throughout the journey."} +{"prompt": "The video begins with a view from inside a vehicle, looking out through the windshield onto a multi-lane urban street intersection under a clear blue sky with a few scattered clouds. The road markings include a \"right-turn only\" lane marker and a single white line, with the number 87 painted on the asphalt. A crosswalk is present at the intersection of the road, and a traffic light with a red signal is present adjacent to it. A \"North 87\" sign mounted on a lamppost is visible on the right side of the road, positioned after a \"Detour Ahead\" sign which is also attached to a lamppost. Several cars are present, including a black sedan and a white sedan, both already stopped in the adjacent lanes. In the background, there's a bridge overpass, and some greenery, including trees and grassy areas, can be seen around the intersection. A building is partially visible on the right side of the frame. As the video progresses, the vehicle continues to move forward, capturing the dynamic motion of the car as it navigates through the intersection. The camera angle shifts slightly, providing a closer view of the road markings and the vehicles ahead. The black sedan and white sedan remain stationary, waiting for the traffic light to change. The surrounding environment, including the trees, grass, and buildings, remains static, with no significant changes in their appearance or position. By the end of the video, the scene shows the vehicles stopped at the red light, with the same urban street intersection and its elements remaining unchanged. The video captures the everyday rhythm of city life, emphasizing the steady flow of traffic and the subtle interplay between moving and stationary elements within the urban landscape."} +{"prompt": "A nighttime urban street scene viewed from inside a car, likely through a dashboard camera, captures the perspective of the ego vehicle moving towards an intersection with a red traffic light. The dashboard and windshield wipers of the ego car are prominently visible in the foreground. The street is lined with trees and buildings, illuminated by streetlights casting a soft glow over the scene. A red sedan is ahead of the ego car, also stopped at the light, with its brake lights illuminated and its right indicator on. Another red car is seen taking a right turn in front of the ego car. The road markings, including a crosswalk and lane indicators, are clearly visible. On the left side of the street, there's a small roundabout with a plant in the center, and a brick pillar stands near the curb. The buildings on either side of the frame appear to be residential, with windows and doors faintly lit. The overall atmosphere is quiet and calm, typical of a late-night city street. As the video progresses, the red sedan turns to the right while the traffic light remains red. The surrounding environment remains unchanged, with the streetlights continuing to illuminate the area and the trees and buildings maintaining their positions. No new objects appear, and no significant actions like collisions occur apart from the movement of the vehicles. By the final frame, the scene remains consistent, with the ego car still moving in the same lane and the same red sedan turning right at the red traffic light, the same streetlights illuminating the area, and the same residential buildings and trees lining the street."} +{"prompt": "A point-of-view video shot from inside a vehicle driving down a snow-covered urban street during a heavy snowfall. The windshield is speckled with snowflakes, and the road ahead is lined with various commercial establishments, including a prominent McDonald's sign on the left side. To the right, a large billboard displays the word \"SOLNIX\" in bold letters. Further down the road, a multi-story building is visible, partially obscured by the falling snow. The sky is overcast, casting a gray hue over the entire scene. As the vehicle moves forward, the snow continues to fall steadily, accumulating on the windshield and the road. The road ahead has been driven over by vehicles, leaving clear tire tracks, and appears very muddy and slushy. The video concludes with the vehicle still traveling down the snow-laden street, the snowflakes still actively falling, and the urban landscape enveloped in a blanket of white."} +{"prompt": "The video begins with a view from inside a vehicle, likely captured by a dashboard camera, showing a street scene under an overcast sky. The road is lined with parked vehicles, including several RVs and cars, suggesting a residential area. On the left side of the street, there are houses with fences and trees, some bare and others with green foliage. Power lines run parallel to the road, supported by utility poles. The road itself has clear lane markings, including a speed limit sign indicating \"35\" mph. The windshield reflects the cloudy sky above, adding to the subdued lighting of the scene. As the video progresses, the ego vehicle slows down to yield to a gray SUV that is executing a u-turn in front. The scene remains largely static, with no significant changes in the positions or actions of the objects. The vehicles on the road appear stationary or moving very slowly, maintaining their positions relative to one another. The RVs and cars remain parked along the left side of the street, while the right side shows a mix of parked and moving vehicles. The trees and houses on either side of the road stay in place, and the power lines and utility poles continue to stand tall. The speed limit sign remains visible throughout a major part of the video, and the overall atmosphere of the scene stays consistent with no notable actions or new objects entering the frame. By the end of the video, the scene continues to depict the same residential street under the same overcast sky. The parked vehicles, including the RVs and cars, remain in their respective positions, and the trees and houses on both sides of the road maintain their stationary state. The power lines and utility poles are still visible, and the speed limit marking on the road is obscured by the hood of the ego vehicle. The overall environment appears unchanged, with no new objects or significant actions occurring. A point-of-view shot from inside a vehicle, capturing a residential street under an overcast sky. The road is lined with parked vehicles, including several RVs and cars, suggesting a quiet neighborhood. Houses with fences and trees line the left side of the street, some bare and others with green foliage. Power lines run parallel to the road, supported by utility poles. The road has clear lane markings, including a speed limit sign indicating \"35\" mph. The windshield reflects the cloudy sky above, adding to the subdued lighting. As the video progresses, the ego vehicle slows down to yield to a gray SUV executing a u-turn in front. The scene remains largely static, with no significant"} +{"prompt": "The video opens with a serene rural scene where a woman is riding a white horse along a paved road. The horse, adorned with a blue saddle pad, moves steadily forward, guided by the rider who holds the reins. To the right of the road, a silver car is parked, with two individuals seated inside, watching the horse and rider pass by. The background features a metal fence lining the road, beyond which lies a grassy embankment dotted with sparse vegetation and a few leafless trees, indicating a late autumn or early spring setting. The sky above is clear and bright, casting distinct shadows on the road, enhancing the tranquil ambiance. As the video progresses, the horse and rider continue their journey down the road, maintaining a consistent pace. The woman remains focused on steering the horse, her posture steady and confident. The car and its occupants remain stationary, their attention fixed on the passing horse. The metal fence and the grassy embankment stay constant, reinforcing the rural setting. The clear sky and the absence of any significant changes in the environment suggest a calm and undisturbed scene. In the final frame, the horse and rider have moved further down the road, gradually moving out of view. The car and its occupants are still visible but appear smaller due to the distance. The metal fence and the grassy embankment remain prominent, and the clear sky continues to dominate the background. The overall scene retains its peaceful and rural character, with no new objects or significant actions occurring, maintaining the tranquil atmosphere established at the beginning."} +{"prompt": "A documentary-style video captures a person meticulously coating a concrete roof with spray paint. Dressed in a white protective suit and equipped with a safety harness, the individual works at height, ensuring their safety. The roof is covered with dark gray, textured tiles, which the person systematically applies using a spray nozzle. The camera follows the worker's deliberate and controlled movements, highlighting their focus and precision. The background, a blurred view of greenery, suggests an outdoor suburban setting. The video emphasizes the steady and methodical nature of the task, showcasing the person's dedication to achieving even coverage."} +{"prompt": "A medium shot captures a cat sitting on a tiled floor in a kitchen setting. The cat, predominantly white with patches of brown fur on its head and back, is engaged in grooming itself. It wears a red bandana adorned with paw prints around its neck. The cat uses its front paws to meticulously clean its face and neck area, showcasing a methodical and focused grooming routine. The background features wooden cabinets with metal legs and a stainless steel appliance, likely a refrigerator, providing a warm and homely atmosphere. The lighting is bright, highlighting the cat's fur and the details of the surroundings. As the video progresses, the cat continues its grooming, maintaining the same posture and actions. The red bandana remains securely in place, and the cat's movements are deliberate and attentive. In the final frame, the cat finishes grooming and looks up, possibly towards the camera or something off-screen, with its mouth slightly open and eyes wide, giving an impression of curiosity or surprise. The red bandana is still in place, and the cat's posture is relaxed yet alert. The overall scene remains in the same kitchen setting, with the wooden cabinets and stainless steel appliance continuing to provide context."} +{"prompt": "A serene video captures two young elephants in a dry, reddish-brown landscape, likely a savanna or a dry riverbed. The ground is covered with loose dirt, and a fallen tree trunk lies horizontally in the background, adding to the natural setting. The two elephants are partially submerged in a small puddle of water, their bodies glistening with mud. The standing elephant, positioned on the left, uses its trunk to gently interact with the lying elephant on the right, which is stretched out on its side. The standing elephant's trunk moves slowly, touching and nuzzling the lying elephant, indicating a playful and affectionate behavior. The lying elephant remains mostly still, occasionally shifting slightly, and appears relaxed and comfortable in the mud. The overall atmosphere is peaceful, highlighting the bond and playful nature of these young elephants in their natural habitat. A medium shot from a slightly elevated perspective captures the interaction between the two elephants."} diff --git a/justfile b/justfile new file mode 100644 index 00000000..929b2783 --- /dev/null +++ b/justfile @@ -0,0 +1,232 @@ +set dotenv-load + +default: + just --list + +package_name := 'cosmos3' +module_name := 'cosmos3' +short_name := 'cosmos3' + +cuda_name := env("CUDA_NAME", + if arch() == "aarch64" { + "cu130" + } else { + "cu128" + } +) +test_max_gpus := env("TEST_MAX_GPUS", + if arch() == "aarch64" { + "4" + } else { + "8" + } +) +test_max_processes := env("TEST_MAX_PROCESSES", "8") + +# Unset environment variables that interfere with uv virtual environment. +export LD_LIBRARY_PATH := '' +# Avoid CUDA out-of-memory errors. +export PYTORCH_ALLOC_CONF := 'expandable_segments:True' + +# Install the Python version specified in .python-version +_python-install: + uv python install + +# Setup the repository +setup: _python-install + +_uv-sync *args: + uv sync --all-extras --group={{cuda_name}} {{args}} + +# Install the repository +install *args: setup (_uv-sync '--reinstall' args) + +# Run a command in the package environment +run *args: _uv-sync + uv run --no-sync {{args}} + +# Setup pre-commit +_pre-commit-setup: setup + uv tool install "pre-commit>=4.5.0" + pre-commit install -c ci/.pre-commit-config-base.yaml + +# Run pre-commit +pre-commit *args: _pre-commit-setup + pre-commit run -a -c ci/.pre-commit-config-base.yaml + pre-commit run -a {{args}} || pre-commit run -a {{args}} + +# Run pyrefly +pyrefly *args: _uv-sync + # TODO: Enable + # uv run --no-sync pyrefly check -c pyrefly-src.toml --output-format=min-text {{args}} + +# Run linting and formatting +lint: pre-commit + +python_env_tmp := ''' +venv_dir="$(mktemp -d)" +trap 'rm -rf "$venv_dir"' EXIT +uv venv -q "$venv_dir" +source "$venv_dir/bin/activate" +''' + +_test-install-uv-sync cuda_name: + #!/usr/bin/env bash + set -euo pipefail + {{python_env_tmp}} + uv sync -q --active --all-extras --group={{cuda_name}} + +_test-install-uv-pip cuda_name: + #!/usr/bin/env bash + set -euo pipefail + {{python_env_tmp}} + uv pip install -q -r pyproject.toml --all-extras --group={{cuda_name}} + +_test-install-advanced cuda_name torch_name='torch210': + #!/usr/bin/env bash + set -euo pipefail + {{python_env_tmp}} + uv pip install -q "torch==2.10.0" "torchvision" --torch-backend={{cuda_name}} + uv pip install -q "natten==0.21.6.dev6+{{cuda_name}}.{{torch_name}}" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/natten + if [ "$(uname -m)" == "x86_64" ]; then + uv pip install -q "flash-attn-3-nv==1.0.3+{{cuda_name}}.{{torch_name}}" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/flash-attn-3-nv + uv pip install -q "flash-attn==2.7.4.post1+{{cuda_name}}.{{torch_name}}" -f https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0/flash-attn + fi + +[parallel] +_test-install cuda_name: \ + (_test-install-uv-sync cuda_name) \ + (_test-install-uv-pip cuda_name) \ + (_test-install-advanced cuda_name) + +# Test installation methods in 'docs/setup.md' +[parallel] +test-install: \ + (_test-install 'cu128') \ + (_test-install 'cu130') + +pytest_args := '-vv --exitfirst --instafail --durations=5 --force-regen' + +# Run a single test +test-single name *args: _uv-sync + uv run --no-sync pytest --capture=no --manual {{pytest_args}} {{args}} {{name}} + +# Run GPU=0 tests +_test-gpu-0 *args: _uv-sync + uv run --no-sync pytest --num-gpus=0 -n logical --maxprocesses={{test_max_processes}} --levels=0 {{pytest_args}} {{args}} + +# Run GPU>0 tests +_test-gpu *args: _uv-sync + #!/usr/bin/env bash + set -euxo pipefail + export TEST_MAX_GPUS={{test_max_gpus}} + export TEST_MAX_PROCESSES={{test_max_processes}} + AVAILABLE_GPUS=$(nvidia-smi -L | wc -l) + for num_gpus in 1 $TEST_MAX_GPUS; do + if [ $num_gpus -gt $AVAILABLE_GPUS ]; then + break + fi + args="{{pytest_args}} {{args}}" + if [ $num_gpus -ne 1 ]; then + # Only run coverage for single-GPU tests. + # All multi-GPU tests should have a corresponding single-GPU smoke test, which has full coverage. + args="$args --no-cov" + fi + uv run --no-sync pytest --num-gpus=$num_gpus -n logical --levels=0 $args + done + +# Run tests +test *args: pyrefly (_test-gpu-0 args) (_test-gpu args) + +coverage_args := '--cov-append --cov-report= --cov=' + module_name + +# Initialize coverage +_coverage-init: + rm -rf outputs/coverage + +# Run tests with coverage +test-coverage *args: _coverage-init (test coverage_args args) (run 'coverage' 'xml') + +# List tests +test-list *args: _uv-sync + uv run --no-sync pytest --collect-only -q --manual {{args}} + +# https://spdx.org/licenses/ +allow_licenses := "MIT BSD-2-CLAUSE BSD-3-CLAUSE APACHE-2.0 ISC" +ignore_package_licenses := "nvidia-* cuda-* hf-xet certifi filelock matplotlib typing-extensions sentencepiece" + +# Run licensecheck +_licensecheck *args: + uvx licensecheck@2025.1.0 \ + --show-only-failing \ + --only-licenses {{allow_licenses}} \ + --ignore-packages {{ignore_package_licenses}} \ + --zero \ + {{args}} + +# Run pip-licenses +_pip-licenses *args: _uv-sync + uvx pip-licenses@5.5.0 \ + --python .venv/bin/python \ + --format=plain-vertical \ + --with-license-file \ + --no-license-path \ + --no-version \ + --with-urls \ + --output-file ATTRIBUTIONS.md \ + {{args}} + pre-commit run --files ATTRIBUTIONS.md || true + +# Update the license +license: _licensecheck _pip-licenses + +# Run link-check +_link-check *args: + pre-commit run -a --hook-stage manual link-check {{args}} + +# Pre-release checks +release-check: license _link-check + +# Release a new version +release pypi_token='dry-run' *args: + ./bin/release.sh {{pypi_token}} {{args}} + +# Build the docker container. +_docker-build *args: + docker build {{args}} . + +# Run the docker container +_docker build_args='' run_args='': (_docker-build build_args) + #!/usr/bin/env bash + set -euxo pipefail + image_tag=$(docker build {{build_args}} -q .) + docker run \ + -it \ + --runtime=nvidia \ + --ipc=host \ + --rm \ + -v .:/workspace \ + -v /workspace/.venv \ + -v /root/.cache:/root/.cache \ + -e HF_TOKEN="$HF_TOKEN" \ + {{run_args}} \ + $image_tag + +docker_cu128_build_args := '--build-arg=CUDA_VERSION=12.8.1' +docker_cu130_build_args := '--build-arg=CUDA_VERSION=13.0.2' +docker_nightly_build_args := '-f docker/nightly.Dockerfile' + +# Run the CUDA 12.8 docker container. +docker-cu128 *run_args: (_docker docker_cu128_build_args run_args) + +# Run the CUDA 13.0 docker container. +docker-cu130 *run_args: (_docker docker_cu130_build_args run_args) + +# Run the nightly docker container. +docker-nightly *run_args: (_docker docker_nightly_build_args run_args) + +# Test the docker containers. +test-docker: \ + (_docker-build '-q' docker_cu128_build_args) \ + (_docker-build '-q' docker_cu130_build_args) \ + (_docker-build '-q' docker_nightly_build_args) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..030bac6a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,341 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "cosmos3" +version = "1.2.1" +authors = [ + {name = "NVIDIA Corporation"}, +] +description = "Cosmos3 World Foundation Model (WFM)" +requires-python = ">=3.10" +license = {text = "Apache-2.0"} +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: GPU :: NVIDIA CUDA", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] +dependencies = [ + "accelerate", + "av", + "cattrs", + "diffusers", + "einops", + "hydra-core", + "imageio-ffmpeg", + "imageio", + "loguru", + "nvidia-cudnn-frontend", + "nvidia-ml-py", + "obstore", + "omegaconf", + "pydantic", + "requests", + "termcolor", + "transformers>=4.57.1,<5.0.0", + "tyro", + "uv", +] + +[project.optional-dependencies] +guardrail = [ + "better-profanity", + "nltk", + "protobuf", + "retinaface-py", + "sentencepiece", +] +interactive = [ + "websockets", +] +serve = [ + "fastapi", + "httpx", + "gradio", + "ray[serve]", +] +train = [ + "aioboto3", + "aiofiles", + "aiohttp", + "arrgh", + "blobfile", + "boto3", + "botocore", + "datasets", + "dists-pytorch", + "einx", + "fastparquet", + "ffmpeg-python", + "ffmpegcv", + "flask", + "flopth", + "ftfy", + "futureproof", + "fvcore", + "glfw", + "h5py", + "iopath>=0.1.10", + "imagecodecs", + "ipycanvas", + "ipyevents", + "jupyter-compare-view", + "jupyterlab", + "kornia", + "lerobot", + "lpips", + "lz4", + "matplotlib", + "mediapy", + "megatron-core", + "more-itertools", + "moviepy", + "multi-storage-client[boto3,google-cloud-storage,fsspec,observability-otel,vault]==0.44.0", + "ninja", + "nvidia-dali-cuda120", + "open-clip-torch", + "openai", + "opencv-contrib-python", + "packaging", + "pandas", + "parse", + "peft", + "pillow", + "plyfile", + "polars", + "polyscope", + "psycopg2-binary", + "py3nvml", + "pycocotools", + "pydispatcher", + "pygltflib", + "pyopengl", + "pytest", + "python-memcached", + "pytz", + "pyyaml", + "qwen-vl-utils", + "robotmq", + "rsa", + "s3fs", + "scikit-image", + "semver", + "setuptools", + "slangtorch", + "soundfile", + "tensorstore", + "tiktoken", + "timm", + "torch-fidelity", + "torch-optimizer", + "torchtitan", + "trimesh", + "typeguard", + "urllib3", + "wandb", + "webdataset", + "xatlas", + "zarr", +] + + +[dependency-groups] +dev = [ + "hatch", + "pyinstrument", + "pyrefly==0.55.0", + "pytest", + "pytest-custom_exit_code", + "pytest-cov", + "pytest-env", + "pytest-instafail", + "pytest-regressions", + "pytest-xdist", + "ruff==0.12.7", + "uv==0.10.8", +] +vllm = [ + "vllm==0.19.0", + "torch==2.10.0", +] +# Match nvcr.io/nvidia/pytorch:25.11-py3: https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-26-01.html +cu128 = [ + "flash-attn-3-nv==1.0.3+cu128.torch210; platform_machine == 'x86_64'", + "flash-attn==2.7.4.post1+cu128.torch210; platform_machine == 'x86_64'", + "natten==0.21.6.dev6+cu128.torch210; platform_machine == 'x86_64'", + "torch==2.10.0+cu128", + "torchcodec==0.10.0+cu128", # https://github.com/meta-pytorch/torchcodec/releases + "torchvision==0.25.0+cu128", # https://github.com/pytorch/vision/releases + # Dependencies determined from 'uv pip install --dry-run "torch==2.10.0+cu128" --index-url https://download.pytorch.org/whl' + # Issue: https://github.com/astral-sh/uv/issues/14237 + "nvidia-cublas-cu12==12.8.4.1", + "nvidia-cuda-cupti-cu12==12.8.90", + "nvidia-cuda-nvrtc-cu12==12.8.93", + "nvidia-cuda-runtime-cu12==12.8.90", + "nvidia-cudnn-cu12==9.10.2.21", + "nvidia-cufft-cu12==11.3.3.83", + "nvidia-cufile-cu12==1.13.1.3", + "nvidia-curand-cu12==10.3.9.90", + "nvidia-cusolver-cu12==11.7.3.90", + "nvidia-cusparse-cu12==12.5.8.93", + "nvidia-cusparselt-cu12==0.7.1", + "nvidia-nccl-cu12==2.27.5", + "nvidia-nvjitlink-cu12==12.8.93", + "nvidia-nvshmem-cu12==3.4.5", + "nvidia-nvtx-cu12==12.8.90", +] +cu128-train = [ + {include-group = "cu128"}, + "torchao==0.16.0+cu128; platform_machine == 'x86_64'", # https://github.com/pytorch/ao/issues/2919 + "transformer-engine==2.12.0+cu128.torch210", + "triton==3.6.0", +] +# Match nvcr.io/nvidia/pytorch:25.11-py3: https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-25-11.html +cu130 = [ + "flash-attn-3-nv==1.0.3+cu130.torch210; platform_machine == 'x86_64'", + "flash-attn==2.7.4.post1+cu130.torch210; platform_machine == 'x86_64'", + "natten==0.21.6.dev6+cu130.torch210", + "torch==2.10.0+cu130", + "torchcodec==0.10.0+cu130", # https://github.com/meta-pytorch/torchcodec/releases + "torchvision==0.25.0+cu130", # https://github.com/pytorch/vision/releases + # Dependencies determined from 'uv pip install --dry-run "torch==2.10.0+cu130" --index-url https://download.pytorch.org/whl' + # Issue: https://github.com/astral-sh/uv/issues/14237 + "nvidia-cublas==13.1.0.3", + "nvidia-cuda-cupti==13.0.85", + "nvidia-cuda-nvrtc==13.0.88", + "nvidia-cuda-runtime==13.0.96", + "nvidia-cudnn-cu13==9.15.1.9", + "nvidia-cufft==12.0.0.61", + "nvidia-cufile==1.15.1.6", + "nvidia-curand==10.4.0.35", + "nvidia-cusolver==12.0.4.66", + "nvidia-cusparse==12.6.3.3", + "nvidia-cusparselt-cu13==0.8.0", + "nvidia-nccl-cu13==2.28.9", + "nvidia-nvjitlink==13.0.88", + "nvidia-nvshmem-cu13==3.4.5", + "nvidia-nvtx==13.0.85", +] +cu130-train = [ + {include-group = "cu130"}, + "torchao==0.16.0+cu130; platform_machine == 'x86_64'", # https://github.com/pytorch/ao/issues/2919 + "transformer-engine==2.12.0+cu130.torch210", + "triton==3.6.0", +] +# LIBERO simulator dependencies for the closed-loop eval client. +# See docs/action_policy_closed_loop_eval.md. Mirrors packages/cosmos-policy +# `libero` group; `libero` from PyPI declares `robosuite` transitively so we +# don't need to list it here. The numpy upper bound that LIBERO/robosuite/numba +# needs is enforced via `[tool.uv].override-dependencies` (cannot be group-scoped +# because uv overrides are global). +libero = [ + "bddl", + "cloudpickle", + "draccus", + "easydict", + "gym", + "imageio[ffmpeg]", + "libero", + "mujoco==3.3.2", +] + +[project.readme] +content-type = "text/markdown" +text = ''' +# Cosmos3-VFM + +[Documentation](https://github.com/nvidia-cosmos/cosmos3/blob/main/README.md) +''' + +[project.urls] +documentation = "https://github.com/nvidia-cosmos/cosmos3/blob/main/README.md" +homepage = "https://research.nvidia.com/labs/dir/cosmos3" +issues = "https://github.com/nvidia-cosmos/cosmos3/issues" +repository = "https://github.com/nvidia-cosmos/cosmos3" + +[tool.uv] +conflicts = [ + [ + {group = "vllm"}, + {group = "cu128"}, + {group = "cu128-train"}, + {group = "cu130"}, + {group = "cu130-train"}, + ], +] +override-dependencies = [ + # Lower bound defeats robomimic/robosuite's old numpy 1.x pins; upper bound + # is required by numba (transitive of robosuite via the `libero` group), whose + # latest release supports numpy <2.3. uv overrides are global, so this also + # applies when `--group=libero` is not active — acceptable because no other + # group currently needs numpy 2.3+. + "numpy>=2.0.0,<2.3", + "lightning; sys_platform == 'never'", # PyPi quarantined: https://pypi.org/project/lightning/ + "pynvml; sys_platform == 'never'", +] +required-environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64'", + "sys_platform == 'linux' and platform_machine == 'aarch64'", +] + +[tool.uv.sources] +flash-attn = { index = "cosmos"} +flash-attn-3-nv = { index = "cosmos" } +lerobot = { git = "https://github.com/mli0603/lerobot.git" } +megatron-core = { git = "https://github.com/NVIDIA/Megatron-LM.git", rev = "de56227" } +natten = { index = "cosmos"} +torch = { index = "pytorch"} +torchao = { index = "pytorch"} +torchcodec = { index = "pytorch" } +torchvision = { index = "pytorch"} +transformer-engine = { index = "cosmos"} +triton = { index = "pytorch"} + +[[tool.uv.index]] +name = "cosmos" +url = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" +explicit = true + +[[tool.uv.index]] +name = "natten" +url = "https://whl.natten.org" +explicit = true +format = "flat" + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl" +explicit = true + +[tool.hatch.build.targets.sdist] +packages = [ + "cosmos3", +] + +[tool.hatch.build.targets.wheel] +packages = [ + "cosmos3", +] +exclude = [ + "*_test.py", +] diff --git a/pyrefly.toml b/pyrefly.toml new file mode 100644 index 00000000..389e1b6b --- /dev/null +++ b/pyrefly.toml @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project-includes = [ + "cosmos3/", +] +project-excludes = [ + "packages/", +] +infer-with-first-use = false +ignore-missing-source = false +search-path = [ "./" ] + +ignore-missing-imports = [ + "cloudpickle.*", + "cloudpickle", + "flash_attn_3.*", + "flash_attn_3", + "launcher.*", + "launcher", + "librosa", + "librosa.*", + "soundfile.*", + "soundfile", + "transformer_engine_torch.*", + "transformer_engine_torch", + "wordfreq.*", + "wordfreq", +] + +[errors] +annotation-mismatch=false +assert-type=true +bad-argument-count=false +bad-argument-type=false +bad-assignment=false +bad-class-definition=false +bad-context-manager=false +bad-function-definition=false +bad-index=false +bad-instantiation=false +bad-keyword-argument=false +bad-match=true +bad-override=false +bad-param-name-override=false +bad-return=false +bad-specialization=false +bad-typed-dict=false +bad-typed-dict-key=false +bad-unpacking=false +deprecated=false +implicit-any=false +implicit-import=false +implicitly-defined-attribute=false +inconsistent-inheritance=false +inconsistent-overload=false +internal-error=false +invalid-annotation=false +invalid-argument=false +invalid-decorator=false +invalid-inheritance=false +invalid-literal=false +invalid-overload=false +invalid-param-spec=false +invalid-self-type=false +invalid-super-call=false +invalid-syntax=false +invalid-type-alias=false +invalid-type-var=false +invalid-type-var-tuple=false +invalid-yield=false +missing-argument=false +missing-attribute=false +missing-import=true +missing-module-attribute=true +missing-source=true +no-access=false +no-matching-overload=false +not-async=false +not-a-type=false +not-callable=false +not-iterable=false +parse-error=true +protocol-implicitly-defined-attribute=false +read-only=false +redundant-cast=true +redundant-condition=true +reveal-type=true +unbound-name=false +unexpected-keyword=false +unexpected-positional-argument=false +unknown-name=false +unsupported-delete=true +unsupported=false +unsupported-operation=false +unused-coroutine=true diff --git a/schemas/OmniSampleOverrides.schema.json b/schemas/OmniSampleOverrides.schema.json new file mode 100644 index 00000000..b2ec920b --- /dev/null +++ b/schemas/OmniSampleOverrides.schema.json @@ -0,0 +1,567 @@ +{ + "$defs": { + "ActionMode": { + "enum": [ + "policy", + "forward_dynamics", + "inverse_dynamics", + "image2video" + ], + "title": "ActionMode", + "type": "string" + }, + "NegativeMetadataMode": { + "enum": [ + "none", + "same", + "inverse" + ], + "title": "NegativeMetadataMode", + "type": "string" + } + }, + "additionalProperties": false, + "properties": { + "action_mode": { + "anyOf": [ + { + "$ref": "#/$defs/ActionMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Action mode. When set, activates Action batch construction." + }, + "action_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to action JSON file. Required for forward_dynamics mode.", + "title": "Action Path" + }, + "domain_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Action domain name passed to get_domain_id().", + "title": "Domain Name" + }, + "image_size": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Target image height in pixels (aspect-ratio-preserving resize).", + "title": "Image Size" + }, + "action_chunk_size": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of action steps to predict.", + "title": "Action Chunk Size" + }, + "raw_action_dim": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Dimension of the raw action data. Required when action_path is not provided.", + "title": "Raw Action Dim" + }, + "vision_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path or URL to conditioning image/video.", + "title": "Vision Path" + }, + "condition_frame_indexes_vision": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video.", + "title": "Condition Frame Indexes Vision" + }, + "resolution": { + "anyOf": [ + { + "enum": [ + "256", + "480", + "720", + "1080" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision resolution.\n\nDefaults to model config resolution.", + "title": "Resolution" + }, + "aspect_ratio": { + "anyOf": [ + { + "enum": [ + "1,1", + "4,3", + "3,4", + "16,9", + "9,16" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision aspect ratio. When None, image_edit preserves the input image's native\naspect ratio; all other modes default to 16:9.", + "title": "Aspect Ratio" + }, + "fps": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range.", + "title": "Fps" + }, + "num_frames": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of vision frames.\n\nRange by resolution: 256p: [24, 400], 480p: [24, 300], 720p: [24, 200].\nImage-only resolutions (e.g. 1080p) require num_frames=1.", + "title": "Num Frames" + }, + "video_save_quality": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Quality of the saved video (0-10).", + "title": "Video Save Quality" + }, + "image_save_quality": { + "anyOf": [ + { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Quality of the saved image (0-100).", + "title": "Image Save Quality" + }, + "prompt_path": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided.", + "title": "Prompt Path" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided.", + "title": "Prompt" + }, + "negative_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative prompt - describing what you don't want in the generated video.", + "title": "Negative Prompt" + }, + "duration_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders.", + "title": "Duration Template" + }, + "resolution_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Template string for appending resolution to prompt. Use {height} and {width} placeholders.", + "title": "Resolution Template" + }, + "negative_metadata_mode": { + "anyOf": [ + { + "$ref": "#/$defs/NegativeMetadataMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative prompt metadata mode: 'none', 'same', or 'inverse'." + }, + "inverse_duration_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Inverse template for duration/fps metadata in the negative prompt.", + "title": "Inverse Duration Template" + }, + "inverse_resolution_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Inverse template for resolution metadata in the negative prompt.", + "title": "Inverse Resolution Template" + }, + "negative_prompt_keep_metadata": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.", + "title": "Negative Prompt Keep Metadata" + }, + "num_steps": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of steps for the diffusion model.", + "title": "Num Steps" + }, + "guidance": { + "anyOf": [ + { + "maximum": 7, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Guidance scale for the diffusion model.", + "title": "Guidance" + }, + "guidance_interval": { + "anyOf": [ + { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "minimum": 0, + "type": "number" + }, + { + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Guidance interval for the diffusion model.", + "title": "Guidance Interval" + }, + "normalize_cfg": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If True, normalize the CFG output.", + "title": "Normalize Cfg" + }, + "shift": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Shift in the UniPC sampler. Ignored when sampler='edm'.", + "title": "Shift" + }, + "sigma_max": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum sigma for the EDM sampler. Ignored when sampler='unipc'.", + "title": "Sigma Max" + }, + "output_dir": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Output directory.", + "title": "Output Dir" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Model name.", + "title": "Model" + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extra arguments.", + "title": "Extra" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of the sample.", + "title": "Name" + }, + "num_outputs": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of outputs to generate per sample.", + "title": "Num Outputs" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for the random number generator.", + "title": "Seed" + }, + "tensors_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to data tensors file.", + "title": "Tensors File" + }, + "pickle_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to data pickle file.", + "title": "Pickle File" + }, + "defaults_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults.", + "title": "Defaults File" + } + }, + "title": "OmniSampleOverrides", + "type": "object" +} diff --git a/schemas/OmniSampleOverrides.yaml b/schemas/OmniSampleOverrides.yaml new file mode 100644 index 00000000..b8fc4158 --- /dev/null +++ b/schemas/OmniSampleOverrides.yaml @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Number of action steps to predict. +action_chunk_size: null +# Action mode. When set, activates Action batch construction. +action_mode: null +# Path to action JSON file. Required for forward_dynamics mode. +action_path: null +# Vision aspect ratio. When None, image_edit preserves the input image's native +# aspect ratio; all other modes default to 16:9. +aspect_ratio: null +# Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video. +condition_frame_indexes_vision: null +# Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults. +defaults_file: null +# Action domain name passed to get_domain_id(). +domain_name: null +# Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders. +duration_template: null +# Extra arguments. +extra: null +# Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range. +fps: null +# Guidance scale for the diffusion model. +guidance: null +# Guidance interval for the diffusion model. +guidance_interval: null +# Quality of the saved image (0-100). +image_save_quality: null +# Target image height in pixels (aspect-ratio-preserving resize). +image_size: null +# Inverse template for duration/fps metadata in the negative prompt. +inverse_duration_template: null +# Inverse template for resolution metadata in the negative prompt. +inverse_resolution_template: null +# Model name. +model: null +# Name of the sample. +name: null +# Negative prompt metadata mode: 'none', 'same', or 'inverse'. +negative_metadata_mode: null +# Negative prompt - describing what you don't want in the generated video. +negative_prompt: null +# Compatibility flag. If True and mode is 'none', mode is promoted to 'same'. +negative_prompt_keep_metadata: null +# If True, normalize the CFG output. +normalize_cfg: null +# Number of vision frames. +# +# Range by resolution: 256p: [24, 400], 480p: [24, 300], 720p: [24, 200]. +# Image-only resolutions (e.g. 1080p) require num_frames=1. +num_frames: null +# Number of outputs to generate per sample. +num_outputs: null +# Number of steps for the diffusion model. +num_steps: null +# Output directory. +output_dir: null +# Path to data pickle file. +pickle_file: null +# Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided. +prompt: null +# Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided. +prompt_path: null +# Dimension of the raw action data. Required when action_path is not provided. +raw_action_dim: null +# Vision resolution. +# +# Defaults to model config resolution. +resolution: null +# Template string for appending resolution to prompt. Use {height} and {width} placeholders. +resolution_template: null +# Seed for the random number generator. +seed: null +# Shift in the UniPC sampler. Ignored when sampler='edm'. +shift: null +# Maximum sigma for the EDM sampler. Ignored when sampler='unipc'. +sigma_max: null +# Path to data tensors file. +tensors_file: null +# Quality of the saved video (0-10). +video_save_quality: null +# Path or URL to conditioning image/video. +vision_path: null diff --git a/schemas/OmniSetupOverrides.schema.json b/schemas/OmniSetupOverrides.schema.json new file mode 100644 index 00000000..6a96b7f2 --- /dev/null +++ b/schemas/OmniSetupOverrides.schema.json @@ -0,0 +1,1004 @@ +{ + "$defs": { + "ActionMode": { + "enum": [ + "policy", + "forward_dynamics", + "inverse_dynamics", + "image2video" + ], + "title": "ActionMode", + "type": "string" + }, + "CheckpointDirHf": { + "additionalProperties": false, + "description": "Config for checkpoint directory on Hugging Face.", + "properties": { + "metadata": { + "additionalProperties": true, + "title": "Metadata", + "type": "object" + }, + "repository": { + "title": "Repository", + "type": "string" + }, + "revision": { + "title": "Revision", + "type": "string" + }, + "subdirectory": { + "default": "", + "title": "Subdirectory", + "type": "string" + }, + "include": { + "default": [], + "items": { + "type": "string" + }, + "title": "Include", + "type": "array" + }, + "exclude": { + "default": [], + "items": { + "type": "string" + }, + "title": "Exclude", + "type": "array" + } + }, + "required": [ + "repository", + "revision" + ], + "title": "CheckpointDirHf", + "type": "object" + }, + "CheckpointType": { + "description": "Checkpoint type.", + "enum": [ + "hf", + "dcp" + ], + "title": "CheckpointType", + "type": "string" + }, + "ConfigFileType": { + "description": "Config file type.", + "enum": [ + "module", + "yaml", + "json" + ], + "title": "ConfigFileType", + "type": "string" + }, + "NegativeMetadataMode": { + "enum": [ + "none", + "same", + "inverse" + ], + "title": "NegativeMetadataMode", + "type": "string" + }, + "OmniSampleOverrides": { + "additionalProperties": false, + "properties": { + "action_mode": { + "anyOf": [ + { + "$ref": "#/$defs/ActionMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Action mode. When set, activates Action batch construction." + }, + "action_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to action JSON file. Required for forward_dynamics mode.", + "title": "Action Path" + }, + "domain_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Action domain name passed to get_domain_id().", + "title": "Domain Name" + }, + "image_size": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Target image height in pixels (aspect-ratio-preserving resize).", + "title": "Image Size" + }, + "action_chunk_size": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of action steps to predict.", + "title": "Action Chunk Size" + }, + "raw_action_dim": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Dimension of the raw action data. Required when action_path is not provided.", + "title": "Raw Action Dim" + }, + "vision_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path or URL to conditioning image/video.", + "title": "Vision Path" + }, + "condition_frame_indexes_vision": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video.", + "title": "Condition Frame Indexes Vision" + }, + "resolution": { + "anyOf": [ + { + "enum": [ + "256", + "480", + "720", + "1080" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision resolution.\n\nDefaults to model config resolution.", + "title": "Resolution" + }, + "aspect_ratio": { + "anyOf": [ + { + "enum": [ + "1,1", + "4,3", + "3,4", + "16,9", + "9,16" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision aspect ratio. When None, image_edit preserves the input image's native\naspect ratio; all other modes default to 16:9.", + "title": "Aspect Ratio" + }, + "fps": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range.", + "title": "Fps" + }, + "num_frames": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of vision frames.\n\nRange by resolution: 256p: [24, 400], 480p: [24, 300], 720p: [24, 200].\nImage-only resolutions (e.g. 1080p) require num_frames=1.", + "title": "Num Frames" + }, + "video_save_quality": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Quality of the saved video (0-10).", + "title": "Video Save Quality" + }, + "image_save_quality": { + "anyOf": [ + { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Quality of the saved image (0-100).", + "title": "Image Save Quality" + }, + "prompt_path": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided.", + "title": "Prompt Path" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided.", + "title": "Prompt" + }, + "negative_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative prompt - describing what you don't want in the generated video.", + "title": "Negative Prompt" + }, + "duration_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders.", + "title": "Duration Template" + }, + "resolution_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Template string for appending resolution to prompt. Use {height} and {width} placeholders.", + "title": "Resolution Template" + }, + "negative_metadata_mode": { + "anyOf": [ + { + "$ref": "#/$defs/NegativeMetadataMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative prompt metadata mode: 'none', 'same', or 'inverse'." + }, + "inverse_duration_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Inverse template for duration/fps metadata in the negative prompt.", + "title": "Inverse Duration Template" + }, + "inverse_resolution_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Inverse template for resolution metadata in the negative prompt.", + "title": "Inverse Resolution Template" + }, + "negative_prompt_keep_metadata": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.", + "title": "Negative Prompt Keep Metadata" + }, + "num_steps": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of steps for the diffusion model.", + "title": "Num Steps" + }, + "guidance": { + "anyOf": [ + { + "maximum": 7, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Guidance scale for the diffusion model.", + "title": "Guidance" + }, + "guidance_interval": { + "anyOf": [ + { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "minimum": 0, + "type": "number" + }, + { + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Guidance interval for the diffusion model.", + "title": "Guidance Interval" + }, + "normalize_cfg": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If True, normalize the CFG output.", + "title": "Normalize Cfg" + }, + "shift": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Shift in the UniPC sampler. Ignored when sampler='edm'.", + "title": "Shift" + }, + "sigma_max": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum sigma for the EDM sampler. Ignored when sampler='unipc'.", + "title": "Sigma Max" + }, + "output_dir": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Output directory.", + "title": "Output Dir" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Model name.", + "title": "Model" + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extra arguments.", + "title": "Extra" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of the sample.", + "title": "Name" + }, + "num_outputs": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of outputs to generate per sample.", + "title": "Num Outputs" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for the random number generator.", + "title": "Seed" + }, + "tensors_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to data tensors file.", + "title": "Tensors File" + }, + "pickle_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to data pickle file.", + "title": "Pickle File" + }, + "defaults_file": { + "anyOf": [ + { + "format": "file-path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults.", + "title": "Defaults File" + } + }, + "title": "OmniSampleOverrides", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "guardrails": { + "default": false, + "description": "Enable guardrails.", + "title": "Guardrails", + "type": "boolean" + }, + "offload_guardrail_models": { + "default": false, + "description": "Offload guardrail models to CPU.", + "title": "Offload Guardrail Models", + "type": "boolean" + }, + "parallelism_preset": { + "default": "latency", + "description": "Preset for automatic sharding.", + "enum": [ + "throughput", + "latency" + ], + "title": "Parallelism Preset", + "type": "string" + }, + "device_memory_utilization": { + "default": 0.75, + "description": "Fraction of device memory to use for model weights.\n\nUsed for automatic sharding.", + "maximum": 1.0, + "minimum": 0.0, + "title": "Device Memory Utilization", + "type": "number" + }, + "dp_replicate_size": { + "default": 0, + "minimum": 0, + "title": "Dp Replicate Size", + "type": "integer" + }, + "dp_shard_size": { + "default": 0, + "minimum": 0, + "title": "Dp Shard Size", + "type": "integer" + }, + "tp_size": { + "default": 1, + "minimum": 0, + "title": "Tp Size", + "type": "integer" + }, + "cp_size": { + "anyOf": [ + { + "maximum": 8, + "minimum": 1, + "type": "integer" + }, + { + "const": 0, + "type": "integer" + } + ], + "default": 0, + "title": "Cp Size" + }, + "cfgp_size": { + "anyOf": [ + { + "maximum": 2, + "minimum": 1, + "type": "integer" + }, + { + "const": 0, + "type": "integer" + } + ], + "default": 0, + "title": "Cfgp Size" + }, + "use_torch_compile": { + "default": true, + "description": "Whether to use torch compile.", + "title": "Use Torch Compile", + "type": "boolean" + }, + "use_cuda_graphs": { + "default": false, + "title": "Use Cuda Graphs", + "type": "boolean" + }, + "compiled_region": { + "default": "all", + "enum": [ + "all", + "language" + ], + "title": "Compiled Region", + "type": "string" + }, + "compile_dynamic": { + "default": true, + "description": "Compile with symbolic-shape kernels (maps to ``torch.compile(dynamic=...)``).\n\nDefaults to ``True`` for backward compatibility with training, which can\nsee varying shapes across batches. Setting to ``False`` produces faster\nkernels when the shapes are stable (e.g. single-prompt AR inference), at\nthe cost of a recompile on shape change.", + "title": "Compile Dynamic", + "type": "boolean" + }, + "use_separate_pipeline_vision_decode_gpu": { + "default": false, + "description": "Whether to place pipeline vision decode on a spare local GPU when one is available.", + "title": "Use Separate Pipeline Vision Decode Gpu", + "type": "boolean" + }, + "config_file": { + "default": "cosmos3/_src/vfm/configs/base/config.py", + "description": "Hydra config store module, Hydra config yaml file, or Hugging Face model json file.", + "title": "Config File", + "type": "string" + }, + "config_file_type": { + "anyOf": [ + { + "$ref": "#/$defs/ConfigFileType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hydra config file type." + }, + "experiment": { + "default": "", + "description": "Hydra experiment name.", + "title": "Experiment", + "type": "string" + }, + "experiment_overrides": { + "description": "Hydra experiment overrides.", + "items": { + "type": "string" + }, + "title": "Experiment Overrides", + "type": "array" + }, + "checkpoint_path": { + "description": "Model name or path.\n\n* Model name: Cosmos3-Nano\n* Local path: /path/to/checkpoint", + "title": "Checkpoint Path", + "type": "string" + }, + "checkpoint_type": { + "anyOf": [ + { + "$ref": "#/$defs/CheckpointType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Checkpoint type." + }, + "model_memory_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Approximate model size in bytes.", + "title": "Model Memory Bytes" + }, + "checkpoint_hf": { + "anyOf": [ + { + "$ref": "#/$defs/CheckpointDirHf" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hugging Face checkpoint directory." + }, + "credential_path": { + "default": "credentials/gcp_checkpoint.secret", + "description": "Path to S3 credentials file for remote checkpoint loading.", + "title": "Credential Path", + "type": "string" + }, + "use_ema_weights": { + "default": true, + "description": "If True, use EMA weights. Otherwise, use regular weights.", + "title": "Use Ema Weights", + "type": "boolean" + }, + "checkpoint_cache_dir": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Directory for caching S3 checkpoints.", + "title": "Checkpoint Cache Dir" + }, + "output_dir": { + "anyOf": [ + { + "format": "path", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Output directory.", + "title": "Output Dir" + }, + "keep_going": { + "default": false, + "description": "If True, catch and log errors instead of raising them.", + "title": "Keep Going", + "type": "boolean" + }, + "debug": { + "default": false, + "description": "If True, enable debug outputs.", + "title": "Debug", + "type": "boolean" + }, + "profile": { + "default": false, + "description": "Run profiler and save report to output directory.", + "title": "Profile", + "type": "boolean" + }, + "benchmark": { + "default": false, + "description": "If set, measures and reports inference runtime (disables tqdm).", + "title": "Benchmark", + "type": "boolean" + }, + "warmup": { + "default": 0, + "description": "Number of warmup generations before each sample.", + "minimum": 0, + "title": "Warmup", + "type": "integer" + }, + "max_model_len": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum total tokens per batch. When set, samples are packed into\nbatches by token count.", + "title": "Max Model Len" + }, + "max_num_seqs": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 1, + "description": "Maximum number of sequences per batch. When set, samples are packed into\nbatches by number of sequences.", + "title": "Max Num Seqs" + }, + "variant": { + "const": "omni", + "default": "omni", + "description": "Discriminator.", + "title": "Variant", + "type": "string" + }, + "sample_overrides": { + "$ref": "#/$defs/OmniSampleOverrides", + "default": { + "action_mode": null, + "action_path": null, + "domain_name": null, + "image_size": null, + "action_chunk_size": null, + "raw_action_dim": null, + "vision_path": null, + "condition_frame_indexes_vision": null, + "resolution": null, + "aspect_ratio": null, + "fps": null, + "num_frames": null, + "video_save_quality": null, + "image_save_quality": null, + "prompt_path": null, + "prompt": null, + "negative_prompt": null, + "duration_template": null, + "resolution_template": null, + "negative_metadata_mode": null, + "inverse_duration_template": null, + "inverse_resolution_template": null, + "negative_prompt_keep_metadata": null, + "num_steps": null, + "guidance": null, + "guidance_interval": null, + "normalize_cfg": null, + "shift": null, + "sigma_max": null, + "output_dir": null, + "model": null, + "extra": null, + "name": null, + "num_outputs": null, + "seed": null, + "tensors_file": null, + "pickle_file": null, + "defaults_file": null + } + }, + "model_size": { + "anyOf": [ + { + "enum": [ + "0.6B", + "2B", + "8B", + "30B-A3B", + "32B", + "235B-A22B" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model Size" + }, + "sampler": { + "default": "unipc", + "enum": [ + "unipc", + "edm" + ], + "title": "Sampler", + "type": "string" + } + }, + "required": [ + "checkpoint_path" + ], + "title": "OmniSetupOverrides", + "type": "object" +} diff --git a/schemas/OmniSetupOverrides.yaml b/schemas/OmniSetupOverrides.yaml new file mode 100644 index 00000000..1fe16ff6 --- /dev/null +++ b/schemas/OmniSetupOverrides.yaml @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# If set, measures and reports inference runtime (disables tqdm). +benchmark: false +cfgp_size: 0 +# Directory for caching S3 checkpoints. +checkpoint_cache_dir: null +# Hugging Face checkpoint directory. +checkpoint_hf: null +# Checkpoint type. +checkpoint_type: null +# Compile with symbolic-shape kernels (maps to ``torch.compile(dynamic=...)``). +# +# Defaults to ``True`` for backward compatibility with training, which can +# see varying shapes across batches. Setting to ``False`` produces faster +# kernels when the shapes are stable (e.g. single-prompt AR inference), at +# the cost of a recompile on shape change. +compile_dynamic: true +compiled_region: all +# Hydra config store module, Hydra config yaml file, or Hugging Face model json file. +config_file: cosmos3/_src/vfm/configs/base/config.py +# Hydra config file type. +config_file_type: null +cp_size: 0 +# Path to S3 credentials file for remote checkpoint loading. +credential_path: credentials/gcp_checkpoint.secret +# If True, enable debug outputs. +debug: false +# Fraction of device memory to use for model weights. +# +# Used for automatic sharding. +device_memory_utilization: 0.75 +dp_replicate_size: 0 +dp_shard_size: 0 +# Hydra experiment name. +experiment: '' +# Hydra experiment overrides. +experiment_overrides: [] +# Enable guardrails. +guardrails: false +# If True, catch and log errors instead of raising them. +keep_going: false +# Maximum total tokens per batch. When set, samples are packed into +# batches by token count. +max_model_len: null +# Maximum number of sequences per batch. When set, samples are packed into +# batches by number of sequences. +max_num_seqs: 1 +# Approximate model size in bytes. +model_memory_bytes: null +model_size: null +# Offload guardrail models to CPU. +offload_guardrail_models: false +# Output directory. +output_dir: null +# Preset for automatic sharding. +parallelism_preset: latency +# Run profiler and save report to output directory. +profile: false +sample_overrides: + # Number of action steps to predict. + action_chunk_size: null + # Action mode. When set, activates Action batch construction. + action_mode: null + # Path to action JSON file. Required for forward_dynamics mode. + action_path: null + # Vision aspect ratio. When None, image_edit preserves the input image's native + # aspect ratio; all other modes default to 16:9. + aspect_ratio: null + # Latent frame indices to condition on. Defaults to [0] for image, [0, 1] for video. + condition_frame_indexes_vision: null + # Path to a JSON file of per-modality default sample fields. Overrides the built-in defaults. + defaults_file: null + # Action domain name passed to get_domain_id(). + domain_name: null + # Template string for appending duration/fps to prompt. Use {duration} and {fps} placeholders. + duration_template: null + # Extra arguments. + extra: null + # Vision frames per second. Recommended range [10, 30]; quality may be degraded outside of this range. + fps: null + # Guidance scale for the diffusion model. + guidance: null + # Guidance interval for the diffusion model. + guidance_interval: null + # Quality of the saved image (0-100). + image_save_quality: null + # Target image height in pixels (aspect-ratio-preserving resize). + image_size: null + # Inverse template for duration/fps metadata in the negative prompt. + inverse_duration_template: null + # Inverse template for resolution metadata in the negative prompt. + inverse_resolution_template: null + # Model name. + model: null + # Name of the sample. + name: null + # Negative prompt metadata mode: 'none', 'same', or 'inverse'. + negative_metadata_mode: null + # Negative prompt - describing what you don't want in the generated video. + negative_prompt: null + # Compatibility flag. If True and mode is 'none', mode is promoted to 'same'. + negative_prompt_keep_metadata: null + # If True, normalize the CFG output. + normalize_cfg: null + # Number of vision frames. + # + # Range by resolution: 256p: [24, 400], 480p: [24, 300], 720p: [24, 200]. + # Image-only resolutions (e.g. 1080p) require num_frames=1. + num_frames: null + # Number of outputs to generate per sample. + num_outputs: null + # Number of steps for the diffusion model. + num_steps: null + # Output directory. + output_dir: null + # Path to data pickle file. + pickle_file: null + # Text prompt for generation. Only one of 'prompt' or 'prompt_path' should be provided. + prompt: null + # Path to a .txt file containing the prompt. Only one of 'prompt' or 'prompt_path' should be provided. + prompt_path: null + # Dimension of the raw action data. Required when action_path is not provided. + raw_action_dim: null + # Vision resolution. + # + # Defaults to model config resolution. + resolution: null + # Template string for appending resolution to prompt. Use {height} and {width} placeholders. + resolution_template: null + # Seed for the random number generator. + seed: null + # Shift in the UniPC sampler. Ignored when sampler='edm'. + shift: null + # Maximum sigma for the EDM sampler. Ignored when sampler='unipc'. + sigma_max: null + # Path to data tensors file. + tensors_file: null + # Quality of the saved video (0-10). + video_save_quality: null + # Path or URL to conditioning image/video. + vision_path: null +sampler: unipc +tp_size: 1 +use_cuda_graphs: false +# If True, use EMA weights. Otherwise, use regular weights. +use_ema_weights: true +# Whether to place pipeline vision decode on a spare local GPU when one is available. +use_separate_pipeline_vision_decode_gpu: false +# Whether to use torch compile. +use_torch_compile: true +# Discriminator. +variant: omni +# Number of warmup generations before each sample. +warmup: 0 diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..cab92c97 --- /dev/null +++ b/uv.lock @@ -0,0 +1,14279 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +required-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] +conflicts = [[ + { package = "cosmos3", group = "cu128" }, + { package = "cosmos3", group = "cu128-train" }, + { package = "cosmos3", group = "cu130" }, + { package = "cosmos3", group = "cu130-train" }, + { package = "cosmos3", group = "vllm" }, +], [ + { package = "cosmos3", group = "cu128-train" }, + { package = "cosmos3", group = "cu130" }, + { package = "cosmos3", group = "cu130-train" }, + { package = "cosmos3", group = "vllm" }, +], [ + { package = "cosmos3", group = "cu128" }, + { package = "cosmos3", group = "cu128-train" }, + { package = "cosmos3", group = "cu130-train" }, + { package = "cosmos3", group = "vllm" }, +], [ + { package = "cosmos3", group = "cu128-train" }, + { package = "cosmos3", group = "cu130-train" }, + { package = "cosmos3", group = "vllm" }, +]] + +[manifest] +overrides = [ + { name = "lightning", marker = "sys_platform == 'never'" }, + { name = "numpy", specifier = ">=2.0.0,<2.3" }, + { name = "pynvml", marker = "sys_platform == 'never'" }, +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "accelerate" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, +] + +[[package]] +name = "aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.89.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/af/862e216dd6c5e9bc02fb374eeaaa19017c51b90ddfa5692668a3811947bd/anthropic-0.89.0.tar.gz", hash = "sha256:f3d75b8ccef4b35f3702639519e461eba437d4bcdfabb69378c65a02ab7bda66", size = 596758, upload-time = "2026-04-03T18:57:01.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/ba/9f973f22abb512d5d17428a76e4ecbc8d49b9dd1b5a1152576d48c24dc1d/anthropic-0.89.0-py3-none-any.whl", hash = "sha256:c6d23854af798f2471ca3bc653cca394d392cc272fe803d3da9d63575b8445f0", size = 478847, upload-time = "2026-04-03T18:56:59.54Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "apache-tvm-ffi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/3d/4594c14de64e92697a91eec8ac6518ad72a3f30776aff432e68c2c6d9d3d/apache_tvm_ffi-0.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d911cbbc83bf12a0d9ec03e5315ff1bb92d95702fe912cd7a050393274382e71", size = 2068752, upload-time = "2026-02-27T19:27:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/0a/827e4f9ae85e1be3037818abd59566d906ba1fe27295c6938b12cc482151/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1c8dd4018420c0d14bace688594710909ce198056ff8ac2ad1cd462b30fe1bdd", size = 2231204, upload-time = "2026-02-27T19:27:04.734Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b6/f1ec5c528918c4dae03885ec472663072a984431d7d7fb04ca0798a2e13c/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f6bc8846d570b8ce38692fc91b530b44cd6ae092c805a844da23970e81b12c0", size = 2323684, upload-time = "2026-02-27T19:27:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/818836fbc0f198da1597896f82d7e6556bf5678cd5150d633214bf14b718/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3ec9149f207a7af3ea3531cad7a0b0d04ded06df4f51a547479d5eb489428dd", size = 2160066, upload-time = "2026-02-27T19:27:07.897Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/2e7d73d055523c2fb31394cd3d55593969a0680619e1c939c2128c2fdd36/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefcd17f61bf503ff0f4ad429e03ef6c241c7d13682f58281d883218b854c9bd", size = 2307014, upload-time = "2026-02-27T19:27:10.287Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/9b99efbeaaed4c78a2b7cfeda6b8fc7d6249616938c05ae0248aa0bf0d56/apache_tvm_ffi-0.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:dd58da01331826fbe6c064d6f0c9bbc2d62883b78df8d15baa8ea21d37507e4d", size = 1993158, upload-time = "2026-02-27T19:27:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/44/130571cede8704b1412e48b3dd78de41b4d31b68241f954743d1a9925bd9/apache_tvm_ffi-0.1.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:932d94e29595a47109f0ef6e0b4209a934451582954ea8b426e758d6b3e307e3", size = 2070368, upload-time = "2026-02-27T19:27:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/9f2cfd6d49b03c5d4ec5c12548d911e2e01265be783f343103b4df716765/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c0449fc3802987c3652bea266ffda2934a6f69c80bba791a3f55b91040656a18", size = 2231154, upload-time = "2026-02-27T19:27:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/43/63faedea83494e99122466a993bcdccd31cf93c7e8a0d56731120e82e2b9/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f16d73a82a9e68a439b7d233d48b1b929be17fe92df4bbf1ee2274e573144a3", size = 2323130, upload-time = "2026-02-27T19:27:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/d735bc4c528efaf0a8a954076963c727aad2dde8577641aa9025ec4f2d52/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01ebb1308b2666c206aa9a4015eb48f03a5d98ea2e9cfb002bd5e2ca0b9c7ef3", size = 2159854, upload-time = "2026-02-27T19:27:18.789Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/6cfc82a3ab5d9e501bbcee5df36eebe09da1c384461d7a55e2a17776d117/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21365abd2a2a1a6d3b4e6e4f048309651125becfa795440c3607f3cc27d30ac7", size = 2307140, upload-time = "2026-02-27T19:27:20.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/3ffe1fe3190e12807a12b72ed0d291c7f66569c2e7c3571fde18175f19e1/apache_tvm_ffi-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:9ee710a9fba3d9ff9747870bbd7e2175eb8d5b9c791f17fd645f35f6dab3f8aa", size = 1993218, upload-time = "2026-02-27T19:27:22.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/f2/b8c4b151169f6d7ba8773c8af68b2e0c1013d7fb3f1bdf87573f47157ce9/apache_tvm_ffi-0.1.9-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:49e52350b0470654847de752e65603b604a4d3323e7e9f5e8a982f44acc4c143", size = 2041756, upload-time = "2026-02-27T19:27:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/1b7dc5f0807f83098183a57db6ee85b2c93b646d74a6e03781c9208aaeb0/apache_tvm_ffi-0.1.9-cp312-abi3-win_amd64.whl", hash = "sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b", size = 1973200, upload-time = "2026-02-27T19:27:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1e/991ae65e64ce132c1ba665562db6638f5696d6133f580e20c653de33b9af/apache_tvm_ffi-0.1.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3349f72ddb8ce206472d0380a729f213017a2180707096f8d57114b81097dd1", size = 2072944, upload-time = "2026-02-27T19:27:34.261Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/1e0643949e683fb3cfababd87058c0cfef122d1a3bb6ce703f719051b842/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d1f4d2b7ec7b1213632e9a104e9330bfc3dec48decffa62114c33aa188c9f43a", size = 2215954, upload-time = "2026-02-27T19:27:35.872Z" }, + { url = "https://files.pythonhosted.org/packages/d6/06/5016191ab61d2db4c3a7d754a3c1184e0836f575a7d08491669738c5e4b9/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4f01d16ba53fe118e363f7257253f07003797e4abe6fc9567f23b6a930dbff2", size = 2307291, upload-time = "2026-02-27T19:27:37.527Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f5/40bf0667330938efbfc0a51743cc53c79e41b4ece1a8abad3076192c9674/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c0581dd6bfbce7b017ef85cfda08bbe38891cc4b3afbcfaa8bc2d383728e426", size = 2143850, upload-time = "2026-02-27T19:27:40.437Z" }, + { url = "https://files.pythonhosted.org/packages/72/4a/421cbd4ed32e8bad3b88af3e8fa145c1f6f493bdd05be15b6f2d9b3cb7d6/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dfa14be2a49347791ef21222a8225ce7f99bfec17104a676cb4f1bf3a107088", size = 2289038, upload-time = "2026-02-27T19:27:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/c8923d819b49872a612033b90d29299c0be73a7cbed1ddb3dc78dfe5e9f1/apache_tvm_ffi-0.1.9-cp314-cp314t-win_amd64.whl", hash = "sha256:a42d7ca27dce83efbdce7ec970fe3e773a69c31d928730ee5d9badb1229d106c", size = 2039007, upload-time = "2026-02-27T19:27:43.618Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, +] + +[[package]] +name = "arrgh" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/6e/58751b087ac207324f00ce41dbd8acda3370a77feec56687afd11917796e/arrgh-1.0.0.tar.gz", hash = "sha256:d5dd608184a4ebf9742e5b72a2418bca9dc52f9189caa0ec5c99188d687e0cd7", size = 4850, upload-time = "2023-06-16T00:17:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/9e/c61a0cb4f5868b1025346714da09a36aa04aa5aa34c07d51abc683084e8e/arrgh-1.0.0-py3-none-any.whl", hash = "sha256:05910d1815cfd1bdf8dfa15ec92e43fb4a74932c30386a8b7e5cdbcba428ad84", size = 5927, upload-time = "2023-06-16T00:17:41.458Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asciitree" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e", size = 3951, upload-time = "2016-09-05T19:10:42.681Z" } + +[[package]] +name = "astor" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "async-lru" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/8a/ca724066c32a53fa75f59e0f21aa822fdaa8a0dffa112d223634e3caabf9/async_lru-2.2.0.tar.gz", hash = "sha256:80abae2a237dbc6c60861d621619af39f0d920aea306de34cb992c879e01370c", size = 14654, upload-time = "2026-02-20T19:11:43.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/5c/af990f019b8dd11c5492a6371fe74a5b0276357370030b67254a87329944/async_lru-2.2.0-py3-none-any.whl", hash = "sha256:e2c1cf731eba202b59c5feedaef14ffd9d02ad0037fcda64938699f2c380eafe", size = 7890, upload-time = "2026-02-20T19:11:42.273Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "av" +version = "16.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/51/2217a9249409d2e88e16e3f16f7c0def9fd3e7ffc4238b2ec211f9935bdb/av-16.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:2395748b0c34fe3a150a1721e4f3d4487b939520991b13e7b36f8926b3b12295", size = 26942590, upload-time = "2026-01-09T20:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/a7070f4febc76a327c38808e01e2ff6b94531fe0b321af54ea3915165338/av-16.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:72d7ac832710a158eeb7a93242370aa024a7646516291c562ee7f14a7ea881fd", size = 21507910, upload-time = "2026-01-09T20:18:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/ec812418cd9b297f0238fe20eb0747d8a8b68d82c5f73c56fe519a274143/av-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6cbac833092e66b6b0ac4d81ab077970b8ca874951e9c3974d41d922aaa653ed", size = 38738309, upload-time = "2026-01-09T20:18:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b8/6c5795bf1f05f45c5261f8bce6154e0e5e86b158a6676650ddd77c28805e/av-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:eb990672d97c18f99c02f31c8d5750236f770ffe354b5a52c5f4d16c5e65f619", size = 40293006, upload-time = "2026-01-09T20:18:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a7/44/5e183bcb9333fc3372ee6e683be8b0c9b515a506894b2d32ff465430c074/av-16.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05ad70933ac3b8ef896a820ea64b33b6cca91a5fac5259cb9ba7fa010435be15", size = 40123516, upload-time = "2026-01-09T20:18:09.955Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/b5346d582a3c3d958b4d26a2cc63ce607233582d956121eb20d2bbe55c2e/av-16.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d831a1062a3c47520bf99de6ec682bd1d64a40dfa958e5457bb613c5270e7ce3", size = 41463289, upload-time = "2026-01-09T20:18:12.459Z" }, + { url = "https://files.pythonhosted.org/packages/fa/31/acc946c0545f72b8d0d74584cb2a0ade9b7dfe2190af3ef9aa52a2e3c0b1/av-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:358ab910fef3c5a806c55176f2b27e5663b33c4d0a692dafeb049c6ed71f8aff", size = 31754959, upload-time = "2026-01-09T20:18:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/48/d0/b71b65d1b36520dcb8291a2307d98b7fc12329a45614a303ff92ada4d723/av-16.1.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:e88ad64ee9d2b9c4c5d891f16c22ae78e725188b8926eb88187538d9dd0b232f", size = 26927747, upload-time = "2026-01-09T20:18:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/2f/79/720a5a6ccdee06eafa211b945b0a450e3a0b8fc3d12922f0f3c454d870d2/av-16.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cb296073fa6935724de72593800ba86ae49ed48af03960a4aee34f8a611f442b", size = 21492232, upload-time = "2026-01-09T20:18:19.266Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4f/a1ba8d922f2f6d1a3d52419463ef26dd6c4d43ee364164a71b424b5ae204/av-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:720edd4d25aa73723c1532bb0597806d7b9af5ee34fc02358782c358cfe2f879", size = 39291737, upload-time = "2026-01-09T20:18:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/1a/31/fc62b9fe8738d2693e18d99f040b219e26e8df894c10d065f27c6b4f07e3/av-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c7f2bc703d0df260a1fdf4de4253c7f5500ca9fc57772ea241b0cb241bcf972e", size = 40846822, upload-time = "2026-01-09T20:18:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/53/10/ab446583dbce730000e8e6beec6ec3c2753e628c7f78f334a35cad0317f4/av-16.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d69c393809babada7d54964d56099e4b30a3e1f8b5736ca5e27bd7be0e0f3c83", size = 40675604, upload-time = "2026-01-09T20:18:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/31/d7/1003be685277005f6d63fd9e64904ee222fe1f7a0ea70af313468bb597db/av-16.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:441892be28582356d53f282873c5a951592daaf71642c7f20165e3ddcb0b4c63", size = 42015955, upload-time = "2026-01-09T20:18:29.461Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4a/fa2a38ee9306bf4579f556f94ecbc757520652eb91294d2a99c7cf7623b9/av-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:273a3e32de64819e4a1cd96341824299fe06f70c46f2288b5dc4173944f0fd62", size = 31750339, upload-time = "2026-01-09T20:18:32.249Z" }, + { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, + { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/2a/63797a4dde34283dd8054219fcb29294ba1c25d68ba8c8c8a6ae53c62c45/av-16.1.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:ce2a1b3d8bf619f6c47a9f28cfa7518ff75ddd516c234a4ee351037b05e6a587", size = 26916715, upload-time = "2026-01-11T09:57:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c4/0b49cf730d0ae8cda925402f18ae814aef351f5772d14da72dd87ff66448/av-16.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:408dbe6a2573ca58a855eb8cd854112b33ea598651902c36709f5f84c991ed8e", size = 21452167, upload-time = "2026-01-11T09:57:50.606Z" }, + { url = "https://files.pythonhosted.org/packages/51/23/408806503e8d5d840975aad5699b153aaa21eb6de41ade75248a79b7a37f/av-16.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:57f657f86652a160a8a01887aaab82282f9e629abf94c780bbdbb01595d6f0f7", size = 39215659, upload-time = "2026-01-11T09:57:53.757Z" }, + { url = "https://files.pythonhosted.org/packages/c4/19/a8528d5bba592b3903f44c28dab9cc653c95fcf7393f382d2751a1d1523e/av-16.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:adbad2b355c2ee4552cac59762809d791bda90586d134a33c6f13727fb86cb3a", size = 40874970, upload-time = "2026-01-11T09:57:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/e8/24/2dbcdf0e929ad56b7df078e514e7bd4ca0d45cba798aff3c8caac097d2f7/av-16.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f42e1a68ec2aebd21f7eb6895be69efa6aa27eec1670536876399725bbda4b99", size = 40530345, upload-time = "2026-01-11T09:58:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/54/27/ae91b41207f34e99602d1c72ab6ffd9c51d7c67e3fbcd4e3a6c0e54f882c/av-16.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58fe47aeaef0f100c40ec8a5de9abbd37f118d3ca03829a1009cf288e9aef67c", size = 41972163, upload-time = "2026-01-11T09:58:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7a/22158fb923b2a9a00dfab0e96ef2e8a1763a94dd89e666a5858412383d46/av-16.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:565093ebc93b2f4b76782589564869dadfa83af5b852edebedd8fee746457d06", size = 31729230, upload-time = "2026-01-11T09:58:07.254Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f1/878f8687d801d6c4565d57ebec08449c46f75126ebca8e0fed6986599627/av-16.1.0-cp313-cp313t-macosx_11_0_x86_64.whl", hash = "sha256:574081a24edb98343fd9f473e21ae155bf61443d4ec9d7708987fa597d6b04b2", size = 27008769, upload-time = "2026-01-11T09:58:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/30/f1/bd4ce8c8b5cbf1d43e27048e436cbc9de628d48ede088a1d0a993768eb86/av-16.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:9ab00ea29c25ebf2ea1d1e928d7babb3532d562481c5d96c0829212b70756ad0", size = 21590588, upload-time = "2026-01-11T09:58:12.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/c81f6f9209201ff0b5d5bed6da6c6e641eef52d8fbc930d738c3f4f6f75d/av-16.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a84a91188c1071f238a9523fd42dbe567fb2e2607b22b779851b2ce0eac1b560", size = 40638029, upload-time = "2026-01-11T09:58:15.399Z" }, + { url = "https://files.pythonhosted.org/packages/15/4d/07edff82b78d0459a6e807e01cd280d3180ce832efc1543de80d77676722/av-16.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c2cd0de4dd022a7225ff224fde8e7971496d700be41c50adaaa26c07bb50bf97", size = 41970776, upload-time = "2026-01-11T09:58:19.075Z" }, + { url = "https://files.pythonhosted.org/packages/da/9d/1f48b354b82fa135d388477cd1b11b81bdd4384bd6a42a60808e2ec2d66b/av-16.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0816143530624a5a93bc5494f8c6eeaf77549b9366709c2ac8566c1e9bff6df5", size = 41764751, upload-time = "2026-01-11T09:58:22.788Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c7/a509801e98db35ec552dd79da7bdbcff7104044bfeb4c7d196c1ce121593/av-16.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e3a28053af29644696d0c007e897d19b1197585834660a54773e12a40b16974c", size = 43034355, upload-time = "2026-01-11T09:58:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e5f530d9e8f640da5f5c5f681a424c65f9dd171c871cd255d8a861785a6e/av-16.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e3e67144a202b95ed299d165232533989390a9ea3119d37eccec697dc6dbb0c", size = 31947047, upload-time = "2026-01-11T09:58:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/df/18/8812221108c27d19f7e5f486a82c827923061edf55f906824ee0fcaadf50/av-16.1.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:39a634d8e5a87e78ea80772774bfd20c0721f0d633837ff185f36c9d14ffede4", size = 26916179, upload-time = "2026-01-11T09:58:36.506Z" }, + { url = "https://files.pythonhosted.org/packages/38/ef/49d128a9ddce42a2766fe2b6595bd9c49e067ad8937a560f7838a541464e/av-16.1.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0ba32fb9e9300948a7fa9f8a3fc686e6f7f77599a665c71eb2118fdfd2c743f9", size = 21460168, upload-time = "2026-01-11T09:58:39.231Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a9/b310d390844656fa74eeb8c2750e98030877c75b97551a23a77d3f982741/av-16.1.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca04d17815182d34ce3edc53cbda78a4f36e956c0fd73e3bab249872a831c4d7", size = 39210194, upload-time = "2026-01-11T09:58:42.138Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/e65aae179929d0f173af6e474ad1489b5b5ad4c968a62c42758d619e54cf/av-16.1.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee0e8de2e124a9ef53c955fe2add6ee7c56cc8fd83318265549e44057db77142", size = 40811675, upload-time = "2026-01-11T09:58:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/5d7edefd26b6a5187d6fac0f5065ee286109934f3dea607ef05e53f05b31/av-16.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22bf77a2f658827043a1e184b479c3bf25c4c43ab32353677df2d119f080e28f", size = 40543942, upload-time = "2026-01-11T09:58:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/f8b17897b67be0900a211142f5646a99d896168f54d57c81f3e018853796/av-16.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2dd419d262e6a71cab206d80bbf28e0a10d0f227b671cdf5e854c028faa2d043", size = 41924336, upload-time = "2026-01-11T09:58:53.344Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/d32bc6bbbcf60b65f6510c54690ed3ae1c4ca5d9fafbce835b6056858686/av-16.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:53585986fd431cd436f290fba662cfb44d9494fbc2949a183de00acc5b33fa88", size = 31735077, upload-time = "2026-01-11T09:58:56.684Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/9b63dc70af8636399bd933e9df4f3025a0294609510239782c1b746fc796/av-16.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:76f5ed8495cf41e1209a5775d3699dc63fdc1740b94a095e2485f13586593205", size = 27014423, upload-time = "2026-01-11T09:58:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/d1/da/787a07a0d6ed35a0888d7e5cfb8c2ffa202f38b7ad2c657299fac08eb046/av-16.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8d55397190f12a1a3ae7538be58c356cceb2bf50df1b33523817587748ce89e5", size = 21595536, upload-time = "2026-01-11T09:59:02.508Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f4/9a7d8651a611be6e7e3ab7b30bb43779899c8cac5f7293b9fb634c44a3f3/av-16.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9d51d9037437218261b4bbf9df78a95e216f83d7774fbfe8d289230b5b2e28e2", size = 40642490, upload-time = "2026-01-11T09:59:05.842Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e4/eb79bc538a94b4ff93cd4237d00939cba797579f3272490dd0144c165a21/av-16.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0ce07a89c15644407f49d942111ca046e323bbab0a9078ff43ee57c9b4a50dad", size = 41976905, upload-time = "2026-01-11T09:59:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/f6db0dd86b70167a4d55ee0d9d9640983c570d25504f2bde42599f38241e/av-16.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cac0c074892ea97113b53556ff41c99562db7b9f09f098adac1f08318c2acad5", size = 41770481, upload-time = "2026-01-11T09:59:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/33651d658e45e16ab7671ea5fcf3d20980ea7983234f4d8d0c63c65581a5/av-16.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7dec3dcbc35a187ce450f65a2e0dda820d5a9e6553eea8344a1459af11c98649", size = 43036824, upload-time = "2026-01-11T09:59:16.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/7f13361db54d7e02f11552575c0384dadaf0918138f4eaa82ea03a9f9580/av-16.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6f90dc082ff2068ddbe77618400b44d698d25d9c4edac57459e250c16b33d700", size = 31948164, upload-time = "2026-01-11T09:59:19.501Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "backports-zstd" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/70/766f6ebbb9db2ed75951f0a671ee15931dc69278c84d9f09b08dd6b67c3e/backports_zstd-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a2db17a6d9bf6b4dc223b3f6414aa9db6d1afe9de9bff61d582c2934ca456a0", size = 435664, upload-time = "2025-12-29T17:25:29.201Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/7b3fad9c6ee5ff3bcd7c941586675007330197ff4a388f01c73198ecc8bb/backports_zstd-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7f16b98ba81780a9517ce6c493e1aea9b7d72de2b1efa08375136c270e1ecba", size = 362060, upload-time = "2025-12-29T17:25:30.94Z" }, + { url = "https://files.pythonhosted.org/packages/68/9e/cad0f508ed7c3fbd07398f22b5bf25aa0523fcf56c84c3def642909e80ae/backports_zstd-1.3.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:1124a169a647671ccb4654a0ef1d0b42d6735c45ce3d0adf609df22fb1f099db", size = 505958, upload-time = "2025-12-29T17:25:32.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/96dc55c043b0d86e53ae9608b496196936244c1ecf7e95cdf66d0dbc0f23/backports_zstd-1.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8410fda08b36202d01ab4503f6787c763898888cb1a48c19fce94711563d3ee3", size = 475571, upload-time = "2025-12-29T17:25:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/48/d9c8c8c2a5ac57fc5697f1945254af31407b0c5f80335a175a7c215b4118/backports_zstd-1.3.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab139d1fc0e91a697e82fa834e6404098802f11b6035607174776173ded9a2cc", size = 581199, upload-time = "2025-12-29T17:25:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/7fe70d2d39ed39e26a6c6f6c1dd229f1ab889500d5c90b17527702b1a21e/backports_zstd-1.3.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f3115d203f387f77c23b5461fb6678d282d4f276f9f39298ad242b00120afc7", size = 640846, upload-time = "2025-12-29T17:25:36.86Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d8/5b8580469e70b72402212885bf19b9d31eaf23549b602e0c294edf380e25/backports_zstd-1.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:116f65cce84e215dfac0414924b051faf8d29dc7188cf3944dd1e5be8dd15a32", size = 491061, upload-time = "2025-12-29T17:25:38.721Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/17a752263fccd1ba24184b7e89c14cd31553d512e2e5b065f38e63a0ba86/backports_zstd-1.3.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04def169e4a9ae291298124da4e097c6d6545d0e93164f934b716da04d24630a", size = 565071, upload-time = "2025-12-29T17:25:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/df23d3fe664b2497ab2ec01dc012cb9304e7d568c67f50b1b324fb2d8cbb/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:481b586291ef02a250f03d4c31a37c9881e5e93556568abbd20ca1ad720d443f", size = 481518, upload-time = "2025-12-29T17:25:41.925Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cd/e50dd85fde890c5d79e1ed5dc241f1c45f87b6c12571fdb60add57f2ee66/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0290979eea67f7275fa42d5859cc5bea94f2c08cca6bc36396673476773d2bad", size = 509464, upload-time = "2025-12-29T17:25:43.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bb/e429156e4b834837fe78b4f32ed512491aea39415444420c79ccd3aa0526/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:01c699d8c803dc9f9c9d6ede21b75ec99f45c3b411821011692befca538928cb", size = 585563, upload-time = "2025-12-29T17:25:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/95/c0/1a0d245325827242aefe76f4f3477ec183b996b8db5105698564f8303481/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2c662912cfc1a5ebd1d2162ac651549d58bd3c97a8096130ec13c703fca355f2", size = 562889, upload-time = "2025-12-29T17:25:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/126b2bc7540a15452c3ebdf190ebfea8a8644e29b22f4e10e2a6aa2389e4/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3180c8eb085396928e9946167e610aa625922b82c3e2263c5f17000556370168", size = 631423, upload-time = "2025-12-29T17:25:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/dc/32/018e49657411582569032b7d1bb5d62e514aad8b44952de740ec6250588d/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5b9a8c75a294e7ffa18fc8425a763facc366435a8b442e4dffdc19fa9499a22c", size = 495122, upload-time = "2025-12-29T17:25:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/cdd1d2e1d3612bb90d9cf9b23bea06f2155cdafccd8b6f28a1c4d7750004/backports_zstd-1.3.0-cp310-cp310-win32.whl", hash = "sha256:845defdb172385f17123d92a00d2e952d341e9ae310bfa2410c292bf03846034", size = 288573, upload-time = "2025-12-29T17:25:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/55/7c/2e9c80f08375bd14262cefa69297a926134f517c9955c0795eec5e1d470e/backports_zstd-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:43a9fea6299c801da85221e387b32d90a9ad7c62aa2a34edf525359ce5ad8f3a", size = 313506, upload-time = "2025-12-29T17:25:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5d/fa67e8174f54db44eb33498abb7f98bea4f2329e873b225391bda0113a5e/backports_zstd-1.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:df8473cb117e1316e6c6101f2724e025bd8f50af2dc009d0001c0aabfb5eb57c", size = 288688, upload-time = "2025-12-29T17:25:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:249f90b39d3741c48620021a968b35f268ca70e35f555abeea9ff95a451f35f9", size = 435660, upload-time = "2025-12-29T17:25:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b0e71e83e46154a9d3ced6d4de9a2fea8207ee1e4832aeecf364dc125eda305c", size = 362056, upload-time = "2025-12-29T17:25:56.729Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/b67ba053a7d6f6dbe2f8a704b7d3a5e01b1d2e2e8edbc9b634f2702ef73c/backports_zstd-1.3.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:cbc6193acd21f96760c94dd71bf32b161223e8503f5277acb0a5ab54e5598957", size = 505957, upload-time = "2025-12-29T17:25:57.941Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1df583adc0ae84a8d13d7139f42eade6d90182b1dd3e0d28f7df3c564b9fd55d", size = 475569, upload-time = "2025-12-29T17:25:59.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/4052473217bd954ccdffda5f7264a0e99e7c4ecf70c0f729845c6a45fc5a/backports_zstd-1.3.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d833fc23aa3cc2e05aeffc7cfadd87b796654ad3a7fb214555cda3f1db2d4dc2", size = 581196, upload-time = "2025-12-29T17:26:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bd/064f6fdb61db3d2c473159ebc844243e650dc032de0f8208443a00127925/backports_zstd-1.3.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:142178fe981061f1d2a57c5348f2cd31a3b6397a35593e7a17dbda817b793a7f", size = 640888, upload-time = "2025-12-29T17:26:02.134Z" }, + { url = "https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eed0a09a163f3a8125a857cb031be87ed052e4a47bc75085ed7fca786e9bb5b", size = 491100, upload-time = "2025-12-29T17:26:03.418Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/f5ac28d74039b7e182a780809dc66b9dbfc893186f5d5444340bba135389/backports_zstd-1.3.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60aa483fef5843749e993dde01229e5eedebca8c283023d27d6bf6800d1d4ce3", size = 565071, upload-time = "2025-12-29T17:26:05.022Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ac/50209aeb92257a642ee987afa1e61d5b6731ab6bf0bff70905856e5aede6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea0886c1b619773544546e243ed73f6d6c2b1ae3c00c904ccc9903a352d731e1", size = 481519, upload-time = "2025-12-29T17:26:06.255Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/b06f64199fb4b2e9437cedbf96d0155ca08aeec35fe81d41065acd44762e/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e137657c830a5ce99be40a1d713eb1d246bae488ada28ff0666ac4387aebdd5", size = 509465, upload-time = "2025-12-29T17:26:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/2c365196e61c8fffbbc930ffd69f1ada7aa1c7210857b3e565031c787ac6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94048c8089755e482e4b34608029cf1142523a625873c272be2b1c9253871a72", size = 585552, upload-time = "2025-12-29T17:26:08.911Z" }, + { url = "https://files.pythonhosted.org/packages/93/8d/c2c4f448bb6b6c9df17410eaedce415e8db0eb25b60d09a3d22a98294d09/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d339c1ec40485e97e600eb9a285fb13169dbf44c5094b945788a62f38b96e533", size = 562893, upload-time = "2025-12-29T17:26:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/2110d4d39115130f7514cbbcec673a885f4052bb68d15e41bc96a7558856/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aeee9210c54cf8bf83f4d263a6d0d6e7a0298aeb5a14a0a95e90487c5c3157c", size = 631462, upload-time = "2025-12-29T17:26:11.99Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a8/d64b59ae0714fdace14e43873f794eff93613e35e3e85eead33a4f44cd80/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba7114a3099e5ea05cbb46568bd0e08bca2ca11e12c6a7b563a24b86b2b4a67f", size = 495125, upload-time = "2025-12-29T17:26:13.218Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/bcff0a091fcf27172c57ae463e49d8dec6dc31e01d7e7bf1ae3aad9c3566/backports_zstd-1.3.0-cp311-cp311-win32.whl", hash = "sha256:08dfdfb85da5915383bfae680b6ac10ab5769ab22e690f9a854320720011ae8e", size = 288664, upload-time = "2025-12-29T17:26:14.791Z" }, + { url = "https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8aac2e7cdcc8f310c16f98a0062b48d0a081dbb82862794f4f4f5bdafde30a4", size = 313633, upload-time = "2025-12-29T17:26:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/e7/eca40858883029fc716660106069b23253e2ec5fd34e86b4101c8cfe864b/backports_zstd-1.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:440ef1be06e82dc0d69dbb57177f2ce98bbd2151013ee7e551e2f2b54caa6120", size = 288814, upload-time = "2025-12-29T17:26:17.571Z" }, + { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" }, + { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" }, + { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" }, + { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" }, + { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" }, + { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" }, + { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" }, + { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/e843d32122f25d9568e75d1e7a29c00eae5e5728015604f3f6d02259b3a5/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3ab0d5632b84eff4355c42a04668cfe6466f7d390890f718978582bd1ff36949", size = 409771, upload-time = "2025-12-29T17:27:48.869Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a5/d6a897d4b91732f54b4506858f1da65d7a5b2dc0dbe36a23992a64f09f5a/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b97cea95dbb1a97c02afd718155fad93f747815069722107a429804c355e206", size = 339289, upload-time = "2025-12-29T17:27:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/f0ce566ec221b284508eebbf574a779ba4a8932830db6ea03b6176f336a2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:477895f2642f9397aeba69618df2c91d7f336e02df83d1e623ac37c5d3a5115e", size = 420335, upload-time = "2025-12-29T17:27:51.455Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/bf55652c84c79b2565d3087265bcb097719540a313dee16359a54d83ab4e/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:330172aaf5fd3bfa53f49318abc6d1d4238cb043c384cf71f7b8f0fe2fb7ce31", size = 393880, upload-time = "2025-12-29T17:27:52.869Z" }, + { url = "https://files.pythonhosted.org/packages/be/e0/d1feebb70ffeb150e2891c6f09700079f4a60085ebc67529eb1ca72fb5c2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32974e71eff15897ed3f8b7766a753d9f3197ea4f1c9025d80f8de099a691b99", size = 413840, upload-time = "2025-12-29T17:27:54.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/28/3b7be27ae51e418d3a724bbc4cb7fea77b6bd38b5007e333a56b0cb165c8/backports_zstd-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:993e3a34eaba5928a2065545e34bf75c65b9c34ecb67e43d5ef49b16cc182077", size = 299685, upload-time = "2025-12-29T17:27:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d9/8c9c246e5ea79a4f45d551088b11b61f2dc7efcdc5dbe6df3be84a506e0c/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:968167d29f012cee7b112ad031a8925e484e97e99288e55e4d62962c3a1013e3", size = 409666, upload-time = "2025-12-29T17:27:57.37Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4f/a55b33c314ca8c9074e99daab54d04c5d212070ae7dbc435329baf1b139e/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8f6fc7d62b71083b574193dd8fb3a60e6bb34880cc0132aad242943af301f7a", size = 339199, upload-time = "2025-12-29T17:27:58.542Z" }, + { url = "https://files.pythonhosted.org/packages/9d/13/ce31bd048b1c88d0f65d7af60b6cf89cfbed826c7c978f0ebca9a8a71cfc/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:e0f2eca6aac280fdb77991ad3362487ee91a7fb064ad40043fb5a0bf5a376943", size = 420332, upload-time = "2025-12-29T17:28:00.332Z" }, + { url = "https://files.pythonhosted.org/packages/cf/80/c0cdbc533d0037b57248588403a3afb050b2a83b8c38aa608e31b3a4d600/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676eb5e177d4ef528cf3baaeea4fffe05f664e4dd985d3ac06960ef4619c81a9", size = 393879, upload-time = "2025-12-29T17:28:01.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/38/c97428867cac058ed196ccaeddfdf82ecd43b8a65965f2950a6e7547e77a/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:199eb9bd8aca6a9d489c41a682fad22c587dffe57b613d0fe6d492d0d38ce7c5", size = 413842, upload-time = "2025-12-29T17:28:03.113Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ec/6247be6536668fe1c7dfae3eaa9c94b00b956b716957c0fc986ba78c3cc4/backports_zstd-1.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2524bd6777a828d5e7ccd7bd1a57f9e7007ae654fc2bd1bc1a207f6428674e4a", size = 299684, upload-time = "2025-12-29T17:28:04.856Z" }, +] + +[[package]] +name = "bddl" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupytext" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/37/0211f82891a9f14efcfd2b2096f8d9e4351398ad637fdd1ee59cfc580b0e/bddl-1.0.1.tar.gz", hash = "sha256:1fa4e6e5050b93888ff6fd8455c39bfb29d3864ce06b4c37c0f781f513a2ae26", size = 164809, upload-time = "2022-03-08T01:48:23.564Z" } + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "better-profanity" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/52966c1c883819f0b48540312a4a7f63b5c49f4e5b1a10838ae7f06ebb3c/better_profanity-0.7.0.tar.gz", hash = "sha256:8a6fdc8606d7471e7b5f6801917eca98ec211098262e82f62da4f5de3a73145b", size = 29977, upload-time = "2020-11-02T10:49:57.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/dd/0b074d89e903cc771721cde2c4bf3d8c9d114b5bd791af5c62bcf5fb9459/better_profanity-0.7.0-py3-none-any.whl", hash = "sha256:bd4c529ea6aa2db1aaa50524be1ed14d0fe5c664f1fd88c8bc388c7e9f9f00e8", size = 46104, upload-time = "2020-11-02T10:49:56.066Z" }, +] + +[[package]] +name = "blake3" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a0/fbe66cf17f72cab1600246b90db6cb39b52a88335b9bd2821688379d8dde/blake3-1.0.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8956bb9aec47b6c37ccce935a943588f1f5e6e2e85d43bb7cb76a574238f8a9b", size = 350634, upload-time = "2025-10-14T06:45:09.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/f4b88873054aa87b8c36398775713bf674807e7449a9c7fefe35d3cf1dc5/blake3-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7adbbee5dd0c302218eb8acdfd82b7006930eb5798f56f79f9cca89f6f192662", size = 328382, upload-time = "2025-10-14T06:45:11.137Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/4c37ced9358cece71f2f380a57f77a449f6e87cc6d9f450613237b7a3078/blake3-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:859cd57bac097a2cd63cb36d64c2f6f16c9edece5590f929e70157478e46dc9e", size = 371337, upload-time = "2025-10-14T06:45:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/d1/df/0825da1cde7ca63a8bcdc785ca7f8647b025e9497eef18c75bb9754dbd26/blake3-1.0.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e1d70bf76c02846d0868a3d413eb6c430b76a315e12f1b2e59b5cf56c1f62a3", size = 374945, upload-time = "2025-10-14T06:45:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/43f10c623179dce789ca9e3b8f4064fb6312e99f05c1aae360d07ad95bb0/blake3-1.0.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3fe26f145fcb82931d1820b55c0279f72f8f8e49450dd9d74efbfd409b28423", size = 448766, upload-time = "2025-10-14T06:45:15.471Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/9431bf5fe0eedeb2aadb4fe81fb18945cf8d49adad98e7988fb3cdac76c2/blake3-1.0.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c076d58ee37eb5b2d8d91bb9db59c5a008fd59c71845dc57fe438aeeabaf10", size = 507107, upload-time = "2025-10-14T06:45:17.055Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/3712cdaebaefa8d5acec46f8df7861ba1832e1e188bc1333dd5acd31f760/blake3-1.0.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78731ce7fca46f776ae45fb5271a2a76c4a92c9687dd4337e84b2ae9a174b28f", size = 393955, upload-time = "2025-10-14T06:45:18.718Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d0/add0441e7aaa6b358cac0ddc9246f0799b60d25f06bd542b554afe19fd85/blake3-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65e373c8b47174b969ee61a89ee56922f722972eb650192845c8546df8d9db9", size = 387577, upload-time = "2025-10-14T06:45:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/e4a61f5c0cad4d51a886e8f4367e590caaead8a4809892292bf724c4421d/blake3-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db54946792d2b8c6fa4be73e6e334519f13c1b52e7ff346b3e2ec8ad3eb59401", size = 550515, upload-time = "2025-10-14T06:45:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/c7/90c01091465628acff96534e82d4b3bc16ca22c515f69916d2715273c0e3/blake3-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:67d9c42c42eb1c7aedcf901591c743266009fcf48babf6d6f8450f567cb94a84", size = 554650, upload-time = "2025-10-14T06:45:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/812d7125c6e99e5e0e841a9af2c4161ac811c027e08886353df76eae7b96/blake3-1.0.8-cp310-cp310-win32.whl", hash = "sha256:444215a1e5201f8fa4e5c7352e938a7070cd33d66aeb1dd9b1103a64b6920f9e", size = 228695, upload-time = "2025-10-14T06:45:24.255Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/ab9b5c4b650ff397d347451bfb1ad7e6e53dc06c945e2fd091f27a76422e/blake3-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:725c52c4d393c7bd1a10682df322d480734002a1389b320366c660568708846b", size = 215660, upload-time = "2025-10-14T06:45:25.381Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e1/1df74c915fde3c48940247ad64984f40f5968191d7b5230bcc7b31402e7c/blake3-1.0.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9a8946cb6b1d2b2096daaaa89856f39887bce2b78503fa31b78173e3a86fa281", size = 350481, upload-time = "2025-10-14T06:45:26.625Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/7c47ae1f5f8d60783ce6234a8b31db351fc62be243006a6276284ca3d40d/blake3-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:adccc3a139207e02bb7d7bb0715fe0b87069685aad5f3afff820b2f829467904", size = 328039, upload-time = "2025-10-14T06:45:32.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/515209b0c282c360e249b89cd85350d97cfd55fadbb4df736c67b77b27a1/blake3-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fcfe81b3ae3fb5d2e88be0d3259603ff95f0d5ed69f655c28fdaef31e49a470", size = 371092, upload-time = "2025-10-14T06:45:34.062Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/9d342a2bf5817f006bbe947335e5d387327541ea47590854947befd01251/blake3-1.0.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ce8d45a5bb5326482de72ea1969a378634236186a970fef63058a5b7b8b435", size = 374859, upload-time = "2025-10-14T06:45:35.262Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fc/ea4bef850a7ec9fbb383503fd3c56056dd9fa44e10c3bc61050ab7b2bac0/blake3-1.0.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83605dbf43f581d8b7175b7f3bfe5388bad5a7c6ac175c9c11d669da31133f4b", size = 448585, upload-time = "2025-10-14T06:45:36.542Z" }, + { url = "https://files.pythonhosted.org/packages/a5/67/167a65a4c431715407d07b1b8b1367698a3ad88e7260edb85f0c5293f08a/blake3-1.0.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b5573b052777142b2cecc453d022c3f21aa4aba75011258410bb98f41c1a727", size = 507519, upload-time = "2025-10-14T06:45:37.814Z" }, + { url = "https://files.pythonhosted.org/packages/32/e2/0886e192d634b264c613b0fbf380745b39992b424a0effc00ef08783644e/blake3-1.0.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe1b02ab49bfd969ef50b9f17482a2011c77536654af21807ba5c2674e0bb2a0", size = 393645, upload-time = "2025-10-14T06:45:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3b/7fb2fe615448caaa5f6632b2c7551117b38ccac747a3a5769181e9751641/blake3-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7780666dc6be809b49442d6d5ce06fdbe33024a87560b58471103ec17644682", size = 387640, upload-time = "2025-10-14T06:45:40.546Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8c/2bfc942c6c97cb3d20f341859343bb86ee20af723fedfc886373e606079b/blake3-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af394b50c6aa0b1b957a99453d1ee440ef67cd2d1b5669c731647dc723de8a3a", size = 550316, upload-time = "2025-10-14T06:45:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/75/0252be37620699b79dbaa799c9b402d63142a131d16731df4ef09d135dd7/blake3-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c63ece266a43014cf29e772a82857cd8e90315ae3ed53e3c5204851596edd5f2", size = 554463, upload-time = "2025-10-14T06:45:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6d/d698ae2d5ddd25976fd2c11b079ca071334aecbba6414da8c9cc8e19d833/blake3-1.0.8-cp311-cp311-win32.whl", hash = "sha256:44c2815d4616fad7e2d757d121c0a11780f70ffc817547b3059b5c7e224031a7", size = 228375, upload-time = "2025-10-14T06:45:44.425Z" }, + { url = "https://files.pythonhosted.org/packages/34/d7/33b01e27dc3542dc9ec44132684506f880cd0257b04da0bf7f4b2afa41c8/blake3-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:8f2ef8527a7a8afd99b16997d015851ccc0fe2a409082cebb980af2554e5c74c", size = 215733, upload-time = "2025-10-14T06:45:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a0/b7b6dff04012cfd6e665c09ee446f749bd8ea161b00f730fe1bdecd0f033/blake3-1.0.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b", size = 347983, upload-time = "2025-10-14T06:45:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/264091cac31d7ae913f1f296abc20b8da578b958ffb86100a7ce80e8bf5c/blake3-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865", size = 325415, upload-time = "2025-10-14T06:45:48.482Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" }, + { url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" }, + { url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/55/d1/ca74aa450cbe10e396e061f26f7a043891ffa1485537d6b30d3757e20995/blake3-1.0.8-cp312-cp312-win32.whl", hash = "sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b", size = 228343, upload-time = "2025-10-14T06:46:01.533Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/bbd02647169e3fbed27558555653ac2578c6f17ccacf7d1956c58ef1d214/blake3-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7", size = 215704, upload-time = "2025-10-14T06:46:02.79Z" }, + { url = "https://files.pythonhosted.org/packages/55/b8/11de9528c257f7f1633f957ccaff253b706838d22c5d2908e4735798ec01/blake3-1.0.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943", size = 347771, upload-time = "2025-10-14T06:46:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/f7668be55c909678b001ecacff11ad7016cd9b4e9c7cc87b5971d638c5a9/blake3-1.0.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc", size = 325431, upload-time = "2025-10-14T06:46:06.136Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/e8a85fa261894bf7ce7af928ff3408aab60287ab8d58b55d13a3f700b619/blake3-1.0.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d", size = 370994, upload-time = "2025-10-14T06:46:07.398Z" }, + { url = "https://files.pythonhosted.org/packages/62/cd/765b76bb48b8b294fea94c9008b0d82b4cfa0fa2f3c6008d840d01a597e4/blake3-1.0.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f54cff7f15d91dc78a63a2dd02a3dccdc932946f271e2adb4130e0b4cf608ba", size = 374372, upload-time = "2025-10-14T06:46:08.698Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/32084eadbb28592bb07298f0de316d2da586c62f31500a6b1339a7e7b29b/blake3-1.0.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7e12a777f6b798eb8d06f875d6e108e3008bd658d274d8c676dcf98e0f10537", size = 447627, upload-time = "2025-10-14T06:46:10.002Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f4/3788a1d86e17425eea147e28d7195d7053565fc279236a9fd278c2ec495e/blake3-1.0.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddfc59b0176fb31168f08d5dd536e69b1f4f13b5a0f4b0c3be1003efd47f9308", size = 507536, upload-time = "2025-10-14T06:46:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/fe/01/4639cba48513b94192681b4da472cdec843d3001c5344d7051ee5eaef606/blake3-1.0.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2336d5b2a801a7256da21150348f41610a6c21dae885a3acb1ebbd7333d88d8", size = 394105, upload-time = "2025-10-14T06:46:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/21/ae/6e55c19c8460fada86cd1306a390a09b0c5a2e2e424f9317d2edacea439f/blake3-1.0.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4072196547484c95a5a09adbb952e9bb501949f03f9e2a85e7249ef85faaba8", size = 386928, upload-time = "2025-10-14T06:46:16.284Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6c/05b7a5a907df1be53a8f19e7828986fc6b608a44119641ef9c0804fbef15/blake3-1.0.8-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0eab3318ec02f8e16fe549244791ace2ada2c259332f0c77ab22cf94dfff7130", size = 550003, upload-time = "2025-10-14T06:46:17.791Z" }, + { url = "https://files.pythonhosted.org/packages/b4/03/f0ea4adfedc1717623be6460b3710fcb725ca38082c14274369803f727e1/blake3-1.0.8-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a33b9a1fb6d1d559a8e0d04b041e99419a6bb771311c774f6ff57ed7119c70ed", size = 553857, upload-time = "2025-10-14T06:46:19.088Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6f/e5410d2e2a30c8aba8389ffc1c0061356916bf5ecd0a210344e7b69b62ab/blake3-1.0.8-cp313-cp313-win32.whl", hash = "sha256:e171b169cb7ea618e362a4dddb7a4d4c173bbc08b9ba41ea3086dd1265530d4f", size = 228315, upload-time = "2025-10-14T06:46:20.391Z" }, + { url = "https://files.pythonhosted.org/packages/79/ef/d9c297956dfecd893f29f59e7b22445aba5b47b7f6815d9ba5dcd73fcae6/blake3-1.0.8-cp313-cp313-win_amd64.whl", hash = "sha256:3168c457255b5d2a2fc356ba696996fcaff5d38284f968210d54376312107662", size = 215477, upload-time = "2025-10-14T06:46:21.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/ba/eaa7723d66dd8ab762a3e85e139bb9c46167b751df6e950ad287adb8fb61/blake3-1.0.8-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4d672c24dc15ec617d212a338a4ca14b449829b6072d09c96c63b6e6b621aed", size = 347289, upload-time = "2025-10-14T06:46:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/6957f6ee27f0d5b8c4efdfda68a1298926a88c099f4dd89c711049d16526/blake3-1.0.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1af0e5a29aa56d4fba904452ae784740997440afd477a15e583c38338e641f41", size = 324444, upload-time = "2025-10-14T06:46:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/13/da/722cebca11238f3b24d3cefd2361c9c9ea47cfa0ad9288eeb4d1e0b7cf93/blake3-1.0.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef153c5860d5bf1cc71aece69b28097d2a392913eb323d6b52555c875d0439fc", size = 370441, upload-time = "2025-10-14T06:46:26.29Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d5/2f7440c8e41c0af995bad3a159e042af0f4ed1994710af5b4766ca918f65/blake3-1.0.8-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ae3689f0c7bfa6ce6ae45cab110e4c3442125c4c23b28f1f097856de26e4d1", size = 374312, upload-time = "2025-10-14T06:46:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6c/fb6a7812e60ce3e110bcbbb11f167caf3e975c589572c41e1271f35f2c41/blake3-1.0.8-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb83532f7456ddeb68dae1b36e1f7c52f9cb72852ac01159bbcb1a12b0f8be0", size = 447007, upload-time = "2025-10-14T06:46:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/13/3b/c99b43fae5047276ea9d944077c190fc1e5f22f57528b9794e21f7adedc6/blake3-1.0.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae7754c7d96e92a70a52e07c732d594cf9924d780f49fffd3a1e9235e0f5ba7", size = 507323, upload-time = "2025-10-14T06:46:30.661Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bb/ba90eddd592f8c074a0694cb0a744b6bd76bfe67a14c2b490c8bdfca3119/blake3-1.0.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bacaae75e98dee3b7da6c5ee3b81ee21a3352dd2477d6f1d1dbfd38cdbf158a", size = 393449, upload-time = "2025-10-14T06:46:31.805Z" }, + { url = "https://files.pythonhosted.org/packages/25/ed/58a2acd0b9e14459cdaef4344db414d4a36e329b9720921b442a454dd443/blake3-1.0.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9456c829601d72852d8ba0af8dae0610f7def1d59f5942efde1e2ef93e8a8b57", size = 386844, upload-time = "2025-10-14T06:46:33.195Z" }, + { url = "https://files.pythonhosted.org/packages/4a/04/fed09845b18d90862100c8e48308261e2f663aab25d3c71a6a0bdda6618b/blake3-1.0.8-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:497ef8096ec4ac1ffba9a66152cee3992337cebf8ea434331d8fd9ce5423d227", size = 549550, upload-time = "2025-10-14T06:46:35.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/1859fddfabc1cc72548c2269d988819aad96d854e25eae00531517925901/blake3-1.0.8-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:511133bab85ff60ed143424ce484d08c60894ff7323f685d7a6095f43f0c85c3", size = 553805, upload-time = "2025-10-14T06:46:36.532Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c7/2969352017f62378e388bb07bb2191bc9a953f818dc1cd6b9dd5c24916e1/blake3-1.0.8-cp313-cp313t-win32.whl", hash = "sha256:9c9fbdacfdeb68f7ca53bb5a7a5a593ec996eaf21155ad5b08d35e6f97e60877", size = 228068, upload-time = "2025-10-14T06:46:37.826Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fc/923e25ac9cadfff1cd20038bcc0854d0f98061eb6bc78e42c43615f5982d/blake3-1.0.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3cec94ed5676821cf371e9c9d25a41b4f3ebdb5724719b31b2749653b7cc1dfa", size = 215369, upload-time = "2025-10-14T06:46:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2a/9f13ea01b03b1b4751a1cc2b6c1ef4b782e19433a59cf35b59cafb2a2696/blake3-1.0.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2c33dac2c6112bc23f961a7ca305c7e34702c8177040eb98d0389d13a347b9e1", size = 347016, upload-time = "2025-10-14T06:46:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/06/8e/8458c4285fbc5de76414f243e4e0fcab795d71a8b75324e14959aee699da/blake3-1.0.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c445eff665d21c3b3b44f864f849a2225b1164c08654beb23224a02f087b7ff1", size = 324496, upload-time = "2025-10-14T06:46:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/49/fa/b913eb9cc4af708c03e01e6b88a8bb3a74833ba4ae4b16b87e2829198e06/blake3-1.0.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47939f04b89c5c6ff1e51e883e5efab1ea1bf01a02f4d208d216dddd63d0dd8", size = 370654, upload-time = "2025-10-14T06:46:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/245e0800c33b99c8f2b570d9a7199b51803694913ee4897f339648502933/blake3-1.0.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73e0b4fa25f6e3078526a592fb38fca85ef204fd02eced6731e1cdd9396552d4", size = 374693, upload-time = "2025-10-14T06:46:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a6/8cb182c8e482071dbdfcc6ec0048271fd48bcb78782d346119ff54993700/blake3-1.0.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0543c57eb9d6dac9d4bced63e9f7f7b546886ac04cec8da3c3d9c8f30cbbb7", size = 447673, upload-time = "2025-10-14T06:46:46.358Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/1cbbb5574d2a9436d1b15e7eb5b9d82e178adcaca71a97b0fddaca4bfe3a/blake3-1.0.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed972ebd553c0c25363459e9fc71a38c045d8419e365b59acd8cd791eff13981", size = 507233, upload-time = "2025-10-14T06:46:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/9c/45/b55825d90af353b3e26c653bab278da9d6563afcf66736677f9397e465be/blake3-1.0.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bafdec95dfffa3f6571e529644744e280337df15ddd9728f224ba70c5779b23", size = 393852, upload-time = "2025-10-14T06:46:49.511Z" }, + { url = "https://files.pythonhosted.org/packages/34/73/9058a1a457dd20491d1b37de53d6876eff125e1520d9b2dd7d0acbc88de2/blake3-1.0.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d78f06f3fb838b34c330e2987090376145cbe5944d8608a0c4779c779618f7b", size = 386442, upload-time = "2025-10-14T06:46:51.205Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/561d537ffc17985e276e08bf4513f1c106f1fdbef571e782604dc4e44070/blake3-1.0.8-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:dd03ff08d1b6e4fdda1cd03826f971ae8966ef6f683a8c68aa27fb21904b5aa9", size = 549929, upload-time = "2025-10-14T06:46:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/03/2f/dbe20d2c57f1a67c63be4ba310bcebc707b945c902a0bde075d2a8f5cd5c/blake3-1.0.8-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4e02a3c499e35bf51fc15b2738aca1a76410804c877bcd914752cac4f71f052a", size = 553750, upload-time = "2025-10-14T06:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/6b/da/c6cb712663c869b2814870c2798e57289c4268c5ac5fb12d467fce244860/blake3-1.0.8-cp314-cp314-win32.whl", hash = "sha256:a585357d5d8774aad9ffc12435de457f9e35cde55e0dc8bc43ab590a6929e59f", size = 228404, upload-time = "2025-10-14T06:46:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/c7dcd8bc3094bba1c4274e432f9e77a7df703532ca000eaa550bd066b870/blake3-1.0.8-cp314-cp314-win_amd64.whl", hash = "sha256:9ab5998e2abd9754819753bc2f1cf3edf82d95402bff46aeef45ed392a5468bf", size = 215460, upload-time = "2025-10-14T06:46:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/75/3c/6c8afd856c353176836daa5cc33a7989e8f54569e9d53eb1c53fc8f80c34/blake3-1.0.8-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2df12f295f95a804338bd300e8fad4a6f54fd49bd4d9c5893855a230b5188a8", size = 347482, upload-time = "2025-10-14T06:47:00.189Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/92cd5501ce8e1f5cabdc0c3ac62d69fdb13ff0b60b62abbb2b6d0a53a790/blake3-1.0.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:63379be58438878eeb76ebe4f0efbeaabf42b79f2cff23b6126b7991588ced67", size = 324376, upload-time = "2025-10-14T06:47:01.413Z" }, + { url = "https://files.pythonhosted.org/packages/11/33/503b37220a3e2e31917ef13722efd00055af51c5e88ae30974c733d7ece6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88d527c247f9609dc1d45a08fd243e39f0d5300d54c57e048de24d4fa9240ebb", size = 370220, upload-time = "2025-10-14T06:47:02.573Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/fe817843adf59516c04d44387bd643b422a3b0400ea95c6ede6a49920737/blake3-1.0.8-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506a47897a11ebe8f3cdeb52f1365d6a2f83959e98ccb0c830f8f73277d4d358", size = 373454, upload-time = "2025-10-14T06:47:03.784Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/90a2a623575373dfc9b683f1bad1bf017feafa5a6d65d94fb09543050740/blake3-1.0.8-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5122a61b3b004bbbd979bdf83a3aaab432da3e2a842d7ddf1c273f2503b4884", size = 447102, upload-time = "2025-10-14T06:47:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/93/ff/4e8ce314f60115c4c657b1fdbe9225b991da4f5bcc5d1c1f1d151e2f39d6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0171e85d56dec1219abdae5f49a0ed12cb3f86a454c29160a64fd8a8166bba37", size = 506791, upload-time = "2025-10-14T06:47:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/44/88/2963a1f18aab52bdcf35379b2b48c34bbc462320c37e76960636b8602c36/blake3-1.0.8-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:003f61e8c41dd9931edddf1cc6a1bb680fb2ac0ad15493ef4a1df9adc59ce9df", size = 393717, upload-time = "2025-10-14T06:47:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/45/d1/a848ed8e8d4e236b9b16381768c9ae99d92890c24886bb4505aa9c3d2033/blake3-1.0.8-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c3151955efb09ba58cd3e1263521e15e9e3866a40d6bd3556d86fc968e8f95", size = 386150, upload-time = "2025-10-14T06:47:10.363Z" }, + { url = "https://files.pythonhosted.org/packages/96/09/e3eb5d60f97c01de23d9f434e6e1fc117efb466eaa1f6ddbbbcb62580d6e/blake3-1.0.8-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:5eb25bca3cee2e0dd746a214784fb36be6a43640c01c55b6b4e26196e72d076c", size = 549120, upload-time = "2025-10-14T06:47:11.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/ad/3d9661c710febb8957dd685fdb3e5a861aa0ac918eda3031365ce45789e2/blake3-1.0.8-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ab4e1dea4fa857944944db78e8f20d99ee2e16b2dea5a14f514fb0607753ac83", size = 553264, upload-time = "2025-10-14T06:47:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/11/55/e332a5b49edf377d0690e95951cca21a00c568f6e37315f9749efee52617/blake3-1.0.8-cp314-cp314t-win32.whl", hash = "sha256:67f1bc11bf59464ef092488c707b13dd4e872db36e25c453dfb6e0c7498df9f1", size = 228116, upload-time = "2025-10-14T06:47:14.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5c/dbd00727a3dd165d7e0e8af40e630cd7e45d77b525a3218afaff8a87358e/blake3-1.0.8-cp314-cp314t-win_amd64.whl", hash = "sha256:421b99cdf1ff2d1bf703bc56c454f4b286fce68454dd8711abbcb5a0df90c19a", size = 215133, upload-time = "2025-10-14T06:47:16.069Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "blobfile" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "lxml" }, + { name = "pycryptodomex" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/3e/9f613b3bf2f70a96a03ee102f8ad0d570d5637674f0e1814e7c301c68134/blobfile-3.2.0.tar.gz", hash = "sha256:78514a9265b9aa7d4607042dc77c5e6461ab27036450ad8e1f6ef9a7f29bf958", size = 78442, upload-time = "2026-02-07T03:10:54.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/ab/e0a104d874f18e2552d981e6e978c64d3c8fa2fad4fbc46e9daa42b31db3/blobfile-3.2.0-py3-none-any.whl", hash = "sha256:e5e4095477da9f09e2077f41320c006001b2102a61f07d41ceaaecdf5d9741d8", size = 76958, upload-time = "2026-02-07T03:10:52.86Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, +] + +[[package]] +name = "braceexpand" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/93/badd4f5ccf25209f3fef2573073da9fe4a45a3da99fca2f800f942130c0f/braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705", size = 7777, upload-time = "2021-05-07T13:49:07.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/93/e8c04e80e82391a6e51f218ca49720f64236bc824e92152a2633b74cf7ab/braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014", size = 5923, upload-time = "2021-05-07T13:49:05.146Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + +[[package]] +name = "cbor2" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/bf/12b337e5354e47f6378da18989480c0c1e2cc5fe9b865e6fab45d6332aa6/cbor2-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55bea0dd9a7d354e35f4e5fe58ceab393e76962713749dc3a0a64a0e5d19545e", size = 70577, upload-time = "2026-03-22T15:55:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/45/7b/74c524ce81c1ddc6c44b4865028ffb7d3a8e7ae653b1061650375a28cbb1/cbor2-5.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095dc49e75572841a9534cbfdabc2a17487ea4ee33341436abc4a7ac7245a3a", size = 261074, upload-time = "2026-03-22T15:55:55.654Z" }, + { url = "https://files.pythonhosted.org/packages/4b/97/c496c71422b2ca18ff2acc5f49e8c45cd7294b7df1359eccec78021b428e/cbor2-5.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25bec7beb2089465382b1be72e78667fe9090598800826559c3e3008cf0db743", size = 255498, upload-time = "2026-03-22T15:55:57.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/40/9bd7e66dba7aea674a440e004faea406de42c49aeac23453954b67768532/cbor2-5.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cc5efec69055c3c470997935d95762be7e4bfd1248d88fb1a33bb7e0f45712e9", size = 255683, upload-time = "2026-03-22T15:55:58.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d1/fa3e158dbe4c08091495b720c604624b285bc272afdbcfac2150725d955b/cbor2-5.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:420d2490c7836c81151b4bd591c35cffc55391e33e7e333c50fda391bcea7d31", size = 250798, upload-time = "2026-03-22T15:55:59.953Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/3186084b441c4b0caebfaaa2c66a57bde82ceaacd469570c77c8030b6b95/cbor2-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d1a21c006760f95acd9509cc5a7d15d6fc82e58f721f94fa9039b4e77189a6e5", size = 69436, upload-time = "2026-03-22T15:56:01.466Z" }, + { url = "https://files.pythonhosted.org/packages/36/1f/57d00cd13e0f9391bcd616795aa78409210a7464570fe21e3b9cd42eb5a7/cbor2-5.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:08388ea54195738602b4c4999966bcaef6f0b17d293c9658658409d9fff96f57", size = 65312, upload-time = "2026-03-22T15:56:02.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/aa/317c7118b8dda4c9563125c1a12c70c5b41e36677964a49c72b1aac061ec/cbor2-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0485d3372fc832c5e16d4eb45fa1a20fc53e806e6c29a1d2b0d3e176cedd52b9", size = 70578, upload-time = "2026-03-22T15:56:03.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/43/fe29b1f897770011a5e7497f4523c2712282ee4a6cbf775ea6383fb7afb9/cbor2-5.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2", size = 268738, upload-time = "2026-03-22T15:56:05.222Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/e494568f3d8aafbcdfe361df44c3bcf5cdab5183e25ea08e3d3f9fcf4075/cbor2-5.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363", size = 262571, upload-time = "2026-03-22T15:56:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/92acd6f87382fd44a34d9d7e85cc45372e6ba664040b72d1d9df648b25d0/cbor2-5.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422", size = 262356, upload-time = "2026-03-22T15:56:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/3f/68/52c039a28688baeeb78b0be7483855e6c66ea05884a937444deede0c87b8/cbor2-5.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7", size = 257604, upload-time = "2026-03-22T15:56:09.835Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/10d96a7f73ed9227090ce6e3df5d73329eb6a267dab7d5b989e6fbf6c504/cbor2-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:1d02b65f070fd726bdc310d927228975bb655d155bf059b6eb7cacefb3dca86f", size = 69388, upload-time = "2026-03-22T15:56:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c6/eea5829aa5a649db540f47ea35f4bf2313383d28246f0cbc50432cfad6b3/cbor2-5.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:837754ece9052b3f607047e1741e5f852a538aa2b0ee3db11c82a8fa11804aa4", size = 65315, upload-time = "2026-03-22T15:56:12.326Z" }, + { url = "https://files.pythonhosted.org/packages/ee/39/72d8a5a4b06565561ec28f4fcb41aff7bb77f51705c01f00b8254a2aca4f/cbor2-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313", size = 71223, upload-time = "2026-03-22T15:56:13.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/fd/7ddf3d3153b54c69c3be77172b8d9aa3a9d74f62a7fbde614d53eaeed9a4/cbor2-5.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc", size = 287865, upload-time = "2026-03-22T15:56:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/db/9d/7ede2cc42f9bb4260492e7d29d2aab781eacbbcfb09d983de1e695077199/cbor2-5.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b", size = 288246, upload-time = "2026-03-22T15:56:16.113Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9d/588ebc7c5bc5843f609b05fe07be8575c7dec987735b0bbc908ac9c1264a/cbor2-5.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f", size = 280214, upload-time = "2026-03-22T15:56:17.519Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a1/6fc8f4b15c6a27e7fbb7966c30c2b4b18c274a3221fa2f5e6235502d34bc/cbor2-5.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073", size = 282162, upload-time = "2026-03-22T15:56:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/cf/20/9a22cfe08be16ddfeef2542cf4eeed1b29f3f57ddbba0b42f7e0bb8331fd/cbor2-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7", size = 70049, upload-time = "2026-03-22T15:56:20.502Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/695f92d09006614034e25a9f5b10620f3b219f79c1bec3c37b7c6f27a7a9/cbor2-5.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4", size = 65382, upload-time = "2026-03-22T15:56:21.526Z" }, + { url = "https://files.pythonhosted.org/packages/81/c5/4901e21a8afe9448fd947b11e8f383903207cd6dd0800e5f5a386838de5b/cbor2-5.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27", size = 71284, upload-time = "2026-03-22T15:56:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/1b/10/df643a381aebc3f05486de4813662bc58accb640fc3275cb276a75e89694/cbor2-5.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0", size = 287682, upload-time = "2026-03-22T15:56:24.024Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/8aa6b766059ae4a0ca1ec3ff96fe3823a69a7be880dba2e249f7fbe2700b/cbor2-5.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a", size = 288009, upload-time = "2026-03-22T15:56:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/6236bc25c183a9cf7e8062e5dddf9eae9b0b14ebf14a58a69fe5a1e872c6/cbor2-5.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec", size = 280437, upload-time = "2026-03-22T15:56:26.479Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0a/84328d23c3c68874ac6497edb9b1900579a1028efa54734df3f1762bbc15/cbor2-5.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b", size = 282247, upload-time = "2026-03-22T15:56:28.644Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f6/89b4627e09d028c8e5fcaf7cb55f225c33ce6e037ec1844e65d02bcfa945/cbor2-5.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf", size = 70089, upload-time = "2026-03-22T15:56:29.801Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/efadcd5f0102db692490e4e206988a2f98d39a09912090db497a2b800885/cbor2-5.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8", size = 65466, upload-time = "2026-03-22T15:56:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/08/7d/9ccc36d10ef96e6038e48046ebe1ce35a1e7814da0e1e204d09e6ef09b8d/cbor2-5.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560", size = 71500, upload-time = "2026-03-22T15:56:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/70/e1/a6cca2cc72e13f00030c6a649f57ae703eb2c620806ab70c40db8eab33fa/cbor2-5.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e", size = 286953, upload-time = "2026-03-22T15:56:33.292Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/24cd5ef488a957d90e016f200a3aad820e4c2f85edd61c9fe4523007a1ee/cbor2-5.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba", size = 285454, upload-time = "2026-03-22T15:56:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/a4/35/dca96818494c0ba47cdd73e8d809b27fa91f8fa0ce32a068a09237687454/cbor2-5.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551", size = 279441, upload-time = "2026-03-22T15:56:35.888Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/d3362378b16e53cf7e535a3f5aed8476e2109068154e24e31981ef5bde9e/cbor2-5.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd", size = 279673, upload-time = "2026-03-22T15:56:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3533a697e5842fff7c2f64912eb251f8dcab3a8b5d88e228d6eebc3b5021/cbor2-5.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3", size = 71940, upload-time = "2026-03-22T15:56:38.366Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e2/c6ba75f3fb25dfa15ab6999cc8709c821987e9ed8e375d7f58539261bcb9/cbor2-5.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975", size = 67639, upload-time = "2026-03-22T15:56:39.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cmake" +version = "4.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/17/f8f42ae205604319cc36f46d9929bd9bfbd83d3d02d6314c44fa97c42006/cmake-4.1.3.tar.gz", hash = "sha256:89f48ddc2570eb62447e33311cffc6dfeb09631bd0a19423d8a59cec8af030f1", size = 34998, upload-time = "2025-11-19T22:41:27.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/79/1bf4009d7ef16d62e0b92ddb78efeda830ca5903149abf9dc01d270c3d4e/cmake-4.1.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:3b6b25ce8fecc768881b36a1dfbca0013adac10a299c73e24cf4cbb99e4c37d6", size = 49246088, upload-time = "2025-11-19T22:40:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9d/14e076406388efa2bbea2366ec0bbe85e2536787ebbb374dda792f068222/cmake-4.1.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893eb9c20d8a8bac3d951bbef9a4ce9d5495cd35a08b4e08d76215f5ead5897", size = 30381441, upload-time = "2025-11-19T22:40:31.55Z" }, + { url = "https://files.pythonhosted.org/packages/f7/9e/0f7216dfef03f1cbac0cdf4685da6994559f5ede3452e563335a35d6a6cb/cmake-4.1.3-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:487faf892ff5e05084c6a7f229dd9e568d0542b88487386acb42f0cb2f6634b6", size = 30781002, upload-time = "2025-11-19T22:40:35.325Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2e/69d9b1eee7b7c68e9ce53f8449e372151b4967c223ecd43c7083a4dece8d/cmake-4.1.3-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3dbddc52f839df0ebc1c6b6915bd78d63d0805137c6f419fbddd587404276c28", size = 32613762, upload-time = "2025-11-19T22:40:39.488Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/deac4d6f8cf4adcaa61d7f16d1ec42d41d471bf330ffcdac4d29c83e46a3/cmake-4.1.3-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b42e99eb6e976f455f29283dd7583270d611b55c7687b5fe8d022d9ae7c95de5", size = 28577197, upload-time = "2025-11-19T22:40:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/323c40671c6f1b3e02bb4a7404fbe2bf653190a56e63cf4b6a4f06e876bc/cmake-4.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81f11b72bc59cbe547d9f283487ef0519bf68176edffcdfa1a4dc5a52f292369", size = 29690899, upload-time = "2025-11-19T22:40:45.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/ab7866f55ee11a07aa446ee31b91b8f337f1b702b9546fc7b18e23d0566f/cmake-4.1.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:fd633c4395b1522caedf0b64034d1a48ea0e483f19e9c2985d14ee7152b21593", size = 26522320, upload-time = "2025-11-19T22:40:48.463Z" }, + { url = "https://files.pythonhosted.org/packages/db/21/a99ed3f1192c85d6d565e61c0cd0161f8046afcf0b0951e6492be632f2f2/cmake-4.1.3-py3-none-manylinux_2_35_riscv64.whl", hash = "sha256:ea40a64b8027f2b7fb1684312a2f170e4d0904b7a4f123cd96e7290103bb1ed4", size = 28869263, upload-time = "2025-11-19T22:40:51.618Z" }, + { url = "https://files.pythonhosted.org/packages/13/66/3c32bb2d5e72f00a0861066b29cc6981cbffcf9786f7339317f151a4d4be/cmake-4.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e3782d5f82e8960290e50747b1fb5ff8396363a656ad5716a3aedc77334ca94f", size = 41751469, upload-time = "2025-11-19T22:40:54.75Z" }, + { url = "https://files.pythonhosted.org/packages/05/60/922c05d62ba5b422afd211966877673ddceb634e95552893bf9a11cc4e58/cmake-4.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44b011b8374aac8f3d7a7fb319b3c25d54c2fd9342d94a855ae3a64240efe828", size = 35040544, upload-time = "2025-11-19T22:40:57.669Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/957336b0489f7d3050cd19010585d4ab5ebcdef485292b9baee68ebbeccf/cmake-4.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f29e924fd6d1a4f2f731eb743cc687b82063f73f15f0b4fb8e2b8a8211faba8", size = 45811680, upload-time = "2025-11-19T22:41:01.124Z" }, + { url = "https://files.pythonhosted.org/packages/85/0d/41e2ac694b156b249bfaccec071897c46b21deeb4db1ec51d949e7843f4b/cmake-4.1.3-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:466cdce904392f18b201471a3a6429cc12b4d98a166faa3ee0ad4461f3043083", size = 45859079, upload-time = "2025-11-19T22:41:04.694Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/42ca38f001b1f1327c19734e4c0080557a7991db832aacfe4b193ba7743a/cmake-4.1.3-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d37db26f98ac26f0858cf6a30157a4be83b29cb195afeb640b355b097f1d94d7", size = 39946757, upload-time = "2025-11-19T22:41:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/73/ab/a3965bfce6376894c76e17af095b0e360a9e1a1719e3df1e244ea6d6d893/cmake-4.1.3-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:18e1e2b7b226763017521ba8721c74d1a2a3cd7d1ec8e889b0b869d4e939370b", size = 44016695, upload-time = "2025-11-19T22:41:11.84Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/fa0e8d3c66459a616f0baf9d22933e14137c259f4b62f0dad9c3723cf42d/cmake-4.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6966746b25d1e9c8d32c731452e220e84331b5133544f710b21bd228a93812ca", size = 43357408, upload-time = "2025-11-19T22:41:15.302Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8f/5c43c6465af62bb16159de113438365c789c5a69261dad36746aa1ec74b8/cmake-4.1.3-py3-none-win32.whl", hash = "sha256:b1c890af27bb548d0a2c0e1affc81ad180fc17d8dfa9545e0658153446fe7db4", size = 34268275, upload-time = "2025-11-19T22:41:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/c1/51/2bc56a4d8d9c2680913f1a7e0b7a33e48100f336df91176b74dda6dff8b3/cmake-4.1.3-py3-none-win_amd64.whl", hash = "sha256:fd5a2ea9a38c6109036d8c912a7db4df2de241cfbc00b7424ae246494387da80", size = 37545974, upload-time = "2025-11-19T22:41:21.85Z" }, + { url = "https://files.pythonhosted.org/packages/36/a5/ec213d5c228ab7a205abeb51cc23aa1be9b586041c40cdccc157c325822a/cmake-4.1.3-py3-none-win_arm64.whl", hash = "sha256:79bd8f92a3385cc6641949b0274cd10ee9a4f45a2c13840121b68b2e90b5af3a", size = 36337597, upload-time = "2025-11-19T22:41:24.968Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorful" +version = "0.5.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/31/109ef4bedeb32b4202e02ddb133162457adc4eb890a9ed9c05c9dd126ed0/colorful-0.5.8.tar.gz", hash = "sha256:bb16502b198be2f1c42ba3c52c703d5f651d826076817185f0294c1a549a7445", size = 209361, upload-time = "2025-10-29T11:53:21.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/11/25cdf9d5fc21efd30134fc74c43702c6f7ef09ebae8ed927f1283403ad8d/colorful-0.5.8-py2.py3-none-any.whl", hash = "sha256:a9381fdda3337fbaba5771991020abc69676afa102646650b759927892875992", size = 201334, upload-time = "2025-10-29T11:53:20.251Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "compressed-tensors" +version = "0.14.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "loguru" }, + { name = "pydantic" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f1/4c9b01ceaf82ad58ad00919223e09b8e74d4073a2ba8e3ab2f97521ef65c/compressed_tensors-0.14.0.1.tar.gz", hash = "sha256:5ad3841184b6f5020e06059b2463191c5c57a144bb97cab9159978d8118839b1", size = 226393, upload-time = "2026-03-11T17:04:35.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/26/16a13993ecf4fdc9c39d63b3a6daabafd32a452cf68b81aa9eb3b8170913/compressed_tensors-0.14.0.1-py3-none-any.whl", hash = "sha256:46c4940a3a779d3d97108c294bfcd9acf4bd0491f7c6737c320f0e815ec732e4", size = 196454, upload-time = "2026-03-11T17:04:33.2Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cosmos3" +version = "1.2.1" +source = { editable = "." } +dependencies = [ + { name = "accelerate" }, + { name = "av" }, + { name = "cattrs" }, + { name = "diffusers" }, + { name = "einops" }, + { name = "hydra-core" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "loguru" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-ml-py" }, + { name = "obstore" }, + { name = "omegaconf" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "termcolor" }, + { name = "transformers" }, + { name = "tyro" }, + { name = "uv" }, +] + +[package.optional-dependencies] +guardrail = [ + { name = "better-profanity" }, + { name = "nltk" }, + { name = "protobuf" }, + { name = "retinaface-py" }, + { name = "sentencepiece" }, +] +interactive = [ + { name = "websockets" }, +] +serve = [ + { name = "fastapi" }, + { name = "gradio" }, + { name = "httpx" }, + { name = "ray", extra = ["serve"] }, +] +train = [ + { name = "aioboto3" }, + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "arrgh" }, + { name = "blobfile" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "datasets" }, + { name = "dists-pytorch" }, + { name = "einx" }, + { name = "fastparquet" }, + { name = "ffmpeg-python" }, + { name = "ffmpegcv" }, + { name = "flask" }, + { name = "flopth" }, + { name = "ftfy" }, + { name = "futureproof" }, + { name = "fvcore" }, + { name = "glfw" }, + { name = "h5py" }, + { name = "imagecodecs", version = "2025.3.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "imagecodecs", version = "2026.1.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "iopath" }, + { name = "ipycanvas" }, + { name = "ipyevents" }, + { name = "jupyter-compare-view" }, + { name = "jupyterlab" }, + { name = "kornia" }, + { name = "lerobot" }, + { name = "lpips" }, + { name = "lz4" }, + { name = "matplotlib" }, + { name = "mediapy" }, + { name = "megatron-core" }, + { name = "more-itertools" }, + { name = "moviepy" }, + { name = "multi-storage-client", extra = ["boto3", "fsspec", "google-cloud-storage", "observability-otel", "vault"] }, + { name = "ninja" }, + { name = "nvidia-dali-cuda120" }, + { name = "open-clip-torch" }, + { name = "openai" }, + { name = "opencv-contrib-python" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "parse" }, + { name = "peft" }, + { name = "pillow" }, + { name = "plyfile" }, + { name = "polars" }, + { name = "polyscope" }, + { name = "psycopg2-binary" }, + { name = "py3nvml" }, + { name = "pycocotools" }, + { name = "pydispatcher" }, + { name = "pygltflib" }, + { name = "pyopengl" }, + { name = "pytest" }, + { name = "python-memcached" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "qwen-vl-utils" }, + { name = "robotmq" }, + { name = "rsa" }, + { name = "s3fs" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "semver" }, + { name = "setuptools" }, + { name = "slangtorch" }, + { name = "soundfile" }, + { name = "tensorstore", version = "0.1.78", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tensorstore", version = "0.1.81", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tiktoken" }, + { name = "timm" }, + { name = "torch-fidelity" }, + { name = "torch-optimizer" }, + { name = "torchtitan" }, + { name = "trimesh" }, + { name = "typeguard" }, + { name = "urllib3" }, + { name = "wandb" }, + { name = "webdataset" }, + { name = "xatlas" }, + { name = "zarr", version = "2.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "zarr", version = "3.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] + +[package.dev-dependencies] +cu128 = [ + { name = "flash-attn", version = "2.7.4.post1+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "flash-attn-3-nv", version = "1.0.3+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "natten", version = "0.21.6.dev6+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cuda-cupti-cu12" }, + { name = "nvidia-cuda-nvrtc-cu12" }, + { name = "nvidia-cuda-runtime-cu12" }, + { name = "nvidia-cudnn-cu12" }, + { name = "nvidia-cufft-cu12" }, + { name = "nvidia-cufile-cu12" }, + { name = "nvidia-curand-cu12" }, + { name = "nvidia-cusolver-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-cusparselt-cu12" }, + { name = "nvidia-nccl-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvshmem-cu12" }, + { name = "nvidia-nvtx-cu12" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchcodec", version = "0.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, +] +cu128-train = [ + { name = "flash-attn", version = "2.7.4.post1+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "flash-attn-3-nv", version = "1.0.3+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "natten", version = "0.21.6.dev6+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cuda-cupti-cu12" }, + { name = "nvidia-cuda-nvrtc-cu12" }, + { name = "nvidia-cuda-runtime-cu12" }, + { name = "nvidia-cudnn-cu12" }, + { name = "nvidia-cufft-cu12" }, + { name = "nvidia-cufile-cu12" }, + { name = "nvidia-curand-cu12" }, + { name = "nvidia-cusolver-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-cusparselt-cu12" }, + { name = "nvidia-nccl-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvshmem-cu12" }, + { name = "nvidia-nvtx-cu12" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchao", version = "0.16.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "platform_machine == 'x86_64'" }, + { name = "torchcodec", version = "0.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "transformer-engine", version = "2.12+cu128.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } }, + { name = "triton" }, +] +cu130 = [ + { name = "flash-attn", version = "2.7.4.post1+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "flash-attn-3-nv", version = "1.0.3+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "natten", version = "0.21.6.dev6+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } }, + { name = "nvidia-cublas" }, + { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cudnn-cu13" }, + { name = "nvidia-cufft" }, + { name = "nvidia-cufile" }, + { name = "nvidia-curand" }, + { name = "nvidia-cusolver" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-cusparselt-cu13" }, + { name = "nvidia-nccl-cu13" }, + { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvshmem-cu13" }, + { name = "nvidia-nvtx" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchcodec", version = "0.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, +] +cu130-train = [ + { name = "flash-attn", version = "2.7.4.post1+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "flash-attn-3-nv", version = "1.0.3+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, marker = "platform_machine == 'x86_64'" }, + { name = "natten", version = "0.21.6.dev6+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } }, + { name = "nvidia-cublas" }, + { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cudnn-cu13" }, + { name = "nvidia-cufft" }, + { name = "nvidia-cufile" }, + { name = "nvidia-curand" }, + { name = "nvidia-cusolver" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-cusparselt-cu13" }, + { name = "nvidia-nccl-cu13" }, + { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvshmem-cu13" }, + { name = "nvidia-nvtx" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchao", version = "0.16.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "platform_machine == 'x86_64'" }, + { name = "torchcodec", version = "0.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, + { name = "transformer-engine", version = "2.12+cu130.torch210", source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } }, + { name = "triton" }, +] +dev = [ + { name = "hatch" }, + { name = "pyinstrument" }, + { name = "pyrefly" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-custom-exit-code" }, + { name = "pytest-env" }, + { name = "pytest-instafail" }, + { name = "pytest-regressions" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "uv" }, +] +libero = [ + { name = "bddl" }, + { name = "cloudpickle" }, + { name = "draccus" }, + { name = "easydict" }, + { name = "gym" }, + { name = "imageio", extra = ["ffmpeg"] }, + { name = "libero" }, + { name = "mujoco" }, +] +vllm = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "vllm" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate" }, + { name = "aioboto3", marker = "extra == 'train'" }, + { name = "aiofiles", marker = "extra == 'train'" }, + { name = "aiohttp", marker = "extra == 'train'" }, + { name = "arrgh", marker = "extra == 'train'" }, + { name = "av" }, + { name = "better-profanity", marker = "extra == 'guardrail'" }, + { name = "blobfile", marker = "extra == 'train'" }, + { name = "boto3", marker = "extra == 'train'" }, + { name = "botocore", marker = "extra == 'train'" }, + { name = "cattrs" }, + { name = "datasets", marker = "extra == 'train'" }, + { name = "diffusers" }, + { name = "dists-pytorch", marker = "extra == 'train'" }, + { name = "einops" }, + { name = "einx", marker = "extra == 'train'" }, + { name = "fastapi", marker = "extra == 'serve'" }, + { name = "fastparquet", marker = "extra == 'train'" }, + { name = "ffmpeg-python", marker = "extra == 'train'" }, + { name = "ffmpegcv", marker = "extra == 'train'" }, + { name = "flask", marker = "extra == 'train'" }, + { name = "flopth", marker = "extra == 'train'" }, + { name = "ftfy", marker = "extra == 'train'" }, + { name = "futureproof", marker = "extra == 'train'" }, + { name = "fvcore", marker = "extra == 'train'" }, + { name = "glfw", marker = "extra == 'train'" }, + { name = "gradio", marker = "extra == 'serve'" }, + { name = "h5py", marker = "extra == 'train'" }, + { name = "httpx", marker = "extra == 'serve'" }, + { name = "hydra-core" }, + { name = "imagecodecs", marker = "extra == 'train'" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "iopath", marker = "extra == 'train'", specifier = ">=0.1.10" }, + { name = "ipycanvas", marker = "extra == 'train'" }, + { name = "ipyevents", marker = "extra == 'train'" }, + { name = "jupyter-compare-view", marker = "extra == 'train'" }, + { name = "jupyterlab", marker = "extra == 'train'" }, + { name = "kornia", marker = "extra == 'train'" }, + { name = "lerobot", marker = "extra == 'train'", git = "https://github.com/mli0603/lerobot.git" }, + { name = "loguru" }, + { name = "lpips", marker = "extra == 'train'" }, + { name = "lz4", marker = "extra == 'train'" }, + { name = "matplotlib", marker = "extra == 'train'" }, + { name = "mediapy", marker = "extra == 'train'" }, + { name = "megatron-core", marker = "extra == 'train'", git = "https://github.com/NVIDIA/Megatron-LM.git?rev=de56227" }, + { name = "more-itertools", marker = "extra == 'train'" }, + { name = "moviepy", marker = "extra == 'train'" }, + { name = "multi-storage-client", extras = ["boto3", "google-cloud-storage", "fsspec", "observability-otel", "vault"], marker = "extra == 'train'", specifier = "==0.44.0" }, + { name = "ninja", marker = "extra == 'train'" }, + { name = "nltk", marker = "extra == 'guardrail'" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-dali-cuda120", marker = "extra == 'train'" }, + { name = "nvidia-ml-py" }, + { name = "obstore" }, + { name = "omegaconf" }, + { name = "open-clip-torch", marker = "extra == 'train'" }, + { name = "openai", marker = "extra == 'train'" }, + { name = "opencv-contrib-python", marker = "extra == 'train'" }, + { name = "packaging", marker = "extra == 'train'" }, + { name = "pandas", marker = "extra == 'train'" }, + { name = "parse", marker = "extra == 'train'" }, + { name = "peft", marker = "extra == 'train'" }, + { name = "pillow", marker = "extra == 'train'" }, + { name = "plyfile", marker = "extra == 'train'" }, + { name = "polars", marker = "extra == 'train'" }, + { name = "polyscope", marker = "extra == 'train'" }, + { name = "protobuf", marker = "extra == 'guardrail'" }, + { name = "psycopg2-binary", marker = "extra == 'train'" }, + { name = "py3nvml", marker = "extra == 'train'" }, + { name = "pycocotools", marker = "extra == 'train'" }, + { name = "pydantic" }, + { name = "pydispatcher", marker = "extra == 'train'" }, + { name = "pygltflib", marker = "extra == 'train'" }, + { name = "pyopengl", marker = "extra == 'train'" }, + { name = "pytest", marker = "extra == 'train'" }, + { name = "python-memcached", marker = "extra == 'train'" }, + { name = "pytz", marker = "extra == 'train'" }, + { name = "pyyaml", marker = "extra == 'train'" }, + { name = "qwen-vl-utils", marker = "extra == 'train'" }, + { name = "ray", extras = ["serve"], marker = "extra == 'serve'" }, + { name = "requests" }, + { name = "retinaface-py", marker = "extra == 'guardrail'" }, + { name = "robotmq", marker = "extra == 'train'" }, + { name = "rsa", marker = "extra == 'train'" }, + { name = "s3fs", marker = "extra == 'train'" }, + { name = "scikit-image", marker = "extra == 'train'" }, + { name = "semver", marker = "extra == 'train'" }, + { name = "sentencepiece", marker = "extra == 'guardrail'" }, + { name = "setuptools", marker = "extra == 'train'" }, + { name = "slangtorch", marker = "extra == 'train'" }, + { name = "soundfile", marker = "extra == 'train'" }, + { name = "tensorstore", marker = "extra == 'train'" }, + { name = "termcolor" }, + { name = "tiktoken", marker = "extra == 'train'" }, + { name = "timm", marker = "extra == 'train'" }, + { name = "torch-fidelity", marker = "extra == 'train'" }, + { name = "torch-optimizer", marker = "extra == 'train'" }, + { name = "torchtitan", marker = "extra == 'train'" }, + { name = "transformers", specifier = ">=4.57.1,<5.0.0" }, + { name = "trimesh", marker = "extra == 'train'" }, + { name = "typeguard", marker = "extra == 'train'" }, + { name = "tyro" }, + { name = "urllib3", marker = "extra == 'train'" }, + { name = "uv" }, + { name = "wandb", marker = "extra == 'train'" }, + { name = "webdataset", marker = "extra == 'train'" }, + { name = "websockets", marker = "extra == 'interactive'" }, + { name = "xatlas", marker = "extra == 'train'" }, + { name = "zarr", marker = "extra == 'train'" }, +] +provides-extras = ["guardrail", "interactive", "serve", "train"] + +[package.metadata.requires-dev] +cu128 = [ + { name = "flash-attn", marker = "platform_machine == 'x86_64'", specifier = "==2.7.4.post1+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "flash-attn-3-nv", marker = "platform_machine == 'x86_64'", specifier = "==1.0.3+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "natten", marker = "platform_machine == 'x86_64'", specifier = "==0.21.6.dev6+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "nvidia-cublas-cu12", specifier = "==12.8.4.1" }, + { name = "nvidia-cuda-cupti-cu12", specifier = "==12.8.90" }, + { name = "nvidia-cuda-nvrtc-cu12", specifier = "==12.8.93" }, + { name = "nvidia-cuda-runtime-cu12", specifier = "==12.8.90" }, + { name = "nvidia-cudnn-cu12", specifier = "==9.10.2.21" }, + { name = "nvidia-cufft-cu12", specifier = "==11.3.3.83" }, + { name = "nvidia-cufile-cu12", specifier = "==1.13.1.3" }, + { name = "nvidia-curand-cu12", specifier = "==10.3.9.90" }, + { name = "nvidia-cusolver-cu12", specifier = "==11.7.3.90" }, + { name = "nvidia-cusparse-cu12", specifier = "==12.5.8.93" }, + { name = "nvidia-cusparselt-cu12", specifier = "==0.7.1" }, + { name = "nvidia-nccl-cu12", specifier = "==2.27.5" }, + { name = "nvidia-nvjitlink-cu12", specifier = "==12.8.93" }, + { name = "nvidia-nvshmem-cu12", specifier = "==3.4.5" }, + { name = "nvidia-nvtx-cu12", specifier = "==12.8.90" }, + { name = "torch", specifier = "==2.10.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "torchcodec", specifier = "==0.10.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "torchvision", specifier = "==0.25.0+cu128", index = "https://download.pytorch.org/whl" }, +] +cu128-train = [ + { name = "flash-attn", marker = "platform_machine == 'x86_64'", specifier = "==2.7.4.post1+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "flash-attn-3-nv", marker = "platform_machine == 'x86_64'", specifier = "==1.0.3+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "natten", marker = "platform_machine == 'x86_64'", specifier = "==0.21.6.dev6+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "nvidia-cublas-cu12", specifier = "==12.8.4.1" }, + { name = "nvidia-cuda-cupti-cu12", specifier = "==12.8.90" }, + { name = "nvidia-cuda-nvrtc-cu12", specifier = "==12.8.93" }, + { name = "nvidia-cuda-runtime-cu12", specifier = "==12.8.90" }, + { name = "nvidia-cudnn-cu12", specifier = "==9.10.2.21" }, + { name = "nvidia-cufft-cu12", specifier = "==11.3.3.83" }, + { name = "nvidia-cufile-cu12", specifier = "==1.13.1.3" }, + { name = "nvidia-curand-cu12", specifier = "==10.3.9.90" }, + { name = "nvidia-cusolver-cu12", specifier = "==11.7.3.90" }, + { name = "nvidia-cusparse-cu12", specifier = "==12.5.8.93" }, + { name = "nvidia-cusparselt-cu12", specifier = "==0.7.1" }, + { name = "nvidia-nccl-cu12", specifier = "==2.27.5" }, + { name = "nvidia-nvjitlink-cu12", specifier = "==12.8.93" }, + { name = "nvidia-nvshmem-cu12", specifier = "==3.4.5" }, + { name = "nvidia-nvtx-cu12", specifier = "==12.8.90" }, + { name = "torch", specifier = "==2.10.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "torchao", marker = "platform_machine == 'x86_64'", specifier = "==0.16.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "torchcodec", specifier = "==0.10.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "torchvision", specifier = "==0.25.0+cu128", index = "https://download.pytorch.org/whl" }, + { name = "transformer-engine", specifier = "==2.12.0+cu128.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "triton", specifier = "==3.6.0", index = "https://download.pytorch.org/whl" }, +] +cu130 = [ + { name = "flash-attn", marker = "platform_machine == 'x86_64'", specifier = "==2.7.4.post1+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "flash-attn-3-nv", marker = "platform_machine == 'x86_64'", specifier = "==1.0.3+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "natten", specifier = "==0.21.6.dev6+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "nvidia-cublas", specifier = "==13.1.0.3" }, + { name = "nvidia-cuda-cupti", specifier = "==13.0.85" }, + { name = "nvidia-cuda-nvrtc", specifier = "==13.0.88" }, + { name = "nvidia-cuda-runtime", specifier = "==13.0.96" }, + { name = "nvidia-cudnn-cu13", specifier = "==9.15.1.9" }, + { name = "nvidia-cufft", specifier = "==12.0.0.61" }, + { name = "nvidia-cufile", specifier = "==1.15.1.6" }, + { name = "nvidia-curand", specifier = "==10.4.0.35" }, + { name = "nvidia-cusolver", specifier = "==12.0.4.66" }, + { name = "nvidia-cusparse", specifier = "==12.6.3.3" }, + { name = "nvidia-cusparselt-cu13", specifier = "==0.8.0" }, + { name = "nvidia-nccl-cu13", specifier = "==2.28.9" }, + { name = "nvidia-nvjitlink", specifier = "==13.0.88" }, + { name = "nvidia-nvshmem-cu13", specifier = "==3.4.5" }, + { name = "nvidia-nvtx", specifier = "==13.0.85" }, + { name = "torch", specifier = "==2.10.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "torchcodec", specifier = "==0.10.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "torchvision", specifier = "==0.25.0+cu130", index = "https://download.pytorch.org/whl" }, +] +cu130-train = [ + { name = "flash-attn", marker = "platform_machine == 'x86_64'", specifier = "==2.7.4.post1+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "flash-attn-3-nv", marker = "platform_machine == 'x86_64'", specifier = "==1.0.3+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "natten", specifier = "==0.21.6.dev6+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "nvidia-cublas", specifier = "==13.1.0.3" }, + { name = "nvidia-cuda-cupti", specifier = "==13.0.85" }, + { name = "nvidia-cuda-nvrtc", specifier = "==13.0.88" }, + { name = "nvidia-cuda-runtime", specifier = "==13.0.96" }, + { name = "nvidia-cudnn-cu13", specifier = "==9.15.1.9" }, + { name = "nvidia-cufft", specifier = "==12.0.0.61" }, + { name = "nvidia-cufile", specifier = "==1.15.1.6" }, + { name = "nvidia-curand", specifier = "==10.4.0.35" }, + { name = "nvidia-cusolver", specifier = "==12.0.4.66" }, + { name = "nvidia-cusparse", specifier = "==12.6.3.3" }, + { name = "nvidia-cusparselt-cu13", specifier = "==0.8.0" }, + { name = "nvidia-nccl-cu13", specifier = "==2.28.9" }, + { name = "nvidia-nvjitlink", specifier = "==13.0.88" }, + { name = "nvidia-nvshmem-cu13", specifier = "==3.4.5" }, + { name = "nvidia-nvtx", specifier = "==13.0.85" }, + { name = "torch", specifier = "==2.10.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "torchao", marker = "platform_machine == 'x86_64'", specifier = "==0.16.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "torchcodec", specifier = "==0.10.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "torchvision", specifier = "==0.25.0+cu130", index = "https://download.pytorch.org/whl" }, + { name = "transformer-engine", specifier = "==2.12.0+cu130.torch210", index = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" }, + { name = "triton", specifier = "==3.6.0", index = "https://download.pytorch.org/whl" }, +] +dev = [ + { name = "hatch" }, + { name = "pyinstrument" }, + { name = "pyrefly", specifier = "==0.55.0" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-custom-exit-code" }, + { name = "pytest-env" }, + { name = "pytest-instafail" }, + { name = "pytest-regressions" }, + { name = "pytest-xdist" }, + { name = "ruff", specifier = "==0.12.7" }, + { name = "uv", specifier = "==0.10.8" }, +] +libero = [ + { name = "bddl" }, + { name = "cloudpickle" }, + { name = "draccus" }, + { name = "easydict" }, + { name = "gym" }, + { name = "imageio", extras = ["ffmpeg"] }, + { name = "libero" }, + { name = "mujoco", specifier = "==3.3.2" }, +] +vllm = [ + { name = "torch", specifier = "==2.10.0", index = "https://download.pytorch.org/whl" }, + { name = "vllm", specifier = "==0.19.0" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d3/20d0402e4e983b66603117ad3dd3b864a05d7997a830206d3ff9cacef9a2/cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d0859c65775e8ebf2cbc084bfd51bd0ffda10266da6f9306451123b89f8e5a63", size = 3558999, upload-time = "2025-07-27T21:21:34.105Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a8/a6e2744288938ccd320a5c6f6f3653faa790f933f5edd088c6e5782a2354/cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d77b9b0aca02a3f6eeeff27fcd315ca5972616c0919ee38e522cce257bcd349", size = 1861558, upload-time = "2025-07-27T21:21:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/29/7961e09a849eea7d8302e7baa6f829dd3ef3faf199cb25ed29b318ae799b/cramjam-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66425bc25b5481359b12a6719b6e7c90ffe76d85d0691f1da7df304bfb8ce45c", size = 1699431, upload-time = "2025-07-27T21:21:38.396Z" }, + { url = "https://files.pythonhosted.org/packages/7a/60/6665e52f01a8919bf37c43dcf0e03b6dd3866f5c4e95440b357d508ee14e/cramjam-2.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd748d3407ec63e049b3aea1595e218814fccab329b7fb10bb51120a30e9fb7e", size = 2025262, upload-time = "2025-07-27T21:21:40.417Z" }, + { url = "https://files.pythonhosted.org/packages/d7/80/79bd84dbeb109e2c6efb74e661b7bd4c3ba393208ebcf69e2ae9454ae80c/cramjam-2.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6d9a23a35b3a105c42a8de60fc2e80281ae6e758f05a3baea0b68eb1ddcb679", size = 1766177, upload-time = "2025-07-27T21:21:42.224Z" }, + { url = "https://files.pythonhosted.org/packages/28/ef/b43280767ebcde022ba31f1e9902137655a956ae30e920d75630fa67e36e/cramjam-2.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40a75b95e05e38a2a055b2446f09994ce1139151721659315151d4ad6289bbff", size = 1854031, upload-time = "2025-07-27T21:21:43.651Z" }, + { url = "https://files.pythonhosted.org/packages/60/1c/79d522757c494dfd9e9b208b0604cc7e97b481483cc477144f5705a06ab7/cramjam-2.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5d042c376d2025300da37d65192d06a457918b63b31140f697f85fd8e310b29", size = 2035812, upload-time = "2025-07-27T21:21:45.473Z" }, + { url = "https://files.pythonhosted.org/packages/c8/70/3bf0670380069b3abd4c6b53f61d3148f4e08935569c08efbeaf7550e87d/cramjam-2.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb148b35ab20c75b19a06c27f05732e2a321adbd86fadc93f9466dbd7b1154a7", size = 2067661, upload-time = "2025-07-27T21:21:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/4f6ca98a4b474348e965a529b359184785d1119ab7c4c9ec1280b8bea50a/cramjam-2.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee47c220f0f5179ddc923ab91fc9e282c27b29fabc60c433dfe06f08084f798", size = 1981523, upload-time = "2025-07-27T21:21:49.704Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/b241511c7ffd5f1da29641429bb0e19b5fbcffafde5ba1bbcbf9394ea456/cramjam-2.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0cf1b5a81b21ea175c976c3ab09e00494258f4b49b7995efc86060cced3f0b2e", size = 2034251, upload-time = "2025-07-27T21:21:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4ef926c8c3c1bf6da96f9c53450ff334cdb6d0fc1efced0aea97e2090803/cramjam-2.11.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:360c00338ecf48921492455007f904be607fc7818de3d681acbcc542aae2fb36", size = 2155322, upload-time = "2025-07-27T21:21:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/be/fb/eb2aef7fb2730e56c5a2c9000817ee8fb4a95c92f19cc6e441afed42ec29/cramjam-2.11.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f31fcc0d30dc3f3e94ea6b4d8e1a855071757c6abf6a7b1e284050ab7d4c299c", size = 2169094, upload-time = "2025-07-27T21:21:55.187Z" }, + { url = "https://files.pythonhosted.org/packages/3b/80/925a5c668dcee1c6f61775067185c5dc9a63c766d5393e5c60d2af4217a7/cramjam-2.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:033be66fdceb3d63b2c99b257a98380c4ec22c9e4dca54a2bfec3718cd24e184", size = 2159089, upload-time = "2025-07-27T21:21:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ac/b2819640eef0592a6de7ca832c0d23c69bd1620f765ce88b60dbc8da9ba2/cramjam-2.11.0-cp310-cp310-win32.whl", hash = "sha256:1c6cea67f6000b81f6bd27d14c8a6f62d00336ca7252fd03ee16f6b70eb5c0d2", size = 1605046, upload-time = "2025-07-27T21:21:58.617Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/06af04727b9556721049e2127656d727306d275c518e3d97f9ed4cffd0d8/cramjam-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:98aa4a351b047b0f7f9e971585982065028adc2c162c5c23c5d5734c5ccc1077", size = 1710647, upload-time = "2025-07-27T21:22:00.279Z" }, + { url = "https://files.pythonhosted.org/packages/d0/89/8001f6a9b6b6e9fa69bec5319789083475d6f26d52aaea209d3ebf939284/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615", size = 3559272, upload-time = "2025-07-27T21:22:01.988Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/001d00070ca92e5fbe6aacc768e455568b0cde46b0eb944561a4ea132300/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b", size = 1861743, upload-time = "2025-07-27T21:22:03.754Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/041a3af01bf3f6158f120070f798546d4383b962b63c35cd91dcbf193e17/cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a", size = 1699631, upload-time = "2025-07-27T21:22:05.192Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/5358b238808abebd0c949c42635c3751204ca7cf82b29b984abe9f5e33c8/cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5", size = 2025603, upload-time = "2025-07-27T21:22:06.726Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/19dba7c03a27408d8d11b5a7a4a7908459cfd4e6f375b73264dc66517bf6/cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569", size = 1766283, upload-time = "2025-07-27T21:22:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/40e4b3408501d886d082db465c33971655fe82573c535428e52ab905f4d0/cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1", size = 1854407, upload-time = "2025-07-27T21:22:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/36/6e/c1b60ceb6d7ea6ff8b0bf197520aefe23f878bf2bfb0de65f2b0c2f82cd1/cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459", size = 2035793, upload-time = "2025-07-27T21:22:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ad/32a8d5f4b1e3717787945ec6d71bd1c6e6bccba4b7e903fc0d9d4e4b08c3/cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11", size = 2067499, upload-time = "2025-07-27T21:22:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cd/3b5a662736ea62ff7fa4c4a10a85e050bfdaad375cc53dc80427e8afe41c/cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f", size = 1981853, upload-time = "2025-07-27T21:22:15.908Z" }, + { url = "https://files.pythonhosted.org/packages/26/8e/1dbcfaaa7a702ee82ee683ec3a81656934dd7e04a7bc4ee854033686f98a/cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a", size = 2034514, upload-time = "2025-07-27T21:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/f11709bfdce74af79a88b410dcb76dedc97612166e759136931bf63cfd7b/cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca", size = 2155343, upload-time = "2025-07-27T21:22:18.854Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/3b98b61841a5376d9a9b8468ae58753a8e6cf22be9534a0fa5af4d8621cc/cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0", size = 2169367, upload-time = "2025-07-27T21:22:20.389Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/bd5db5c49dbebc8b002f1c4983101b28d2e7fc9419753db1c31ec22b03ef/cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23", size = 2159334, upload-time = "2025-07-27T21:22:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/203c57acdb6eea727e7078b2219984e64ed4ad043c996ed56321301ba167/cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac", size = 1605313, upload-time = "2025-07-27T21:22:24.126Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bd/102d6deb87a8524ac11cddcd31a7612b8f20bf9b473c3c645045e3b957c7/cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46", size = 1710991, upload-time = "2025-07-27T21:22:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, + { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/82e35ec3c5387f1864f46b3c24bce89a07af8bb3ef242ae47281db2c1848/cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:37bed927abc4a7ae2d2669baa3675e21904d8a038ed8e4313326ea7b3be62b2b", size = 3573104, upload-time = "2025-07-27T21:24:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4e/0c821918080a32ba1e52c040e12dd02dada67728f07305c5f778b808a807/cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50e4a58635fa8c6897d84847d6e065eb69f92811670fc5e9f2d9e3b6279a02b6", size = 1873441, upload-time = "2025-07-27T21:24:42.333Z" }, + { url = "https://files.pythonhosted.org/packages/a8/fd/848d077bf6abc4ce84273d8e3f3a70d61a2240519a339462f699d8acf829/cramjam-2.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d1ba626dd5f81f7f09bbf59f70b534e2b75e0d6582b056b7bd31b397f1c13e9", size = 1702589, upload-time = "2025-07-27T21:24:44.305Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1c/899818999bbdb59c601756b413e87d37fd65875d1315346c10e367bb3505/cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71e140d5eb3145d61d59d0be0bf72f07cc4cf4b32cb136b09f712a3b1040f5f", size = 1773646, upload-time = "2025-07-27T21:24:46.495Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/c2813c5422c43b3dcd8b6645bc359f08870737c44325ee4accc18f24eee0/cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ed7926a5cca28edebad7d0fedd2ad492710ae3524d25fc59a2b20546d9ce1", size = 1994179, upload-time = "2025-07-27T21:24:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4f/af984f8d7f963f0301812cdd620ddcfd8276461ed7a786c0f89e82b14739/cramjam-2.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5eb4ed3cea945b164b0513fd491884993acac2153a27b93a84019c522e8eda82", size = 1714790, upload-time = "2025-07-27T21:24:51.045Z" }, + { url = "https://files.pythonhosted.org/packages/81/da/b3301962ccd6fce9fefa1ecd8ea479edaeaa38fadb1f34d5391d2587216a/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f", size = 3573546, upload-time = "2025-07-27T21:24:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/410ddb8ad4b9dfb129284666293cb6559479645da560f7077dc19d6bee9e/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54", size = 1873654, upload-time = "2025-07-27T21:24:54.958Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/f68a443c64f7ce7aff5bed369b0aa5b2fac668fa3dfd441837e316e97a1f/cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301", size = 1702846, upload-time = "2025-07-27T21:24:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/02/0ff358ab773def1ee3383587906c453d289953171e9c92db84fdd01bf172/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c", size = 1773683, upload-time = "2025-07-27T21:24:59.28Z" }, + { url = "https://files.pythonhosted.org/packages/e9/31/3298e15f87c9cf2aabdbdd90b153d8644cf989cb42a45d68a1b71e1f7aaf/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d", size = 1994136, upload-time = "2025-07-27T21:25:01.565Z" }, + { url = "https://files.pythonhosted.org/packages/c7/90/20d1747255f1ee69a412e319da51ea594c18cca195e7a4d4c713f045eff5/cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23", size = 1714982, upload-time = "2025-07-27T21:25:05.79Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/31/bfcc870f69c6a017c4ad5c42316207fc7551940db6f3639aa4466ec5faf3/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a022c96b8bd847e8dc0675523431149a4c3e872f440e3002213dbb9e08f0331a", size = 11800959, upload-time = "2025-10-21T14:51:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1e/9c8ed3f3dbed7b7d038805fdc65cbc65fda9983e84437778a9571e7092bc/cuda_bindings-12.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:f69107389e6b9948969bfd0a20c4f571fd1aefcfb1d2e1b72cc8ba5ecb7918ab", size = 11464568, upload-time = "2025-10-21T14:51:31.454Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2b/ebcbb60aa6dba830474cd360c42e10282f7a343c0a1f58d24fbd3b7c2d77/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306", size = 11840604, upload-time = "2025-10-21T14:51:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/dd/be/90d32049e06abcfba4b2e7df1dbcb5e16215c8852eef0cd8b25f38a66bd4/cuda_bindings-12.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913", size = 11490933, upload-time = "2025-10-21T14:51:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/ec/07/6aff13bc1e977e35aaa6b22f52b172e2890c608c6db22438cf7ed2bf43a6/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d", size = 11566797, upload-time = "2025-10-21T14:51:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3c/972edfddb4ae8a9fccd3c3766ed47453b6f805b6026b32f10209dd4b8ad4/cuda_bindings-12.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c", size = 11894363, upload-time = "2025-10-21T14:51:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b5/96a6696e20c4ffd2b327f54c7d0fde2259bdb998d045c25d5dedbbe30290/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5", size = 11624530, upload-time = "2025-10-21T14:52:01.539Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/e6/87/652796522cc1a7af559460e1ce59b642e05c1468b9c08522a9a096b4cf04/cuda_bindings-12.9.4-cp314-cp314-win_amd64.whl", hash = "sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575", size = 11517716, upload-time = "2025-10-21T14:52:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/39/73/d2fc40c043bac699c3880bf88d3cebe9d88410cd043795382826c93a89f0/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f", size = 11565056, upload-time = "2025-10-21T14:52:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { url = "https://files.pythonhosted.org/packages/ab/52/a30f46e822bfa6b4a659d1e8de8c4a4adf908ea075dac568b55362541bd8/cuda_bindings-12.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43", size = 12055608, upload-time = "2025-10-21T14:52:12.335Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-vllm' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/98/0666ee759cd2e5306f911cbc95d2c6c814326906ed6b9c09e817a4b4a7c8/cuda_bindings-13.0.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56e46a9e984bb754e56b9d060cf027fe99f08a97651ce6d8aa1c2032476d01e", size = 11762523, upload-time = "2025-10-21T15:08:45.913Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/2b2a43c8a6f8d8ff7a5ec7de1357ba3f1438ea69281c9deb90df29d55d56/cuda_bindings-13.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f797ce534a303525259be0ae7ee9cfcf4f7874b22f1f9b8e85555509dccb83", size = 12136098, upload-time = "2025-10-21T15:08:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/47/67/5de1d48189511114859a1a131193896f88271c067a64b1159787e2d9f89b/cuda_bindings-13.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:74307cea1feee6c32a6e27b42e77beb22cd21cff4b7764fd214fa6ff89f8bd69", size = 11106982, upload-time = "2025-10-21T15:08:50.433Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9e171ee6359d4aabf2d8202802c85487cae6c2eb52b9352bb7754583802f/cuda_bindings-13.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfd66c25a133365c4f93e3396c38c64b04400ccafd18c3a889ae251a1bfabaa1", size = 11807212, upload-time = "2025-10-21T15:08:52.988Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/d7036d9e402e6b5b57877a7496aba3bf2a0090f4fa3f072743fce3373eba/cuda_bindings-13.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9afede5937474864aa794eb57399dbdf5d2b05427aadcac275209b0857528a61", size = 12198791, upload-time = "2025-10-21T15:08:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/83/25/620ce2afb6ea6d5da89d98375c85641f691924eef574247f7f0dd99f8bee/cuda_bindings-13.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:fce6d6b162457475b35e1a259ab643e683d1d20a84459fea898782e2f1e10a3b", size = 11138783, upload-time = "2025-10-21T15:08:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/61/3c/c33fd3aa5fcc89aa1c135e477a0561f29142ab5fe028ca425fc87f7f0a74/cuda_bindings-13.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b899e5a513c11eaa18648f9bf5265d8de2a93f76ef66a6bfca0a2887303965cd", size = 11709086, upload-time = "2025-10-21T15:09:00.005Z" }, + { url = "https://files.pythonhosted.org/packages/21/ac/6b34452a3836c9fbabcd360689a353409d15f500dd9d9ced7f837549e383/cuda_bindings-13.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf41d9e69019939aa15296fa66ea7d3fdb8d2c6383f729f4b1156c8b37808a06", size = 12128303, upload-time = "2025-10-21T15:09:02.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/ad9cc2f0496886c37aefbc00256197a6043a3f04bbe959481e6908310afe/cuda_bindings-13.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:6b12ccd98f447aea9589d32caf9efda0c193994080752a60f790b646d519fe8c", size = 11237397, upload-time = "2025-10-21T15:09:05.421Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/41ccc303eb6be8ae82c5edd2ccae938876e8a794660e8bb96a193174a978/cuda_bindings-13.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb16a7f769c9c67469add7a1d9f6c14dd44637f6921cb6b9eb82cb5015b35c3d", size = 11537064, upload-time = "2025-10-21T15:09:07.84Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ac/699889100536f1b63779646291e74eefa818087a0974eb271314d850f5dc/cuda_bindings-13.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:512d0d803a5e47a8a42d5a34ce0932802bf72fe952fdb11ac798715a35c6e5cb", size = 11910447, upload-time = "2025-10-21T15:09:09.942Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/a2f5910aaf21f4cd43f456ea80f47f1424eece5b8f063dac1980304b8ef0/cuda_bindings-13.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:dd83e8d79587e265b82d3e589ba6b061770537443dfb1bb4a74f755c8b13f62b", size = 11211659, upload-time = "2025-10-21T15:09:12.639Z" }, + { url = "https://files.pythonhosted.org/packages/11/67/9656e003f18c5b32e1a2496998b24f4355ec978c5f3639b0eb9f6d0ff83f/cuda_bindings-13.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c859e326c776a47e66c50386a10c84fe34291eb6e711610c9fd7cc27d446334f", size = 11522409, upload-time = "2025-10-21T15:09:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/a83379caa7c1bed4195e704c24467a6c07fe8e29c7055ccd4f00c5702363/cuda_bindings-13.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e675dbd009fb5e66d63fd13a8ff35f849120f01bcc4dafadbced3004605c3588", size = 11903148, upload-time = "2025-10-21T15:09:16.918Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e0/ff1eeda06364df8c750843432ac6efb33a06df38261f0a1ceee59bb7dac2/cuda_bindings-13.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:193762306b6032c00a141fc38bcef92c6fb4d332fd2d6a550c7f950e7fd8acd8", size = 11543153, upload-time = "2025-10-21T15:09:19.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/99/0042dc5e98e3364480b1aaabc0f5c150d037825b264bba35ac7a883e46ee/cuda_bindings-13.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c7e6e89cdfc9b34f16a065cc6ad6c4bab19ce5dcef8da3ace8ad10bda899fa0", size = 11594384, upload-time = "2025-10-21T15:09:21.938Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c4/a931a90ce763bd7d587e18e73e4ce246b8547c78247c4f50ee24efc0e984/cuda_bindings-13.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e93866465e7ff4b7ebdf711cf9cd680499cd875f992058c68be08d4775ac233d", size = 11920899, upload-time = "2025-10-21T15:09:26.306Z" }, + { url = "https://files.pythonhosted.org/packages/14/3e/5725b2e5b9ac22bf19a50ec5f7611301ab6111c98ccf1b6b125fdaa71550/cuda_bindings-13.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bd2364bc49925837ce18dda259c3a36e539977ca0297799a54891cae1d5213f5", size = 11160621, upload-time = "2025-10-21T15:09:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/ec611e27ba48a9056f3b0610c5e27727e539f3905356cfe07acea18e772c/cuda_bindings-13.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed06ef3507bd0aefb0da367e3d15676a8c7443bd68a88f298562d60b41078c20", size = 11521928, upload-time = "2025-10-21T15:09:30.714Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2e/02cebf281ef5201b6bb9ea193b1a4d26e6233c46571cfb04c4a7dede12b9/cuda_bindings-13.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ab845487ca2c14accdcb393a559a3070469ea4b591d05e6ef439471f47f3e24", size = 11902749, upload-time = "2025-10-21T15:09:32.688Z" }, + { url = "https://files.pythonhosted.org/packages/36/d2/088c28751f54df7a251259ef3f99d34c428e12653f15db02fd62a96247af/cuda_bindings-13.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:aaa0934e16aa20ec10fbb1ecc53a6961b8d1c06a970fe05cc6ee7d2a805a090f", size = 11697137, upload-time = "2025-10-21T15:09:35.232Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/60/d8f1dbfb7f06b94c662e98c95189e6f39b817da638bc8fcea0d003f89e5d/cuda_pathfinder-1.4.0-py3-none-any.whl", hash = "sha256:437079ca59e7b61ae439ecc501d69ed87b3accc34d58153ef1e54815e2c2e118", size = 38406, upload-time = "2026-02-25T22:13:00.807Z" }, +] + +[[package]] +name = "cuda-python" +version = "13.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", version = "13.0.3", source = { registry = "https://pypi.org/simple" } }, + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5f/beaa12a11b051027eec0b041df01c6690db4f02e3b2e8fadd5a0eeb4df52/cuda_python-13.0.3-py3-none-any.whl", hash = "sha256:914cd7e2dd075bd06a2d5121c1d9ccdd3d0c94b03ea5a44dbd98d24d8ed93bab", size = 7605, upload-time = "2025-10-21T15:48:59.222Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "datasets" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/94/eb81c6fe32e9b6ef92223141b5a553aeff2e9456968424a8533cbe88f476/datasets-4.6.1.tar.gz", hash = "sha256:140ce500bc41939ff6ce995702d66b1f4b2ee7f117bb9b07512fab6804d4070a", size = 593865, upload-time = "2026-02-27T23:26:49.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/f0/99fe6eb530c7ee9ee1faee48059eb8a6437f80c893a496b98a78864e0fc6/datasets-4.6.1-py3-none-any.whl", hash = "sha256:f53228e6dadc9f837037b1bf3051d7d8c054abbb3eb29f1f022926e08090e0da", size = 520667, upload-time = "2026-02-27T23:26:46.855Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "deepdiff" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderly-set" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "depyf" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astor" }, + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/65/4df6936130b56e1429114e663e7c1576cf845f3aef1b2dd200c0a5d19dba/depyf-0.20.0-py3-none-any.whl", hash = "sha256:d31effad4261cebecb58955d832e448ace88f432328f95f82fd99c30fd9308d4", size = 39381, upload-time = "2025-10-13T12:33:33.647Z" }, +] + +[[package]] +name = "diffusers" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "importlib-metadata" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/3b/01d0ff800b811c5ad8bba682f4c6abf1d7071cd81464c01724333fefb7ba/diffusers-0.37.0.tar.gz", hash = "sha256:408789af73898585f525afd07ca72b3955affea4216a669558e9f59b5b1fe704", size = 4141136, upload-time = "2026-03-05T14:58:39.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/55/586a3a2b9c95f371c9c3cb048c3cac15aedcce8d6d53ebd6bbc46860722d/diffusers-0.37.0-py3-none-any.whl", hash = "sha256:7eab74bf896974250b5e1027cae813aba1004f02d97c9b44891b83713386aa08", size = 5000449, upload-time = "2026-03-05T14:58:37.361Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dists-pytorch" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/78/f76cbe06c88076f6d6d5ef16a7591a7e3b7eb225e39b035e16d95f0fb89c/DISTS_pytorch-0.1-py3-none-any.whl", hash = "sha256:73e54ce13e7cf3ca962ee8e433d129884ec3dc75e00a27af90525e1da760ac61", size = 20501, upload-time = "2020-04-17T00:40:40.359Z" }, +] + +[[package]] +name = "dm-tree" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "attrs" }, + { name = "numpy" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/83/ce29720ccf934c6cfa9b9c95ebbe96558386e66886626066632b5e44afed/dm_tree-0.1.9.tar.gz", hash = "sha256:a4c7db3d3935a5a2d5e4b383fc26c6b0cd6f78c6d4605d3e7b518800ecd5342b", size = 35623, upload-time = "2025-01-30T20:45:37.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/88f685534d87072a5174fe229e77aab6b7da50092d5151ebc172f6270b5c/dm_tree-0.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5d5b28ee2e461b6af65330c143806a6d0945dcabbb8d22d2ba863e6dabd9254e", size = 173568, upload-time = "2025-03-31T08:35:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6a/64924e102f559c1380263a28a751f20a1bdd18e85ea599e216feead84adf/dm_tree-0.1.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d5616015412311df154908069fcf2c2d8786f6088a2ae3554d186cdf2b1e15", size = 146935, upload-time = "2025-01-30T20:45:16.505Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/ba0f7274164eb6bd06a36c2f8cb21b0debc32fd9ba8e73a7c9e50c90041b/dm_tree-0.1.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831699d2c60a1b38776a193b7143ae0acad0a687d87654e6d3342584166816bc", size = 152892, upload-time = "2025-01-30T20:45:18.021Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/8b96a34a15c5c4d1d6af44795963fa44381716975aabac83beab4fe80974/dm_tree-0.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:1ae3cbff592bb3f2e197f5a8030de4a94e292e6cdd85adeea0b971d07a1b85f2", size = 101469, upload-time = "2025-01-30T20:45:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/2d2de9f8901ccc5b6f34aea678e732816853015b9d756c86efcec189bf4b/dm_tree-0.1.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7d7d784afaeb4b67d87d858261aaf02503939ddc1f09c4cca70728f9892ab004", size = 173561, upload-time = "2025-03-31T08:35:40.042Z" }, + { url = "https://files.pythonhosted.org/packages/3e/07/57459f32cf5683c25b596ab58f42a3305f91876c2f03d2fa6e9d0df75fcb/dm_tree-0.1.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e660d1779ddcbd1348410d08f67db4870d413a3ec4ba8b4b045bd5ce4bd8f35c", size = 146926, upload-time = "2025-01-30T20:45:20.622Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/939fbf81177c7cb3b1e5ddebd696237b3be9520769cce882f064de497103/dm_tree-0.1.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294dc1cecf87552a45cdd5ddb215e7f5295a5a47c46f1f0a0463c3dd02a527d7", size = 152851, upload-time = "2025-01-30T20:45:23.032Z" }, + { url = "https://files.pythonhosted.org/packages/35/3e/a46933e0157b0ac87619a754ce1a796b2afc6386fca7c11f95c010f40745/dm_tree-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:12f4cc6cd52a39aa38ff31577b6d79b6136a9a89273a876bf62335c9f65c27bf", size = 101522, upload-time = "2025-01-30T20:45:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/ee/02/61aa90ab695918b4389d75c99bf0ec3cd0abacf1cadbef4053626f23ce34/dm_tree-0.1.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a8d20eeab7fde77a3ed71f07716021eb0edfb4812a128eb381d108af3a310257", size = 175012, upload-time = "2025-03-31T08:35:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/120cd40556407879c1069941bd8b0d1a75754128c1a5bf0e27dbcf2a49fc/dm_tree-0.1.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c43417814b1181d3367b335460bfdd30b79ee187a64220e11f6ddd093a4b15", size = 147204, upload-time = "2025-01-30T20:45:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/86/52/27607a275c12858b979b8e943d2bd3bd0f9028503bb7079d5830a8b3cac0/dm_tree-0.1.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2334cfe9d2ed4293f9f1c7aefba0657deaab9ea74b5fadd966f6d01d9b6b42d9", size = 153013, upload-time = "2025-01-30T20:45:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/ea/97/4f78412f73a9350bc8f934441bae5b68b102c8f4240a7f06b4114b51d6de/dm_tree-0.1.9-cp312-cp312-win_amd64.whl", hash = "sha256:9020a5ce256fcc83aa4bc190cc96dd66e87685db0a6e501b0c06aa492c2e38fc", size = 102022, upload-time = "2025-01-30T20:45:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/823788cd0f7964cadcfa56d1e0f9e5e987ee73b5db6273bc00168f524f1a/dm_tree-0.1.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cfa33c2e028155810ad1b4e11928707bf47489516763a86e79cab2954d23bf68", size = 175000, upload-time = "2025-03-31T08:35:42.483Z" }, + { url = "https://files.pythonhosted.org/packages/37/6a/512abdf7f20acc6cd6fce77f7663014d129aa313b5953aa2603d58fdb0c9/dm_tree-0.1.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05622d074353cf434049206e53c12147903a048c4bd7d77f2800d427413ad78", size = 147210, upload-time = "2025-01-30T20:45:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0a/f4d72ffb64ab3edc1fa66261f81ee3b4142ab14cd8aa1dfc7bbeca5ee4ba/dm_tree-0.1.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68b0efad76703dd4648586c75618a48cdd671b68c3266fe980e323c15423607", size = 153043, upload-time = "2025-01-30T20:45:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ee/529ce999770b4d621a64af86c60cfee52f0cdd7294752105179ebf1c07c6/dm_tree-0.1.9-cp313-cp313-win_amd64.whl", hash = "sha256:e97c34fcb44941c36b7ee81dcdbceba0fbe728bddcc77e5837ab2eb665bcbff8", size = 102043, upload-time = "2025-01-30T20:45:32.004Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3c/5b40f8862390e9172e776cf610f3791c1af01f140a5698799fbe4a97206f/dm_tree-0.1.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b06e7a5da1c31a82521a60060573527e8d24b9920fdd20b2ec86f08412737598", size = 180821, upload-time = "2025-03-31T08:35:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/84/1d/3cdbeeb3f6937a47a26cee502bffeccc2e55b97dfcce8a1d1135ea1b5b47/dm_tree-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6893fcdc5cf1a4f459cfc383526d35d42e7c671ae565d7e429a2f2cb2cb93e89", size = 147282, upload-time = "2025-01-30T20:45:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/c5/37/15603079854394f16e3833a7b50696c1f3cbf30a2243a119f64f18a16f36/dm_tree-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1f5d1e96b3a7de22b25b13a5eb30f41f8cf9c02dd4479a24920de99e780903c", size = 153052, upload-time = "2025-01-30T20:45:35.907Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "donfig" +version = "0.8.1.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, +] + +[[package]] +name = "draccus" +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "pyyaml" }, + { name = "pyyaml-include" }, + { name = "toml" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/68/9a054c0d242a948c41b7ae9416a4148bf562df7d244c7a39a4b67e7ff742/draccus-0.11.5.tar.gz", hash = "sha256:b82e2c2027030ae1a0f105515125f914e050c0766ab518df3a0560a07d10ddc9", size = 67384, upload-time = "2025-04-22T20:41:48.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/52/3144a4a8341f125d357a8f6e07aeb23b0bde7133dabbd0c78b984c079a1f/draccus-0.11.5-py3-none-any.whl", hash = "sha256:67da70bfbdea8fe67667322c64068d6d8cd832694a418b6c71d9d3b9f5c3c005", size = 78447, upload-time = "2025-04-22T20:41:47.414Z" }, +] + +[[package]] +name = "easydict" +version = "1.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/9f/d18d6b5e19244788a6d09c14a8406376b4f4bfcc008e6d17a4f4c15362e8/easydict-1.13.tar.gz", hash = "sha256:b1135dedbc41c8010e2bc1f77ec9744c7faa42bce1a1c87416791449d6c87780", size = 6809, upload-time = "2024-03-04T12:04:41.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/ec/fa6963f1198172c2b75c9ab6ecefb3045991f92f75f5eb41b6621b198123/easydict-1.13-py3-none-any.whl", hash = "sha256:6b787daf4dcaf6377b4ad9403a5cee5a86adbc0ca9a5bcf5410e9902002aeac2", size = 6804, upload-time = "2024-03-04T12:04:39.508Z" }, +] + +[[package]] +name = "egl-probe" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c3/c5fea892e18631721a7d7e492a790bde8e3dc930861866bedb38762d0d7c/egl_probe-1.0.2.tar.gz", hash = "sha256:29bdca7b08da1e060cfb42cd46af8300a7ac4f3b1b2eeb16e545ea16d9a5ac93", size = 217495, upload-time = "2021-07-16T08:50:18.481Z" } + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "einx" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozendict" }, + { name = "numpy" }, + { name = "sympy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/cb/a1f879a5aafee02e085f5c08b43e94714e0c84a5ce1cbf43f6e55bd30105/einx-0.4.1.tar.gz", hash = "sha256:290248d4b1e979175cd5421e1144e66eaa58b992dde6f4a788429271184fc973", size = 113997, upload-time = "2026-03-05T10:44:37.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/18/21309290f76e435e3e438a89ca7c0e80bb1421785faf9c76fa8c49aea178/einx-0.4.1-py3-none-any.whl", hash = "sha256:0ee5ceea61586e04837208ecc62b255f422e242129390c7b405d77a442992324", size = 139538, upload-time = "2026-03-05T10:44:35.775Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "etils" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/a0/522bbff0f3cdd37968f90dd7f26c7aa801ed87f5ba335f156de7f2b88a48/etils-1.13.0.tar.gz", hash = "sha256:a5b60c71f95bcd2d43d4e9fb3dc3879120c1f60472bb5ce19f7a860b1d44f607", size = 106368, upload-time = "2025-07-15T10:29:10.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl", hash = "sha256:d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb", size = 170603, upload-time = "2025-07-15T10:29:09.076Z" }, +] + +[package.optional-dependencies] +epath = [ + { name = "fsspec", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "importlib-resources", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "zipp", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "etils" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +sdist = { url = "https://files.pythonhosted.org/packages/26/ce/6e067242fde898841922ac6fc82b0bb2fe35c38e995880bdffdfbe30182a/etils-1.14.0.tar.gz", hash = "sha256:8136e7f4c4173cd0af0ca5481c4475152f0b8686192951eefa60ee8711e1ede4", size = 108127, upload-time = "2026-03-04T17:41:36.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/3d/589663aeeacd59bb2f3e8596bfd3e81cf0fb18d70bb433199041f469771b/etils-1.14.0-py3-none-any.whl", hash = "sha256:b5df7341f54dbe1405a4450b2741207b4a8c279780402b45f87202b94dfc52b4", size = 172934, upload-time = "2026-03-04T17:41:35.01Z" }, +] + +[package.optional-dependencies] +epath = [ + { name = "fsspec", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "zipp", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "evdev" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723, upload-time = "2026-02-05T21:54:24.987Z" } + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "farama-notifications" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/91/14397890dde30adc4bee6462158933806207bc5dd10d7b4d09d5c33845cf/farama_notifications-0.0.6.tar.gz", hash = "sha256:b19acac4bb41d76e59e03394b5dd165f4761c86fa327f56307a35cbee3b60158", size = 2517, upload-time = "2026-04-24T08:43:57.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f0/21f81892e4ed10f4ec3ef2e7cf8635fb76e7c0907c55d0da66be50094760/farama_notifications-0.0.6-py3-none-any.whl", hash = "sha256:f84839188efa1ce5bb361c2a84881b2dc2c0d0d7fb661ff00421820170930935", size = 2897, upload-time = "2026-04-24T08:43:56.785Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f2/fcd66ce245b7e3c3d84ca8717eda8896945fbc17c87a9b03f490ff06ace7/fastapi_cloud_cli-0.15.1.tar.gz", hash = "sha256:71a46f8a1d9fea295544113d6b79f620dc5768b24012887887306d151165745d", size = 43851, upload-time = "2026-03-26T10:23:12.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/11/ecb0d5e1d114e8aaec1cdc8ee2d7b0f54292585067effe2756bde7e7a4b0/fastapi_cloud_cli-0.15.1-py3-none-any.whl", hash = "sha256:b1e8b3b26dc314e180fc0ab67dfd39d7d9fe160d3951081d09184eafaacf5649", size = 32284, upload-time = "2026-03-26T10:23:14.151Z" }, +] + +[[package]] +name = "fastar" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19223fea7104631bea254751026e75bf99f2b6d0d1568/fastar-0.9.0.tar.gz", hash = "sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410", size = 70124, upload-time = "2026-03-20T14:26:34.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/48/3d8e24c9ae7796e59231f50133640463c6a20b00ce684b308dc6de0e28fe/fastar-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:19a384395f26daa3ecb6c24054f3a50ce919e250e06b82614a252a0fadcbca17", size = 709092, upload-time = "2026-03-20T14:25:30.007Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/4d7dc06f3ad5457b9a1510a75e3f9ec431ad020688fcf954012a2bcae6e8/fastar-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9c82b1fef26d8fd4abad1152f4c74eeb86bc9d46c814757b695847a751b9b0b", size = 630252, upload-time = "2026-03-20T14:25:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/ebb285a263cc2070d04d39917288b5d1c7f49e1c47ed5544e86283e091c6/fastar-0.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e5c91cb4527a6e634e7477a01aa52ccfbb978df1d9803172685c1e0802a2c18c", size = 869584, upload-time = "2026-03-20T14:24:52.067Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/a293b6f75ea1b9e14d384859253ee65f966a73be306cea39552a557c9e34/fastar-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6bc32f40a3e8ab12b8ebce48c4808d2bcf89bd3dac3023980b8a9b4aaf719f2", size = 762379, upload-time = "2026-03-20T14:23:47.429Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/a31f00c31f16a3bffd6f6ab3414964100fb35a79983f21283fc8b81d3cec/fastar-0.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee4a1d85a58cd955a5409b221450762b851879ce6e080d6d717265fb9a4e939d", size = 759567, upload-time = "2026-03-20T14:24:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/b0/46/5a4b1fb1e5c8b6cd1eb464e658ed75d667f1f53834f353e6323ca71bd113/fastar-0.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72e25ec1cbad0fc2a5f93a147978cc41e054ce5789807ebd3bcece5f276c0c2", size = 925850, upload-time = "2026-03-20T14:24:13.669Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/a5543fb1b059a82ce4c6fc571fe429390294e8150c09bb537d228471eac6/fastar-0.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862ddfaf73c7388d708bcbeb75e2e336605465b88d952407621c847bab5d3cb", size = 818858, upload-time = "2026-03-20T14:24:39.431Z" }, + { url = "https://files.pythonhosted.org/packages/53/9a/af5ae6d24e1170702d096225989b4ee3470b22bbecb5c09c899e816aefd7/fastar-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3471fa2627b9703830d13c8b0a6ba19eeff4e2e0ff924631065ecceca56abb2b", size = 821941, upload-time = "2026-03-20T14:25:05.534Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/399d8b080f7c5fe1fa88dadaa7a30bd0bb885ad490d3c2ef2c667877c5c4/fastar-0.9.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:e3d2e68e0239ab24b65b0674f2b74ac71d8fb5ea221a3e0d0ab966292bd83e12", size = 886548, upload-time = "2026-03-20T14:24:26.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/cd/034b5f61e99df67e092e1d3d538150a5f562d00c0259e6402cbcb62e15a9/fastar-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dcabfe31c48ff6a994c3dc4ddc27287b15d78a09c737beef8a6b1f210b720a6a", size = 970244, upload-time = "2026-03-20T14:25:42.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8f/3a8b0d711050b300a3448c9d145c6d234958e148e456ab4a15daca6e4b05/fastar-0.9.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f1723bb9cc3dcd087b5dd066a0369f27529a925d467ccc896d1f6cd0212417bf", size = 1036944, upload-time = "2026-03-20T14:25:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/34/11/cd5ebd16529c5fbff2431b494bd6f3f8ecafeca8f874449bf65ccf58c77b/fastar-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3ec2e699af02ba78f359b1cf1f4b3da22f41dec3a327f1cda6a1d31a43365a71", size = 1078612, upload-time = "2026-03-20T14:26:09.042Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/752c184e3c5e8de592e5d7ce3d081bf665ae5dbbe4a3df816daf38043143/fastar-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:761708eb2f6e402d4cda04ac81d0c2406b1c10375601e238083d2e885ec52a42", size = 1029368, upload-time = "2026-03-20T14:26:21.79Z" }, + { url = "https://files.pythonhosted.org/packages/68/b3/2a5551942adaecb9874ebc0d0922f3ab9dd058298b7a36a7900da93a3e68/fastar-0.9.0-cp310-cp310-win32.whl", hash = "sha256:a5ea0969c94845faed7bf681850df704da9617ad7231850dbc7ca4017080133a", size = 454507, upload-time = "2026-03-20T14:26:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/23/30/7a2f25837ee7353ff5eaa815d9a6321f8704fcc39a94570a1b2d958639c0/fastar-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5646f10a747282904f2def929612ed19cace4bd702029c3d7c78205ef604abd", size = 486500, upload-time = "2026-03-20T14:26:42.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/01/4ecbe0b4938608f9c6c5c4d4f6b872975fe30152bfaa8e44fe0e3b6cbcc4/fastar-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:facc7522bd1c1e7569bedb602932fc7292408a320f415d72180634d58f661bf0", size = 708809, upload-time = "2026-03-20T14:25:31.299Z" }, + { url = "https://files.pythonhosted.org/packages/11/6a/085b3cae0e04da4d42306dc07e2cc4f95d9c8f27df4dfd1a25d0f80516cb/fastar-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8ac3e8aaee57dfc822b04f570f0a963c2381a9dc8990fe0c6e965efd23fd451", size = 629764, upload-time = "2026-03-20T14:25:19.017Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c2/cdd996a37837e6cc5edc4d09775d2a2bc63e9e931129db69947cf4c77148/fastar-0.9.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d90493b4bb56db728b38eb18a551df386113d72ad4e7f1a97572f3662a9b8a85", size = 869631, upload-time = "2026-03-20T14:24:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/d4/4a5a3c341d26197ea3ae6bed79fc9bb4ead8ddc74a93bdb74e4ee0bac18e/fastar-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e2c3b46408193ea13c1e1177275ca7951e88bd3dce16baccb8de4f5e0dc2e8", size = 762096, upload-time = "2026-03-20T14:23:49.175Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/1d346cdfcd3064f6c435eff90a8d7cf0021487e3681453bdd681b9488d81/fastar-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:52f96a3d4cfbe4f06b376706fa0562f3a1d2329bc37168119af0e47e1ac21cab", size = 759627, upload-time = "2026-03-20T14:24:01.984Z" }, + { url = "https://files.pythonhosted.org/packages/02/a1/e91eb7ae1e41c0d3ead86dc199beb13a0b80101e2948d66adeb578b09e60/fastar-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57e9b94e485713c79bb259f7ecff1213527d05e9aa43a157c3fbc88812cf163e", size = 926211, upload-time = "2026-03-20T14:24:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/9fea9604e7aecc2f062f0df5729f74712d81615a1b18fa6a1a13106184fa/fastar-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb06d0a0cc3cf52a9c07559bb16ab99eb75afe0b3d5ce68f5c299569460851ac", size = 818748, upload-time = "2026-03-20T14:24:40.765Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f8/521438041d69873bb68b144b09080ae4f1621cebb8238b1e54821057206b/fastar-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c75e779f72d845037d4bf6692d01ac66f014eaef965c9231d41d5cc1276b89fc", size = 822380, upload-time = "2026-03-20T14:25:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/f33cc3f5f96ffb7d81a7f06c9239d4eea584527292a030a73d3218148f41/fastar-0.9.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:24b13fc4ef3f1e3c9cc2dcf07ad9445900db9d3ce09b73021547a55994d0407f", size = 886569, upload-time = "2026-03-20T14:24:27.567Z" }, + { url = "https://files.pythonhosted.org/packages/60/32/6e7cb45dce544f97b0199325084a0a5a895cb903e0539690619e78d8d7cf/fastar-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec7852de506d022ad36ad56f4aefb10c259dd59e485bf87af827954d404ba9d5", size = 969993, upload-time = "2026-03-20T14:25:44.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/04cf9374e5e6a82ddc87073d684c1fa7a9ca368bf85c2786535b1bfc38a9/fastar-0.9.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a79c53c3003958dca88a7ec3dd805bf9c2fb2a659110039f44571d57e329e3d4", size = 1036738, upload-time = "2026-03-20T14:25:57.551Z" }, + { url = "https://files.pythonhosted.org/packages/b6/94/e6f6ad29c25c5f531a406e3a35ef5c034ea177748f9fb621073519adb3d5/fastar-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00328ce7ae76be7f9e2faa6a221a0b41212e4115c27e2ac5e585bcf226bfc2eb", size = 1078557, upload-time = "2026-03-20T14:26:10.358Z" }, + { url = "https://files.pythonhosted.org/packages/1f/44/a1c9f6afe93d1cc1abb68a7cda2bada509d756d24e22d5d949ca86b4f45e/fastar-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c03fad1ad9ac57cf03a4db9e18c7109c37416ff4eb9ebfca98fcd2b233a26c4", size = 1029251, upload-time = "2026-03-20T14:26:23.215Z" }, + { url = "https://files.pythonhosted.org/packages/75/31/9e77bc2af3c8b8a433b7175d14b9c75d0ab901542c7452fdf942ece5a155/fastar-0.9.0-cp311-cp311-win32.whl", hash = "sha256:163ba4c543d2112c8186be2f134d11456b593071ba9ea3faba4f155bde7c5dac", size = 454633, upload-time = "2026-03-20T14:26:55.344Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/a78d51d1290cdce2d6d3162a18d12c736b71d3feef5a446b3fe021443eb3/fastar-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:2137d5d26044b44bb19197a8fc959256c772615ee959cddd0f74320b548fc966", size = 486772, upload-time = "2026-03-20T14:26:43.569Z" }, + { url = "https://files.pythonhosted.org/packages/fa/39/471aefca4c8180689cc0dc6f2f23bc283a3ca07114f713307fb947d320af/fastar-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:ecb94de3bc96d9fae95641a7907385541517a4c17416153d3b952d37dce0a2a3", size = 463586, upload-time = "2026-03-20T14:26:35.483Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9b/300bc0dafa8495718976076db216f42d57b251a582589566a63b4ed2cb82/fastar-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7a8b5daa50d9b4c07367dffc40880467170bf1c31ca63a2286506edbe6d3d65b", size = 706914, upload-time = "2026-03-20T14:25:32.501Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/f1e34c8224dc373c6fab5b33e33be0d184751fdc27013af3278b1e4e6e6c/fastar-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ec841a69fea73361c6df6d9183915c09e9ce3bd96493763fa46019e79918400", size = 627422, upload-time = "2026-03-20T14:25:20.318Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ad/e2499d136e24c2d896f2ec58183c91c6f8185d758177537724ed2f3e1b54/fastar-0.9.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad46bc23040142e9be4b4005ea366834dbf0f1b6a90b8ecdc3ec96c42dec4adf", size = 865265, upload-time = "2026-03-20T14:24:55.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/b6ad68b2ab1d7b74b0d38725d817418016bdd64880b36108be80d2460b4d/fastar-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de264da9e8ef6407aa0b23c7c47ed4e34fde867e7c1f6e3cb98945a93e5f89f2", size = 760583, upload-time = "2026-03-20T14:23:50.447Z" }, + { url = "https://files.pythonhosted.org/packages/b8/96/086116ad46e3b98f6c217919d680e619f2857ffa6b5cc0d7e46e4f214b83/fastar-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c70be3a7da3ff9342f64c15ec3749c13ef56bc28e69075d82d03768532a8d0", size = 758000, upload-time = "2026-03-20T14:24:03.471Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e6/ea642ea61eea98d609343080399a296a9ff132bd0492a6638d6e0d9e41a7/fastar-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a734506b071d2a8844771fe735fbd6d67dd0eec80eef5f189bbe763ebe7a0b8", size = 923647, upload-time = "2026-03-20T14:24:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/53874aad61e4a664af555a2aa7a52fe46cfadd423db0e592fa0cfe0fa668/fastar-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eac084ab215aaf65fa406c9b9da1ac4e697c3d3a1a183e09c488e555802f62d", size = 816528, upload-time = "2026-03-20T14:24:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/41/df/d663214d35380b07a24a796c48d7d7d4dc3a28ec0756edbcb7e2a81dc572/fastar-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf", size = 819050, upload-time = "2026-03-20T14:25:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5a/455b53f11527568100ba6d5847635430645bad62d676f0bae4173fc85c90/fastar-0.9.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f2f399fffb74bcd9e9d4507e253ace2430b5ccf61000596bda41e90414bcf4f2", size = 885257, upload-time = "2026-03-20T14:24:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dd/0a8ea7b910293b07f8c82ef4e6451262ccf2a6f2020e880f184dc4abd6c2/fastar-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b", size = 968135, upload-time = "2026-03-20T14:25:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/5c7e9231d6ba00e225623947068db09ddd4e401800b0afaf39eece14bfee/fastar-0.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d012644421d669d9746157193f4eafd371e8ae56ff7aef97612a4922418664c", size = 1034940, upload-time = "2026-03-20T14:25:58.893Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b4/eccfcf7fe9d2a0cea6d71630acc48a762404058c9b3ae1323f74abcda005/fastar-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:094fd03b2e41b20a2602d340e2b52ad10051d82caa1263411cf247c1b1bc139f", size = 1073807, upload-time = "2026-03-20T14:26:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/8b/53/6ddda28545b428d54c42f341d797046467c689616a36eae9a43ba56f2545/fastar-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484", size = 1025314, upload-time = "2026-03-20T14:26:24.624Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/71e2a67b0a69971044ad57fe7d196287ac32ab710bfc47f34745bb4a7834/fastar-0.9.0-cp312-cp312-win32.whl", hash = "sha256:25a1fd512ce23eb5aaab514742e7c6120244c211c349b86af068c3ae35792ec3", size = 452740, upload-time = "2026-03-20T14:26:56.604Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/0ffa2fffac0d80d2283db577ff23f8d91886010ea858c657f8278c2a222c/fastar-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:b10a409797d01ee4062547e95e4a89f6bb52677b144076fd5a1f9d28d463ab10", size = 485282, upload-time = "2026-03-20T14:26:44.926Z" }, + { url = "https://files.pythonhosted.org/packages/14/20/999d72dc12e793a6c7889176fc42ad917d568d802c91b4126629e9be45a9/fastar-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea4d98fc62990986ce00d2021f08ff2aa6eae71636415c5a5f65f3a6a657dc5e", size = 461795, upload-time = "2026-03-20T14:26:36.728Z" }, + { url = "https://files.pythonhosted.org/packages/9a/26/ea9339facfe4ee224be673c6888dbf077f28b0f81185f80353966c9f4925/fastar-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7b55ae4a3a481fd90a63ac558a7e8aab652ac1dfd15d8657266e71bf65346408", size = 706740, upload-time = "2026-03-20T14:25:33.741Z" }, + { url = "https://files.pythonhosted.org/packages/77/52/f3b06867e5ca8d5b2c1c15a1563415e0037b5831f2058ee72b03960296d9/fastar-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400", size = 627615, upload-time = "2026-03-20T14:25:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/52/32/021b0a633bca18bca4f831392c2938c15c4605de2d9895b783ad6d64679c/fastar-0.9.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:90f46492e05141089766699e95c79d470e8013192fbbb16ef16b576281f3b8ee", size = 864584, upload-time = "2026-03-20T14:24:56.941Z" }, + { url = "https://files.pythonhosted.org/packages/3f/54/e2e1b4c8512d670373047e5e585b1d1ff9ffd722b0a17647d22c9c9bd248/fastar-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f", size = 760246, upload-time = "2026-03-20T14:23:51.964Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7d/1e283dd8dbb3647049594bb477bdc053045c6fff2d3f06386d2dcacce7aa/fastar-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d17d311cfbb559154ba940972b6d07a3a7ac221a2a01208f119ad03495f01d32", size = 757024, upload-time = "2026-03-20T14:24:04.69Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/82d3cb64d318ce16c5d1a26a40b8aa570fcc9b23684221aece838c4cbada/fastar-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2ef34e7088f308e73460e1b8d9b0479a743f679816782a80db6ae87ee68714a", size = 921630, upload-time = "2026-03-20T14:24:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b8/3e7892f1a25a1a2054a20de6c846c0794b8fa361e5b9d3d00915b41e97bd/fastar-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c93bf4732d0dd6adae4a8b3bbebe19af76ee1072b7688bf39c5a1d120425a772", size = 815791, upload-time = "2026-03-20T14:24:43.28Z" }, + { url = "https://files.pythonhosted.org/packages/db/5e/8fcc662db1fd0985f4f8a54e79276416565a0d1fcb8da66665b2061ead30/fastar-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834", size = 818980, upload-time = "2026-03-20T14:25:09.545Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/37291fbd6c9b5b0905712da6191bdfc25a7dc236efbf130e3a1a7d1b9440/fastar-0.9.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:912efe3121dc1f3c05940cfa1c6b09b8868d702d24566506aa1d0d96e429923a", size = 884578, upload-time = "2026-03-20T14:24:30.584Z" }, + { url = "https://files.pythonhosted.org/packages/94/19/7b3b7af978ae4f012664781554716d67549ab19ddbcb6e6d1adc04d7a5e7/fastar-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e", size = 967790, upload-time = "2026-03-20T14:25:46.975Z" }, + { url = "https://files.pythonhosted.org/packages/e6/38/4cce2a8e529a7d3e99e427c9bbcccd7013ff6b3ba295613e6f1c573c9e6c/fastar-0.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d0aff74ea98642784c941d3cd8c35943258d4b9626157858901c5b181683339b", size = 1033892, upload-time = "2026-03-20T14:26:00.22Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3f/86f25d79b1b369c2756ee338b76d1696a9cac3a737e819459b0ad7822ede/fastar-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3e8a1deaf490f4ec15eca7e66127ff89cdefd20217f358739d4b7b1cb322f663", size = 1072969, upload-time = "2026-03-20T14:26:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/10/4f/6ec0c123c15bbcb9a9b82e979dc81273789ebbfbb4a2b41a1a6941577c94/fastar-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d", size = 1025768, upload-time = "2026-03-20T14:26:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d1/cbdcdb78ca034ed51a9f53c2650885873d8b06727452c1cc33f56ad0c66a/fastar-0.9.0-cp313-cp313-win32.whl", hash = "sha256:11b35e6453a2da8715dd8415b3999ea57805125493e44ce41a32404bf9a510a7", size = 452742, upload-time = "2026-03-20T14:26:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/74/ee/138d2f8e3504232a279afa224d3e5922c15dc7126613e6c135cfc8e10ec9/fastar-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:10a1e7f7bfa1c6f03e4c657fdc0a32ebe42d8e48f681403dc0c67258e1cb5bef", size = 484917, upload-time = "2026-03-20T14:26:46.135Z" }, + { url = "https://files.pythonhosted.org/packages/db/ca/f518ee9dccc45097560a2cff245590c65b7b348171c8d2f2e487cf92a69f/fastar-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:e5484ac1415e0ca8bc7b69231e3e3afb52887fed10b839ca676767635a13f06f", size = 461202, upload-time = "2026-03-20T14:26:37.937Z" }, + { url = "https://files.pythonhosted.org/packages/cf/00/99700dd33273c118d7d9ab7ad5db6650b430448d4cfae62aec6ef6ca4cb7/fastar-0.9.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ccb2289f24ee6555330eb77149486d3a2ec8926450a96157dd20c636a0eec085", size = 707059, upload-time = "2026-03-20T14:25:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4808dcfa8dddb9d7f50d830a39a9084d9d148ed06fcac8b040620848bc24/fastar-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2bfee749a46666785151b33980aef8f916e6e0341c3d241bde4d3de6be23f00c", size = 627135, upload-time = "2026-03-20T14:25:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/9c92e97d760d769846cae6ce53332a5f2a9246eb07b369ac2a4ebf10480c/fastar-0.9.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f6096ec3f216a21fa9ac430ce509447f56c5bd979170c4c0c3b4f3cb2051c1a8", size = 864974, upload-time = "2026-03-20T14:24:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/84/38/9dadebd0b7408b4f415827db35169bbd0741e726e38e3afd3e491b589c61/fastar-0.9.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7a806e54d429f7f57e35dc709e801da8c0ba9095deb7331d6574c05ae4537ea", size = 760262, upload-time = "2026-03-20T14:23:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/7afc5721429515aa0873b268513f656f905d27ff1ca54d875af6be9e9bc6/fastar-0.9.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9a06abf8c7f74643a75003334683eb6e94fabef05f60449b7841eeb093a47b0", size = 757575, upload-time = "2026-03-20T14:24:06.143Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5d/7498842c62bd6057553aa598cd175a0db41fdfeda7bdfde48dab63ffb285/fastar-0.9.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e9b5c155946f20ce3f999fb1362ed102876156ad6539e1b73a921f14efb758c", size = 924827, upload-time = "2026-03-20T14:24:19.364Z" }, + { url = "https://files.pythonhosted.org/packages/69/ab/13322e98fe1a00ed6efbfa5bf06fcfff8a6979804ef7fcef884b5e0c6f85/fastar-0.9.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdedac6a84ef9ebc1cee6d777599ad51c9e98ceb8ebb386159483dcd60d0e16", size = 816536, upload-time = "2026-03-20T14:24:44.844Z" }, + { url = "https://files.pythonhosted.org/packages/fe/fd/0aa5b9994c8dba75b73a9527be4178423cb926db9f7eca562559e27ccdfd/fastar-0.9.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51df60a2f7af09f75b2a4438b25cb903d8774e24c492acf2bca8b0863026f34c", size = 818686, upload-time = "2026-03-20T14:25:10.799Z" }, + { url = "https://files.pythonhosted.org/packages/46/d6/e000cd49ef85c11a8350e461e6c48a4345ace94fb52242ac8c1d5dad1dfc/fastar-0.9.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:15016d0da7dbc664f09145fc7db549ba8fe32628c6e44e20926655b82de10658", size = 885043, upload-time = "2026-03-20T14:24:32.231Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/ee734fe273475b9b25554370d92a21fc809376cf79aa072de29d23c17518/fastar-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c66a8e1f7dae6357be8c1f83ce6330febbc08e49fc40a5a2e91061e7867bbcbf", size = 967965, upload-time = "2026-03-20T14:25:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/165b3a75f1ee8045af9478c8aae5b5e20913cca2d4a5adb1be445e8d015a/fastar-0.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1c6829be3f55d2978cb62921ef4d7c3dd58fe68ee994f81d49bd0a3c5240c977", size = 1034507, upload-time = "2026-03-20T14:26:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4e/4097b5015da02484468c16543db2f8dec2fe827d321a798acbd9068e0f13/fastar-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68db849e01d49543f31d56ef2fe15527afe2b9e0fb21794edc4d772553d83407", size = 1073388, upload-time = "2026-03-20T14:26:14.448Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/3b86af4e63a551398763a1bbbbac91e1c0754ece7ac7157218b33a065f4c/fastar-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5569510407c0ded580cfeec99e46ebe85ce27e199e020c5c1ea6f570e302c946", size = 1025190, upload-time = "2026-03-20T14:26:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/8c50a60f03e095053306fcf57d9d99343bce0e99d5b758bf96de31aec849/fastar-0.9.0-cp314-cp314-win32.whl", hash = "sha256:3f7be0a34ffbead52ab5f4a1e445e488bf39736acb006298d3b3c5b4f2c5915e", size = 452301, upload-time = "2026-03-20T14:26:59.234Z" }, + { url = "https://files.pythonhosted.org/packages/ee/69/aa6d67b09485ba031408296d6ff844c7d83cdcb9f8fcc240422c6f83be87/fastar-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf7f68b98ed34ce628994c9bbd4f56cf6b4b175b3f7b8cbe35c884c8efec0a5b", size = 484948, upload-time = "2026-03-20T14:26:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/20/6d/dba29d87ca929f95a5a7025c7d30720ad8478beed29fff482f29e1e8b045/fastar-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:155dae97aca4b245eabb25e23fd16bfd42a0447f9db7f7789ab1299b02d94487", size = 461170, upload-time = "2026-03-20T14:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/96/8f/c3ea0adac50a8037987ee7f15ff94767ebb604faf6008cbd2b8efa46c372/fastar-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a63df018232623e136178953031057c7ac0dbf0acc6f0e8c1dc7dbc19e64c22f", size = 705857, upload-time = "2026-03-20T14:25:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/e0e1aad1778065559680a73cdf982ed07b04300c2e5bf778dec8668eda6f/fastar-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6fb44f8675ef87087cb08f9bf4dfa15e818571a5f567ff692f3ea007cff867b5", size = 626210, upload-time = "2026-03-20T14:25:24.361Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/3c117335cbea26b3bc05382c27e6028278ed048d610b8de427c68f2fec84/fastar-0.9.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81092daa991d0f095424e0e28ed589e03c81a21eeddc9b981184ddda5869bf9d", size = 864879, upload-time = "2026-03-20T14:25:00.131Z" }, + { url = "https://files.pythonhosted.org/packages/26/5d/e8d00ec3b2692d14ea111ddae25bf10e0cb60d5d79915c3d8ea393a87d5c/fastar-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e8793e2618d0d6d5a7762d6007371f57f02544364864e40e6b9d304b0f151b2", size = 759117, upload-time = "2026-03-20T14:23:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/1a/61/6e080fdbc28c72dded8b6ff396035d6dc292f9b1c67b8797ac2372ca5733/fastar-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f7ef7056791fc95b6afa987238368c9a73ad0edcedc6bc80076f9fbd3a2a78", size = 756527, upload-time = "2026-03-20T14:24:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/e8/97/2cf1a07884d171c028bd4ae5ecf7ded6f31581f79ab26711dcdad0a3d5ab/fastar-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3a456230fcc0e560823f5d04ae8e4c867300d8ee710b14ddcdd1b316ac3dd8d", size = 921763, upload-time = "2026-03-20T14:24:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/c1d698a45f9f5dc892ed7d64badc9c38f1e5c1667048191969c438d2b428/fastar-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a60b117ebadc46c10c87852d2158a4d6489adbfbbec37be036b4cfbeca07b449", size = 815493, upload-time = "2026-03-20T14:24:46.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/38/e124a404043fba75a8cb2f755ca49e4f01e18400bb6607a5f76526e07164/fastar-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6199b4ca0c092a7ae47f5f387492d46a0a2d82cb3b7aa0bf50d7f7d5d8d57f", size = 819166, upload-time = "2026-03-20T14:25:12.027Z" }, + { url = "https://files.pythonhosted.org/packages/85/4a/5b1ea5c8d0dbdfcec2fd1e6a243d6bb5a1c7cd55e132cc532eb8b1cbd6d9/fastar-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:34efe114caf10b4d5ea404069ff1f6cc0e55a708c7091059b0fc087f65c0a331", size = 883618, upload-time = "2026-03-20T14:24:33.552Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/ae46e5722a67a3c2e0ff83d539b0907d6e5092f6395840c0eb6ede81c5d6/fastar-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d44c1f8d9c5a3e4e58e6ffb77f4ca023ba9d9ddd88e7c613b3419a8feaa3db7", size = 966294, upload-time = "2026-03-20T14:25:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/b161cf8711f4a50a3e57b6f89bc703c1aed282cad50434b3bc8524738b20/fastar-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d2af970a1f773965b05f1765017a417380ad080ea49590516eb25b23c039158a", size = 1033177, upload-time = "2026-03-20T14:26:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/e2/76/faac7292bce9b30106a6b6a9f5ddb658fdb03abe2644688b82023c8f76b9/fastar-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1675346d7cbdde0d21869c3b597be19b5e31a36442bdf3a48d83a49765b269dc", size = 1073620, upload-time = "2026-03-20T14:26:16.121Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/dd55ffcc302d6f0ff4aba1616a0da3edc8fcefb757869cad81de74604a35/fastar-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc440daa28591aeb4d387c171e824f179ad2ab256ce7a315472395b8d5f80392", size = 1025147, upload-time = "2026-03-20T14:26:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/080bbb2b3c4e739fe6486fd765a09905f6c16c1068b2fcf2bb51a5e83937/fastar-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:32787880600a988d11547628034993ef948499ae4514a30509817242c4eb98b1", size = 452317, upload-time = "2026-03-20T14:27:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/00553739a7e9e35f78a0c5911d181acf6b6e132337adc9bbc3575f5f6f04/fastar-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92fa18ec4958f33473259980685d29248ac44c96eed34026ad7550f93dd9ee23", size = 483994, upload-time = "2026-03-20T14:26:52.76Z" }, + { url = "https://files.pythonhosted.org/packages/4f/36/a7af08d233624515d9a0f5d41b7a01a51fd825b8c795e41800215a3200e7/fastar-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:34f646ac4f5bed3661a106ca56c1744e7146a02aacf517d47b24fd3f25dc1ff6", size = 460604, upload-time = "2026-03-20T14:26:40.771Z" }, + { url = "https://files.pythonhosted.org/packages/69/9f/4aeaa0a1ac2aca142a276ea136e651e94ba1341bd840ba455ed250d1970b/fastar-0.9.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b74ce299066288f3b90221dca8507f59c7d9e8df91387948006b9a0fea4f9bdc", size = 710738, upload-time = "2026-03-20T14:25:41.17Z" }, + { url = "https://files.pythonhosted.org/packages/d0/19/9f8fb5c0e803254c5d535c362102dd604d9bdb206d5a36150f4637cadf09/fastar-0.9.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76be31936cabce31cbb6381128f851cf0a6da2d5c25357615cd1504b26dc31cf", size = 633000, upload-time = "2026-03-20T14:25:28.496Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8d/0d1d9a87a78f1e686bb6c7c69688a4c9ad1efb65e49cc66310b97fdf900b/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c4c9ea0e0d69445b0ca3b0bd80bd8237fec8a914275b0472ecca2b555c12f3a3", size = 871226, upload-time = "2026-03-20T14:25:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/366937320b1cca522570c527a45b1254bd68d057e68956baefc49eacae27/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b665c33afcd1d581b82235b690d999c5446ccc2c4d80c4a95f30df3b43d22494", size = 763872, upload-time = "2026-03-20T14:23:59.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f2/121c5432bb152da68fc466a0d0206d66383a40a2f9beff5583d9277aceee/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2a9a49f9217f4f60f9ba23fdd1f7f3f04fed97391145eb9460ec83ca0b4bd33", size = 762897, upload-time = "2026-03-20T14:24:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/88d3a603b997063e032f94cc0fff74031d76903f38cc30416a400395df03/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d860e82a531e9cc67e7f500a299bffbe6e93d80bbf48401fd8f452a0c58f28", size = 927024, upload-time = "2026-03-20T14:24:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/a6/17/d6dc778c45b0c7d9a279706d7a5d62122dab0a7a0cb39aac6f5ef42f13f6/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3feede2d72ec0782b5ccc18568f36cbe33816be396551aa47b3e1b73c322cdd2", size = 821265, upload-time = "2026-03-20T14:24:50.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e0/cec25d43df7ea4b4e3e875352c6d51c848c855792ba276c546732a7170af/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ac410d32cbb514e966c45f0fedd0f9447b0dea9e734af714648da503603df6", size = 824024, upload-time = "2026-03-20T14:25:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/90/c354969770d21d1b07c9281b5e23052392c288d22984a1917d30940e86cb/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:40b8c08df809e5e58d1839ccb37bafe4485deb6ee56bb7c5f0cbb72d701eb965", size = 888886, upload-time = "2026-03-20T14:24:38.229Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/eb2a01ed94e79b72003840448d2b69644a54a47f615c7d693432a1337caa/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d62a4fd86eda3bea7cc32efd64d43b6d0fcdbbec009558b750fc362f20142789", size = 972503, upload-time = "2026-03-20T14:25:54.207Z" }, + { url = "https://files.pythonhosted.org/packages/8d/88/f7e28100fa7ff4a26a3493ad7a5d45d70f6de858c05f5c34aca3570c5839/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:7bf6958bb6f94e5ec522e4a255b8e940d3561ad973f0be5dde6115b5a0854af5", size = 1039106, upload-time = "2026-03-20T14:26:07.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/de/52c578180fdaaf0f3289de8a878f1ac070f7e3e18a0689d3fd44dd7dae2c/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:c210b839c0a33cf8d08270963ad237bcb63029dddf6d6025333f7e5ca63930bd", size = 1080754, upload-time = "2026-03-20T14:26:20.299Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/1ea024be428ad9d89e9f738c9379507e97df9f9ed97e50e4a1d10ff90fef/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fad70e257daefb42bab68dcd68beaf2e2a99da056d65f2c9f988449a4e869306", size = 1031304, upload-time = "2026-03-20T14:26:33.294Z" }, +] + +[[package]] +name = "fasteners" +version = "0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/18/7881a99ba5244bfc82f06017316ffe93217dbbbcfa52b887caa1d4f2a6d3/fasteners-0.20.tar.gz", hash = "sha256:55dce8792a41b56f727ba6e123fcaee77fd87e638a6863cec00007bfea84c8d8", size = 25087, upload-time = "2025-08-11T10:19:37.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "fastparquet" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/ad/87f7f5750685e8e0a359d732c85332481ba9b5723af579f8755f81154d0b/fastparquet-2025.12.0.tar.gz", hash = "sha256:85f807d3846c7691855a68ed7ff6ee40654b72b997f5b1199e6310a1e19d1cd5", size = 480045, upload-time = "2025-12-18T16:22:22.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e2/71a0650e8d1fbc4901554f71b05c7d86ea64a8b8c795dd06195648788ebf/fastparquet-2025.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2912c916c3514edeefab23a84a39807e2833ea34e29af6155e9bbb127ed25b3e", size = 893079, upload-time = "2025-12-18T21:53:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6c/2d50358911e8e70cfc78b269385ecf3a721e28ad2e8fda9c1af5dac48dbb/fastparquet-2025.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1506d3ea082e7cf8b306176644af42f5ced54cad64ddfe389222bb53892b060b", size = 686979, upload-time = "2025-12-18T21:54:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/d3a517e5d3ae2b7c63d3abc2731015bd7f83e70e6f004e4d13ff168067fd/fastparquet-2025.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:44e294e2c6b74ff06126c8f9a87d3b107099d39ec456b4f7d0c89f223188ca12", size = 1730074, upload-time = "2025-12-18T21:58:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6d/c6c93d2f076adedd10305a05b50509f10e214372adaecfa50c8f772bc25c/fastparquet-2025.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d21b364a33d1686484f643dc15f72494e052fe7147a866cbb130482fd3e5d2e4", size = 1765289, upload-time = "2025-12-18T21:57:28.07Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f0/da97efbef37980097757f509848bca09aba1e1e8239fe67453873b9551fe/fastparquet-2025.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33686f292c0cc7ef162c0c83832305483a326faa21e0eda8f305c5e445345a6c", size = 1772015, upload-time = "2025-12-18T21:58:36.611Z" }, + { url = "https://files.pythonhosted.org/packages/54/9b/58644cf73464d4e764007636ee7ffdfa495000adbe2a1f05f5b39a3db2fb/fastparquet-2025.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:82995d2d629b7f766b681b8fd4f4bc1193350d1829c3cde5929a341c14c004c7", size = 1755631, upload-time = "2025-12-18T21:58:06.361Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b6/950b3434ca6f2153298d2381aaeec121685784ab3097fb6f096ebfe8f213/fastparquet-2025.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:612337bfb49f414d5d4337c04747d9b27cacdcdc808e20dc11de7f20c7e8924d", size = 1782160, upload-time = "2025-12-18T21:58:38.075Z" }, + { url = "https://files.pythonhosted.org/packages/64/9b/f4c5fed2ed1b110bc113a741f8dd7ce820b85d08d33650a64eeec6ae39fb/fastparquet-2025.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:650a253e9ececf11a01898b08f78b10aa69e3d6665dcc0ed70d0d907ebeba154", size = 669688, upload-time = "2025-12-18T21:59:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/27/13/abd53c73d1a146ffae523285214c3db3dafe855bd70af787bf9bf9295224/fastparquet-2025.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:268ca27e80f49e07f5bedf8b534971d3d3ef5621ea26fed1fb3d5c122b25abe1", size = 891061, upload-time = "2025-12-18T21:53:51.162Z" }, + { url = "https://files.pythonhosted.org/packages/37/4d/805a46985cfc3747adfa8b614307fc097eecf6f4708557ac8557484f1f29/fastparquet-2025.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48b8d3f0565986d4dfe4627b6104cb8a0488fbdd642b6cf0585e2b907c11cb49", size = 685874, upload-time = "2025-12-18T21:54:12.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/18f00d0d959920d8a8b687c481509604315c25d33f1578497243581b3d98/fastparquet-2025.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:67a99515d9ccaf59bc815f0e0244468a07be7a0d2eb5940308993f1fdd2acb0a", size = 1783730, upload-time = "2025-12-18T21:58:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/af/9b68c6236a0cfb3004438b02e927dc8eec72e90dce0474847a56735ba438/fastparquet-2025.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:809e17bd79b16b9b47526a48e04447b365df404f30c33a118884a365569a3a6b", size = 1830217, upload-time = "2025-12-18T21:57:30.158Z" }, + { url = "https://files.pythonhosted.org/packages/d0/62/b98920ded66cf9987d30571f4a16c24d0611a1f08334b4e6175b57a2b234/fastparquet-2025.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4826ff55a2d7b99309752a5bda6569e17028f53a7e7bfbca6ac067dd5af659f6", size = 1836015, upload-time = "2025-12-18T21:58:39.875Z" }, + { url = "https://files.pythonhosted.org/packages/22/6d/e8d25713d995664f9babc15055d00669458aa0b2cb4bf765febe5c71c881/fastparquet-2025.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:053581b9456848b27e2d9289dce41686a6739f107573731121f213064dd5baac", size = 1810518, upload-time = "2025-12-18T21:58:09.659Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/311ad9acadf0a944a977bf98df46cbab8b172adf0693a5689f4d7d5b8996/fastparquet-2025.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a79b2520449511503b434b0e45746d604ecf88a56d1e6e1297dc5b6b0871b5bf", size = 1843846, upload-time = "2025-12-18T21:58:41.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bc/3a59e6ca8bb2cd925e7547a49cba74107d1ff9dab9e098a61355a52bdc49/fastparquet-2025.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:1724e03aa2b68cd585dff52ce7994110366c8d73782f895197db29cee833a840", size = 669220, upload-time = "2025-12-18T21:59:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b2/229a4482d80a737d0fe6706c4f93adb631f42ec5b0a2b154247d63bb48fe/fastparquet-2025.12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:27b1cf0557ddddbf0e28db64d4d3bea1384be1d245b2cef280d001811e3600fe", size = 896986, upload-time = "2025-12-18T21:53:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/953117c43bf617379eff79ce8a2318ef49f7f41908faade051fa12281ac8/fastparquet-2025.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9356c59e48825d61719960ccb9ce799ad5cd1b04f2f13368f03fab1f3c645d1e", size = 687642, upload-time = "2025-12-18T21:54:13.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/41deaa9a4fc9ab6c00f3b49afe56cbafee13a111032aa41f23d077b69ad6/fastparquet-2025.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c92e299a314d4b542dc881eeb4d587dc075c0a5a86c07ccf171d8852e9736d", size = 1764260, upload-time = "2025-12-18T21:58:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0f/a229b3f699aaccc7b5ec3f5e21cff8aa99bc199499bff08cf38bc6ab52c6/fastparquet-2025.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4881dc91c7e6d1d08cda9968ed1816b0c66a74b1826014c26713cad923aaca71", size = 1810920, upload-time = "2025-12-18T21:57:31.514Z" }, + { url = "https://files.pythonhosted.org/packages/90/c2/ca76afca0c2debef368a42a701d501e696490e0a7138f0337709a724b189/fastparquet-2025.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8d70d90614f19752919037c4a88aaaeda3cd7667aeb54857c48054e2a9e3588", size = 1819692, upload-time = "2025-12-18T21:58:43.095Z" }, + { url = "https://files.pythonhosted.org/packages/ab/41/f235c0d8171f6676b9d4fb8468c781fbe7bf90fed2c4383f2d8d82e574db/fastparquet-2025.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e2ccf387f629cb11b72fec6f15a55e0f40759b47713124764a9867097bcd377", size = 1784357, upload-time = "2025-12-18T21:58:13.258Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/c86bf33b363cf5a1ad71d3ebd4a352131ba99566c78aa58d9e56c98526ba/fastparquet-2025.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1978e7f3c32044f2f7a0b35784240dfc3eaeb8065a879fa3011c832fea4e7037", size = 1815777, upload-time = "2025-12-18T21:58:44.432Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0b/769333ab6e6ed401755b550b3338cee96b8f6502db5da55312d86a97db62/fastparquet-2025.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:25e87fff63c011fe658a7547ba83355e02568db1ee26a65e6b75c2287701d5dc", size = 667555, upload-time = "2026-01-06T21:24:36.381Z" }, + { url = "https://files.pythonhosted.org/packages/13/cf/1801afbc1e84ad0413ec66bf93590472152462c454593e3be3265861aa0f/fastparquet-2025.12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1bd79ca75977aaeaae8d2a6cb1958e806991f0ff23207b938522a59a724491b2", size = 893835, upload-time = "2025-12-18T21:53:53.87Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/5539b19ae7e1e0ad77f5b8a1e8d480fdf0193639cf97239734173b8730ab/fastparquet-2025.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b6db801b72433d8227fcb92009a631f14d6d49a43b3c599911b58a8a6ffde9e3", size = 686010, upload-time = "2025-12-18T21:54:15.234Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d9/0f39782c500bbf6b2e40a67cac3c9ec2eae70bdaa8b283106c2b3d532a95/fastparquet-2025.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:23cce7202de91b64abb251cec07125d94e8108eb99aab6ffa42570a89a5c869d", size = 1755599, upload-time = "2025-12-18T21:58:15.016Z" }, + { url = "https://files.pythonhosted.org/packages/b5/16/d0d0c5ca6a9fa13e2f36e6983452d798d8116bd5d05bf23246efd1c23dc8/fastparquet-2025.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:038c3ed1f211f538cd03df7b053cc842677efd5832e37b000a8c721584ff42b4", size = 1801454, upload-time = "2025-12-18T21:57:33.097Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/6c6a1cae46104a3ec5da87cb5fefb3eac0c07f04e56786f928164942e91a/fastparquet-2025.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:424ffcfc89c678eb8e695ff882d114e46beda8b7e13be58b6793f2ee07c84a6f", size = 1812257, upload-time = "2025-12-18T21:58:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/69/77/6a7158e2817d44fb80f32a4a4c3f8cadf7e273fac34e04155588bf2b3141/fastparquet-2025.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f25aae3e585dd033ed02ee167a825bf1fcb440629c63f7d59d6c4d2789c327a3", size = 1776841, upload-time = "2025-12-18T21:58:16.654Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/58b1d885dcf05ba619d3a9bbf61b3bff611c4636880077be8659bf29ce94/fastparquet-2025.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90ac4a51e5acb2644ec111532c8fcfc128efcc351ba2ee914394a58460310b93", size = 1810507, upload-time = "2025-12-18T21:58:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/f8/10/380cba3ee18b25384cbf0d229b8cad47d63eb89c630f267cf1e11c64fe16/fastparquet-2025.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7ac92db3b3200fe3be07363277678bfd532c6723510b40c20510631ca434a049", size = 667416, upload-time = "2025-12-18T21:59:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3a/7bc677df8d4dadc4f7f2dee035c9578aa0e79e2c0f58ddc78e197e24fbc2/fastparquet-2025.12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c0fe3f8a73160be7778e1a54ac4463b49a7e35e1f6c7fb9876b36d2ec572bead", size = 900184, upload-time = "2025-12-18T21:53:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/c5/aa/2c726bfd2a6c0e18854a924c3faeee1c2e934b03915c8d2111a3c3f7c0fd/fastparquet-2025.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:aec3a736e3c43f7d8f911946f4c56b8cc17e803932ca0cb75bb2643796adabeb", size = 692174, upload-time = "2025-12-18T21:54:16.329Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/a0936ac68c7209ab4979ac45ab59d6efa700b5ddac62031f4ddd6b462f0d/fastparquet-2025.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8aa32817dd571b10974b04c66e470a181208840466f155280ff3df43946c6b92", size = 1755044, upload-time = "2025-12-18T21:58:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/64/54/0b06b3c8a778fd0795426e2a529672cb6925541ba2a1076e3d8940a6c565/fastparquet-2025.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f5a9dc0036838950e449d6d05dd48e25b6b2741568b4e0872823195e23890b1", size = 1793074, upload-time = "2025-12-18T21:57:34.995Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/7b5109f7ec39dbe3dc847a3a3d63105a78717d9fe874abbba7a90f047b31/fastparquet-2025.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05971c0974b5bb00c01622fe248f83008e58f06224212c778f7d46ccb092a7d2", size = 1802137, upload-time = "2025-12-18T21:58:50.504Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8b/f3acc13ffec64803bbbb56977147e8ea105426f5034c9041d5d6d01c7e62/fastparquet-2025.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e86a3407933ff510dad077139eaae2c664d2bdeeb0b6ece2a1e1c98c87257dd3", size = 1781629, upload-time = "2025-12-18T21:58:20.015Z" }, + { url = "https://files.pythonhosted.org/packages/13/66/c102a8b01976afd4408ccfc7f121516168faaafb86a201716116ce5120d0/fastparquet-2025.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:00349200d1103a34e34a94f535c1bf19870ab1654388b8a2aa50ca34046fc071", size = 1806721, upload-time = "2025-12-18T21:58:52.495Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/13340110f7daa99db2c9f090a2790602515dabc6dc263e88931482aaaf66/fastparquet-2025.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:8f42036889a5729da1cae6e2a599b9c8b93af6f99973015ac14225d529300982", size = 673274, upload-time = "2025-12-18T21:59:13.642Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/22f149b01de42cc69a4faa1047e1902a91bf1085e79ccba20caceded8607/fastparquet-2025.12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a4e9165c98f0fdac70aba728055424b0b2830a9cb02e9048d3d82d2e9c0294c1", size = 929604, upload-time = "2025-12-18T21:53:57.814Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e8/18b0831254eb8a3b07caf374a23dc011eeffa5f8bc5507d2b43498bc577d/fastparquet-2025.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69b80faf4c9d154fc95d3f291a55b1d782c684e9fcfe443a274c3e92d36a963c", size = 708902, upload-time = "2025-12-18T21:54:17.803Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0c/a29aa2c84b46d35e5dc4ece79f0fca67a6889a51ac3d0330a7fb22cf82fd/fastparquet-2025.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b9c9108127778d9628cce342f4e4c98890a4b686f677ed4973bc0edd6e25af9", size = 1771639, upload-time = "2025-12-18T21:58:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/2d851d5effe3c95b36ae948fb7da46d00ae8f88ae0d6907403b2ac5183c9/fastparquet-2025.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c052cacccfc6f8cb2ca98e809380969214b79471d49867f802184d3ea68d1e9", size = 1830649, upload-time = "2025-12-18T21:57:36.884Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a1/868f2d5db3fc9965e4ca6a68f6ab5fef3ade0104136e3556299c952bc720/fastparquet-2025.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c027278b5372e11a005b8d1ad9d85e86a9d70077dc8918cda99f90e657dc7251", size = 1820867, upload-time = "2025-12-18T21:58:54.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/f900734e546425509cf1f5cc9cd4f75275dff45c40d8c65feb0f148e4118/fastparquet-2025.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:618cc4388f5bc1d85587c0842f6c0d1af8ab2e27a5aa8074aa233b157f68f2c0", size = 1786865, upload-time = "2025-12-18T21:58:23.136Z" }, + { url = "https://files.pythonhosted.org/packages/34/14/88068907d837964d407d5835df6672ea635881d6e0937ca21dac088342bc/fastparquet-2025.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3e3fac9215a00a6a6836400437a7797841cb2f6393e38ff0a77c5e1aa37cfa44", size = 1817440, upload-time = "2025-12-18T21:58:56.702Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/5c4a0871d7b111c7115c02feb071c07a0a1c1da0afc1c35d9acb7958fd95/fastparquet-2025.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1bbacfff213b1cfbfa189ba1023f3fa9e3025ce6590c1becdb76a6ac1e84e623", size = 707783, upload-time = "2025-12-18T21:59:15.138Z" }, +] + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, +] + +[[package]] +name = "ffmpegcv" +version = "0.3.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/a4/fd0f0f5bb3d9c82f2bff0a65681a41be69e7c78570754950454226ee6b43/ffmpegcv-0.3.18.tar.gz", hash = "sha256:ba65969ae41c66dad30bda12e06ee595c7737ce88364c41662321566b228df6f", size = 30341, upload-time = "2025-04-15T13:50:10.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/91/e372814132b796045caedfba5b7c6abf7f555019df3860d15aa9bb51bd27/ffmpegcv-0.3.18-py3-none-any.whl", hash = "sha256:e6c46db914750c0bf99fd517e54b7ada0626dd337f30ebdf5eec77279301ea48", size = 32610, upload-time = "2025-04-15T13:50:09.312Z" }, +] + +[[package]] +name = "ffmpy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "flash-attn" +version = "2.7.4.post1+cu128.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/flash_attn-2.7.4.post1%2Bcu128.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:c756d55ffb46a8ea222cbadbf1c142482f18035264a224d4b04f1f13f3702b79" }, +] + +[[package]] +name = "flash-attn" +version = "2.7.4.post1+cu130.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/flash_attn-2.7.4.post1%2Bcu130.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:38d4fc21714942dae9c26e1c889962329472e24d90aafdf02e67b988b949bda2" }, +] + +[[package]] +name = "flash-attn-3-nv" +version = "1.0.3+cu128.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "ninja" }, + { name = "packaging" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/flash_attn_3_nv-1.0.3%2Bcu128.torch210-cp39-abi3-linux_x86_64.whl", hash = "sha256:ed7b3cf08ffacdeadfaa44ee674a3a5e67e7011829e0e757eb6f3463fb8d443e" }, +] + +[[package]] +name = "flash-attn-3-nv" +version = "1.0.3+cu130.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "ninja" }, + { name = "packaging" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/flash_attn_3_nv-1.0.3%2Bcu130.torch210-cp39-abi3-linux_x86_64.whl", hash = "sha256:14289654a28530ddedb03f938c68504c47bc2e9be8aefcb2035519c005575fcb" }, +] + +[[package]] +name = "flashinfer-cubin" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/e8/826f9452bc5f76b94d7eb025f03dcaf1b51b9ed7790386c0285191e69be4/flashinfer_cubin-0.6.6-py3-none-any.whl", hash = "sha256:36508dfc792eb5ecfb15d2c140a7702812e1fa1ab0fb03929b2ed55e3e8191f3", size = 267661457, upload-time = "2026-03-11T01:36:36.538Z" }, +] + +[[package]] +name = "flashinfer-python" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "click" }, + { name = "einops" }, + { name = "ninja" }, + { name = "numpy" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl" }, + { name = "nvidia-ml-py" }, + { name = "packaging" }, + { name = "requests" }, + { name = "tabulate" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/70/c5a235297351021f5d3d3233523a85f5a6468495587489ad2f257e8eafe2/flashinfer_python-0.6.6.tar.gz", hash = "sha256:0730ba7c7aad332961933bcebc5119762797161ede57d955f6fd199818ed1d92", size = 5344156, upload-time = "2026-03-11T01:36:21.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/61/385d06755f3ab66333018285657adf0daf8a90a129448231fd09e315bd2e/flashinfer_python-0.6.6-py3-none-any.whl", hash = "sha256:078f158636969eec1a0d3dea19c3ca90b426b66df89bbf7b7b8276ce2ec08148", size = 7817047, upload-time = "2026-03-11T01:36:19.198Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "flopth" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "tabulate" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/da/130344cd229f4480631ff5c1b07b83375f2b0abf22aee05ab11b0a9190e9/flopth-0.1.6.tar.gz", hash = "sha256:56544978046b93ae476fe53ecc9f04ee8cc0a90b9d3c2669269fcd0a9ec74789", size = 38949, upload-time = "2024-11-23T15:25:39.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/92/8e15ee8ce39a43b6c879cccb4796de544c90f7a5a7312d912c3a47ec1f7e/flopth-0.1.6-py3-none-any.whl", hash = "sha256:5dd6a7d07357fdd0ffa5238468cfb836c3100bc89d69cb8ee2e448c1371baa38", size = 11908, upload-time = "2024-11-23T15:25:37.321Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "frozendict" +version = "2.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/b2/2a3d1374b7780999d3184e171e25439a8358c47b481f68be883c14086b4c/frozendict-2.4.7.tar.gz", hash = "sha256:e478fb2a1391a56c8a6e10cc97c4a9002b410ecd1ac28c18d780661762e271bd", size = 317082, upload-time = "2025-11-11T22:40:14.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/bd/920b1c5ff1df427a5fc3fd4c2f13b0b0e720c3d57fafd80557094c1fefe0/frozendict-2.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd37c087a538944652363cfd77fb7abe8100cc1f48afea0b88b38bf0f469c3d2", size = 59848, upload-time = "2025-11-11T22:37:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/e3e186925b1d84f816d458be4e2ea785bbeba15fd2e9e85c5ae7e7a90421/frozendict-2.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b96f224a5431889f04b2bc99c0e9abe285679464273ead83d7d7f2a15907d35", size = 38164, upload-time = "2025-11-11T22:37:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/af931d88c51ee2fcbf8c817557dcb975133a188f1b44bfa82caa940beeab/frozendict-2.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c1781f28c4bbb177644b3cb6d5cf7da59be374b02d91cdde68d1d5ef32e046b", size = 38341, upload-time = "2025-11-11T22:37:13.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/c1fd4f736758cf93939cc3b7c8399fe1db0c121881431d41fcdbae344343/frozendict-2.4.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a06f6c3d3b8d487226fdde93f621e04a54faecc5bf5d9b16497b8f9ead0ac3e", size = 112882, upload-time = "2025-11-11T22:37:15.098Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/304294f7cd099582a98d63e7a9cec34a9905d07f7628b42fc3f9c9a9bc94/frozendict-2.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b809d1c861436a75b2b015dbfd94f6154fa4e7cb0a70e389df1d5f6246b21d1e", size = 120482, upload-time = "2025-11-11T22:37:16.182Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/689212ea4124fcbd097c0ac02c2c6a4e345ccc132d9104d054ff6b43ab64/frozendict-2.4.7-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75eefdf257a84ea73d553eb80d0abbff0af4c9df62529e4600fd3f96ff17eeb3", size = 113527, upload-time = "2025-11-11T22:37:17.389Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9b/38a762f4e76903efd4340454cac2820f583929457822111ef6a00ff1a3f4/frozendict-2.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4d2b27d8156922c9739dd2ff4f3934716e17cfd1cf6fb61aa17af7d378555e9", size = 130068, upload-time = "2025-11-11T22:37:18.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/41/9751e9ec1a2e810e8f961aea4f8958953157478daff6b868277ab7c5ef8c/frozendict-2.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ebd953c41408acfb8041ff9e6c3519c09988fb7e007df7ab6b56e229029d788", size = 126184, upload-time = "2025-11-11T22:37:19.789Z" }, + { url = "https://files.pythonhosted.org/packages/71/be/b179b5f200cb0f52debeccc63b786cabcc408c4542f47c4245f978ad36e3/frozendict-2.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c64d34b802912ee6d107936e970b90750385a1fdfd38d310098b2918ba4cbf2", size = 120168, upload-time = "2025-11-11T22:37:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/25/c2/1536bc363dbce414e6b632f496aa8219c0db459a99eeafa02eba380e4cfa/frozendict-2.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:294a7d7d51dd979021a8691b46aedf9bd4a594ce3ed33a4bdf0a712d6929d712", size = 114997, upload-time = "2025-11-11T22:37:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/29/63/3e9efb490c00a0bf3c7bbf72fc73c90c4a6ebe30595e0fc44f59182b2ae7/frozendict-2.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f65d1b90e9ddc791ea82ef91a9ae0ab27ef6c0cfa88fadfa0e5ca5a22f8fa22f", size = 117292, upload-time = "2025-11-11T22:37:22.978Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/d25b1e94f9b0e64025d5cadc77b9b857737ebffd8963ee91de7c5a06415a/frozendict-2.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:82d5272d08451bcef6fb6235a0a04cf1816b6b6815cec76be5ace1de17e0c1a4", size = 110656, upload-time = "2025-11-11T22:38:37.652Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5d/0e7e3294e18bf41d38dbc9ee82539be607c8d26e763ae12d9e41f03f2dae/frozendict-2.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5943c3f683d3f32036f6ca975e920e383d85add1857eee547742de9c1f283716", size = 113225, upload-time = "2025-11-11T22:38:38.631Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fb/b72c9b261ac7a7803528aa63bba776face8ad8d39cc4ca4825ddaa7777a9/frozendict-2.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88c6bea948da03087035bb9ca9625305d70e084aa33f11e17048cb7dda4ca293", size = 126713, upload-time = "2025-11-11T22:38:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/e13af40bd9ef27b5c9ba10b0e31b03acac9468236b878dab030c75102a47/frozendict-2.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ffd1a9f9babec9119712e76a39397d8aa0d72ef8c4ccad917c6175d7e7f81b74", size = 114166, upload-time = "2025-11-11T22:38:41.073Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/435583b11f5332cd3eb479d0a67a87bc9247c8b094169b07bd8f0777fc48/frozendict-2.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0ff6f57854cc8aa8b30947ec005f9246d96e795a78b21441614e85d39b708822", size = 121542, upload-time = "2025-11-11T22:38:42.199Z" }, + { url = "https://files.pythonhosted.org/packages/38/25/097f3c0dc916d7c76f782cb65544e683ff3940a0ed997fc32efdb0989c45/frozendict-2.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d774df483c12d6cba896eb9a1337bbc5ad3f564eb18cfaaee3e95fb4402f2a86", size = 118610, upload-time = "2025-11-11T22:38:43.339Z" }, + { url = "https://files.pythonhosted.org/packages/61/d1/6964158524484d7f3410386ff27cbc8f33ef06f8d9ee0e188348efb9a139/frozendict-2.4.7-cp310-cp310-win32.whl", hash = "sha256:a10d38fa300f6bef230fae1fdb4bc98706b78c8a3a2f3140fde748469ef3cfe8", size = 34547, upload-time = "2025-11-11T22:38:44.327Z" }, + { url = "https://files.pythonhosted.org/packages/94/27/c22d614332c61ace4406542787edafaf7df533c6f02d1de8979d35492587/frozendict-2.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:dd518f300e5eb6a8827bee380f2e1a31c01dc0af069b13abdecd4e5769bd8a97", size = 37693, upload-time = "2025-11-11T22:38:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d8/9d6604357b1816586612e0e89bab6d8a9c029e95e199862dc99ce8ae2ed5/frozendict-2.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:3842cfc2d69df5b9978f2e881b7678a282dbdd6846b11b5159f910bc633cbe4f", size = 35563, upload-time = "2025-11-11T22:38:46.642Z" }, + { url = "https://files.pythonhosted.org/packages/38/74/f94141b38a51a553efef7f510fc213894161ae49b88bffd037f8d2a7cb2f/frozendict-2.4.7-py3-none-any.whl", hash = "sha256:972af65924ea25cf5b4d9326d549e69a9a4918d8a76a9d3a7cd174d98b237550", size = 16264, upload-time = "2025-11-11T22:40:12.836Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "ftfy" +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, +] + +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + +[[package]] +name = "futureproof" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/06/1ce98cf280d84b598e8b56b33ecef846ee5dec4c6e6bac353d0049a33379/futureproof-0.3.1.tar.gz", hash = "sha256:274365159bfd8b7aa39677abfa7b16c8253a771d72d3c3ee9de4a03c8d6d2d9b", size = 17627, upload-time = "2021-10-16T11:11:41.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2c/8996c6d29ec09de4815143476ad3df22dd878c1d937b74d7d78e8b1a2935/futureproof-0.3.1-py2.py3-none-any.whl", hash = "sha256:849d664cc201c39eb4503e1aa35d81b437e89e99cf34c3960ed106d7bbf6f154", size = 11273, upload-time = "2021-10-16T11:11:38.408Z" }, +] + +[[package]] +name = "fvcore" +version = "0.1.5.post20221221" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iopath" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tabulate" }, + { name = "termcolor" }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/93/d056a9c4efc6c79ba7b5159cc66bb436db93d2cc46dca18ed65c59cc8e4e/fvcore-0.1.5.post20221221.tar.gz", hash = "sha256:f2fb0bb90572ae651c11c78e20493ed19b2240550a7e4bbb2d6de87bdd037860", size = 50217, upload-time = "2022-12-21T08:10:53.563Z" } + +[[package]] +name = "gast" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, +] + +[[package]] +name = "gguf" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/26/7622a41c39db9d7090225a4bf8368550e59694dcf7313b44f9a82b501209/gguf-0.18.0.tar.gz", hash = "sha256:b4659093d5d0dccdb5902a904d54b327f4052879fe5e90946ad5fce9f8018c2e", size = 107170, upload-time = "2026-02-27T15:05:39.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0c/e0f1eae7535a97476fb903f65301e35da2a66182b8161066b7eb312b2cb8/gguf-0.18.0-py3-none-any.whl", hash = "sha256:af93f7ef198a265cbde5fa6a6b3101528bca285903949ab0a3e591cd993a1864", size = 114244, upload-time = "2026-02-27T15:05:37.991Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "glfw" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/72/642d4f12f61816ac96777f7360d413e3977a7dd08237d196f02da681b186/glfw-2.10.0.tar.gz", hash = "sha256:801e55d8581b34df9aa2cfea43feb06ff617576e2a8cc5dac23ee75b26d10abe", size = 31475, upload-time = "2025-09-12T08:54:38.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/1f/a9ce08b1173b0ab625ee92f0c47a5278b3e76fd367699880d8ee7d56c338/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:5f365a8c94bcea71ec91327e7c16e7cf739128479a18b8c1241b004b40acc412", size = 105329, upload-time = "2025-09-12T08:54:27.938Z" }, + { url = "https://files.pythonhosted.org/packages/7c/96/5a2220abcbd027eebcf8bedd28207a2de168899e51be13ba01ebdd4147a1/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:5328db1a92d07abd988730517ec02aa8390d3e6ef7ce98c8b57ecba2f43a39ba", size = 102179, upload-time = "2025-09-12T08:54:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/a5bd1d9e1808f400102bd7d328c4ac17b65fb2fc8014014ec6f23d02f662/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:312c4c1dd5509613ed6bc1e95a8dbb75a36b6dcc4120f50dc3892b40172e9053", size = 230039, upload-time = "2025-09-12T08:54:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/80/aa/3b503c448609dee6cb4e7138b4109338f0e65b97be107ab85562269d378d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:59c53387dc08c62e8bed86bbe3a8d53ab1b27161281ffa0e7f27b64284e2627c", size = 241984, upload-time = "2025-09-12T08:54:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2d/bfe39a42cad8e80b02bf5f7cae19ba67832c1810bbd3624a8e83153d74a4/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:c6f292fdaf3f9a99e598ede6582d21c523a6f51f8f5e66213849101a6bcdc699", size = 231052, upload-time = "2025-09-12T08:54:32.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/02/6e639e90f181dc9127046e00d0528f9f7ad12d428972e3a5378b9aefdb0b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:7916034efa867927892635733a3b6af8cd95ceb10566fd7f1e0d2763c2ee8b12", size = 243525, upload-time = "2025-09-12T08:54:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/cb588ca65561defe0fc48d1df4c2ac12569b81231ae4f2b52ab37007d0bd/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:6c9549da71b93e367b4d71438798daae1da2592039fd14204a80a1a2348ae127", size = 552685, upload-time = "2025-09-12T08:54:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/86/27/00c9c96af18ac0a5eac2ff61cbe306551a2d770d7173f396d0792ee1a59e/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:6292d5d6634d668cd23d337e6089491d3945a9aa4ac6e1667b0003520d7caa51", size = 559466, upload-time = "2025-09-12T08:54:37.661Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/de0b33f6f00687499ca1371f22aa73396341b85bf88f1a284f9da8842493/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_10_6_intel.whl", hash = "sha256:2aab89d2d9535635ba011fc7303390685169a1aa6731ad580d08d043524b8899", size = 105326, upload-time = "2026-01-28T05:57:56.083Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:23936202a107039b5372f0b88ae1d11080746aa1c78910a45d4a0c4cf408cfaa", size = 102180, upload-time = "2026-01-28T05:57:57.787Z" }, + { url = "https://files.pythonhosted.org/packages/58/19/d81b19e8261b9cb51b81d1402167791fef81088dfe91f0c4e9d136fdc5ca/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7be06d0838f61df67bd54cb6266a6193d54083acb3624ff3c3812a6358406fa4", size = 230038, upload-time = "2026-01-28T05:57:59.105Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/b035636cd82198b97b51a93efe9cfc4343d6b15cefbd336a3f2be871d848/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:91d36b3582a766512eff8e3b5dcc2d3ffcbf10b7cf448551085a08a10f1b8244", size = 241983, upload-time = "2026-01-28T05:58:00.352Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl", hash = "sha256:27c9e9a2d5e1dc3c9e3996171d844d9df9a5a101e797cb94cce217b7afcf8fd9", size = 231053, upload-time = "2026-01-28T05:58:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl", hash = "sha256:ce6724bb7cb3d0543dcba17206dce909f94176e68220b8eafee72e9f92bcf542", size = 243522, upload-time = "2026-01-28T05:58:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b9/b04c3aa0aad2870cfe799f32f8b59789c98e1816bbce9e83f4823c5b840b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win32.whl", hash = "sha256:fca724a21a372731edb290841edd28a9fb1ee490f833392752844ac807c0086a", size = 552682, upload-time = "2026-01-28T05:58:05.649Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:823c0bd7770977d4b10e0ed0aef2f3682276b7c88b8b65cfc540afce5951392f", size = 559464, upload-time = "2026-01-28T05:58:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a8/d4dab8a58fc2e6981fc7a58c4e56ba9d777fb24931cec6a22152edbb3540/glfw-2.10.0-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:a0d1f29f206219cc291edfb6cace663a86da2470632551c998e3db82d48ea177", size = 105288, upload-time = "2026-03-10T17:21:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/14/61/68d35e001872a7705112418da236fa2418d4f2e5419f8b2837f9b81bb3da/glfw-2.10.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d28d6f3ef217e64e35dc6fd0a7acb4cec9bfe7cd14dd9b35a7228a87002de154", size = 102139, upload-time = "2026-03-10T17:21:21.645Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/ca5984081aaae07c9d371cb11dc4e4ff603510678ed9b73e58b6c351fe63/glfw-2.10.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:f968b522bb6a0e04aaf4dcac30a476d7229308bb2bac406a60587debb5a61e29", size = 229998, upload-time = "2026-03-10T17:21:23.549Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c4/82ac75fdcfba2896da7a573c0fc7f8ceb8f77ead6866d500d06c32f1c464/glfw-2.10.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:68cf3752bdadb6f4bc0a876247c28c88c7251ac39f8af076ed938fdfd71e72dd", size = 241944, upload-time = "2026-03-10T17:21:26.102Z" }, + { url = "https://files.pythonhosted.org/packages/e3/96/9f691823cca5eb6a08f346bd0ff03b78032db9370b509a1e9c8976fb20a5/glfw-2.10.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:44d98de5dbf8f727e0cb29f9b29d29528ea7570f2e6f42f8430a69df05f12b48", size = 231009, upload-time = "2026-03-10T17:21:28.481Z" }, + { url = "https://files.pythonhosted.org/packages/3f/93/977b9e679e356871d428ae7a1139ec767dd5177bed58a6344b4d2199e00f/glfw-2.10.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cca5158d62189e08792b1ae54f92307a282921a0e7783315b467e21b0a381c88", size = 243480, upload-time = "2026-03-10T17:21:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bd/cea9569c8f2188b0a104472951420434a3e1f5cf26f5836ef9d7227a1a30/glfw-2.10.0-py2.py3-none-win32.whl", hash = "sha256:5e024509989740e8e7b86cc4aab508195495f79879072b0e1f68bd036a2916ad", size = 552641, upload-time = "2026-03-10T17:21:32.653Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9b/4366ad3e1c0688146c70aa6143584d6a8d88583b9390f106250e25a3d5cd/glfw-2.10.0-py2.py3-none-win_amd64.whl", hash = "sha256:7f787ee8645781f10e8800438ce4357ab38c573ffb191aba380c1e72eba6311c", size = 559423, upload-time = "2026-03-10T17:21:34.766Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/b1/4f0798e88285b50dfc60ed3a7de071def538b358db2da468c2e0deecbb40/google_cloud_storage-3.9.0.tar.gz", hash = "sha256:f2d8ca7db2f652be757e92573b2196e10fbc09649b5c016f8b422ad593c641cc", size = 17298544, upload-time = "2026-02-02T13:36:34.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0b/816a6ae3c9fd096937d2e5f9670558908811d57d59ddf69dd4b83b326fd1/google_cloud_storage-3.9.0-py3-none-any.whl", hash = "sha256:2dce75a9e8b3387078cbbdad44757d410ecdb916101f8ba308abf202b6968066", size = 321324, upload-time = "2026-02-02T13:36:32.271Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, + { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "gradio" +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/25/68322ca161fb0f49714ea721529140ad1d4cd28804cd933ca28f9fa50899/gradio-6.8.0.tar.gz", hash = "sha256:79154e7b77ff2deb4570e94eba9a900b7b9495121e24376c2f6cfa30fee1b92d", size = 40122652, upload-time = "2026-02-27T14:42:00.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3c/1568ee82f0e4308612aaf964168e93b7f2adb64c3e77b7877fd397e773bf/gradio-6.8.0-py3-none-any.whl", hash = "sha256:1ece9f3d075bec3da5a6e69c77fa03ddcf3b14af56a9ca843a499406c2b3afd6", size = 24162460, upload-time = "2026-02-27T14:41:57.656Z" }, +] + +[[package]] +name = "gradio-client" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/89/52722478fb42f650bacb198b525c7ce5818c95442b2872ef913dc1ab6b60/gradio_client-2.2.0.tar.gz", hash = "sha256:f25a98cd902b44ab77222f3bd752de67dd9dd9723ab8453383d78e5bc1c69497", size = 57378, upload-time = "2026-02-26T14:49:28.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/49/86b919727b5d6574528d5d9e67c6455bb707a3654abc87d61d7f1d742395/gradio_client-2.2.0-py3-none-any.whl", hash = "sha256:090f72106b41ba17dcf1b437db54a7bba4c406244f8b01210d5bf24b2af6a85d", size = 58333, upload-time = "2026-02-26T14:49:26.832Z" }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "gym" +version = "0.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "gym-notices" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b1/eb05a423eb801ab7d0715d6a3b28d92589e30b437052553df19ca2087240/gym-0.26.2.tar.gz", hash = "sha256:e0d882f4b54f0c65f203104c24ab8a38b039f1289986803c7d02cdbe214fbcc4", size = 721689, upload-time = "2022-10-04T23:57:43.247Z" } + +[[package]] +name = "gym-notices" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/4d/035922b950b224ee4b65a9a4550a22eac8985a3f0e1ef42546d9047e7a72/gym_notices-0.1.0.tar.gz", hash = "sha256:9f9477ef68a8c15e42625d4fa53631237e3e6ae947f325b5c149c081499adc1b", size = 3084, upload-time = "2025-07-27T10:12:41.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/55/55d157aa8693090954fc9639bf27218240517c3bc7afa6e97412da6ebfd9/gym_notices-0.1.0-py3-none-any.whl", hash = "sha256:a943af4446cb619d04fd1e470b9272b4473e08a06d1c7cc9005755a4a0b8c905", size = 3349, upload-time = "2025-07-27T10:12:40.039Z" }, +] + +[[package]] +name = "gymnasium" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "farama-notifications" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/ff/14b6880d703dfaca204490979d3254ccd280c99550798993319902873658/gymnasium-1.3.0.tar.gz", hash = "sha256:6939e86e835d6b71b6ba6bfd360487420876deafc79bfb7bacba83a7c446bcf3", size = 830646, upload-time = "2026-04-22T13:47:14.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/73/fda6a25f3beeb5e49d74330b44092b9e5a547395ccd478d1103ddcbff1fc/gymnasium-1.3.0-py3-none-any.whl", hash = "sha256:6b8c159a8540dcbcb221722d7efda24d78ebbcbc3bd2ea1c2611aa2a34471fc2", size = 953904, upload-time = "2026-04-22T13:47:12.13Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" }, + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "hatch" +version = "1.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-zstd", marker = "python_full_version < '3.14' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "pyproject-hooks" }, + { name = "python-discovery" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/02/ce9c4c439fa3f195b21b4b5bb18b44d1076297c86477ef7e3d2de6064ec3/hatch-1.16.5.tar.gz", hash = "sha256:57bdeeaa72577859ce37091a5449583875331c06f9cb6af9077947ad40b3a1de", size = 5220741, upload-time = "2026-02-27T18:45:31.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/8a/11ae7e271870f0ad8fa0012e4265982bebe0fdc21766b161fb8b8fc3aefc/hatch-1.16.5-py3-none-any.whl", hash = "sha256:d9b8047f2cd10d3349eb6e8f278ad728a04f91495aace305c257d5c2747188fb", size = 141269, upload-time = "2026-02-27T18:45:29.573Z" }, +] + +[[package]] +name = "hatchling" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, +] + +[[package]] +name = "hf-egl-probe" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/3e/ffad88145b342d5a972711d0b47e202f2e51c9a436d9b6bdd55f10ed893e/hf_egl_probe-1.0.2.tar.gz", hash = "sha256:0cbc6352236f5c14f681328e2e0d2cb4d101828c81aa58793dbc9a91feee5e93", size = 217668, upload-time = "2025-11-03T15:58:15.592Z" } + +[[package]] +name = "hf-xet" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[[package]] +name = "hvac" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/57/b46c397fb3842cfb02a44609aa834c887f38dd75f290c2fc5a34da4b2fee/hvac-2.4.0.tar.gz", hash = "sha256:e0056ad9064e7923e874e6769015b032580b639e29246f5ab1044f7959c1c7e0", size = 332543, upload-time = "2025-10-30T12:57:47.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/33/71e45a6bd6875f44a26f99da31c63b6840123e88bedf2c0b1ce429b8be12/hvac-2.4.0-py3-none-any.whl", hash = "sha256:008db5efd8c2f77bd37d2368ea5f713edceae1c65f11fd608393179478649e0f", size = 155921, upload-time = "2025-10-30T12:57:46.253Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ijson" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/32/21c1b47a1afb7319944d0b9685c0997a9d574a77b030c82f6a1ac2cef4eb/ijson-3.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea8dcac10d86adaeead454bc25c97b68d0bda573d5fd6f86f5e21cf8f7906f88", size = 88935, upload-time = "2026-02-24T03:56:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/6ac7ebbb3cd767c87cdcbb950a6754afd1c0977756347bfe03eb8e5b866d/ijson-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92b0495bbb2150bbf14fc5d98fb6d76bcd1c526605a172709e602e6fedc96495", size = 60567, upload-time = "2026-02-24T03:56:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/c4/98/1140de9ae872468a8bc2e87c171228e25e58b1eb696b7fb430f7590fea44/ijson-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af0c4c8943be8b09a4e57bdc1da6001dae7b36526d4154fe5c8224738d0921f", size = 60620, upload-time = "2026-02-24T03:56:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/60/e1/67dfe0774e4c7ca6ec8702e280e8764d356f3db54358999818cda6df7679/ijson-3.5.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45887d5e84ff0d2b138c926cebd9071830733968afe8d9d12080b3c178c7f918", size = 126558, upload-time = "2026-02-24T03:56:43.922Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ef/23d614fc773d428caeb6e197218b7e32adcc668ff5b98777039149571208/ijson-3.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a70b575be8e57a28c80e90ed349ad3a851c3478524c70e36e07d6092ecd12c9", size = 133091, upload-time = "2026-02-24T03:56:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b8/80/99727603cd8a1d32edafa4392f4056b2420bf48c15afd34481c68a2d4435/ijson-3.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2adeecd45830bfd5580ca79a584154713aabef0b9607e16249133df5d2859813", size = 130249, upload-time = "2026-02-24T03:56:46.333Z" }, + { url = "https://files.pythonhosted.org/packages/0b/94/3a3d623ca80768e834be8a834ef05960e3b9e79af1a911704ff10c9e8792/ijson-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d873e72889e7fc5962ab58909f1adff338d7c2f49e450e5b5fe844eff8155a14", size = 133501, upload-time = "2026-02-24T03:56:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f6/df2c14ad340834eccee379046f155e4b66a16ddafd445429dee7b3323614/ijson-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9a88c559456a79708592234d697645d92b599718f4cbbeaa6515f83ac63ca0ae", size = 128438, upload-time = "2026-02-24T03:56:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/9ff5b8b5fee113f5607bc4149b707382a898eeb545153189b075e5ec8d59/ijson-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf83f58ad50dc0d39a2105cb26d4f359b38f42cef68b913170d4d47d97d97ba5", size = 131116, upload-time = "2026-02-24T03:56:49.737Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/954ce0d440d7cf72a3d8361b14406f9cdbf624b1625c10f8488857c769d6/ijson-3.5.0-cp310-cp310-win32.whl", hash = "sha256:aec4580a7712a19b1f95cd41bed260fc6a31266d37ef941827772a4c199e8143", size = 52724, upload-time = "2026-02-24T03:56:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/24/33/ece87d60502c6115642cbabeb8c122fa982212b392bc4f4ff5aab8e02dac/ijson-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a9c4c70501e23e8eb1675330686d1598eebfa14b6f0dbc8f00c2e081cc628fa", size = 55125, upload-time = "2026-02-24T03:56:51.942Z" }, + { url = "https://files.pythonhosted.org/packages/65/da/644343198abca5e0f6e2486063f8d8f3c443ca0ef5e5c890e51ef6032e33/ijson-3.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5616311404b858d32740b7ad8b9a799c62165f5ecb85d0a8ed16c21665a90533", size = 88964, upload-time = "2026-02-24T03:56:53.099Z" }, + { url = "https://files.pythonhosted.org/packages/5b/63/8621190aa2baf96156dfd4c632b6aa9f1464411e50b98750c09acc0505ea/ijson-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9733f94029dd41702d573ef64752e2556e72aea14623d6dbb7a44ca1ccf30fd", size = 60582, upload-time = "2026-02-24T03:56:54.261Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/6a3f041fdd17dacff33b7d7d3ba3df6dca48740108340c6042f974b2ad20/ijson-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db8398c6721b98412a4f618da8022550c8b9c5d9214040646071b5deb4d4a393", size = 60632, upload-time = "2026-02-24T03:56:55.159Z" }, + { url = "https://files.pythonhosted.org/packages/e4/68/474541998abbdecfd46a744536878335de89aceb9f085bff1aaf35575ceb/ijson-3.5.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c061314845c08163b1784b6076ea5f075372461a32e6916f4e5f211fd4130b64", size = 131988, upload-time = "2026-02-24T03:56:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/e05ff8b72a44fe9d192f41c5dcbc35cfa87efc280cdbfe539ffaf4a7535e/ijson-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1111a1c5ac79119c5d6e836f900c1a53844b50a18af38311baa6bb61e2645aca", size = 138669, upload-time = "2026-02-24T03:56:57.555Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/955a83b031102c7a602e2c06d03aff0a0e584212f09edb94ccc754d203ac/ijson-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e74aff8c681c24002b61b1822f9511d4c384f324f7dbc08c78538e01fdc9fcb", size = 135093, upload-time = "2026-02-24T03:56:59.267Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f2/30250cfcb4d2766669b31f6732689aab2bb91de426a15a3ebe482df7ee48/ijson-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:739a7229b1b0cc5f7e2785a6e7a5fc915e850d3fed9588d0e89a09f88a417253", size = 138715, upload-time = "2026-02-24T03:57:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/a2/05/785a145d7e75e04e04480d59b6323cd4b1d9013a6cd8643fa635fbc93490/ijson-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ef88712160360cab3ca6471a4e5418243f8b267cf1fe1620879d1b5558babc71", size = 133194, upload-time = "2026-02-24T03:57:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/eb/80d6f8a748dead4034cea0939494a67d10ccf88d6413bf6e860393139676/ijson-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ca0d1b6b5f8166a6248f4309497585fb8553b04bc8179a0260fad636cfdb798", size = 135588, upload-time = "2026-02-24T03:57:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/bbc21f9400ebdbca48fab272593e0d1f875691be1e927d264d90d48b8c47/ijson-3.5.0-cp311-cp311-win32.whl", hash = "sha256:966039cf9047c7967febf7b9a52ec6f38f5464a4c7fbb5565e0224b7376fefff", size = 52721, upload-time = "2026-02-24T03:57:04.365Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2e/4e8c0208b8f920ee80c88c956f93e78318f2cfb646455353b182738b490c/ijson-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bad6a1634cb7c9f3f4c7e52325283b35b565f5b6cc27d42660c6912ce883422", size = 55121, upload-time = "2026-02-24T03:57:05.498Z" }, + { url = "https://files.pythonhosted.org/packages/aa/17/9c63c7688025f3a8c47ea717b8306649c8c7244e49e20a2be4e3515dc75c/ijson-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ebefbe149a6106cc848a3eaf536af51a9b5ccc9082de801389f152dba6ab755", size = 88536, upload-time = "2026-02-24T03:57:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/e15c2400244c117b06585452ebc63ae254f5a6964f712306afd1422daae0/ijson-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e30d9f00f82e64de689c0b8651b9cfed879c184b139d7e1ea5030cec401c21", size = 60499, upload-time = "2026-02-24T03:57:09.155Z" }, + { url = "https://files.pythonhosted.org/packages/77/a9/bf4fe3538a0c965f16b406f180a06105b875da83f0743e36246be64ef550/ijson-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a04a33ee78a6f27b9b8528c1ca3c207b1df3b8b867a4cf2fcc4109986f35c227", size = 60330, upload-time = "2026-02-24T03:57:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/a7254a065933c0e2ffd3586f46187d84830d3d7b6f41cfa5901820a4f87d/ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c", size = 53079, upload-time = "2026-02-24T03:57:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/2edca79b359fc9f95d774616867a03ecccdf333797baf5b3eea79733918c/ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944", size = 55500, upload-time = "2026-02-24T03:57:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/d67e764a712c3590627480643a3b51efcc3afa4ef3cb54ee4c989073c97e/ijson-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e9cedc10e40dd6023c351ed8bfc7dcfce58204f15c321c3c1546b9c7b12562a4", size = 88544, upload-time = "2026-02-24T03:57:21.293Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/f1c299371686153fa3cf5c0736b96247a87a1bee1b7145e6d21f359c505a/ijson-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3647649f782ee06c97490b43680371186651f3f69bebe64c6083ee7615d185e5", size = 60495, upload-time = "2026-02-24T03:57:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/16/94/b1438e204d75e01541bebe3e668fe3e68612d210e9931ae1611062dd0a56/ijson-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90e74be1dce05fce73451c62d1118671f78f47c9f6be3991c82b91063bf01fc9", size = 60325, upload-time = "2026-02-24T03:57:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/30/e2/4aa9c116fa86cc8b0f574f3c3a47409edc1cd4face05d0e589a5a176b05d/ijson-3.5.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78e9ad73e7be2dd80627504bd5cbf512348c55ce2c06e362ed7683b5220e8568", size = 138774, upload-time = "2026-02-24T03:57:24.683Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/738b88752a70c3be1505faa4dcd7110668c2712e582a6a36488ed1e295d4/ijson-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9577449313cc94be89a4fe4b3e716c65f09cc19636d5a6b2861c4e80dddebd58", size = 149820, upload-time = "2026-02-24T03:57:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/0b3ab9f393ca8f72ea03bc896ba9fdc987e90ae08cdb51c32a4ee0c14d5e/ijson-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e4c1178fb50aff5f5701a30a5152ead82a14e189ce0f6102fa1b5f10b2f54ff", size = 149747, upload-time = "2026-02-24T03:57:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/b0037119f75131b78cb00acc2657b1a9d0435475f1f2c5f8f5a170b66b9c/ijson-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0eb402ab026ffb37a918d75af2b7260fe6cfbce13232cc83728a714dd30bd81d", size = 151027, upload-time = "2026-02-24T03:57:28.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/a0/cb344de1862bf09d8f769c9d25c944078c87dd59a1b496feec5ad96309a4/ijson-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b08ee08355f9f729612a8eb9bf69cc14f9310c3b2a487c6f1c3c65d85216ec4", size = 142996, upload-time = "2026-02-24T03:57:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/ca/32/a8ffd67182e02ea61f70f62daf43ded4fa8a830a2520a851d2782460aba8/ijson-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bda62b6d48442903e7bf56152108afb7f0f1293c2b9bef2f2c369defea76ab18", size = 152068, upload-time = "2026-02-24T03:57:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/3578df8e75d446aab0ae92e27f641341f586b85e1988536adebc65300cb4/ijson-3.5.0-cp313-cp313-win32.whl", hash = "sha256:8d073d9b13574cfa11083cc7267c238b7a6ed563c2661e79192da4a25f09c82c", size = 53065, upload-time = "2026-02-24T03:57:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a2/f7cdaf5896710da3e69e982e44f015a83d168aa0f3a89b6f074b5426779d/ijson-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:2419f9e32e0968a876b04d8f26aeac042abd16f582810b576936bbc4c6015069", size = 55499, upload-time = "2026-02-24T03:57:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/13e2492d17e19a2084523e18716dc2809159f2287fd2700c735f311e76c4/ijson-3.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4d4b0cd676b8c842f7648c1a783448fac5cd3b98289abd83711b3e275e143524", size = 93019, upload-time = "2026-02-24T03:57:33.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/483fc97ece0c3f1cecabf48f6a7a36e89d19369eec462faaeaa34c788992/ijson-3.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:252dec3680a48bb82d475e36b4ae1b3a9d7eb690b951bb98a76c5fe519e30188", size = 62714, upload-time = "2026-02-24T03:57:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/793fe020a0fe9d9eed4c285cf4a5cfdb0a935708b3bde0d72f35c794b513/ijson-3.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:aa1b5dca97d323931fde2501172337384c958914d81a9dac7f00f0d4bfc76bc7", size = 62460, upload-time = "2026-02-24T03:57:35.874Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/f1a2690aa8d4df1f4e262b385e65a933ffdc250b091531bac9a449c19e16/ijson-3.5.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7a5ec7fd86d606094bba6f6f8f87494897102fa4584ef653f3005c51a784c320", size = 199273, upload-time = "2026-02-24T03:57:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/f1346d5299e79b988ab472dc773d5381ec2d57c23cb2f1af3ede4a810e62/ijson-3.5.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:009f41443e1521847701c6d87fa3923c0b1961be3c7e7de90947c8cb92ea7c44", size = 216884, upload-time = "2026-02-24T03:57:38.346Z" }, + { url = "https://files.pythonhosted.org/packages/28/3c/8b637e869be87799e6c2c3c275a30a546f086b1aed77e2b7f11512168c5a/ijson-3.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4c3651d1f9fe2839a93fdf8fd1d5ca3a54975349894249f3b1b572bcc4bd577", size = 207306, upload-time = "2026-02-24T03:57:39.718Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/18b1c1df6951ca056782d7580ec40cea4ff9a27a0947d92640d1cc8c4ae3/ijson-3.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:945b7abcfcfeae2cde17d8d900870f03536494245dda7ad4f8d056faa303256c", size = 211364, upload-time = "2026-02-24T03:57:40.953Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/e795812e82851574a9dba8a53fde045378f531ef14110c6fb55dbd23b443/ijson-3.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0574b0a841ff97495c13e9d7260fbf3d85358b061f540c52a123db9dbbaa2ed6", size = 200608, upload-time = "2026-02-24T03:57:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/013c85b4749b57a4cb4c2670014d1b32b8db4ab1a7be92ea7aeb5d7fe7b5/ijson-3.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f969ffb2b89c5cdf686652d7fb66252bc72126fa54d416317411497276056a18", size = 205127, upload-time = "2026-02-24T03:57:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7c/faf643733e3ab677f180018f6a855c4ef70b7c46540987424c563c959e42/ijson-3.5.0-cp313-cp313t-win32.whl", hash = "sha256:59d3f9f46deed1332ad669518b8099920512a78bda64c1f021fcd2aff2b36693", size = 55282, upload-time = "2026-02-24T03:57:44.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/22/94ddb47c24b491377aca06cd8fc9202cad6ab50619842457d2beefde21ea/ijson-3.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c2839fa233746d8aad3b8cd2354e441613f5df66d721d59da4a09394bd1db2b", size = 58016, upload-time = "2026-02-24T03:57:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/7a/93/0868efe753dc1df80cc405cf0c1f2527a6991643607c741bff8dcb899b3b/ijson-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25a5a6b2045c90bb83061df27cfa43572afa43ba9408611d7bfe237c20a731a9", size = 89094, upload-time = "2026-02-24T03:57:46.115Z" }, + { url = "https://files.pythonhosted.org/packages/24/94/fd5a832a0df52ef5e4e740f14ac8640725d61034a1b0c561e8b5fb424706/ijson-3.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8976c54c0b864bc82b951bae06567566ac77ef63b90a773a69cd73aab47f4f4f", size = 60715, upload-time = "2026-02-24T03:57:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/1b9a90af5732491f9eec751ee211b86b11011e1158c555c06576d52c3919/ijson-3.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:859eb2038f7f1b0664df4241957694cc35e6295992d71c98659b22c69b3cbc10", size = 60638, upload-time = "2026-02-24T03:57:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/2c551ea980fe56f68710a8d5389cfbd015fc45aaafd17c3c52c346db6aa1/ijson-3.5.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c911aa02991c7c0d3639b6619b93a93210ff1e7f58bf7225d613abea10adc78e", size = 140667, upload-time = "2026-02-24T03:57:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/27b887879ba6a5bc29766e3c5af4942638c952220fd63e1e442674f7883a/ijson-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903cbdc350173605220edc19796fbea9b2203c8b3951fb7335abfa8ed37afda8", size = 149850, upload-time = "2026-02-24T03:57:50.329Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/23e10e1bc04bf31193b21e2960dce14b17dbd5d0c62204e8401c59d62c08/ijson-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4549d96ded5b8efa71639b2160235415f6bdb8c83367615e2dbabcb72755c33", size = 149206, upload-time = "2026-02-24T03:57:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/8e/90/e552f6495063b235cf7fa2c592f6597c057077195e517b842a0374fd470c/ijson-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b2dcf6349e6042d83f3f8c39ce84823cf7577eba25bac5aae5e39bbbbbe9c1c", size = 150438, upload-time = "2026-02-24T03:57:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/5c/18/45bf8f297c41b42a1c231d261141097babd953d2c28a07be57ae4c3a1a02/ijson-3.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e44af39e6f8a17e5627dcd89715d8279bf3474153ff99aae031a936e5c5572e5", size = 144369, upload-time = "2026-02-24T03:57:53.22Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/deb9772bb2c0cead7ad64f00c3598eec9072bdf511818e70e2c512eeabbe/ijson-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9260332304b7e7828db56d43f08fc970a3ab741bf84ff10189361ea1b60c395b", size = 151352, upload-time = "2026-02-24T03:57:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/67f4d80cd58ad7eab0cd1af5fe28b961886338956b2f88c0979e21914346/ijson-3.5.0-cp314-cp314-win32.whl", hash = "sha256:63bc8121bb422f6969ced270173a3fa692c29d4ae30c860a2309941abd81012a", size = 53610, upload-time = "2026-02-24T03:57:55.655Z" }, + { url = "https://files.pythonhosted.org/packages/70/d3/263672ea22983ba3940f1534316dbc9200952c1c2a2332d7a664e4eaa7ae/ijson-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:01b6dad72b7b7df225ef970d334556dfad46c696a2c6767fb5d9ed8889728bca", size = 56301, upload-time = "2026-02-24T03:57:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/86f7fac35e0835faa188085ae0579e813493d5261ce056484015ad533445/ijson-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2ea4b676ec98e374c1df400a47929859e4fa1239274339024df4716e802aa7e4", size = 93069, upload-time = "2026-02-24T03:57:57.849Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/e7366ed9c6e60228d35baf4404bac01a126e7775ea8ce57f560125ed190a/ijson-3.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:014586eec043e23c80be9a923c56c3a0920a0f1f7d17478ce7bc20ba443968ef", size = 62767, upload-time = "2026-02-24T03:57:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/3e703e8cc4b3ada79f13b28070b51d9550c578f76d1968657905857b2ddd/ijson-3.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5b8b886b0248652d437f66e7c5ac318bbdcb2c7137a7e5327a68ca00b286f5f", size = 62467, upload-time = "2026-02-24T03:58:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/0c91af32c1ee8a957fdac2e051b5780756d05fd34e4b60d94a08d51bac1d/ijson-3.5.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:498fd46ae2349297e43acf97cdc421e711dbd7198418677259393d2acdc62d78", size = 200447, upload-time = "2026-02-24T03:58:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/796ea0e391b7e2d45c5b1b451734bba03f81c2984cf955ea5eaa6c4920ad/ijson-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a51b4f9b81f12793731cf226266d1de2112c3c04ba4a04117ad4e466897e05", size = 217820, upload-time = "2026-02-24T03:58:02.598Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9636c710dc4ac4a281baa266a64f323b4cc165cec26836af702c44328b59a515", size = 208310, upload-time = "2026-02-24T03:58:04.759Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ad/8b3105a78774fd4a65e534a21d975ef3a77e189489fe3029ebcaeba5e243/ijson-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7168a39e8211107666d71b25693fd1b2bac0b33735ef744114c403c6cac21e1", size = 211843, upload-time = "2026-02-24T03:58:05.836Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/a2739f6072d6e1160581bc3ed32da614c8cced023dcd519d9c5fa66e0425/ijson-3.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8696454245415bc617ab03b0dc3ae4c86987df5dc6a90bad378fe72c5409d89e", size = 200906, upload-time = "2026-02-24T03:58:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5e/e06c2de3c3d4a9cfb655c1ad08a68fb72838d271072cdd3196576ac4431a/ijson-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c21bfb61f71f191565885bf1bc29e0a186292d866b4880637b833848360bdc1b", size = 205495, upload-time = "2026-02-24T03:58:09.163Z" }, + { url = "https://files.pythonhosted.org/packages/7c/11/778201eb2e202ddd76b36b0fb29bf3d8e3c167389d8aa883c62524e49f47/ijson-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:a2619460d6795b70d0155e5bf016200ac8a63ab5397aa33588bb02b6c21759e6", size = 56280, upload-time = "2026-02-24T03:58:10.116Z" }, + { url = "https://files.pythonhosted.org/packages/23/28/96711503245339084c8086b892c47415895eba49782d6cc52d9f4ee50301/ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854", size = 58965, upload-time = "2026-02-24T03:58:11.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3b/d31ecfa63a218978617446159f3d77aab2417a5bd2885c425b176353ff78/ijson-3.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d64c624da0e9d692d6eb0ff63a79656b59d76bf80773a17c5b0f835e4e8ef627", size = 57715, upload-time = "2026-02-24T03:58:24.545Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/b170e646d378e8cccf9637c05edb5419b00c2c4df64b0258c3af5355608e/ijson-3.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:876f7df73b7e0d6474f9caa729b9cdbfc8e76de9075a4887dfd689e29e85c4ca", size = 57205, upload-time = "2026-02-24T03:58:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/ef/83/44dbd0231b0a8c6c14d27473d10c4e27dfbce7d5d9a833c79e3e6c33eb40/ijson-3.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e7dbff2c8d9027809b0cde663df44f3210da10ea377121d42896fb6ee405dd31", size = 71229, upload-time = "2026-02-24T03:58:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/cf84048b7c6cec888826e696a31f45bee7ebcac15e532b6be1fc4c2c9608/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4217a1edc278660679e1197c83a1a2a2d367792bfbb2a3279577f4b59b93730d", size = 71217, upload-time = "2026-02-24T03:58:28.021Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0a/e34c729a87ff67dc6540f6bcc896626158e691d433ab57db0086d73decd2/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04f0fc740311388ee745ba55a12292b722d6f52000b11acbb913982ba5fbdf87", size = 68618, upload-time = "2026-02-24T03:58:28.918Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0f/e849d072f2e0afe49627de3995fc9dae54b4c804c70c0840f928d95c10e1/ijson-3.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fdeee6957f92e0c114f65c55cf8fe7eabb80cfacab64eea6864060913173f66d", size = 55369, upload-time = "2026-02-24T03:58:29.839Z" }, +] + +[[package]] +name = "imagecodecs" +version = "2025.3.30" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/bf/81c848ffe2b42fc141b6db3e4e8e650183b7aab8c4535498ebff25740a3b/imagecodecs-2025.3.30.tar.gz", hash = "sha256:29256f44a7fcfb8f235a3e9b3bae72b06ea2112e63bcc892267a8c01b7097f90", size = 9506573, upload-time = "2025-03-30T04:44:50.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/0a/d9418201f0372deacc394e129c42a11b253e79f81ee1d3b5141315a9aa51/imagecodecs-2025.3.30-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:b5c9be23ccc7fd8aee233db5d714e61be2fc85acd77305290eb86ddb36d643b6", size = 17936592, upload-time = "2025-03-30T04:42:50.413Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/1d5ee18476e763ad32555fe3cca7e55af3912f21357cdd18488dead7d34d/imagecodecs-2025.3.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7870308a908f1e1748c3b9e113a5e3e56878426e8cb7a173c98557d5db7776ea", size = 15046409, upload-time = "2025-03-30T04:42:53.976Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/b7b329905006f1b3627e1f531de8ab36bd544fa3d6136576c19f9d90de84/imagecodecs-2025.3.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c62f210f7e1c152306fa5efec0a172680932d1beb7e06d8a8dd039e718bdeb1", size = 41997395, upload-time = "2025-03-30T04:42:58.842Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/214e5f157979e55d57f4a4816659004b99b7ab3b0b7a5f3a950b8cb2ef53/imagecodecs-2025.3.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b5959cdd42debac19e6635ee3dadbb3d6db0d7be2fbf5f763484d4c21363e3", size = 43372901, upload-time = "2025-03-30T04:43:04.455Z" }, + { url = "https://files.pythonhosted.org/packages/0d/13/4fd766723152c1a453134b32276019f18cdd2156ef7127f216d8dcd26834/imagecodecs-2025.3.30-cp310-cp310-win32.whl", hash = "sha256:4cdcef20630c156d91981215dc56549520c431c99b996d685fdfb3c79c913432", size = 24134766, upload-time = "2025-03-30T04:43:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4c/4f825eabaa350a5fc55035ea6e769b9928196aac133f0bddb30a199ea0b4/imagecodecs-2025.3.30-cp310-cp310-win_amd64.whl", hash = "sha256:e09556e03c9048852e6b8e74f569c545cda20f8d4f0e466f61ac64246fa4994e", size = 28873864, upload-time = "2025-03-30T04:43:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/8c475f73685e864c3742dc38596e3a2b897006402199f42905a09d05395d/imagecodecs-2025.3.30-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:7e0afe1a05a942391abd7d1f25722a07de05d9d12eb6f3ca1ef48e0719c6796a", size = 17946410, upload-time = "2025-03-30T04:43:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/6b/81/cd6df5a61c85a5f227a3e0b242ad7a04192f8f5dd8b0f65308872e618dbb/imagecodecs-2025.3.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44dc270d78b7cda29e2d430acbd8dab66322766412e596f450871e2831148aa2", size = 15050151, upload-time = "2025-03-30T04:43:20.36Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/929ad2025a60e5cfda80330749d6b44ff7a5e1ccf457d998e0e622010881/imagecodecs-2025.3.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cee56331d9a700e9ec518caeba6d9813ffd7c042f1fae47d2dafcdfc259d2a5", size = 44161549, upload-time = "2025-03-30T04:43:25.322Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e4/23f8d23822b1fab85edc2b11ee9af7dffc5325e57fc1c05fbd8ba64b67b8/imagecodecs-2025.3.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e354fa2046bb7029d0a1ff15a8bb31487ca0d479cd42fdb5c312bcd9408ce3fc", size = 45559360, upload-time = "2025-03-30T04:43:31.614Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/4148e036c1f4d4a56aa437dfccf1d1e38ade691242ae4fb1ed6c75198984/imagecodecs-2025.3.30-cp311-cp311-win_amd64.whl", hash = "sha256:7debc7231780d8e44ffcd13aee2178644d93115c19ff73c96cf3068b219ac3a2", size = 28881661, upload-time = "2025-03-30T04:43:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/bd/65/52c9ed63fe3ef0601775d3469b495eadf00174ac0f38d9499871866a5e3b/imagecodecs-2025.3.30-cp311-cp311-win_arm64.whl", hash = "sha256:2b5c1c02c70da9561da9b728b97599b3ed0ef7d5399979017ce90029f522587b", size = 23780188, upload-time = "2025-03-30T04:43:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/07/a8/8d5e87c271ad56076d5d41b29a72bde06f9c576796f658f84be1d704c440/imagecodecs-2025.3.30-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:dad3f0fc39eb9a88cecb2ccfe0e13eac35b21da36c0171285e4b289b12085235", size = 17999942, upload-time = "2025-03-30T04:43:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a1/5781188860b9f77ba56743ca70c770bad3500980f6a0be0ead28bfd69679/imagecodecs-2025.3.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2806b6e605e674d7e3d21099779a88cb30b9da4807a88e0f02da3ea249085e5f", size = 15088408, upload-time = "2025-03-30T04:43:52.644Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d6/7dea5c27b5e14746095f3e01a4d5ee4a3e0dbfc534b978675cfd6bbd5270/imagecodecs-2025.3.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abfb2231f4741262c91f3e77af85ce1f35b7d44f71414c5d1ba6008cfc3e5672", size = 43661256, upload-time = "2025-03-30T04:43:57.461Z" }, + { url = "https://files.pythonhosted.org/packages/20/ad/f751aed397ad9ba002ace15c028c5261c9dd57e0b366e8642e574332f318/imagecodecs-2025.3.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6583fdcac9a4cd75a7701ed7fac7e74d3836807eb9f8aee22f60f519b748ff56", size = 45247507, upload-time = "2025-03-30T04:44:03.489Z" }, + { url = "https://files.pythonhosted.org/packages/b6/42/e73497e12c5e1f3a98dc0c07a8ac80ee3b728e03cb397475337540b02432/imagecodecs-2025.3.30-cp312-cp312-win_amd64.whl", hash = "sha256:0b0f6e0f118674c76982e5a25bfeec5e6fc4fc4fc102c0d356e370f473e7b512", size = 28889883, upload-time = "2025-03-30T04:44:12.192Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f0/66792e83443b32442a3c3377e5933b59ccf1be366973cecfc2182ee0840c/imagecodecs-2025.3.30-cp312-cp312-win_arm64.whl", hash = "sha256:bde3bd80cdf65afddb64af4c433549e882a5aa15d300e3781acab8d4df1c94a9", size = 23746584, upload-time = "2025-03-30T04:44:17.341Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e4/9d5fca3816391f28cc3f5310d5765372e60f5208bf8ab1c01c6d1486db86/imagecodecs-2025.3.30-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:0bf7248a7949525848f3e2c7d09e837e8333d52c7ac0436c6eed36235da8227b", size = 17932366, upload-time = "2025-03-30T04:44:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/e9/90/4a13b60aeedcf3ada27cfa6e9a58f0bb1cc50340980f6f9d4a00ced7d753/imagecodecs-2025.3.30-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e598b6ec77df2517a8d4af6b66393250ba4a8764fccda5dbe6546236df5d11c", size = 15030556, upload-time = "2025-03-30T04:44:24.267Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/03439594c4a7c79dbd85a282387eb399a94702875e58a11e41592dfd8b7c/imagecodecs-2025.3.30-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:212ae6ba7c656ddf24e8aabefc56c5e2300335ed1305838508c57de202e6dbe4", size = 43563048, upload-time = "2025-03-30T04:44:29.186Z" }, + { url = "https://files.pythonhosted.org/packages/ef/86/21a7f96f5446595df83ba18d20a6f5d2e99eef37c8f0fee807e78bf7e4aa/imagecodecs-2025.3.30-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa7b1c7d7af449c8153a040f7782d4296350245f8809e49dd4fb5bef4d740e6", size = 45213087, upload-time = "2025-03-30T04:44:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/d3/be/e4aa5ed727ab4178362c695ea862d4c3e25988020ec1b05f8fedbef2ef5f/imagecodecs-2025.3.30-cp313-cp313-win_amd64.whl", hash = "sha256:1c51fef75fec66b4ea5e98b4ab47889942049389278749e1f96329c38f31c377", size = 28865173, upload-time = "2025-03-30T04:44:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ad/5c21694d68a563a0dcbae97b460093ec165efbb795695ea02b24415d6c79/imagecodecs-2025.3.30-cp313-cp313-win_arm64.whl", hash = "sha256:eda70c0b9d2bcf225f7ae12dbefd0e3ab92ea7db30cdb56b292517fb61357ad7", size = 23731786, upload-time = "2025-03-30T04:44:47.697Z" }, +] + +[[package]] +name = "imagecodecs" +version = "2026.1.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/75/cedaa3dba300df85712515b9c9e13d848ea9557b796b6c44b50bd361571e/imagecodecs-2026.1.14.tar.gz", hash = "sha256:e37ef5116d41ba90b1c9d1d7121846671fd65c271f0c15ef24208353fa79b283", size = 9527808, upload-time = "2026-01-14T04:24:31.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b2/c36b8633c3303ed8d80c8d8490c8488baa7f9e6f46fd11688cb9e68eae5e/imagecodecs-2026.1.14-cp311-abi3-macosx_10_14_x86_64.whl", hash = "sha256:b94c57922816eb025d443f4594f1235d80f0f56b4b48aa9b60bf9d679ea49415", size = 12861649, upload-time = "2026-01-14T04:23:45.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/71780373cef2ec5d9d2f111010ff0a16de0788fdc9a8f26f3cce05d0ed38/imagecodecs-2026.1.14-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:bd72bdff628d6c32c71f086e488f483abbf84816b05d439964d980af49f2a9c5", size = 10700009, upload-time = "2026-01-14T04:23:48.108Z" }, + { url = "https://files.pythonhosted.org/packages/84/b4/48fc1a9b2379941a752d046b6d9217a1e82c09ed11184a18245cbb0d9c8b/imagecodecs-2026.1.14-cp311-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e92f8b3bddf632c23d3a832f35e5a2c2326eb0e2ae1ebce419789cce63e5c30", size = 23748325, upload-time = "2026-01-14T04:23:51.644Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/8a299b91a4abee7c299c0c6625f9c0985c623fd4b6b41b5a5fe92508bb18/imagecodecs-2026.1.14-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a78451926905459f42e827c207d80e1601abf68eaf0e38fd65ddd3c346d1f47", size = 24716561, upload-time = "2026-01-14T04:23:55.275Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/f03fa2a60617a46814fea5523f8d53a703aabbb316e029b21efe8fe04f9f/imagecodecs-2026.1.14-cp311-abi3-win32.whl", hash = "sha256:6bbf7defac9f71e0401305440f7e94160201789e03ee75e6e5709bc904742429", size = 17224233, upload-time = "2026-01-14T04:23:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/ae01473653dbad9e10a1632b5d8ae6473de0f3ec2b82c912836661d501a9/imagecodecs-2026.1.14-cp311-abi3-win_amd64.whl", hash = "sha256:13ec4659d05010aa072644f100d0e1e1fcc61d7eaa960923b8216272682e6c9a", size = 21544843, upload-time = "2026-01-14T04:24:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/86fa1a07aee2ea39acfa04e372a144a72b4cdc80f40a11a3ee312c12d312/imagecodecs-2026.1.14-cp311-abi3-win_arm64.whl", hash = "sha256:2a6102f3b99c66e090a619f8a0204e6e95c01399854ffa09e4a9de476dc1671a", size = 16893409, upload-time = "2026-01-14T04:24:05.764Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a3/f92989c2de762ee4db9f5454bbafa540cfa13d28006b4aeb01936f9a4a8a/imagecodecs-2026.1.14-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:455915b32ae1ecccaeb9dad32ad1499aa2e6928b6b853e3635cb32af9aa56857", size = 13202548, upload-time = "2026-01-14T04:24:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/bb/da/9fb0e446f5af6fa21f44a1f1d4950e538345d7e8931b5983a543a24b9c38/imagecodecs-2026.1.14-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c189f5d842f1f2524c27c307220c5f2cf201af44dab74b1f7476645f72eb2e87", size = 11004937, upload-time = "2026-01-14T04:24:11.054Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e9/0400722f5bdaaa9bc13ad77c78606cfb767f2b609cb136cc81d81c380807/imagecodecs-2026.1.14-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca3325e68dffeb29383f4a28957ac61d3c0ade1b244ebf70797bdaf009ad1578", size = 27442756, upload-time = "2026-01-14T04:24:15.247Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/f39cacf55d5e7175f1e7b5222178e7c56d6ca1a49e61011adb1150d85aec/imagecodecs-2026.1.14-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6819b8365cf28e2a85dcb04fb9c1f1acb63cf5da61495910cd751a7e2548e1d", size = 28006255, upload-time = "2026-01-14T04:24:19.458Z" }, + { url = "https://files.pythonhosted.org/packages/ad/58/b098490e93435ef4311c473e30588e80f966f6d676f1343804f8ab3fcff1/imagecodecs-2026.1.14-cp314-cp314t-win32.whl", hash = "sha256:ae422def59fab163ba0cb79470e10aa64b8a62b29e74d50ecd0edab4349444c2", size = 17909361, upload-time = "2026-01-14T04:24:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/b3/35/cb1fc0ce35dd61475672af65607fe2c3cc30d7844d617067a3e649b8366e/imagecodecs-2026.1.14-cp314-cp314t-win_amd64.whl", hash = "sha256:9b11ced125e7b8da2a9d1a30505827cdb27f7023ccee4cd4cc5e3b352dce8574", size = 22496362, upload-time = "2026-01-14T04:24:25.874Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/2dfe80928bf0c6c6e7411954fa008a71d0b08e72fe1028fe9a8a8116588a/imagecodecs-2026.1.14-cp314-cp314t-win_arm64.whl", hash = "sha256:d4c19df6142481d2fda62d91542c078aecd647f120da52cc79019ed6938e587d", size = 17534288, upload-time = "2026-01-14T04:24:28.911Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[package.optional-dependencies] +ffmpeg = [ + { name = "imageio-ffmpeg" }, + { name = "psutil" }, +] + +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "interegular" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9d/8b6dde58a028a3962ce17e84d5fe73758df61378e00ef8ac3d85da34b0ff/interegular-0.3.3.tar.gz", hash = "sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600", size = 24705, upload-time = "2024-01-06T23:01:22.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/01/72d6472f80651673716d1deda2a5bbb633e563ecf94f4479da5519d69d25/interegular-0.3.3-py37-none-any.whl", hash = "sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c", size = 23635, upload-time = "2024-01-06T23:01:20.829Z" }, +] + +[[package]] +name = "iopath" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "portalocker" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/b3d451dfc523756cf177d3ebb0af76dc7751b341c60e2a21871be400ae29/iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01", size = 42226, upload-time = "2022-07-09T19:00:50.866Z" } + +[[package]] +name = "ipycanvas" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipywidgets" }, + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/56/484c8979bbcaa3e3f2da4eac6a1eb41e998e353e4c6ef89e9612889813c8/ipycanvas-0.14.3.tar.gz", hash = "sha256:c6a53a22eebf4d611b168b8f4434145883f27a7575509bd99a4bfc48c5385a39", size = 4150499, upload-time = "2025-12-11T09:12:59.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/37/c6880bd16093793dcb4c005011cf968f45fd815b7b5094fa8374524add26/ipycanvas-0.14.3-py2.py3-none-any.whl", hash = "sha256:8a2f48e1e079355d3e7d5683e5c6e7684a87c15c3750c8d8cd2289c95383ee3e", size = 142962, upload-time = "2025-12-11T09:12:50.5Z" }, +] + +[[package]] +name = "ipyevents" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipywidgets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/e5/ac7e199dd22ce3f7e0d43c6433fef5450197fa81994393e3381893b12d61/ipyevents-2.0.4.tar.gz", hash = "sha256:bd8c66a9ff26f481494cda1ec1d979722ca9eba19f8ef52538c7cc2db2a1a139", size = 210601, upload-time = "2025-09-15T14:02:34.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d3/642a6dc3db8ea558a9b5fbc83815b197861868dc98f98a789b85c7660670/ipyevents-2.0.4-py3-none-any.whl", hash = "sha256:e532e05b037f70373850723f483d2830cb8a633e5aa19637ee9e7adaf41421f1", size = 102433, upload-time = "2025-09-15T14:02:33.255Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "8.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "decorator", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jedi", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pexpect", marker = "(python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pygments", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "stack-data", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "traitlets", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "decorator", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jedi", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pexpect", marker = "(python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version != '3.11.*' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pygments", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "stack-data", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "traitlets", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.12' and sys_platform == 'win32') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform != 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "decorator", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jedi", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pexpect", marker = "(python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pygments", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "stack-data", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "traitlets", marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json5" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-compare-view" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jinja2" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/4b/382b3882faa4de741bf4a923d7681d597d5e3694ae2076047d45f619dcce/jupyter_compare_view-0.2.4.tar.gz", hash = "sha256:0edae58d8cc47450d7a545c74a9d5cdfe0e20dc9b1003939d5130737177643c1", size = 12195, upload-time = "2023-03-14T09:03:55.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/66/9b31bc102a9b959ca60aa4b70153809e8d4773e520618cb115860105e50a/jupyter_compare_view-0.2.4-py3-none-any.whl", hash = "sha256:5b31e56c38d4ace49cca9acc52f8aa3f9013a4bafbdf9c7bc3a70644db03d7dc", size = 11681, upload-time = "2023-03-14T09:03:53.752Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "(os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128-train') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "(os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128-train') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/953a5612a34a3c799a62566a548e711d103f631672fd49650e0f2de80870/jupyterlab-4.5.5.tar.gz", hash = "sha256:eac620698c59eb810e1729909be418d9373d18137cac66637141abba613b3fda", size = 23968441, upload-time = "2026-02-23T18:57:34.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/52/372d3494766d690dfdd286871bf5f7fb9a6c61f7566ccaa7153a163dd1df/jupyterlab-4.5.5-py3-none-any.whl", hash = "sha256:a35694a40a8e7f2e82f387472af24e61b22adcce87b5a8ab97a5d9c486202a6d", size = 12446824, upload-time = "2026-02-23T18:57:30.398Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "jupytext" +version = "1.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/4f13fcba0ed05965a48fca197d89fb8c78c4b61051dc0c9ee9ed92e77a8d/jupytext-1.19.2.tar.gz", hash = "sha256:da6198a42406a09142b6b26ebc46a3ec7077f525222a8f12b1811a0e289a2216", size = 4309931, upload-time = "2026-05-10T17:10:40.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl", hash = "sha256:8a31e896c7e9215841783aade24336e945543057e1c2d7f00b22f9e870348688", size = 170653, upload-time = "2026-05-10T17:10:38.418Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "secretstorage", marker = "sys_platform == 'linux' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "kornia" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kornia-rs" }, + { name = "packaging" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/e6/45e757d4924176e4d4e111e10effaab7db382313243e0188a06805010073/kornia-0.8.2.tar.gz", hash = "sha256:5411b2ce0dd909d1608016308cd68faeef90f88c47f47e8ecd40553fd4d8b937", size = 667151, upload-time = "2025-11-08T12:10:03.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d4/e9bd12b7b4cbd23b4dfb47e744ee1fa54d6d9c3c9bc406ec86c1be8c8307/kornia-0.8.2-py2.py3-none-any.whl", hash = "sha256:32dfe77c9c74a87a2de49395aa3c2c376a1b63c27611a298b394d02d13905819", size = 1095012, upload-time = "2025-11-08T12:10:01.226Z" }, +] + +[[package]] +name = "kornia-rs" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/17/8b3518ece01512a575b18f86b346879793d3dea264b314796bbd44d42e11/kornia_rs-0.1.10.tar.gz", hash = "sha256:5fd3fbc65240fa751975f5870b079f98e7fdcaa2885ea577b3da324d8bf01d81", size = 145610, upload-time = "2025-11-08T11:29:32.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/40/6a59c5b99e19e1be7fff1d68dd3b3eddc80e5304dab75ebb78d0199e2484/kornia_rs-0.1.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e95ccd4b3f73a0d5cbe16c03fd705fa8d75e9df7b044ba9a6c5957b2591003f3", size = 2815093, upload-time = "2025-11-08T11:30:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/17/aa/77f5882707467a6af0833b3ac1497638352bc391d082713b24a3bd187f73/kornia_rs-0.1.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bc8d5de7d7611b68746b2feff594a073740c4f915d0ccc37bd1d189029c20fe", size = 2078046, upload-time = "2025-11-08T11:30:04.952Z" }, + { url = "https://files.pythonhosted.org/packages/31/43/cff694d864bf60e1e3853ec98bdabc66433f0eeffb7e7f97d27a0b907a88/kornia_rs-0.1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb94ce80a614a5f00cb68755d7a182236482584e78388217974ef811f4dbb30", size = 2204974, upload-time = "2025-11-08T11:29:30.626Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/907580d1e573bc1862557cb70d1f1888bde1b33240d15cd10ff06c93a57c/kornia_rs-0.1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859942c70f503bba813c99a39d3011d3e51f294db8058f87842efa180955cab4", size = 3042947, upload-time = "2025-11-08T11:29:48.431Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/3d0a45f297b1a3f308893751b62080898ce415c61a3629a3d6b04cf55db7/kornia_rs-0.1.10-cp310-cp310-win_amd64.whl", hash = "sha256:74f17ea9afc0a5312832c32f6670e1a4b2162dcf9a8908fb46bbb56d2c707e9e", size = 2543744, upload-time = "2025-11-08T11:30:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/ab91a87cefd8d92a10749fa5d923366dfd2a2d240d9e57260e4218e9a5af/kornia_rs-0.1.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6757940733f13c52c4f142b9b11e3e9bd12ef9d209e333300602e86e21f5ae2f", size = 2811949, upload-time = "2025-11-08T11:30:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/61/6125a970249e04dd31cf3edf3fb0ceb98ea65269bc416ba48fd70f9a8f5e/kornia_rs-0.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68e90101a34ba2bbce920332b25fd4d25c8c546d9a241b2606a6d886df2dd1ed", size = 2078639, upload-time = "2025-11-08T11:30:06.363Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e4/c3484e5921a08e6368f0565c30646741fd12b46cb45c962d519cac3d12ad/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b0adb81858a8963455f2f0da01fcd6ea3296147b918306488edeeaf6bc2a979", size = 2204722, upload-time = "2025-11-08T11:29:33.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/a4/2e6e33da900f19ae6411bfad41d317e56f1ae4f204bd73e61f0881bd5418/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3e237a8428524ad9f86599c0c47b355bc3007669fe297ea3fbd59cd64bc2f7", size = 3042890, upload-time = "2025-11-08T11:29:50.15Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/5e171c98b742139bebd1bd593d768e3c045f824bf0ae14190b63f0ac0acc/kornia_rs-0.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:1d300ea6d4666e47302fba6cc438556d91e37ce41caf291a9a04a8f74c231d0b", size = 2544572, upload-time = "2025-11-08T11:30:32.32Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6c/8248f08c90a10d6b8ca2e74783da8df7fa509f46b64a3b4fbb7dd0ac4e9c/kornia_rs-0.1.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0809277e51156d59be3c39605ba9659e94f7a4cf3b0b6c035ec2f06f6067881", size = 2811606, upload-time = "2025-11-08T11:30:21.346Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/29e5710cbc5d01c155ee1fd7621db48b94378a7ae394741bb34a6bfb36d9/kornia_rs-0.1.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ecf2ba0291cc1bb178073d56e46b16296a8864a20272b63af02ee88771cb574", size = 2076141, upload-time = "2025-11-08T11:30:07.527Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/0b3e90b9d0a25e6211c7ac9fa1dfed4db1306a812c359ee49678390a1bdc/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d874ca12dd58871f9849672d9bf9fa998398470a88b52d61223ce2133b196662", size = 2205562, upload-time = "2025-11-08T11:29:35.353Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/315f358b2a2c29d9af3a73f3d1973c2fd8e0cdeb65a57af98643e66fa7c8/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f332a2a034cc791006f25c2d85e342a060887145e9236e8e43562badcadededf", size = 3042197, upload-time = "2025-11-08T11:29:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b8/0ddbdf1d35fec3ef24f5b8cc29eb633ce5ce16c94c9fb090408c1280abe9/kornia_rs-0.1.10-cp312-cp312-win_amd64.whl", hash = "sha256:34111ce1c8abe930079b4b0aeb8d372f876c621a867ed03f77181de685e71a8f", size = 2539656, upload-time = "2025-11-08T11:30:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/90/01/1d658b11635431f8c31f416c90ca99befdc1f4fdd20e91a05b480b9c0ea8/kornia_rs-0.1.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:950a943f91c2cff94d80282886b0d48bbc15ef4a7cc4b15ac819724dfdb2f414", size = 2811810, upload-time = "2025-11-08T11:30:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/bd970ded1d819557cc33055d982b1847eb385151ea5b0c915c16ed74f5c0/kornia_rs-0.1.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:63b802aaf95590276d3426edc6d23ff11caf269d2bc2ec37cb6c679b7b2a8ee0", size = 2076195, upload-time = "2025-11-08T11:30:08.726Z" }, + { url = "https://files.pythonhosted.org/packages/c1/10/afd700455105fdba5b043d724f3a65ca36259b89c736a3b71d5a03103808/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38087da7cdf2bffe10530c0d53335dd1fc107fae6521f2dd4797c6522b6d11b3", size = 2205781, upload-time = "2025-11-08T11:29:36.8Z" }, + { url = "https://files.pythonhosted.org/packages/25/16/ec8dc3ce1d79660ddd6a186a77037e0c3bf61648e6c72250280b648fb291/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa3464de8f9920d87415721c36840ceea23e054dcb54dd9f69189ba9eabce0c7", size = 3042272, upload-time = "2025-11-08T11:29:52.936Z" }, + { url = "https://files.pythonhosted.org/packages/f7/75/62785aba777d35a562a97a987d65840306fab7a8ecd2d928dd8ac779e29b/kornia_rs-0.1.10-cp313-cp313-win_amd64.whl", hash = "sha256:c57d157bebe64c22e2e44c72455b1c7365eee4d767e0c187dc28f22d072ebaf7", size = 2539802, upload-time = "2025-11-08T11:30:35.753Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d5/32b23d110109eb77b2dc952be75411f7e495da9105058e2cb08924a9cc90/kornia_rs-0.1.10-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:0b375f02422ef5986caed612799b4ddcc91f57f303906868b0a8c397a17e7607", size = 2810244, upload-time = "2025-11-08T11:30:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/5f/5ecde42b7c18e7df26c413848a98744427c3d370f5eed725b65f0bc356fb/kornia_rs-0.1.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2bcfa438d6b5dbe07d573afc980f2871f6639b2eac5148b8c0bba4f82357b9a", size = 2074220, upload-time = "2025-11-08T11:30:09.972Z" }, + { url = "https://files.pythonhosted.org/packages/18/6c/6fc86eb855bcc723924c3b91de98dc6c0f381987ce582e080b8eade3bc88/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:021b0a02b2356b12b3954a298f369ed4fe2dd522dcf8b6d72f91bf3bd8eea201", size = 2204672, upload-time = "2025-11-08T11:29:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/19/26/3ac706d1b36761c0f7a36934327079adcb42d761c8c219865123d49fc1b2/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b07e2ae79e423b3248d94afd092e324c5ddfe3157fafc047531cc8bffa6a3", size = 3042797, upload-time = "2025-11-08T11:29:54.719Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/d62728d86bc67f5516249b154ff0bdfcf38a854dae284ff0ce62da87af99/kornia_rs-0.1.10-cp313-cp313t-win_amd64.whl", hash = "sha256:b80a037e34d63cb021bcd5fc571e41aff804a2981311f66e883768c6b8e5f8de", size = 2543855, upload-time = "2025-11-08T11:30:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/8ed1288a51d2ad71a6c01152ceccdd2d92f21692dfd2304b1ae9383496fa/kornia_rs-0.1.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:119eb434d1384257cae6c1ee9444e1aa1b0fda617f6d5a79fef3f145fdac70ac", size = 2809873, upload-time = "2025-11-08T11:30:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/fd5f919723aaa69ec5c1e60b10b7904a9126be5b9d6ccc0267fa42ca77e0/kornia_rs-0.1.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60bca692911e5969e51d256299ecc6e90d32b9a2c5bf7bd1c7eb8f096cb9234b", size = 2074360, upload-time = "2025-11-08T11:30:11.327Z" }, + { url = "https://files.pythonhosted.org/packages/43/ec/7987aa5fb7d188180866bd8dafa5bb5b1f00a74ba738bb4e2abe63c589ac/kornia_rs-0.1.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61f126644f49ff9947d9402126edacfeeb4b47c0999a7af487d27ce4fc4cbc2a", size = 2206111, upload-time = "2025-11-08T11:29:40.608Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/cb73b7e87a07b2af1146988d159d48722f0a28f550f920397c8964ab7c19/kornia_rs-0.1.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:614aeffdb1d39c4041ace0e0fc318b36eb211f6c133912984e946174e67dbb42", size = 3041436, upload-time = "2025-11-08T11:29:55.984Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/9f50fce2d8e9edd6b2d09908b6d5613f9ead992bf2e80060e080f2e7d64d/kornia_rs-0.1.10-cp314-cp314-win_amd64.whl", hash = "sha256:6de4e73b1c90cc76b7486491866eb9e61e5cf34d3a4016957d4563ac7d3ee39a", size = 2544067, upload-time = "2025-11-08T11:30:38.638Z" }, + { url = "https://files.pythonhosted.org/packages/d1/8f/45895818f3c7a5009737119b075db6b88bbf00938275611bc5d2cfbd0b2a/kornia_rs-0.1.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f0db8b41ae03a746bb0dcb970d5ff2fd66213adb4a3b4de1186fe86205698e89", size = 2806089, upload-time = "2025-11-08T11:30:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/38/af/831e79b45702f8b6102438b1ff9b44a912669890cdf209cd275257f6d655/kornia_rs-0.1.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b63ee175125892ef18027bd3a43b447fd53f9bf42cea4d6f699ab4e69cf3f16", size = 2064116, upload-time = "2025-11-08T11:30:13.481Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/e92606e0fa9a1b52ecf57faf322dcc076ae35315b4e1870d380fd64926d7/kornia_rs-0.1.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68eb25ba4639fa5e1cd94a10fb6410c8840c9f0162e5912d834c4a8c7c174493", size = 2197890, upload-time = "2025-11-08T11:29:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fa/a2adce992b5eb65ef8adfc6f4465989948bfa8b875638e17c214541af25a/kornia_rs-0.1.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc18ba839f5c10ceb4757342ee7530cef8a0ecdd20486b8bbe14a56f72fa7037", size = 3040852, upload-time = "2025-11-08T11:29:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/40a3e3a235c370f5f61a8f9a7bdedf47d1bdd8f7d7e145e551545babff6b/kornia_rs-0.1.10-cp314-cp314t-win_amd64.whl", hash = "sha256:257eb0a780f990c0c44ac47acb77504dd95b8df0c592fd31354da1228df6678d", size = 2543609, upload-time = "2025-11-08T11:30:40.1Z" }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "lerobot" +version = "0.4.4" +source = { git = "https://github.com/mli0603/lerobot.git#1a4316c6845330bc552fb982dbc44bdb4f66f2f1" } +dependencies = [ + { name = "cmake" }, + { name = "deepdiff" }, + { name = "jsonlines" }, + { name = "packaging" }, + { name = "pynput" }, + { name = "pyserial" }, + { name = "setuptools" }, + { name = "termcolor" }, +] + +[[package]] +name = "libero" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bddl" }, + { name = "cloudpickle" }, + { name = "easydict" }, + { name = "einops" }, + { name = "future" }, + { name = "gymnasium" }, + { name = "hf-egl-probe" }, + { name = "hydra-core" }, + { name = "matplotlib" }, + { name = "mujoco" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "robomimic" }, + { name = "robosuite" }, + { name = "thop" }, + { name = "transformers" }, + { name = "wandb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/7a/5f0ec2fb6013e55fbf6a7fb3b29b98bc7b9a7f1fee6ecdfcb9b1d60225fb/libero-0.1.1.tar.gz", hash = "sha256:31f17f69ced68e2ecde630353dda924372b0b7b647991e21eade231b241fc373", size = 2963467, upload-time = "2025-11-03T09:51:48.589Z" } + +[[package]] +name = "llguidance" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/48/3f7a9d3ff1b36bba92b5107a3a21286821227afe9ea464736133994d61fb/llguidance-1.3.0.tar.gz", hash = "sha256:861249afd51dc325646834462ea827e57a5c2b2042e108e6aae7059fdad9104d", size = 1070460, upload-time = "2025-10-20T19:58:44.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/33/be5acb85cd8cdc4afde33d9c234eece9f318e087920255af3c05864cd3e7/llguidance-1.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7685222660a762e481ac633d49cc559c64980fe2ee59c8f932a5bb5cbc0c2c2", size = 3220647, upload-time = "2025-10-20T19:58:42.542Z" }, + { url = "https://files.pythonhosted.org/packages/82/e6/b48bda5b15efeaeb62bd0dba8fc6a01d4ae5457a85dbb5d18632385fe15c/llguidance-1.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:098030ff0687261a3f1bd54cf21fe951fc861d56d37a0671250dd36677eaf224", size = 3099830, upload-time = "2025-10-20T19:58:40.826Z" }, + { url = "https://files.pythonhosted.org/packages/aa/11/44389d3d1526d7a5c38ffd587a5ebc61d7bee443ac1dea95f2089ad58f5f/llguidance-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f6caca5d78db7f76e1fbb0fff8607b861c32d47fa3d5dee2fc49de27ee269df", size = 2835242, upload-time = "2025-10-20T19:58:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ca/53ea256396405e4dee70d5a4a35e18543408e18bb16b251d6ca6b5d80310/llguidance-1.3.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0612bb3f034d2487b6e8f9561f02a94a6039d88273bf0c5c539a3bd3895e47d2", size = 3297480, upload-time = "2025-10-20T19:58:37.033Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/1ff2bedb8f9acb46a2d2d603415d272bb622c142ea86f5b95445cc6e366c/llguidance-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc17e9dd602c3879bf91664a64bf72f54c74dbfbeb24ccfab6a5fe435b12f7aa", size = 3033133, upload-time = "2025-10-20T19:58:38.721Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a7/9b8086c0cfdddf3f6d47b173a404fa7ac46272f7affbee082c36740f4f1c/llguidance-1.3.0-cp39-abi3-win32.whl", hash = "sha256:2f6f558485a43e273fc5c6c974a9a3ace5d5e170076db9b40e0560e41c3ff18f", size = 2598109, upload-time = "2025-10-20T19:58:47.656Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/809349638231f469b9056c0e1bfd924d5ef5558b3b3ec72d093b6fad33b1/llguidance-1.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:1d1cd1c8618d1a13605d3e057c978651e551c8c469b481ee4041f1d6c436002d", size = 2789946, upload-time = "2025-10-20T19:58:45.958Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" }, +] + +[[package]] +name = "lm-format-enforcer" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/ef/11292bb0b85cf4c93447cab5a29f64576ed14d3ab4280e35ddd23486594a/lm_format_enforcer-0.11.3-py3-none-any.whl", hash = "sha256:cf586350875def1ae7a8fba84fcbbfc8371424b6c9d05c1fcba70aa233fbf06f", size = 45418, upload-time = "2025-08-24T19:37:46.325Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "win32-setctime", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "lpips" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/2d/4b8148d32f5bd461eb7d5daa54fcc998f86eaa709a57f4ef6aa4c62f024f/lpips-0.1.4.tar.gz", hash = "sha256:3846331df6c69688aec3d300a5eeef6c529435bc8460bd58201c3d62e56188fa", size = 18029, upload-time = "2021-08-25T22:10:32.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/13/1df50c7925d9d2746702719f40e864f51ed66f307b20ad32392f1ad2bb87/lpips-0.1.4-py3-none-any.whl", hash = "sha256:fd537af5828b69d2e6ffc0a397bd506dbc28ca183543617690844c08e102ec5e", size = 53763, upload-time = "2021-08-25T22:10:31.257Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, + { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, + { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/2f/46/08fd8ef19b782f301d56a9ccfd7dafec5fd4fc1a9f017cf22a1accb585d7/lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c", size = 207171, upload-time = "2025-11-03T13:01:56.595Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3f/ea3334e59de30871d773963997ecdba96c4584c5f8007fd83cfc8f1ee935/lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a", size = 207163, upload-time = "2025-11-03T13:01:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/7b3a2a0feb998969f4793c650bb16eff5b06e80d1f7bff867feb332f2af2/lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d", size = 1292136, upload-time = "2025-11-03T13:02:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/d1/f1d259352227bb1c185288dd694121ea303e43404aa77560b879c90e7073/lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c", size = 1279639, upload-time = "2025-11-03T13:02:01.649Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fb/ba9256c48266a09012ed1d9b0253b9aa4fe9cdff094f8febf5b26a4aa2a2/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64", size = 1368257, upload-time = "2025-11-03T13:02:03.35Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6d/dee32a9430c8b0e01bbb4537573cabd00555827f1a0a42d4e24ca803935c/lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832", size = 88191, upload-time = "2025-11-03T13:02:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/18/e0/f06028aea741bbecb2a7e9648f4643235279a770c7ffaf70bd4860c73661/lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22", size = 99502, upload-time = "2025-11-03T13:02:05.886Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/5bef44afb303e56078676b9f2486f13173a3c1e7f17eaac1793538174817/lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9", size = 91285, upload-time = "2025-11-03T13:02:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/6a5c2952971af73f15ed4ebfdd69774b454bd0dc905b289082ca8664fba1/lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f", size = 207348, upload-time = "2025-11-03T13:02:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d7/fd62cbdbdccc35341e83aabdb3f6d5c19be2687d0a4eaf6457ddf53bba64/lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba", size = 207340, upload-time = "2025-11-03T13:02:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/225ffadaacb4b0e0eb5fd263541edd938f16cd21fe1eae3cd6d5b6a259dc/lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d", size = 1293398, upload-time = "2025-11-03T13:02:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/2ce59ba4a21ea5dc43460cba6f34584e187328019abc0e66698f2b66c881/lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67", size = 1281209, upload-time = "2025-11-03T13:02:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/4d946bd1624ec229b386a3bc8e7a85fa9a963d67d0a62043f0af0978d3da/lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d", size = 1369406, upload-time = "2025-11-03T13:02:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/d429ba4720a9064722698b4b754fb93e42e625f1318b8fe834086c7c783b/lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901", size = 88325, upload-time = "2025-11-03T13:02:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/7ba10c9b97c06af6c8f7032ec942ff127558863df52d866019ce9d2425cf/lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb", size = 99643, upload-time = "2025-11-03T13:02:15.978Z" }, + { url = "https://files.pythonhosted.org/packages/77/4d/a175459fb29f909e13e57c8f475181ad8085d8d7869bd8ad99033e3ee5fa/lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd", size = 91504, upload-time = "2025-11-03T13:02:17.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/9c/70bdbdb9f54053a308b200b4678afd13efd0eafb6ddcbb7f00077213c2e5/lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f", size = 207586, upload-time = "2025-11-03T13:02:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/cb/bfead8f437741ce51e14b3c7d404e3a1f6b409c440bad9b8f3945d4c40a7/lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6", size = 207161, upload-time = "2025-11-03T13:02:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/18/b192b2ce465dfbeabc4fc957ece7a1d34aded0d95a588862f1c8a86ac448/lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9", size = 1292415, upload-time = "2025-11-03T13:02:20.829Z" }, + { url = "https://files.pythonhosted.org/packages/67/79/a4e91872ab60f5e89bfad3e996ea7dc74a30f27253faf95865771225ccba/lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668", size = 1279920, upload-time = "2025-11-03T13:02:22.013Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/d52c7b11eaa286d49dae619c0eec4aabc0bf3cda7a7467eb77c62c4471f3/lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f", size = 1368661, upload-time = "2025-11-03T13:02:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/137ddeea14c2cb86864838277b2607d09f8253f152156a07f84e11768a28/lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67", size = 90139, upload-time = "2025-11-03T13:02:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/8332080fd293f8337779a440b3a143f85e374311705d243439a3349b81ad/lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be", size = 101497, upload-time = "2025-11-03T13:02:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" }, +] + +[[package]] +name = "makefun" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/6780ab8bc3b84a1cce3e4400aed3d64b6db7d5e227a2f75b6ded5674701a/makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947", size = 73565, upload-time = "2025-05-09T15:00:42.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/c0/4bc973defd1270b89ccaae04cef0d5fa3ea85b59b108ad2c08aeea9afb76/makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4", size = 22923, upload-time = "2025-05-09T15:00:41.042Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/3d/e0e8d9d1cee04f758120915e2b2a3a07eb41f8cf4654b4734788a522bcd1/mdit_py_plugins-0.6.0.tar.gz", hash = "sha256:2436f14a7295837ac9228a36feeabda867c4abc488c8d019ad5c0bda88eee040", size = 56025, upload-time = "2026-05-07T12:20:42.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl", hash = "sha256:f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90", size = 66655, upload-time = "2026-05-07T12:20:41.226Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mediapy" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/eb/8a0499fb1a2f373f97e2b4df91797507c3971c42c59f1610bed090c57ddc/mediapy-1.2.6.tar.gz", hash = "sha256:2c866cfa0a170213f771b1dd5584a2e82d8d0dc0fa94982f83e29aae27e49c83", size = 28143, upload-time = "2026-02-03T10:29:31.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/8c/52f0299f1675cdfa1ab39a6028a2e5adf9032ae1118c9895c84b08af162b/mediapy-1.2.6-py3-none-any.whl", hash = "sha256:0a0ea00eb0da83c3c54d588b49c49a41ba456174aa33e530ffe13e17269c9072", size = 27494, upload-time = "2026-02-03T10:29:30.245Z" }, +] + +[[package]] +name = "megatron-core" +version = "0.16.0rc0" +source = { git = "https://github.com/NVIDIA/Megatron-LM.git?rev=de56227#de56227b26ba56de885897361272513697de7dc6" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistral-common" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydantic-extra-types", extra = ["pycountry"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/97/753c85b5c0a19f4331ac99e0300ac8da06d4b29b629c9cb03064b38561bd/mistral_common-1.11.0.tar.gz", hash = "sha256:439b7fa38f9c3f020154af51bdf30eb81def507643017d8ce9f798384ec47ec3", size = 6355512, upload-time = "2026-04-01T13:54:12.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e4/73ad3c27e3fb613c3ce0953c928202c46cddebac3989b87be1b6f305a9f6/mistral_common-1.11.0-py3-none-any.whl", hash = "sha256:1d3ecaf7c3aa7338cb37b596fd0fb294485753958ee8e7254a6cc23eb30b249b", size = 6531513, upload-time = "2026-04-01T13:54:16.536Z" }, +] + +[package.optional-dependencies] +image = [ + { name = "opencv-python-headless" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "model-hosting-container-standards" +version = "0.1.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "jmespath" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "starlette" }, + { name = "supervisor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/3d/cf5c6029648cb0a116f7b5c2f74aa155ab0c6dd723a1f204a6d7ff354526/model_hosting_container_standards-0.1.14.tar.gz", hash = "sha256:b6cf4c46d88ce6acd6e543a578bb88ffd55d1179a7c09c22e61ae1d8a567c564", size = 90386, upload-time = "2026-03-18T21:25:14.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/94/052452842d39c562237a70345c57ec213a9db22bd25bba998fd2b32d70a7/model_hosting_container_standards-0.1.14-py3-none-any.whl", hash = "sha256:d678be6745899b8ba1e8246c96b101e7802a6a4ea3fb5d90ae8d6eb4204e84c6", size = 121406, upload-time = "2026-03-18T21:25:12.932Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "moviepy" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "proglog" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/61/15f9476e270f64c78a834e7459ca045d669f869cec24eed26807b8cd479d/moviepy-2.2.1.tar.gz", hash = "sha256:c80cb56815ece94e5e3e2d361aa40070eeb30a09d23a24c4e684d03e16deacb1", size = 58431438, upload-time = "2025-05-21T19:31:52.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/73/7d3b2010baa0b5eb1e4dfa9e4385e89b6716be76f2fa21a6c0fe34b68e5a/moviepy-2.2.1-py3-none-any.whl", hash = "sha256:6b56803fec2ac54b557404126ac1160e65448e03798fa282bd23e8fab3795060", size = 129871, upload-time = "2025-05-21T19:31:50.11Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "msgspec" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/5e/151883ba2047cca9db8ed2f86186b054ad200bc231352df15b0c1dd75b1f/msgspec-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23a6ec2a3b5038c233b04740a545856a068bc5cb8db184ff493a58e08c994fbf", size = 195191, upload-time = "2025-11-24T03:55:08.549Z" }, + { url = "https://files.pythonhosted.org/packages/50/88/a795647672f547c983eff0823b82aaa35db922c767e1b3693e2dcf96678d/msgspec-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cde2c41ed3eaaef6146365cb0d69580078a19f974c6cb8165cc5dcd5734f573e", size = 188513, upload-time = "2025-11-24T03:55:10.008Z" }, + { url = "https://files.pythonhosted.org/packages/4b/91/eb0abb0e0de142066cebfe546dc9140c5972ea824aa6ff507ad0b6a126ac/msgspec-0.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5da0daa782f95d364f0d95962faed01e218732aa1aa6cad56b25a5d2092e75a4", size = 216370, upload-time = "2025-11-24T03:55:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/15/2a/48e41d9ef0a24b1c6e67cbd94a676799e0561bfbc163be1aaaff5ca853f5/msgspec-0.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9369d5266144bef91be2940a3821e03e51a93c9080fde3ef72728c3f0a3a8bb7", size = 222653, upload-time = "2025-11-24T03:55:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/90/c9/14b825df203d980f82a623450d5f39e7f7a09e6e256c52b498ea8f29d923/msgspec-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90fb865b306ca92c03964a5f3d0cd9eb1adda14f7e5ac7943efd159719ea9f10", size = 222337, upload-time = "2025-11-24T03:55:14.777Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/39a5c3ddd294f587d6fb8efccc8361b6aa5089974015054071e665c9d24b/msgspec-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8112cd48b67dfc0cfa49fc812b6ce7eb37499e1d95b9575061683f3428975d3", size = 225565, upload-time = "2025-11-24T03:55:16.4Z" }, + { url = "https://files.pythonhosted.org/packages/98/bd/5db3c14d675ee12842afb9b70c94c64f2c873f31198c46cbfcd7dffafab0/msgspec-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:666b966d503df5dc27287675f525a56b6e66a2b8e8ccd2877b0c01328f19ae6c", size = 188412, upload-time = "2025-11-24T03:55:17.747Z" }, + { url = "https://files.pythonhosted.org/packages/76/c7/06cc218bc0c86f0c6c6f34f7eeea6cfb8b835070e8031e3b0ef00f6c7c69/msgspec-0.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:099e3e85cd5b238f2669621be65f0728169b8c7cb7ab07f6137b02dc7feea781", size = 173951, upload-time = "2025-11-24T03:55:19.335Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, + { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, + { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, + { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, +] + +[[package]] +name = "mujoco" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "etils", version = "1.13.0", source = { registry = "https://pypi.org/simple" }, extra = ["epath"], marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "etils", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, extra = ["epath"], marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "glfw" }, + { name = "numpy" }, + { name = "pyopengl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/75/97e2ab2b51a66495c0404f6fb6bf3a1c1ba8cdc3316e63fbf4d49367f388/mujoco-3.3.2.tar.gz", hash = "sha256:77773c714f572f6e71a937bc387f7a36c129d0932fd246962c3e6a48fdf6bbac", size = 806858, upload-time = "2025-04-28T22:14:02.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/9d/514b2a27c36664762a5b2fb86b25a1b0cf9b81f2fd462553bc7789ef2c34/mujoco-3.3.2-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f9ac42ca8ceef8ac64636d01592fc94d6c71be455e77bfbd55240499c3a6fc13", size = 6547326, upload-time = "2025-04-28T22:12:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/b9/10/3ec1f30231113b23e7e51cedcdee9c9f041d8cb78a3bb66f6956b7f59e52/mujoco-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:49330df7ab6174539c731a8bd6d1eb05f8083d41414005d4d14d22d886573e1f", size = 6571509, upload-time = "2025-04-28T22:12:33.851Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/ecb34ca586d56941c78ef1049966ad8065ce2760f925534fc355d4dca0a8/mujoco-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df38a1942b2a42e681272ed3b91bcc47e965a4f51001ac346c236bb27580b455", size = 6305981, upload-time = "2025-04-28T22:12:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/7e/11/c768c9b88f71c91952c01d69ab84c0bc8322a053c7b634812a0218cef624/mujoco-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ee42d6567730547d6de421debbe0a73d3ed6cfcbc512a3c191cc0afbcf1bfe", size = 6595880, upload-time = "2025-04-28T22:12:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/332dd332272d30b591c33b3d5f3ce8ee6a38c45942b6054fe9a27c3e9ea0/mujoco-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:92c4712938278a84cc09c519450a70aed683a0c2c157bbc50f6bd87113ba3f12", size = 4924586, upload-time = "2025-04-28T22:12:45.506Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/5952ea29b1295c76f5e925085986d0a32180914c422686f0e72c57edf88d/mujoco-3.3.2-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:975acda0a5dbee53facd6aabedebf3a277c66c3bd891add988c7664abe4e203c", size = 6560851, upload-time = "2025-04-28T22:12:50.18Z" }, + { url = "https://files.pythonhosted.org/packages/f3/98/2e3490a87ae62ee03d32988f1620bae83327ba2bdd2c3a4a5edacb56d0b6/mujoco-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499637be489cb915b68c3b90b731fae1274f6a9297d8551d6b443a0395ee1956", size = 6583213, upload-time = "2025-04-28T22:12:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/5a/71/2ca3b738d027f78ff99f17a2bda24d2112704626ddb67e53c66f57a94674/mujoco-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409d54ec5402da6d781e2cab1a6e23e363c87d0377046b183fd8064cd75572e1", size = 6321239, upload-time = "2025-04-28T22:12:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/65/fa/5e72cdb37469264ec29e7c7d3b2612a0b7fe38156dd1a232870def6b6a3b/mujoco-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7e19ef4ded262d9ff454ac9346203dcb35a6f794e7d9406ee803f1c73702560", size = 6611583, upload-time = "2025-04-28T22:13:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ce/6c4474f9a5d71ecc058c96874b7c757c73806bb1e9b23829f4746aa3a1cb/mujoco-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:c2539ad7b4978ac9a48d3b768de4f0ece716ea8533bd2988e6a1bceaf033954a", size = 4949316, upload-time = "2025-04-28T22:13:04.923Z" }, + { url = "https://files.pythonhosted.org/packages/49/1f/a9e26057c0bcef69b40b3acf1ecd80fb5683b954121873c8b19e1d9c961d/mujoco-3.3.2-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:bc856ab6b349c269c67e834d4e2e20e8193c5caa0d1337a6d13374660c3a769e", size = 6653107, upload-time = "2025-04-28T22:13:07.92Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9c/7259a5c7ee3462c076970ed8705a3122867692ac4cb1d4876d2768d3cc00/mujoco-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df9a1f48aff2888a7911a882baefc2ed227e46c1bec5d5dd31ac027361b6aba1", size = 6557148, upload-time = "2025-04-28T22:13:11.669Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/29e48aa328124025886a042481a0dcf480ebc2cba1bb7c27955b0f42b778/mujoco-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e673742a8024381669fe2830a0cf029ada818c5c0327b5863dc2dcb99af288dc", size = 6330787, upload-time = "2025-04-28T22:13:15.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/a4d4e829666f3fbc63fa425cc0fa10452f042544165b6a487215d9fd92db/mujoco-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cfb4b21aea627b2e47a964935c24cc5664325886973bd4aa2d1866f3e1ba7c", size = 6687174, upload-time = "2025-04-28T22:13:19.037Z" }, + { url = "https://files.pythonhosted.org/packages/de/05/257df55bc65365708f56fae4ae864780918bea568fee262ab636fb4dde33/mujoco-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:1a1c412580c1c6c817ebfce397e9a212ca6a8bd1b13f90c2d3bd26f1c0ab6c6e", size = 4994918, upload-time = "2025-04-28T22:13:22.386Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/83cfaab1953d9f8c9764d944c02d728b2980920a1ba1f183308846990fd7/mujoco-3.3.2-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:d19b582c496db9a7e4c4084d78e7378aa90f8044881db203d44bcc3fe5bf5315", size = 6653378, upload-time = "2025-04-28T22:13:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/32aed80d7feb5def1571c7171294890e2969c8b27f5f6b2531a551bf8552/mujoco-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf65755576fa4ba9e054b5ad026452d2e207dd8c80e63739fd6ea8a52aef0662", size = 6556985, upload-time = "2025-04-28T22:13:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/ba600a34b61707d1a6bdd51bb65e6fdeb4c9291e5295b93f4a1be51d58da/mujoco-3.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65b31a8486677a9c32a9e400409983f356c3e17f2555111aaf2525ca20292e24", size = 6330705, upload-time = "2025-04-28T22:13:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d3/6bc8f329ffd1a32270530802c533b0b048db1741c96682c2d0045fcf9f01/mujoco-3.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5697d32f73240d893b9ef41ca754a3baf5ddb7c582dca7191ee43fa316737ef1", size = 6687999, upload-time = "2025-04-28T22:13:36.533Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/197765f4517308c68b0f278439835d656b9eb601300c58cc846f67dbc46b/mujoco-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:f549aa3319e3814a043dbba502a0611e74933800ae8847191e65d3e7da9828ec", size = 4995270, upload-time = "2025-04-28T22:13:40.202Z" }, +] + +[[package]] +name = "multi-storage-client" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "jmespath" }, + { name = "jsonschema" }, + { name = "lark", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "lark", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or extra != 'group-7-cosmos3-vllm'" }, + { name = "opentelemetry-api" }, + { name = "prettytable" }, + { name = "psutil" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "tzdata" }, + { name = "wcmatch" }, + { name = "xattr" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/2a/4041592b18f9a84353974bb89124991b27c9c65d3b47324a161f6edad11f/multi_storage_client-0.44.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:28e68b3ac8475fbc373707eb279e8f3bdfc297df836794ed986ddd6b6ae52a05", size = 9058162, upload-time = "2026-03-06T22:01:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/69/89/b81846327d51d34635cba8eed047175706aca19bd53495ff1d97777d3ad0/multi_storage_client-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7120603e9b1936c1919493bfdbfc80084f08d2411b82f09e6384525338666451", size = 5400815, upload-time = "2026-03-06T22:03:06.163Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/26640a2d8883719da42c5516962dfa5b71fbdd2e2e2ad3b4d489039233b0/multi_storage_client-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9c163d1c5c870df1adbc993d58f884006fcd62727fb90b4a5c429373f62778", size = 5604930, upload-time = "2026-03-06T21:57:19.147Z" }, + { url = "https://files.pythonhosted.org/packages/80/54/1489bf146831c73dec8b620fba0d9651a8506f0e7513f2b1bafa5bf3dd30/multi_storage_client-0.44.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:fa900ac3b781707feba16c8da2562ec84ca4f1b7d99c7bce88b6311882e0cb22", size = 9057010, upload-time = "2026-03-06T22:01:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/5c/22/fb4131638365a0d49393ebb2761608ed393e4b6fbeb661ff28cd82b3ce3a/multi_storage_client-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d06c99da4cbfb2f600d4ca6ae0f6daf9c6842f5f162e5fedb0f9cbd75c29d5f", size = 5401057, upload-time = "2026-03-06T22:01:09.262Z" }, + { url = "https://files.pythonhosted.org/packages/a2/26/6daf323ec45761b20ae1de6d9ef8b4013a17eae086b54ec225edb9e5bbf4/multi_storage_client-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81581731532dba0a40ea5ff0bb04e512b3ca458db69306902ffa1c047e02bfe0", size = 5606993, upload-time = "2026-03-06T22:02:42.929Z" }, + { url = "https://files.pythonhosted.org/packages/af/bd/6c66e4546ecda95da63b882eafa6d9659af032282f924b01e046ac057290/multi_storage_client-0.44.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:045a1973602b8eaacb1ab86d363c58d901d8768c6b0bb79c0088e417b3aabb0f", size = 9047082, upload-time = "2026-03-06T21:59:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/61/5a/3fd40749a360145e218a7a7bd8db562f650954404f55b8a219843ec709a2/multi_storage_client-0.44.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f3d5456acfac890fd5291a4776ab57c38ffecceb160ede324836c2a02427d18", size = 5400355, upload-time = "2026-03-06T22:02:19.753Z" }, + { url = "https://files.pythonhosted.org/packages/41/c8/aa938f0602fd7d73dc541c1783d3ce2ddb5f9ea2121122f83e75f946c014/multi_storage_client-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a0a0f8951d722b719cb7d68cd36d639bad3664cf5ac823611b83056541e7b8f", size = 5610085, upload-time = "2026-03-06T21:57:42.001Z" }, + { url = "https://files.pythonhosted.org/packages/a5/cf/47ac58a9ce3913fc43feef4f2d327a07367c171c5d7f0f684eb5f2df9a6e/multi_storage_client-0.44.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f82f58b71dbcb07516fa1732ebd7df53cd62e65a9e50d694a1db6bb0cb5fbd29", size = 9044542, upload-time = "2026-03-06T21:58:37.73Z" }, + { url = "https://files.pythonhosted.org/packages/45/62/53b69c19554146345d33cb20cdae1516e2f2e5a35edee99a7c0ce58c7720/multi_storage_client-0.44.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd49feebdcd513a0f35701b8af03eb653f1b0efac7e8d686ee6c1bcdc40cbe5", size = 5399626, upload-time = "2026-03-06T21:58:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/055349d323aa74e8b792d99d731660f52d0be7b16c2072486773b11b5fa7/multi_storage_client-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17612cc379be566d6abb81a6e3a4732ee73a30717a6e2d3432db071c7b4e3d13", size = 5609202, upload-time = "2026-03-06T21:56:55.451Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] +fsspec = [ + { name = "fsspec" }, +] +google-cloud-storage = [ + { name = "google-cloud-storage" }, +] +observability-otel = [ + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, +] +vault = [ + { name = "hvac" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503, upload-time = "2025-04-17T03:11:27.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f8/7f9a8f08bf98cea1dfaa181e05cc8bbcb59cecf044b5a9ac3cce39f9c449/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df", size = 135083, upload-time = "2025-04-17T03:11:04.223Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/b7b10dbfc17b2b3ce07d4d30b3ba8367d0ed32d6d46cd166e298f161dd46/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213", size = 135128, upload-time = "2025-04-17T03:11:06.045Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a3/5f8d3b9690ea5580bee5868ab7d7e2cfca74b7e826b28192b40aa3881cdc/multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2", size = 135132, upload-time = "2025-04-17T03:11:07.533Z" }, + { url = "https://files.pythonhosted.org/packages/55/4d/9af0d1279c84618bcd35bf5fd7e371657358c7b0a523e54a9cffb87461f8/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6", size = 144695, upload-time = "2025-04-17T03:11:09.161Z" }, + { url = "https://files.pythonhosted.org/packages/17/bf/87323e79dd0562474fad3373c21c66bc6c3c9963b68eb2a209deb4c8575e/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3", size = 144742, upload-time = "2025-04-17T03:11:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/74/cb8c831e58dc6d5cf450b17c7db87f14294a1df52eb391da948b5e0a0b94/multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797", size = 144745, upload-time = "2025-04-17T03:11:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948, upload-time = "2025-04-17T03:11:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462, upload-time = "2025-04-17T03:11:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287, upload-time = "2025-04-17T03:11:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917, upload-time = "2025-04-17T03:11:24.044Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636, upload-time = "2025-04-17T03:11:24.936Z" }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "natten" +version = "0.21.6.dev6+cu128.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/natten-0.21.6.dev6%2Bcu128.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:164b4cd16d36f747ddd19c6f0cb1cd0a275e758e01f2402d4e957be5d7818a0d" }, +] + +[[package]] +name = "natten" +version = "0.21.6.dev6+cu130.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/natten-0.21.6.dev6%2Bcu130.torch210-cp313-cp313-linux_aarch64.whl", hash = "sha256:d157abf86d3f01aa683ef6637abce4f1fdf2b7aa86b5af52736e5d53e9c42da5" }, + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/natten-0.21.6.dev6%2Bcu130.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:13d81645a33e58500c33f4c8840c992cab1bf43b2f476b96e7a70c3864906511" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numba" +version = "0.61.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" }, +] + +[[package]] +name = "numcodecs" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/56/8895a76abe4ec94ebd01eeb6d74f587bc4cddd46569670e1402852a5da13/numcodecs-0.13.1.tar.gz", hash = "sha256:a3cf37881df0898f3a9c0d4477df88133fe85185bffe57ba31bcc2fa207709bc", size = 5955215, upload-time = "2024-10-09T16:28:00.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/c0/6d72cde772bcec196b7188731d41282993b2958440f77fdf0db216f722da/numcodecs-0.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96add4f783c5ce57cc7e650b6cac79dd101daf887c479a00a29bc1487ced180b", size = 1580012, upload-time = "2024-10-09T16:27:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/f81fc1fa9210bbea97258242393a1f9feab4f6d8fb201f81f76003005e4b/numcodecs-0.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:237b7171609e868a20fd313748494444458ccd696062f67e198f7f8f52000c15", size = 1176919, upload-time = "2024-10-09T16:27:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/16/e4/b9ec2f4dfc34ecf724bc1beb96a9f6fa9b91801645688ffadacd485089da/numcodecs-0.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96e42f73c31b8c24259c5fac6adba0c3ebf95536e37749dc6c62ade2989dca28", size = 8625842, upload-time = "2024-10-09T16:27:24.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/90/299952e1477954ec4f92813fa03e743945e3ff711bb4f6c9aace431cb3da/numcodecs-0.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:eda7d7823c9282e65234731fd6bd3986b1f9e035755f7fed248d7d366bb291ab", size = 828638, upload-time = "2024-10-09T16:27:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/34b8e869ef143e88d62e8231f4dbfcad85e5c41302a11fc5bd2228a13df5/numcodecs-0.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2eda97dd2f90add98df6d295f2c6ae846043396e3d51a739ca5db6c03b5eb666", size = 1580199, upload-time = "2024-10-09T16:27:29.336Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/f70797d86bb585d258d1e6993dced30396f2044725b96ce8bcf87a02be9c/numcodecs-0.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a86f5367af9168e30f99727ff03b27d849c31ad4522060dde0bce2923b3a8bc", size = 1177203, upload-time = "2024-10-09T16:27:31.011Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d14ad69b63fde041153dfd05d7181a49c0d4864de31a7a1093c8370da957/numcodecs-0.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233bc7f26abce24d57e44ea8ebeb5cd17084690b4e7409dd470fdb75528d615f", size = 8868743, upload-time = "2024-10-09T16:27:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/27a7b5af0b33f6d61e198faf177fbbf3cb83ff10d9d1a6857b7efc525ad5/numcodecs-0.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:796b3e6740107e4fa624cc636248a1580138b3f1c579160f260f76ff13a4261b", size = 829603, upload-time = "2024-10-09T16:27:35.415Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/bc09808425e7d3df41e5fc73fc7a802c429ba8c6b05e55f133654ade019d/numcodecs-0.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5195bea384a6428f8afcece793860b1ab0ae28143c853f0b2b20d55a8947c917", size = 1575806, upload-time = "2024-10-09T16:27:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/dc74d0bfdf9ec192332a089d199f1e543e747c556b5659118db7a437dcca/numcodecs-0.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3501a848adaddce98a71a262fee15cd3618312692aa419da77acd18af4a6a3f6", size = 1178233, upload-time = "2024-10-09T16:27:40.169Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/434e8e3970b8e92ae9ab6d9db16cb9bc7aa1cd02e17c11de6848224100a1/numcodecs-0.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2230484e6102e5fa3cc1a5dd37ca1f92dfbd183d91662074d6f7574e3e8f53", size = 8857827, upload-time = "2024-10-09T16:27:42.743Z" }, + { url = "https://files.pythonhosted.org/packages/83/e7/1d8b1b266a92f9013c755b1c146c5ad71a2bff147ecbc67f86546a2e4d6a/numcodecs-0.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:e5db4824ebd5389ea30e54bc8aeccb82d514d28b6b68da6c536b8fa4596f4bca", size = 826539, upload-time = "2024-10-09T16:27:44.808Z" }, + { url = "https://files.pythonhosted.org/packages/83/8b/06771dead2cc4a8ae1ea9907737cf1c8d37a323392fa28f938a586373468/numcodecs-0.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a60d75179fd6692e301ddfb3b266d51eb598606dcae7b9fc57f986e8d65cb43", size = 1571660, upload-time = "2024-10-09T16:27:47.125Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/d925bf85f92dfe4635356018da9fe4bfecb07b1c72f62b01c1bc47f936b1/numcodecs-0.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f593c7506b0ab248961a3b13cb148cc6e8355662ff124ac591822310bc55ecf", size = 1169925, upload-time = "2024-10-09T16:27:49.512Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/643a3839d571d8e439a2c77dc4b0b8cab18d96ac808e4a81dbe88e959ab6/numcodecs-0.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80d3071465f03522e776a31045ddf2cfee7f52df468b977ed3afdd7fe5869701", size = 8814257, upload-time = "2024-10-09T16:27:52.059Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c5/f3e56bc9b4e438a287fff738993d6d11abef368c0328a612ac2842ba9fca/numcodecs-0.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:90d3065ae74c9342048ae0046006f99dcb1388b7288da5a19b3bddf9c30c3176", size = 821887, upload-time = "2024-10-09T16:27:55.039Z" }, +] + +[[package]] +name = "numcodecs" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/85/1ac101a40ead81eaa1c7dc49a8827a30e2e436211b43ebdc63c590eb1347/numcodecs-0.16.5-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:78382dcea50622f2ef1e6e7a71dbe7f861d8fe376b27b7c297c26907304fef1e", size = 1621795, upload-time = "2025-11-21T02:49:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2d04a19cb57a3c519b4127ac377cca6471aee1990d7c18f5b1e3a4fe1306689", size = 1153030, upload-time = "2025-11-21T02:49:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/e120ee1b390730ac5987cde2afd82e2b8442cec315ab40b94b0373e93e73/numcodecs-0.16.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c043af648eb280cd61785c99c22ff5c3c3460f906eb51a8511327c4f5111b283", size = 8510503, upload-time = "2025-11-21T02:49:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c398919ef2eb0e56b8e97456f622640bfd3deed06de3acc976989cbcb22628a3", size = 9123428, upload-time = "2025-11-21T02:49:22.328Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/af02c417954f46e5c7bd5163ac251f535877d909fce54861c99ae197f6f6/numcodecs-0.16.5-cp311-cp311-win_amd64.whl", hash = "sha256:3820860ed302d4d84a1c66e70981ff959d5eb712555be4e7d8ced49888594773", size = 801542, upload-time = "2025-11-21T02:49:24.265Z" }, + { url = "https://files.pythonhosted.org/packages/75/cc/55420f3641a67f78392dc0bc5d02cb9eb0a9dcebf2848d1ac77253ca61fa/numcodecs-0.16.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:24e675dc8d1550cd976a99479b87d872cb142632c75cc402fea04c08c4898523", size = 1656287, upload-time = "2025-11-21T02:49:25.755Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6c/86644987505dcb90ba6d627d6989c27bafb0699f9fd00187e06d05ea8594/numcodecs-0.16.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ddfa4341d1a3ab99989d13b01b5134abb687d3dab2ead54b450aefe4ad5bd6", size = 1148899, upload-time = "2025-11-21T02:49:26.87Z" }, + { url = "https://files.pythonhosted.org/packages/97/1e/98aaddf272552d9fef1f0296a9939d1487914a239e98678f6b20f8b0a5c8/numcodecs-0.16.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b554ab9ecf69de7ca2b6b5e8bc696bd9747559cb4dd5127bd08d7a28bec59c3a", size = 8534814, upload-time = "2025-11-21T02:49:28.547Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/78c98ef5c8b2b784453487f3e4d6c017b20747c58b470393e230c78d18e8/numcodecs-0.16.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad1a379a45bd3491deab8ae6548313946744f868c21d5340116977ea3be5b1d6", size = 9173471, upload-time = "2025-11-21T02:49:30.444Z" }, + { url = "https://files.pythonhosted.org/packages/1c/20/2fdec87fc7f8cec950d2b0bea603c12dc9f05b4966dc5924ba5a36a61bf6/numcodecs-0.16.5-cp312-cp312-win_amd64.whl", hash = "sha256:845a9857886ffe4a3172ba1c537ae5bcc01e65068c31cf1fce1a844bd1da050f", size = 801412, upload-time = "2025-11-21T02:49:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/38/38/071ced5a5fd1c85ba0e14ba721b66b053823e5176298c2f707e50bed11d9/numcodecs-0.16.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25be3a516ab677dad890760d357cfe081a371d9c0a2e9a204562318ac5969de3", size = 1654359, upload-time = "2025-11-21T02:49:33.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0107e839ef75b854e969cb577e140b1aadb9847893937636582d23a2a4c6ce50", size = 1144237, upload-time = "2025-11-21T02:49:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/787ea5f237b8ea7bc67140c99155f9c00b5baf11c49afc5f3bfefa298f95/numcodecs-0.16.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:015a7c859ecc2a06e2a548f64008c0ec3aaecabc26456c2c62f4278d8fc20597", size = 8483064, upload-time = "2025-11-21T02:49:36.454Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84230b4b9dad2392f2a84242bd6e3e659ac137b5a1ce3571d6965fca673e0903", size = 9126063, upload-time = "2025-11-21T02:49:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl", hash = "sha256:5088145502ad1ebf677ec47d00eb6f0fd600658217db3e0c070c321c85d6cf3d", size = 799275, upload-time = "2025-11-21T02:49:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/38e7ca8184c958b51f45d56a4aeceb1134ecde2d8bd157efadc98502cc42/numcodecs-0.16.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b05647b8b769e6bc8016e9fd4843c823ce5c9f2337c089fb5c9c4da05e5275de", size = 1654721, upload-time = "2025-11-21T02:49:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/260fa42e7b2b08e6e00ad632f8dd620961a60a459426c26cea390f8c68d0/numcodecs-0.16.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3832bd1b5af8bb3e413076b7d93318c8e7d7b68935006b9fa36ca057d1725a8f", size = 1146887, upload-time = "2025-11-21T02:49:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/4e/15/e2e1151b5a8b14a15dfd4bb4abccce7fff7580f39bc34092780088835f3a/numcodecs-0.16.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f7b7d24f103187f53135bed28bb9f0ed6b2e14c604664726487bb6d7c882e1", size = 8476987, upload-time = "2025-11-21T02:49:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/6d/30/16a57fc4d9fb0ba06c600408bd6634f2f1753c54a7a351c99c5e09b51ee2/numcodecs-0.16.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aec9736d81b70f337d89c4070ee3ffeff113f386fd789492fa152d26a15043e4", size = 9102377, upload-time = "2025-11-21T02:49:45.508Z" }, + { url = "https://files.pythonhosted.org/packages/31/a5/a0425af36c20d55a3ea884db4b4efca25a43bea9214ba69ca7932dd997b4/numcodecs-0.16.5-cp314-cp314-win_amd64.whl", hash = "sha256:b16a14303800e9fb88abc39463ab4706c037647ac17e49e297faa5f7d7dbbf1d", size = 819022, upload-time = "2025-11-21T02:49:47.39Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "nvdlfw-inspect" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/86/94188e03e5d4dd7b73c390b0cddcde5618b3799c18e327b2bf15763f6137/nvdlfw_inspect-0.2.2-py3-none-any.whl", hash = "sha256:8a4dc2814c5a4cd19ae304170b9bfa514538ef3c3eb243a45a82404ec3cb279d", size = 30964, upload-time = "2025-12-03T10:52:01.933Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, + { url = "https://files.pythonhosted.org/packages/10/f5/f50bc3f5c2bb57ab8f5b4d78bc1146b57810d42cb8fcb28cbe2e14050376/nvidia_cublas-13.1.0.3-py3-none-win_amd64.whl", hash = "sha256:2a3b94a37def342471c59fad7856caee4926809a72dd5270155d6a31b5b277be", size = 404355960, upload-time = "2025-10-09T09:07:00.987Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/61/7d7b3c70186fb651d0fbd35b01dbfc8e755f69fd58f817f3d0f642df20c3/nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af", size = 567544208, upload-time = "2025-03-07T01:53:30.535Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/b74b10025c1205695c5676373f2edd3e87a7202cc62ead0dfbc373b0f6ea/nvidia_cuda_cupti-13.0.85-py3-none-win_amd64.whl", hash = "sha256:683f58d301548deeefcb8f6fac1b8d907691b9d8b18eccab417f51e362102f00", size = 7736776, upload-time = "2025-09-04T08:38:08.38Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/83f5426095d93694ae39fe1311431b5d5a9bb82e48bf0dd8e19be2765942/nvidia_cuda_cupti_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e", size = 7015759, upload-time = "2025-03-07T01:51:11.355Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/4a/af/345fedb9f4c76c84ab4fa445b36bd4048a4d9db60e6bc76b4f913ff4b852/nvidia_cuda_nvrtc-13.0.88-py3-none-win_amd64.whl", hash = "sha256:6bcd4e7f8e205cbe644f5a98f2f799bef9556fefc89dd786e79a16312ce49872", size = 76807835, upload-time = "2025-09-04T08:39:15.274Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/45/51/52a3d84baa2136cc8df15500ad731d74d3a1114d4c123e043cb608d4a32b/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909", size = 73586838, upload-time = "2025-03-07T01:52:13.483Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/b7/94/6b867483bec07da24ffa32736c79fabb94ef3a7af4d787a9d4a974868576/nvidia_cuda_runtime-13.0.96-py3-none-win_amd64.whl", hash = "sha256:f79298c8a098cec150a597c8eba58ecdab96e3bdc4b9bc4f9983635031740492", size = 2927037, upload-time = "2025-10-09T09:04:23.782Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/a515b7600ad361ea14bfa13fb4d6687abf500adc270f19e89849c0590492/nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8", size = 944318, upload-time = "2025-03-07T01:51:01.794Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/3d/90/0bd6e586701b3a890fd38aa71c387dab4883d619d6e5ad912ccbd05bfd67/nvidia_cudnn_cu12-9.10.2.21-py3-none-win_amd64.whl", hash = "sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e", size = 692992268, upload-time = "2025-06-06T21:55:18.114Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.15.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/93/b3c9db2c35d6183361333d2dcfea50e094c012d012c8a4d7effbfb53ef62/nvidia_cudnn_cu13-9.15.1.9-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:44cd2ec83c3ef62a7357614bd02ce7f3dac35ffcbb04ad20999e730741f0ba17", size = 415636241, upload-time = "2025-11-12T20:22:26.582Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d0/90f98fc55c48a7d8f5ad0a03a6321acc1a7024bdd550d96b3547a04ea6b4/nvidia_cudnn_cu13-9.15.1.9-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ebc9a647918df0d7298d67cfaf41579fd4c78ead9aba246f5ad9414d61b9ec4c", size = 351298418, upload-time = "2025-11-12T20:23:21.455Z" }, + { url = "https://files.pythonhosted.org/packages/01/e4/f899b1cd1b3be20e8a5065fba3670d492f634da07eba5e09425bc96647be/nvidia_cudnn_cu13-9.15.1.9-py3-none-win_amd64.whl", hash = "sha256:e99068ca372b9791483759705ff79545e6e0ab5fe6352772e084cc6d1a419ab6", size = 336136012, upload-time = "2025-11-12T20:24:43.179Z" }, +] + +[[package]] +name = "nvidia-cudnn-frontend" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/be/f5a1e633c524c13c0182213ab27dab42dca29a3c785be5ff74d2d185aed1/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:baa6fbc8e7c55f1c78c0374ed9a890e1cf81acaca0c92d6135d18a8e3c985244", size = 2023500, upload-time = "2026-01-27T23:31:34.747Z" }, + { url = "https://files.pythonhosted.org/packages/82/a7/765a17c6a9496196c34f269d17dfb902b6c618c0261c0962511e95302e81/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4bcca42259e358002c8867e3624a558f66cd5dff2cc6c3aafd860ef2f41730", size = 2154278, upload-time = "2026-01-27T23:06:55.784Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/7caae2243540bc60e47eae95f0fd913c9baa05cf94df0471914f70d45158/nvidia_cudnn_frontend-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:06252021ef1e5a7256f1e70429a426b01792636c05cc547fe8e64c6885a9652e", size = 1590158, upload-time = "2026-01-27T23:08:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9a/83d3d080118de4a7810fa019349edec634b8b37b9cafaacd05719de62dd6/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6d4d0b88d617b233a503c84980b54d840b60b2734497d1a7a071ec5293daec2", size = 2023709, upload-time = "2026-01-27T23:32:10.912Z" }, + { url = "https://files.pythonhosted.org/packages/13/c7/c3624b3ed77b102618f26295e816b27f1c3ebb1143730237a9f51d403c3f/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382ea063b92cbfd5b442cb75ff8422932d78276aecf139e46713ed1ad3d07af4", size = 2155568, upload-time = "2026-01-27T23:07:13.277Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/8613dfd029d076b86a8a87efe3f4bb4ab73cec15fa8fc27e665098f4d167/nvidia_cudnn_frontend-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:baa509effc4d299d3f04e549d4188f88bca8a8b527f483cbd2f66bc18f13a8b1", size = 1591244, upload-time = "2026-01-27T23:08:44.691Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b4/604e230378680ee117849a4e1045baca092f93161a829291a84d5acce70c/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:310b417f2848a83d1437203fcaeea320a74fb7f28af20bf42bf5afc9c01f1c12", size = 2027408, upload-time = "2026-01-27T23:32:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/c6/52/08f98262e77b1cbcc834cc1a5db494d0661ea1dbdea58c2e2d51a57fdaca/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c023539ca6de99234cf5102c3ec0d6af817f5396fc93028a22ba5b834a35b8a", size = 2159245, upload-time = "2026-01-27T23:07:32.664Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/751a5a8cfdc95fb4dc556192d37369ae488c30c473fe9a3ec720b23d07ea/nvidia_cudnn_frontend-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:e13f7dd46cdb4762dde87f181f06d1c5e15e9478bbdd547bfa74d9b11f415aae", size = 1591041, upload-time = "2026-01-27T23:09:04.118Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/db791a26ebb6a6e1268f518e18c82d8ad18546f7008f4b0d5bde15f927de/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a6e2b7bd43705ffa4af3b187374fdd5e7d09fc228a4d65fc8b4b0a537a8e605", size = 2027249, upload-time = "2026-01-27T23:33:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/19/74/3038cf496d5de7cfdff730f5202e438c17d9123de507059340e02ddff9d7/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0544206b02cae9da4f044ca3fe7416b99e0c8a8052285dd3e5a8fc445d34f9c", size = 2160001, upload-time = "2026-01-27T23:07:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5e/148cc6609dba326e620e4d949246020dfba05ca07d0387442e62b71d19b6/nvidia_cudnn_frontend-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:7eefa5f10cc003df5f3593f82f1ee6c001fc3412bdc78430c751914dfceefd7f", size = 1591270, upload-time = "2026-01-27T23:09:21.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0a/515209dd2afc6027bf1112bf415f575bfe9628d18877abe7424cb597dd7b/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b489da1b30f1d7da822b37b89cc4f68afd80e020eb57e4ab24921f8b57f6e946", size = 2028689, upload-time = "2026-02-11T21:32:04.235Z" }, + { url = "https://files.pythonhosted.org/packages/ab/57/52d18e1f50979eeabfafb408ec73068afc5a1e1ccd21636240317cd456d4/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37688c81a34ac590aff9de4c34d2968bab949411af707baa327616ebd4b34ae1", size = 2160182, upload-time = "2026-02-11T21:25:18.437Z" }, + { url = "https://files.pythonhosted.org/packages/67/53/df2810b56d259ef96fa6beaa1381bd14c29fbe82836b409516e864c5e177/nvidia_cudnn_frontend-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:5053b473fa74168b5fbf35934cd6187f88aa03b8447b9f2cd417332d5e5c9569", size = 1592759, upload-time = "2026-02-11T21:32:33.87Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/f8af21a2ed1beed337a6a02c5a28aeb85441f4d578ec3d529543c775ea4b/nvidia_cufft-12.0.0.61-py3-none-win_amd64.whl", hash = "sha256:2abce5b39d2f5ae12730fb7e5db6696533e36c26e2d3e8fd1750bdd2853364eb", size = 213342123, upload-time = "2025-09-04T08:40:51.145Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/ce1629f1e478bb5ccd208986b5f9e0316a78538dd6ab1d0484f012f8e2a1/nvidia_cufft_cu12-11.3.3.83-py3-none-win_amd64.whl", hash = "sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7", size = 192216559, upload-time = "2025-03-07T01:53:57.106Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, + { url = "https://files.pythonhosted.org/packages/99/27/72103153b1ffc00e09fdc40ac970235343dcd1ea8bd762e84d2d73219ffa/nvidia_curand-10.4.0.35-py3-none-win_amd64.whl", hash = "sha256:65b1710aa6961d326b411e314b374290904c5ddf41dc3f766ebc3f1d7d4ca69f", size = 55242481, upload-time = "2025-08-04T10:30:41.831Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, + { url = "https://files.pythonhosted.org/packages/b9/75/70c05b2f3ed5be3bb30b7102b6eb78e100da4bbf6944fd6725c012831cab/nvidia_curand_cu12-10.3.9.90-py3-none-win_amd64.whl", hash = "sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec", size = 62765309, upload-time = "2025-03-07T01:54:20.478Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparse", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvjitlink", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, + { url = "https://files.pythonhosted.org/packages/99/ef/332a0101260ca78a1daef046bf0b06199e8ed4dac1d2aa698289c358169c/nvidia_cusolver-12.0.4.66-py3-none-win_amd64.whl", hash = "sha256:16515bd33a8e76bb54d024cfa068fa68d30e80fc34b9e1090813ea9362e0cb65", size = 193551444, upload-time = "2025-09-04T08:41:46.813Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparse-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvjitlink-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/76ca8551b8a84146ffa189fec81c26d04adba4bc0dbe09cd6e6fd9b7de04/nvidia_cusolver_cu12-11.7.3.90-py3-none-win_amd64.whl", hash = "sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34", size = 256720438, upload-time = "2025-03-07T01:54:39.898Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, + { url = "https://files.pythonhosted.org/packages/02/b0/b043d6f3480f102f885cf87fc3ffd3edcb5e23b855025a50e2ef4d059185/nvidia_cusparse-12.6.3.3-py3-none-win_amd64.whl", hash = "sha256:cbcf42feb737bd7ec15b4c0a63e62351886bd3f975027b8815d7f720a2b5ea79", size = 143783033, upload-time = "2025-09-04T08:42:12.391Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/62/07/f3b2ad63f8e3d257a599f422ae34eb565e70c41031aecefa3d18b62cabd1/nvidia_cusparse_cu12-12.5.8.93-py3-none-win_amd64.whl", hash = "sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd", size = 284937404, upload-time = "2025-03-07T01:55:07.742Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d8/a6b0d0d0c2435e9310f3e2bb0d9c9dd4c33daef86aa5f30b3681defd37ea/nvidia_cusparselt_cu12-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075", size = 271020911, upload-time = "2025-02-26T00:14:47.204Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/8f0578928b9b1246d7b1324db0528e6b9f9fb54496a49f40bf71f09f1a27/nvidia_cusparselt_cu13-0.8.0-py3-none-win_amd64.whl", hash = "sha256:e80212ed7b1afc97102fbb2b5c82487aa73f6a0edfa6d26c5a152593e520bb8f", size = 156459710, upload-time = "2025-08-13T19:24:18.043Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl" +version = "4.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cutlass-dsl-libs-base" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/03/678dab0383db1ddfc449da216220f40404189eb36eeed9d87a4fa4bdb0e6/nvidia_cutlass_dsl-4.4.2-py3-none-any.whl", hash = "sha256:7cfb9ef19062b055b9372c7a627004724e2755e4c8b16c3cc88807d64501a4ae", size = 10167, upload-time = "2026-03-16T02:18:59.043Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/07/af1b456b5b6dd4a49e71a952a182a99fc863f70b9f78725324f89e0384e5/nvidia_cutlass_dsl_libs_base-4.4.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06acb3acff3dcf4bf6630476efac7de94de30b988ded4fa00b647bbcec4224ff", size = 75471025, upload-time = "2026-03-16T02:23:49.61Z" }, + { url = "https://files.pythonhosted.org/packages/b1/12/f0770811d2874af7e04623d3baa83c445c49f38c00c4e5d20e1daae54b5d/nvidia_cutlass_dsl_libs_base-4.4.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:916bf612fba5fbc5162e300fe18196e960dac2328c1c1360c0939d3be05c7c71", size = 74355272, upload-time = "2026-03-16T02:24:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/b9d0fd1ba281b111c941d9616dd9f98a509d84bf35076e60fef27ec7abd6/nvidia_cutlass_dsl_libs_base-4.4.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:261832dafe7579dc83cd3816ab9ea845e3de3737d876c215f01fb4edff1f4473", size = 75476977, upload-time = "2026-03-16T02:26:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/86dda6d69a3fc29d0cde2a8b54c056ad69b73a6e5e230e18d906d2ec3b7c/nvidia_cutlass_dsl_libs_base-4.4.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40c2352b2fcc80789a216cbeb9b2ee10c85c15de839cda8f5c1d18166b8249df", size = 74356100, upload-time = "2026-03-16T02:26:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/0df5e38d11e52cc72095a14d6448bc1c5d0d4b00b069a1189ca417fb225b/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2ec8812eeadcbb6fe20bda2e295ed9c00653f8253b78e33cf0ab65a47b829e73", size = 75473821, upload-time = "2026-03-16T02:27:08.371Z" }, + { url = "https://files.pythonhosted.org/packages/56/98/e264964741d9cc9816625d9600d17a5249fd5cbd8c2d166fb0d0c34dfe5a/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:22e37b58f7a6f2f43bba533c4df8a088012122e0b4e9a632eca23937adeafb39", size = 74355593, upload-time = "2026-03-16T02:25:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c9/2f17950ee2deb4b5f6b82f8155515a21792fe296e81bb638f164d8e2ca9b/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b59a052cbfb9a25747d1b6d413615456bea38d1f377da085af07c0d86a4c8b39", size = 75477304, upload-time = "2026-03-16T02:27:35.645Z" }, + { url = "https://files.pythonhosted.org/packages/e1/68/27380038ebd9c8eab4be364e833fea144aef597704f44948921668f7adf4/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8e3324a33afa7424e93beae7e54a311e80db82b9e4ed4bba2aeeda1d6c888cd9", size = 74355765, upload-time = "2026-03-16T02:24:16.778Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/0dc7f2e5b5c65106a5bb05e60654f1a79abe92e27e9b00588a73cd26ca1f/nvidia_cutlass_dsl_libs_base-4.4.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:af96c1170569138b3cb965202907fbf5ab95d7c1dcc210952d00cdf9ab7b859a", size = 75472171, upload-time = "2026-03-16T02:28:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ae/0998f328b28b956d7eb399d16f4ee681ca318b306007264444a623e86c64/nvidia_cutlass_dsl_libs_base-4.4.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:95db0c8d1d56992e2f5c2dcd5b3baab0297bedc0cbcefc1e70b57acd934e7b23", size = 74356280, upload-time = "2026-03-16T02:25:43.789Z" }, +] + +[[package]] +name = "nvidia-dali-cuda120" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astunparse" }, + { name = "dm-tree" }, + { name = "gast" }, + { name = "makefun" }, + { name = "numpy" }, + { name = "nvidia-libnvcomp-cu12" }, + { name = "nvidia-nvimgcodec-cu12", extra = ["all"] }, + { name = "nvtx" }, + { name = "packaging" }, + { name = "six" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/f9/af5c0888c53cea8d869c54d454c3c97b9698ebe24add01abcee4febb1abd/nvidia_dali_cuda120-2.0.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:afbde358aeccc508ad718789d83481cc0b6e54d6fa876326955103027cb6a948", size = 293086967, upload-time = "2026-03-02T17:57:02.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a0/b6f70f0a27591aada92011997d0edb59017bdddd096e1e6c96646ca7307f/nvidia_dali_cuda120-2.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:db05cd32ff79ef7d95a773867e4e49f1077ba9821cb673e15df1443777bc575c", size = 418294681, upload-time = "2026-03-03T06:57:31.654Z" }, +] + +[[package]] +name = "nvidia-libnvcomp-cu12" +version = "5.1.0.21" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/23/b20f2381c7e92c704386428fe79736a13c50f452376453fdc60fcc0ec1b0/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:77dfb3cb8c8995dfa0279ba99b0501e03cbe77e876aab44f4693abdcfac549ce", size = 28802614, upload-time = "2025-12-02T19:05:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/08/ab/844fcbaa46cc1242632b4b94b4ffc210ec3d8d8f30ad8f7f1c27767389a9/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:68de61183edb9a870c9a608273a2b5da97dea18e3552096c61fafd9bb2689db0", size = 28958714, upload-time = "2025-12-02T19:01:40.466Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/c6e92d9587b9ad63c08b1b94c5ae2216319491d0bd4f40f2a9a431d4841f/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-win_amd64.whl", hash = "sha256:1352c7c4264ee5357f8f20e4a8da7f2f91debe21d8968f44576a7f4b51f91533", size = 28490640, upload-time = "2025-12-02T19:07:28.096Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.590.48" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/a0/f4fc18cf72f06821a9a665085435b901449986855519d5b3843532db35e9/nvidia_ml_py-13.590.48.tar.gz", hash = "sha256:8184d1be52914ac7f0991cd1c0d946c65dc88a840c754cd12c274b77b88760dd", size = 49732, upload-time = "2026-01-22T01:14:56.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/72/fb2af0d259a651affdce65fd6a495f0e07a685a0136baf585c5065204ee7/nvidia_ml_py-13.590.48-py3-none-any.whl", hash = "sha256:fd43d30ee9cd0b7940f5f9f9220b68d42722975e3992b6c21d14144c48760e43", size = 50680, upload-time = "2026-01-22T01:14:55.281Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvimgcodec-cu12" +version = "0.7.0.11" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/48/74d33dd126f84a4212480e2cf07504f457b5bae5acd33c0f6bf839ea17d4/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:52d834be8122bb5b8fc3151cc3bedb95368b3e7ac76af0c4561772ab2a847b2b", size = 27409358, upload-time = "2025-12-02T09:28:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/f06528ebcb82da84f4a96efe7a210c277767cb86ad2f61f8b1a17d17f251/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:32d3457859c5784e4c0f6a2f56b6a9afec8fe646cec1cbe4bb5c320948d92dfe", size = 33735220, upload-time = "2025-12-02T09:30:02.546Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/95b36049a9504d59d79929e9f3bec001b270f29aec8486e5fb9783a9502c/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-win_amd64.whl", hash = "sha256:495e07e071fcb2115f7f1948a04f6c51f96d61b83c614af753f7cc1bf369a46c", size = 18448810, upload-time = "2025-12-02T09:20:33.838Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "nvidia-libnvcomp-cu12" }, + { name = "nvidia-nvjpeg-cu12" }, + { name = "nvidia-nvjpeg2k-cu12" }, + { name = "nvidia-nvtiff-cu12" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, + { url = "https://files.pythonhosted.org/packages/e4/01/07530b0e37546231052e30234540289c42eaffa486f1a34a87fed340157b/nvidia_nvjitlink-13.0.88-py3-none-win_amd64.whl", hash = "sha256:634e96e3da9ef845ae744097a1f289238ecf946ce0b82e93cdce14b9782e682f", size = 36035115, upload-time = "2025-09-04T08:43:03.001Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d7/34f02dad2e30c31b10a51f6b04e025e5dd60e5f936af9045a9b858a05383/nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f", size = 268553710, upload-time = "2025-03-07T01:56:24.13Z" }, +] + +[[package]] +name = "nvidia-nvjpeg-cu12" +version = "12.4.0.76" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/5c12a3e6afe070ff563375cc72b42e9c7400bd0b44c734591049410be7fd/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f52c5ef7cf56e8bffac8903a59f14494017a52e4fe89d5a1d16c1e88d7bbf194", size = 5273693, upload-time = "2025-06-05T20:10:35.162Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/d3526394584134a23f2500833c62d3352e1feda7547041f4612b1a183aa3/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3888f10b32fbd58e80166c48e01073732d752fa5f167b7cb5b9615f1c6375a20", size = 5313609, upload-time = "2025-06-05T20:10:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/bc/28/e05bb8e6cdb98e79c6822f8bbd7154a26d8102412b3a0bfd5e4c7c52db8c/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-win_amd64.whl", hash = "sha256:21923726db667bd53050d0de88320983ff423322b7f376057dd943e487c40abc", size = 4741398, upload-time = "2025-06-05T20:16:19.152Z" }, +] + +[[package]] +name = "nvidia-nvjpeg2k-cu12" +version = "0.9.1.47" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/421625f754862b893c2f487090b4b6b86337801451f0623cda9d21d111b4/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f6787aed8f9d0c839ea4e0ae190af90bcc71a9a6b4e3965d5b67c22a00f58714", size = 7344958, upload-time = "2025-11-13T18:17:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/85/91/41abf44089ceb8b29479cdef2ca952277cc6667d40affedd39c3f1744d7e/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6672c85e47ab61ffe3d19da8a41fd597155852e6e219ddc90a133623b54f7818", size = 7402941, upload-time = "2025-11-13T18:13:28.977Z" }, + { url = "https://files.pythonhosted.org/packages/01/b2/ab62e6c008f3080743477de31da22eb83b374c37fe5d387e7435e507914f/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-win_amd64.whl", hash = "sha256:ebb5d34d68beb70c2718c769996d9d8e49a2d9acacc79f6235c07649a4045e97", size = 6973975, upload-time = "2025-11-13T18:25:26.611Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtiff-cu12" +version = "0.6.0.78" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/19/9529fbda1e7a24b45649c9bc86cf6490d5b53f63e6b17d851f1528ff8380/nvidia_nvtiff_cu12-0.6.0.78-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9193a46eaef2d52a92178c34e2404f621b581d651d2c7ab2d83c24fee6fcc136", size = 2478534, upload-time = "2025-11-13T18:26:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/62/4b/24805e9c56936dd57a1830b65b53234853f429cea5edbcbfdf853ceebdcf/nvidia_nvtiff_cu12-0.6.0.78-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b48517578de6f1a6e806e00ef0da6d673036957560efbe9fa2934707d5d18c00", size = 2518414, upload-time = "2025-11-13T18:16:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/1d818455e6c6182354fb5b17a6c9d7dcfb002e64e258554fe3410ea44510/nvidia_nvtiff_cu12-0.6.0.78-py3-none-win_amd64.whl", hash = "sha256:daf9035b5efc315ef904b449564d1d9d9a502f38e115cf5757d98f9c52a284d0", size = 2055719, upload-time = "2025-11-13T18:29:01.023Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d2/50/0e2220f8620a177de994211186ffc5bfa9f2ce1e1282797f8f90096f9f88/nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519", size = 137066, upload-time = "2025-09-04T08:39:25.649Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/9f/99/4c9c0c329bf9fc125008c3b54c7c94c0023518d06fc025ae36431375e1fe/nvidia_nvtx_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e", size = 56492, upload-time = "2025-03-07T01:52:24.69Z" }, +] + +[[package]] +name = "nvtx" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/03/b8a4391523a92163167fd0fee6769c223e8612043cb07aebc1173ca83fc9/nvtx-0.2.14.tar.gz", hash = "sha256:12945242a31bde70b1f15cae867f8706bdff290e2f808a11738e03ebefdf847f", size = 119864, upload-time = "2025-12-01T18:06:16.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/ca/fa76ea4985fd8f3d8c437bffec2580b1cac7f2401671089ac842610ae466/nvtx-0.2.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b70b2415ab97edf19514be226d5058898922c6b6bb1d7fdd5ef92d1e086f3e0f", size = 695204, upload-time = "2025-11-27T17:28:52.688Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1f/0aa62d52062d700dbed36dd2ebfddf5133c72180d448cce66545e5ccbe5d/nvtx-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23ab874f9c70e5433f39e40ca318ffcfc14fb43ed6798e6be5a30f74e4ca831f", size = 686698, upload-time = "2025-11-27T17:23:19.335Z" }, + { url = "https://files.pythonhosted.org/packages/18/c9/a12d48157221a8e939f3f7ec8f8a543e232fb9248820afb164ff9eb3eaa7/nvtx-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:3a22be895546ca609e83e54614b56739200ab6f4d13e15f5685544082b1b7908", size = 119654, upload-time = "2025-11-27T17:32:08.536Z" }, + { url = "https://files.pythonhosted.org/packages/87/a6/4d473abd7c07a6d1060c0f708e21ddf46a960258532ffc897681db5c0f46/nvtx-0.2.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:227f6406d2fe1a4b890be17eb1f4c1f5bd4df8f7032dd1cb8c7651d379f35541", size = 732764, upload-time = "2025-11-27T17:26:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/94/06/3ab72e5a463af1b95934638cb8377e99f58e5ef21a47cbf69b92267d6602/nvtx-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0664aa75b24e2ad0abdd0fa52c49e9c8a120652f2194289c85dc2d93cbc6017f", size = 724555, upload-time = "2025-11-27T17:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/64f6078a5ab4134af91ba294035ee1ebb3512edaaa9d60d8f0f023178620/nvtx-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:10f5971661d61c1a90cd36c3069240452c904ecec4b3a08d0d6fdba1e5398165", size = 119660, upload-time = "2025-11-27T17:32:30.406Z" }, + { url = "https://files.pythonhosted.org/packages/8a/de/2cc15bb805b1b18317b60837b853ed023757730d0db82de291635fc88bc3/nvtx-0.2.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ece46f555e725db879df06549980744f89db5923a77e6f7a5aecda75292421a", size = 727708, upload-time = "2025-11-27T17:25:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/81/94/b37d634fef8677ce525b5bfd2886737ea2c064bc3576fc84423973ff5b97/nvtx-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17efe5d903996bceb0c8a12cae80fa9b66bee7ee895923bd9d8ec2a5af1aabd8", size = 737691, upload-time = "2025-11-27T17:21:27.87Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c1/f633aa32003050ff83626a19402f03c83990a15b4df658a7bf1b590ee83e/nvtx-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:f40db4746714d525d3020c702a0df866c2335efd6a27c41e869e577402a53a4b", size = 119193, upload-time = "2025-11-27T17:31:42.943Z" }, + { url = "https://files.pythonhosted.org/packages/04/a3/603ecdfd5cd97feee59c7e51da4929e22eac8dbe68ac78df53e74152813f/nvtx-0.2.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8cd1f2b464675b4d3c2036b7bbaf975baa9307f0795107dc69c556c0c8d191d", size = 710057, upload-time = "2025-11-27T17:28:08.127Z" }, + { url = "https://files.pythonhosted.org/packages/97/29/945dd440e6bd459e6064f321ed425dbae7d03d39ffa97a38e5434fbcda27/nvtx-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6532556d81f782e24eb12c5e0c75e297493d6ab0431177c93c12bb29c523ea9e", size = 717825, upload-time = "2025-11-27T17:22:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/16/3e/5d7872f2a0809237e3d524f81a7a3c7fbeb98bdc9dcec4723b75a45cd552/nvtx-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:cd86f78ed56aede301b03e5ab8cb1aaeb8ba0b5ed683f98f87fbe474996d73f2", size = 118546, upload-time = "2025-11-27T17:30:32.549Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/1c8b1ce8b729a96218c1d9c0d399ea556765ab2199311ca9e1693507834d/nvtx-0.2.14-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51d48a98db0c3f4b701d3422ef34bf34c0c9256036d036dd115d48c6286b7b82", size = 791447, upload-time = "2025-11-28T22:52:07.744Z" }, + { url = "https://files.pythonhosted.org/packages/72/a8/608bfa862de1673e63386b0e32520a05ed968524c22babe273565a1c9027/nvtx-0.2.14-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:638f66b6119fb3adfe3f5e2ba2d0cca9580bc4a898cd702b639d727a4a405c59", size = 742277, upload-time = "2025-11-28T22:56:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/32/bb/579545bb24e4d1d643e42c9e323d32fcf327522027346686c12595f15ed9/nvtx-0.2.14-cp313-cp313t-win_amd64.whl", hash = "sha256:d5dfaf02a91fd2a123e104d59681dc768c07b66b05e4afc4c05ee125e45f6261", size = 131705, upload-time = "2025-11-28T22:57:30.24Z" }, + { url = "https://files.pythonhosted.org/packages/07/60/9b4ed6dd0153b17817d3344f444bed731d284907c99a4fcc0910a594b114/nvtx-0.2.14-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12c21b01b426e85054606d5d8e78d08ab804f1231d4f24be6ded595f901b1125", size = 740863, upload-time = "2025-11-28T22:53:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/93/e5/c4095778d690c8eac535048c44f4aff61e77ad0573b324655e3c8d4b7b86/nvtx-0.2.14-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:296aa978e572d2854a980506cb9de6fe641d496b46879b56c6e6df7467056303", size = 747776, upload-time = "2025-11-28T22:55:54.441Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3f/05150e9953b6e818b2c103ff881a43c99063cf06f7e9b474f94f79674fcc/nvtx-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:e265cce4d7ecfb56b9e974be736bba308be47402edfc09dd6a5f91a8eafa90c3", size = 120583, upload-time = "2025-11-28T22:58:37.289Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4c/b607cb591d4600ff1771e64563cf6b395024ffad0b13fe09aa10f7b8d786/nvtx-0.2.14-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc5522766fff59cf62e42c31324b1c405d308d7755e847e25d286f29e217f54a", size = 794398, upload-time = "2025-11-28T22:51:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/9d67e2995673e25711ee79bcc52a552926c074943fc59b42fa56996ad50f/nvtx-0.2.14-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:671b592464038054cc31a5d8c53a460d22fc38b066bbd055e086be8dd49fa43b", size = 746054, upload-time = "2025-11-28T22:55:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/143ce5b3e07921176fc2b6f808afde7335f06e93af1a29ac6f4cfa02cf4b/nvtx-0.2.14-cp314-cp314t-win_amd64.whl", hash = "sha256:2567ce29e905062c239a33ba91a46ca7307561c40fd7b37ec64c00cd78f9bdab", size = 138050, upload-time = "2025-11-28T22:57:09.773Z" }, +] + +[[package]] +name = "obstore" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/8c/9ec984edd0f3b72226adfaa19b1c61b15823b35b52f311ca4af36d009d15/obstore-0.8.2.tar.gz", hash = "sha256:a467bc4e97169e2ba749981b4fd0936015428d9b8f3fb83a5528536b1b6f377f", size = 168852, upload-time = "2025-09-16T15:34:55.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e9/0a1e340ef262f225ad71f556ccba257896f85ca197f02cd228fe5e20b45a/obstore-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:49104c0d72688c180af015b02c691fbb6cf6a45b03a9d71b84059ed92dbec704", size = 3622821, upload-time = "2025-09-16T15:32:53.79Z" }, + { url = "https://files.pythonhosted.org/packages/24/86/2b53e8b0a838dbbf89ef5dfddde888770bc1a993c691698dae411a407228/obstore-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c49776abd416e4d80d003213522d82ad48ed3517bee27a6cf8ce0f0cf4e6337e", size = 3356349, upload-time = "2025-09-16T15:32:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/79/1ba6dc854d7de7704a2c474d723ffeb01b6884f72eea7cbe128efc472f4a/obstore-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1636372b5e171a98369612d122ea20b955661daafa6519ed8322f4f0cb43ff74", size = 3454842, upload-time = "2025-09-16T15:32:57.072Z" }, + { url = "https://files.pythonhosted.org/packages/ca/03/ca67ccc9b9e63cfc0cd069b84437807fed4ef880be1e445b3f29d11518e0/obstore-0.8.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2efed0d86ad4ebffcbe3d0c4d84f26c2c6b20287484a0a748499c169a8e1f2c4", size = 3688363, upload-time = "2025-09-16T15:32:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2f/c78eb4352d8be64a072934fe3ff2af79a1d06f4571af7c70d96f9741766b/obstore-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00c5542616dc5608de82ab6f6820633c9dbab6ff048e770fb8a5fcd1d30cd656", size = 3960133, upload-time = "2025-09-16T15:32:59.614Z" }, + { url = "https://files.pythonhosted.org/packages/4f/34/9e828d19194e227fd9f1d2dd70710da99c2bd2cd728686d59ea80be10b7c/obstore-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9df46aaf25ce80fff48c53382572adc67b6410611660b798024450281a3129", size = 3925493, upload-time = "2025-09-16T15:33:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7d/9ec5967f3e2915fbc441f72c3892a7f0fb3618e3ae5c8a44181ce4aa641c/obstore-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ccf0f03a7fe453fb8640611c922bce19f021c6aaeee6ee44d6d8fb57db6be48", size = 3769401, upload-time = "2025-09-16T15:33:02.373Z" }, + { url = "https://files.pythonhosted.org/packages/85/bf/00b65013068bde630a7369610a2dae4579315cd6ce82d30e3d23315cf308/obstore-0.8.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:ddfbfadc88c5e9740b687ef0833384329a56cea07b34f44e1c4b00a0e97d94a9", size = 3534383, upload-time = "2025-09-16T15:33:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/39/1b684fd96c9a33974fc52f417c52b42c1d50df40b44e588853c4a14d9ab1/obstore-0.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:53ad53bb16e64102f39559ec470efd78a5272b5e3b84c53aa0423993ac5575c1", size = 3697939, upload-time = "2025-09-16T15:33:05.355Z" }, + { url = "https://files.pythonhosted.org/packages/85/58/93a2c78935f17fde7e22842598a6373e46a9c32d0243ec3b26b5da92df27/obstore-0.8.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b0b905b46354db0961ab818cad762b9c1ac154333ae5d341934c90635a6bd7ab", size = 3681746, upload-time = "2025-09-16T15:33:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/38/90/225c2972338d18f92e7a56f71e34df6935b0b1bd7458bb6a0d2bd4d48f92/obstore-0.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee235694406ebb2dc4178752cf5587f471d6662659b082e9786c716a0a9465c", size = 3765156, upload-time = "2025-09-16T15:33:10.457Z" }, + { url = "https://files.pythonhosted.org/packages/79/eb/aca27e895bfcbbcd2bf05ea6a2538a94b718e6f6d72986e16ab158b753ec/obstore-0.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c36faf7ace17dd0832aa454118a63ea21862e3d34f71b9297d0c788d00f4985", size = 3941190, upload-time = "2025-09-16T15:33:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/ce/c8251a397e7507521768f05bc355b132a0daaff3739e861e51fa6abd821e/obstore-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:948a1db1d34f88cfc7ab7e0cccdcfd84cf3977365634599c95ba03b4ef80d1c4", size = 3970041, upload-time = "2025-09-16T15:33:13.035Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c4/018f90701f1e5ea3fbd57f61463f42e1ef5218e548d3adcf12b6be021c34/obstore-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2edaa97687c191c5324bb939d72f6fe86a7aa8191c410f1648c14e8296d05c1c", size = 3622568, upload-time = "2025-09-16T15:33:14.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/72dd1e7d52fc554bb1fdb1a9499bda219cf3facea5865a1d97fdc00b3a1b/obstore-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c4fb7ef8108f08d14edc8bec9e9a6a2e5c4d14eddb8819f5d0da498aff6e8888", size = 3356109, upload-time = "2025-09-16T15:33:15.315Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ae/089fe5b9207091252fe5ce352551214f04560f85eb8f2cc4f716a6a1a57e/obstore-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fda8f658c0edf799ab1e264f9b12c7c184cd09a5272dc645d42e987810ff2772", size = 3454588, upload-time = "2025-09-16T15:33:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/10/1865ae2d1ba45e8ae85fb0c1aada2dc9533baf60c4dfe74dab905348d74a/obstore-0.8.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87fe2bc15ce4051ecb56abd484feca323c2416628beb62c1c7b6712114564d6e", size = 3688627, upload-time = "2025-09-16T15:33:17.604Z" }, + { url = "https://files.pythonhosted.org/packages/a6/09/5d7ba6d0aeac563ea5f5586401c677bace4f782af83522b1fdf15430e152/obstore-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2482aa2562ab6a4ca40250b26bea33f8375b59898a9b5615fd412cab81098123", size = 3959896, upload-time = "2025-09-16T15:33:18.789Z" }, + { url = "https://files.pythonhosted.org/packages/16/15/2b3eda59914761a9ff4d840e2daec5697fd29b293bd18d3dc11c593aed06/obstore-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4153b928f5d2e9c6cb645e83668a53e0b42253d1e8bcb4e16571fc0a1434599a", size = 3933162, upload-time = "2025-09-16T15:33:19.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/5fc63b41526587067537fb1498c59a210884664c65ccf0d1f8f823b0875a/obstore-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbfa9c38620cc191be98c8b5558c62071e495dc6b1cc724f38293ee439aa9f92", size = 3769605, upload-time = "2025-09-16T15:33:21.389Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/2208ab6e1fc021bf8b7e117249a10ab75d0ed24e0f2de1a8d7cd67d885b5/obstore-0.8.2-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:0822836eae8d52499f10daef17f26855b4c123119c6eb984aa4f2d525ec2678d", size = 3534396, upload-time = "2025-09-16T15:33:22.574Z" }, + { url = "https://files.pythonhosted.org/packages/1d/8f/a0e2882edd6bd285c82b8a5851c4ecf386c93fe75b6e340d5d9d30e809fc/obstore-0.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ef6435dfd586d83b4f778e7927a5d5b0d8b771e9ba914bc809a13d7805410e6", size = 3697777, upload-time = "2025-09-16T15:33:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/94/78/ebf0c33bed5c9a8eed3b00eefafbcc0a687eeb1e05451c76fcf199d29ff8/obstore-0.8.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0f2cba91f4271ca95a932a51aa8dda1537160342b33f7836c75e1eb9d40621a2", size = 3681546, upload-time = "2025-09-16T15:33:24.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/21/9bf4fb9e53fd5f01af580b6538de2eae857e31d24b0ebfc4d916c306a1e4/obstore-0.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:23c876d603af0627627808d19a58d43eb5d8bfd02eecd29460bc9a58030fed55", size = 3765336, upload-time = "2025-09-16T15:33:26.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3c/7f6895c23719482d231b2d6ed328e3223fdf99785f6850fba8d2fc5a86ee/obstore-0.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff3c4b5d07629b70b9dee494cd6b94fff8465c3864752181a1cb81a77190fe42", size = 3941142, upload-time = "2025-09-16T15:33:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/93/a4/56ccdb756161595680a28f4b0def2c04f7048ffacf128029be8394367b26/obstore-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:aadb2cb72de7227d07f4570f82729625ffc77522fadca5cf13c3a37fbe8c8de9", size = 3970172, upload-time = "2025-09-16T15:33:28.393Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/60fefbb5736e69eab56657bca04ca64dc07fdeccb3814164a31b62ad066b/obstore-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bb70ce297a47392b1d9a3e310f18d59cd5ebbb9453428210fef02ed60e4d75d1", size = 3612955, upload-time = "2025-09-16T15:33:29.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/844e8f382e5a12b8a3796a05d76a03e12c7aedc13d6900419e39207d7868/obstore-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1619bf618428abf1f607e0b219b2e230a966dcf697b717deccfa0983dd91f646", size = 3346564, upload-time = "2025-09-16T15:33:30.698Z" }, + { url = "https://files.pythonhosted.org/packages/89/73/8537f99e09a38a54a6a15ede907aa25d4da089f767a808f0b2edd9c03cec/obstore-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4605c3ed7c9515aeb4c619b5f7f2c9986ed4a79fe6045e536b5e59b804b1476", size = 3460809, upload-time = "2025-09-16T15:33:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/b4/99/7714dec721e43f521d6325a82303a002cddad089437640f92542b84e9cc8/obstore-0.8.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce42670417876dd8668cbb8659e860e9725e5f26bbc86449fd259970e2dd9d18", size = 3692081, upload-time = "2025-09-16T15:33:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/4ac4175fe95a24c220a96021c25c432bcc0c0212f618be0737184eebbaad/obstore-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a3e893b2a06585f651c541c1972fe1e3bf999ae2a5fda052ee55eb7e6516f5", size = 3957466, upload-time = "2025-09-16T15:33:34.528Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/caa288fb735484fc5cb019bdf3d896eaccfae0ac4622e520d05692c46790/obstore-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08462b32f95a9948ed56ed63e88406e2e5a4cae1fde198f9682e0fb8487100ed", size = 3951293, upload-time = "2025-09-16T15:33:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/d380239da2d6a1fda82e17df5dae600a404e8a93a065784518ff8325d5f6/obstore-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a0bf7763292a8fc47d01cd66e6f19002c5c6ad4b3ed4e6b2729f5e190fa8a0d", size = 3766199, upload-time = "2025-09-16T15:33:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/28/41/d391be069d3da82969b54266948b2582aeca5dd735abeda4d63dba36e07b/obstore-0.8.2-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:bcd47f8126cb192cbe86942b8f73b1c45a651ce7e14c9a82c5641dfbf8be7603", size = 3529678, upload-time = "2025-09-16T15:33:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4c/4862fdd1a3abde459ee8eea699b1797df638a460af235b18ca82c8fffb72/obstore-0.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57eda9fd8c757c3b4fe36cf3918d7e589cc1286591295cc10b34122fa36dd3fd", size = 3698079, upload-time = "2025-09-16T15:33:39.696Z" }, + { url = "https://files.pythonhosted.org/packages/68/ca/014e747bc53b570059c27e3565b2316fbe5c107d4134551f4cd3e24aa667/obstore-0.8.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ea44442aad8992166baa69f5069750979e4c5d9ffce772e61565945eea5774b9", size = 3687154, upload-time = "2025-09-16T15:33:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/6db5f8edd93028e5b8bfbeee15e6bd3e56f72106107d31cb208b57659de4/obstore-0.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:41496a3ab8527402db4142aaaf0d42df9d7d354b13ba10d9c33e0e48dd49dd96", size = 3773444, upload-time = "2025-09-16T15:33:42.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/e5/c9e2cc540689c873beb61246e1615d6e38301e6a34dec424f5a5c63c1afd/obstore-0.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43da209803f052df96c7c3cbec512d310982efd2407e4a435632841a51143170", size = 3939315, upload-time = "2025-09-16T15:33:43.252Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c9/bb53280ca50103c1ffda373cdc9b0f835431060039c2897cbc87ddd92e42/obstore-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:1836f5dcd49f9f2950c75889ab5c51fb290d3ea93cdc39a514541e0be3af016e", size = 3978234, upload-time = "2025-09-16T15:33:44.393Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/8c3316cc958d386d5e6ab03e9db9ddc27f8e2141cee4a6777ae5b92f3aac/obstore-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:212f033e53fe6e53d64957923c5c88949a400e9027f7038c705ec2e9038be563", size = 3612027, upload-time = "2025-09-16T15:33:45.6Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4d/699359774ce6330130536d008bfc32827fab0c25a00238d015a5974a3d1d/obstore-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bee21fa4ba148d08fa90e47a96df11161661ed31e09c056a373cb2154b0f2852", size = 3344686, upload-time = "2025-09-16T15:33:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/82/37/55437341f10512906e02fd9fa69a8a95ad3f2f6a916d3233fda01763d110/obstore-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c66594b59832ff1ced4c72575d9beb8b5f9b4e404ac1150a42bfb226617fd50", size = 3459860, upload-time = "2025-09-16T15:33:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/7a/51/4245a616c94ee4851965e33f7a563ab4090cc81f52cc73227ff9ceca2e46/obstore-0.8.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:089f33af5c2fe132d00214a0c1f40601b28f23a38e24ef9f79fb0576f2730b74", size = 3691648, upload-time = "2025-09-16T15:33:49.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/4e2fb24171e3ca3641a4653f006be826e7e17634b11688a5190553b00b83/obstore-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87f658dfd340d5d9ea2d86a7c90d44da77a0db9e00c034367dca335735110cf", size = 3956867, upload-time = "2025-09-16T15:33:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/42/f5/b703115361c798c9c1744e1e700d5908d904a8c2e2bd38bec759c9ffb469/obstore-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2e4fa92828c4fbc2d487f3da2d3588701a1b67d9f6ca3c97cc2afc912e9c63", size = 3950599, upload-time = "2025-09-16T15:33:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/53/20/08c6dc0f20c1394e2324b9344838e4e7af770cdcb52c30757a475f50daeb/obstore-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab440e89c5c37a8ec230857dd65147d4b923e0cada33297135d05e0f937d696a", size = 3765865, upload-time = "2025-09-16T15:33:53.291Z" }, + { url = "https://files.pythonhosted.org/packages/77/20/77907765e29b2eba6bd8821872284d91170d7084f670855b2dfcb249ea14/obstore-0.8.2-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:b9beed107c5c9cd995d4a73263861fcfbc414d58773ed65c14f80eb18258a932", size = 3529807, upload-time = "2025-09-16T15:33:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/f629d39cc30d050f52b1bf927e4d65c1cc7d7ffbb8a635cd546b5c5219a0/obstore-0.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b75b4e7746292c785e31edcd5aadc8b758238372a19d4c5e394db5c305d7d175", size = 3693629, upload-time = "2025-09-16T15:33:56.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/ff/106763fd10f2a1cb47f2ef1162293c78ad52f4e73223d8d43fc6b755445d/obstore-0.8.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f33e6c366869d05ab0b7f12efe63269e631c5450d95d6b4ba4c5faf63f69de70", size = 3686176, upload-time = "2025-09-16T15:33:57.247Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/d2ccb6f32feeca906d5a7c4255340df5262af8838441ca06c9e4e37b67d5/obstore-0.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:12c885a9ce5ceb09d13cc186586c0c10b62597eff21b985f6ce8ff9dab963ad3", size = 3773081, upload-time = "2025-09-16T15:33:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/40d1cc504cefc89c9b3dd8874287f3fddc7d963a8748d6dffc5880222013/obstore-0.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4accc883b93349a81c9931e15dd318cc703b02bbef2805d964724c73d006d00e", size = 3938589, upload-time = "2025-09-16T15:33:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/916c6777222db3271e9fb3cf9a97ed92b3a9b3e465bdeec96de9ab809d53/obstore-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ec850adf9980e5788a826ccfd5819989724e2a2f712bfa3258e85966c8d9981e", size = 3977768, upload-time = "2025-09-16T15:34:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/66f8dc98bbf5613bbfe5bf21747b4c8091442977f4bd897945895ab7325c/obstore-0.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1431e40e9bb4773a261e51b192ea6489d0799b9d4d7dbdf175cdf813eb8c0503", size = 3623364, upload-time = "2025-09-16T15:34:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/1a/66/6d527b3027e42f625c8fc816ac7d19b0d6228f95bfe7666e4d6b081d2348/obstore-0.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ddb39d4da303f50b959da000aa42734f6da7ac0cc0be2d5a7838b62c97055bb9", size = 3347764, upload-time = "2025-09-16T15:34:04.236Z" }, + { url = "https://files.pythonhosted.org/packages/0d/79/c00103302b620192ea447a948921ad3fed031ce3d19e989f038e1183f607/obstore-0.8.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e01f4e13783db453e17e005a4a3ceff09c41c262e44649ba169d253098c775e8", size = 3460981, upload-time = "2025-09-16T15:34:05.595Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d9/bfe4ed4b1aebc45b56644dd5b943cf8e1673505cccb352e66878a457e807/obstore-0.8.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0fc2d0bc17caff9b538564ddc26d7616f7e8b7c65b1a3c90b5048a8ad2e797", size = 3692711, upload-time = "2025-09-16T15:34:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/cd6c2cbb18e1f40c77e7957a4a03d2d83f1859a2e876a408f1ece81cad4c/obstore-0.8.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e439d06c99a140348f046c9f598ee349cc2dcd9105c15540a4b231f9cc48bbae", size = 3958362, upload-time = "2025-09-16T15:34:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ea/5ee82bf23abd71c7d6a3f2d008197ae8f8f569d41314c26a8f75318245be/obstore-0.8.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e37d9046669fcc59522d0faf1d105fcbfd09c84cccaaa1e809227d8e030f32c", size = 3957082, upload-time = "2025-09-16T15:34:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ee/46650405e50fdaa8d95f30375491f9c91fac9517980e8a28a4a6af66927f/obstore-0.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2646fdcc4bbe92dc2bb5bcdff15574da1211f5806c002b66d514cee2a23c7cb8", size = 3775539, upload-time = "2025-09-16T15:34:10.726Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/348a7ebebe2ca3d94dfc75344ea19675ae45472823e372c1852844078307/obstore-0.8.2-cp314-cp314-manylinux_2_24_aarch64.whl", hash = "sha256:e31a7d37675056d93dfc244605089dee67f5bba30f37c88436623c8c5ad9ba9d", size = 3535048, upload-time = "2025-09-16T15:34:12.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/b7a16cc0da91a4b902d47880ad24016abfe7880c63f7cdafda45d89a2f91/obstore-0.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:656313dd8170dde0f0cd471433283337a63912e8e790a121f7cc7639c83e3816", size = 3699035, upload-time = "2025-09-16T15:34:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/3269a3a58347e0b019742d888612c4b765293c9c75efa44e144b1e884c0d/obstore-0.8.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329038c9645d6d1741e77fe1a53e28a14b1a5c1461cfe4086082ad39ebabf981", size = 3687307, upload-time = "2025-09-16T15:34:14.501Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/4fd4819ad6a49d2f462a45be453561f4caebded0dc40112deeffc34b89b1/obstore-0.8.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e4df99b369790c97c752d126b286dc86484ea49bff5782843a265221406566f", size = 3776076, upload-time = "2025-09-16T15:34:16.207Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/7c4f958fa0b9fc4778fb3d232e38b37db8c6b260f641022fbba48b049d7e/obstore-0.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9e1c65c65e20cc990414a8a9af88209b1bbc0dd9521b5f6b0293c60e19439bb7", size = 3947445, upload-time = "2025-09-16T15:34:17.423Z" }, + { url = "https://files.pythonhosted.org/packages/c3/37/14bae1f5bf4369027abc5315cdba2428ad4c16e2fd3bd5d35b7ee584aa0c/obstore-0.8.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6ea04118980a9c22fc8581225ff4507b6a161baf8949d728d96e68326ebaab59", size = 3624857, upload-time = "2025-09-16T15:34:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c4/8cba91629aa20479ba86a57c2c2b3bc0a54fc6a31a4594014213603efae6/obstore-0.8.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f33a7570b6001b54252260fbec18c3f6d21e25d3ec57e9b6c5e7330e8290eb2", size = 3355999, upload-time = "2025-09-16T15:34:36.954Z" }, + { url = "https://files.pythonhosted.org/packages/f2/10/3e40557d6d9c38c5a0f7bac1508209b9dbb8c4da918ddfa9326ba9a1de3f/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11fa78dfb749edcf5a041cd6db20eae95b3e8b09dfdd9b38d14939da40e7c115", size = 3457322, upload-time = "2025-09-16T15:34:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/dcf7988350c286683698cbdd8c15498aec43cbca72eaabad06fd77f0f34a/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872bc0921ff88305884546ba05e258ccd95672a03d77db123f0d0563fd3c000b", size = 3689452, upload-time = "2025-09-16T15:34:39.638Z" }, + { url = "https://files.pythonhosted.org/packages/97/02/643eb2ede58933e47bdbc92786058c83d9aa569826d5bf6e83362d24a27a/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72556a2fbf018edd921286283e5c7eec9f69a21c6d12516d8a44108eceaa526a", size = 3961171, upload-time = "2025-09-16T15:34:41.232Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5d/c0b515df6089d0f54109de8031a6f6ed31271361948bee90ab8271d22f79/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fa1abf21499dfcfb0328941a175f89a9aa58245bf00e3318fe928e4b10d297", size = 3935988, upload-time = "2025-09-16T15:34:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/97/114d7bc172bb846472181d6fa3e950172ee1b1ccd11291777303c499dbdd/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54f72f30cd608c4399679781c884bf8a0e816c1977a2fac993bf5e1fb30609f", size = 3771781, upload-time = "2025-09-16T15:34:44.405Z" }, + { url = "https://files.pythonhosted.org/packages/c3/43/4aa6de6dc406ef5e109b21a5614c34999575de638254deb456703fae24aa/obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:b044ebf1bf7b8f7b0ca309375c1cd9e140be79e072ae8c70bbd5d9b2ad1f7678", size = 3536689, upload-time = "2025-09-16T15:34:45.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/a5/870ce541aa1a9ee1d9c3e99c2187049bf5a4d278ee9678cc449aae0a4e68/obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b1326cd2288b64d6fe8857cc22d3a8003b802585fc0741eff2640a8dc35e8449", size = 3700560, upload-time = "2025-09-16T15:34:47.252Z" }, + { url = "https://files.pythonhosted.org/packages/7d/93/76a5fc3833aaa833b4152950d9cdfd328493a48316c24e32ddefe9b8870f/obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:ba6863230648a9b0e11502d2745d881cf74262720238bc0093c3eabd22a3b24c", size = 3683450, upload-time = "2025-09-16T15:34:49.589Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/4c389362c187630c42f61ef9214e67fc336e44b8aafc47cf49ba9ab8007d/obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:887615da9eeefeb2df849d87c380e04877487aa29dbeb367efc3f17f667470d3", size = 3766628, upload-time = "2025-09-16T15:34:51.937Z" }, + { url = "https://files.pythonhosted.org/packages/03/12/08547e63edf2239ec6660af434602208ab6f394955ef660a6edda13a0bee/obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4eec1fb32ffa4fb9fe9ad584611ff031927a5c22732b56075ee7204f0e35ebdf", size = 3944069, upload-time = "2025-09-16T15:34:54.108Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "onnx" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/cc/4ba3c80cfaffdb541dc5a23eaccb045a627361e94ecaeba30496270f15b3/onnx-1.20.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be", size = 17904206, upload-time = "2026-01-10T01:38:58.574Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fc/3a1c4ae2cd5cfab2d0ebc1842769b04b417fe13946144a7c8ce470dd9c85/onnx-1.20.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38", size = 17414849, upload-time = "2026-01-10T01:39:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ab/5017945291b981f2681fb620f2d5b6070e02170c648770711ef1eac79d56/onnx-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031", size = 17513600, upload-time = "2026-01-10T01:39:04.348Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b0/063e79dc365972af876d786bacc6acd8909691af2b9296615ff74ad182f3/onnx-1.20.1-cp310-cp310-win32.whl", hash = "sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826", size = 16239159, upload-time = "2026-01-10T01:39:07.254Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/a992271eb3683e676239d71b5a78ad3cf4d06d2223c387e701bf305da199/onnx-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a", size = 16391718, upload-time = "2026-01-10T01:39:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/0c/38/1a0e74d586c08833404100f5c052f92732fb5be417c0b2d7cb0838443bfe/onnx-1.20.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095", size = 17904965, upload-time = "2026-01-10T01:39:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/96/25/64b076e9684d17335f80b15b3bf502f7a8e1a89f08a6b208d4f2861b3011/onnx-1.20.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945", size = 17415179, upload-time = "2026-01-10T01:39:16.516Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d5/6743b409421ced20ad5af1b3a7b4c4e568689ffaca86db431692fca409a6/onnx-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0", size = 17513672, upload-time = "2026-01-10T01:39:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6b/dae82e6fdb2043302f29adca37522312ea2be55b75907b59be06fbdffe87/onnx-1.20.1-cp311-cp311-win32.whl", hash = "sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e", size = 16239336, upload-time = "2026-01-10T01:39:22.506Z" }, + { url = "https://files.pythonhosted.org/packages/8e/17/a0d7863390c1f2067d7c02dcc1477034965c32aaa1407bfcf775305ffee4/onnx-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf", size = 16392120, upload-time = "2026-01-10T01:39:25.106Z" }, + { url = "https://files.pythonhosted.org/packages/aa/72/9b879a46eb7a3322223791f36bf9c25d95da9ed93779eabb75a560f22e5b/onnx-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2", size = 16346923, upload-time = "2026-01-10T01:39:27.782Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" }, +] + +[[package]] +name = "onnx-ir" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "onnx" }, + { name = "sympy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/a5/acc43c8fa6edbc584d127fb6bbd13ae9ebfc01b9675c74e0da2de15fa4a6/onnx_ir-0.2.0.tar.gz", hash = "sha256:8bad3906691987290789b26d05e0dbff467029a0b1e411e12e4cae02e43503e4", size = 141693, upload-time = "2026-02-24T02:31:10.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/df/a99736bcca6b16e36c687ce4996abcf4ce73c514fddd9e730cfcb6a334f2/onnx_ir-0.2.0-py3-none-any.whl", hash = "sha256:eb14d1399c2442bd1ff702719e70074e9cedfa3af5729416a32752c9e0f82591", size = 164100, upload-time = "2026-02-24T02:31:09.454Z" }, +] + +[[package]] +name = "onnxscript" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "onnx" }, + { name = "onnx-ir" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/538fdeb0e25bed5d7e0f954af5710543e2629499fb74381afc3333f8a8ae/onnxscript-0.6.2.tar.gz", hash = "sha256:abb2e6f464db40c9b8c7fbb3e64cca04cf3f4495e67c4eda5eac17b784191ce3", size = 590865, upload-time = "2026-02-10T22:53:39.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/56/e6b179397497ab93266b6eb00743403a6a699a29063a423c4a14595d3db9/onnxscript-0.6.2-py3-none-any.whl", hash = "sha256:20e3c3fd1da19b3655549d5455a2df719db47374fe430e01e865ae69127c37b9", size = 689064, upload-time = "2026-02-10T22:53:41.663Z" }, +] + +[[package]] +name = "open-clip-torch" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ftfy" }, + { name = "huggingface-hub" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "timm" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/1f/2bc9795047fa2c1ad2567ef78ce6dfc9a7b763fa534acee09a94da2a5b8f/open_clip_torch-3.3.0.tar.gz", hash = "sha256:904b1a9f909df8281bb3de60ab95491cd2994a509177ea4f9d6292a84fe24d6d", size = 1503380, upload-time = "2026-02-27T00:32:46.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/b5/41c315ccd94ca332ead3e832e83eee343ab245005c3e43d9d3e75eae34eb/open_clip_torch-3.3.0-py3-none-any.whl", hash = "sha256:c549ad5ed6bfc119cc11105033c0a2b9d7a2a4afeb40a58a09aab3da1a0043ce", size = 1547268, upload-time = "2026-02-27T00:32:44.902Z" }, +] + +[[package]] +name = "openai" +version = "2.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/2b/a442b206ed74908dd79e2ad1ef3ffaeae66422b1fb506af981f0ef671ba0/openai-2.25.0.tar.gz", hash = "sha256:c3e1965d83c333dbd341eb2c7c8aceb783c272cd57fc57353404d9a443634e29", size = 666577, upload-time = "2026-03-05T18:35:38.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/e6/ca496976bacdd7f86d4fa69359c2d9b5a547d7acbf0ae5cac7bff107ff50/openai-2.25.0-py3-none-any.whl", hash = "sha256:7c7c01a0a4b69771a29913e28d06bc1e9cee8781b4d206bb56cb946d8d2fcb23", size = 1136347, upload-time = "2026-03-05T18:35:36.497Z" }, +] + +[[package]] +name = "openai-harmony" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/c6/2502f416d46be3ec08bb66d696cccffb57781a499e3ff2e4d7c174af4e8f/openai_harmony-0.0.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:029ec25ca74abe48fdb58eb9fdd2a8c1618581fc33ce8e5653f8a1ffbfbd9326", size = 2627806, upload-time = "2025-11-05T19:06:57.063Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d2/ce6953ca87db9cae3e775024184da7d1c5cb88cead19a2d75b42f00a959c/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4f709815924ec325b9a890e6ab2bbb0ceec8e319a4e257328eb752cf36b2efc", size = 2948463, upload-time = "2025-11-05T19:06:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/b553c9651662d6ce102ca7f3629d268b23df1abe5841e24bed81e8a8e949/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cfcfd963b50a41fc656c84d3440ca6eecdccd6c552158ce790b8f2e33dfb5a9", size = 2704083, upload-time = "2025-11-05T19:06:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/9b/af/4eec8f9ab9c27bcdb444460c72cf43011d176fc44c79d6e113094ca1e152/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a3a16972aa1cee38ea958470cd04ac9a2d5ac38fdcf77ab686611246220c158", size = 2959765, upload-time = "2025-11-05T19:06:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/11/3c/33f3374e4624e0e776f6b13b73c45a7ead7f9c4529f8369ed5bfcaa30cac/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4d5cfa168e74d08f8ba6d58a7e49bc7daef4d58951ec69b66b0d56f4927a68d", size = 3427031, upload-time = "2025-11-05T19:06:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/4327dbf87f75ae813405fd9a9b4a5cde63d506ffed0a096a440a4cabd89c/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cbaa3bda75ef0d8836e1f8cc84af62f971b1d756d740efc95c38c3e04c0bfde2", size = 2932931, upload-time = "2025-11-05T19:07:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c8/1774eec4f6f360ef57618fb8f52e3d3af245b2491bd0297513aa09eec04b/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:772922a9bd24e133950fad71eb1550836f415a88e8c77870e12d0c3bd688ddc2", size = 2996140, upload-time = "2025-11-05T19:07:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/3d1e01e2dba517a91760e4a03e4f20ffc75039a6fe584d0e6f9b5c78fd15/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:007b0476a1f331f8130783f901f1da6f5a7057af1a4891f1b6a31dec364189b5", size = 3205080, upload-time = "2025-11-05T19:07:05.078Z" }, + { url = "https://files.pythonhosted.org/packages/14/63/119de431572d7c70a7bf1037034a9be6ed0a7502a7498ba7302bca5b3242/openai_harmony-0.0.8-cp38-abi3-win32.whl", hash = "sha256:a9b5f893326b28d9e935ade14b4f655f5a840942473bc89b201c25f7a15af9cf", size = 2082457, upload-time = "2025-11-05T19:07:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/c83cf5a206c263ee70448a5ae4264682555f4d0b5bed0d2cc6ca1108103d/openai_harmony-0.0.8-cp38-abi3-win_amd64.whl", hash = "sha256:39d44f0d8f466bd56698e7ead708bead3141e27b9b87e3ab7d5a6d0e4a869ee5", size = 2438369, upload-time = "2025-11-05T19:07:08.1Z" }, +] + +[[package]] +name = "opencensus" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "opencensus-context" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/a7/a46dcffa1b63084f9f17fe3c8cb20724c4c8f91009fd0b2cfdb27d5d2b35/opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2", size = 64966, upload-time = "2024-01-03T18:04:07.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ed/9fbdeb23a09e430d87b7d72d430484b88184633dc50f6bfb792354b6f661/opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864", size = 128225, upload-time = "2024-01-03T18:04:05.127Z" }, +] + +[[package]] +name = "opencensus-context" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/96/3b6f638f6275a8abbd45e582448723bffa29c1fb426721dedb5c72f7d056/opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c", size = 4066, upload-time = "2022-08-03T22:20:22.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/68/162c97ea78c957d68ecf78a5c5041d2e25bd5562bdf5d89a6cbf7f8429bf/opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039", size = 5060, upload-time = "2022-08-03T22:20:20.352Z" }, +] + +[[package]] +name = "opencv-contrib-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/4c/a45c96b9fe90b2c48ee604f5176eb7deb46ce7c2e87c8d819d2945dbcab6/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:53c8ab81376210dda5836307eb6bda7266f39a3820a9a070c7131510ba815fe1", size = 52041546, upload-time = "2026-02-05T07:01:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6c/ba1f3177927deeb3002b62fb8db89daea3b5dc732d61de5bf4c73ed6ebf7/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1973d0fc773873f9d1b5bf0d1b65895da2f47b06ba033b7d58393f5c28ba0778", size = 38830319, upload-time = "2026-02-05T07:01:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7a/fe87eaf109b454af4a2579f46958b3cafb0f804b9c788c108760723a9bb7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f9cb522dd9e465dfca3536c15288f7936b9827432fb9c885eaf94dc5f88c2a3", size = 53339457, upload-time = "2026-02-05T10:09:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b6/27/3665ca4b75ddfd218f9ab139f0463d9571e87aaf59391d3c4f5546c08df7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9ceec5886419860a31b518991a99e978e5a6a78dca1470103ad4ede0155f156", size = 76591184, upload-time = "2026-02-05T10:11:51.298Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/10c46e9527c4591d5264117debd8fe0e21bb23dbf378ce760add6b1e85b6/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a3c54377c5cf9c45d9b1a207df26dc8fe4f1042d07036cb17d80930c04b25d97", size = 52544155, upload-time = "2026-02-05T10:13:32.068Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/3c645c21358079097201090de7c30d110f5ec3fa01008e3ee81b0a77a354/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fc5ee50e2be9d40e913536f7f20cc6f87f25d8e413ebb32a3335ab6edf245d3e", size = 79150872, upload-time = "2026-02-05T10:16:03.465Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/bf4622e0ed8a93f5a685c76933e287477cf185a160c66478cf144fece489/opencv_contrib_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:f5d02357f4d5575c300eab3ec1c7ecfed3a9a53e55a76927bab7cfc9e0a67b68", size = 36829959, upload-time = "2026-02-05T07:02:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/d9/98/a03f69ff6fb86a67d584ecc990d85a95e6930b96e3f39ad1f8e019cb8ada/opencv_contrib_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:cb694dcf76bb2c8d7fa573fc1a99339e8b6640194d7778381e74cc3445369e45", size = 46486178, upload-time = "2026-02-05T07:02:19.551Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, +] + +[[package]] +name = "orderly-set" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "outlines-core" +version = "0.2.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/d3/e04e9145f8f806723dec9b9e5227ad695a3efcd3ced7794cf7c22b15df5e/outlines_core-0.2.11.tar.gz", hash = "sha256:dfce56f717ff5083e54cbcfdb66cad243365437fccbb5509adaa7e31e030f1d8", size = 197263, upload-time = "2025-05-19T10:12:51.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/8f/83c83e2afd142067c7f3cf2e152809195eee72d6a9b6c8745f13b827273d/outlines_core-0.2.11-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:89d79d8454b321f60047541a896d410ca9db631d241960266c4fe839cf5cd1b1", size = 1961650, upload-time = "2025-05-19T10:11:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e9/c6b99b4364b7026b71badc06b9809a2fc4154d6b0c475bc03ab4471f81e5/outlines_core-0.2.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:44d581893f8644da02db7be11887229a40d26077cbdd22072ad1ed1db0ad0b2d", size = 2133920, upload-time = "2025-05-19T10:11:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b8/cfa2bd8e1260eb1870c42a1a34389e9673a12335d09004ea6f1c82266a5e/outlines_core-0.2.11-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e88b7f717915d91136d915adb65c2603d2aa6457ec3fc336884bdb0b28d3188a", size = 1960688, upload-time = "2025-05-19T10:11:56.773Z" }, + { url = "https://files.pythonhosted.org/packages/b9/02/4cffd04e360e315b060692bf1a80f84bac1671ef90f12daf765db6d68791/outlines_core-0.2.11-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8c7ecdba2162e9b30b837251387c26b1a23f80f58d01d02e7600e4b1962c5333", size = 2130263, upload-time = "2025-05-19T10:11:58.1Z" }, + { url = "https://files.pythonhosted.org/packages/4e/85/69a450a486824026eca181a8d573aae3ecfdb25f0c2af852065dde17a372/outlines_core-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5fcefd221c10c95ce74838869450c6fdbbe2f581f0ba27e57a95232bd88c3a", size = 2289453, upload-time = "2025-05-19T10:11:59.919Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3c/d7cb3eac6870a68b9034854fbfa07e67abfa1fa0d92198b9fee83fe6d044/outlines_core-0.2.11-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a3c7774b112106f3afe931c65637fb3e0725d43707ceff1d34d6899cf0fa8200", size = 2115289, upload-time = "2025-05-19T10:12:01.527Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5f/4cef22e2cf1ec286bd78c0052a0fa7ecf8519144477e7d4e276cbd70c625/outlines_core-0.2.11-cp310-cp310-win32.whl", hash = "sha256:1cfbb4cdcf34be5c6b08d279928b2b1050ed4c5e96e6e8405e3e624305c6799e", size = 1768059, upload-time = "2025-05-19T10:12:03.058Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/ce6aceb6545bb1e13cf05c1f34468c5c14c8c8be92cdabcf777b4bb067ef/outlines_core-0.2.11-cp310-cp310-win_amd64.whl", hash = "sha256:670c1c1fca26fb5c7f00dbb11d1f81cca4204863c3dfdeee82017a6846397bf9", size = 2062413, upload-time = "2025-05-19T10:12:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ca/d5e92e197b40f62deb46dcc55567a51c8bf37943df7bc6658d93f30740f1/outlines_core-0.2.11-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e96b8d0b56afcd3b86f4efca466c578f3725da1148ef62423249c92993841762", size = 1961746, upload-time = "2025-05-19T10:12:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/02/b2/f3d6e7e37ebe1de3c345b53d8dc01e9b5c5f05b20e494fe94bf8972db4b0/outlines_core-0.2.11-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:d108ee8cd5e2fe71c2b0720b949d004901fec8bdb64bcd0c01b8abe38ab7ae1c", size = 2133815, upload-time = "2025-05-19T10:12:07.934Z" }, + { url = "https://files.pythonhosted.org/packages/07/21/62a680da6941b53d765160d22bdcf35849c22b7a987f4e9e8b7db7885c9f/outlines_core-0.2.11-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ebf42ab5b7ae38235d3c3333b5cacd6e91449b87b8a48a85094ea28ad9de9878", size = 1960539, upload-time = "2025-05-19T10:12:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/20cfb402aee1a7be0e08d861349570255ad2d17ba7fe7f8fd5706326588c/outlines_core-0.2.11-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:fd4305ff8418d14059d95dc3276ca96ba1b5aa499908e1af8bb3c7207aa7ac68", size = 2129894, upload-time = "2025-05-19T10:12:10.534Z" }, + { url = "https://files.pythonhosted.org/packages/4c/db/32c6e1170f139420e948fdd18a09a6175244bc0760dcf4dc2470e18411b9/outlines_core-0.2.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:132605b8dd1e3d1369da6a851992dd357f6376068292f6bd47caa7a28b794d19", size = 2289078, upload-time = "2025-05-19T10:12:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/25/c3/b6e6f4e08fa84d2424f82705a6dc47fee33cb91989010fa678736957dcf6/outlines_core-0.2.11-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b31d5fc83b78aad282dd667b8d6e684614481fe08a7609ce0ce45dee64cd2991", size = 2115075, upload-time = "2025-05-19T10:12:13.761Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9b/b84c4933e4f35b34e9b23fadd63a365ad8563cc7561d8528b33de4ee8102/outlines_core-0.2.11-cp311-cp311-win32.whl", hash = "sha256:3e316a79f3ecfa12c17746edebcbd66538ee22a43986982f6b96166fb94ee6b1", size = 1768254, upload-time = "2025-05-19T10:12:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/380c933c65ca9744c163fe4a3702ad7f3e9ca02e09ac84a09b6837cff9b6/outlines_core-0.2.11-cp311-cp311-win_amd64.whl", hash = "sha256:c260a042b5854ff69291649cfd112066e6bab0dad0bb9cec8a6c3705ef3a59cd", size = 2062167, upload-time = "2025-05-19T10:12:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2c/c7636823244c70e2960060bf9bd978248dffb55c5e7c91c46d18354b2a24/outlines_core-0.2.11-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4a9db4872bae083631d720994f4cee603bce0536b33d5a988814576863b657cf", size = 1957668, upload-time = "2025-05-19T10:12:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c7/09/5c62047da139d722317a444a4d01cd5f11943a8c2eaecce784341dd0844a/outlines_core-0.2.11-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8359a45c59f6a8f2eb717245806501a59044c75f6ea8bd08faaa131cc8cdec45", size = 2130493, upload-time = "2025-05-19T10:12:19.537Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/d6a2810f90e37d550168e0c0a9a915086ea721444727e3ca2c630898d1ef/outlines_core-0.2.11-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:5d26a46591377340e0b870b8a96ea8341058341a62ee0bded9098e0c88dd24f4", size = 1956804, upload-time = "2025-05-19T10:12:20.755Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ea/339e6c273b5581128c3b7ca27d428d8993c3085912af1a467aa32ef0e9d1/outlines_core-0.2.11-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:ae460a34675fb11d92a5c605a480fbae4cd6c1b2d11b3698da64a7fcaba64dcf", size = 2127085, upload-time = "2025-05-19T10:12:22.02Z" }, + { url = "https://files.pythonhosted.org/packages/92/c7/a65d1fddf49830ebc41422294eacde35286d9f68994a8aa905cb14f5aade/outlines_core-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86df9740368866295077346440d911df4972da2b3f1f54b8125e6f329e8a8891", size = 2287677, upload-time = "2025-05-19T10:12:24.24Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/8795aed8be9b77dd69d78e7cfbfcf28c179e6b08da6e56bbbf48a09fe55f/outlines_core-0.2.11-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:96ce4dd78f106799be4a0a5795cefd1352806162973756a4b6fce4bb6eddd7e4", size = 2113000, upload-time = "2025-05-19T10:12:25.446Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/cbe9294b06d92ee1892dbb6f2125d833d68e8629d45d080d6daba54eec2d/outlines_core-0.2.11-cp312-cp312-win32.whl", hash = "sha256:358db161cce3650ba822e118dcf0a1efa571c7deb4864ab9d64ca2c9cca7425d", size = 1765703, upload-time = "2025-05-19T10:12:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c9/ed3cf362515fac16e313368b9b2f2497051f4ded88679205830b6f889f54/outlines_core-0.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:231f9d20d2630c70665345821780d7808b29539620a75c99f65113b518c51032", size = 2060945, upload-time = "2025-05-19T10:12:28.294Z" }, + { url = "https://files.pythonhosted.org/packages/11/58/df6f57546f7792c990a4380ceaf99243a0b26b24c199e34e0a9277c89976/outlines_core-0.2.11-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0907ff25d79edbf8650268028de85a1b41b38696f147059e007da4626a1031f1", size = 1957172, upload-time = "2025-05-19T10:12:29.737Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cf/b07e33c44544e7865ec481554788807dfa6ad10fd86191ad21f2200f145e/outlines_core-0.2.11-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:f4146da5957f97550eebd19e80635e48035886fd10f03e9735cc111caaf74e93", size = 2130284, upload-time = "2025-05-19T10:12:31.408Z" }, + { url = "https://files.pythonhosted.org/packages/83/70/8f981706e2620914c48fd1edb42f9409d76b84c72149d48e89d14820fab6/outlines_core-0.2.11-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:8776a6db8843187c90e4c54bf94510cda68ca7a11c9b48d90587179fd3224bc2", size = 1956727, upload-time = "2025-05-19T10:12:32.996Z" }, + { url = "https://files.pythonhosted.org/packages/89/de/fba234a9c3984408f017ee0b1ca2e9d6191f8086afa649d3e4b04ed055e2/outlines_core-0.2.11-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:d44f38a89028bed50494420b47d08ebefa78f34b129e2ea6383c801e5ba62c26", size = 2126905, upload-time = "2025-05-19T10:12:34.261Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/7dcdc5198844145ab35528f9f93a58c3d47b87e54d0f79357c631d7b7a9a/outlines_core-0.2.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daef6eaaf8c3403455ab5cbf265cb5c6838df571eb7c4b23cddac19cfc701726", size = 2287320, upload-time = "2025-05-19T10:12:35.515Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/b420b6a3beaadbf8e9f2a82132120027efd6424634013fbeca8c2fed7467/outlines_core-0.2.11-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:76b2512417c68863f8f227a080e87f755682dfd895e23b021121318be11da579", size = 2112861, upload-time = "2025-05-19T10:12:36.742Z" }, + { url = "https://files.pythonhosted.org/packages/78/d6/7c2a016f7a5eab2f3df2b3a258f270872c78fe0dd7d9fbee87429f1b6b1f/outlines_core-0.2.11-cp313-cp313-win32.whl", hash = "sha256:707eeb3d190485f55a27ad9a6ad70df86688fa2bf405894a118283be7f59bd55", size = 1765574, upload-time = "2025-05-19T10:12:37.98Z" }, + { url = "https://files.pythonhosted.org/packages/a5/39/4c07f1d1f8e6ed85db9fe73a021113795a05aae8a84f36f0bdebb08bfde8/outlines_core-0.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:ad46698564c9b13cbfbc744067de12be73bd740d7b2de20ec6b979ad7511f7c9", size = 2060567, upload-time = "2025-05-19T10:12:39.228Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parse" +version = "1.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/18/0bea374e5ec3c8ba15365570002187f3fef9d7265ffbc2f649529878cc80/parse-1.21.1.tar.gz", hash = "sha256:825e1a88e9d9fb481b8d2ca709c6195558b6eaa97c559ad3a9a20aa2d12815a3", size = 29105, upload-time = "2026-02-19T02:20:07.645Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/13/114daf766c33aec6c5a3954e7ea653f8a7ade9602c5c5a2228281698c490/parse-1.21.1-py2.py3-none-any.whl", hash = "sha256:55339ca698019815df3b8e8b550e5933933527e623b0cdf1ca2f404da35ffb47", size = 19693, upload-time = "2026-02-19T02:20:06.575Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "partial-json-parser" +version = "0.2.1.1.post7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "peft" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "plyfile" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d8/f68ec9a54568236ba4c00fc0b002f74d2a559841c1fce86ab356599da032/plyfile-1.1.3.tar.gz", hash = "sha256:1c37720cb0470b762cec2dfef573ee7996a616c359c0ec34fdd766ace3ea0634", size = 36163, upload-time = "2025-10-22T01:58:40.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/22/1755bb4c7db15bb1ed63b4eb7a7fc133bf42a3f9cc806c0d5941e107ba90/plyfile-1.1.3-py3-none-any.whl", hash = "sha256:581302f07b1c298431dcaa9038bba2ae80f3f7868b29ccb826a07bc4488ff38a", size = 36455, upload-time = "2025-10-22T01:58:38.614Z" }, +] + +[[package]] +name = "polars" +version = "1.38.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/5e/208a24471a433bcd0e9a6889ac49025fd4daad2815c8220c5bd2576e5f1b/polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239", size = 717667, upload-time = "2026-02-06T18:13:23.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c", size = 810368, upload-time = "2026-02-06T18:11:55.819Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.38.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/4b/04d6b3fb7cf336fbe12fbc4b43f36d1783e11bb0f2b1e3980ec44878df06/polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec", size = 2812631, upload-time = "2026-02-06T18:13:25.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/a00defbddadd8cf1042f52380dcba6b6592b03bac8e3b34c436b62d12d3b/polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef", size = 44108001, upload-time = "2026-02-06T18:11:58.127Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac", size = 40230140, upload-time = "2026-02-06T18:12:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323", size = 41994039, upload-time = "2026-02-06T18:12:04.332Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba", size = 45755804, upload-time = "2026-02-06T18:12:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/54/02cd4074c98c361ccd3fec3bcb0bd68dbc639c0550c42a4436b0ff0f3ccf/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa", size = 42159605, upload-time = "2026-02-06T18:12:10.919Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/b2a5e720cc56eaa38b4518e63aa577b4bbd60e8b05a00fe43ca051be5879/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2", size = 45336615, upload-time = "2026-02-06T18:12:14.074Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/ee2e4b7de948090cfb3df37d401c521233daf97bfc54ddec5d61d1d31618/polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437", size = 45680732, upload-time = "2026-02-06T18:12:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/72c216f4ab0c82b907009668f79183ae029116ff0dd245d56ef58aac48e7/polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4", size = 41639413, upload-time = "2026-02-06T18:12:22.044Z" }, +] + +[[package]] +name = "polyscope" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/dc/622c79b24104da8a8162d21e728e126cf86f8d44e9799d929e1a2ea4bf07/polyscope-2.6.1.tar.gz", hash = "sha256:f7806a951f8a16376adc431300e48f856808ee80ab5cb7597db055cbc3b6ab21", size = 16946275, upload-time = "2026-02-26T23:34:32.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d5/16cdc68c168307a68ffe43af4c1b8801471000297795e8b6f21cd92da00f/polyscope-2.6.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:d1531a8244cd82ceb4012461a41d684de3d4c05ff00017d7a2f24cc1e6086f9a", size = 7721866, upload-time = "2026-02-26T23:32:40.844Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/875460fd39a4d9b189931ff083079abf61130c9305f14ee9b20c13f3d9fa/polyscope-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41c021a4c02eed298bb5858ff6b2ebb1068537884ad37e3d87153a5a2ae5cf", size = 7562651, upload-time = "2026-02-26T23:32:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/91ffa41c740baa2cba2cbd2e7580aec3689ff832622aecaa098b6339e8d9/polyscope-2.6.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cef24316adaa1bb362f59a17d4a3033bec70d0e5449c4dde7e12cf38be0873a8", size = 8323819, upload-time = "2026-02-26T23:32:44.989Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/78b25f0105ad82c4d5cede05f77d57fc8d6b37baf49c2e1b637141035d41/polyscope-2.6.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7014c004e951d6f44e7381d6aea95049802facc1f33ed18b9644145b1101b87a", size = 8858020, upload-time = "2026-02-26T23:32:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/94/57/04c0cac32ca9a58d844f57d01575d247968cd6f741c44fddc64de235f555/polyscope-2.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86b61a59af3d5fdb88d3ead19f52b6cb6f5eefa5b2430def480fe2e7b8091ed9", size = 9808191, upload-time = "2026-02-26T23:32:50.59Z" }, + { url = "https://files.pythonhosted.org/packages/65/4e/e795d108d0ec025321623509d03ed8584c5cd974f4289a2cc4e012daa567/polyscope-2.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f853332b0d37cdc9a1fa45aa5619450dbb3c78d8f8d7c805d2ae42a066cce4f", size = 10248182, upload-time = "2026-02-26T23:32:53.076Z" }, + { url = "https://files.pythonhosted.org/packages/36/d5/84c837b1dc2c84d45ad3471b6ec51bc918789b7fcbe48aad9aaeebff8e23/polyscope-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:248f53f1bf06da9a1e14b81beb1140924192311e120fb7c0cba011c689faa833", size = 9931126, upload-time = "2026-02-26T23:32:55.167Z" }, + { url = "https://files.pythonhosted.org/packages/19/54/9ee2b410ade2ffca03ef17130d9664fc797db5f19c42b8f0eaa6ba0e6769/polyscope-2.6.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1cce4512a764ba6caf708df886bdfaec8949a6bb0cb17fda63d5987a26297b26", size = 7722217, upload-time = "2026-02-26T23:32:57.342Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e8/7c61e76375741b3b3bfe5fe6a19eb882ecb3fe3d46c2e79c1957f5a3b6d3/polyscope-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dadd2041dc81348e091e9dfe9b6ab8600a6c28336e4076949e142a37236b53e8", size = 7563259, upload-time = "2026-02-26T23:32:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/4ea9b32bc7101216f7481b0ff28608346fc3dc9c6d77912a06035c66b7da/polyscope-2.6.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1b824348f5038010f4e83374b8a8708edf148647f087430e8c0bbf365850e35", size = 8324164, upload-time = "2026-02-26T23:33:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/3e/31/ee37048dc198fe404321a916a8a0a345fbc0ecdc1fde703616b1bb1a2247/polyscope-2.6.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f9355e572832cfaf780c882121ed0a99107e6844eb02eab52642f7fc35c1e47", size = 8858227, upload-time = "2026-02-26T23:33:03.26Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/31aac11566a1bda0e51ab6ea858935c6de20dcdc710671bc043569d80866/polyscope-2.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4cfadd10be44d66bd85e0a62d8326baae8ad807b5aa8346d5ad28fafbf8fac80", size = 9807938, upload-time = "2026-02-26T23:33:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/af01fd0d3e10d77ec9a6e5dc0278577a3c4c911191bbcb012476b7aedc5f/polyscope-2.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cfa948c88df587e908f752827eba3aeacdfe9c8d90a4d14e8ed8b83eb3e2376c", size = 10248502, upload-time = "2026-02-26T23:33:07.887Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e4/bf9bb5a94fc345073a0f0a64550b8b2e5257c004994f26f585fbb8316255/polyscope-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8a67315175bf65e13b0b20c2f9fae8400d7c1f6199c34ec33a63d2cbf00ab108", size = 9931266, upload-time = "2026-02-26T23:33:10.43Z" }, + { url = "https://files.pythonhosted.org/packages/08/a7/517139d11cb494fad5aa2b08cf59fcbbc34c15c4e8395ee1d582d0168300/polyscope-2.6.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:f61886e7569f3eb1846c3017c18dc49e6dd442c0bf2da1e05507e8b3e5d4af7a", size = 7724141, upload-time = "2026-02-26T23:33:12.72Z" }, + { url = "https://files.pythonhosted.org/packages/06/6a/35f02a0b922f74a5d2483b69d17bf1d19cbec33abee09f1ab169199ed084/polyscope-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac3a7cbab533f8d18d15e848b871db6c4d4402f8f82632fcec95c436d4c80551", size = 7563297, upload-time = "2026-02-26T23:33:14.526Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/cfcdd797da77d3ea2f5a5ee417e4bc8e436e660bede1cd3650fd6084c08e/polyscope-2.6.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b9a609826419637ae86668ee42929a0989d2d0310c14b85267b8dd5d596adf8", size = 8324969, upload-time = "2026-02-26T23:33:16.771Z" }, + { url = "https://files.pythonhosted.org/packages/73/50/d5cf303f7b6d56a95a58cb27ec36509728428916e876cabf0966dfaff5f2/polyscope-2.6.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8a3876c9b4c53c63107080da8b83be11d073dfee9716ae81414fb22f6bf45c9", size = 8859044, upload-time = "2026-02-26T23:33:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/9144aa7cdb3995ab004bef4e46ca7a5a4b1643363592ce36756752e018b2/polyscope-2.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea6395d325e8f59a9a4b12b565da041402082a8acef2cfbd0893f85e25cecf57", size = 9808052, upload-time = "2026-02-26T23:33:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/19df9e6ac88763c586806fa3e98b2b44fdf3e46455ae36440c02375116f1/polyscope-2.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9326278149143046ebca8daa287fd24cbff10416d356c23f66d069f1558501a1", size = 10250253, upload-time = "2026-02-26T23:33:23.744Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/cd98d5a4e6ded193e83b0fce45ec0bb5b8ce02ef60e7ca557c57f062570b/polyscope-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:983810d96b11dd3d3b2d6c46a0c2baef304a5d54186588e3650960af088e14d9", size = 9938674, upload-time = "2026-02-26T23:33:26.984Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7b/e2f106e4ff9818155abd53e1f9b1feade237a6d201627461ec468f8be2df/polyscope-2.6.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:b09955d3c9a867cb361138bc334796a56fa70396dd54960622671ba9ed7cb777", size = 7724100, upload-time = "2026-02-26T23:33:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/48/e4/946997877e6b65daffee9a93732d83be61ee6b0ee885662761b3d8ac72e5/polyscope-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f3aab3920373cda3d3f0bf080641937471cdf5bd0d46cebd6d509a8be6782473", size = 7563606, upload-time = "2026-02-26T23:33:31.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6e/ca9244f2fd527584009b9fd4d103a8e74453cff85e258db181ef91fe315d/polyscope-2.6.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34bf22148e481972bdc5575d7fab3180d07c7c3a14cc6ffdf9e0f0a0bf1466c", size = 8324957, upload-time = "2026-02-26T23:33:33.616Z" }, + { url = "https://files.pythonhosted.org/packages/9b/08/b87ba04af1e5558d7e6b1cba79e3b3252d0c18c2bb43d38a867bde04ae12/polyscope-2.6.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:997d2d9e5e80e9f2b73d69fae7c46a30d040d1b342845f7c3b3abed22a189ae1", size = 8858835, upload-time = "2026-02-26T23:33:35.449Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7f/4fbf7316e39d88b07612a600e228f58db3fd9249df109de88dd2ab8967cd/polyscope-2.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5c91f7d753af40057b47adf367c180ded65e532f757b4d51c66efe6ff27e386a", size = 9808313, upload-time = "2026-02-26T23:33:38.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/20/21db6d916ef78414dd4acf1cf995101521b431598e6dffc7cb9445bbd560/polyscope-2.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:17129e85d51c7c4913084db22cdff5c3c1f8900996c3618808c0c1c6a525d144", size = 10250268, upload-time = "2026-02-26T23:33:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/89/43/fb61019fe9d11366ecf498c786f0ba38255421bba50c54d12150b0df693f/polyscope-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:50e5a774bfb427fd63f3353292397a3c093b77dd4bf1c2d64e4019f89b6d5a96", size = 9938658, upload-time = "2026-02-26T23:33:42.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/e71d8820f656dd3d01df69be63500afae07ffc0c13ed89ba3b75b162c458/polyscope-2.6.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:65f18063eebc018f72691b46a3d44adfbfb79efce4058cb1afd3a08ac4dafdb5", size = 7724600, upload-time = "2026-02-26T23:33:45.136Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/95e9fc4554635b2c6984fe7003e86f7502f132c3d63212130844f91dccdc/polyscope-2.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d5209ad6f9ac910442df026fba5517d9177e0f06b2685e233cae5e3cef33a4", size = 7563691, upload-time = "2026-02-26T23:33:46.876Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fc/53b6135a978d0ef4b8b6358b122a7ee5161f33732ddc01bfa56b2bd58a96/polyscope-2.6.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba32deca0b1a87d933aa5964059c25ad151a07f5a7ca164a5e435876b770f71e", size = 8325611, upload-time = "2026-02-26T23:33:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/fe567601abc012b5baa93bc596853135eba4c597f8b8a2b5d8ad86e756f7/polyscope-2.6.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7494b7da2fee2d1f6511b5d95543a41c1221ebd22bf72a78acf07623ea4fdb8c", size = 8859144, upload-time = "2026-02-26T23:33:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/b9/dd/14964cdf4a6336938e11822abbe9f756bd8b495f09b31316e35bbcd1f289/polyscope-2.6.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57c67b84c232fca8e2cab5d0f5f1dc1b4a24e4a5b324c2bbdc13048b7d9315b3", size = 9808537, upload-time = "2026-02-26T23:33:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/17a4c0fa879692430f937ecbb3427e06dddd19390d26a107355545bb6784/polyscope-2.6.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce451d7c77860944877ea42bc1d8e4a959968f9b61355075150cc7c51c18f9cf", size = 10250955, upload-time = "2026-02-26T23:33:55.588Z" }, + { url = "https://files.pythonhosted.org/packages/12/96/63506b65eb7bdf3ce7f4cfab4d30b0017eb48dc9464fadc051015508a937/polyscope-2.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:3dee233aa9e8d1d7034f052f28a8dfa0c48f563da47aa02f1a9842f94638d89b", size = 10094458, upload-time = "2026-02-26T23:33:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/33/50/0db33586428032d3101a3813440b6d80170b91a159f210c1f520917aa805/polyscope-2.6.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:621599eadee068f7651df727f7860c3b5d1e6cbbae94bbacb2fff7d1c2acefc4", size = 7736026, upload-time = "2026-02-26T23:34:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/c4d447b7651abe7db2e203834d0eab15a1472cb895ad0d9d7ee013a9214a/polyscope-2.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9c30168e49192b8323f56793cb333dc1ab418f20ea1830330385969c2cd4fdb0", size = 7571461, upload-time = "2026-02-26T23:34:02.77Z" }, + { url = "https://files.pythonhosted.org/packages/65/5a/372346f5c6f189aed1a8e7980b06630f12abe74c06a0c34ac6d4e581d165/polyscope-2.6.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4f2c729d0ec325c8379a14caedd78c2ad4067c63e3aa3ed30da8872202a5c49", size = 8339326, upload-time = "2026-02-26T23:34:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/6e55200e76ad3358d1edf9e84dce4ab6c687c67eb412a0def78583c13f7b/polyscope-2.6.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf8080a1222d2e1dee796e59eabb5fc70e37535e2ddf17b1a9a9d1819ece0e97", size = 8873826, upload-time = "2026-02-26T23:34:06.846Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/06efd908898fce870ef1f058ac80f35c96379a478f27eb707f2d583fb2da/polyscope-2.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8400c1a23a79750df049c42b4ba5876767a07ff12b11eb72acc5874ea16a5a10", size = 9825762, upload-time = "2026-02-26T23:34:08.911Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1c/ad5f7c591198ce0405604edf3b40d2628d68871a3298c36f6c66ce3d2819/polyscope-2.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6046b718bda5db995ad769824e5c785713e5f8ea53894706091ec57c033a160f", size = 10262814, upload-time = "2026-02-26T23:34:11.07Z" }, + { url = "https://files.pythonhosted.org/packages/e4/00/fceab71136a0cc8e64aa3a6c99371ce1c799a08093c9b3dd29a209b19f5e/polyscope-2.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b62e09ab2beaa5dae44acd165ce25a301d4df16f9daca46cbe2cd488d327efe1", size = 10133788, upload-time = "2026-02-26T23:34:13.668Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + +[[package]] +name = "proglog" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/af/c108866c452eda1132f3d6b3cb6be2ae8430c97e9309f38ca9dbd430af37/proglog-0.1.12.tar.gz", hash = "sha256:361ee074721c277b89b75c061336cb8c5f287c92b043efa562ccf7866cda931c", size = 8794, upload-time = "2025-05-09T14:36:18.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/1b/f7ea6cde25621cd9236541c66ff018f4268012a534ec31032bcb187dc5e7/proglog-0.1.12-py3-none-any.whl", hash = "sha256:ccaafce51e80a81c65dc907a460c07ccb8ec1f78dc660cfd8f9ec3a22f01b84c", size = 6337, upload-time = "2025-05-09T14:36:16.798Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, + { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, + { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "py-spy" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/e2/ff811a367028b87e86714945bb9ecb5c1cc69114a8039a67b3a862cef921/py_spy-0.4.1.tar.gz", hash = "sha256:e53aa53daa2e47c2eef97dd2455b47bb3a7e7f962796a86cc3e7dbde8e6f4db4", size = 244726, upload-time = "2025-07-31T19:33:25.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc", size = 3682508, upload-time = "2025-07-31T19:33:13.753Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/e4d280e9e0bec71d39fc646654097027d4bbe8e04af18fb68e49afcff404/py_spy-0.4.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:1fb8bf71ab8df95a95cc387deed6552934c50feef2cf6456bc06692a5508fd0c", size = 1796395, upload-time = "2025-07-31T19:33:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084", size = 2034938, upload-time = "2025-07-31T19:33:17.194Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/36862e3eea59f729dfb70ee6f9e14b051d8ddce1aa7e70e0b81d9fe18536/py_spy-0.4.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:532d3525538254d1859b49de1fbe9744df6b8865657c9f0e444bf36ce3f19226", size = 2658968, upload-time = "2025-07-31T19:33:18.916Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/9ea0b586b065a623f591e5e7961282ec944b5fbbdca33186c7c0296645b3/py_spy-0.4.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4972c21890b6814017e39ac233c22572c4a61fd874524ebc5ccab0f2237aee0a", size = 2147541, upload-time = "2025-07-31T19:33:20.565Z" }, + { url = "https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29", size = 2763338, upload-time = "2025-07-31T19:33:22.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl", hash = "sha256:d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc", size = 1818784, upload-time = "2025-07-31T19:33:23.802Z" }, +] + +[[package]] +name = "py3nvml" +version = "0.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/7e/fa282e456b87570d663ce97946b4dcb16850d4495ce4bd625a1a10c8ed56/py3nvml-0.2.7.tar.gz", hash = "sha256:09ee1d04598a6e664e24465f804ce3bfe119a6fdb5362df1c168f8aa929fbd73", size = 58224, upload-time = "2021-11-22T14:30:27.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/3a/ea6f2419bd20f97f65ee55a9910c722313fe99cacc0bf77afb4b74b446ff/py3nvml-0.2.7-py3-none-any.whl", hash = "sha256:30101170d1f51419c8d21fd8ca6cdc333a552b4f8a945c2fc7d107d77e4220dd", size = 55503, upload-time = "2021-11-22T14:30:25.794Z" }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/16d7af6fae7803f4c691856bc0d8d433ccf30e106432e2ef7707ee19a38a/pybase64-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f63aa7f29139b8a05ce5f97cdb7fad63d29071e5bdc8a638a343311fe996112a", size = 38241, upload-time = "2025-12-06T13:22:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3e/268beb8d2240ab55396af4d1b45d2494935982212549b92a5f5b57079bd3/pybase64-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5943ec1ae87a8b4fe310905bb57205ea4330c75e2c628433a7d9dd52295b588", size = 31672, upload-time = "2025-12-06T13:22:28.854Z" }, + { url = "https://files.pythonhosted.org/packages/80/14/4365fa33222edcc46b6db4973f9e22bda82adfb6ab2a01afff591f1e41c8/pybase64-1.4.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5f2b8aef86f35cd5894c13681faf433a1fffc5b2e76544dcb5416a514a1a8347", size = 65978, upload-time = "2025-12-06T13:22:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/1c/22/e89739d8bc9b96c68ead44b4eec42fe555683d9997e4ba65216d384920fc/pybase64-1.4.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6ec7e53dd09b0a8116ccf5c3265c7c7fce13c980747525be76902aef36a514a", size = 68903, upload-time = "2025-12-06T13:22:31.29Z" }, + { url = "https://files.pythonhosted.org/packages/77/e1/7e59a19f8999cdefe9eb0d56bfd701dd38263b0f6fb4a4d29fce165a1b36/pybase64-1.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7528604cd69c538e1dbaafded46e9e4915a2adcd6f2a60fcef6390d87ca922ea", size = 57516, upload-time = "2025-12-06T13:22:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/42/ad/f47dc7e6fe32022b176868b88b671a32dab389718c8ca905cab79280aaaf/pybase64-1.4.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4ec645f32b50593879031e09158f8681a1db9f5df0f72af86b3969a1c5d1fa2b", size = 54533, upload-time = "2025-12-06T13:22:33.457Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/7ab312b5a324833953b00e47b23eb4f83d45bd5c5c854b4b4e51b2a0cf5b/pybase64-1.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:634a000c5b3485ccc18bb9b244e0124f74b6fbc7f43eade815170237a7b34c64", size = 57187, upload-time = "2025-12-06T13:22:34.566Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/80acab1fcbaaae103e6b862ef5019192c8f2cd8758433595a202179a0d1d/pybase64-1.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:309ea32ad07639a485580af1be0ad447a434deb1924e76adced63ac2319cfe15", size = 57730, upload-time = "2025-12-06T13:22:35.581Z" }, + { url = "https://files.pythonhosted.org/packages/1f/24/84256d472400ea3163d7d69c44bb7e2e1027f0f1d4d20c47629a7dc4578e/pybase64-1.4.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d10d517566b748d3f25f6ac7162af779360c1c6426ad5f962927ee205990d27c", size = 53036, upload-time = "2025-12-06T13:22:36.621Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/33aecbed312ee0431798a73fa25e00dedbffdd91389ee23121fed397c550/pybase64-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74cc0f4d835400857cc5c6d27ec854f7949491e07a04e6d66e2137812831f4c", size = 56321, upload-time = "2025-12-06T13:22:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/a341b050746658cbec8cab3c733aeb3ef52ce8f11e60d0d47adbdf729ebf/pybase64-1.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b591d774ac09d5eb73c156a03277cb271438fbd8042bae4109ff3a827cd218c", size = 50114, upload-time = "2025-12-06T13:22:38.752Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/f7e6680ae6dc4ddff39112ad66e0fa6b2ec346e73881bafc08498c560bc0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5eb588d35a04302ef6157d17db62354a787ac6f8b1585dd0b90c33d63a97a550", size = 66570, upload-time = "2025-12-06T13:22:40.221Z" }, + { url = "https://files.pythonhosted.org/packages/4c/71/774748eecc7fe23869b7e5df028e3c4c2efa16b506b83ea3fa035ea95dc2/pybase64-1.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df8b122d5be2c96962231cc4831d9c2e1eae6736fb12850cec4356d8b06fe6f8", size = 55700, upload-time = "2025-12-06T13:22:41.289Z" }, + { url = "https://files.pythonhosted.org/packages/b3/91/dd15075bb2fe0086193e1cd4bad80a43652c38d8a572f9218d46ba721802/pybase64-1.4.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31b7a85c661fc591bbcce82fb8adaebe2941e6a83b08444b0957b77380452a4b", size = 52491, upload-time = "2025-12-06T13:22:42.628Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/f357d63ea3774c937fc47160e040419ed528827aa3d4306d5ec9826259c0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e6d7beaae65979fef250e25e66cf81c68a8f81910bcda1a2f43297ab486a7e4e", size = 53957, upload-time = "2025-12-06T13:22:44.615Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/243693771701a54e67ff5ccbf4c038344f429613f5643169a7befc51f007/pybase64-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a6276bc3a3962d172a2b5aba544d89881c4037ea954517b86b00892c703d007", size = 68422, upload-time = "2025-12-06T13:22:45.641Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/f987081bf6bc1d1eda3012dae1b06ad427732ef9933a632cb8b58f9917f8/pybase64-1.4.3-cp310-cp310-win32.whl", hash = "sha256:4bdd07ef017515204ee6eaab17e1ad05f83c0ccb5af8ae24a0fe6d9cb5bb0b7a", size = 33622, upload-time = "2025-12-06T13:22:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/c169a769fe90128f16d394aad87b2096dd4bf2f035ae0927108a46b617df/pybase64-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:5db0b6bbda15110db2740c61970a8fda3bf9c93c3166a3f57f87c7865ed1125c", size = 35799, upload-time = "2025-12-06T13:22:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/bdbe6af0bd4f3fe5bc70e77ead7f7d523bb9d3ca3ad50ac42b9adbb9ca14/pybase64-1.4.3-cp310-cp310-win_arm64.whl", hash = "sha256:f96367dfc82598569aa02b1103ebd419298293e59e1151abda2b41728703284b", size = 31158, upload-time = "2025-12-06T13:22:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cf/6e712491bd665ea8633efb0b484121893ea838d8e830e06f39f2aae37e58/pybase64-1.4.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94cf50c36bb2f8618982ee5a978c4beed9db97d35944fa96e8586dd953c7994a", size = 38007, upload-time = "2025-12-06T13:26:32.804Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/9272cae1c49176337dcdbd97511e2843faae1aaf5a5fb48569093c6cd4ce/pybase64-1.4.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:01bc3ff5ca1341685c6d2d945b035f442f7b9c3b068a5c6ee8408a41fda5754e", size = 31538, upload-time = "2025-12-06T13:26:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/17546f97befe429c73f622bbd869ceebb518c40fdb0dec4c4f98312e80a5/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03d0aa3761a99034960496280c02aa063f856a3cc9b33771bc4eab0e4e72b5c2", size = 40682, upload-time = "2025-12-06T13:26:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/464b36d5dfb61f3da17858afaeaa876a9342d58e9f17803ce7f28b5de9e8/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ca5b1ce768520acd6440280cdab35235b27ad2faacfcec064bc9c3377066ef1", size = 41306, upload-time = "2025-12-06T13:26:36.351Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/a748dfc0969a8d960ecf1e82c8a2a16046ffec22f8e7ece582aa3b1c6cf9/pybase64-1.4.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3caa1e2ddad1c50553ffaaa1c86b74b3f9fbd505bea9970326ab88fc68c4c184", size = 35452, upload-time = "2025-12-06T13:26:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/4d37bd3577d1aa6c732dc099087fe027c48873e223de3784b095e5653f8b/pybase64-1.4.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bd47076f736b27a8b0f9b30d93b6bb4f5af01b0dc8971f883ed3b75934f39a99", size = 36125, upload-time = "2025-12-06T13:26:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + +[[package]] +name = "pycountry" +version = "26.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5", size = 1627695, upload-time = "2025-05-17T17:23:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798", size = 1675772, upload-time = "2025-05-17T17:23:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f", size = 1668083, upload-time = "2025-05-17T17:23:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea", size = 1706056, upload-time = "2025-05-17T17:23:24.031Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe", size = 1806478, upload-time = "2025-05-17T17:23:26.066Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/d3/3be31542180c0300b6860129ff1e3a428f3ef580727616ce22462626129b/pydantic_extra_types-2.11.2.tar.gz", hash = "sha256:3a2b83b61fe920925688e7838b59caa90a45637d1dbba2b1364b8d1f7ff72a0a", size = 203929, upload-time = "2026-04-05T20:50:51.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/a4/7b6ab05c18d6c6e682382a0f0235301684452c4131a869f45961d1d032c9/pydantic_extra_types-2.11.2-py3-none-any.whl", hash = "sha256:683b8943252543e49760f89733b1519bc62f31d1a287ebbdc5a7b7959fb4acfd", size = 82851, upload-time = "2026-04-05T20:50:50.036Z" }, +] + +[package.optional-dependencies] +pycountry = [ + { name = "pycountry" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pydispatcher" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/db/030d0700ae90d2f9d52c2f3c1f864881e19cef8cba3b0a08759c8494c19c/PyDispatcher-2.0.7.tar.gz", hash = "sha256:b777c6ad080dc1bad74a4c29d6a46914fa6701ac70f94b0d66fbcfde62f5be31", size = 38891, upload-time = "2023-02-17T20:11:13.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/0e/9ee7bc0b48ec45d93b302fa2d787830dca4dc454d31a237faa5815995988/PyDispatcher-2.0.7-py3-none-any.whl", hash = "sha256:96543bea04115ffde08f851e1d45cacbfd1ee866ac42127d9b476dc5aefa7de0", size = 12040, upload-time = "2023-02-17T20:11:11.991Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pygltflib" +version = "1.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dataclasses-json" }, + { name = "deprecated" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/e8/f8232abdf9c085333689b0a428dcd1d0f83edd1ecafa6ed878a633d8c9d5/pygltflib-1.16.5.tar.gz", hash = "sha256:1f15740d5a7aaf71a5083e285af6b361184958e255659132f4ba8fe4f3d21ea9", size = 43272, upload-time = "2025-07-24T06:35:38.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d6/7eb8a0e4eb30add2b76c957a41107a5f2ba26472d656e2733728bec0476b/pygltflib-1.16.5-py3-none-any.whl", hash = "sha256:41d3349c59dcf1586faeaee29c967be07ac2bf7cecdb8ae2b527da1f25afdaac", size = 27557, upload-time = "2025-07-24T06:35:37.328Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyinstrument" +version = "5.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/7f/d3c4ef7c43f3294bd5a475dfa6f295a9fee5243c292d5c8122044fa83bcb/pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0", size = 266889, upload-time = "2026-01-04T18:38:58.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/74/c66e1bf3565600d78f53195efb6f8fd31610f85a58aa3fee39c56bf71d1b/pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6", size = 131470, upload-time = "2026-01-04T18:37:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6b/606c5bfa311b5be74f58ef505c678216dda2be3b76a2ac770c2b0fccff77/pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236", size = 124567, upload-time = "2026-01-04T18:37:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/15/70/c8a88defb77873513971f590549c48ceb70f7ef10f30a689762ef36dd877/pyinstrument-5.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75a7e17377d4405666bbaf126b1fd7bbb7e206d7246e6db3d62864d3d4790ae3", size = 149205, upload-time = "2026-01-04T18:37:25.696Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4b/0e64fefb939af472c3fbc63ab45224766447bde73f51579f3ecc335b0a49/pyinstrument-5.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5381cc6583d26e04d9298acded4242f4fe71986f1472c8aee6992c6816f0cac5", size = 147900, upload-time = "2026-01-04T18:37:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/38/6e/b4209711c61176acfeb6c351e9f88a37ed3d3bc3b749c374c0a655ee8f50/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec08a530bef8d3492d31d8b0b12d0cfde09539f2a1c4b9678662ebc3c843e478", size = 148133, upload-time = "2026-01-04T18:37:29.047Z" }, + { url = "https://files.pythonhosted.org/packages/26/28/f323b70789833baf0628af7b9f797b8c1a13b695bd8aa582b1312f14b602/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d671168508129b472be570bc9aee361190ba917b997c703bd134bb4de445ce7", size = 147652, upload-time = "2026-01-04T18:37:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/16/cd/9b0af0307a3a2cffb48ca76275c50b8bec3f85ca6e7b996e2e6cfbda1207/pyinstrument-5.1.2-cp310-cp310-win32.whl", hash = "sha256:5957a94f84564b374a7f856d1b322345d600964280b0d687b8ddcc483f21e576", size = 125793, upload-time = "2026-01-04T18:37:31.906Z" }, + { url = "https://files.pythonhosted.org/packages/05/89/fe4c650c252aefb8064bfdff6c0a020d33d15c55dc22abfa1f352dcc2dd1/pyinstrument-5.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:38a2180a7801c51610b50e5d423674b21872efd019ccf05a11b7f9016cb1dcfc", size = 126679, upload-time = "2026-01-04T18:37:33.59Z" }, + { url = "https://files.pythonhosted.org/packages/79/ef/0288edd620fb0cf2074d8c8e3567007a6bac66307b839d99988563de4eb8/pyinstrument-5.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3739a05583ea6312c385eb59fe985cd20d9048e95f9eeeb6a2f6c35202e2d36e", size = 131284, upload-time = "2026-01-04T18:37:35.01Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4e/2a90a6997d9f7a39a6998d56de72e52673ebf5a9169a1c39dbf173e95105/pyinstrument-5.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c9ee05dc75ac5fb18498c311e624f77f7f321f7ff325b251aa09e52e46f1d6a", size = 124468, upload-time = "2026-01-04T18:37:36.628Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/7bfd403e81f9b5ec523f60cced8f516ee52312752bb2e0fafabfd90bbd78/pyinstrument-5.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a49a55ca5b75218767e29cacbe515d0b66fc18cb48a937bca0f77b8dafc7202", size = 148057, upload-time = "2026-01-04T18:37:37.998Z" }, + { url = "https://files.pythonhosted.org/packages/50/3a/7205d7c199947d18edcd013af4ddf4d3cca85c5488fbe493050035947f7c/pyinstrument-5.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c45c14974ff04b1bfdc6c2a448627c6da7409c7800d0eb7bd03fb435dcb41d7", size = 146526, upload-time = "2026-01-04T18:37:39.642Z" }, + { url = "https://files.pythonhosted.org/packages/24/e8/f6864172e7ebe4bc5209bafbc574a619b4c511b9506b941789b11441be7c/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22b9c04b3982c41c04b1c5ed05d1bc3a2ba26533450084058119f6dc160e70a3", size = 147179, upload-time = "2026-01-04T18:37:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/6d/04/89ef2d1c34767bfdbcc74ab0c7e0d021d7fac5e79873239e4ca26e97d6da/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c4995ee0774801790c138f0dfec17d4e7a7ef09a6d56d53cbcbf0578a711021", size = 146354, upload-time = "2026-01-04T18:37:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/64441547ec12391b92c739a3b0685059e7dfa088d928df8364676ef7abc7/pyinstrument-5.1.2-cp311-cp311-win32.whl", hash = "sha256:fe449e4a8ee60a2a27cf509350a584670f4c3704649601be7937598f09dbe7ca", size = 125790, upload-time = "2026-01-04T18:37:44.141Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8b/0a5f6b239294decb0ecd932711f3470bfbd42fc2e08a94cd5c1f4f6da7f1/pyinstrument-5.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:3fb839429671a42bf349335af4c1ce5cf83386ac11f04df0bc40720d4cb7d77d", size = 126578, upload-time = "2026-01-04T18:37:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/26/d9/8fa5571ddd21b2b7189bd8b0bb4e90be1659a54dda5af51c7f6bf2b5666f/pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0", size = 131419, upload-time = "2026-01-04T18:37:46.843Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/0512adb83cadfeaa1a215dc9784defff5043c5aa052d15015e3d8013af75/pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5", size = 124446, upload-time = "2026-01-04T18:37:48.572Z" }, + { url = "https://files.pythonhosted.org/packages/9b/78/c45f0b668fb3c8c0d32058a451a8e1d34737cd7586387982185e12df1977/pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26", size = 149694, upload-time = "2026-01-04T18:37:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/2ca3ca9906ce6e05070f431c54d54ccbaf57a980cfa58032d35b0b0ac1f8/pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53", size = 148461, upload-time = "2026-01-04T18:37:51.544Z" }, + { url = "https://files.pythonhosted.org/packages/18/d2/bfe84a4326172ef68655b65b49fd041eeb94c8e59ee47258589b8b79dd3b/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e", size = 148560, upload-time = "2026-01-04T18:37:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/db7f5def351e869230b0165828c4edacbf3fdda8d66aff30dd73a62082c2/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d", size = 148178, upload-time = "2026-01-04T18:37:54.278Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bc/aea3329576e20b987d205027b8e6442ece845d681b9f9d8682d5404f81f3/pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281", size = 125927, upload-time = "2026-01-04T18:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/d928434ec3a840478e95fd0d73b0dfc0b8060a07b06f4b45e9df30444e9a/pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45", size = 126675, upload-time = "2026-01-04T18:37:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/b9aea969eec67c129652000446384d550a0df45c297adc9fd74da2f8482c/pyinstrument-5.1.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b8bab2334bf1d4c9e92d61db574300b914b594588a6b6dd67c45450152dfc29", size = 131418, upload-time = "2026-01-04T18:37:58.642Z" }, + { url = "https://files.pythonhosted.org/packages/8f/62/76418eb29b5591f3e5500369a6777ce928135c3aa6ccdb0c861a9c6ca93b/pyinstrument-5.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:13dcc138a61298ef4994b7aebff509d2c06db89dfd6e2021f0b9cd96aaa44ec3", size = 124448, upload-time = "2026-01-04T18:37:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/07/73/874bccc04bcf6f4babc3de1a9568e209e7e40998563974f5030b0fb4d3e0/pyinstrument-5.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8abd4a7ffa2e7f9e00039a5e549e8eebc80d7ca8d43f0fb51a50ff2b117ce4a", size = 149853, upload-time = "2026-01-04T18:38:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/268446c4388d77ff4abdeaff202356e1527b3ff9576f5587443a24980bec/pyinstrument-5.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3a05108edebc30f31e2c69c904576042f1158b2513ab80adc08f7848a7a8f0", size = 148641, upload-time = "2026-01-04T18:38:03.086Z" }, + { url = "https://files.pythonhosted.org/packages/fc/15/4f8dea3381483e68d00582a9b823a21a088acfe77a847a7991a1a8feed76/pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f70d588b53f3f35829d1d1ddfa05e07fcebf1434b3b1509d542ca317d8e9a2a5", size = 148674, upload-time = "2026-01-04T18:38:04.805Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/72c180454b6511d5b90166f8828e1bab3b45d0489952a1fe48c5c585233d/pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b007327e0d6a6a01d5064883dd27c19996f044ce7488d507826fee7884e6a32e", size = 148315, upload-time = "2026-01-04T18:38:06.114Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f0/4c27cebddf22a8840bd8b419366bb321ce41f921ca1893e309c932ab28bf/pyinstrument-5.1.2-cp313-cp313-win32.whl", hash = "sha256:9ba0e6b17a7e86c3dc02d208e4c25506e8f914d9964ae89449f1f37f0b70abc0", size = 125926, upload-time = "2026-01-04T18:38:07.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/20/6b1bee88ddef065b0df3a3ba4ba60ed8a9ca443d5cded7152a8a9750914f/pyinstrument-5.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:660d7fc486a839814db0b2f716bc13d8b99b9c780aaeb47f74a70a34adc02a7b", size = 126678, upload-time = "2026-01-04T18:38:08.826Z" }, + { url = "https://files.pythonhosted.org/packages/66/0f/7d5154c92904bdf25be067a7fe4cad4ba48919f16ccbb51bb953d9ae1a20/pyinstrument-5.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0baed297beee2bb9897e737bbd89e3b9d45a2fbbea9f1ad4e809007d780a9b1e", size = 131388, upload-time = "2026-01-04T18:38:10.491Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/bf83231a3f951e11b4dfaf160e1eeba1ce29377eab30e3d2eb6ee22ff3ba/pyinstrument-5.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebb910a32a45bde6c3fc30c578efc28a54517990e11e94b5e48a0d5479728568", size = 124456, upload-time = "2026-01-04T18:38:11.792Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/762cf10896d907268629e1db08a48f128984a53e8d92b99ea96f862597e5/pyinstrument-5.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad403c157f9c6dba7f731a6fca5bfcd8ca2701a39bcc717dcc6e0b10055ffc4", size = 149594, upload-time = "2026-01-04T18:38:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/48580e16e623d89af58b89c552c95a2ae65f70a1f4fab1d97879f34791db/pyinstrument-5.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f456cabdb95fd343c798a7f2a56688b028f981522e283c5f59bd59195b66df5", size = 148339, upload-time = "2026-01-04T18:38:14.767Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/38157a8a6ec67789d8ee109fd09877ea3340df44e1a7add8f249e30a8ade/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e9c4dcc1f2c4a0cd6b576e3604abc37496a7868243c9a1443ad3b9db69d590f", size = 148485, upload-time = "2026-01-04T18:38:16.121Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/31ee72b19cfc48a82801024b5d653f07982154a11381a3ae65bbfdbf2c7b/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acf93b128328c6d80fdb85431068ac17508f0f7845e89505b0ea6130dead5ca6", size = 148106, upload-time = "2026-01-04T18:38:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b4/7ab20243187262d66ab062778b1ccac4ca55090752f32a83f603f4e5e3a2/pyinstrument-5.1.2-cp314-cp314-win32.whl", hash = "sha256:9c7f0167903ecff8b1d744f7e37b2bd4918e05a69cca724cb112f5ed59d1e41b", size = 126593, upload-time = "2026-01-04T18:38:18.968Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a0/db6a8ae3182546227f5a043b1be29b8d5f98bf973e20d922981ef206de85/pyinstrument-5.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:ce3f6b1f9a2b5d74819ecc07d631eadececf915f551474a75ad65ac580ec5a0e", size = 127358, upload-time = "2026-01-04T18:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/59/d2/719f439972b3f80e35fb5b1bcd888c3218d60dbc91957b99ffafd7ac9221/pyinstrument-5.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:af8651b239049accbeecd389d35823233f649446f76f47fd005316b05d08cef2", size = 132317, upload-time = "2026-01-04T18:38:21.669Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/0ebfef69ae926665fae635424c5647411235c3689c9a9ad69fd68de6cae2/pyinstrument-5.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6082f1c3e43e1d22834e91ba8975f0080186df4018a04b4dd29f9623c59df1d", size = 124917, upload-time = "2026-01-04T18:38:23.385Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ee/5599f769f515a0f1c97443edc7394fe2b9829bf39f404c046499c1a62378/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c031eb066ddc16425e1e2f56aad5c1ce1e27b2432a70329e5385b85e812decee", size = 157407, upload-time = "2026-01-04T18:38:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/fd/40/32aa865252288caef301237488ee309bd6701125888bf453d23ab764e357/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f447ec391cad30667ba412dce41607aaa20d4a2496a7ab867e0c199f0fe3ae3d", size = 155068, upload-time = "2026-01-04T18:38:26.112Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/0b56a1540fe1c357dfcda82d4f5b52c87fada5962cbf18703ea39ccbbe69/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50299bddfc1fe0039898f895b10ef12f9db08acffb4d85326fad589cda24d2ee", size = 155186, upload-time = "2026-01-04T18:38:27.914Z" }, + { url = "https://files.pythonhosted.org/packages/7a/48/7ef84abfc3e41148cf993095214f104e75ecff585e94c6e8be001e672573/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a193ff08825ece115ececa136832acb14c491c77ab1e6b6a361905df8753d5c6", size = 153979, upload-time = "2026-01-04T18:38:29.236Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cf/a28ad117d58b33c1d74bcdfbbcf1603b67346883800ac7d510cff8d3bcee/pyinstrument-5.1.2-cp314-cp314t-win32.whl", hash = "sha256:de887ba19e1057bd2d86e6584f17788516a890ae6fe1b7eed9927873f416b4d8", size = 127267, upload-time = "2026-01-04T18:38:30.619Z" }, + { url = "https://files.pythonhosted.org/packages/8e/97/03635143a12a5d941f545548b00f8ac39d35565321a2effb4154ed267338/pyinstrument-5.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b6a71f5e7f53c86c9b476b30cf19509463a63581ef17ddbd8680fee37ae509db", size = 128164, upload-time = "2026-01-04T18:38:32.281Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pynput" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "evdev", marker = "'linux' in sys_platform or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "python-xlib", marker = "'linux' in sys_platform or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/c3/dccf44c68225046df5324db0cc7d563a560635355b3e5f1d249468268a6f/pynput-1.8.1.tar.gz", hash = "sha256:70d7c8373ee98911004a7c938742242840a5628c004573d84ba849d4601df81e", size = 82289, upload-time = "2025-03-17T17:12:01.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl", hash = "sha256:42dfcf27404459ca16ca889c8fb8ffe42a9fe54f722fd1a3e130728e59e768d2", size = 91693, upload-time = "2025-03-17T17:12:00.094Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748, upload-time = "2025-11-14T09:30:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-applicationservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/9d/3cf36e7b08832e71f5d48ddfa1047865cf2dfc53df8c0f2a82843ea9507a/pyobjc_framework_applicationservices-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4fd1b008757182b9e2603a63c6ffa930cc412fab47294ec64260ab3f8ec695d", size = 32791, upload-time = "2025-11-14T09:36:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/17/86/d07eff705ff909a0ffa96d14fc14026e9fc9dd716233648c53dfd5056b8e/pyobjc_framework_applicationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdddd492eeac6d14ff2f5bd342aba29e30dffa72a2d358c08444da22129890e2", size = 32784, upload-time = "2025-11-14T09:36:08.755Z" }, + { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" }, + { url = "https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38", size = 32859, upload-time = "2025-11-14T09:36:15.208Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/0f1d4dcf2345e875e5ea9761d5a70969e241d24089133d21f008dde596f5/pyobjc_framework_applicationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a5d2845249b6a85ba9e320a9848468c3f8cd6f59605a9a43f406a7810eaa830", size = 33115, upload-time = "2025-11-14T09:36:18.384Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3196b40fec68b4413c92875311f17ccf4c3ff7d2e53676f8fc18ad29bd18/pyobjc_framework_applicationservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f43c9a24ad97a9121276d4d571aa04a924282c80d7291cfb3b29839c3e2013a8", size = 32997, upload-time = "2025-11-14T09:36:21.58Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/dab21d2210d3ef7dd0616df7e8ea89b5d8d62444133a25f76e649a947168/pyobjc_framework_applicationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f72e20009a4ebfd5ed5b23dc11c1528ad6b55cc63ee71952ddb2a5e5f1cb7da", size = 33238, upload-time = "2025-11-14T09:36:24.751Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825, upload-time = "2025-11-14T09:40:28.354Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-coretext" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1c/ddecc72a672d681476c668bcedcfb8ade16383c028eac566ac7458fb91ef/pyobjc_framework_coretext-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1c8315dcef6699c2953461d97117fe81402f7c29cff36d2950dacce028a362fd", size = 29987, upload-time = "2025-11-14T09:46:58.028Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/7b8efc41e743adfa2d74b92dec263c91bcebfb188d2a8f5eea1886a195ff/pyobjc_framework_coretext-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f6742ba5b0bb7629c345e99eff928fbfd9e9d3d667421ac1a2a43bdb7ba9833", size = 29990, upload-time = "2025-11-14T09:47:01.206Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" }, + { url = "https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f", size = 30110, upload-time = "2025-11-14T09:47:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5d/85e059349e9cfbd57269a1f11f56747b3ff5799a3bcbd95485f363c623d8/pyobjc_framework_coretext-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:14100d1e39efb30f57869671fb6fce8d668f80c82e25e7930fb364866e5c0dab", size = 30697, upload-time = "2025-11-14T09:47:10.932Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/adf9d306e9ead108167ab7a974ab7d171dbacf31c72fad63e12585f58023/pyobjc_framework_coretext-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:782a1a9617ea267c05226e9cd81a8dec529969a607fe1e037541ee1feb9524e9", size = 30095, upload-time = "2025-11-14T09:47:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ca/6321295f47a47b0fca7de7e751ddc0ddc360413f4e506335fe9b0f0fb085/pyobjc_framework_coretext-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7afe379c5a870fa3e66e6f65231c3c1732d9ccd2cd2a4904b2cd5178c9e3c562", size = 30702, upload-time = "2025-11-14T09:47:17.292Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin' or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130') or (sys_platform != 'linux' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/f4/50c42c84796886e4d360407fb629000bb68d843b2502c88318375441676f/pyobjc_framework_quartz-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c6f312ae79ef8b3019dcf4b3374c52035c7c7bc4a09a1748b61b041bb685a0ed", size = 217799, upload-time = "2025-11-14T09:59:32.62Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, +] + +[[package]] +name = "pyopengl" +version = "3.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyrefly" +version = "0.55.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/c4/76e0797215e62d007f81f86c9c4fb5d6202685a3f5e70810f3fd94294f92/pyrefly-0.55.0.tar.gz", hash = "sha256:434c3282532dd4525c4840f2040ed0eb79b0ec8224fe18d957956b15471f2441", size = 5135682, upload-time = "2026-03-03T00:46:38.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/b0/16e50cf716784513648e23e726a24f71f9544aa4f86103032dcaa5ff71a2/pyrefly-0.55.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:49aafcefe5e2dd4256147db93e5b0ada42bff7d9a60db70e03d1f7055338eec9", size = 12210073, upload-time = "2026-03-03T00:46:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/89500c01bac3083383011600370289fbc67700c5be46e781787392628a3a/pyrefly-0.55.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2827426e6b28397c13badb93c0ede0fb0f48046a7a89e3d774cda04e8e2067cd", size = 11767474, upload-time = "2026-03-03T00:46:18.003Z" }, + { url = "https://files.pythonhosted.org/packages/78/68/4c66b260f817f304ead11176ff13985625f7c269e653304b4bdb546551af/pyrefly-0.55.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7346b2d64dc575bd61aa3bca854fbf8b5a19a471cbdb45e0ca1e09861b63488c", size = 33260395, upload-time = "2026-03-03T00:46:20.509Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/10bd48c9f860064f29f412954126a827d60f6451512224912c265e26bbe6/pyrefly-0.55.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:233b861b4cff008b1aff62f4f941577ed752e4d0060834229eb9b6826e6973c9", size = 35848269, upload-time = "2026-03-03T00:46:23.418Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/bc65cdd5243eb2dfea25dd1321f9a5a93e8d9c3a308501c4c6c05d011585/pyrefly-0.55.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5aa85657d76da1d25d081a49f0e33c8fc3ec91c1a0f185a8ed393a5a3d9e178", size = 38449820, upload-time = "2026-03-03T00:46:26.309Z" }, + { url = "https://files.pythonhosted.org/packages/e5/64/58b38963b011af91209e87f868cc85cfc762ec49a4568ce610c45e7a5f40/pyrefly-0.55.0-py3-none-win32.whl", hash = "sha256:23f786a78536a56fed331b245b7d10ec8945bebee7b723491c8d66fdbc155fe6", size = 11259415, upload-time = "2026-03-03T00:46:30.875Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0b/a4aa519ff632a1ea69eec942566951670b870b99b5c08407e1387b85b6a4/pyrefly-0.55.0-py3-none-win_amd64.whl", hash = "sha256:d465b49e999b50eeb069ad23f0f5710651cad2576f9452a82991bef557df91ee", size = 12043581, upload-time = "2026-03-03T00:46:33.674Z" }, + { url = "https://files.pythonhosted.org/packages/f1/51/89017636fbe1ffd166ad478990c6052df615b926182fa6d3c0842b407e89/pyrefly-0.55.0-py3-none-win_arm64.whl", hash = "sha256:732ff490e0e863b296e7c0b2471e08f8ba7952f9fa6e9de09d8347fd67dde77f", size = 11548076, upload-time = "2026-03-03T00:46:36.193Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-custom-exit-code" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/9d/e1eb0af5e96a5c34f59b9aa69dfb680764420fe60f2ec28cfbc5339f99f8/pytest-custom_exit_code-0.3.0.tar.gz", hash = "sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635", size = 3633, upload-time = "2019-08-07T09:45:15.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a0/effb6cbbccfd1c106c572d3d619b3418d71093afb4cd4f91f51e6a1799d2/pytest_custom_exit_code-0.3.0-py3-none-any.whl", hash = "sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b", size = 4055, upload-time = "2019-08-07T09:45:13.767Z" }, +] + +[[package]] +name = "pytest-datadir" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/46/db060b291999ca048edd06d6fa9ee95945d088edc38b1172c59eeb46ec45/pytest_datadir-1.8.0.tar.gz", hash = "sha256:7a15faed76cebe87cc91941dd1920a9a38eba56a09c11e9ddf1434d28a0f78eb", size = 11848, upload-time = "2025-07-30T13:52:12.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/7a/33895863aec26ac3bb5068a73583f935680d6ab6af2a9567d409430c3ee1/pytest_datadir-1.8.0-py3-none-any.whl", hash = "sha256:5c677bc097d907ac71ca418109adc3abe34cf0bddfe6cf78aecfbabd96a15cf0", size = 6512, upload-time = "2025-07-30T13:52:11.525Z" }, +] + +[[package]] +name = "pytest-env" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/56/a931c6f6194917ff44be41b8586e2ffd13a18fa70fb28d9800a4695befa5/pytest_env-1.5.0.tar.gz", hash = "sha256:db8994b9ce170f135a37acc09ac753a6fc697d15e691b576ed8d8ca261c40246", size = 15271, upload-time = "2026-02-17T18:31:39.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/af/99b52a8524983bfece35e51e65a0b517b22920c023e57855c95e744e19e4/pytest_env-1.5.0-py3-none-any.whl", hash = "sha256:89a15686ac837c9cd009a8a2d52bd55865e2f23c82094247915dae4540c87161", size = 10122, upload-time = "2026-02-17T18:31:37.496Z" }, +] + +[[package]] +name = "pytest-instafail" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/bd/e0ba6c3cd20b9aa445f0af229f3a9582cce589f083537978a23e6f14e310/pytest-instafail-0.5.0.tar.gz", hash = "sha256:33a606f7e0c8e646dc3bfee0d5e3a4b7b78ef7c36168cfa1f3d93af7ca706c9e", size = 5849, upload-time = "2023-03-31T17:17:32.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c0/c32dc39fc172e684fdb3d30169843efb65c067be1e12689af4345731126e/pytest_instafail-0.5.0-py3-none-any.whl", hash = "sha256:6855414487e9e4bb76a118ce952c3c27d3866af15487506c4ded92eb72387819", size = 4176, upload-time = "2023-03-31T17:17:30.065Z" }, +] + +[[package]] +name = "pytest-regressions" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-datadir" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/d7/6d7525320538d59c1763ebb9f9fdde957966fea607236b2c905ded6f8c98/pytest_regressions-2.10.0.tar.gz", hash = "sha256:5239d29ffe5760acb4a37d95d575383473a2e62c55ede2e89cff735d3bbd2ac9", size = 115513, upload-time = "2026-02-10T13:37:08.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/32b0a304563e42693049e31be097427f05451aa42c04e3819b4a5c0afe78/pytest_regressions-2.10.0-py3-none-any.whl", hash = "sha256:e40b98fd1e26435bf694fbd497ac74f4580cbda3b794562faab3dcea2300c0eb", size = 25087, upload-time = "2026-02-10T13:37:06.661Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "python-memcached" +version = "1.62" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/3c/204e5c6087efc85b52a68edce8678d44eb28718f5f145e036c277beb467c/python-memcached-1.62.tar.gz", hash = "sha256:0285470599b7f593fbf3bec084daa1f483221e68c1db2cf1d846a9f7c2655103", size = 28591, upload-time = "2024-01-14T15:56:03.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/1b/3b15a37831ae34a264d7d5b71f3ae9fe74a81251453a3ec2135e76888ef1/python_memcached-1.62-py2.py3-none-any.whl", hash = "sha256:1bdd8d2393ff53e80cd5e9442d750e658e0b35c3eebb3211af137303e3b729d1", size = 15136, upload-time = "2024-01-14T15:56:01.725Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "python-xlib" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, +] + +[[package]] +name = "pytorch-ranger" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/32/9269ee5981995e760c3bf51d6cf7f84a2ce051eca2315753910585bce50d/pytorch_ranger-0.1.1.tar.gz", hash = "sha256:aa7115431cef11b57d7dd7bc86e7302a911dae467f62ec5d0b10e1ff744875db", size = 7865, upload-time = "2020-03-30T07:37:22.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/70/12256257d861bbc3e176130d25be1de085ce7a9e60594064888a950f2154/pytorch_ranger-0.1.1-py3-none-any.whl", hash = "sha256:1e69156c9cc8439185cb8ba4725b18c91947fbe72743e25aca937da8aeb0c8ec", size = 14436, upload-time = "2020-03-30T07:37:21.198Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/28/a652709bd76ca7533cd1c443b03add9f5051fdf71bc6bdb8801dddd4e7a3/pywinpty-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:ff05f12d775b142b11c6fe085129bdd759b61cf7d41da6c745e78e3a1ef5bf40", size = 2114320, upload-time = "2026-02-04T21:53:50.972Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/a0181cc5c2d5635d3dbc3802b97bc8e3ad4fa7502ccef576651a5e08e54c/pywinpty-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:340ccacb4d74278a631923794ccd758471cfc8eeeeee4610b280420a17ad1e82", size = 235670, upload-time = "2026-02-04T21:50:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-include" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/be/2d07ad85e3d593d69640876a8686eae2c533db8cb7bf298d25c421b4d2d5/pyyaml-include-1.4.1.tar.gz", hash = "sha256:1a96e33a99a3e56235f5221273832464025f02ff3d8539309a3bf00dec624471", size = 20592, upload-time = "2024-03-25T14:56:43.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/ca/6a2cc3a73170d10b5af1f1613baa2ed1f8f46f62dd0bfab2bffd2c2fe260/pyyaml_include-1.4.1-py3-none-any.whl", hash = "sha256:323c7f3a19c82fbc4d73abbaab7ef4f793e146a13383866831631b26ccc7fb00", size = 19079, upload-time = "2024-03-25T14:56:41.274Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "quack-kernels" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "nvidia-cutlass-dsl" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch-c-dlpack-ext" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/db/d2e480fd71c38b88ffcbf40298d604400c64e0ffcaa06d6aa61a87b2673a/quack_kernels-0.3.9.tar.gz", hash = "sha256:4fd272f52142e408a591b94be7c6a0261e222e034e599bce6da827eeae8ad04d", size = 212760, upload-time = "2026-04-05T06:34:58.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/a8/eea5885361143c19505a8e86890a681c363ac0f9ac6ba02b5c2c82ebe44b/quack_kernels-0.3.9-py3-none-any.whl", hash = "sha256:160364a32fd72df6e934adb2bb2ae324843ddccffc88aaa6f5de4c9a00ec7ac8", size = 216038, upload-time = "2026-04-05T06:34:57.426Z" }, +] + +[[package]] +name = "qwen-vl-utils" +version = "0.0.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/b1/ad4fc2260a3badd278b38d642f3b987412f1f6682f0ef2b31b0572d5caa8/qwen_vl_utils-0.0.14.tar.gz", hash = "sha256:9c7cad5ae803b3a10f8bb7194deb12aeacdd032f92f4224e880c73587a7346ad", size = 8453, upload-time = "2025-09-23T09:38:57.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/43/80f67e0336cb2fc725f8e06f7fe35c1d0fe946f4d2b8b2175e797e07349e/qwen_vl_utils-0.0.14-py3-none-any.whl", hash = "sha256:5e28657bfd031e56bd447c5901b58ddfc3835285ed100f4c56580e0ade054e96", size = 8120, upload-time = "2025-09-23T09:38:56.297Z" }, +] + +[[package]] +name = "ray" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/a2/0cc3dc138d149dbbf69a90ded986118ae9420a5ac0dbf99e0c5883152950/ray-2.46.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:719244b84df79502e5f09497f256618d94d78d66fbaf229422008a0568d3a0ff", size = 68482152, upload-time = "2025-05-07T21:04:19.538Z" }, + { url = "https://files.pythonhosted.org/packages/9f/73/ab50b18ee34bc2acc7cd2a6082d217f4ffdc3b737f680f1139f94be38d47/ray-2.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4378a86919e6643238a1094f711b87fa8dc1a18b998d4190f69ab33c64a22a8c", size = 65800160, upload-time = "2025-05-07T21:04:28.868Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1e/d6759d149a1a2ac1739b70e5790366e52751c66a3ccfdbf2879af166cac9/ray-2.46.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:396b912a4dbf64966e2fdfca9facbcafe57b792ca4842ac5ae17507fdbdfe89f", size = 67436067, upload-time = "2025-05-07T21:04:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/c1f24e22ea404e779550c506e421101005f70571caece30d0de7b0b91374/ray-2.46.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c12850608c57c8afd9613a9f757d77663c50d4bd4e77ba2f181425052520c01a", size = 68361851, upload-time = "2025-05-07T21:04:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/6ac0a38a85769225f7edbdb2f3b7f45b1602aa6394096aa8c16ac98e822f/ray-2.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc953aa4879c7a77893f921905df5cf65227cafd94fbc8273bec65ea393eacdd", size = 26027424, upload-time = "2025-05-07T21:04:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/b9469a45f69a41e3c14ec49193bd54b9b0a82ea71eb73d78d2c847fdf95b/ray-2.46.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:942ba51de6f9cd7fb2ed17618181af48ce6b9517743d3235d846ec32295eca76", size = 68436815, upload-time = "2025-05-07T21:04:57.649Z" }, + { url = "https://files.pythonhosted.org/packages/14/69/bbbfd35ea2d1e2ec50108e98a6530afb05a700ccc76022c8d6ce4cb28bf8/ray-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af84f3ed0854bb6de28192ca9e0a3bfa1eb34d69f118ae6348522198896480c8", size = 65755431, upload-time = "2025-05-07T21:05:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/34/a3/4df839e1c9af7bb2638cc660d99c9d6af6ca21c66ab845a207aeb0eae109/ray-2.46.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:81c8ce8b7ba33cb607ec78f5eb2555470e3046bb317732d8282e8189bb58ccbd", size = 67570047, upload-time = "2025-05-07T21:05:13.212Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/768c58d5f5826ee49fde22f6375e12f33d7dfc7eb5f7a41bae9523b1d9cc/ray-2.46.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:d4ddedc3f4d48df564bcee7b131c98c9f898fef0a57483f4ba335f47f951a62f", size = 68468355, upload-time = "2025-05-07T21:05:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/72/75/9c0f7ba79e42be4d3d65006c1e2956a31f9086dbc86645eb5605a57ed0ff/ray-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:130415c4d231830156f37ce70acbdb5fdee10f6886adc4e85bdc4533d51c24c6", size = 25975781, upload-time = "2025-05-07T21:05:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a6/39aeebaec26afdae18ead68e6da1f1ea59d14c6e4b869f4b5f0c1d0647d6/ray-2.46.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:d1f37ead29299637144726f809c2e0ff958dd9c0e75930ef614156d6a0a3a57f", size = 68426042, upload-time = "2025-05-07T21:05:33.616Z" }, + { url = "https://files.pythonhosted.org/packages/43/d8/9bdf2980bbaee14d941e1d12edd909416d655d768a0f03150a9c647d07f1/ray-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7a064acfeee7f0677d9e3f25daef9c59593559faea764b44a3e2c5331d5d832", size = 65739806, upload-time = "2025-05-07T21:05:40.426Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a8/f5653816755171eb000aae416e916903a6630e747f979bae62d6418097ca/ray-2.46.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:006cbe1a8fdc37664114aa218773100ee891399785e256c202e48958d2dac167", size = 67581031, upload-time = "2025-05-07T21:05:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/21/c3/b2f2f09da4a85a8dcd5a3e63a5707b6c126c5be29bf846aa78dfb1168631/ray-2.46.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:5cec1edda93f618ffd2301f81d5398037f03fa9b16825e7e4d8a00ae7a9a4381", size = 68518627, upload-time = "2025-05-07T21:05:55.354Z" }, + { url = "https://files.pythonhosted.org/packages/37/be/9bbfd67580b8a412d2fdb3086440f1183407277316e4213b036a6c4ff4c3/ray-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d3160f8d187baaea91a86d16a9fd81136cf8607419c94b7a74d66fce774b5c2", size = 25960038, upload-time = "2025-05-07T21:06:03.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/95/81d71592f294526a8a0ada660e2c452ec6d6523a5fad4f50a765b35ab1e7/ray-2.46.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b2fc2c43ea0a37521193c61ef9a27b6fca8dbab116a58a52fd44344cd73e1ece", size = 68418157, upload-time = "2025-05-07T21:06:14.249Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ac/300ad38988e17ac0d860fdde662ee3ec9d2f2f5f4f42a4ad7394cac482e0/ray-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4296dd8c0174256a04ee4b54abe013b6802a45fb85fb7cfdb1375231965d6d4d", size = 65730961, upload-time = "2025-05-07T21:06:21.996Z" }, + { url = "https://files.pythonhosted.org/packages/01/75/29fa07686becd4c61f92f6356bbfcda333bbc060f97e58401d6d19da62cb/ray-2.46.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:808daece1f12bd8924b9c6382a0f98da6f5c6886cfb271ed8d89407a89413cd5", size = 67531710, upload-time = "2025-05-07T21:06:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/78fa9cb52d4d396af51400c249978881b9bb6febd9dd462c082a398de697/ray-2.46.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:a5a28c0a311d2c3221dcf729c40898a6df82466bb5af21e81be0453e09856adf", size = 68471790, upload-time = "2025-05-07T21:06:41.73Z" }, +] + +[package.optional-dependencies] +serve = [ + { name = "aiohttp" }, + { name = "aiohttp-cors" }, + { name = "colorful" }, + { name = "fastapi" }, + { name = "grpcio" }, + { name = "opencensus" }, + { name = "prometheus-client" }, + { name = "py-spy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "starlette" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "virtualenv" }, + { name = "watchfiles" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "retinaface-py" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "opencv-python" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/de/f90cc5c235b8ccc8d893b0616a5d7ec59c59fe19339fa3b7fa7867b9f1ff/retinaface_py-0.0.2-py3-none-any.whl", hash = "sha256:d8cc969841714e4275c35f2cbcec951b88e23a583608588c6c4ecd985a71d66d", size = 37388, upload-time = "2023-02-14T15:45:16.209Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "lark", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or extra != 'group-7-cosmos3-vllm'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "robomimic" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "egl-probe" }, + { name = "h5py" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "tensorboard" }, + { name = "tensorboardx" }, + { name = "termcolor" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/c3/44b1d1ea4bcb4bbed43d19e09505f4142714451ded74020d4f679cdc89fb/robomimic-0.2.0.tar.gz", hash = "sha256:ee3bb5cf9c3e1feead6b57b43c5db738fd0a8e0c015fdf6419808af8fffdc463", size = 192919, upload-time = "2021-12-17T19:00:33.279Z" } + +[[package]] +name = "robosuite" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mujoco" }, + { name = "numba" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/a1/9dd07a9a5e09c6aa032faf531da985808b34437cbf6c8f358fe8f7c47118/robosuite-1.4.0.tar.gz", hash = "sha256:a8a6233d7458dbd91bf00a86cab15aa1c178bd9d1b28d515db2cf3d152cb48e6", size = 192182147, upload-time = "2022-12-01T07:31:55.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/08/fe231064caaf2766d47818ca12fd8acf10dc4e762a33dabc6293e83bfead/robosuite-1.4.0-py3-none-any.whl", hash = "sha256:aba065e7b36745738cede259457b2cb349427f3608728d867ef3a2034cb62994", size = 193477875, upload-time = "2022-12-01T07:28:53.457Z" }, +] + +[[package]] +name = "robotmq" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/f0/ef9c4f0225c32c19ee46b400665e4d64f211622caa34756aef9a88f733d5/robotmq-0.1.12-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d4da1c810994b9de01b920b6fa5161ef41264b51da940d35e4d8323a5a23e3e", size = 1343883, upload-time = "2025-11-24T18:30:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/9b524bf9c104296143cf96fd064d09b5b7cb696ec716f0f9c57bebf0bf0f/robotmq-0.1.12-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ef8cf40e2ea5e0f8b4b9660185b29c2424a4e86a8bb4fc06319ff2363d4e2e0", size = 1470340, upload-time = "2025-11-19T06:00:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/18/74/b12192335a637f4dd320c24bcea0a3e524e3b059d5a3d14c450ce31fd981/robotmq-0.1.12-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b433871bdf757e82af8c1bc4e765a8ed4d455489b67805fdae450415c473e080", size = 1660348, upload-time = "2025-11-24T18:30:25.927Z" }, + { url = "https://files.pythonhosted.org/packages/f5/78/eaded45995e814090018fc65aa93490d7d2452367090f387c965c4424f96/robotmq-0.1.12-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c34d349b5f4bb9dc7b56e7b6c0db7a098866367698ac1a6b68c81141b50dc0f3", size = 1817457, upload-time = "2025-11-19T06:00:48.968Z" }, + { url = "https://files.pythonhosted.org/packages/86/20/34ba3a8ab4a4d5e7064335d9a4a39b89d0b3a3409491268ef0aa16f01df9/robotmq-0.1.12-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90923b470931608b433ac9e7ef2af81a641b9baa3a9cf0327c85f42187e97b40", size = 1977276, upload-time = "2025-11-24T18:30:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3b/8bf5d47927dea6621b4acfd410a8abe5af4f604c20ca3e1cd7ddfe03d80c/robotmq-0.1.12-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d48b3e0586c79c12aa4d7e6e49fcd15fdd8601b84634ee0bca41141eeff0c7a0", size = 2166868, upload-time = "2025-11-19T06:00:51.307Z" }, + { url = "https://files.pythonhosted.org/packages/09/08/cdd5a8733354d892088219f90b9c5e5feffb580cbddbf74b86add57dd038/robotmq-0.1.12-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572dd633fbb4c62bb1c8bbd916757aa57d4e11a14824f20a5d1a8e7dfe2355ed", size = 2294107, upload-time = "2025-11-24T18:30:28.67Z" }, + { url = "https://files.pythonhosted.org/packages/43/62/6ce2fa8cb35d79313a112fb35460ae607405c268b9001be1d0360f1a584d/robotmq-0.1.12-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9689c87b4e7a65a59c3a52be44de2505926b001355a908efff66f2074618f8e4", size = 2516369, upload-time = "2025-11-19T06:00:53.627Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, +] + +[[package]] +name = "s3fs" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore" }, + { name = "aiohttp" }, + { name = "fsspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/be/392c8c5e0da9bfa139e41084690dd49a5e3e931099f78f52d3f6070105c6/s3fs-2026.2.0.tar.gz", hash = "sha256:91cb2a9f76e35643b76eeac3f47a6165172bb3def671f76b9111c8dd5779a2ac", size = 84152, upload-time = "2026-02-05T21:57:57.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl", hash = "sha256:65198835b86b1d5771112b0085d1da52a6ede36508b1aaa6cae2aedc765dfe10", size = 31328, upload-time = "2026-02-05T21:57:56.532Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "safehttpx" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "lazy-loader", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pillow", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "lazy-loader", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pillow", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'linux' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jeepney", marker = "sys_platform == 'linux' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "slangtorch" +version = "1.3.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "hatchling" }, + { name = "ninja" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/ca/d3cd00226bc622d7edf37face855de85ea1119d0afd5a02d666edf95fbb6/slangtorch-1.3.19.tar.gz", hash = "sha256:7141dc5c8b6af4acc6eaf3f2a17f2eca5320b61cc6c62abd426e0647d115e775", size = 21183333, upload-time = "2026-02-12T14:59:01.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/64/7f23b15411a270258bae40270913472396bbde1f490cbbe044898d41392d/slangtorch-1.3.19-py3-none-any.whl", hash = "sha256:fe2d70375518144b1618598fe74936d73c7e143243089e9ae8864609e9a3c906", size = 21281856, upload-time = "2026-02-12T14:58:56.937Z" }, +] + +[[package]] +name = "smart-open" +version = "7.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/be/a66598b305763861a9ab15ff0f2fbc44e47b1ce7a776797337a4eef37c66/smart_open-7.5.1.tar.gz", hash = "sha256:3f08e16827c4733699e6b2cc40328a3568f900cb12ad9a3ad233ba6c872d9fe7", size = 54034, upload-time = "2026-02-23T11:01:28.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ea/dcdecd68acebb49d3fd560473a43499b1635076f7f1ae8641c060fe7ce74/smart_open-7.5.1-py3-none-any.whl", hash = "sha256:3e07cbbd9c8a908bcb8e25d48becf1a5cbb4886fa975e9f34c672ed171df2318", size = 64108, upload-time = "2026-02-23T11:01:27.429Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "supervisor" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/b5/37e7a3706de436a8a2d75334711dad1afb4ddffab09f25e31d89e467542f/supervisor-4.3.0.tar.gz", hash = "sha256:4a2bf149adf42997e1bb44b70c43b613275ec9852c3edacca86a9166b27e945e", size = 468912, upload-time = "2025-08-23T18:25:02.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "tensorboardx" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/a9/fc520ea91ab1f3ba51cbf3fe24f2b6364ed3b49046969e0868d46d6da372/tensorboardx-2.6.5.tar.gz", hash = "sha256:ca176db3997ee8c07d2eb77381225956a3fd1c10c91beafab1f17069adc47017", size = 4770195, upload-time = "2026-04-03T15:40:23.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0f/69fbab4c30b2f3a76e6de67585ea72a8eccf381751f5c0046b966fde9658/tensorboardx-2.6.5-py3-none-any.whl", hash = "sha256:c10b891d00af306537cb8b58a039b2ba41571f0da06f433a41c4ca8d6abe1373", size = 87510, upload-time = "2026-04-03T15:40:22.111Z" }, +] + +[[package]] +name = "tensorstore" +version = "0.1.78" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/ee/05eb424437f4db63331c90e4605025eedc0f71da3faff97161d5d7b405af/tensorstore-0.1.78.tar.gz", hash = "sha256:e26074ffe462394cf54197eb76d6569b500f347573cd74da3f4dd5f510a4ad7c", size = 6913502, upload-time = "2025-10-06T17:44:29.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/1e/77eff7bb320f72a9cb6e9a19eee4d78bee4a6ac1c28ceef60df28b4ab670/tensorstore-0.1.78-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f1bc58164ad964d9cc298d20b62ca704ab6241639a21015e47ce6ea5b5cae27f", size = 15710776, upload-time = "2025-10-06T17:43:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/55/df/f74f8004b246006ae03c90c28e32d71eb8a86a5b325d2d84dda327babdcc/tensorstore-0.1.78-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1910101ea85b6507958da28628ef53712c5311df19a795f449604f82bae6a24b", size = 13771121, upload-time = "2025-10-06T17:43:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/be/b8/ab0d0b2afc53f47fbfd95c10d9ae21d393019aca45c8513657b8d7002f1f/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e92195db0c8c3ca749f24b1e930ab93382ac27430ac4ad2e3f53fc8f739323f", size = 18154513, upload-time = "2025-10-06T17:43:51.694Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/c1b4cc6a089a39f63e8d189a55c715e393995628b12b4c8560b3ae4874ba/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90570b867f9100f7405e4116c73910d0bd283a101500ea5680c5a8a881ea05c6", size = 20048971, upload-time = "2025-10-06T17:43:54.358Z" }, + { url = "https://files.pythonhosted.org/packages/58/2a/7167087885b12473f20ae4fddb9a8feeed6bd44ea8d42c73ae29ad3d1591/tensorstore-0.1.78-cp310-cp310-win_amd64.whl", hash = "sha256:4de9d4ee93d712cb665890af0738f4d74cac3b9b9a0492d477a3ee63fbbf445b", size = 12707793, upload-time = "2025-10-06T17:43:56.405Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/45070c393586306cef44c7bfc47ed2eddfb8930e648aaa847f615e3ae797/tensorstore-0.1.78-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1c91e7ff93561612bd9868f3ee56702b0e4fecb45079a4c152dff9a6aa751913", size = 15712387, upload-time = "2025-10-06T17:43:58.458Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d8/c045da71460301f37704e1ab1eec9e7e480dc711dbd281d86dc3d792c50e/tensorstore-0.1.78-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:781e123d392b2d9115e94b01849797a4540f54cd6d34c6ee32b9491f2f2a399c", size = 13773158, upload-time = "2025-10-06T17:44:00.285Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/2b0d48100816649ec516fca31d02ad8028c090324e77b1c309c09a172350/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e650d363ad43754626a828a242785e6359a59fedb171276e9a0c66c0bd963cd4", size = 18154388, upload-time = "2025-10-06T17:44:02.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a1/d9be82de18afe764c0fc7fb21b3d3bb0ad12845d202861fff7189afdb99d/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33fed0ffa7a42ad24ce203486cf039f81b211723b45bd54859ba237a9d3aedb9", size = 20050304, upload-time = "2025-10-06T17:44:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fc/b980958f91a9780e4dbc1038da723d2ad91307dbe30563359606f78926e5/tensorstore-0.1.78-cp311-cp311-win_amd64.whl", hash = "sha256:c02df3d8de4703d9ee42c8f620b2288f41c19a0fd5ffa907b72a736678e22188", size = 12708115, upload-time = "2025-10-06T17:44:06.574Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/5853c04bebaed2d3c0ada9245328ffe3fff8b0f0f1c64f4776f67b42033f/tensorstore-0.1.78-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ce375a8f6621cdb94638b9cdc5266519db16a58353d4c6920e8b9d6bdd419e21", size = 15727539, upload-time = "2025-10-06T17:44:08.631Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e2/f67fcca8f90258c1cf1326aa366fe10f559f4c60102f53fdcc6614159c45/tensorstore-0.1.78-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82f68fa5a3b4c84365a667ea0a7465a53d5d969c4d3909ac990f314d1569ffc3", size = 13780753, upload-time = "2025-10-06T17:44:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/95013db6ef3b6a14b4237b95184c21becdf56d16605bf42903bb141f729e/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dc0bd6361d73e3f67d70980f96f4e8bcbd8e810b5475a01333ca9c37f0785a5", size = 18157446, upload-time = "2025-10-06T17:44:12.831Z" }, + { url = "https://files.pythonhosted.org/packages/e2/75/6e7cef68cab3a672c6668cc80c399ae6626a498a3ef04b35b3704b41e9cc/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75a17cef99f05fad9cc6fda37f1a1868d5f1502fd577af13174382931481c948", size = 20060211, upload-time = "2025-10-06T17:44:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/4ff3e395c44348c7442523c8ddd8ccc72d9ac81838e7a8f6afdd92131c3e/tensorstore-0.1.78-cp312-cp312-win_amd64.whl", hash = "sha256:56271d4652a7cb445879089f620af47801c091765d35a005505d6bfb8d00c535", size = 12711274, upload-time = "2025-10-06T17:44:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/18/36/cfb5a2acf9005896c88f80b93c2aee42f00fab9d0045369fef6e1b297242/tensorstore-0.1.78-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:8a1d0ae7996c80f2e623be5b8cfbc32a307d08dfef3d2dcb455f592908ecd46d", size = 15727334, upload-time = "2025-10-06T17:44:19.93Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/d1bcc3aab5be4298616dbc060b5aa2012b686270aaa16a9579c7945d0a1c/tensorstore-0.1.78-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:311846cfb2d644cd4a7861005e521a79816093e76d7924c83de5d06ca323067e", size = 13780722, upload-time = "2025-10-06T17:44:21.822Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/b0bb4440a9d67859b1abb367e436c62b0a27991dd7109f20be9dabff488f/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630538a66eb9964bd2975c4e09ae83be9984f2e4ebd5f7969983137bfda92071", size = 18157269, upload-time = "2025-10-06T17:44:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/d6/d95cde18ca2475bf317051b2be168cc963c5cfcd67e9c59786326ccdca53/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6886bec93b8ba22f83c4dc9e7c1ee20b11025ea9a5a839de21d0cbf7fd7aada2", size = 20060053, upload-time = "2025-10-06T17:44:25.942Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/dbd1af0e97d5d549051309d72c6e3f2fe81fae636f9db3692d21adc9c731/tensorstore-0.1.78-cp313-cp313-win_amd64.whl", hash = "sha256:e0073de8fa3074bc4cc92ced0210310fd89851899faf42a5ba256f0ba87d095c", size = 12711250, upload-time = "2025-10-06T17:44:27.926Z" }, +] + +[[package]] +name = "tensorstore" +version = "0.1.81" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/f6/e2403fc05b97ba74ad408a98a42c288e6e1b8eacc23780c153b0e5166179/tensorstore-0.1.81.tar.gz", hash = "sha256:687546192ea6f6c8ae28d18f13103336f68017d928b9f5a00325e9b0548d9c25", size = 7120819, upload-time = "2026-02-06T18:56:12.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/df/f472bd0dee801d7e33c53335ad0fcde9c71e5f9324241faa0a6b4be4270a/tensorstore-0.1.81-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:f64fb510f293079f9e5c63cb227e8a76904655a32912fc107c1e63bd8dc3e187", size = 16501390, upload-time = "2026-02-06T18:55:13.678Z" }, + { url = "https://files.pythonhosted.org/packages/5a/93/5f40c51d7b15d3574b1788a251dd4e3abd0415dab71811e126d2da5e826b/tensorstore-0.1.81-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4282587598885ff447f08369ac9bb681a65e224888cfa8ef8f3dd63544759e6c", size = 14535592, upload-time = "2026-02-06T18:55:16.44Z" }, + { url = "https://files.pythonhosted.org/packages/76/48/b7adcc8eca502ce8050c18cea066ca0c0122df7a686e10da6470e55456b4/tensorstore-0.1.81-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b4ea06038f6912bb6ed8a89db0c31e4e3d1b2404f3365dc756e4bc42bd6a89c", size = 19038732, upload-time = "2026-02-06T18:55:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/40/b0/99294895b030bd7d9ebc06e7ed523d0c09ab65667e031f8a67923f398f86/tensorstore-0.1.81-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51d59f7db9cdae02fce9d347300c0ccfb8265052945757e95592a265eb620b15", size = 21038447, upload-time = "2026-02-06T18:55:21.085Z" }, + { url = "https://files.pythonhosted.org/packages/32/e6/1ce977baf09aa3889f10f04460b588a6c8876ea441e51090c671f0400a6f/tensorstore-0.1.81-cp311-cp311-win_amd64.whl", hash = "sha256:fdb9579a729cccc02127cab5abf26f57a0e27968ba65c9c548ad058f5a45417f", size = 13221673, upload-time = "2026-02-06T18:55:23.195Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/00037db699f74d792efe2696305ddd6932e04306899e3701824a7f7de961/tensorstore-0.1.81-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7aefa1e3eadca804bce05215184c9cde29205ac2f3b443ca15a4e1846d31af4e", size = 16521245, upload-time = "2026-02-06T18:55:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/1deca1b955cb959eec13fd342ffaa2fd84e4770b4e2bcb95a2f541875a52/tensorstore-0.1.81-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e001d3edc6758eb5dc80556da9e945c1381f0529102fcc0301358ba6b9b70ed", size = 14543561, upload-time = "2026-02-06T18:55:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/b4343eae773f72a8777f82c5328191a06d8a5195e62105c14b7dcc49823f/tensorstore-0.1.81-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c27e07f4e91e6dc6a0878e13e2c5931d1716196b67b0df927f2f571de2576e9", size = 19043982, upload-time = "2026-02-06T18:55:30.076Z" }, + { url = "https://files.pythonhosted.org/packages/31/6c/d8c8508a9f4a83dc910d2365c484ba0debf5e531782065e3657fc8fc9b54/tensorstore-0.1.81-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcb4786c4955e2d88d518b5b5a367427e3ad21d059cba366ad7aebf5fcc2302e", size = 21049171, upload-time = "2026-02-06T18:55:34.383Z" }, + { url = "https://files.pythonhosted.org/packages/44/a9/c1a751e35a0fcff7f795398c4f98b6c8ea0f00fe7d7704f66a1e08d4352f/tensorstore-0.1.81-cp312-cp312-win_amd64.whl", hash = "sha256:b96cbf1ee74d9038762b2d81305ee1589ec89913a440df6cbd514bc5879655d2", size = 13226573, upload-time = "2026-02-06T18:55:36.463Z" }, + { url = "https://files.pythonhosted.org/packages/06/c0/32f7d52bfcf1728f557cccb17ac85f57bcc3fa92f4034368d6e7d7d06406/tensorstore-0.1.81-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:7bb563ad4d4d6c4748d9fe4f01f639ddf4ffef83ac180fc3b6d73f46ad854e62", size = 16521316, upload-time = "2026-02-06T18:55:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/38/b9/06ffc44e38ca18aeb3973f6b709d4d2102e17a8d700c7c3e2af3f2830722/tensorstore-0.1.81-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ff7e6c457596cf21f31c690e451fe634ac804fc98ff8131188e99d5ef7d29bc", size = 14543212, upload-time = "2026-02-06T18:55:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/00/01/3c27962f7258ad0bb552c3cd324fa2e01f746c8b6e81bd25d468f72204e8/tensorstore-0.1.81-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b218a6fe09c72c002f2c6480fc58b78cdbba8bb9c6f3a0d7dd1f70625cb37995", size = 19044489, upload-time = "2026-02-06T18:55:44.957Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/fe0f14a1da96d6e0aa6c24d6c31f3ce4b203f8e8a1a2e359489e52b33400/tensorstore-0.1.81-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33e7c11035c14dad01aeba012051643110cbb95c239e512106fe1be692c98b6", size = 21052658, upload-time = "2026-02-06T18:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e2/cc189d799982f02c200b22405c4d3f28845df6321de2ac3a35ae087758ed/tensorstore-0.1.81-cp313-cp313-win_amd64.whl", hash = "sha256:b55126bcf084cc5fe0151bf465f3a5dedb5b5da0133d01227f75d0e71f9cfae5", size = 13226848, upload-time = "2026-02-06T18:55:49.631Z" }, + { url = "https://files.pythonhosted.org/packages/89/b0/0ca436391f832fad365977623f3c08c4fbbf553fd9a112604aa106646654/tensorstore-0.1.81-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a48c23e4df50681d8f4f365b08a0beb114ab210accbde9f34d37fd7b45c31005", size = 16525537, upload-time = "2026-02-06T18:55:51.708Z" }, + { url = "https://files.pythonhosted.org/packages/8a/02/c10052b86cf8d47b4cf41e5f139b4003c69bb69e506759b0eb87b873d213/tensorstore-0.1.81-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0be0ce646263820f3d4c9ba738d8e9be7da241cbe093ca2fd02e25023344347c", size = 14547490, upload-time = "2026-02-06T18:55:53.899Z" }, + { url = "https://files.pythonhosted.org/packages/01/d1/bd86c46367624522967e896ca45d77ba9085de3f15081fdad6576ba70aa9/tensorstore-0.1.81-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93996e756dce82589f5a19e27b4e7c0b5b40221a7e41ddce46dc13d378dbd157", size = 19050938, upload-time = "2026-02-06T18:55:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/11/a2/59a8e9a33cd9e17461f918bda4a20712ed3c51c52e0e42b2f673441bc90d/tensorstore-0.1.81-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:444c088919a739c20ca1f87935d72de4fd87605eb2c0f093b8d49251b7884aef", size = 21055275, upload-time = "2026-02-06T18:55:58.259Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ec/2988f210729b523975b1bee030cabd64b256943c08463331598f1e03bd4f/tensorstore-0.1.81-cp314-cp314-win_amd64.whl", hash = "sha256:f7aa0a3a470c4d832faff7d77dd688b1d352b718d110c95ceba54ec637ca3ffa", size = 13614713, upload-time = "2026-02-06T18:56:00.291Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5d/60e990df3f1dc57c33644375a0eccb906a79fd8a5e2d81238f856c65ad7f/tensorstore-0.1.81-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6c36d8a827120aa15e50ec5c36dd7e73978d86ba4f46d073fb648d8dda3948e9", size = 16605091, upload-time = "2026-02-06T18:56:02.807Z" }, + { url = "https://files.pythonhosted.org/packages/85/22/f599576815227735d3e34f86f05a8b39d8b15fd979d0029383ebae23978d/tensorstore-0.1.81-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c31d831707c4ff3c6ecdcba129f7c39e982572837b2f93e02ccb83fc8581bca", size = 14631573, upload-time = "2026-02-06T18:56:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/b5d0b424b7af057a3d4de3f312eba9ddf8a3c750a766b42e0b7f6c2ebef0/tensorstore-0.1.81-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fba383f108d7450bf9a03487ac7fa3bb2c3080c91cee9d2da3bb217b560846b", size = 19065251, upload-time = "2026-02-06T18:56:06.972Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/0f113eae73b1e8eb2f712cf5f1efd269452f0f0045158fae43ce7b4701b4/tensorstore-0.1.81-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88c52f592e2982682045199cabf360462146749d48b7be2969cd640e877c6c3", size = 21066488, upload-time = "2026-02-06T18:56:10.236Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu128-train') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130') or (os_name == 'nt' and sys_platform == 'darwin' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (os_name != 'nt' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "thop" +version = "0.1.1.post2209072238" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/0f/72beeab4ff5221dc47127c80f8834b4bcd0cb36f6ba91c0b1d04a1233403/thop-0.1.1.post2209072238-py3-none-any.whl", hash = "sha256:01473c225231927d2ad718351f78ebf7cffe6af3bed464c4f1ba1ef0f7cdda27", size = 15443, upload-time = "2022-09-07T14:38:37.211Z" }, +] + +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "timm" +version = "1.0.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/2c/593109822fe735e637382aca6640c1102c19797f7791f1fd1dab2d6c3cb1/timm-1.0.25.tar.gz", hash = "sha256:47f59fc2754725735cc81bb83bcbfce5bec4ebd5d4bb9e69da57daa92fcfa768", size = 2414743, upload-time = "2026-02-23T16:49:00.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/50/de09f69a74278a16f08f1d562047a2d6713783765ee3c6971881a2b21a3f/timm-1.0.25-py3-none-any.whl", hash = "sha256:bef7f61dd717cb2dbbb7e326f143e13d660a47ecbd84116e6fe33732bed5c484", size = 2565837, upload-time = "2026-02-23T16:48:58.324Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "fsspec", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jinja2", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "sympy", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:4db72a4d257c45c3502f11764ee41460a87312fdc3dff47a8957812efe961725", upload-time = "2026-02-06T16:27:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0826ac8e409551e12b2360ac18b4161a838cbd111933e694752f351191331d09", upload-time = "2026-02-06T16:27:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7fbbf409143a4fe0812a40c0b46a436030a7e1d14fe8c5234dfbe44df47f617e", upload-time = "2026-02-06T16:27:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:b39cafff7229699f9d6e172cac74d85fd71b568268e439e08d9c540e54732a3e", upload-time = "2026-02-06T16:27:17Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:7417ef370d7c3969dd509dae8d5c7daeb945af335ab76dd38358ba30a91251c1", upload-time = "2026-02-10T19:55:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:90821a3194b8806d9fa9fdaa9308c1bc73df0c26808274b14129a97c99f35794", upload-time = "2026-02-10T19:55:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:358bd7125cbec6e692d60618a5eec7f55a51b29e3652a849fd42af021d818023", upload-time = "2026-02-10T19:55:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:470de4176007c2700735e003a830828a88d27129032a3add07291da07e2a94e8", upload-time = "2026-02-10T19:55:43Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2d16abfce6c92584ceeb00c3b2665d5798424dd9ed235ea69b72e045cd53ae97", upload-time = "2026-01-23T15:09:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd", upload-time = "2026-01-23T15:09:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407", upload-time = "2026-01-23T15:09:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:339e05502b6c839db40e88720cb700f5a3b50cda332284873e851772d41b2c1e", upload-time = "2026-01-23T15:09:57Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:840351da59cedb7bcbc51981880050813c19ef6b898a7fecf73a3afc71aff3fe", upload-time = "2026-01-23T15:09:59Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c88b1129fd4e14f0f882963c6728315caae35d2f47374d17edeed1edc7697497", upload-time = "2026-01-23T15:09:59Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f4bea7dc451267c028593751612ad559299589304e68df54ae7672427893ff2c", upload-time = "2026-01-23T15:10:01Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0+cu128" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "cuda-bindings", version = "12.9.4", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "filelock", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "fsspec", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jinja2", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'group-7-cosmos3-cu128') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cublas-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cudnn-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cufft-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cufile-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-curand-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusolver-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparse-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparselt-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nccl-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvshmem-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvtx-cu12", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and extra == 'group-7-cosmos3-cu128') or (python_full_version >= '3.12' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "sympy", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "triton", marker = "(sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128') or (sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e186f57ef1de1aa877943259819468fc6f27efb583b4a91f9215ada7b7f4e6cc", upload-time = "2026-01-21T15:21:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:36368507b56eaa51acbd3c96ac8893bb9a86991ffcd0699fea3a1a74a2b8bdcb", upload-time = "2026-01-21T15:21:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:14d2831b9292c3a9b0d80116451315a08ffe8db745d403d06000bc47165b1f9e", upload-time = "2026-01-21T15:21:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:85ed7944655ea6fd69377692e9cbfd7bba28d99696ceae79985e7caa99cf0a95", upload-time = "2026-01-21T15:21:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1d01ffaebf64715c0f507a39463149cb19e596ff702bd4bcf862601f2881dabc", upload-time = "2026-01-21T15:21:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:3523fda6e2cfab2b04ae09b1424681358e508bb3faa11ceb67004113d5e7acad", upload-time = "2026-01-21T15:22:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca", upload-time = "2026-01-21T15:22:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5", upload-time = "2026-01-21T15:22:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:fbde8f6a9ec8c76979a0d14df21c10b9e5cab6f0d106a73ca73e2179bc597cae", upload-time = "2026-01-21T15:22:17Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bdbcc703382f948e951c063448c9406bf38ce66c41dd698d9e2733fcf96c037a", upload-time = "2026-01-21T15:22:29Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7b4bd23ed63de97456fcc81c26fea9f02ee02ce1112111c4dac0d8cfe574b23e", upload-time = "2026-01-21T15:22:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:4d1b0b49c54223c7c04050b49eac141d77b6edbc34aea1dfc74a6fdb661baa8c", upload-time = "2026-01-21T15:22:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f1f8b840c64b645a4bc61a393db48effb9c92b2dc26c8373873911f0750d1ea7", upload-time = "2026-01-21T15:23:28Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:23f58258012bcf1c349cb22af387e33aadca7f83ea617b080e774eb41e4fe8ff", upload-time = "2026-01-21T15:23:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:01b216e097b17a5277cfb47c383cdcacf06abeadcb0daca0c76b59e72854c3b6", upload-time = "2026-01-21T15:23:53Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:c42377bc2607e3e1c60da71b792fb507c3938c87fd6edab8b21c59c91473c36d", upload-time = "2026-01-21T15:23:56Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:37d71feea068776855686a1512058df3f19f6f040a151f055aa746601678744f", upload-time = "2026-01-21T15:24:08Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:c57017ca29e62271e362fdeee7d20070e254755a5148b30b553d8a10fc83c7ef", upload-time = "2026-01-21T15:24:10Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:777461f50b2daf77e4bdd8e2ad34bdfc5a993bf1bdf2ab9ef39f5edfe4e9c12b", upload-time = "2026-01-21T15:24:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7bcba6a7c5f0987a13298b1ca843155dcceceac758fa3c7ccd5c7af4059a1080", upload-time = "2026-01-21T15:24:44Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:70d89143c956389d4806cb4e5fe0b1129fe0db280e1073288d17fa76c101cba4", upload-time = "2026-01-21T15:24:46Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0+cu130" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "cuda-bindings", version = "13.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "filelock", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "fsspec", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "jinja2", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu130') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.11' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cublas", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-cupti", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-nvrtc", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cuda-runtime", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cudnn-cu13", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cufft", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cufile", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-curand", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusolver", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparse", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-cusparselt-cu13", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nccl-cu13", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvjitlink", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvshmem-cu13", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "nvidia-nvtx", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (python_full_version >= '3.12' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.12' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version < '3.12' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "sympy", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "triton", marker = "(sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:adfb2bca94f12a5baca6ccf9aa40d6c5b1259748ebb38938be670b07a24d4e15", upload-time = "2026-01-21T19:02:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0f8566d1b3d8353b7f18d5a15375a00dae89d886bb7d01143bb394f475832286", upload-time = "2026-01-21T19:02:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp310-cp310-win_amd64.whl", hash = "sha256:0de43ec229a78a2e80ae8578252f9eb14f898457d1cf2be81c8c91a4108c6c79", upload-time = "2026-01-21T19:00:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ea3239d544b2e569a8f47db5c7fa4fd42a2fe96aefb84bb1eda45ce213020fd2", upload-time = "2026-01-21T19:01:56Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:22cfa45e73f1e8c64f4012737987a727d01d152121b93d196b0ca22f39a3f8e3", upload-time = "2026-01-21T19:02:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-win_amd64.whl", hash = "sha256:218ae0f323d5ebe8f2770e46cbfb7bbff9af2c8d192d5187878d0964d43c8b71", upload-time = "2026-01-21T19:00:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4fc8f67637f4c92b989a07d80ffe755e79a3510ca02ebf23ce66396fb277c88d", upload-time = "2026-01-21T19:01:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:858f0cbcc78d726fea9499eb3464faa98392fa093845a3262209bd226b7844d6", upload-time = "2026-01-21T19:02:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-win_amd64.whl", hash = "sha256:224649fa0ab181ec483cc368e3303dda1760e4ba31bea806b88979f855436aaa", upload-time = "2026-01-21T19:00:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:75780283308df9fede371eeda01e9607c8862a1803a2f2f31a08a2c0deaed342", upload-time = "2026-01-21T19:01:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7e0d9922e9e91f780b2761a0c5ebac3c15c9740bab042e1b59149afa6d6474eb", upload-time = "2026-01-21T19:02:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-win_amd64.whl", hash = "sha256:48af94af745a9dd9b42be81ea15b56aba981666bcfe10394dceca6d9476a50fa", upload-time = "2026-01-21T19:00:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:46699da91f0367d8dfa1b606cb0352aaf190b5853f463010e75ff08f15a94e7d", upload-time = "2026-01-21T19:02:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:775d1fff07e302fb669d555a5005f781aa460aa80dff7a512e8e6e723f9def83", upload-time = "2026-01-21T19:02:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-win_amd64.whl", hash = "sha256:b38e5b505b015903a51c2b3f12e50a9f152f92fe7e3992e79f504138cf90601d", upload-time = "2026-01-21T19:01:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:18f87ae628c02f095f2e97756e4fa249ceef6ed6e87d5a3c79b5338abf842511", upload-time = "2026-01-21T19:01:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:db5a61791b7da3c1aa5a496e64cd72dbd4ef3ef2cbb69680fd45dc255b0da2f3", upload-time = "2026-01-21T19:02:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314-win_amd64.whl", hash = "sha256:dda35d473dd34cafa0668be176b9ad2cb69b1ff570d0336715a6541e89e27640", upload-time = "2026-01-21T19:00:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7781235583ea06b214075c10fa95f83b9805f06af44efc6e9946808413cff94f", upload-time = "2026-01-21T19:01:59Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:526b737db11d632281795484ec729baae5f193a5a0d76a1f7d822f7897c8b4f5", upload-time = "2026-01-21T19:02:44Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp314-cp314t-win_amd64.whl", hash = "sha256:d5ea18790a18b660d655f6e75a8ca6e8d6298b55fc338f8c921764b94c886743", upload-time = "2026-01-21T19:01:35Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/49/67a66932ab2fcdda3c5a4dcf606e713d86883a4a9a99a3bb832815b52b8e/torch_c_dlpack_ext-0.1.5-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e0f6c197d5293884898b9ebf13d07501de39cb94799b374ed43f91731087d557", size = 7056755, upload-time = "2026-01-12T11:24:31.817Z" }, + { url = "https://files.pythonhosted.org/packages/ae/28/d2d6bf90e01a1f4da3277c9a56d9ecac648b6d6adaa8e20c17f802deb7fb/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba3d88f0f7d5e1d9c3d4a3179037fc8e261c3b77ac1fad23edc0d3a9214ef193", size = 432066, upload-time = "2026-01-12T11:24:33.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/a1f9584a3af4ac6ae5ad5cf86927d8c3a9b6bb50d54e54d19313411216a0/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7468df84ec152d930fbc3acf460c44a60b3462b95af3d3a676d133629c7e176", size = 879488, upload-time = "2026-01-12T11:24:34.837Z" }, + { url = "https://files.pythonhosted.org/packages/6c/08/478cfcb5814e29f9b720111bdef315fc2fbc8b276e4b1183c8b9c9414a4f/torch_c_dlpack_ext-0.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:78dd4904bd26170a2dd7c0eab56367756ee0a15672ce9b84146169e68f0c6ddc", size = 1461437, upload-time = "2026-01-12T11:24:36.385Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/c12a9bb3a5ddc0962c00467891bf1ffdda39a4d4780bf0fbbf54523ff34e/torch_c_dlpack_ext-0.1.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:56bd25a2af19280bf8a06aa62cff5510106f43235b9327d8561b3e9a659c4d84", size = 5076782, upload-time = "2026-01-12T11:24:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/64e1e579d107064785549e70758e38a42376ab7e73d86897ed4beab10e74/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fba674110e1fab0b176bb5a28223e157db65c90767d4ba74abdbee9f537b0e9d", size = 440949, upload-time = "2026-01-12T11:24:39.716Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/3e1382a620824f92920ab3fae132d8fb4e85898284c99e0c6a7764e452ce/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3448c4f0d64104d0b2e58080a7efa72304a04960c18f338024b80b13cd3eca26", size = 897768, upload-time = "2026-01-12T11:24:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/76ea1006b9038b496d01e916c91efd17cb782abde2491a261cf203f57e30/torch_c_dlpack_ext-0.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:74676474e0afa9a4216c4755ea7cf05e8158be1d168f6bda669ba91097c263f2", size = 1479088, upload-time = "2026-01-12T11:24:42.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/67/10d236698525d7b7db4d74ec0a4b01f5b2db33968995fdd9ac6b4635e327/torch_c_dlpack_ext-0.1.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c0f2bd51fcd99c0e5b50314e1985f2728c4941bfa821f065e6c30951d1f995ca", size = 5291237, upload-time = "2026-01-12T11:24:44.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e6/7d7a97a3953208d6d6ce749180c34d1dab48464ded9a76cecabe9d021ce6/torch_c_dlpack_ext-0.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:670fbbab70123cc228bed41693a3720757af57a0ad22669063c9db25321e8f55", size = 1482855, upload-time = "2026-01-12T11:24:49.581Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/65346a201d921b616731311fc9941f15137672b444cebdad702cb52ccee0/torch_c_dlpack_ext-0.1.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:74acea2ed395cadda63342845b9e9ee7cd4537846223dacfb4431b4610109265", size = 1993243, upload-time = "2026-01-12T11:24:51.079Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/faf10be09a5812b1c5ec9922b53fb5def5fc4080b81a653b9347bb169ebb/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f1e99d13c64e22dac0a34a1560e9e5a398a49a9fa81df83053e04fde6ec5bd", size = 443798, upload-time = "2026-01-12T11:24:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/2d/68/f434b48700f3e04f33882f54d8d3910327b935f55e14ec49da7d607bf470/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:debe62e5ef93e631065d6b9f6e60d3d39bae6b89fa1b25d9523f40b3efbf8aba", size = 755004, upload-time = "2026-01-12T11:24:54.004Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/cc64e563f05ea99bd79bdb43f71f0f46452d3acd734da4843ede5fc73a35/torch_c_dlpack_ext-0.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:30e3eab616dbc81dfdb7492aca557be551a9163ba9b585f97394a42b336b113a", size = 999126, upload-time = "2026-01-12T11:24:55.44Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/449324ca8e81573e650b6851fc31c1038f750d1de85d0b185d788e1c7a3a/torch_c_dlpack_ext-0.1.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:cac94a4905d391889e679a8da31e46dc325af5d55d13b7c70c0ce3d71d1ced6d", size = 1982154, upload-time = "2026-01-12T11:24:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/20/62/11c05b99f69aa5152bca0313e0dfa6d125a020cf890dc888ef009aa7891c/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a58fdf45fb0bda7bc459632cec891570f31c11636d5851c825cf308ec8b73c2", size = 163825, upload-time = "2026-01-12T11:24:59.474Z" }, + { url = "https://files.pythonhosted.org/packages/15/b5/be613cd8e71c9982bd07af530f86c5a7f30df7831d14cec5414857af7149/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b985a324c68241cf83a9474b28015524b66775b12a91930dd4c0760aa628d01", size = 171740, upload-time = "2026-01-12T11:25:00.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/11/52e291f1659e2ec70a09f5ca4ad27e015eb4f0a1371ae68d23a9fbd1c704/torch_c_dlpack_ext-0.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:d794e19fa3f330ab7a29987c07e031fc08e4953aec516d35701d0827863e356b", size = 277086, upload-time = "2026-01-12T11:25:01.901Z" }, +] + +[[package]] +name = "torch-fidelity" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/37/94e4d31a157bae81622557b7f94f93ca3749e6b592203633858dbca8c835/torch_fidelity-0.4.0.tar.gz", hash = "sha256:73548b97f1a844cb94dee7736c8485cbf1e5ae43670679f62ffb7bbb8bb3d455", size = 1349034, upload-time = "2026-02-17T21:04:57.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/35/e0988c963538d155befe1c4e1b90ff2925eaa40acc27fb2bba737dcb7288/torch_fidelity-0.4.0-py3-none-any.whl", hash = "sha256:99f4c610ea16972628eceb26afb0008e528da2d44123db947b408750efbd35d4", size = 85594, upload-time = "2026-02-17T21:04:56.166Z" }, +] + +[[package]] +name = "torch-optimizer" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytorch-ranger" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/13/c4c0a206131e978d8ceaa095ad1e3153d7daf48efad207b6057efe3491a2/torch-optimizer-0.3.0.tar.gz", hash = "sha256:b2180629df9d6cd7a2aeabe71fa4a872bba938e8e275965092568cd9931b924c", size = 54409, upload-time = "2021-10-31T03:00:22.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/54/bbb1b4c15afc2dac525c8359c340ade685542113394fd4c6564ee3c71da3/torch_optimizer-0.3.0-py3-none-any.whl", hash = "sha256:7de8e57315e43561cdd0370a1b67303cc8ef1b053f9b5573de629a62390f2af9", size = 61897, upload-time = "2021-10-31T03:00:19.812Z" }, +] + +[[package]] +name = "torchao" +version = "0.16.0+cu128" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://download.pytorch.org/whl/cu128/torchao-0.16.0%2Bcu128-cp310-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda63ff11519c563d2f664b11e52cd7dcd0d7ff34c88cdfdef330e409d3f253f", upload-time = "2026-02-10T18:44:08Z" }, +] + +[[package]] +name = "torchao" +version = "0.16.0+cu130" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://download.pytorch.org/whl/cu130/torchao-0.16.0%2Bcu130-cp310-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d817e7272558bb630a2789e587bb84270d12f67e44c5a2c23f78dea0637948c", upload-time = "2026-02-10T18:44:11Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/59/88ab8ebff9d91f1f1365088b30f1b9ccce07c5eeac666038a5dee5e2f9b1/torchaudio-2.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cde383582a6240c1315443df5c5638863e96b03acf1cb44a298aff07a72d373", size = 734944, upload-time = "2026-01-21T16:28:49.535Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d6/41f25f9ae9b37c191bed4cd474e403626685d2be8f7d20d011e6601fede1/torchaudio-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cfb2ad4b7847d81931989127d803487263c8284f21156e9000daec1ac16c0831", size = 390449, upload-time = "2026-01-21T16:28:48.585Z" }, + { url = "https://files.pythonhosted.org/packages/43/ac/a14425fddd1cf56bb052a3bfd38880258008f8c3cd17f37bba55b3a88ce7/torchaudio-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:316cdb15fb37290fca89894b095d97b4dc14a90c4c61148ae5c96bb334d962cd", size = 1891070, upload-time = "2026-01-21T16:28:47.323Z" }, + { url = "https://files.pythonhosted.org/packages/6e/03/d1898db1bf7ecd47ca9b4e1b70927597d236cf721e3736d953d555901832/torchaudio-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:013079d1ba2a652184703e671b8339cbc7991f17e4ed927071fe7635f908a4a1", size = 474045, upload-time = "2026-01-21T16:28:46.191Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e7/401fe1d024bf9352371d854be6f339ad9928669e6bc8a5ba08e9dbce81cf/torchaudio-2.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcab0e39eb18da84cba1a0c87f600abb6ce97c882200cb46e841caea106f037f", size = 736373, upload-time = "2026-01-21T16:28:41.589Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b7/c66dc34a27441d78997e20d0ffe2f5ad73db9f7b1267511be255bb94ac9b/torchaudio-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:87c841a21e82703ebd4a29170c4e60c25a2b47312dc212930087ad58965ac0c8", size = 391843, upload-time = "2026-01-21T16:28:43.093Z" }, + { url = "https://files.pythonhosted.org/packages/13/ae/a2a34a64947c4fa4a61b4c86d8f36fbcb4ebfec30fdde140267db260f96c/torchaudio-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b2c77fb9114dd463dc805560bf55a1ac2a52e219794cc32b7b32cf2aeffd2826", size = 1894140, upload-time = "2026-01-21T16:28:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/69/26/cd2aec609b4f8918e4e85e5c6a3f569bc7b5f72a7ecba3f784077102749c/torchaudio-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:4c6e9609046143b30a30183893d23ff1ce5de603dbe914b3cce5cc29f5aa5a9c", size = 474792, upload-time = "2026-01-21T16:28:45.254Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/28a6f3e857616cf7576bdbf8170e483b8c5d0a1f8d349ecb2b75921236aa/torchaudio-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d0fbdbfd2f621c51d28571050d6d0c7287791034e5c7303b31480af1258f33f", size = 737144, upload-time = "2026-01-21T16:28:44.189Z" }, + { url = "https://files.pythonhosted.org/packages/ea/3f/df620439a76ece170472d41438d11a1545d5db5dc9f1eaeab8c6e055a328/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:42b148a0921a3721abd1f6ae098b1ec9f89703e555c4f7a0d44da87b8decbcb9", size = 391973, upload-time = "2026-01-21T16:28:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/98/25/e55a30d7138f8fe56ed006df25b0a3c27681f0ec7bc9989e1778e6d559c3/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0e77b2956448d63790a99beed0b74ac8b8cd3a94dcdd9ad01974411078f46278", size = 1895234, upload-time = "2026-01-21T16:28:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/da53c7d20fac15f66f8838653b91162de1bf21fb40fee88cf839e4ef5174/torchaudio-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f76a01ecebf1869e1f2c50a261f1cf07e5fccb24402b4e9bbb82d6725b9c7dd", size = 475470, upload-time = "2026-01-21T16:28:40.615Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/341e7bd588355f82c5180103cb2f8070a72ab1be920ab27553a1135d4aa6/torchaudio-2.10.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8fd38d28ee150c584d3ee3b05f39e021f0ad8a8ec8fec1f26dfe150c9db9b2f5", size = 737164, upload-time = "2026-01-21T16:28:38.354Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/831c2595c81b17141180ca11ab3c0836cc544ef13e15aa0e7b2cb619e582/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5bc39ff3ea341097ce1ab023dd88c9dd8ca5f96ebf48821e7d23766137bb55d7", size = 392757, upload-time = "2026-01-21T16:28:33.631Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d8/405c80c57dc68ca5855bddfaae57c3d84ea7397bf1eb2aa5d59c9fa1d3a9/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3057c4286db5673d266124a2a10ca54e19f516772e9057f44573a7da5b85e328", size = 1897099, upload-time = "2026-01-21T16:28:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/73/cf/0e48d67788c935e3b3d00e6f55a930a54a67f432e04c33ef80a38cb764fd/torchaudio-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:99e74d1901742bc10961d807fe75c0dd9496f4a4a4ff4bb317c5de4a0b6f24e6", size = 475476, upload-time = "2026-01-21T16:28:28.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/30bcce0f17a8279b051b09250993691a828f89a03278306b23571c18df04/torchaudio-2.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6cfe98ef0ea9bee6d6297493ce67ce0c54a38d80caf6535a3ae48900fd5f3769", size = 742449, upload-time = "2026-01-21T16:28:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/8c/653e7f67855424bf3b7cbb48335f8316f7fb02bb01a6cab38f6bf9555676/torchaudio-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b41b254d958632dc00dc7768431cadda516c91641d798775cbb19bcd4f0d2be4", size = 393430, upload-time = "2026-01-21T16:28:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1f/f91fcb9dd47a19b720fb48042a2f6f023651948e73726e98fff60d5ed5c7/torchaudio-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:da1081d1018a1e95f5a13947402aeb037cf5ac8861219a6164df004898a96bb1", size = 1897271, upload-time = "2026-01-21T16:28:23.519Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/270c26890f43838e8faa5d3e52f079bd9d9d09f9a535a11cf6b94e20ed21/torchaudio-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f1afa53146a5655258d3a86e689c6879dfe78581d9bee9ef611ace98722f86bb", size = 478966, upload-time = "2026-01-21T16:28:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/0e54b162bd0d1ec2f87b545553af839f906b940888d0122cdef04b965385/torchaudio-2.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1f2897fbf776d55afcb5f6d9b7bdfaea850ca7a129c8f5e4b3a4b025c431130d", size = 739544, upload-time = "2026-01-21T16:28:26.947Z" }, + { url = "https://files.pythonhosted.org/packages/57/a1/ef5571406858f4ea89c18d6ad844d21cb9858708149e6bbd9a789ee30ea5/torchaudio-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b2d5e11a2bec08f02a4f5fb7d1902ff82d48c533a27ceedc21e6ade650cf65b3", size = 393061, upload-time = "2026-01-21T16:28:25.802Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0f/a0cf0ebc6f71b1868ea056dd4cd4f1a2244b8da8bc38372a1adc984a7c1f/torchaudio-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:77f6cf11a3b61af1b0967cd642368ecd30a86d70f622b22410ae6cb42d980b72", size = 1897137, upload-time = "2026-01-21T16:28:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/48/98e6710a4601e190bc923c3683629c29d41fb18a818a9328515541f023ed/torchaudio-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:4711c2a86a005685ca3b5da135b2f370d81ac354e3dcb142ef45fe2c78b9c9c4", size = 475154, upload-time = "2026-01-21T16:28:22.438Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9b/cd02f8add38bd98761548b0821a5e54c564117a9bbeafaf95f665ab0fd72/torchaudio-2.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13bdc1bde0c88e999699d1503304a56fc9dea6401b76bc08a5f268368129d46c", size = 742453, upload-time = "2026-01-21T16:28:20.989Z" }, + { url = "https://files.pythonhosted.org/packages/53/8a/946aa07393845b918d318b5e34b3bd0359fd27fc9fac10a85fae2bb86382/torchaudio-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ed912de8ec1b400e17a5172badcfcddc601a9cd4e02d200f3a9504fc8e54961c", size = 393434, upload-time = "2026-01-21T16:28:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/e1/68/e37e8fbbae986afa80f8851e08fc017eb8ae5f7b398ee28ed92303da163e/torchaudio-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:f7aa33a8198e87949896e16ea245ea731906445becdf10130e8823c68494a94a", size = 1897289, upload-time = "2026-01-21T16:28:17.059Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/0e1f464463b85bc677036faffdfd23493aa17e8c3fc3a649abca8c019701/torchaudio-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e49f6a18a8552620c4394f8529b7551eda9312d46dfdd3500bd2be459c86aea4", size = 478968, upload-time = "2026-01-21T16:28:19.542Z" }, +] + +[[package]] +name = "torchcodec" +version = "0.10.0+cu128" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:aaf6318fd4cb407c18aaa95a25c0b3816fdb40d7efab5613d75be86fede16924", upload-time = "2026-01-26T17:33:06Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6995040e2a8a01b4ef66a84bc78a0c3bfc4afef6ddca94798afdcc481bea6393", upload-time = "2026-01-26T17:33:06Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:935a8332ec4d2cda4765d9e7da9e16c49c3ed41a3787965167995873f891d8f7", upload-time = "2026-01-26T17:33:07Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1d696c23ead006b94d9cbc670c81906c6348154ce8ae334667dc94f3a3d1afc5", upload-time = "2026-01-26T17:33:07Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bef47589149b941b296b0448855ad89269784015f735afd446264dd7367c7883", upload-time = "2026-01-26T17:33:08Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5ecb4aeb61b4f14f30ceed11ce892308f38232d82eee64605ae19583c51a8e72", upload-time = "2026-01-26T17:33:08Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ec23567f5466cd8e69c1994380f697097ad430199063df7a6d6ecd8302b4faa9", upload-time = "2026-01-26T17:33:09Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a2c3c16c7be56989360cb2894d8443d6f067cc615ac1830f89cc5f37482861d5", upload-time = "2026-01-26T17:33:09Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cd9b2f9bab06c3d962b9f3400dfcaf6b5d4b9baab66dd6d7692ecd0e97c453ff", upload-time = "2026-01-26T17:33:10Z" }, + { url = "https://download.pytorch.org/whl/cu128/torchcodec-0.10.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c02e3addfb5dccde976596ab4fbbe33bfd41f0d4144a075510383e9d5915a366", upload-time = "2026-01-26T17:33:10Z" }, +] + +[[package]] +name = "torchcodec" +version = "0.10.0+cu130" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +wheels = [ + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:afa37028fb9a29bac6ea278c709e734715d7a5afc2adb6276a969ac404d16918", upload-time = "2026-04-27T20:18:44Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:dd91a7ca48cdede22dd56cbb4e2d1488bfe9fd56edfffbab1ee6fd19c1ce0ecb", upload-time = "2026-04-27T20:18:45Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6cebb48c5b151968317b804a9ed4e3df0957365c78b3d9223aa464d9dd01a3fb", upload-time = "2026-04-27T20:18:45Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cf5b465a7731202c5daab9d19c576eb17da0ef4eed2092eeb7d8e6900b4dd1f5", upload-time = "2026-04-27T20:18:46Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:46ac44201fbcd6006574d1efd7071986b062e546e0a866e0f8d8040c6583c1ae", upload-time = "2026-04-27T20:18:46Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:49512fa4b4ed8325821595a4298423faa809f1a7f695701bcc747001d6e84b40", upload-time = "2026-04-27T20:18:46Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:dd980acc3b97d0033f45979c8b8fe10622db834f8c2a383c10f99ac668d99840", upload-time = "2026-04-27T20:18:47Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:db2c833d3fe41f6c4b0ab793bfc5095b503302efb2f8aca97bf285b253c9910f", upload-time = "2026-04-27T20:18:47Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:5a55d014241a697a474d401122f38f40b5ef65d2df09ad0a6bfa47ed87925d19", upload-time = "2026-04-27T20:18:48Z" }, + { url = "https://download.pytorch.org/whl/cu130/torchcodec-0.10.0%2Bcu130-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:cc9c808f8ad2857529ec86f18c0b8c3c04c6f6e90da437bb77dca4578cd5aa0e", upload-time = "2026-04-27T20:18:48Z" }, +] + +[[package]] +name = "torchdata" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "urllib3" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/d4/af694ef718aedbe95a72760ab9ff7a6a7a44ace2d7f70c27bfeb67c5c503/torchdata-0.11.0-py3-none-any.whl", hash = "sha256:52b940fbbe0e00fb21cabddf528449d1bec5bfb0d0823b7487b15f951658ee33", size = 61968, upload-time = "2025-02-20T22:26:30.666Z" }, +] + +[[package]] +name = "torchtitan" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets" }, + { name = "einops" }, + { name = "fsspec" }, + { name = "pillow" }, + { name = "tensorboard" }, + { name = "tokenizers" }, + { name = "tomli" }, + { name = "torchdata" }, + { name = "tyro" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/fc/de6756235ae3b44ae2cf65ea817b94bf5bc0c104a0024e4d9a11f2d11a02/torchtitan-0.2.2.tar.gz", hash = "sha256:f96babf03ce954dbd9078dab04eed61874c9144e23152bebca7be508de9eefd4", size = 344817, upload-time = "2026-02-20T22:48:29.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/06/a8908cb4fd0185be48a640615ee2ec9055c35cd7aaafdb13859b7fb723a7/torchtitan-0.2.2-py3-none-any.whl", hash = "sha256:c95ccaaa89238085ed7d3d4befb20a298cc3fcc666b9573a1f5ba92ec8c3ca60", size = 450309, upload-time = "2026-02-20T22:48:27.834Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pillow", marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7810841916a2a870cbfb581f4084b59479908fb9db6edfdf78139931392d8677", upload-time = "2026-01-20T18:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:38e97afb81984d94a0436514ee83ffa05016f6b843f33cac8d3f09792d861c65", upload-time = "2026-04-27T19:00:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a76ce7b8d4fce291a25721ee2f921c783acc6dbd4fc32dc741ed2a1d5a8dde2f", upload-time = "2026-01-20T18:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3b8575ba0884be7718d70ac2ceebcf6c3706866e9412155fba25ee4d8a5795b6", upload-time = "2026-04-27T19:00:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:724f212a58a0d0d758649ce288601056b5f46a01de545702f42bccc5b25cb0cc", upload-time = "2026-01-20T18:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:51509291a329ff4032be8719fbb75ff625363fb7047eaacc061154caad5f0586", upload-time = "2026-04-27T19:00:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6c444119c6af8fa48b79c59f1529fc15a45a2bbf823cf85f851e7203d84f727f", upload-time = "2026-01-20T18:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a878d4b7a015edc34c3f5ef73c8e5700ef8d829da48d99e8fdb5b48401c6df51", upload-time = "2026-04-27T19:00:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:80895a40faa5783ce19ed5a692a54062c7888a385490e866324355f8d59b2eb3", upload-time = "2026-01-20T18:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:52981a8df802ba2d36dc83e533b50413ec5e3b351a7dcf5b75c30a975e5613f0", upload-time = "2026-04-27T19:00:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a64ae543fe0a057a93954af94c239629723db196ed65090e6775f136726b22f8", upload-time = "2026-01-20T18:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:26447bcf8e8accf66ab0ac711ac6649713ca882b42969a06d2623f063d20209c", upload-time = "2026-04-27T19:00:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b7df71942e31b74a5161dea3452da35e5fb1d36c752aedf61f2dd3c7be35739a", upload-time = "2026-01-20T18:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu129/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:32ab64be70fdd4495f4b339a1f2870c138b7e11977d2dab70a3bfe90a42bdc3a", upload-time = "2026-04-27T19:00:42Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0+cu128" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "numpy", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pillow", marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" }, marker = "extra == 'group-7-cosmos3-cu128' or extra == 'group-7-cosmos3-cu128-train' or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7fe593ec669dcb74c3749548c21ae302edbb610d1844bf2bae622accdef76dbc", upload-time = "2026-01-21T22:32:19Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:43350ca113e9f224dedb90b0dbc31853fde4f322794cbe4a7589fccab5768f0c", upload-time = "2026-01-21T22:32:19Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:cef372be1a78856000cd04b72e5d738a0d851022c68b6900cbbd61dc91c25f79", upload-time = "2026-01-21T22:32:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5d576c65d40198627e0fad03bddeb0ef536371312f2bdfcc804c22fd28fa6018", upload-time = "2026-01-21T22:32:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ebf2b495c76097796b9a2eac9290efbcae96e0fd9e5ae52c40eff188610bb440", upload-time = "2026-01-21T22:32:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:af00b4e0cdb3f490f4393e9a335b622fe1b92fd5afb181033256ccba03b9637c", upload-time = "2026-01-21T22:32:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8623e534ef6a815bd6407d4b52dd70c7154e2eda626ad4b9cb895d36c5a3305b", upload-time = "2026-01-21T22:32:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1255a0ca2bf987acf9f103b96c5c4cfe3415fc4a1eef17fa08af527a04a4f573", upload-time = "2026-01-21T22:32:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:068e519838b4a8b32a09521244b170edd8c2ac9eeb6538b7bf492cd70e57ebf5", upload-time = "2026-01-21T22:32:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:12c253520a26483fe3c614f63ff16eca6d9b0b4ebe510699b7d15d88e6c0cd35", upload-time = "2026-01-21T22:32:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a9c0de893dce9c2913c9c7ae88a916910f92d02b99da149678806d18e8079f29", upload-time = "2026-01-21T22:32:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:e2e0317e3861bba1b5aeba7c1cb4bcd50937cf0bffdbea478619d1f5f73e9050", upload-time = "2026-01-21T22:32:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:58b2971b55c761f1d2491bd80fcc4618ea97d363d387a9dd3aff23220cbee264", upload-time = "2026-01-21T22:32:28Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:1b6878b043513ea3dea1b90bfb5193455d9b248b8c4d5e66ea9f5d1643a43f13", upload-time = "2026-01-21T22:32:29Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:96cd2ba7b289117873b2a8f4c80605d38118d920b1045f3ce21a9f0ca68a701e", upload-time = "2026-01-21T22:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2dbf9ea9f4b2416822249e96ff3ad873d9a84e51285d6b9967732be3015c523", upload-time = "2026-01-21T22:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5b7ad3fb6cf03ef2a2fd617cb4b4e41efa9bb0143c67f506c2a3e6765c7b12ad", upload-time = "2026-01-21T22:32:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:a52ff3b072e89280f41499813e11c418d168ffc502b86cb17767bab29f432b3a", upload-time = "2026-01-21T22:32:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:687987fbcb074fd7f7a61cf2b407b1eac07588ace8351a3a36978546a00adc52", upload-time = "2026-01-21T22:32:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:84c5e2cb699235339b8a5c295e974a795244a45d1104ecee658d9d19600cdc75", upload-time = "2026-01-21T22:32:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:d1cf27bc2da13fd9e83694ae601b1bf4135c24d9c9e9ec249056896395a78a9e", upload-time = "2026-01-21T22:32:35Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0+cu130" +source = { registry = "https://download.pytorch.org/whl" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "numpy", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "pillow", marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train') or extra == 'group-7-cosmos3-cu130' or extra == 'group-7-cosmos3-cu130-train' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a82d06af6e538c2a860ed9bfdce696cfe0a130a27dc495d32cda099d8d68c85a", upload-time = "2026-01-21T22:34:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:9391dc5b2e038a5e365d07b425e4046fb8f605eeb6095fcb90f38a7b6c0aba7c", upload-time = "2026-01-21T22:34:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp310-cp310-win_amd64.whl", hash = "sha256:f73a8c772e00dc764bcfce1bd39acb014a867105a71842ca2dbb3faa948434a5", upload-time = "2026-01-21T22:34:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6e2da988706ac6557c8e07f24503320187412e6e564e26a6c9e7e58e3e494d82", upload-time = "2026-01-21T22:34:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:67ff8c6d59d609d5310fcb477f65517d6c4b40d716257994005ceefbd3e804f2", upload-time = "2026-01-21T22:34:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp311-cp311-win_amd64.whl", hash = "sha256:ce5c80ade0b6cdf398e86978ab72d41737da64599d22d36f67eebe536c039552", upload-time = "2026-01-21T22:34:43Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3646e6c8fa5066da392d0ff13002cc683301386fe1933f8f1432fc5292e5d288", upload-time = "2026-01-21T22:34:43Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c2f5e38f0cd57a2796e4503c0f13365deba01dbc167ef820f0beec7ca96f5f2e", upload-time = "2026-01-21T22:34:44Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp312-cp312-win_amd64.whl", hash = "sha256:7dd245ee7df0ceb00125e57615de31ca7232bf046143c2c3fe7a3b321bb50958", upload-time = "2026-01-21T22:34:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f915e3fbd381602003cc3fe5d4a52b10820765f1fb6ba63722f25055ffd9640c", upload-time = "2026-01-21T22:34:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e85337d59bdbf7006cd0c76012da3663ded5606dadc68f72280d9f2ab1e9191a", upload-time = "2026-01-21T22:34:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313-win_amd64.whl", hash = "sha256:f4ac532eee577ce712fb013e16da0174567f59fa472256e2aa37197e0117e7c5", upload-time = "2026-01-21T22:34:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:03073da31033316cb1965244ff4c2b21c6b0bff57c0afc33efdd6ce86e880723", upload-time = "2026-01-21T22:34:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6dffd64f99f35c66651d30c60f43d3218eb399e19930ca0243294b9ab2375f5", upload-time = "2026-01-21T22:34:49Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp313-cp313t-win_amd64.whl", hash = "sha256:d3cb744887e59513a6408bb24870b6259787997b3eea23b5281716bb14aa7da9", upload-time = "2026-01-21T22:34:49Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b5357a6e4d12d144f83c97d7951b0cc4bca49d3506232956b20a1847fcb4a5c3", upload-time = "2026-01-21T22:34:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e00be4ee50c76093674d1cb74eea01185a5f428cd798ae0fe951d5b3ddaafef2", upload-time = "2026-01-21T22:34:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314-win_amd64.whl", hash = "sha256:6ec12923ff773c800f1197d8cb5f50febc5bc209ad8a3ea3021dda0f6b62c520", upload-time = "2026-01-21T22:34:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2074e1878dbfbb0e3a1e86c903e417d62489fb04011372cc41df84891cbe007f", upload-time = "2026-01-21T22:34:53Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b16fd43b27b93e20c7ca355b4cd247e4cd4cbd3d001a7957760ef577f85cbe10", upload-time = "2026-01-21T22:34:53Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.25.0%2Bcu130-cp314-cp314t-win_amd64.whl", hash = "sha256:de5b0fdbe0766926d7f3814fce286e3bde10d7c8dd5a85bcc2f6e87578ebf303", upload-time = "2026-01-21T22:34:55Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "transformer-engine" +version = "2.12+cu128.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "importlib-metadata" }, + { name = "nvdlfw-inspect" }, + { name = "onnx" }, + { name = "onnxscript" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/transformer_engine-2.12%2Bcu128.torch210-cp313-cp313-linux_aarch64.whl", hash = "sha256:8a3536facb6f6d23dd550090fdd031609cfa053599a0a3112e677f21a9b9a15e" }, + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/transformer_engine-2.12%2Bcu128.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:ca95c8214e02e20e9904d6baefef26fe88eea1f7d96c6a10bb50ffd0b0a60bbc" }, +] + +[[package]] +name = "transformer-engine" +version = "2.12+cu130.torch210" +source = { registry = "https://nvidia-cosmos.github.io/cosmos-dependencies/v1.5.0" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and sys_platform != 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform != 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform != 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform != 'linux'", +] +dependencies = [ + { name = "einops" }, + { name = "importlib-metadata" }, + { name = "nvdlfw-inspect" }, + { name = "onnx" }, + { name = "onnxscript" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" } }, +] +wheels = [ + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/transformer_engine-2.12%2Bcu130.torch210-cp313-cp313-linux_aarch64.whl", hash = "sha256:b72ba3c37f06f1cf01366cb2a9683eca7f36f808f6c3e3b1dda96e004bd5db5e" }, + { url = "https://github.com/nvidia-cosmos/cosmos-dependencies/releases/download/v1.5.0/transformer_engine-2.12%2Bcu130.torch210-cp313-cp313-linux_x86_64.whl", hash = "sha256:b038ea45fc6cffcd66ddae2719def197c2dde51e172df372e6187c1ffc069b3f" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "trimesh" +version = "4.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/41/de14e2fa9b2d99214c60402fc57d2efb201f2925b16d6bee289565901d83/trimesh-4.11.2.tar.gz", hash = "sha256:30fbde5b8dd7c157e7ff4d54286cb35291844fd3f4d0364e8b2727f1b308fb06", size = 835044, upload-time = "2026-02-10T16:00:27.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/b9/da09903ea53b677a58ba770112de6fe8b2acb8b4cd9bffae4ff6cfe7c072/trimesh-4.11.2-py3-none-any.whl", hash = "sha256:25e3ab2620f9eca5c9376168c67aabdd32205dad1c4eea09cd45cd4a3edf775a", size = 740328, upload-time = "2026-02-10T16:00:25.246Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://download.pytorch.org/whl" } +wheels = [ + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d0b7b9b4f29ade31cf7bd7c237b50d270a2487cf943d034b307a9b3f73d9c7", upload-time = "2026-01-22T23:13:01Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:72d6b3eeaa13444ca29ba65d55fc57b6411373a2b8e619f63ae5962d3d9ea249", upload-time = "2026-01-22T23:13:08Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:290687f4f310ce794a07d6a43fa94dea9bda86615bf04578533378503bed715a", upload-time = "2026-01-22T23:13:16Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e021e87e8f266c6f87bf379b669c43c2800aac6247bdf5d518cb553e3c403c00", upload-time = "2026-01-22T23:13:25Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e42d084864d12b8784736fe51a0cd05f6cc56775b25c8102c9d80c421f4e298", upload-time = "2026-01-22T23:13:34Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f5928e6d44c34a97bbe164cceddc0ef2007121c89ebcfba5415cf452de7ee9f", upload-time = "2026-01-22T23:13:42Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58d57d6796b0004076315433526fe9d4af42044d430afdee1e6cd42a76bd6d09", upload-time = "2026-01-22T23:13:51Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0075039ff27765480083b1a109999bf27110f3542f1f9fad95f0f9065a36da79", upload-time = "2026-01-22T23:14:00Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ff9ec4c96f405bf2735c531af27f8cc2215927440e52fb1fbc8014961d5d12", upload-time = "2026-01-22T23:14:09Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a619c81f8e77116b1d10aec34d62db72daa5c0fb70a6478c9afab25edb729c52", upload-time = "2026-01-22T23:14:19Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20ed2f770f7217b8a994cba99e7cac37e7be735a3991344990b80ab4fea964c4", upload-time = "2026-01-22T23:14:28Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d800bda666ebbe7ec2146e3f5eb271ee87f8fac724011dc7f25dee9bfb5f7f", upload-time = "2026-01-22T23:14:40Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89b87d2837849a9bcd3d960ad2e3dac0345c6e1359b9859e29f68083cbda9b1d", upload-time = "2026-01-22T23:14:51Z" }, + { url = "https://download-r2.pytorch.org/whl/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:621c0e5973d834507ec69dcf909f35f0b02c5df490fde100433cef83359b7eeb", upload-time = "2026-01-22T23:15:01Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + +[[package]] +name = "typeguard" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tyro" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "typeguard" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/89/f4d76ba5ac4ca627c3a0413fdc8d90f202929c45d9c25deec29c9a86a0ae/tyro-1.0.8.tar.gz", hash = "sha256:c22bf238ce029b8cc262759129fccfd968300c3761f00ce186570af3891e14bb", size = 460626, upload-time = "2026-02-25T00:18:45.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7c/9c5a4ab9e24f1428fd5ab2728b7f68878f54d549a11fa431ead76d3d127b/tyro-1.0.8-py3-none-any.whl", hash = "sha256:abb6a4054f20616d9ca5181ba0c6ae0e35cf8f35ae0c78d3d4dbd1f678e0643d", size = 181935, upload-time = "2026-02-25T00:18:43.689Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, +] + +[[package]] +name = "uv" +version = "0.10.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/e7/600a90d4662dbd8414c1f6b709c8c79075d37d2044f72b94acbfaf29baad/uv-0.10.8.tar.gz", hash = "sha256:4b23242b5224c7eaea481ce6c6dbc210f0eafb447cf60211633980947cd23de4", size = 3936600, upload-time = "2026-03-03T21:35:22.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/6c/8ef256575242d5f3869c5a445ffd4363b91a89acb34a3e043bec2ad5a1be/uv-0.10.8-py3-none-linux_armv6l.whl", hash = "sha256:d214c82c7c14dd23f9aeb609d03070b8ea2b2f0cf249c9321cbbb5375a17e5df", size = 22461003, upload-time = "2026-03-03T21:35:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fb/fd0656a92e6b9c4f92ddba7dcd76bd87469be500755125e06fea853dc212/uv-0.10.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1315c3901c5859aec2c5b4a17da4c5410d17f6890890f9f1a31f25aa0fa9ace", size = 21549446, upload-time = "2026-03-03T21:35:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/64/b9/1a4105df3afe7af99791f5b00fb037d85b2e3aaa1227e95878538d51ecf3/uv-0.10.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a253e5d2cae9e02654de31918b610dfc8f1f16a33f34046603757820bc45ee1b", size = 20222180, upload-time = "2026-03-03T21:35:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/c5/72/6e98e0f8b3fe80cb881c36492dca6d932fbb05f956dfdccbdb8ebe4ceff4/uv-0.10.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:57a24e15fd9dd4a36bcec2ccbe4b26d2a172c109e954a8940f5e8a8b965dae74", size = 22064813, upload-time = "2026-03-03T21:35:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/737da8577f4b1799f7024f6cd98fffcac77076a1b078b277cffc84946e96/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:675dc659195f9b9811ef5534eb3f16459fc88e109aefacbc91c07751b5b9715a", size = 22064861, upload-time = "2026-03-03T21:35:25.067Z" }, + { url = "https://files.pythonhosted.org/packages/7e/21/464ee3cd81f44345953cb26dd49870811f7647f3074f7651775cadb2158b/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:18d2968b0a50111c2fc6b782f7c63ded4f461c44efab537f552cf565f9aaae25", size = 22054515, upload-time = "2026-03-03T21:35:44.572Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/1c592d7b843ffa999502116b0dc573732b40cb37061a4acc741dcdb181da/uv-0.10.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed3c7ebb6f757cddedb56dec3d7c745e5ea7310b11e12ae1c28f1e8172e7bbf", size = 23433992, upload-time = "2026-03-03T21:35:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/2b716f0613746138294598668bbe65295a8da3d8fa104a756dec6284bf3c/uv-0.10.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffaf115501e33be0d4f13cb5b7c2b46b031d4c679a6109e24a7edfb719c44c6c", size = 24257250, upload-time = "2026-03-03T21:35:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/0165e82cd1117cd6f8a7d9a2122c23cc091f7cf738aa4a2a54579420a08f/uv-0.10.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0209ee8cb573e113ff4a760360f28448f9ebcdcf9c91ca49e872821de5d2d054", size = 23338918, upload-time = "2026-03-03T21:35:33.795Z" }, + { url = "https://files.pythonhosted.org/packages/20/74/652129a25145732482bb0020602507f52d9a5ca0e1a40ddd6deb27402333/uv-0.10.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11dc790f732dc5fee61f0f6bd998fc2e9c200df1082245604ac091c32c23a523", size = 23259370, upload-time = "2026-03-03T21:35:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/19/c5/6e5923d6c9e3b50dc8542647bea692b7c227a9489f59ddff4fdfb20d8459/uv-0.10.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e26f8c35684face38db814d452dd1a2181152dbf7f7b2de1f547e6ba0c378d67", size = 22174747, upload-time = "2026-03-03T21:35:42.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/cd/eee9e1883888327d07f51e7595ed5952e0bca2dc79d1c03b8a6e4309553e/uv-0.10.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:385add107d40c43dc00ca8c1a21ecf43101f846f8339eb7026bf6c9f6df7760d", size = 22893359, upload-time = "2026-03-03T21:35:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/bf/36/407a22917e55ce5cc2e7af956e3b9d91648a96558858acef84e3c50d5ca8/uv-0.10.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:24e8eb28c4f05acb38e60fefe2a2b15f4283a3849ce580bf2a62aca0a13123b3", size = 22637451, upload-time = "2026-03-03T21:35:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/21/d5/dabef9914e1ff27ad95e4b1daf59cd97c80e26a44c04c2870bcca7c83fc0/uv-0.10.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:73a8c1a1fceac73cd983dcc0a64f4f94f5fd1e5428681a5a76132574264504fb", size = 23480991, upload-time = "2026-03-03T21:35:52.809Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c0/1a4a45a9246f087e9446d0d804a436f6ee0befeaef731b04d1b2802d9d8f/uv-0.10.8-py3-none-win32.whl", hash = "sha256:9f344fdb34938ce35e9211a1b866adfa0c7f043967652ed1431917514aeec062", size = 21579030, upload-time = "2026-03-03T21:35:28.176Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2b/b29510efa1e6f409db105dbdafbd942ca3a2b638bef682ff2e5b9f6e4021/uv-0.10.8-py3-none-win_amd64.whl", hash = "sha256:1e63015284ed28c2112717256c328513215fb966a57c5870788eac2e8f949f28", size = 23944828, upload-time = "2026-03-03T21:36:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9e/b5a11b0523171c0103c4fed54da76685a765ad4d3215e8220facfd24aed9/uv-0.10.8-py3-none-win_arm64.whl", hash = "sha256:a80284f46b6f2e0b3d03eb7c2d43e17139a4ec313e8b9f56a71efafc996804cb", size = 22322224, upload-time = "2026-03-03T21:35:14.148Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "(platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (platform_python_implementation == 'PyPy' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'cygwin' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'win32' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + +[[package]] +name = "vllm" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anthropic" }, + { name = "blake3" }, + { name = "cachetools" }, + { name = "cbor2" }, + { name = "cloudpickle" }, + { name = "compressed-tensors" }, + { name = "depyf" }, + { name = "diskcache" }, + { name = "einops" }, + { name = "fastapi", extra = ["standard"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "filelock" }, + { name = "flashinfer-cubin" }, + { name = "flashinfer-python" }, + { name = "gguf" }, + { name = "ijson" }, + { name = "lark", version = "1.2.2", source = { registry = "https://pypi.org/simple" } }, + { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, + { name = "lm-format-enforcer" }, + { name = "mcp" }, + { name = "mistral-common", extra = ["image"], marker = "(extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "model-hosting-container-standards" }, + { name = "msgspec" }, + { name = "ninja" }, + { name = "numba" }, + { name = "numpy" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl" }, + { name = "openai" }, + { name = "openai-harmony" }, + { name = "opencv-python-headless" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, + { name = "outlines-core" }, + { name = "partial-json-parser" }, + { name = "pillow" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "pyzmq" }, + { name = "quack-kernels" }, + { name = "regex" }, + { name = "requests" }, + { name = "sentencepiece" }, + { name = "setproctitle" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "six", marker = "python_full_version >= '3.12'" }, + { name = "tiktoken" }, + { name = "tokenizers" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchaudio" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torchvision", version = "0.25.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, + { name = "xgrammar", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/14/c330a72309051f762b357a2e41d5015bedbb106ad1e16a231bdfda2e2163/vllm-0.19.0.tar.gz", hash = "sha256:81e59cf87175e7a62eb8d9acf5989484bbd17089d5eface353f89067bda282d9", size = 31071745, upload-time = "2026-04-03T04:04:52.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/51/467e7a8cb4838022daa731b7f8b34c228691e36f938e1803c3a702c7bd69/vllm-0.19.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:6ab90ccca5d7ca3bd2c8f90133f0fac85e8f4af582a1c67c6cc3f63c615521e3", size = 384650557, upload-time = "2026-04-03T04:05:52.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/08/6a431731e4c163bc1fab85b63e269d84104aad0fba98dac1af34fdc5077f/vllm-0.19.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:2d0e5fae45367bdbf111fcad68f4c0f8fdddd2f2fb643e52f0f2daebef7b41cf", size = 432281473, upload-time = "2026-04-03T04:05:22.07Z" }, +] + +[[package]] +name = "wandb" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/60/d94952549920469524b689479c864c692ca47eca4b8c2fe3389b64a58778/wandb-0.25.0.tar.gz", hash = "sha256:45840495a288e34245d69d07b5a0b449220fbc5b032e6b51c4f92ec9026d2ad1", size = 43951335, upload-time = "2026-02-13T00:17:45.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/7d/0c131db3ec9deaabbd32263d90863cbfbe07659527e11c35a5c738cecdc5/wandb-0.25.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:5eecb3c7b5e60d1acfa4b056bfbaa0b79a482566a9db58c9f99724b3862bc8e5", size = 23287536, upload-time = "2026-02-13T00:17:20.265Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/31bb7f76a966ec87495e5a72ac7570685be162494c41757ac871768dbc4f/wandb-0.25.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:daeedaadb183dc466e634fba90ab2bab1d4e93000912be0dee95065a0624a3fd", size = 25196062, upload-time = "2026-02-13T00:17:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/258cdedbf30cebc692198a774cf0ef945b7ed98ee64bdaf62621281c95d8/wandb-0.25.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5e0127dbcef13eea48f4b84268da7004d34d3120ebc7b2fa9cefb72b49dbb825", size = 22799744, upload-time = "2026-02-13T00:17:26.437Z" }, + { url = "https://files.pythonhosted.org/packages/de/91/ec9465d014cfd199c5b2083d271d31b3c2aedeae66f3d8a0712f7f54bdf3/wandb-0.25.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6c4c38077836f9b7569a35b0e1dcf1f0c43616fcd936d182f475edbfea063665", size = 25262839, upload-time = "2026-02-13T00:17:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/cb2d1c7143f534544147fb53fe87944508b8cb9a058bc5b6f8a94adbee15/wandb-0.25.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6edd8948d305cb73745bf564b807bd73da2ccbd47c548196b8a362f7df40aed8", size = 22853714, upload-time = "2026-02-13T00:17:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/d7/94/68163f70c1669edcf130822aaaea782d8198b5df74443eca0085ec596774/wandb-0.25.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ada6f08629bb014ad6e0a19d5dec478cdaa116431baa3f0a4bf4ab8d9893611f", size = 25358037, upload-time = "2026-02-13T00:17:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/9578eed2c01b2fc6c8b693da110aa9c73a33d7bb556480f5cfc42e48c94e/wandb-0.25.0-py3-none-win32.whl", hash = "sha256:020b42ca4d76e347709d65f59b30d4623a115edc28f462af1c92681cb17eae7c", size = 24604118, upload-time = "2026-02-13T00:17:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/25/97/460f6cb738aaa39b4eb2e6b4c630b2ae4321cdd70a79d5955ea75a878981/wandb-0.25.0-py3-none-win_amd64.whl", hash = "sha256:78307ac0b328f2dc334c8607bec772851215584b62c439eb320c4af4fb077a00", size = 24604122, upload-time = "2026-02-13T00:17:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/27/6c/5847b4dda1dfd52630dac08711d4348c69ed657f0698fc2d949c7f7a6622/wandb-0.25.0-py3-none-win_arm64.whl", hash = "sha256:c6174401fd6fb726295e98d57b4231c100eca96bd17de51bfc64038a57230aaf", size = 21785298, upload-time = "2026-02-13T00:17:42.475Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webdataset" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "braceexpand" }, + { name = "numpy" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/3a/68800d92e065cf4750ebecf973b13979c0c929b439e1293012938862038d/webdataset-1.0.2.tar.gz", hash = "sha256:7f0498be827cfa46cc5430a58768a24e2c6a410676a61be1838f53d61afdaab4", size = 80090, upload-time = "2025-06-19T23:26:21.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/00/aca6beb3658dab4ed3dbb41a78e6e7f31342e0b41d28088f205525751601/webdataset-1.0.2-py3-none-any.whl", hash = "sha256:3dbfced32b25c0d199c6b9787937b6f85742bc3c84f652c846893075c1c082d9", size = 74956, upload-time = "2025-06-19T23:26:20.354Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + +[[package]] +name = "wheel" +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xatlas" +version = "0.0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/fe/32adb4dbdf5485c185de9b62b83d434f12ca5be8a000d9882c871808987b/xatlas-0.0.11.tar.gz", hash = "sha256:72f0bc6c42c19252be87e947d9dfe251c8d6c6943fd43e3d173ddc6b1afad693", size = 7718208, upload-time = "2025-07-04T16:46:43.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/37/ef61e4637d92bd7a79ce762a3406375fb2bf6d430796e3aa408d577fc73a/xatlas-0.0.11-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8275d2b38b5888d7bb13c10b5704e9469ba0c35eba3e8d61f9120caede4e1621", size = 222385, upload-time = "2025-07-04T16:45:21.335Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d0/56fc00dc1b449db824f81ce82724eb11b8e4deb6ab806f82bf408d1bf9c7/xatlas-0.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed78fddec204e7ee42a14672df3adea54cbae3d2aa366e23a80163eb4a43b749", size = 199833, upload-time = "2025-07-04T16:45:22.689Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c1/bee69092bfb9906edad278fc7a4b7a1662c5a3b288dd191ed7b8ec381dcb/xatlas-0.0.11-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00095691e1725eacb84370b3e8151ddebea050f0992c08fc02773a24663901fd", size = 267932, upload-time = "2025-07-04T16:45:23.786Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/faefe42f007abc2e3ea94d16e74577837106842711fddd2c1dce0a26af6b/xatlas-0.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc1eefc14b69bf9ae4a529809cf9ceca50c45b87f2fff5220a80291b3ff2e3f0", size = 261604, upload-time = "2025-07-04T16:45:25.109Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b0/84a3770b11d693aa0aca38d374a32f30613571802abedf5fd3d7a6cc43ae/xatlas-0.0.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cb2e790bfbde25af65f1d7ffa9a7e2c3bacf964cec7a0503732b82d7c805f783", size = 1410910, upload-time = "2025-07-04T16:45:26.377Z" }, + { url = "https://files.pythonhosted.org/packages/45/6f/1744898c8096fd1c91950e8b5d19244d88acb92dbaa9d4d579e9ea304518/xatlas-0.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7643d17f9c75f37a82c497e94ee280455a7e72246da600ac30df8abc69dc774", size = 1287993, upload-time = "2025-07-04T16:45:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7b/6da64b23378b4f6d4e47e415e6ea1635ce9bcf78d1e99c2e7a105fb8dfc1/xatlas-0.0.11-cp310-cp310-win32.whl", hash = "sha256:20b506f75572a64e463039e7b28722031bc6f1e96165b5d3c214b82487f6a147", size = 184644, upload-time = "2025-07-04T16:45:28.788Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/059ced56f634b37a2e06ecb5e3f85d2c11d8494f23318f856d95b6789b8b/xatlas-0.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:c6030feb083bd1cead948246445c9b997c80cd1b1300d0f52c3b5e745c294462", size = 206203, upload-time = "2025-07-04T16:45:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/70b7d671884a0b667832d90e7c2097d9d7c2e6364a06b1bf4a7e1a6410aa/xatlas-0.0.11-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:364bb9f8a90d66490ea409c8f697400daece61cb6b2806d4365a1ed1a22d9025", size = 223436, upload-time = "2025-07-04T16:45:30.852Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ff/884c13f9380ae028283b14edd43e1b47c9874884edbc164fc3be99027d2e/xatlas-0.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68c2e52266a0126d48ae79634f3a9d476aaebc83eda7a15b767e122241c2adbe", size = 201013, upload-time = "2025-07-04T16:45:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/f78c7b6a2ec3961446400f455e22de51050e7d8790f382b27925d950a15f/xatlas-0.0.11-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:747edb43d8d9747cf8d36e280c6c320740efc260ef18d56fac62447a0aba3146", size = 268566, upload-time = "2025-07-04T16:45:33.379Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e2/603763b0c7ca641c50d571d0b8f961c152d9039e0febeb269baa942ddf2d/xatlas-0.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c044ee009a427898b26c0929646bfb0e0356f6762be40a140f4af41a33026208", size = 262197, upload-time = "2025-07-04T16:45:34.431Z" }, + { url = "https://files.pythonhosted.org/packages/38/a8/6c90414b9bbbec5bb1536983aa374b2d182392d9d13422b319234b918b23/xatlas-0.0.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e87662f784ed6245db45771c80fb0459500547482ac1561b2c008ebfd01c2131", size = 1411957, upload-time = "2025-07-04T16:45:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cb/2835c85c45fdc951d447ba083b0cd6b69d5d129f6c3a37585a30ed16f392/xatlas-0.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8198cc3d8a30a7b1c3f801b7ff2d28e6ca355900d4e1955698475bd53e60cf57", size = 1288950, upload-time = "2025-07-04T16:45:36.901Z" }, + { url = "https://files.pythonhosted.org/packages/76/ee/e6d8c448b8bd88197548a2f83b3e3121b5e447152055b548658d2334cfab/xatlas-0.0.11-cp311-cp311-win32.whl", hash = "sha256:5d1a042c599a934391e118d77570d3a25234455d08523b0e52e63d50b7c98cc8", size = 185689, upload-time = "2025-07-04T16:45:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/02/26/626fbe77e28d8cdea9ccadc941862560b6bbccc41db5b2837c3325617620/xatlas-0.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:967b2c7434d2d0412d876a7cc6d852a0bd4acdb9d68971acad302c97bf883745", size = 206764, upload-time = "2025-07-04T16:45:39.244Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9ef325565fc643844436d7b5c890b7ee93ac0d87c1454419be3cfb35471f/xatlas-0.0.11-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:6963bc0831dd1539d423e4f8199f99effdc4c21e034753c742fced2c3bab8b9c", size = 223431, upload-time = "2025-07-04T16:45:40.441Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/dd97d03a7c4a59740394b5afcd77e64cba137c786e5bb177794907f0a658/xatlas-0.0.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60ed8502af10c980e988bda8bc933b097899f328d61f15c4c5b9a912796493ae", size = 201053, upload-time = "2025-07-04T16:45:41.425Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/17a683ba157f72d5523f4c3a8179722556f6474b3011600860b425f246e2/xatlas-0.0.11-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4d7aec5931173776ce3786a2aeb16cca67b13c56bd900f18c78fb675f120312", size = 268343, upload-time = "2025-07-04T16:45:42.453Z" }, + { url = "https://files.pythonhosted.org/packages/f4/68/39e33ff980f60442c4294d3a4ce37000b2f2e439106f05b014f0d453c186/xatlas-0.0.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19a0b0c0b1966d177e7ded15539091981c697cf8f9aee459eae9f9ecba919c07", size = 260988, upload-time = "2025-07-04T16:45:44.052Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/eae55125ccc03cec0daa06665469270d443a180d2221e4424a757087ac55/xatlas-0.0.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bed71a09bfea30c2636cd31857c7f7bfd72a41a74092a4d85d770f065ac411e6", size = 1414363, upload-time = "2025-07-04T16:45:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/c43bcf36516ba076cc3cba5c319a8d03f486192a06c33f9a7f06efee82f7/xatlas-0.0.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b10092e3aec1fd0423198b8b7bd7bc0dba350f9a382520e79b948e5105f2ee60", size = 1291680, upload-time = "2025-07-04T16:45:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/34/13/8a708742cecf65c65d8b99d30ab567d662153b838b121e5b8f4fa5b2a9c2/xatlas-0.0.11-cp312-cp312-win32.whl", hash = "sha256:ee6d6056b0d5e15aa72e7617d4bf66d5c13aad950c5e26536eeb4bef8a6bc3f8", size = 185880, upload-time = "2025-07-04T16:45:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/5a73459ff4e2d3618d8b6cabc28d6f2369e3bf772cf2c68b26e126295dff/xatlas-0.0.11-cp312-cp312-win_amd64.whl", hash = "sha256:5f3cb843ac313af7cd81c72d4e3c16554d00108b9b8b28973e7eb87f0f30d54c", size = 207923, upload-time = "2025-07-04T16:45:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/1dd383c0d28e396f16eee548e2b3f9f4cec8d3ab3db004c8bbcf57a44939/xatlas-0.0.11-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:7d41023193a433ea783df563ccde2bb8bb039dca0896469a3cb32e58db9763da", size = 223491, upload-time = "2025-07-04T16:45:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/ccc8ec8aa31eaa09b8b2b8922c50d97af8d41a24254499f5eaf926352c26/xatlas-0.0.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:70a459492c0ffcf7e37a77ac37f8a4483acd87aee07de9600e4f056e56c326dd", size = 201092, upload-time = "2025-07-04T16:45:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/f7/9f/2bd9b63d24751db2ba6c85944e09be2bc897aaace224d22dbc764a19487f/xatlas-0.0.11-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7739f75c588808a0ad0b3a730661143dddddceec84401451989b829d9986df94", size = 268197, upload-time = "2025-07-04T16:45:52.673Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/df846c46097331af6a10d3675edefc2ab893cb784c02fc7ab1e8cc580457/xatlas-0.0.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c2ba5ca5e26dba5e9a04aab98ee5de0e5a0e8c87a1feae83da11052b83d86dd", size = 260836, upload-time = "2025-07-04T16:45:53.718Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9a/962bc0ddcce7c81e9f08b020132d8b0143f18d8ee49744aca669a2fe1a1b/xatlas-0.0.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:494206aa15a28f44691acd1575344181717b300e5602187e7db04fa72e0d1e9b", size = 1414366, upload-time = "2025-07-04T16:45:54.942Z" }, + { url = "https://files.pythonhosted.org/packages/11/15/fe587b62db22b9d5220576995de67d7d00f0d3b8a6d461cc670831820676/xatlas-0.0.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ff4c084c1f2cccbc781302484613c4d87fcbab1fa4972a4e84194acb18bbf9d9", size = 1291580, upload-time = "2025-07-04T16:45:56.18Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/8eba622eead5571c73fb997095ab156ba40f8e99471b9b37053d32a338d4/xatlas-0.0.11-cp313-cp313-win32.whl", hash = "sha256:12d5f37f60bc6a82e538dc553eb3ebe85fd622ecb9b6d32505db4c3ea1202706", size = 185822, upload-time = "2025-07-04T16:45:57.222Z" }, + { url = "https://files.pythonhosted.org/packages/8c/91/116e12b0b7bce720eea981b06b39150ceaef49b044bd3b535316443cfd05/xatlas-0.0.11-cp313-cp313-win_amd64.whl", hash = "sha256:81431d19c3a11dc3fbae95460cd6b16239636b21fa7c30bd5796976a03844ede", size = 207911, upload-time = "2025-07-04T16:45:58.526Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/914cd8dec344443f1e26d47a3db1d92ec45fe86c62b8147bcb7ca616644f/xatlas-0.0.11-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e789e85f2cdb451ee0ba91ec274f0e1d1a8daaf75b467dcd7533edf9cd220699", size = 222495, upload-time = "2025-07-04T16:46:17.956Z" }, + { url = "https://files.pythonhosted.org/packages/de/8d/1b0240c9472cacbd787cda227c7a7d3e3251c5267707fded47beb2556948/xatlas-0.0.11-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b26e2cfb175a049ef44ff6e01bbecc52aff740e8cbab0e74ce2349a1bab220fc", size = 199701, upload-time = "2025-07-04T16:46:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/5d/da/426cfed3879d7b692080428c7b287d323aa1b6e52895c01ea698860c46ed/xatlas-0.0.11-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f90b69073aef1a39fe0b708576ffe4034d9508ebe9edcbfbd4092e1e683eff32", size = 267893, upload-time = "2025-07-04T16:46:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/aa/24/cafe4af289301c84e194165bb4ae10014cc40fca2430ef2987bff968d169/xatlas-0.0.11-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6ac223ffc49167b553ff7e81af7b8b5a14a73d5b9e98789821511483b4cdee9", size = 262002, upload-time = "2025-07-04T16:46:21.107Z" }, + { url = "https://files.pythonhosted.org/packages/75/86/16ea4181e60462f773109e384a08db3fe79b1d73c1765e77adeefeccac31/xatlas-0.0.11-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:426c73173353800414a05270005070333afebfd1ab130f73767738d04664ba8e", size = 205708, upload-time = "2025-07-04T16:46:22.196Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/e33fe835e6e3c0c38109a367009e88554de9eefa4e65c01c1f6360c487d0/xatlas-0.0.11-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8facf1189f775f0c0ff9f67b0fb78cc18d8a40d5b3d56af61238315fd204205d", size = 223700, upload-time = "2025-07-04T16:46:23.199Z" }, + { url = "https://files.pythonhosted.org/packages/de/0b/d9d8963dbf0b1aa62a2d4709574490780c0f5b78d0fade21672cdace000b/xatlas-0.0.11-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad5d661b15bbcba44a37d4053d67553c3625739421805061becbda285be9b663", size = 200990, upload-time = "2025-07-04T16:46:25.747Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/ce1812f2396ee187f9744e415050f44112089911de3ef8b1bff7b3e00e8b/xatlas-0.0.11-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:430a0553f539f4d2ea0180cbfa7888b596393ae9dd1ad4b046df6895b87f4703", size = 268799, upload-time = "2025-07-04T16:46:26.831Z" }, + { url = "https://files.pythonhosted.org/packages/73/3f/34beca37b4b66fa11f7932ef73f147a43c413b704ccf029d638eccb23ac2/xatlas-0.0.11-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54569a3aba9c1081ca23209391feecd1639228aad08c384c2995432dd57a3919", size = 263463, upload-time = "2025-07-04T16:46:28.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/6e7999da339ea492ad562b6b46a2f58e6d1c8e3d87f6f0acb67fb426ebc0/xatlas-0.0.11-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3e4b3fd1b22d44d669c997c2196d608060220fcc631fa258fb8530c1c34563e3", size = 206464, upload-time = "2025-07-04T16:46:29.476Z" }, +] + +[[package]] +name = "xattr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d5/25f7b19af3a2cb4000cac4f9e5525a40bec79f4f5d0ac9b517c0544586a0/xattr-1.3.0.tar.gz", hash = "sha256:30439fabd7de0787b27e9a6e1d569c5959854cb322f64ce7380fedbfa5035036", size = 17148, upload-time = "2025-10-13T22:16:47.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/11/bbb25ab921e02efb789efcab5b7d03581b5d28f71d829f21e4ea6aba09fb/xattr-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a80c4617e08670cdc3ba71f1dbb275c1627744c5c3641280879cb3bc95a07237", size = 23453, upload-time = "2025-10-13T22:15:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/be/88/66021fdfbb2037a94fc5b16c1dce1894b8e9da7a1829e4be0b491b3f24ff/xattr-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51cdaa359f5cd2861178ae01ea3647b56dbdfd98e724a8aa3c04f77123b78217", size = 18551, upload-time = "2025-10-13T22:15:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/5dd21fcfc48487a59fcec33ffe02eb671f256424869e9aef87e33c65d95b/xattr-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2fea070768d7d2d25797817bea93bf0a6fda6449e88cfee8bb3d75de9ed11c7b", size = 18852, upload-time = "2025-10-13T22:15:53.104Z" }, + { url = "https://files.pythonhosted.org/packages/af/2a/e29753ac17a92aadf27b9e16b1d600584d9f10acd0b399d2c06f47af2dff/xattr-1.3.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69bca34be2d7a928389aff4e32f27857e1c62d04c91ec7c1519b1636870bd58f", size = 38547, upload-time = "2025-10-13T22:15:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/f4/46/b2c9185d24b93542e4307ce30cd3d4eb6af8efdc843d98ff9f07fcb048d9/xattr-1.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05f8e068409742d246babba60cff8310b2c577745491f498b08bf068e0c867a3", size = 38755, upload-time = "2025-10-13T22:15:55.738Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0a/93cf1f03536bf38e8fd3fe57eb04124e4dfe2e16c0c5ced589d3360a1858/xattr-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bbd06987102bc11f5cbd08b15d1029832b862cf5bc61780573fc0828812f01ca", size = 38052, upload-time = "2025-10-13T22:15:57.031Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/60e43f7e1037cee671e14c2a283e3e7168b756c9938eba62f0616e6599aa/xattr-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b8589744116d2c37928b771c50383cb281675cd6dcfd740abfab6883e3d4af85", size = 37560, upload-time = "2025-10-13T22:15:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/64/292426ad5653e72c6e1325bbff22868a20077290d967cebb9c0624ad08b6/xattr-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:331a51bf8f20c27822f44054b0d760588462d3ed472d5e52ba135cf0bea510e8", size = 23448, upload-time = "2025-10-13T22:15:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/63/84/6539fbe620da8e5927406e76b9c8abad8953025d5f578d792747c38a8c0e/xattr-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:196360f068b74fa0132a8c6001ce1333f095364b8f43b6fd8cdaf2f18741ef89", size = 18553, upload-time = "2025-10-13T22:16:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bb/c1c2e24a49f8d13ff878fb85aabc42ea1b2f98ce08d8205b9661d517a9cc/xattr-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:405d2e4911d37f2b9400fa501acd920fe0c97fe2b2ec252cb23df4b59c000811", size = 18848, upload-time = "2025-10-13T22:16:01.046Z" }, + { url = "https://files.pythonhosted.org/packages/02/c2/a60aad150322b217dfe33695d8d9f32bc01e8f300641b6ba4b73f4b3c03f/xattr-1.3.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ae3a66ae1effd40994f64defeeaa97da369406485e60bfb421f2d781be3b75d", size = 38547, upload-time = "2025-10-13T22:16:01.973Z" }, + { url = "https://files.pythonhosted.org/packages/c6/58/2eca142bad4ea0a2be6b58d3122d0acce310c4e53fa7defd168202772178/xattr-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:69cd3bfe779f7ba87abe6473fdfa428460cf9e78aeb7e390cfd737b784edf1b5", size = 38753, upload-time = "2025-10-13T22:16:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/2b/50/d032e5254c2c27d36bdb02abdf2735db6768a441f0e3d0f139e0f9f56638/xattr-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c5742ca61761a99ae0c522f90a39d5fb8139280f27b254e3128482296d1df2db", size = 38054, upload-time = "2025-10-13T22:16:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/458a306439aabe0083ca0a7b14c3e6a800ab9782b5ec0bdcec4ec9f3dc6c/xattr-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a04ada131e9bdfd32db3ab1efa9f852646f4f7c9d6fde0596c3825c67161be3", size = 37562, upload-time = "2025-10-13T22:16:05.97Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/00bdc9290066173e53e1e734d8d8e1a84a6faa9c66aee9df81e4d9aeec1c/xattr-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd4e63614722d183e81842cb237fd1cc978d43384166f9fe22368bfcb187ebe5", size = 23476, upload-time = "2025-10-13T22:16:06.942Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/5243722294eb982514fa7b6b87a29dfb7b29b8e5e1486500c5babaf6e4b3/xattr-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:995843ef374af73e3370b0c107319611f3cdcdb6d151d629449efecad36be4c4", size = 18556, upload-time = "2025-10-13T22:16:08.209Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/d7ab0e547bea885b55f097206459bd612cefb652c5fc1f747130cbc0d42c/xattr-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa23a25220e29d956cedf75746e3df6cc824cc1553326d6516479967c540e386", size = 18869, upload-time = "2025-10-13T22:16:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/25/25cc7d64f07de644b7e9057842227adf61017e5bcfe59a79df79f768874c/xattr-1.3.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4345387087fffcd28f709eb45aae113d911e1a1f4f0f70d46b43ba81e69ccdd", size = 38797, upload-time = "2025-10-13T22:16:11.624Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/cc350bcdbed006dfcc6ade0ac817693b8b3d4b2787f20e427fd0697042e4/xattr-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe92bb05eb849ab468fe13e942be0f8d7123f15d074f3aba5223fad0c4b484de", size = 38956, upload-time = "2025-10-13T22:16:13.121Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b2/9416317ac89e2ed759a861857cda0d5e284c3691e6f460d36cc2bd5ce4d1/xattr-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c42ef5bdac3febbe28d3db14d3a8a159d84ba5daca2b13deae6f9f1fc0d4092", size = 38214, upload-time = "2025-10-13T22:16:14.389Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, + { url = "https://files.pythonhosted.org/packages/27/d3/6a1731a339842afcbb2643bc93628d4ab9c52d1bf26a7b085ca8f35bba6e/xattr-1.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:937d8c91f6f372788aff8cc0984c4be3f0928584839aaa15ff1c95d64562071c", size = 23474, upload-time = "2025-10-13T22:16:16.33Z" }, + { url = "https://files.pythonhosted.org/packages/1b/25/6741ed3d4371eaa2fae70b259d17a580d858ebff8af0042a59e11bb6385f/xattr-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e470b3f15e9c3e263662506ff26e73b3027e1c9beac2cbe9ab89cad9c70c0495", size = 18558, upload-time = "2025-10-13T22:16:17.251Z" }, + { url = "https://files.pythonhosted.org/packages/ba/84/cc450688abeb8647aa93a62c1435bb532db11313abfeb9d43b28b4751503/xattr-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f2238b2a973fcbf5fefa1137db97c296d27f4721f7b7243a1fac51514565e9ec", size = 18869, upload-time = "2025-10-13T22:16:18.607Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/0e2315225ba7557e9801f9f0168a0195a7e13a3223088081eb32d2760533/xattr-1.3.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f32bb00395371f4a3bed87080ae315b19171ba114e8a5aa403a2c8508998ce78", size = 38702, upload-time = "2025-10-13T22:16:19.539Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8c/de4f4441c318ac38a5d3d7d4b8b940305a667e9320c34a45e57f6eb6b0e8/xattr-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:78df56bfe3dd4912548561ed880225437d6d49ef082fe6ccd45670810fa53cfe", size = 38869, upload-time = "2025-10-13T22:16:20.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2a/38e0498c22aa733a9b5265f4929af4613e5b967659cf3e5f2f933b3ba118/xattr-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:864c34c14728f21c3ef89a9f276d75ae5e31dd34f48064e0d37e4bf0f671fc6e", size = 38210, upload-time = "2025-10-13T22:16:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/62/21/49b386eb8dcf42ac8e3ff55b6e8ea0a1e8b6b799571599c795265d2dc1b5/xattr-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1fd185b3f01121bd172c98b943f9341ca3b9ea6c6d3eb7fe7074723614d959ff", size = 37753, upload-time = "2025-10-13T22:16:23.959Z" }, + { url = "https://files.pythonhosted.org/packages/24/49/b8bc589427696d67bc2b0992c188e576f70242c586a379f97698772c0c3d/xattr-1.3.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:630c85020282bd0bcb72c3d031491c4e91d7f29bb4c094ebdfb9db51375c5b07", size = 23543, upload-time = "2025-10-13T22:16:25.242Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0a/03192e78071cfb86e6d8ceae0e5dcec4bacf0fd734755263aabd01532e50/xattr-1.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:95f1e14a4d9ca160b4b78c527bf2bac6addbeb0fd9882c405fc0b5e3073a8752", size = 18673, upload-time = "2025-10-13T22:16:26.224Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/9ab4f0b5c3d10df3aceaecf7e395cabe7fb7c7c004b2dc3f3cff0ef70fc3/xattr-1.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:88557c0769f64b1d014aada916c9630cfefa38b0be6c247eae20740d2d8f7b47", size = 18877, upload-time = "2025-10-13T22:16:27.164Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1c/ab905d19a1349e847e37e02933316d17adfd1dd70b64d366885ab0bd959d/xattr-1.3.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6992eb5da32c0a1375a9eeacfab15c66eebc8bd34be63ebd1eae80cc2f8bf03", size = 38782, upload-time = "2025-10-13T22:16:28.157Z" }, + { url = "https://files.pythonhosted.org/packages/83/a7/f615a6e5d48d47e9febbe5a62b94ffa0d8bfc6d325b899873281abac10c4/xattr-1.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da5954424099ca9d402933eaf6112c29ddde26e6da59b32f0bf5a4e35eec0b28", size = 38936, upload-time = "2025-10-13T22:16:29.291Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6c/a8221567a7cbc00ac305a4842318562f90bb1fdd16636e1379361133f1f4/xattr-1.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:726b4d0b66724759132cacdcd84a5b19e00b0cdf704f4c2cf96d0c08dc5eaeb5", size = 38268, upload-time = "2025-10-13T22:16:30.238Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/38a98df630e19360d98df8d98ec4a2560612840823f0bf55f81e0e84c866/xattr-1.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:928c49ceb0c70fc04732e46fa236d7c8281bfc3db1b40875e5f548bb14d2668c", size = 37825, upload-time = "2025-10-13T22:16:31.557Z" }, + { url = "https://files.pythonhosted.org/packages/97/3f/6d50237645edd83e9dc6bf6521e4e28335845b674cabefd69f12bc4db59a/xattr-1.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f3bef26fd2d5d7b17488f4cc4424a69894c5a8ed71dd5f657fbbf69f77f68a51", size = 23788, upload-time = "2025-10-13T22:16:32.465Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8b/3efd48c85e08d1bfcbd46f87368b155d3d3de78bb660b408fbaff7623572/xattr-1.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:64f1fb511f8463851e0d97294eb0e0fde54b059150da90582327fb43baa1bb92", size = 18825, upload-time = "2025-10-13T22:16:33.442Z" }, + { url = "https://files.pythonhosted.org/packages/fd/19/4b4e3e2ea5fa213ff4220e84450628fecde042b0961e7b4e6d845e555ade/xattr-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1e6c216927b16fd4b72df655d5124b69b2a406cb3132b5231179021182f0f0d1", size = 19023, upload-time = "2025-10-13T22:16:34.395Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4a/6460befb22ce8d43abdb22d2bf5aa63b8311507c75dc50ad402681b4b095/xattr-1.3.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c0d9ab346cdd20539afddf2f9e123efee0fe8d54254d9fc580b4e2b4e6d77351", size = 43732, upload-time = "2025-10-13T22:16:35.41Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/3fa83e9f91dc868d764b2ca3758bf449945c4b1511e137e33a6210609b58/xattr-1.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2c5e7ba0e893042deef4e8638db7a497680f587ac7bd6d68925f29af633dfa6b", size = 43851, upload-time = "2025-10-13T22:16:36.416Z" }, + { url = "https://files.pythonhosted.org/packages/28/b3/06bf7f691c3f35e94a37e097ae1868fbaa916cc174b1b916fb7aeca441e4/xattr-1.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e0dabb39596d8d7b83d6f9f7fa30be68cf15bfb135cb633e2aad9887d308a32", size = 43274, upload-time = "2025-10-13T22:16:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/df/41/d6298c95513eabe091a6851bff5e7928fab49ffd9143808feaaf7721cf33/xattr-1.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5eeaa944516b7507ec51456751334b4880e421de169bbd067c4f32242670d606", size = 42864, upload-time = "2025-10-13T22:16:38.811Z" }, +] + +[[package]] +name = "xgrammar" +version = "0.1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pydantic" }, + { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform == 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm')" }, + { name = "torch", version = "2.10.0+cu130", source = { registry = "https://download.pytorch.org/whl" }, marker = "(sys_platform != 'darwin' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "transformers" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/43/e5dfddb1d2a4fccf3e3a88f103e88698cdefc3182f4e169a359ffe1c1794/xgrammar-0.1.33.tar.gz", hash = "sha256:8dbe5fc3d76651ab1fac7a68fc2a118b885fa0ec7189927fb6e0dce0081aea99", size = 2398956, upload-time = "2026-03-27T10:16:36.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ad/ac47eaa00e10877a74171b2290e599bb7b3002ebe89dc58f8e400619c3be/xgrammar-0.1.33-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6cdb24f917b104cdd3f4f8ec9c8ef79223d2865622e9bf4da8835776c81409d8", size = 22766493, upload-time = "2026-03-27T10:14:32.628Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/7eb0a1a94f4634899c4e7d4cf56b1aa7cb37177b31b3d2d5c552baebf7b0/xgrammar-0.1.33-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a38aee7a91f1b062b045e2c3e57279aef9155a84fa898bf9a9ae7820090465ce", size = 22703593, upload-time = "2026-03-27T10:14:35.993Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/61485241556663db7cabefd1ffc6039ec42145821c8c8008996b6d96c3aa/xgrammar-0.1.33-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb61a21f44f0c3bd4884426b2d18479622202e2fd0928b63c10a0168972c502e", size = 42131816, upload-time = "2026-03-27T10:14:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/62/a9/69fbbcde74ac0ccc914e0eb222ac0916688fe291f9f234852dc77912a1bf/xgrammar-0.1.33-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b8aad12a149e3b689b5c4aef8ff529e58e55aee1406bd372c850b504131a8b7", size = 42204408, upload-time = "2026-03-27T10:14:43.692Z" }, + { url = "https://files.pythonhosted.org/packages/63/2a/1ba21492fe956a2a284d037e2035e585c170107cd522e888a865659fb8a2/xgrammar-0.1.33-cp310-cp310-win_amd64.whl", hash = "sha256:19299330f8b04242eaefc37967edd317d044b024c2d509363ba7dd4d483532ab", size = 7222752, upload-time = "2026-03-27T10:14:46.553Z" }, + { url = "https://files.pythonhosted.org/packages/7d/df/695172c6e16e3145ebeffadf7045d1b43d874990da19c7519b01c49ef45a/xgrammar-0.1.33-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:b5f0bbabe128ff5c985b77b4315d25f19a3c7247a0847708a4a484ae6214041a", size = 22766437, upload-time = "2026-03-27T10:14:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/a0/de/14ab62bfa6035d0ad276f10f0795fb957cfafb0e3ebc77e87ef36befc461/xgrammar-0.1.33-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08c7befefb38c89bf368c26a8e75d00204f03fc303eed61ca570dd3f568d9ead", size = 22703373, upload-time = "2026-03-27T10:14:53.501Z" }, + { url = "https://files.pythonhosted.org/packages/4b/16/f8297e0e3b468636d8e0190002badfe4a6d8d1c2af295fea2d164e7b5a8a/xgrammar-0.1.33-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f561e676df8c9e941c7a2f6df9612bbf645bf1fc714b4a9282cf75cff532f8", size = 42132308, upload-time = "2026-03-27T10:14:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/12/e0/629b892a3810446097635dd1be7e4d977107c42232efb229d70e5c827227/xgrammar-0.1.33-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bc9151d9f0d05862c253998c533f04c000273f57180fb6a4e3623e321fd47db", size = 42204526, upload-time = "2026-03-27T10:15:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/29/f5/aee458b54919ef989ca21d4a39721a51b8a3cba37614148b863968ef5c8c/xgrammar-0.1.33-cp311-cp311-win_amd64.whl", hash = "sha256:27f0cf751b9130805c7db745a7abb86f05228d58523d8388b0b970cada6dee0a", size = 7222644, upload-time = "2026-03-27T10:15:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5f/9a1ebc9505392ff626b9ba8fca54d46bdba454af80551169676ee5cd27d4/xgrammar-0.1.33-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:16d05f8f9df9852f2055e112adf2ce22440062979bc3dc66869a0b7c2f93eb0a", size = 22765695, upload-time = "2026-03-27T10:15:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/82/82/7081feb505873238583a003f790b10ce84d66ab3a8e8e244e8c1c4729d70/xgrammar-0.1.33-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb9e0ed6748b8e82d4569be4ebc8c9e60694369295c4fed0ebaa4bab4aa4eb4", size = 22702262, upload-time = "2026-03-27T10:15:12.9Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/43d4baca876f5ae1b45897ec30a59801a2da37f16da1fcd85f9555e4c125/xgrammar-0.1.33-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c803e60d791854c5d1f271ece7e1f34d73c82dd4a8b2a06b7af5331482a78ac", size = 42133168, upload-time = "2026-03-27T10:15:16.994Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a8/672833a3cff027253793aa999401d8364896ebf396967e475c7a878b895f/xgrammar-0.1.33-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b8eaa533282a0efb0835db6998ae72e7b3c7875d7a52e360ffebff9b78c30a", size = 42205803, upload-time = "2026-03-27T10:15:21.599Z" }, + { url = "https://files.pythonhosted.org/packages/04/38/3fd9f21b101871b4b7f86ee2e15fe6d0cb61a3753f18b391bdee22c74810/xgrammar-0.1.33-cp312-cp312-win_amd64.whl", hash = "sha256:94fea66b41feb28be7e91f95f078986cbc850f42f7adb2d8987634eadf1fb94b", size = 7222161, upload-time = "2026-03-27T10:15:24.636Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/cce9f6d12b9de0db8b86401ea739fe79ac555f3da56e47faa5b874d41e42/xgrammar-0.1.33-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5b46b922fb04fd1848198da5273ddc20f16693fba5871bac1837f1c90f59584", size = 22702353, upload-time = "2026-03-27T10:15:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/4d186d4065f645a051be992919c51aaf96cfa8a32f7ecc8512a6e41f969f/xgrammar-0.1.33-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eec984a20fd54d4c79536d99e2515bac54bd4e1380162fa047f5ff45bdf6d8", size = 42133430, upload-time = "2026-03-27T10:15:31.409Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ca/db765035b3bb1854bdb833c118e0f09dacc623ce5e867466d63610d635fa/xgrammar-0.1.33-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d705f62d91a3675997a81d09aa371c375d7793ce1021aff7b7ed5a92021c7379", size = 42206830, upload-time = "2026-03-27T10:15:35.574Z" }, + { url = "https://files.pythonhosted.org/packages/f5/17/635fc8933b35f24d0749fe177209abb5b526c99a2d098abb71c0e601f356/xgrammar-0.1.33-cp313-cp313-win_amd64.whl", hash = "sha256:2c626de8f503858efa28cab099cbb1719c4926af4250e8dea8efddfa2c6b6c91", size = 7222102, upload-time = "2026-03-27T10:15:38.617Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/ec456e86c2d3f1c01addac8123765ee3b77c1aa1c62298f0f0fdf57d5499/xgrammar-0.1.33-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:59b9c3bb8cc0c5c82155cdc4ff0fd9f7b82348346d9e11307d5df2d3cc81eb3b", size = 22765922, upload-time = "2026-03-27T10:15:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/ac/24/8afe2a6dd42f1d1ac2f702c9fdc757e724f4af4c6eae5508e063acfb70a9/xgrammar-0.1.33-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a55912a1d7dcd3d2134617083c28cffe97ba4eaafff39bd41ca28a5ffda66073", size = 22702579, upload-time = "2026-03-27T10:15:45.14Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/8fbd675aa49b180d0912aeb90fa72dca9bb1f476724f76d3097561cca161/xgrammar-0.1.33-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bba458ffe06b3774be3a24eaf58dc217eec3a781ba41340c2eecf76aa9347aa3", size = 42133038, upload-time = "2026-03-27T10:15:48.98Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c5/64558fd11130624267f788be5d665f898f627b87c6916b523c6e0d4cebf9/xgrammar-0.1.33-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:776a15eaadda463987fba97d8a07b60c262c96353d800fc8639efedb57b7cbbb", size = 42206382, upload-time = "2026-03-27T10:15:53.458Z" }, + { url = "https://files.pythonhosted.org/packages/9c/37/921445f60d8e8cba7caa006d7583e8a059b07e6d7eb5c9dc150c2219415a/xgrammar-0.1.33-cp314-cp314-win_amd64.whl", hash = "sha256:247dec78e11f5c361f7f3f2bc571574c118fc88045bf13fc792b6432fc32bc68", size = 7312992, upload-time = "2026-03-27T10:15:56.274Z" }, + { url = "https://files.pythonhosted.org/packages/a2/31/fb51cd12733e53c3832f6cfadf5f958414102c80c477beb8b233bd4324f4/xgrammar-0.1.33-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:915451b9c86c840e44500782696650ef5535eab87662d748d649f0569d84c322", size = 22768311, upload-time = "2026-03-27T10:15:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1e/f3de2066ac889a8b4f4589b617364a07a46aea40097c8e0abd62b61f72a4/xgrammar-0.1.33-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c98e2c89bb5efa31e5f3648159dc3477dde51bbf3da8138d6b6fdc49c8fddd44", size = 22705773, upload-time = "2026-03-27T10:16:02.918Z" }, + { url = "https://files.pythonhosted.org/packages/70/fb/523113e066b74428b843e66baed815671faa1dd366a2967b687498aa8cba/xgrammar-0.1.33-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51e52aef50c2d91122a23ce67f7b187fc6caffa620b7412fd3a5eebb00a29377", size = 42134611, upload-time = "2026-03-27T10:16:07.459Z" }, + { url = "https://files.pythonhosted.org/packages/c5/07/6ea6bf8efff3c29c07f594f1e8665dc3ed43abdad86a6a27da9a3ddcbbef/xgrammar-0.1.33-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cbb91580da8ac6c86de0464339c1ca1899fb0032d604175bdb384c1a13b9b7", size = 42197758, upload-time = "2026-03-27T10:16:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/e3/05/813842e384723c636ad61b6902117dd689d8c04a8d34e0da91b35fbb9f8b/xgrammar-0.1.33-cp314-cp314t-win_amd64.whl", hash = "sha256:ae8877dc35cbdf07b23ce5584e093cca36ed72ad35798d4ce6ed858cd3b19fea", size = 7317563, upload-time = "2026-03-27T10:16:15.774Z" }, +] + +[[package]] +name = "xmltodict" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zarr" +version = "2.18.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version < '3.11' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "asciitree", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "fasteners", marker = "(python_full_version < '3.11' and sys_platform != 'emscripten') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (python_full_version >= '3.11' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (sys_platform == 'emscripten' and extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numcodecs", version = "0.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/c4/187a21ce7cf7c8f00c060dd0e04c2a81139bb7b1ab178bba83f2e1134ce2/zarr-2.18.3.tar.gz", hash = "sha256:2580d8cb6dd84621771a10d31c4d777dca8a27706a1a89b29f42d2d37e2df5ce", size = 3603224, upload-time = "2024-09-04T23:20:16.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/142095e654c2b97133ff71df60979422717b29738b08bc8a1709a5d5e0d0/zarr-2.18.3-py3-none-any.whl", hash = "sha256:b1f7dfd2496f436745cdd4c7bcf8d3b4bc1dceef5fdd0d589c87130d842496dd", size = 210723, upload-time = "2024-09-04T23:20:14.491Z" }, +] + +[[package]] +name = "zarr" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version >= '3.14' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.13.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", + "python_full_version == '3.11.*' and sys_platform == 'darwin' and extra != 'group-7-cosmos3-cu128' and extra != 'group-7-cosmos3-cu128-train' and extra != 'group-7-cosmos3-cu130' and extra != 'group-7-cosmos3-cu130-train' and extra != 'group-7-cosmos3-vllm'", +] +dependencies = [ + { name = "donfig", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "google-crc32c", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numcodecs", version = "0.16.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu128-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu128-train' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-cu130-train') or (extra == 'group-7-cosmos3-cu130' and extra == 'group-7-cosmos3-vllm') or (extra == 'group-7-cosmos3-cu130-train' and extra == 'group-7-cosmos3-vllm')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/76/7fa87f57c112c7b9c82f0a730f8b6f333e792574812872e2cd45ab604199/zarr-3.1.5.tar.gz", hash = "sha256:fbe0c79675a40c996de7ca08e80a1c0a20537bd4a9f43418b6d101395c0bba2b", size = 366825, upload-time = "2025-11-21T14:06:01.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl", hash = "sha256:29cd905afb6235b94c09decda4258c888fcb79bb6c862ef7c0b8fe009b5c8563", size = 284067, upload-time = "2025-11-21T14:05:59.235Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/vllm-cosmos3/README.md b/vllm-cosmos3/README.md new file mode 100644 index 00000000..fa1101e2 --- /dev/null +++ b/vllm-cosmos3/README.md @@ -0,0 +1,14 @@ +# Cosmos3 vLLM Plugin + +See [Cosmos-Reason2](https://github.com/nvidia-cosmos/cosmos-reason2) for inference examples. + +```shell +VLLM_USE_DEEP_GEMM=0 uvx --torch-backend=auto --with-editable ./vllm-cosmos3 vllm@latest serve nvidia/Cosmos3-Nano-Internal \ + --revision spectralflight/vllm-shim \ + --trust-remote-code \ + --allowed-local-media-path "$(pwd)" \ + --max-model-len 16384 \ + --media-io-kwargs '{"video": {"num_frames": -1}}' \ + --reasoning-parser qwen3 \ + --port 8000 +``` diff --git a/vllm-cosmos3/pyproject.toml b/vllm-cosmos3/pyproject.toml new file mode 100644 index 00000000..0a98a896 --- /dev/null +++ b/vllm-cosmos3/pyproject.toml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "vllm-cosmos3" +version = "0.1.0" +description = "vLLM plugin that loads Cosmos3 checkpoints" +requires-python = ">=3.10" +dependencies = [] + +[project.entry-points."vllm.general_plugins"] +register_cosmos3 = "vllm_cosmos3:register" diff --git a/vllm-cosmos3/uv.lock b/vllm-cosmos3/uv.lock new file mode 100644 index 00000000..eaa8b513 --- /dev/null +++ b/vllm-cosmos3/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "vllm-cosmos3" +version = "0.1.0" +source = { editable = "." } diff --git a/vllm-cosmos3/vllm_cosmos3/__init__.py b/vllm-cosmos3/vllm_cosmos3/__init__.py new file mode 100644 index 00000000..9d6cfd6c --- /dev/null +++ b/vllm-cosmos3/vllm_cosmos3/__init__.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""vLLM plugin: register Cosmos3 models.""" +# See https://docs.vllm.ai/en/latest/design/plugin_system + + +def register(): + from vllm import ModelRegistry + + arch = "Cosmos3ForConditionalGeneration" + if arch not in ModelRegistry.get_supported_archs(): + print(f"Registering architecture {arch}") + ModelRegistry.register_model( + arch, + "vllm_cosmos3.model:Cosmos3ForConditionalGeneration", + ) diff --git a/vllm-cosmos3/vllm_cosmos3/model.py b/vllm-cosmos3/vllm_cosmos3/model.py new file mode 100644 index 00000000..1e13fe0a --- /dev/null +++ b/vllm-cosmos3/vllm_cosmos3/model.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Subclass of Qwen3VLForConditionalGeneration that strips a configurable +prefix from checkpoint weight names before loading. + +The prefix is read from the env var COSMOS3_CKPT_PREFIX (e.g. "my_wrapper."). +""" + +import os +from collections.abc import Iterable + +import torch +from vllm.model_executor.models.qwen3_vl import Qwen3VLForConditionalGeneration + +_COSMOS3_DROP_TOKENS: tuple[str, ...] = ( + "_moe_gen", + "llm2vae", + "vae2llm", + "time_embedder", +) + + +def _is_und_tower_weight(name: str) -> bool: + """Return True if `name` belongs to the understanding (text) tower.""" + return not any(token in name for token in _COSMOS3_DROP_TOKENS) + + +def _to_qwen3vl_hf_name(name: str) -> str: + """Wrap flat Qwen3 keys into the nested HF Qwen3-VL namespace. + + Cosmos3's understanding tower is saved with the standalone Qwen3 layout + (``model.layers.*``, ``model.embed_tokens.*``, ``model.norm.*``, + ``lm_head.*``), but vLLM's ``Qwen3VLForConditionalGeneration`` expects HF + Qwen3-VL naming (``model.language_model.layers.*`` etc.). Its + ``hf_to_vllm_mapper`` then rewrites ``model.language_model.`` to + ``language_model.model.`` for the runtime module tree. + """ + if name.startswith("model.") and not name.startswith("model.language_model."): + return "model.language_model." + name[len("model.") :] + return name + + +class Cosmos3ForConditionalGeneration(Qwen3VLForConditionalGeneration): + def __init__(self, *, vllm_config, prefix: str = "") -> None: + super().__init__(vllm_config=vllm_config, prefix=prefix) + overrides = getattr(vllm_config.model_config.hf_config, "allow_patterns_overrides", None) + if overrides: + self.allow_patterns_overrides = list(overrides) + if any(p.endswith(".safetensors") for p in self.allow_patterns_overrides): + vllm_config.load_config.load_format = "safetensors" + + def load_weights( + self, + weights: Iterable[tuple[str, torch.Tensor]], + ) -> set[str]: + prefix = os.environ.get("COSMOS3_CKPT_PREFIX", "") + if prefix: + prefix = prefix.rstrip(".") + "." + + def _iter() -> Iterable[tuple[str, torch.Tensor]]: + for name, tensor in weights: + stripped = name.removeprefix(prefix) if prefix else name + if _is_und_tower_weight(stripped): + yield _to_qwen3vl_hf_name(stripped), tensor + + loaded = super().load_weights(_iter()) + + # The cosmos3 checkpoint has no `visual.*` weights — they don't exist + # in the OmniMoT understanding tower. Treat them as intentionally + # uninitialized so vLLM 0.19+ doesn't hard-error on missing weights. + # This means the ViT runs with random init; safe iff prompts are + # text-only (the vision pathway is then never invoked). + for name, _ in self.named_parameters(): + if name.startswith("visual."): + loaded.add(name) + return loaded From 4fc15a51ea173f14d12efccc73689aa60f72a16e Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Mon, 11 May 2026 21:01:18 -0700 Subject: [PATCH 02/24] Merge Cosmos3 README content with deprecated-repo notice Combine the Cosmos3 documentation (user guide, overview, setup, inference, models, modalities, CLI reference) from cosmos3-internal with the original notice pointing to the archived-ces2025 branch and the nvidia-cosmos GitHub organization. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 138 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3c833802..698335f8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,143 @@

-NVIDIA Cosmos Logo

-
-**This repository has been deprecated and is no longer maintained.** To view the initial release of NVIDIA Cosmos from this repository, please check out branch `archived-ces2025`. +

🤗 Hugging Face | Paper Draft

+ +> **Note:** The initial CES2025 release of NVIDIA Cosmos lives on the [`archived-ces2025`](https://github.com/NVIDIA/Cosmos/tree/archived-ces2025) branch. For other NVIDIA Cosmos projects, visit the [NVIDIA Cosmos](https://github.com/nvidia-cosmos) GitHub organization. + +# Cosmos3 + +## User Guide + +- [Gallery](./docs/gallery.md) +- [Quickstart](#setup) +- [Setup](./docs/setup.md): Installation, environment, checkpoints +- [Prompting](./docs/prompting.md): Prompt engineering, upsampling +- [Inference](./docs/inference.md): Sample arguments, default values, offline batch inference + - [Online Inference](./docs/inference_online.md): Online serving, web UI + - [Action Policy Closed-Loop Evaluation on LIBERO](./docs/action_policy_closed_loop_eval.md): Action policy server/client setup and LIBERO evaluation. The client setup clones and installs the external [Lifelong-Robot-Learning/LIBERO](https://github.com/Lifelong-Robot-Learning/LIBERO) simulator package. +- [Training](./docs/training.md): Post-Training (Supervised Fine-Tuning) +- [FAQ](./docs/faq.md): FAQ, tips, troubleshooting +- [AGENTS.md](./AGENTS.md): AI agent entry point — start here for quick codebase orientation + +## Overview + +**Cosmos3** is a world foundation model that unifies understanding and generation within a single Mixture-of-Transformer (MoT) architecture. Two tightly coupled towers—a **Reasoner** (vision-language model) and a **Generator** (world simulator)—share latent representations so that structured perception directly grounds realistic, temporally consistent simulation. + +

Image

+ +One model, many capabilities: + +| Input Modality | Output Modality | Application | EA1 | +| ----------------------- | --------------- | --------------------- | ------------ | +| Video \| Text | Video | Video Generator | ✅ | +| Video \| Text | Text | Vision Language Model | Coming soon! | +| Action \| Video \| Text | Video | World Model | ✅ | +| Video \| Text | Video & Action | Policy Model | ✅ | + +## Supported Features (Cosmos3 EA1 — Robotics Backbone) + +### User Stories + +- **Video Backbone**: Evaluate and benchmark the model’s task understanding and review its architecture to inform codebase decisions. + +### Modalities Supported + +- Text2Image (t2i), Text2Video (t2v), Image2Video (i2v) + +### Base Model Specifications + +| Spec | Value | +| ---------------- | -------------------------------------------------------------------------- | +| Model Size | Nano, Super | +| Resolution | 256p / 480p / 720p | +| Frame Rate (FPS) | 10–30 | +| Num of Frames | Default: 189 (max by resolution: `256p → 400`, `480p → 300`, `720p → 200`) | +| Max Duration | Variable | +| View | Single view only | + +## Setup + +For more details and alternative installation methods, see [Setup](./docs/setup.md#installation). + +Install system dependencies: + +```shell +sudo apt-get update && sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget +``` + +Install the package with `uv`: + +```shell +uv sync --all-extras --group=cu130-train +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +## Prompting + +For prompting guidance, see [Prompting](./docs/prompting.md). + +## Inference + +For more details, see [Inference](./docs/inference.md). + +### Offline Batch Inference + +Generate a single sample with 1 GPU: + +```shell +python -m cosmos3.scripts.inference \ + --parallelism-preset=latency \ + -i "inputs/omni/t2v.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +Generate multiple samples with 8 GPUs (~5 mins): + +```shell +torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ + --parallelism-preset=throughput \ + -i "inputs/omni/*.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +**Note:** The progress bar only prints on rank 0. + +### Models + +| Model | Arguments | +| ------------- | --------------------------------- | +| Cosmos3-Nano | `--checkpoint-path=Cosmos3-Nano` | +| Cosmos3-Super | `--checkpoint-path=Cosmos3-Super` | + +### Modalities + +| Modality | Example | +| ---------------- | -------------------------------------------------------------------------------------------------- | +| Text2Image | [`-i "inputs/omni/t2i.json"`](inputs/omni/t2i.json) | +| Text2Video | [`-i "inputs/omni/t2v.json"`](inputs/omni/t2v.json) | +| Image2Video | [`-i "inputs/omni/i2v.json"`](inputs/omni/i2v.json) | +| Forward Dynamics | [`-i "inputs/omni/action_forward_dynamics*.json"`](inputs/omni/action_forward_dynamics_robot.json) | +| Inverse Dynamics | [`-i "inputs/omni/action_inverse_dynamics*.json"`](inputs/omni/action_inverse_dynamics_av.json) | +| Policy | [`-i "inputs/omni/action_policy*.json"`](inputs/omni/action_policy_robot.json) | + +To generate all examples, use `-i "inputs/omni/*.json"`. + +For more information regarding action inference, please see [Action Inference](./docs/inference.md#action-inference). + +### CLI Reference + +To see all available arguments: + +```shell +python -m cosmos3.scripts.inference --help +``` From 0858cbc053ef6ff20eb62ac002ef6fde98e1e7a8 Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Mon, 11 May 2026 21:31:07 -0700 Subject: [PATCH 03/24] Restructure: rename cosmos3 to cosmos-inference, add cosmos/ skeleton Rename the existing cosmos3 package directory to cosmos-inference and introduce a new top-level cosmos/ package skeleton matching the planned framework layout: cosmos/ model/, inference/, data/, trainer/ algorithm/{loss,reward,rl}/ controller/, workers/{simulations,rollout,reference,reward}/ communicator/, checkpoint/, callbacks/, tools/ utils/, evaluation/, launcher/ Also add root-level tests/ and tools/ placeholders. Each new Python subpackage has an empty __init__.py; tests/ and tools/ use .gitkeep. Note: imports inside cosmos-inference/ still reference `cosmos3.*` and will need updating in a follow-up; pyproject.toml package config is also unchanged for now. Co-Authored-By: Claude Opus 4.7 (1M context) --- {cosmos3 => cosmos-inference}/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/attention/README.md | 0 .../_src/imaginaire/attention/__init__.py | 0 .../_src/imaginaire/attention/backends.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/attention/checks.py | 0 .../_src/imaginaire/attention/docs/README.md | 0 .../_src/imaginaire/attention/docs/apis.md | 0 .../_src/imaginaire/attention/docs/backends.md | 0 .../_src/imaginaire/attention/docs/features.md | 0 .../_src/imaginaire/attention/docs/multi-dim.md | 0 .../_src/imaginaire/attention/flash2/__init__.py | 0 .../_src/imaginaire/attention/flash2/checks.py | 0 .../_src/imaginaire/attention/flash2/functions.py | 0 .../_src/imaginaire/attention/flash2/meta.py | 0 .../_src/imaginaire/attention/flash2/stubs.py | 0 .../_src/imaginaire/attention/flash3/__init__.py | 0 .../_src/imaginaire/attention/flash3/checks.py | 0 .../_src/imaginaire/attention/flash3/functions.py | 0 .../_src/imaginaire/attention/flash3/meta.py | 0 .../_src/imaginaire/attention/flash3/stubs.py | 0 .../_src/imaginaire/attention/frontend.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/attention/masks.py | 0 .../_src/imaginaire/attention/natten/__init__.py | 0 .../_src/imaginaire/attention/natten/checks.py | 0 .../_src/imaginaire/attention/natten/functions.py | 0 .../_src/imaginaire/attention/natten/meta.py | 0 .../_src/imaginaire/attention/natten/stubs.py | 0 .../_src/imaginaire/attention/utils/__init__.py | 0 .../_src/imaginaire/attention/utils/determinism.py | 0 .../_src/imaginaire/attention/utils/environment.py | 0 .../_src/imaginaire/attention/utils/safe_ops/__init__.py | 0 .../_src/imaginaire/attention/utils/safe_ops/functools.py | 0 .../_src/imaginaire/attention/utils/safe_ops/log.py | 0 .../_src/imaginaire/attention/utils/version.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/attention/varlen.py | 0 .../_src/imaginaire/auxiliary/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py | 0 .../imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py | 0 .../_src/imaginaire/auxiliary/guardrail/blocklist/utils.py | 0 .../_src/imaginaire/auxiliary/guardrail/common/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/common/core.py | 0 .../_src/imaginaire/auxiliary/guardrail/common/io_utils.py | 0 .../_src/imaginaire/auxiliary/guardrail/common/presets.py | 0 .../imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py | 0 .../imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py | 0 .../auxiliary/guardrail/face_blur_filter/face_blur_filter.py | 0 .../auxiliary/guardrail/face_blur_filter/retinaface_utils.py | 0 .../_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py | 0 .../imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py | 0 .../_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py | 0 .../_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py | 0 .../_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py | 0 .../auxiliary/guardrail/video_content_safety_filter/__init__.py | 0 .../auxiliary/guardrail/video_content_safety_filter/model.py | 0 .../video_content_safety_filter/video_content_safety_filter.py | 0 .../guardrail/video_content_safety_filter/vision_encoder.py | 0 .../_src/imaginaire/callbacks/__init__.py | 0 .../_src/imaginaire/callbacks/every_n.py | 0 .../_src/imaginaire/callbacks/image_grad_clip.py | 0 .../_src/imaginaire/callbacks/manual_gc.py | 0 .../_src/imaginaire/checkpointer/__init__.py | 0 .../_src/imaginaire/checkpointer/base.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/ddp.py | 0 .../_src/imaginaire/checkpointer/dummy.py | 0 .../_src/imaginaire/checkpointer/s3_filesystem.py | 0 .../_src/imaginaire/checkpointer/safe_broadcast.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/tp.py | 0 .../_src/imaginaire/checkpointer/tp_ema.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/config.py | 0 .../_src/imaginaire/configs/lr_scheduler.py | 0 .../_src/imaginaire/datasets/__init__.py | 0 .../_src/imaginaire/datasets/augmentors/merge_datadict.py | 0 .../_src/imaginaire/datasets/augmentors/v3_text_transforms.py | 0 .../_src/imaginaire/datasets/decoders/__init__.py | 0 .../_src/imaginaire/datasets/decoders/json_loader.py | 0 .../_src/imaginaire/datasets/decoders/pkl_loader.py | 0 .../_src/imaginaire/datasets/decoders/video_decoder.py | 0 .../_src/imaginaire/datasets/joint_training.py | 0 .../_src/imaginaire/datasets/mock_dataset.py | 0 .../_src/imaginaire/datasets/webdataset/__init__.py | 0 .../_src/imaginaire/datasets/webdataset/augmentors/augmentor.py | 0 .../imaginaire/datasets/webdataset/augmentors/geometry/camera.py | 0 .../imaginaire/datasets/webdataset/augmentors/geometry/depth.py | 0 .../datasets/webdataset/augmentors/geometry/pointcloud.py | 0 .../imaginaire/datasets/webdataset/augmentors/image/__init__.py | 0 .../imaginaire/datasets/webdataset/augmentors/image/cropping.py | 0 .../_src/imaginaire/datasets/webdataset/augmentors/image/flip.py | 0 .../_src/imaginaire/datasets/webdataset/augmentors/image/misc.py | 0 .../imaginaire/datasets/webdataset/augmentors/image/normalize.py | 0 .../imaginaire/datasets/webdataset/augmentors/image/padding.py | 0 .../imaginaire/datasets/webdataset/augmentors/image/resize.py | 0 .../_src/imaginaire/datasets/webdataset/config/schema.py | 0 .../_src/imaginaire/datasets/webdataset/dataloader.py | 0 .../_src/imaginaire/datasets/webdataset/decoders/__init__.py | 0 .../_src/imaginaire/datasets/webdataset/decoders/depth.py | 0 .../_src/imaginaire/datasets/webdataset/decoders/image.py | 0 .../_src/imaginaire/datasets/webdataset/decoders/pickle.py | 0 .../_src/imaginaire/datasets/webdataset/distributors/__init__.py | 0 .../_src/imaginaire/datasets/webdataset/distributors/basic.py | 0 .../datasets/webdataset/distributors/multi_aspect_ratio.py | 0 .../datasets/webdataset/distributors/multi_aspect_ratio_v2.py | 0 .../webdataset/distributors/weighted_multi_aspect_ratio.py | 0 .../_src/imaginaire/datasets/webdataset/utils/iterators.py | 0 .../_src/imaginaire/datasets/webdataset/utils/misc.py | 0 .../_src/imaginaire/datasets/webdataset/utils/stream.py | 0 .../webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py | 0 .../datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py | 0 .../utils/unit_test/RetryingStreamStatsOverheadBenchmark.py | 0 .../webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py | 0 .../datasets/webdataset/utils/unit_test/mpi_rank_worker.py | 0 .../_src/imaginaire/datasets/webdataset/webdataset.py | 0 .../_src/imaginaire/datasets/webdataset/webdataset_ext.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/flags.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/flops/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/flops/omni_mot.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/flops/wan_vae.py | 0 .../_src/imaginaire/functional/batch_ops.py | 0 .../_src/imaginaire/functional/lr_scheduler.py | 0 .../_src/imaginaire/functional/multi_step.py | 0 .../_src/imaginaire/functional/runge_kutta.py | 0 .../_src/imaginaire/lazy_config/__init__.py | 0 .../_src/imaginaire/lazy_config/file_io.py | 0 .../_src/imaginaire/lazy_config/instantiate.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/lazy.py | 0 .../_src/imaginaire/lazy_config/lazy_call.py | 0 .../_src/imaginaire/lazy_config/omegaconf_patch.py | 0 .../_src/imaginaire/lazy_config/registry.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/model.py | 0 .../_src/imaginaire/models/abstract_emb_model.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/modules/camera.py | 0 .../_src/imaginaire/modules/denoiser_scaling.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/modules/edm_sde.py | 0 .../_src/imaginaire/modules/image_embeddings.py | 0 .../_src/imaginaire/modules/res_sampler.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/serialization.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/trainer.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/callback.py | 0 .../_src/imaginaire/utils/checkpoint_db.py | 0 .../_src/imaginaire/utils/checkpointer.py | 0 .../_src/imaginaire/utils/cluster_env.py | 0 .../_src/imaginaire/utils/config_helper.py | 0 .../_src/imaginaire/utils/context_managers.py | 0 .../_src/imaginaire/utils/context_parallel.py | 0 .../_src/imaginaire/utils/count_params.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/dataloader.py | 0 .../_src/imaginaire/utils/dataset_utils.py | 0 .../_src/imaginaire/utils/denoise_prediction.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/device.py | 0 .../_src/imaginaire/utils/disabled_train.py | 0 .../_src/imaginaire/utils/distributed.py | 0 .../_src/imaginaire/utils/easy_io/__init__.py | 0 .../_src/imaginaire/utils/easy_io/backends/__init__.py | 0 .../_src/imaginaire/utils/easy_io/backends/auto_auth.py | 0 .../_src/imaginaire/utils/easy_io/backends/base_backend.py | 0 .../_src/imaginaire/utils/easy_io/backends/boto3_backend.py | 0 .../_src/imaginaire/utils/easy_io/backends/boto3_client.py | 0 .../_src/imaginaire/utils/easy_io/backends/http_backend.py | 0 .../_src/imaginaire/utils/easy_io/backends/local_backend.py | 0 .../_src/imaginaire/utils/easy_io/backends/msc_backend.py | 0 .../_src/imaginaire/utils/easy_io/backends/registry_utils.py | 0 .../_src/imaginaire/utils/easy_io/easy_io.py | 0 .../_src/imaginaire/utils/easy_io/file_client.py | 0 .../_src/imaginaire/utils/easy_io/handlers/__init__.py | 0 .../_src/imaginaire/utils/easy_io/handlers/base.py | 0 .../_src/imaginaire/utils/easy_io/handlers/byte_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/csv_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/gzip_handler.py | 0 .../imaginaire/utils/easy_io/handlers/imageio_video_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/json_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/np_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/pandas_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/pickle_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/pil_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/registry_utils.py | 0 .../_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/torch_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/txt_handler.py | 0 .../_src/imaginaire/utils/easy_io/handlers/yaml_handler.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/ema.py | 0 .../_src/imaginaire/utils/embedding_concat_strategy.py | 0 .../_src/imaginaire/utils/env_parsers/cred_env_parser.py | 0 .../_src/imaginaire/utils/env_parsers/customization_env_parser.py | 0 .../_src/imaginaire/utils/env_parsers/env_parser.py | 0 .../_src/imaginaire/utils/env_parsers/inference_env_parser.py | 0 .../_src/imaginaire/utils/fsdp_helper.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/fused_adam.py | 0 .../_src/imaginaire/utils/fused_nan_to_num.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/graph.py | 0 .../_src/imaginaire/utils/high_sigma_strategy.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/launch.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/log.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/misc.py | 0 .../_src/imaginaire/utils/nsys_wrapper.sh | 0 .../_src/imaginaire/utils/object_store.py | 0 .../_src/imaginaire/utils/optim_instantiate.py | 0 .../_src/imaginaire/utils/parallel_state_helper.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/primitives.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/profiling.py | 0 .../_src/imaginaire/utils/progress_bar.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/registry.py | 0 .../_src/imaginaire/utils/replace_bg_color.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/s3_utils.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/scheduler.py | 0 .../_src/imaginaire/utils/submit_job_helper.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/timer.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/tone_curve.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/training.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/validator.py | 0 .../_src/imaginaire/utils/validator_params.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/utils/wandb_util.py | 0 .../_src/imaginaire/visualize/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/visualize/img.py | 0 {cosmos3 => cosmos-inference}/_src/imaginaire/visualize/video.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/__init__.py | 0 .../_src/vfm/callbacks/compile_tokenizer.py | 0 .../_src/vfm/callbacks/dataloader_state.py | 0 .../_src/vfm/callbacks/dataloading_monitor.py | 0 .../_src/vfm/callbacks/device_monitor.py | 0 .../_src/vfm/callbacks/every_n_draw_audio_video_sample.py | 0 .../_src/vfm/callbacks/every_n_draw_sample.py | 0 .../_src/vfm/callbacks/every_n_dur_fps_draw_sample.py | 0 .../_src/vfm/callbacks/expert_heatmap.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/generation.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/grad_clip.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/heart_beat.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/hf_export.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/iter_speed.py | 0 .../_src/vfm/callbacks/load_pretrained.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/low_precision.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/mfu.py | 0 .../_src/vfm/callbacks/moe_specialization_callback.py | 0 .../_src/vfm/callbacks/moe_stability_callback.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/norm_monitor.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/ofu.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/param_count.py | 0 .../_src/vfm/callbacks/sequence_packing_padding.py | 0 .../_src/vfm/callbacks/sigma_loss_analysis.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/skip_nan_step.py | 0 .../_src/vfm/callbacks/training_stats.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/vlm/grad_clip.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/callbacks/wandb_log.py | 0 .../_src/vfm/callbacks/wandb_log_eval.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/checkpointer/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/checkpointer/dcp.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/conditioner.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/configs/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/configs/base/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/configs/base/config.py | 0 .../_src/vfm/configs/base/defaults/__init__.py | 0 .../_src/vfm/configs/base/defaults/callbacks.py | 0 .../_src/vfm/configs/base/defaults/checkpointer.py | 0 .../_src/vfm/configs/base/defaults/cluster.py | 0 .../_src/vfm/configs/base/defaults/conditioner.py | 0 .../_src/vfm/configs/base/defaults/ema.py | 0 .../_src/vfm/configs/base/defaults/model.py | 0 .../_src/vfm/configs/base/defaults/model_config.py | 0 .../_src/vfm/configs/base/defaults/multiview_dataloader.py | 0 .../_src/vfm/configs/base/defaults/optimizer.py | 0 .../_src/vfm/configs/base/defaults/tokenizer.py | 0 .../_src/vfm/configs/base/defaults/unittest.py | 0 .../_src/vfm/configs/base/defaults/vlm.py | 0 .../_src/vfm/configs/base/vlm/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/config.py | 0 .../_src/vfm/configs/base/vlm/defaults/__init__.py | 0 .../_src/vfm/configs/base/vlm/defaults/callbacks.py | 0 .../_src/vfm/configs/base/vlm/defaults/checkpointer.py | 0 .../_src/vfm/configs/base/vlm/defaults/config.py | 0 .../_src/vfm/configs/base/vlm/defaults/dataloader.py | 0 .../_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py | 0 .../_src/vfm/configs/base/vlm/defaults/model.py | 0 .../_src/vfm/configs/base/vlm/defaults/optimizer.py | 0 .../_src/vfm/configs/base/vlm/defaults/training.py | 0 .../_src/vfm/configs/base/vlm/defaults/vlm_policy.py | 0 .../_src/vfm/configs/base/vlm/experiment/__init__.py | 0 .../configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py | 0 .../_src/vfm/configs/base/vlm/experiment/pre_exp01x.py | 0 .../_src/vfm/configs/base/vlm/experiment/utils.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/datasets/__init__.py | 0 .../_src/vfm/datasets/action/action_normalization.py | 0 .../_src/vfm/datasets/action/action_spec.py | 0 .../_src/vfm/datasets/action/dataloaders.py | 0 .../_src/vfm/datasets/action/domain_utils.py | 0 .../_src/vfm/datasets/action/json_formatter.py | 0 .../_src/vfm/datasets/action/libero_dataset.py | 0 .../_src/vfm/datasets/action/libero_pose_utils.py | 0 .../normalizers/libero_native_frame_wise_relative_rot6d.json | 0 .../_src/vfm/datasets/action/pose_utils.py | 0 .../_src/vfm/datasets/action/transforms.py | 0 .../_src/vfm/datasets/action/unified_dataset.py | 0 .../_src/vfm/datasets/action/viewpoint_utils.py | 0 .../_src/vfm/datasets/augmentors/__init__.py | 0 .../_src/vfm/datasets/augmentors/append_fps_frames_for_image.py | 0 .../_src/vfm/datasets/augmentors/audio_caption.py | 0 .../_src/vfm/datasets/augmentors/audio_parsing.py | 0 .../_src/vfm/datasets/augmentors/caption_filter.py | 0 .../_src/vfm/datasets/augmentors/cropping.py | 0 .../_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py | 0 .../_src/vfm/datasets/augmentors/idle_frames_text_info.py | 0 .../_src/vfm/datasets/augmentors/image_editing_transform.py | 0 .../_src/vfm/datasets/augmentors/image_resolution_filter.py | 0 .../_src/vfm/datasets/augmentors/interleaved_image_transform.py | 0 .../_src/vfm/datasets/augmentors/interleaved_video_parsing.py | 0 .../_src/vfm/datasets/augmentors/merge_datadict.py | 0 .../_src/vfm/datasets/augmentors/pkl_to_media.py | 0 .../_src/vfm/datasets/augmentors/resolution_text_info.py | 0 .../_src/vfm/datasets/augmentors/sequence_plan.py | 0 .../_src/vfm/datasets/augmentors/sound_sequence_plan.py | 0 .../_src/vfm/datasets/augmentors/text_tokenizer.py | 0 .../_src/vfm/datasets/augmentors/text_transforms_for_image.py | 0 .../_src/vfm/datasets/augmentors/text_transforms_for_video.py | 0 .../vfm/datasets/augmentors/transfer_control_input/__init__.py | 0 .../_src/vfm/datasets/augmentors/transfer_control_input/blur.py | 0 .../datasets/augmentors/transfer_control_input/control_input.py | 0 .../vfm/datasets/augmentors/transfer_control_input/fast_blur.py | 0 .../_src/vfm/datasets/augmentors/transfer_control_input/seg.py | 0 .../_src/vfm/datasets/augmentors/transfer_control_transform.py | 0 .../_src/vfm/datasets/augmentors/video_parsing.py | 0 .../_src/vfm/datasets/joint_dataloader.py | 0 .../_src/vfm/datasets/local_datasets/__init__.py | 0 .../_src/vfm/datasets/local_datasets/helper.py | 0 .../_src/vfm/datasets/local_datasets/sft_dataset.py | 0 .../_src/vfm/datasets/sequence_packing.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/datasets/utils.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/diffusion/__init__.py | 0 .../_src/vfm/diffusion/rectified_flow.py | 0 .../_src/vfm/diffusion/samplers/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/edm.py | 0 .../_src/vfm/diffusion/samplers/fixed_step.py | 0 .../_src/vfm/diffusion/samplers/fm_solvers_unipc.py | 0 .../_src/vfm/diffusion/samplers/unipc.py | 0 .../_src/vfm/diffusion/samplers/utils.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/evaluation/__init__.py | 0 .../_src/vfm/evaluation/action/__init__.py | 0 .../_src/vfm/evaluation/action/libero/__init__.py | 0 .../_src/vfm/evaluation/action/libero/closed_loop_eval.py | 0 .../vfm/evaluation/action/libero/dataset_reply_action_server.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/hf_model.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/llm/__init__.py | 0 .../_src/vfm/models/llm/qwen3/__init__.py | 0 .../_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json | 0 .../_src/vfm/models/llm/qwen3/configs/__init__.py | 0 .../_src/vfm/models/llm/qwen3/configuration_qwen3.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/qwen3.py | 0 .../_src/vfm/models/llm/qwen3/test_qwen3.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/mot/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/mot/attention.py | 0 .../_src/vfm/models/mot/context_parallel_utils.py | 0 .../_src/vfm/models/mot/cosmos3_vfm_network.py | 0 .../_src/vfm/models/mot/domain_aware_linear.py | 0 .../_src/vfm/models/mot/dot_product_attention.py | 0 .../_src/vfm/models/mot/modeling_utils.py | 0 .../_src/vfm/models/mot/parallelize_unified_mot.py | 0 .../_src/vfm/models/mot/parallelize_vfm_network.py | 0 .../_src/vfm/models/mot/qwen3_vl_unified_mot.py | 0 .../_src/vfm/models/mot/unified_3dmrope_utils.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/mot/unified_mot.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/omni_mot_model.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/parallelize_vlm.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/utils/__init__.py | 0 .../_src/vfm/models/utils/data_and_condition.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/utils/dcp_loader.py | 0 .../_src/vfm/models/utils/load_balancing_loss.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/utils/memory.py | 0 .../_src/vfm/models/utils/safetensors_loader.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/utils/taylorseer.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/__init__.py | 0 .../_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py | 0 .../vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json | 0 .../vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py | 0 .../vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/__init__.py | 0 .../vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json | 0 .../vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json | 0 .../vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json | 0 .../vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json | 0 .../_src/vfm/models/vlm/qwen3_vl/configs/__init__.py | 0 .../_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/utils.py | 0 .../_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/__init__.py | 0 .../vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json | 0 .../vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py | 0 .../vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/moe.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/models/vlm_model.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/interface.py | 0 .../_src/vfm/tokenizers/tokenization_qwen2.py | 0 .../_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/context_parallel.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/data_utils.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/dtensor_helper.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/flash_attn.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/fused_adam.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/loss.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/misc.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/model_loader.py | 0 .../_src/vfm/utils/model_weights_stats.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/monkey_patch.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/optimizer.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/parallelism.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/rand_state.py | 0 .../_src/vfm/utils/tokenizer_benchmarking.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/vlm/__init__.py | 0 {cosmos3 => cosmos-inference}/_src/vfm/utils/vlm/optimizer.py | 0 .../_src/vfm/utils/vlm/pretrained_models_downloader.py | 0 {cosmos3 => cosmos-inference}/_test.py | 0 {cosmos3 => cosmos-inference}/_test/autoregressive.sh | 0 {cosmos3 => cosmos-inference}/_test/distilled.sh | 0 {cosmos3 => cosmos-inference}/_test/latency.sh | 0 {cosmos3 => cosmos-inference}/_test/omni-super.sh | 0 {cosmos3 => cosmos-inference}/_test/omni.sh | 0 {cosmos3 => cosmos-inference}/_test/omni_param.sh | 0 {cosmos3 => cosmos-inference}/_test/sft.sh | 0 {cosmos3 => cosmos-inference}/action.py | 0 {cosmos3 => cosmos-inference}/args.py | 0 {cosmos3 => cosmos-inference}/args_test.py | 0 {cosmos3 => cosmos-inference}/common/__init__.py | 0 {cosmos3 => cosmos-inference}/common/args.py | 0 {cosmos3 => cosmos-inference}/common/args_test.py | 0 {cosmos3 => cosmos-inference}/common/checkpoints.py | 0 {cosmos3 => cosmos-inference}/common/config.py | 0 {cosmos3 => cosmos-inference}/common/config_test.py | 0 {cosmos3 => cosmos-inference}/common/inference.py | 0 {cosmos3 => cosmos-inference}/common/inference_test.py | 0 {cosmos3 => cosmos-inference}/common/init.py | 0 .../configs/experiment/action_policy_sft_8b.yaml | 0 .../configs/experiment/mixed_modality_sft_8b.yaml | 0 {cosmos3 => cosmos-inference}/dataset_samples.py | 0 .../defaults/forward_dynamics/sample_args.json | 0 .../defaults/image2video/sample_args.json | 0 .../defaults/inverse_dynamics/sample_args.json | 0 {cosmos3 => cosmos-inference}/defaults/policy/sample_args.json | 0 {cosmos3 => cosmos-inference}/defaults/prompt_upsampler.txt | 0 .../defaults/text2image/sample_args.json | 0 .../defaults/text2video/sample_args.json | 0 .../defaults/video2video/sample_args.json | 0 {cosmos3 => cosmos-inference}/defaults/video_captioner.txt | 0 {cosmos3 => cosmos-inference}/fixtures/__init__.py | 0 {cosmos3 => cosmos-inference}/fixtures/args.py | 0 {cosmos3 => cosmos-inference}/fixtures/script.py | 0 {cosmos3 => cosmos-inference}/flags.py | 0 {cosmos3 => cosmos-inference}/inference.py | 0 {cosmos3 => cosmos-inference}/model.py | 0 {cosmos3 => cosmos-inference}/model_test.py | 0 {cosmos3 => cosmos-inference}/ray/__init__.py | 0 {cosmos3 => cosmos-inference}/ray/configs/latency.yaml | 0 {cosmos3 => cosmos-inference}/ray/configs/throughput.yaml | 0 {cosmos3 => cosmos-inference}/ray/gradio.py | 0 {cosmos3 => cosmos-inference}/ray/serve.py | 0 {cosmos3 => cosmos-inference}/ray/serve_test.py | 0 {cosmos3 => cosmos-inference}/ray/submit.py | 0 {cosmos3 => cosmos-inference}/scripts/__init__.py | 0 {cosmos3 => cosmos-inference}/scripts/_test.py | 0 .../scripts/_test/convert_model_to_dcp.sh | 0 .../scripts/_test/convert_model_to_diffusers.sh | 0 {cosmos3 => cosmos-inference}/scripts/_test/export_config.sh | 0 {cosmos3 => cosmos-inference}/scripts/_test/export_model.sh | 0 {cosmos3 => cosmos-inference}/scripts/action_policy_server.py | 0 {cosmos3 => cosmos-inference}/scripts/caption_from_video.py | 0 {cosmos3 => cosmos-inference}/scripts/captions_to_sft_jsonl.py | 0 {cosmos3 => cosmos-inference}/scripts/convert_model_to_dcp.py | 0 {cosmos3 => cosmos-inference}/scripts/eval_utils.py | 0 {cosmos3 => cosmos-inference}/scripts/export_config.py | 0 {cosmos3 => cosmos-inference}/scripts/export_model.py | 0 {cosmos3 => cosmos-inference}/scripts/export_schemas.py | 0 {cosmos3 => cosmos-inference}/scripts/export_schemas_test.py | 0 {cosmos3 => cosmos-inference}/scripts/hydra.py | 0 {cosmos3 => cosmos-inference}/scripts/inference.py | 0 {cosmos3 => cosmos-inference}/scripts/prefetch_hf_checkpoints.py | 0 {cosmos3 => cosmos-inference}/scripts/train.py | 0 {cosmos3 => cosmos-inference}/scripts/upsample_prompts.py | 0 {cosmos3 => cosmos-inference}/vision.py | 0 cosmos/__init__.py | 0 cosmos/algorithm/__init__.py | 0 cosmos/algorithm/loss/__init__.py | 0 cosmos/algorithm/reward/__init__.py | 0 cosmos/algorithm/rl/__init__.py | 0 cosmos/callbacks/__init__.py | 0 cosmos/checkpoint/__init__.py | 0 cosmos/communicator/__init__.py | 0 cosmos/controller/__init__.py | 0 cosmos/data/__init__.py | 0 cosmos/evaluation/__init__.py | 0 cosmos/inference/__init__.py | 0 cosmos/launcher/__init__.py | 0 cosmos/model/__init__.py | 0 cosmos/tools/__init__.py | 0 cosmos/trainer/__init__.py | 0 cosmos/utils/__init__.py | 0 cosmos/workers/__init__.py | 0 cosmos/workers/reference/__init__.py | 0 cosmos/workers/reward/__init__.py | 0 cosmos/workers/rollout/__init__.py | 0 cosmos/workers/simulations/__init__.py | 0 tests/.gitkeep | 0 tools/.gitkeep | 0 513 files changed, 0 insertions(+), 0 deletions(-) rename {cosmos3 => cosmos-inference}/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/README.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/backends.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/checks.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/docs/README.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/docs/apis.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/docs/backends.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/docs/features.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/docs/multi-dim.md (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash2/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash2/checks.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash2/functions.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash2/meta.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash2/stubs.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash3/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash3/checks.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash3/functions.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash3/meta.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/flash3/stubs.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/frontend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/masks.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/natten/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/natten/checks.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/natten/functions.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/natten/meta.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/natten/stubs.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/determinism.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/environment.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/safe_ops/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/safe_ops/functools.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/safe_ops/log.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/utils/version.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/attention/varlen.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/common/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/common/core.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/common/io_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/common/presets.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/callbacks/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/callbacks/every_n.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/callbacks/image_grad_clip.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/callbacks/manual_gc.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/base.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/ddp.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/dummy.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/s3_filesystem.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/safe_broadcast.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/tp.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/checkpointer/tp_ema.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/config.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/configs/lr_scheduler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/augmentors/merge_datadict.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/augmentors/v3_text_transforms.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/decoders/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/decoders/json_loader.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/decoders/pkl_loader.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/decoders/video_decoder.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/joint_training.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/mock_dataset.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/config/schema.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/dataloader.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/decoders/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/decoders/depth.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/decoders/image.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/decoders/pickle.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/distributors/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/distributors/basic.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/iterators.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/misc.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/stream.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/webdataset.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/datasets/webdataset/webdataset_ext.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/flags.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/flops/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/flops/omni_mot.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/flops/wan_vae.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/functional/batch_ops.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/functional/lr_scheduler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/functional/multi_step.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/functional/runge_kutta.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/file_io.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/instantiate.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/lazy.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/lazy_call.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/omegaconf_patch.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/lazy_config/registry.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/model.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/models/abstract_emb_model.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/modules/camera.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/modules/denoiser_scaling.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/modules/edm_sde.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/modules/image_embeddings.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/modules/res_sampler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/serialization.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/trainer.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/callback.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/checkpoint_db.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/checkpointer.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/cluster_env.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/config_helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/context_managers.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/context_parallel.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/count_params.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/dataloader.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/dataset_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/denoise_prediction.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/device.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/disabled_train.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/distributed.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/auto_auth.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/base_backend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/boto3_backend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/boto3_client.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/http_backend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/local_backend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/msc_backend.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/backends/registry_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/easy_io.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/file_client.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/base.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/byte_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/csv_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/json_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/np_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/pil_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/registry_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/torch_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/txt_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/ema.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/embedding_concat_strategy.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/env_parsers/cred_env_parser.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/env_parsers/customization_env_parser.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/env_parsers/env_parser.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/env_parsers/inference_env_parser.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/fsdp_helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/fused_adam.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/fused_nan_to_num.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/graph.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/high_sigma_strategy.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/launch.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/log.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/misc.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/nsys_wrapper.sh (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/object_store.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/optim_instantiate.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/parallel_state_helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/primitives.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/profiling.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/progress_bar.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/registry.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/replace_bg_color.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/s3_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/scheduler.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/submit_job_helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/timer.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/tone_curve.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/training.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/validator.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/validator_params.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/utils/wandb_util.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/visualize/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/visualize/img.py (100%) rename {cosmos3 => cosmos-inference}/_src/imaginaire/visualize/video.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/compile_tokenizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/dataloader_state.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/dataloading_monitor.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/device_monitor.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/every_n_draw_audio_video_sample.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/every_n_draw_sample.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/expert_heatmap.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/generation.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/grad_clip.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/heart_beat.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/hf_export.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/iter_speed.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/load_pretrained.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/low_precision.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/mfu.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/moe_specialization_callback.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/moe_stability_callback.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/norm_monitor.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/ofu.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/param_count.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/sequence_packing_padding.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/sigma_loss_analysis.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/skip_nan_step.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/training_stats.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/vlm/grad_clip.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/wandb_log.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/callbacks/wandb_log_eval.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/checkpointer/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/checkpointer/dcp.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/conditioner.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/config.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/callbacks.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/checkpointer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/cluster.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/conditioner.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/ema.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/model.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/model_config.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/multiview_dataloader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/optimizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/tokenizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/unittest.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/defaults/vlm.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/config.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/callbacks.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/checkpointer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/config.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/dataloader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/model.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/optimizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/training.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/defaults/vlm_policy.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/experiment/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/configs/base/vlm/experiment/utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/action_normalization.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/action_spec.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/dataloaders.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/domain_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/json_formatter.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/libero_dataset.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/libero_pose_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/pose_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/transforms.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/unified_dataset.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/action/viewpoint_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/audio_caption.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/audio_parsing.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/caption_filter.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/cropping.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/idle_frames_text_info.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/image_editing_transform.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/image_resolution_filter.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/interleaved_image_transform.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/interleaved_video_parsing.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/merge_datadict.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/pkl_to_media.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/resolution_text_info.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/sequence_plan.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/sound_sequence_plan.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/text_tokenizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/text_transforms_for_image.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/text_transforms_for_video.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_input/blur.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_input/seg.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/transfer_control_transform.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/augmentors/video_parsing.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/joint_dataloader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/local_datasets/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/local_datasets/helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/local_datasets/sft_dataset.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/sequence_packing.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/datasets/utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/rectified_flow.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/edm.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/fixed_step.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/fm_solvers_unipc.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/unipc.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/diffusion/samplers/utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/evaluation/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/evaluation/action/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/evaluation/action/libero/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/evaluation/action/libero/closed_loop_eval.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/hf_model.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/configs/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/configuration_qwen3.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/qwen3.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/llm/qwen3/test_qwen3.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/attention.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/context_parallel_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/cosmos3_vfm_network.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/domain_aware_linear.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/dot_product_attention.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/modeling_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/parallelize_unified_mot.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/parallelize_vfm_network.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/qwen3_vl_unified_mot.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/unified_3dmrope_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/mot/unified_mot.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/omni_mot_model.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/parallelize_vlm.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/data_and_condition.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/dcp_loader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/load_balancing_loss.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/memory.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/safetensors_loader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/utils/taylorseer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/moe.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/models/vlm_model.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/interface.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/tokenization_qwen2.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/context_parallel.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/data_utils.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/dtensor_helper.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/flash_attn.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/fused_adam.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/loss.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/misc.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/model_loader.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/model_weights_stats.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/monkey_patch.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/optimizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/parallelism.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/rand_state.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/tokenizer_benchmarking.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/vlm/__init__.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/vlm/optimizer.py (100%) rename {cosmos3 => cosmos-inference}/_src/vfm/utils/vlm/pretrained_models_downloader.py (100%) rename {cosmos3 => cosmos-inference}/_test.py (100%) rename {cosmos3 => cosmos-inference}/_test/autoregressive.sh (100%) rename {cosmos3 => cosmos-inference}/_test/distilled.sh (100%) rename {cosmos3 => cosmos-inference}/_test/latency.sh (100%) rename {cosmos3 => cosmos-inference}/_test/omni-super.sh (100%) rename {cosmos3 => cosmos-inference}/_test/omni.sh (100%) rename {cosmos3 => cosmos-inference}/_test/omni_param.sh (100%) rename {cosmos3 => cosmos-inference}/_test/sft.sh (100%) rename {cosmos3 => cosmos-inference}/action.py (100%) rename {cosmos3 => cosmos-inference}/args.py (100%) rename {cosmos3 => cosmos-inference}/args_test.py (100%) rename {cosmos3 => cosmos-inference}/common/__init__.py (100%) rename {cosmos3 => cosmos-inference}/common/args.py (100%) rename {cosmos3 => cosmos-inference}/common/args_test.py (100%) rename {cosmos3 => cosmos-inference}/common/checkpoints.py (100%) rename {cosmos3 => cosmos-inference}/common/config.py (100%) rename {cosmos3 => cosmos-inference}/common/config_test.py (100%) rename {cosmos3 => cosmos-inference}/common/inference.py (100%) rename {cosmos3 => cosmos-inference}/common/inference_test.py (100%) rename {cosmos3 => cosmos-inference}/common/init.py (100%) rename {cosmos3 => cosmos-inference}/configs/experiment/action_policy_sft_8b.yaml (100%) rename {cosmos3 => cosmos-inference}/configs/experiment/mixed_modality_sft_8b.yaml (100%) rename {cosmos3 => cosmos-inference}/dataset_samples.py (100%) rename {cosmos3 => cosmos-inference}/defaults/forward_dynamics/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/image2video/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/inverse_dynamics/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/policy/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/prompt_upsampler.txt (100%) rename {cosmos3 => cosmos-inference}/defaults/text2image/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/text2video/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/video2video/sample_args.json (100%) rename {cosmos3 => cosmos-inference}/defaults/video_captioner.txt (100%) rename {cosmos3 => cosmos-inference}/fixtures/__init__.py (100%) rename {cosmos3 => cosmos-inference}/fixtures/args.py (100%) rename {cosmos3 => cosmos-inference}/fixtures/script.py (100%) rename {cosmos3 => cosmos-inference}/flags.py (100%) rename {cosmos3 => cosmos-inference}/inference.py (100%) rename {cosmos3 => cosmos-inference}/model.py (100%) rename {cosmos3 => cosmos-inference}/model_test.py (100%) rename {cosmos3 => cosmos-inference}/ray/__init__.py (100%) rename {cosmos3 => cosmos-inference}/ray/configs/latency.yaml (100%) rename {cosmos3 => cosmos-inference}/ray/configs/throughput.yaml (100%) rename {cosmos3 => cosmos-inference}/ray/gradio.py (100%) rename {cosmos3 => cosmos-inference}/ray/serve.py (100%) rename {cosmos3 => cosmos-inference}/ray/serve_test.py (100%) rename {cosmos3 => cosmos-inference}/ray/submit.py (100%) rename {cosmos3 => cosmos-inference}/scripts/__init__.py (100%) rename {cosmos3 => cosmos-inference}/scripts/_test.py (100%) rename {cosmos3 => cosmos-inference}/scripts/_test/convert_model_to_dcp.sh (100%) rename {cosmos3 => cosmos-inference}/scripts/_test/convert_model_to_diffusers.sh (100%) rename {cosmos3 => cosmos-inference}/scripts/_test/export_config.sh (100%) rename {cosmos3 => cosmos-inference}/scripts/_test/export_model.sh (100%) rename {cosmos3 => cosmos-inference}/scripts/action_policy_server.py (100%) rename {cosmos3 => cosmos-inference}/scripts/caption_from_video.py (100%) rename {cosmos3 => cosmos-inference}/scripts/captions_to_sft_jsonl.py (100%) rename {cosmos3 => cosmos-inference}/scripts/convert_model_to_dcp.py (100%) rename {cosmos3 => cosmos-inference}/scripts/eval_utils.py (100%) rename {cosmos3 => cosmos-inference}/scripts/export_config.py (100%) rename {cosmos3 => cosmos-inference}/scripts/export_model.py (100%) rename {cosmos3 => cosmos-inference}/scripts/export_schemas.py (100%) rename {cosmos3 => cosmos-inference}/scripts/export_schemas_test.py (100%) rename {cosmos3 => cosmos-inference}/scripts/hydra.py (100%) rename {cosmos3 => cosmos-inference}/scripts/inference.py (100%) rename {cosmos3 => cosmos-inference}/scripts/prefetch_hf_checkpoints.py (100%) rename {cosmos3 => cosmos-inference}/scripts/train.py (100%) rename {cosmos3 => cosmos-inference}/scripts/upsample_prompts.py (100%) rename {cosmos3 => cosmos-inference}/vision.py (100%) create mode 100644 cosmos/__init__.py create mode 100644 cosmos/algorithm/__init__.py create mode 100644 cosmos/algorithm/loss/__init__.py create mode 100644 cosmos/algorithm/reward/__init__.py create mode 100644 cosmos/algorithm/rl/__init__.py create mode 100644 cosmos/callbacks/__init__.py create mode 100644 cosmos/checkpoint/__init__.py create mode 100644 cosmos/communicator/__init__.py create mode 100644 cosmos/controller/__init__.py create mode 100644 cosmos/data/__init__.py create mode 100644 cosmos/evaluation/__init__.py create mode 100644 cosmos/inference/__init__.py create mode 100644 cosmos/launcher/__init__.py create mode 100644 cosmos/model/__init__.py create mode 100644 cosmos/tools/__init__.py create mode 100644 cosmos/trainer/__init__.py create mode 100644 cosmos/utils/__init__.py create mode 100644 cosmos/workers/__init__.py create mode 100644 cosmos/workers/reference/__init__.py create mode 100644 cosmos/workers/reward/__init__.py create mode 100644 cosmos/workers/rollout/__init__.py create mode 100644 cosmos/workers/simulations/__init__.py create mode 100644 tests/.gitkeep create mode 100644 tools/.gitkeep diff --git a/cosmos3/__init__.py b/cosmos-inference/__init__.py similarity index 100% rename from cosmos3/__init__.py rename to cosmos-inference/__init__.py diff --git a/cosmos3/_src/imaginaire/__init__.py b/cosmos-inference/_src/imaginaire/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/__init__.py rename to cosmos-inference/_src/imaginaire/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/README.md b/cosmos-inference/_src/imaginaire/attention/README.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/README.md rename to cosmos-inference/_src/imaginaire/attention/README.md diff --git a/cosmos3/_src/imaginaire/attention/__init__.py b/cosmos-inference/_src/imaginaire/attention/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/__init__.py rename to cosmos-inference/_src/imaginaire/attention/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/backends.py b/cosmos-inference/_src/imaginaire/attention/backends.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/backends.py rename to cosmos-inference/_src/imaginaire/attention/backends.py diff --git a/cosmos3/_src/imaginaire/attention/checks.py b/cosmos-inference/_src/imaginaire/attention/checks.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/checks.py rename to cosmos-inference/_src/imaginaire/attention/checks.py diff --git a/cosmos3/_src/imaginaire/attention/docs/README.md b/cosmos-inference/_src/imaginaire/attention/docs/README.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/docs/README.md rename to cosmos-inference/_src/imaginaire/attention/docs/README.md diff --git a/cosmos3/_src/imaginaire/attention/docs/apis.md b/cosmos-inference/_src/imaginaire/attention/docs/apis.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/docs/apis.md rename to cosmos-inference/_src/imaginaire/attention/docs/apis.md diff --git a/cosmos3/_src/imaginaire/attention/docs/backends.md b/cosmos-inference/_src/imaginaire/attention/docs/backends.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/docs/backends.md rename to cosmos-inference/_src/imaginaire/attention/docs/backends.md diff --git a/cosmos3/_src/imaginaire/attention/docs/features.md b/cosmos-inference/_src/imaginaire/attention/docs/features.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/docs/features.md rename to cosmos-inference/_src/imaginaire/attention/docs/features.md diff --git a/cosmos3/_src/imaginaire/attention/docs/multi-dim.md b/cosmos-inference/_src/imaginaire/attention/docs/multi-dim.md similarity index 100% rename from cosmos3/_src/imaginaire/attention/docs/multi-dim.md rename to cosmos-inference/_src/imaginaire/attention/docs/multi-dim.md diff --git a/cosmos3/_src/imaginaire/attention/flash2/__init__.py b/cosmos-inference/_src/imaginaire/attention/flash2/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash2/__init__.py rename to cosmos-inference/_src/imaginaire/attention/flash2/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/flash2/checks.py b/cosmos-inference/_src/imaginaire/attention/flash2/checks.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash2/checks.py rename to cosmos-inference/_src/imaginaire/attention/flash2/checks.py diff --git a/cosmos3/_src/imaginaire/attention/flash2/functions.py b/cosmos-inference/_src/imaginaire/attention/flash2/functions.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash2/functions.py rename to cosmos-inference/_src/imaginaire/attention/flash2/functions.py diff --git a/cosmos3/_src/imaginaire/attention/flash2/meta.py b/cosmos-inference/_src/imaginaire/attention/flash2/meta.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash2/meta.py rename to cosmos-inference/_src/imaginaire/attention/flash2/meta.py diff --git a/cosmos3/_src/imaginaire/attention/flash2/stubs.py b/cosmos-inference/_src/imaginaire/attention/flash2/stubs.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash2/stubs.py rename to cosmos-inference/_src/imaginaire/attention/flash2/stubs.py diff --git a/cosmos3/_src/imaginaire/attention/flash3/__init__.py b/cosmos-inference/_src/imaginaire/attention/flash3/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash3/__init__.py rename to cosmos-inference/_src/imaginaire/attention/flash3/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/flash3/checks.py b/cosmos-inference/_src/imaginaire/attention/flash3/checks.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash3/checks.py rename to cosmos-inference/_src/imaginaire/attention/flash3/checks.py diff --git a/cosmos3/_src/imaginaire/attention/flash3/functions.py b/cosmos-inference/_src/imaginaire/attention/flash3/functions.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash3/functions.py rename to cosmos-inference/_src/imaginaire/attention/flash3/functions.py diff --git a/cosmos3/_src/imaginaire/attention/flash3/meta.py b/cosmos-inference/_src/imaginaire/attention/flash3/meta.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash3/meta.py rename to cosmos-inference/_src/imaginaire/attention/flash3/meta.py diff --git a/cosmos3/_src/imaginaire/attention/flash3/stubs.py b/cosmos-inference/_src/imaginaire/attention/flash3/stubs.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/flash3/stubs.py rename to cosmos-inference/_src/imaginaire/attention/flash3/stubs.py diff --git a/cosmos3/_src/imaginaire/attention/frontend.py b/cosmos-inference/_src/imaginaire/attention/frontend.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/frontend.py rename to cosmos-inference/_src/imaginaire/attention/frontend.py diff --git a/cosmos3/_src/imaginaire/attention/masks.py b/cosmos-inference/_src/imaginaire/attention/masks.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/masks.py rename to cosmos-inference/_src/imaginaire/attention/masks.py diff --git a/cosmos3/_src/imaginaire/attention/natten/__init__.py b/cosmos-inference/_src/imaginaire/attention/natten/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/natten/__init__.py rename to cosmos-inference/_src/imaginaire/attention/natten/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/natten/checks.py b/cosmos-inference/_src/imaginaire/attention/natten/checks.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/natten/checks.py rename to cosmos-inference/_src/imaginaire/attention/natten/checks.py diff --git a/cosmos3/_src/imaginaire/attention/natten/functions.py b/cosmos-inference/_src/imaginaire/attention/natten/functions.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/natten/functions.py rename to cosmos-inference/_src/imaginaire/attention/natten/functions.py diff --git a/cosmos3/_src/imaginaire/attention/natten/meta.py b/cosmos-inference/_src/imaginaire/attention/natten/meta.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/natten/meta.py rename to cosmos-inference/_src/imaginaire/attention/natten/meta.py diff --git a/cosmos3/_src/imaginaire/attention/natten/stubs.py b/cosmos-inference/_src/imaginaire/attention/natten/stubs.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/natten/stubs.py rename to cosmos-inference/_src/imaginaire/attention/natten/stubs.py diff --git a/cosmos3/_src/imaginaire/attention/utils/__init__.py b/cosmos-inference/_src/imaginaire/attention/utils/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/__init__.py rename to cosmos-inference/_src/imaginaire/attention/utils/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/utils/determinism.py b/cosmos-inference/_src/imaginaire/attention/utils/determinism.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/determinism.py rename to cosmos-inference/_src/imaginaire/attention/utils/determinism.py diff --git a/cosmos3/_src/imaginaire/attention/utils/environment.py b/cosmos-inference/_src/imaginaire/attention/utils/environment.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/environment.py rename to cosmos-inference/_src/imaginaire/attention/utils/environment.py diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py b/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py rename to cosmos-inference/_src/imaginaire/attention/utils/safe_ops/__init__.py diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py b/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/functools.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py rename to cosmos-inference/_src/imaginaire/attention/utils/safe_ops/functools.py diff --git a/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py b/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/log.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py rename to cosmos-inference/_src/imaginaire/attention/utils/safe_ops/log.py diff --git a/cosmos3/_src/imaginaire/attention/utils/version.py b/cosmos-inference/_src/imaginaire/attention/utils/version.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/utils/version.py rename to cosmos-inference/_src/imaginaire/attention/utils/version.py diff --git a/cosmos3/_src/imaginaire/attention/varlen.py b/cosmos-inference/_src/imaginaire/attention/varlen.py similarity index 100% rename from cosmos3/_src/imaginaire/attention/varlen.py rename to cosmos-inference/_src/imaginaire/attention/varlen.py diff --git a/cosmos3/_src/imaginaire/auxiliary/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/core.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/core.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/io_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/io_utils.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/presets.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/presets.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py diff --git a/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py b/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py similarity index 100% rename from cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py rename to cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py diff --git a/cosmos3/_src/imaginaire/callbacks/__init__.py b/cosmos-inference/_src/imaginaire/callbacks/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/callbacks/__init__.py rename to cosmos-inference/_src/imaginaire/callbacks/__init__.py diff --git a/cosmos3/_src/imaginaire/callbacks/every_n.py b/cosmos-inference/_src/imaginaire/callbacks/every_n.py similarity index 100% rename from cosmos3/_src/imaginaire/callbacks/every_n.py rename to cosmos-inference/_src/imaginaire/callbacks/every_n.py diff --git a/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py b/cosmos-inference/_src/imaginaire/callbacks/image_grad_clip.py similarity index 100% rename from cosmos3/_src/imaginaire/callbacks/image_grad_clip.py rename to cosmos-inference/_src/imaginaire/callbacks/image_grad_clip.py diff --git a/cosmos3/_src/imaginaire/callbacks/manual_gc.py b/cosmos-inference/_src/imaginaire/callbacks/manual_gc.py similarity index 100% rename from cosmos3/_src/imaginaire/callbacks/manual_gc.py rename to cosmos-inference/_src/imaginaire/callbacks/manual_gc.py diff --git a/cosmos3/_src/imaginaire/checkpointer/__init__.py b/cosmos-inference/_src/imaginaire/checkpointer/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/__init__.py rename to cosmos-inference/_src/imaginaire/checkpointer/__init__.py diff --git a/cosmos3/_src/imaginaire/checkpointer/base.py b/cosmos-inference/_src/imaginaire/checkpointer/base.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/base.py rename to cosmos-inference/_src/imaginaire/checkpointer/base.py diff --git a/cosmos3/_src/imaginaire/checkpointer/ddp.py b/cosmos-inference/_src/imaginaire/checkpointer/ddp.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/ddp.py rename to cosmos-inference/_src/imaginaire/checkpointer/ddp.py diff --git a/cosmos3/_src/imaginaire/checkpointer/dummy.py b/cosmos-inference/_src/imaginaire/checkpointer/dummy.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/dummy.py rename to cosmos-inference/_src/imaginaire/checkpointer/dummy.py diff --git a/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py b/cosmos-inference/_src/imaginaire/checkpointer/s3_filesystem.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py rename to cosmos-inference/_src/imaginaire/checkpointer/s3_filesystem.py diff --git a/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py b/cosmos-inference/_src/imaginaire/checkpointer/safe_broadcast.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py rename to cosmos-inference/_src/imaginaire/checkpointer/safe_broadcast.py diff --git a/cosmos3/_src/imaginaire/checkpointer/tp.py b/cosmos-inference/_src/imaginaire/checkpointer/tp.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/tp.py rename to cosmos-inference/_src/imaginaire/checkpointer/tp.py diff --git a/cosmos3/_src/imaginaire/checkpointer/tp_ema.py b/cosmos-inference/_src/imaginaire/checkpointer/tp_ema.py similarity index 100% rename from cosmos3/_src/imaginaire/checkpointer/tp_ema.py rename to cosmos-inference/_src/imaginaire/checkpointer/tp_ema.py diff --git a/cosmos3/_src/imaginaire/config.py b/cosmos-inference/_src/imaginaire/config.py similarity index 100% rename from cosmos3/_src/imaginaire/config.py rename to cosmos-inference/_src/imaginaire/config.py diff --git a/cosmos3/_src/imaginaire/configs/lr_scheduler.py b/cosmos-inference/_src/imaginaire/configs/lr_scheduler.py similarity index 100% rename from cosmos3/_src/imaginaire/configs/lr_scheduler.py rename to cosmos-inference/_src/imaginaire/configs/lr_scheduler.py diff --git a/cosmos3/_src/imaginaire/datasets/__init__.py b/cosmos-inference/_src/imaginaire/datasets/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py b/cosmos-inference/_src/imaginaire/datasets/augmentors/merge_datadict.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py rename to cosmos-inference/_src/imaginaire/datasets/augmentors/merge_datadict.py diff --git a/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py b/cosmos-inference/_src/imaginaire/datasets/augmentors/v3_text_transforms.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py rename to cosmos-inference/_src/imaginaire/datasets/augmentors/v3_text_transforms.py diff --git a/cosmos3/_src/imaginaire/datasets/decoders/__init__.py b/cosmos-inference/_src/imaginaire/datasets/decoders/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/decoders/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/decoders/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py b/cosmos-inference/_src/imaginaire/datasets/decoders/json_loader.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/decoders/json_loader.py rename to cosmos-inference/_src/imaginaire/datasets/decoders/json_loader.py diff --git a/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py b/cosmos-inference/_src/imaginaire/datasets/decoders/pkl_loader.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py rename to cosmos-inference/_src/imaginaire/datasets/decoders/pkl_loader.py diff --git a/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py b/cosmos-inference/_src/imaginaire/datasets/decoders/video_decoder.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py rename to cosmos-inference/_src/imaginaire/datasets/decoders/video_decoder.py diff --git a/cosmos3/_src/imaginaire/datasets/joint_training.py b/cosmos-inference/_src/imaginaire/datasets/joint_training.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/joint_training.py rename to cosmos-inference/_src/imaginaire/datasets/joint_training.py diff --git a/cosmos3/_src/imaginaire/datasets/mock_dataset.py b/cosmos-inference/_src/imaginaire/datasets/mock_dataset.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/mock_dataset.py rename to cosmos-inference/_src/imaginaire/datasets/mock_dataset.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/config/schema.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/config/schema.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/dataloader.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/dataloader.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/depth.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/depth.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/image.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/image.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/pickle.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/pickle.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/__init__.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/basic.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/basic.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/iterators.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/iterators.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/misc.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/misc.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/stream.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/stream.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset.py diff --git a/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py b/cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset_ext.py similarity index 100% rename from cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py rename to cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset_ext.py diff --git a/cosmos3/_src/imaginaire/flags.py b/cosmos-inference/_src/imaginaire/flags.py similarity index 100% rename from cosmos3/_src/imaginaire/flags.py rename to cosmos-inference/_src/imaginaire/flags.py diff --git a/cosmos3/_src/imaginaire/flops/__init__.py b/cosmos-inference/_src/imaginaire/flops/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/flops/__init__.py rename to cosmos-inference/_src/imaginaire/flops/__init__.py diff --git a/cosmos3/_src/imaginaire/flops/omni_mot.py b/cosmos-inference/_src/imaginaire/flops/omni_mot.py similarity index 100% rename from cosmos3/_src/imaginaire/flops/omni_mot.py rename to cosmos-inference/_src/imaginaire/flops/omni_mot.py diff --git a/cosmos3/_src/imaginaire/flops/wan_vae.py b/cosmos-inference/_src/imaginaire/flops/wan_vae.py similarity index 100% rename from cosmos3/_src/imaginaire/flops/wan_vae.py rename to cosmos-inference/_src/imaginaire/flops/wan_vae.py diff --git a/cosmos3/_src/imaginaire/functional/batch_ops.py b/cosmos-inference/_src/imaginaire/functional/batch_ops.py similarity index 100% rename from cosmos3/_src/imaginaire/functional/batch_ops.py rename to cosmos-inference/_src/imaginaire/functional/batch_ops.py diff --git a/cosmos3/_src/imaginaire/functional/lr_scheduler.py b/cosmos-inference/_src/imaginaire/functional/lr_scheduler.py similarity index 100% rename from cosmos3/_src/imaginaire/functional/lr_scheduler.py rename to cosmos-inference/_src/imaginaire/functional/lr_scheduler.py diff --git a/cosmos3/_src/imaginaire/functional/multi_step.py b/cosmos-inference/_src/imaginaire/functional/multi_step.py similarity index 100% rename from cosmos3/_src/imaginaire/functional/multi_step.py rename to cosmos-inference/_src/imaginaire/functional/multi_step.py diff --git a/cosmos3/_src/imaginaire/functional/runge_kutta.py b/cosmos-inference/_src/imaginaire/functional/runge_kutta.py similarity index 100% rename from cosmos3/_src/imaginaire/functional/runge_kutta.py rename to cosmos-inference/_src/imaginaire/functional/runge_kutta.py diff --git a/cosmos3/_src/imaginaire/lazy_config/__init__.py b/cosmos-inference/_src/imaginaire/lazy_config/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/__init__.py rename to cosmos-inference/_src/imaginaire/lazy_config/__init__.py diff --git a/cosmos3/_src/imaginaire/lazy_config/file_io.py b/cosmos-inference/_src/imaginaire/lazy_config/file_io.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/file_io.py rename to cosmos-inference/_src/imaginaire/lazy_config/file_io.py diff --git a/cosmos3/_src/imaginaire/lazy_config/instantiate.py b/cosmos-inference/_src/imaginaire/lazy_config/instantiate.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/instantiate.py rename to cosmos-inference/_src/imaginaire/lazy_config/instantiate.py diff --git a/cosmos3/_src/imaginaire/lazy_config/lazy.py b/cosmos-inference/_src/imaginaire/lazy_config/lazy.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/lazy.py rename to cosmos-inference/_src/imaginaire/lazy_config/lazy.py diff --git a/cosmos3/_src/imaginaire/lazy_config/lazy_call.py b/cosmos-inference/_src/imaginaire/lazy_config/lazy_call.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/lazy_call.py rename to cosmos-inference/_src/imaginaire/lazy_config/lazy_call.py diff --git a/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py b/cosmos-inference/_src/imaginaire/lazy_config/omegaconf_patch.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py rename to cosmos-inference/_src/imaginaire/lazy_config/omegaconf_patch.py diff --git a/cosmos3/_src/imaginaire/lazy_config/registry.py b/cosmos-inference/_src/imaginaire/lazy_config/registry.py similarity index 100% rename from cosmos3/_src/imaginaire/lazy_config/registry.py rename to cosmos-inference/_src/imaginaire/lazy_config/registry.py diff --git a/cosmos3/_src/imaginaire/model.py b/cosmos-inference/_src/imaginaire/model.py similarity index 100% rename from cosmos3/_src/imaginaire/model.py rename to cosmos-inference/_src/imaginaire/model.py diff --git a/cosmos3/_src/imaginaire/models/abstract_emb_model.py b/cosmos-inference/_src/imaginaire/models/abstract_emb_model.py similarity index 100% rename from cosmos3/_src/imaginaire/models/abstract_emb_model.py rename to cosmos-inference/_src/imaginaire/models/abstract_emb_model.py diff --git a/cosmos3/_src/imaginaire/modules/camera.py b/cosmos-inference/_src/imaginaire/modules/camera.py similarity index 100% rename from cosmos3/_src/imaginaire/modules/camera.py rename to cosmos-inference/_src/imaginaire/modules/camera.py diff --git a/cosmos3/_src/imaginaire/modules/denoiser_scaling.py b/cosmos-inference/_src/imaginaire/modules/denoiser_scaling.py similarity index 100% rename from cosmos3/_src/imaginaire/modules/denoiser_scaling.py rename to cosmos-inference/_src/imaginaire/modules/denoiser_scaling.py diff --git a/cosmos3/_src/imaginaire/modules/edm_sde.py b/cosmos-inference/_src/imaginaire/modules/edm_sde.py similarity index 100% rename from cosmos3/_src/imaginaire/modules/edm_sde.py rename to cosmos-inference/_src/imaginaire/modules/edm_sde.py diff --git a/cosmos3/_src/imaginaire/modules/image_embeddings.py b/cosmos-inference/_src/imaginaire/modules/image_embeddings.py similarity index 100% rename from cosmos3/_src/imaginaire/modules/image_embeddings.py rename to cosmos-inference/_src/imaginaire/modules/image_embeddings.py diff --git a/cosmos3/_src/imaginaire/modules/res_sampler.py b/cosmos-inference/_src/imaginaire/modules/res_sampler.py similarity index 100% rename from cosmos3/_src/imaginaire/modules/res_sampler.py rename to cosmos-inference/_src/imaginaire/modules/res_sampler.py diff --git a/cosmos3/_src/imaginaire/serialization.py b/cosmos-inference/_src/imaginaire/serialization.py similarity index 100% rename from cosmos3/_src/imaginaire/serialization.py rename to cosmos-inference/_src/imaginaire/serialization.py diff --git a/cosmos3/_src/imaginaire/trainer.py b/cosmos-inference/_src/imaginaire/trainer.py similarity index 100% rename from cosmos3/_src/imaginaire/trainer.py rename to cosmos-inference/_src/imaginaire/trainer.py diff --git a/cosmos3/_src/imaginaire/utils/__init__.py b/cosmos-inference/_src/imaginaire/utils/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/__init__.py rename to cosmos-inference/_src/imaginaire/utils/__init__.py diff --git a/cosmos3/_src/imaginaire/utils/callback.py b/cosmos-inference/_src/imaginaire/utils/callback.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/callback.py rename to cosmos-inference/_src/imaginaire/utils/callback.py diff --git a/cosmos3/_src/imaginaire/utils/checkpoint_db.py b/cosmos-inference/_src/imaginaire/utils/checkpoint_db.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/checkpoint_db.py rename to cosmos-inference/_src/imaginaire/utils/checkpoint_db.py diff --git a/cosmos3/_src/imaginaire/utils/checkpointer.py b/cosmos-inference/_src/imaginaire/utils/checkpointer.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/checkpointer.py rename to cosmos-inference/_src/imaginaire/utils/checkpointer.py diff --git a/cosmos3/_src/imaginaire/utils/cluster_env.py b/cosmos-inference/_src/imaginaire/utils/cluster_env.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/cluster_env.py rename to cosmos-inference/_src/imaginaire/utils/cluster_env.py diff --git a/cosmos3/_src/imaginaire/utils/config_helper.py b/cosmos-inference/_src/imaginaire/utils/config_helper.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/config_helper.py rename to cosmos-inference/_src/imaginaire/utils/config_helper.py diff --git a/cosmos3/_src/imaginaire/utils/context_managers.py b/cosmos-inference/_src/imaginaire/utils/context_managers.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/context_managers.py rename to cosmos-inference/_src/imaginaire/utils/context_managers.py diff --git a/cosmos3/_src/imaginaire/utils/context_parallel.py b/cosmos-inference/_src/imaginaire/utils/context_parallel.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/context_parallel.py rename to cosmos-inference/_src/imaginaire/utils/context_parallel.py diff --git a/cosmos3/_src/imaginaire/utils/count_params.py b/cosmos-inference/_src/imaginaire/utils/count_params.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/count_params.py rename to cosmos-inference/_src/imaginaire/utils/count_params.py diff --git a/cosmos3/_src/imaginaire/utils/dataloader.py b/cosmos-inference/_src/imaginaire/utils/dataloader.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/dataloader.py rename to cosmos-inference/_src/imaginaire/utils/dataloader.py diff --git a/cosmos3/_src/imaginaire/utils/dataset_utils.py b/cosmos-inference/_src/imaginaire/utils/dataset_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/dataset_utils.py rename to cosmos-inference/_src/imaginaire/utils/dataset_utils.py diff --git a/cosmos3/_src/imaginaire/utils/denoise_prediction.py b/cosmos-inference/_src/imaginaire/utils/denoise_prediction.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/denoise_prediction.py rename to cosmos-inference/_src/imaginaire/utils/denoise_prediction.py diff --git a/cosmos3/_src/imaginaire/utils/device.py b/cosmos-inference/_src/imaginaire/utils/device.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/device.py rename to cosmos-inference/_src/imaginaire/utils/device.py diff --git a/cosmos3/_src/imaginaire/utils/disabled_train.py b/cosmos-inference/_src/imaginaire/utils/disabled_train.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/disabled_train.py rename to cosmos-inference/_src/imaginaire/utils/disabled_train.py diff --git a/cosmos3/_src/imaginaire/utils/distributed.py b/cosmos-inference/_src/imaginaire/utils/distributed.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/distributed.py rename to cosmos-inference/_src/imaginaire/utils/distributed.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/__init__.py b/cosmos-inference/_src/imaginaire/utils/easy_io/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/__init__.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/__init__.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/__init__.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/auto_auth.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/auto_auth.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/base_backend.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/base_backend.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_backend.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_backend.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_client.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_client.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/http_backend.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/http_backend.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/local_backend.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/local_backend.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/msc_backend.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/msc_backend.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py b/cosmos-inference/_src/imaginaire/utils/easy_io/backends/registry_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/backends/registry_utils.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py b/cosmos-inference/_src/imaginaire/utils/easy_io/easy_io.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/easy_io.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/easy_io.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/file_client.py b/cosmos-inference/_src/imaginaire/utils/easy_io/file_client.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/file_client.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/file_client.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/__init__.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/base.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/base.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/byte_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/byte_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/csv_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/csv_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/json_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/json_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/np_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/np_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pil_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pil_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/registry_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/registry_utils.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torch_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torch_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/txt_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/txt_handler.py diff --git a/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py b/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py rename to cosmos-inference/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py diff --git a/cosmos3/_src/imaginaire/utils/ema.py b/cosmos-inference/_src/imaginaire/utils/ema.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/ema.py rename to cosmos-inference/_src/imaginaire/utils/ema.py diff --git a/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py b/cosmos-inference/_src/imaginaire/utils/embedding_concat_strategy.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py rename to cosmos-inference/_src/imaginaire/utils/embedding_concat_strategy.py diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py b/cosmos-inference/_src/imaginaire/utils/env_parsers/cred_env_parser.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py rename to cosmos-inference/_src/imaginaire/utils/env_parsers/cred_env_parser.py diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py b/cosmos-inference/_src/imaginaire/utils/env_parsers/customization_env_parser.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py rename to cosmos-inference/_src/imaginaire/utils/env_parsers/customization_env_parser.py diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py b/cosmos-inference/_src/imaginaire/utils/env_parsers/env_parser.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py rename to cosmos-inference/_src/imaginaire/utils/env_parsers/env_parser.py diff --git a/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py b/cosmos-inference/_src/imaginaire/utils/env_parsers/inference_env_parser.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py rename to cosmos-inference/_src/imaginaire/utils/env_parsers/inference_env_parser.py diff --git a/cosmos3/_src/imaginaire/utils/fsdp_helper.py b/cosmos-inference/_src/imaginaire/utils/fsdp_helper.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/fsdp_helper.py rename to cosmos-inference/_src/imaginaire/utils/fsdp_helper.py diff --git a/cosmos3/_src/imaginaire/utils/fused_adam.py b/cosmos-inference/_src/imaginaire/utils/fused_adam.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/fused_adam.py rename to cosmos-inference/_src/imaginaire/utils/fused_adam.py diff --git a/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py b/cosmos-inference/_src/imaginaire/utils/fused_nan_to_num.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/fused_nan_to_num.py rename to cosmos-inference/_src/imaginaire/utils/fused_nan_to_num.py diff --git a/cosmos3/_src/imaginaire/utils/graph.py b/cosmos-inference/_src/imaginaire/utils/graph.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/graph.py rename to cosmos-inference/_src/imaginaire/utils/graph.py diff --git a/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py b/cosmos-inference/_src/imaginaire/utils/high_sigma_strategy.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/high_sigma_strategy.py rename to cosmos-inference/_src/imaginaire/utils/high_sigma_strategy.py diff --git a/cosmos3/_src/imaginaire/utils/launch.py b/cosmos-inference/_src/imaginaire/utils/launch.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/launch.py rename to cosmos-inference/_src/imaginaire/utils/launch.py diff --git a/cosmos3/_src/imaginaire/utils/log.py b/cosmos-inference/_src/imaginaire/utils/log.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/log.py rename to cosmos-inference/_src/imaginaire/utils/log.py diff --git a/cosmos3/_src/imaginaire/utils/misc.py b/cosmos-inference/_src/imaginaire/utils/misc.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/misc.py rename to cosmos-inference/_src/imaginaire/utils/misc.py diff --git a/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh b/cosmos-inference/_src/imaginaire/utils/nsys_wrapper.sh similarity index 100% rename from cosmos3/_src/imaginaire/utils/nsys_wrapper.sh rename to cosmos-inference/_src/imaginaire/utils/nsys_wrapper.sh diff --git a/cosmos3/_src/imaginaire/utils/object_store.py b/cosmos-inference/_src/imaginaire/utils/object_store.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/object_store.py rename to cosmos-inference/_src/imaginaire/utils/object_store.py diff --git a/cosmos3/_src/imaginaire/utils/optim_instantiate.py b/cosmos-inference/_src/imaginaire/utils/optim_instantiate.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/optim_instantiate.py rename to cosmos-inference/_src/imaginaire/utils/optim_instantiate.py diff --git a/cosmos3/_src/imaginaire/utils/parallel_state_helper.py b/cosmos-inference/_src/imaginaire/utils/parallel_state_helper.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/parallel_state_helper.py rename to cosmos-inference/_src/imaginaire/utils/parallel_state_helper.py diff --git a/cosmos3/_src/imaginaire/utils/primitives.py b/cosmos-inference/_src/imaginaire/utils/primitives.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/primitives.py rename to cosmos-inference/_src/imaginaire/utils/primitives.py diff --git a/cosmos3/_src/imaginaire/utils/profiling.py b/cosmos-inference/_src/imaginaire/utils/profiling.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/profiling.py rename to cosmos-inference/_src/imaginaire/utils/profiling.py diff --git a/cosmos3/_src/imaginaire/utils/progress_bar.py b/cosmos-inference/_src/imaginaire/utils/progress_bar.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/progress_bar.py rename to cosmos-inference/_src/imaginaire/utils/progress_bar.py diff --git a/cosmos3/_src/imaginaire/utils/registry.py b/cosmos-inference/_src/imaginaire/utils/registry.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/registry.py rename to cosmos-inference/_src/imaginaire/utils/registry.py diff --git a/cosmos3/_src/imaginaire/utils/replace_bg_color.py b/cosmos-inference/_src/imaginaire/utils/replace_bg_color.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/replace_bg_color.py rename to cosmos-inference/_src/imaginaire/utils/replace_bg_color.py diff --git a/cosmos3/_src/imaginaire/utils/s3_utils.py b/cosmos-inference/_src/imaginaire/utils/s3_utils.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/s3_utils.py rename to cosmos-inference/_src/imaginaire/utils/s3_utils.py diff --git a/cosmos3/_src/imaginaire/utils/scheduler.py b/cosmos-inference/_src/imaginaire/utils/scheduler.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/scheduler.py rename to cosmos-inference/_src/imaginaire/utils/scheduler.py diff --git a/cosmos3/_src/imaginaire/utils/submit_job_helper.py b/cosmos-inference/_src/imaginaire/utils/submit_job_helper.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/submit_job_helper.py rename to cosmos-inference/_src/imaginaire/utils/submit_job_helper.py diff --git a/cosmos3/_src/imaginaire/utils/timer.py b/cosmos-inference/_src/imaginaire/utils/timer.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/timer.py rename to cosmos-inference/_src/imaginaire/utils/timer.py diff --git a/cosmos3/_src/imaginaire/utils/tone_curve.py b/cosmos-inference/_src/imaginaire/utils/tone_curve.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/tone_curve.py rename to cosmos-inference/_src/imaginaire/utils/tone_curve.py diff --git a/cosmos3/_src/imaginaire/utils/training.py b/cosmos-inference/_src/imaginaire/utils/training.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/training.py rename to cosmos-inference/_src/imaginaire/utils/training.py diff --git a/cosmos3/_src/imaginaire/utils/validator.py b/cosmos-inference/_src/imaginaire/utils/validator.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/validator.py rename to cosmos-inference/_src/imaginaire/utils/validator.py diff --git a/cosmos3/_src/imaginaire/utils/validator_params.py b/cosmos-inference/_src/imaginaire/utils/validator_params.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/validator_params.py rename to cosmos-inference/_src/imaginaire/utils/validator_params.py diff --git a/cosmos3/_src/imaginaire/utils/wandb_util.py b/cosmos-inference/_src/imaginaire/utils/wandb_util.py similarity index 100% rename from cosmos3/_src/imaginaire/utils/wandb_util.py rename to cosmos-inference/_src/imaginaire/utils/wandb_util.py diff --git a/cosmos3/_src/imaginaire/visualize/__init__.py b/cosmos-inference/_src/imaginaire/visualize/__init__.py similarity index 100% rename from cosmos3/_src/imaginaire/visualize/__init__.py rename to cosmos-inference/_src/imaginaire/visualize/__init__.py diff --git a/cosmos3/_src/imaginaire/visualize/img.py b/cosmos-inference/_src/imaginaire/visualize/img.py similarity index 100% rename from cosmos3/_src/imaginaire/visualize/img.py rename to cosmos-inference/_src/imaginaire/visualize/img.py diff --git a/cosmos3/_src/imaginaire/visualize/video.py b/cosmos-inference/_src/imaginaire/visualize/video.py similarity index 100% rename from cosmos3/_src/imaginaire/visualize/video.py rename to cosmos-inference/_src/imaginaire/visualize/video.py diff --git a/cosmos3/_src/vfm/__init__.py b/cosmos-inference/_src/vfm/__init__.py similarity index 100% rename from cosmos3/_src/vfm/__init__.py rename to cosmos-inference/_src/vfm/__init__.py diff --git a/cosmos3/_src/vfm/callbacks/__init__.py b/cosmos-inference/_src/vfm/callbacks/__init__.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/__init__.py rename to cosmos-inference/_src/vfm/callbacks/__init__.py diff --git a/cosmos3/_src/vfm/callbacks/compile_tokenizer.py b/cosmos-inference/_src/vfm/callbacks/compile_tokenizer.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/compile_tokenizer.py rename to cosmos-inference/_src/vfm/callbacks/compile_tokenizer.py diff --git a/cosmos3/_src/vfm/callbacks/dataloader_state.py b/cosmos-inference/_src/vfm/callbacks/dataloader_state.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/dataloader_state.py rename to cosmos-inference/_src/vfm/callbacks/dataloader_state.py diff --git a/cosmos3/_src/vfm/callbacks/dataloading_monitor.py b/cosmos-inference/_src/vfm/callbacks/dataloading_monitor.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/dataloading_monitor.py rename to cosmos-inference/_src/vfm/callbacks/dataloading_monitor.py diff --git a/cosmos3/_src/vfm/callbacks/device_monitor.py b/cosmos-inference/_src/vfm/callbacks/device_monitor.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/device_monitor.py rename to cosmos-inference/_src/vfm/callbacks/device_monitor.py diff --git a/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py b/cosmos-inference/_src/vfm/callbacks/every_n_draw_audio_video_sample.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py rename to cosmos-inference/_src/vfm/callbacks/every_n_draw_audio_video_sample.py diff --git a/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py b/cosmos-inference/_src/vfm/callbacks/every_n_draw_sample.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/every_n_draw_sample.py rename to cosmos-inference/_src/vfm/callbacks/every_n_draw_sample.py diff --git a/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py b/cosmos-inference/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py rename to cosmos-inference/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py diff --git a/cosmos3/_src/vfm/callbacks/expert_heatmap.py b/cosmos-inference/_src/vfm/callbacks/expert_heatmap.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/expert_heatmap.py rename to cosmos-inference/_src/vfm/callbacks/expert_heatmap.py diff --git a/cosmos3/_src/vfm/callbacks/generation.py b/cosmos-inference/_src/vfm/callbacks/generation.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/generation.py rename to cosmos-inference/_src/vfm/callbacks/generation.py diff --git a/cosmos3/_src/vfm/callbacks/grad_clip.py b/cosmos-inference/_src/vfm/callbacks/grad_clip.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/grad_clip.py rename to cosmos-inference/_src/vfm/callbacks/grad_clip.py diff --git a/cosmos3/_src/vfm/callbacks/heart_beat.py b/cosmos-inference/_src/vfm/callbacks/heart_beat.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/heart_beat.py rename to cosmos-inference/_src/vfm/callbacks/heart_beat.py diff --git a/cosmos3/_src/vfm/callbacks/hf_export.py b/cosmos-inference/_src/vfm/callbacks/hf_export.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/hf_export.py rename to cosmos-inference/_src/vfm/callbacks/hf_export.py diff --git a/cosmos3/_src/vfm/callbacks/iter_speed.py b/cosmos-inference/_src/vfm/callbacks/iter_speed.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/iter_speed.py rename to cosmos-inference/_src/vfm/callbacks/iter_speed.py diff --git a/cosmos3/_src/vfm/callbacks/load_pretrained.py b/cosmos-inference/_src/vfm/callbacks/load_pretrained.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/load_pretrained.py rename to cosmos-inference/_src/vfm/callbacks/load_pretrained.py diff --git a/cosmos3/_src/vfm/callbacks/low_precision.py b/cosmos-inference/_src/vfm/callbacks/low_precision.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/low_precision.py rename to cosmos-inference/_src/vfm/callbacks/low_precision.py diff --git a/cosmos3/_src/vfm/callbacks/mfu.py b/cosmos-inference/_src/vfm/callbacks/mfu.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/mfu.py rename to cosmos-inference/_src/vfm/callbacks/mfu.py diff --git a/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py b/cosmos-inference/_src/vfm/callbacks/moe_specialization_callback.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/moe_specialization_callback.py rename to cosmos-inference/_src/vfm/callbacks/moe_specialization_callback.py diff --git a/cosmos3/_src/vfm/callbacks/moe_stability_callback.py b/cosmos-inference/_src/vfm/callbacks/moe_stability_callback.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/moe_stability_callback.py rename to cosmos-inference/_src/vfm/callbacks/moe_stability_callback.py diff --git a/cosmos3/_src/vfm/callbacks/norm_monitor.py b/cosmos-inference/_src/vfm/callbacks/norm_monitor.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/norm_monitor.py rename to cosmos-inference/_src/vfm/callbacks/norm_monitor.py diff --git a/cosmos3/_src/vfm/callbacks/ofu.py b/cosmos-inference/_src/vfm/callbacks/ofu.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/ofu.py rename to cosmos-inference/_src/vfm/callbacks/ofu.py diff --git a/cosmos3/_src/vfm/callbacks/param_count.py b/cosmos-inference/_src/vfm/callbacks/param_count.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/param_count.py rename to cosmos-inference/_src/vfm/callbacks/param_count.py diff --git a/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py b/cosmos-inference/_src/vfm/callbacks/sequence_packing_padding.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/sequence_packing_padding.py rename to cosmos-inference/_src/vfm/callbacks/sequence_packing_padding.py diff --git a/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py b/cosmos-inference/_src/vfm/callbacks/sigma_loss_analysis.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py rename to cosmos-inference/_src/vfm/callbacks/sigma_loss_analysis.py diff --git a/cosmos3/_src/vfm/callbacks/skip_nan_step.py b/cosmos-inference/_src/vfm/callbacks/skip_nan_step.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/skip_nan_step.py rename to cosmos-inference/_src/vfm/callbacks/skip_nan_step.py diff --git a/cosmos3/_src/vfm/callbacks/training_stats.py b/cosmos-inference/_src/vfm/callbacks/training_stats.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/training_stats.py rename to cosmos-inference/_src/vfm/callbacks/training_stats.py diff --git a/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py b/cosmos-inference/_src/vfm/callbacks/vlm/grad_clip.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/vlm/grad_clip.py rename to cosmos-inference/_src/vfm/callbacks/vlm/grad_clip.py diff --git a/cosmos3/_src/vfm/callbacks/wandb_log.py b/cosmos-inference/_src/vfm/callbacks/wandb_log.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/wandb_log.py rename to cosmos-inference/_src/vfm/callbacks/wandb_log.py diff --git a/cosmos3/_src/vfm/callbacks/wandb_log_eval.py b/cosmos-inference/_src/vfm/callbacks/wandb_log_eval.py similarity index 100% rename from cosmos3/_src/vfm/callbacks/wandb_log_eval.py rename to cosmos-inference/_src/vfm/callbacks/wandb_log_eval.py diff --git a/cosmos3/_src/vfm/checkpointer/__init__.py b/cosmos-inference/_src/vfm/checkpointer/__init__.py similarity index 100% rename from cosmos3/_src/vfm/checkpointer/__init__.py rename to cosmos-inference/_src/vfm/checkpointer/__init__.py diff --git a/cosmos3/_src/vfm/checkpointer/dcp.py b/cosmos-inference/_src/vfm/checkpointer/dcp.py similarity index 100% rename from cosmos3/_src/vfm/checkpointer/dcp.py rename to cosmos-inference/_src/vfm/checkpointer/dcp.py diff --git a/cosmos3/_src/vfm/conditioner.py b/cosmos-inference/_src/vfm/conditioner.py similarity index 100% rename from cosmos3/_src/vfm/conditioner.py rename to cosmos-inference/_src/vfm/conditioner.py diff --git a/cosmos3/_src/vfm/configs/__init__.py b/cosmos-inference/_src/vfm/configs/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/__init__.py rename to cosmos-inference/_src/vfm/configs/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/__init__.py b/cosmos-inference/_src/vfm/configs/base/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/__init__.py rename to cosmos-inference/_src/vfm/configs/base/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/config.py b/cosmos-inference/_src/vfm/configs/base/config.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/config.py rename to cosmos-inference/_src/vfm/configs/base/config.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/__init__.py b/cosmos-inference/_src/vfm/configs/base/defaults/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/__init__.py rename to cosmos-inference/_src/vfm/configs/base/defaults/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/callbacks.py b/cosmos-inference/_src/vfm/configs/base/defaults/callbacks.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/callbacks.py rename to cosmos-inference/_src/vfm/configs/base/defaults/callbacks.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py b/cosmos-inference/_src/vfm/configs/base/defaults/checkpointer.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/checkpointer.py rename to cosmos-inference/_src/vfm/configs/base/defaults/checkpointer.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/cluster.py b/cosmos-inference/_src/vfm/configs/base/defaults/cluster.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/cluster.py rename to cosmos-inference/_src/vfm/configs/base/defaults/cluster.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/conditioner.py b/cosmos-inference/_src/vfm/configs/base/defaults/conditioner.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/conditioner.py rename to cosmos-inference/_src/vfm/configs/base/defaults/conditioner.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/ema.py b/cosmos-inference/_src/vfm/configs/base/defaults/ema.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/ema.py rename to cosmos-inference/_src/vfm/configs/base/defaults/ema.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/model.py b/cosmos-inference/_src/vfm/configs/base/defaults/model.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/model.py rename to cosmos-inference/_src/vfm/configs/base/defaults/model.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/model_config.py b/cosmos-inference/_src/vfm/configs/base/defaults/model_config.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/model_config.py rename to cosmos-inference/_src/vfm/configs/base/defaults/model_config.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py b/cosmos-inference/_src/vfm/configs/base/defaults/multiview_dataloader.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py rename to cosmos-inference/_src/vfm/configs/base/defaults/multiview_dataloader.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/optimizer.py b/cosmos-inference/_src/vfm/configs/base/defaults/optimizer.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/optimizer.py rename to cosmos-inference/_src/vfm/configs/base/defaults/optimizer.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py b/cosmos-inference/_src/vfm/configs/base/defaults/tokenizer.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/tokenizer.py rename to cosmos-inference/_src/vfm/configs/base/defaults/tokenizer.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/unittest.py b/cosmos-inference/_src/vfm/configs/base/defaults/unittest.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/unittest.py rename to cosmos-inference/_src/vfm/configs/base/defaults/unittest.py diff --git a/cosmos3/_src/vfm/configs/base/defaults/vlm.py b/cosmos-inference/_src/vfm/configs/base/defaults/vlm.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/defaults/vlm.py rename to cosmos-inference/_src/vfm/configs/base/defaults/vlm.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/__init__.py b/cosmos-inference/_src/vfm/configs/base/vlm/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/__init__.py rename to cosmos-inference/_src/vfm/configs/base/vlm/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/config.py b/cosmos-inference/_src/vfm/configs/base/vlm/config.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/config.py rename to cosmos-inference/_src/vfm/configs/base/vlm/config.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/callbacks.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/callbacks.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/checkpointer.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/checkpointer.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/config.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/config.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/config.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/model.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/model.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/model.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/optimizer.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/optimizer.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/training.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/training.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/training.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py b/cosmos-inference/_src/vfm/configs/base/vlm/defaults/vlm_policy.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py rename to cosmos-inference/_src/vfm/configs/base/vlm/defaults/vlm_policy.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py b/cosmos-inference/_src/vfm/configs/base/vlm/experiment/__init__.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py rename to cosmos-inference/_src/vfm/configs/base/vlm/experiment/__init__.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py b/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py rename to cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py b/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py rename to cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py diff --git a/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py b/cosmos-inference/_src/vfm/configs/base/vlm/experiment/utils.py similarity index 100% rename from cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py rename to cosmos-inference/_src/vfm/configs/base/vlm/experiment/utils.py diff --git a/cosmos3/_src/vfm/datasets/__init__.py b/cosmos-inference/_src/vfm/datasets/__init__.py similarity index 100% rename from cosmos3/_src/vfm/datasets/__init__.py rename to cosmos-inference/_src/vfm/datasets/__init__.py diff --git a/cosmos3/_src/vfm/datasets/action/action_normalization.py b/cosmos-inference/_src/vfm/datasets/action/action_normalization.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/action_normalization.py rename to cosmos-inference/_src/vfm/datasets/action/action_normalization.py diff --git a/cosmos3/_src/vfm/datasets/action/action_spec.py b/cosmos-inference/_src/vfm/datasets/action/action_spec.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/action_spec.py rename to cosmos-inference/_src/vfm/datasets/action/action_spec.py diff --git a/cosmos3/_src/vfm/datasets/action/dataloaders.py b/cosmos-inference/_src/vfm/datasets/action/dataloaders.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/dataloaders.py rename to cosmos-inference/_src/vfm/datasets/action/dataloaders.py diff --git a/cosmos3/_src/vfm/datasets/action/domain_utils.py b/cosmos-inference/_src/vfm/datasets/action/domain_utils.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/domain_utils.py rename to cosmos-inference/_src/vfm/datasets/action/domain_utils.py diff --git a/cosmos3/_src/vfm/datasets/action/json_formatter.py b/cosmos-inference/_src/vfm/datasets/action/json_formatter.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/json_formatter.py rename to cosmos-inference/_src/vfm/datasets/action/json_formatter.py diff --git a/cosmos3/_src/vfm/datasets/action/libero_dataset.py b/cosmos-inference/_src/vfm/datasets/action/libero_dataset.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/libero_dataset.py rename to cosmos-inference/_src/vfm/datasets/action/libero_dataset.py diff --git a/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py b/cosmos-inference/_src/vfm/datasets/action/libero_pose_utils.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/libero_pose_utils.py rename to cosmos-inference/_src/vfm/datasets/action/libero_pose_utils.py diff --git a/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json b/cosmos-inference/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json similarity index 100% rename from cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json rename to cosmos-inference/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json diff --git a/cosmos3/_src/vfm/datasets/action/pose_utils.py b/cosmos-inference/_src/vfm/datasets/action/pose_utils.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/pose_utils.py rename to cosmos-inference/_src/vfm/datasets/action/pose_utils.py diff --git a/cosmos3/_src/vfm/datasets/action/transforms.py b/cosmos-inference/_src/vfm/datasets/action/transforms.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/transforms.py rename to cosmos-inference/_src/vfm/datasets/action/transforms.py diff --git a/cosmos3/_src/vfm/datasets/action/unified_dataset.py b/cosmos-inference/_src/vfm/datasets/action/unified_dataset.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/unified_dataset.py rename to cosmos-inference/_src/vfm/datasets/action/unified_dataset.py diff --git a/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py b/cosmos-inference/_src/vfm/datasets/action/viewpoint_utils.py similarity index 100% rename from cosmos3/_src/vfm/datasets/action/viewpoint_utils.py rename to cosmos-inference/_src/vfm/datasets/action/viewpoint_utils.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/__init__.py b/cosmos-inference/_src/vfm/datasets/augmentors/__init__.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/__init__.py rename to cosmos-inference/_src/vfm/datasets/augmentors/__init__.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py b/cosmos-inference/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py rename to cosmos-inference/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py b/cosmos-inference/_src/vfm/datasets/augmentors/audio_caption.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/audio_caption.py rename to cosmos-inference/_src/vfm/datasets/augmentors/audio_caption.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py b/cosmos-inference/_src/vfm/datasets/augmentors/audio_parsing.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py rename to cosmos-inference/_src/vfm/datasets/augmentors/audio_parsing.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py b/cosmos-inference/_src/vfm/datasets/augmentors/caption_filter.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/caption_filter.py rename to cosmos-inference/_src/vfm/datasets/augmentors/caption_filter.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/cropping.py b/cosmos-inference/_src/vfm/datasets/augmentors/cropping.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/cropping.py rename to cosmos-inference/_src/vfm/datasets/augmentors/cropping.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py b/cosmos-inference/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py rename to cosmos-inference/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py b/cosmos-inference/_src/vfm/datasets/augmentors/idle_frames_text_info.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py rename to cosmos-inference/_src/vfm/datasets/augmentors/idle_frames_text_info.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py b/cosmos-inference/_src/vfm/datasets/augmentors/image_editing_transform.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py rename to cosmos-inference/_src/vfm/datasets/augmentors/image_editing_transform.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py b/cosmos-inference/_src/vfm/datasets/augmentors/image_resolution_filter.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py rename to cosmos-inference/_src/vfm/datasets/augmentors/image_resolution_filter.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py b/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_image_transform.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py rename to cosmos-inference/_src/vfm/datasets/augmentors/interleaved_image_transform.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py b/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py rename to cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py b/cosmos-inference/_src/vfm/datasets/augmentors/merge_datadict.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py rename to cosmos-inference/_src/vfm/datasets/augmentors/merge_datadict.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py b/cosmos-inference/_src/vfm/datasets/augmentors/pkl_to_media.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py rename to cosmos-inference/_src/vfm/datasets/augmentors/pkl_to_media.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py b/cosmos-inference/_src/vfm/datasets/augmentors/resolution_text_info.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py rename to cosmos-inference/_src/vfm/datasets/augmentors/resolution_text_info.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py b/cosmos-inference/_src/vfm/datasets/augmentors/sequence_plan.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py rename to cosmos-inference/_src/vfm/datasets/augmentors/sequence_plan.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py b/cosmos-inference/_src/vfm/datasets/augmentors/sound_sequence_plan.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py rename to cosmos-inference/_src/vfm/datasets/augmentors/sound_sequence_plan.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py b/cosmos-inference/_src/vfm/datasets/augmentors/text_tokenizer.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py rename to cosmos-inference/_src/vfm/datasets/augmentors/text_tokenizer.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py b/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_image.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py rename to cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_image.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py b/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_video.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py rename to cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_video.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/blur.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/blur.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/seg.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/seg.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py b/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_transform.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py rename to cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_transform.py diff --git a/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py b/cosmos-inference/_src/vfm/datasets/augmentors/video_parsing.py similarity index 100% rename from cosmos3/_src/vfm/datasets/augmentors/video_parsing.py rename to cosmos-inference/_src/vfm/datasets/augmentors/video_parsing.py diff --git a/cosmos3/_src/vfm/datasets/joint_dataloader.py b/cosmos-inference/_src/vfm/datasets/joint_dataloader.py similarity index 100% rename from cosmos3/_src/vfm/datasets/joint_dataloader.py rename to cosmos-inference/_src/vfm/datasets/joint_dataloader.py diff --git a/cosmos3/_src/vfm/datasets/local_datasets/__init__.py b/cosmos-inference/_src/vfm/datasets/local_datasets/__init__.py similarity index 100% rename from cosmos3/_src/vfm/datasets/local_datasets/__init__.py rename to cosmos-inference/_src/vfm/datasets/local_datasets/__init__.py diff --git a/cosmos3/_src/vfm/datasets/local_datasets/helper.py b/cosmos-inference/_src/vfm/datasets/local_datasets/helper.py similarity index 100% rename from cosmos3/_src/vfm/datasets/local_datasets/helper.py rename to cosmos-inference/_src/vfm/datasets/local_datasets/helper.py diff --git a/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py b/cosmos-inference/_src/vfm/datasets/local_datasets/sft_dataset.py similarity index 100% rename from cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py rename to cosmos-inference/_src/vfm/datasets/local_datasets/sft_dataset.py diff --git a/cosmos3/_src/vfm/datasets/sequence_packing.py b/cosmos-inference/_src/vfm/datasets/sequence_packing.py similarity index 100% rename from cosmos3/_src/vfm/datasets/sequence_packing.py rename to cosmos-inference/_src/vfm/datasets/sequence_packing.py diff --git a/cosmos3/_src/vfm/datasets/utils.py b/cosmos-inference/_src/vfm/datasets/utils.py similarity index 100% rename from cosmos3/_src/vfm/datasets/utils.py rename to cosmos-inference/_src/vfm/datasets/utils.py diff --git a/cosmos3/_src/vfm/diffusion/__init__.py b/cosmos-inference/_src/vfm/diffusion/__init__.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/__init__.py rename to cosmos-inference/_src/vfm/diffusion/__init__.py diff --git a/cosmos3/_src/vfm/diffusion/rectified_flow.py b/cosmos-inference/_src/vfm/diffusion/rectified_flow.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/rectified_flow.py rename to cosmos-inference/_src/vfm/diffusion/rectified_flow.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/__init__.py b/cosmos-inference/_src/vfm/diffusion/samplers/__init__.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/__init__.py rename to cosmos-inference/_src/vfm/diffusion/samplers/__init__.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/edm.py b/cosmos-inference/_src/vfm/diffusion/samplers/edm.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/edm.py rename to cosmos-inference/_src/vfm/diffusion/samplers/edm.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py b/cosmos-inference/_src/vfm/diffusion/samplers/fixed_step.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/fixed_step.py rename to cosmos-inference/_src/vfm/diffusion/samplers/fixed_step.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py b/cosmos-inference/_src/vfm/diffusion/samplers/fm_solvers_unipc.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py rename to cosmos-inference/_src/vfm/diffusion/samplers/fm_solvers_unipc.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/unipc.py b/cosmos-inference/_src/vfm/diffusion/samplers/unipc.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/unipc.py rename to cosmos-inference/_src/vfm/diffusion/samplers/unipc.py diff --git a/cosmos3/_src/vfm/diffusion/samplers/utils.py b/cosmos-inference/_src/vfm/diffusion/samplers/utils.py similarity index 100% rename from cosmos3/_src/vfm/diffusion/samplers/utils.py rename to cosmos-inference/_src/vfm/diffusion/samplers/utils.py diff --git a/cosmos3/_src/vfm/evaluation/__init__.py b/cosmos-inference/_src/vfm/evaluation/__init__.py similarity index 100% rename from cosmos3/_src/vfm/evaluation/__init__.py rename to cosmos-inference/_src/vfm/evaluation/__init__.py diff --git a/cosmos3/_src/vfm/evaluation/action/__init__.py b/cosmos-inference/_src/vfm/evaluation/action/__init__.py similarity index 100% rename from cosmos3/_src/vfm/evaluation/action/__init__.py rename to cosmos-inference/_src/vfm/evaluation/action/__init__.py diff --git a/cosmos3/_src/vfm/evaluation/action/libero/__init__.py b/cosmos-inference/_src/vfm/evaluation/action/libero/__init__.py similarity index 100% rename from cosmos3/_src/vfm/evaluation/action/libero/__init__.py rename to cosmos-inference/_src/vfm/evaluation/action/libero/__init__.py diff --git a/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py b/cosmos-inference/_src/vfm/evaluation/action/libero/closed_loop_eval.py similarity index 100% rename from cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py rename to cosmos-inference/_src/vfm/evaluation/action/libero/closed_loop_eval.py diff --git a/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py b/cosmos-inference/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py similarity index 100% rename from cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py rename to cosmos-inference/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py diff --git a/cosmos3/_src/vfm/models/__init__.py b/cosmos-inference/_src/vfm/models/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/__init__.py rename to cosmos-inference/_src/vfm/models/__init__.py diff --git a/cosmos3/_src/vfm/models/hf_model.py b/cosmos-inference/_src/vfm/models/hf_model.py similarity index 100% rename from cosmos3/_src/vfm/models/hf_model.py rename to cosmos-inference/_src/vfm/models/hf_model.py diff --git a/cosmos3/_src/vfm/models/llm/__init__.py b/cosmos-inference/_src/vfm/models/llm/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/__init__.py rename to cosmos-inference/_src/vfm/models/llm/__init__.py diff --git a/cosmos3/_src/vfm/models/llm/qwen3/__init__.py b/cosmos-inference/_src/vfm/models/llm/qwen3/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/__init__.py rename to cosmos-inference/_src/vfm/models/llm/qwen3/__init__.py diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json b/cosmos-inference/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json rename to cosmos-inference/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py b/cosmos-inference/_src/vfm/models/llm/qwen3/configs/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py rename to cosmos-inference/_src/vfm/models/llm/qwen3/configs/__init__.py diff --git a/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py b/cosmos-inference/_src/vfm/models/llm/qwen3/configuration_qwen3.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py rename to cosmos-inference/_src/vfm/models/llm/qwen3/configuration_qwen3.py diff --git a/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py b/cosmos-inference/_src/vfm/models/llm/qwen3/qwen3.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/qwen3.py rename to cosmos-inference/_src/vfm/models/llm/qwen3/qwen3.py diff --git a/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py b/cosmos-inference/_src/vfm/models/llm/qwen3/test_qwen3.py similarity index 100% rename from cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py rename to cosmos-inference/_src/vfm/models/llm/qwen3/test_qwen3.py diff --git a/cosmos3/_src/vfm/models/mot/__init__.py b/cosmos-inference/_src/vfm/models/mot/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/__init__.py rename to cosmos-inference/_src/vfm/models/mot/__init__.py diff --git a/cosmos3/_src/vfm/models/mot/attention.py b/cosmos-inference/_src/vfm/models/mot/attention.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/attention.py rename to cosmos-inference/_src/vfm/models/mot/attention.py diff --git a/cosmos3/_src/vfm/models/mot/context_parallel_utils.py b/cosmos-inference/_src/vfm/models/mot/context_parallel_utils.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/context_parallel_utils.py rename to cosmos-inference/_src/vfm/models/mot/context_parallel_utils.py diff --git a/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py b/cosmos-inference/_src/vfm/models/mot/cosmos3_vfm_network.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py rename to cosmos-inference/_src/vfm/models/mot/cosmos3_vfm_network.py diff --git a/cosmos3/_src/vfm/models/mot/domain_aware_linear.py b/cosmos-inference/_src/vfm/models/mot/domain_aware_linear.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/domain_aware_linear.py rename to cosmos-inference/_src/vfm/models/mot/domain_aware_linear.py diff --git a/cosmos3/_src/vfm/models/mot/dot_product_attention.py b/cosmos-inference/_src/vfm/models/mot/dot_product_attention.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/dot_product_attention.py rename to cosmos-inference/_src/vfm/models/mot/dot_product_attention.py diff --git a/cosmos3/_src/vfm/models/mot/modeling_utils.py b/cosmos-inference/_src/vfm/models/mot/modeling_utils.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/modeling_utils.py rename to cosmos-inference/_src/vfm/models/mot/modeling_utils.py diff --git a/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py b/cosmos-inference/_src/vfm/models/mot/parallelize_unified_mot.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py rename to cosmos-inference/_src/vfm/models/mot/parallelize_unified_mot.py diff --git a/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py b/cosmos-inference/_src/vfm/models/mot/parallelize_vfm_network.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py rename to cosmos-inference/_src/vfm/models/mot/parallelize_vfm_network.py diff --git a/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py b/cosmos-inference/_src/vfm/models/mot/qwen3_vl_unified_mot.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py rename to cosmos-inference/_src/vfm/models/mot/qwen3_vl_unified_mot.py diff --git a/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py b/cosmos-inference/_src/vfm/models/mot/unified_3dmrope_utils.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py rename to cosmos-inference/_src/vfm/models/mot/unified_3dmrope_utils.py diff --git a/cosmos3/_src/vfm/models/mot/unified_mot.py b/cosmos-inference/_src/vfm/models/mot/unified_mot.py similarity index 100% rename from cosmos3/_src/vfm/models/mot/unified_mot.py rename to cosmos-inference/_src/vfm/models/mot/unified_mot.py diff --git a/cosmos3/_src/vfm/models/omni_mot_model.py b/cosmos-inference/_src/vfm/models/omni_mot_model.py similarity index 100% rename from cosmos3/_src/vfm/models/omni_mot_model.py rename to cosmos-inference/_src/vfm/models/omni_mot_model.py diff --git a/cosmos3/_src/vfm/models/parallelize_vlm.py b/cosmos-inference/_src/vfm/models/parallelize_vlm.py similarity index 100% rename from cosmos3/_src/vfm/models/parallelize_vlm.py rename to cosmos-inference/_src/vfm/models/parallelize_vlm.py diff --git a/cosmos3/_src/vfm/models/utils/__init__.py b/cosmos-inference/_src/vfm/models/utils/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/__init__.py rename to cosmos-inference/_src/vfm/models/utils/__init__.py diff --git a/cosmos3/_src/vfm/models/utils/data_and_condition.py b/cosmos-inference/_src/vfm/models/utils/data_and_condition.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/data_and_condition.py rename to cosmos-inference/_src/vfm/models/utils/data_and_condition.py diff --git a/cosmos3/_src/vfm/models/utils/dcp_loader.py b/cosmos-inference/_src/vfm/models/utils/dcp_loader.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/dcp_loader.py rename to cosmos-inference/_src/vfm/models/utils/dcp_loader.py diff --git a/cosmos3/_src/vfm/models/utils/load_balancing_loss.py b/cosmos-inference/_src/vfm/models/utils/load_balancing_loss.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/load_balancing_loss.py rename to cosmos-inference/_src/vfm/models/utils/load_balancing_loss.py diff --git a/cosmos3/_src/vfm/models/utils/memory.py b/cosmos-inference/_src/vfm/models/utils/memory.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/memory.py rename to cosmos-inference/_src/vfm/models/utils/memory.py diff --git a/cosmos3/_src/vfm/models/utils/safetensors_loader.py b/cosmos-inference/_src/vfm/models/utils/safetensors_loader.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/safetensors_loader.py rename to cosmos-inference/_src/vfm/models/utils/safetensors_loader.py diff --git a/cosmos3/_src/vfm/models/utils/taylorseer.py b/cosmos-inference/_src/vfm/models/utils/taylorseer.py similarity index 100% rename from cosmos3/_src/vfm/models/utils/taylorseer.py rename to cosmos-inference/_src/vfm/models/utils/taylorseer.py diff --git a/cosmos3/_src/vfm/models/vlm/__init__.py b/cosmos-inference/_src/vfm/models/vlm/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py b/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json b/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json rename to cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py b/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py rename to cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py diff --git a/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py b/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py rename to cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/utils.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/utils.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py diff --git a/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py b/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py rename to cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py diff --git a/cosmos3/_src/vfm/models/vlm_model.py b/cosmos-inference/_src/vfm/models/vlm_model.py similarity index 100% rename from cosmos3/_src/vfm/models/vlm_model.py rename to cosmos-inference/_src/vfm/models/vlm_model.py diff --git a/cosmos3/_src/vfm/tokenizers/__init__.py b/cosmos-inference/_src/vfm/tokenizers/__init__.py similarity index 100% rename from cosmos3/_src/vfm/tokenizers/__init__.py rename to cosmos-inference/_src/vfm/tokenizers/__init__.py diff --git a/cosmos3/_src/vfm/tokenizers/interface.py b/cosmos-inference/_src/vfm/tokenizers/interface.py similarity index 100% rename from cosmos3/_src/vfm/tokenizers/interface.py rename to cosmos-inference/_src/vfm/tokenizers/interface.py diff --git a/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py b/cosmos-inference/_src/vfm/tokenizers/tokenization_qwen2.py similarity index 100% rename from cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py rename to cosmos-inference/_src/vfm/tokenizers/tokenization_qwen2.py diff --git a/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py b/cosmos-inference/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py similarity index 100% rename from cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py rename to cosmos-inference/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py diff --git a/cosmos3/_src/vfm/utils/__init__.py b/cosmos-inference/_src/vfm/utils/__init__.py similarity index 100% rename from cosmos3/_src/vfm/utils/__init__.py rename to cosmos-inference/_src/vfm/utils/__init__.py diff --git a/cosmos3/_src/vfm/utils/context_parallel.py b/cosmos-inference/_src/vfm/utils/context_parallel.py similarity index 100% rename from cosmos3/_src/vfm/utils/context_parallel.py rename to cosmos-inference/_src/vfm/utils/context_parallel.py diff --git a/cosmos3/_src/vfm/utils/data_utils.py b/cosmos-inference/_src/vfm/utils/data_utils.py similarity index 100% rename from cosmos3/_src/vfm/utils/data_utils.py rename to cosmos-inference/_src/vfm/utils/data_utils.py diff --git a/cosmos3/_src/vfm/utils/dtensor_helper.py b/cosmos-inference/_src/vfm/utils/dtensor_helper.py similarity index 100% rename from cosmos3/_src/vfm/utils/dtensor_helper.py rename to cosmos-inference/_src/vfm/utils/dtensor_helper.py diff --git a/cosmos3/_src/vfm/utils/flash_attn.py b/cosmos-inference/_src/vfm/utils/flash_attn.py similarity index 100% rename from cosmos3/_src/vfm/utils/flash_attn.py rename to cosmos-inference/_src/vfm/utils/flash_attn.py diff --git a/cosmos3/_src/vfm/utils/fused_adam.py b/cosmos-inference/_src/vfm/utils/fused_adam.py similarity index 100% rename from cosmos3/_src/vfm/utils/fused_adam.py rename to cosmos-inference/_src/vfm/utils/fused_adam.py diff --git a/cosmos3/_src/vfm/utils/loss.py b/cosmos-inference/_src/vfm/utils/loss.py similarity index 100% rename from cosmos3/_src/vfm/utils/loss.py rename to cosmos-inference/_src/vfm/utils/loss.py diff --git a/cosmos3/_src/vfm/utils/misc.py b/cosmos-inference/_src/vfm/utils/misc.py similarity index 100% rename from cosmos3/_src/vfm/utils/misc.py rename to cosmos-inference/_src/vfm/utils/misc.py diff --git a/cosmos3/_src/vfm/utils/model_loader.py b/cosmos-inference/_src/vfm/utils/model_loader.py similarity index 100% rename from cosmos3/_src/vfm/utils/model_loader.py rename to cosmos-inference/_src/vfm/utils/model_loader.py diff --git a/cosmos3/_src/vfm/utils/model_weights_stats.py b/cosmos-inference/_src/vfm/utils/model_weights_stats.py similarity index 100% rename from cosmos3/_src/vfm/utils/model_weights_stats.py rename to cosmos-inference/_src/vfm/utils/model_weights_stats.py diff --git a/cosmos3/_src/vfm/utils/monkey_patch.py b/cosmos-inference/_src/vfm/utils/monkey_patch.py similarity index 100% rename from cosmos3/_src/vfm/utils/monkey_patch.py rename to cosmos-inference/_src/vfm/utils/monkey_patch.py diff --git a/cosmos3/_src/vfm/utils/optimizer.py b/cosmos-inference/_src/vfm/utils/optimizer.py similarity index 100% rename from cosmos3/_src/vfm/utils/optimizer.py rename to cosmos-inference/_src/vfm/utils/optimizer.py diff --git a/cosmos3/_src/vfm/utils/parallelism.py b/cosmos-inference/_src/vfm/utils/parallelism.py similarity index 100% rename from cosmos3/_src/vfm/utils/parallelism.py rename to cosmos-inference/_src/vfm/utils/parallelism.py diff --git a/cosmos3/_src/vfm/utils/rand_state.py b/cosmos-inference/_src/vfm/utils/rand_state.py similarity index 100% rename from cosmos3/_src/vfm/utils/rand_state.py rename to cosmos-inference/_src/vfm/utils/rand_state.py diff --git a/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py b/cosmos-inference/_src/vfm/utils/tokenizer_benchmarking.py similarity index 100% rename from cosmos3/_src/vfm/utils/tokenizer_benchmarking.py rename to cosmos-inference/_src/vfm/utils/tokenizer_benchmarking.py diff --git a/cosmos3/_src/vfm/utils/vlm/__init__.py b/cosmos-inference/_src/vfm/utils/vlm/__init__.py similarity index 100% rename from cosmos3/_src/vfm/utils/vlm/__init__.py rename to cosmos-inference/_src/vfm/utils/vlm/__init__.py diff --git a/cosmos3/_src/vfm/utils/vlm/optimizer.py b/cosmos-inference/_src/vfm/utils/vlm/optimizer.py similarity index 100% rename from cosmos3/_src/vfm/utils/vlm/optimizer.py rename to cosmos-inference/_src/vfm/utils/vlm/optimizer.py diff --git a/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py b/cosmos-inference/_src/vfm/utils/vlm/pretrained_models_downloader.py similarity index 100% rename from cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py rename to cosmos-inference/_src/vfm/utils/vlm/pretrained_models_downloader.py diff --git a/cosmos3/_test.py b/cosmos-inference/_test.py similarity index 100% rename from cosmos3/_test.py rename to cosmos-inference/_test.py diff --git a/cosmos3/_test/autoregressive.sh b/cosmos-inference/_test/autoregressive.sh similarity index 100% rename from cosmos3/_test/autoregressive.sh rename to cosmos-inference/_test/autoregressive.sh diff --git a/cosmos3/_test/distilled.sh b/cosmos-inference/_test/distilled.sh similarity index 100% rename from cosmos3/_test/distilled.sh rename to cosmos-inference/_test/distilled.sh diff --git a/cosmos3/_test/latency.sh b/cosmos-inference/_test/latency.sh similarity index 100% rename from cosmos3/_test/latency.sh rename to cosmos-inference/_test/latency.sh diff --git a/cosmos3/_test/omni-super.sh b/cosmos-inference/_test/omni-super.sh similarity index 100% rename from cosmos3/_test/omni-super.sh rename to cosmos-inference/_test/omni-super.sh diff --git a/cosmos3/_test/omni.sh b/cosmos-inference/_test/omni.sh similarity index 100% rename from cosmos3/_test/omni.sh rename to cosmos-inference/_test/omni.sh diff --git a/cosmos3/_test/omni_param.sh b/cosmos-inference/_test/omni_param.sh similarity index 100% rename from cosmos3/_test/omni_param.sh rename to cosmos-inference/_test/omni_param.sh diff --git a/cosmos3/_test/sft.sh b/cosmos-inference/_test/sft.sh similarity index 100% rename from cosmos3/_test/sft.sh rename to cosmos-inference/_test/sft.sh diff --git a/cosmos3/action.py b/cosmos-inference/action.py similarity index 100% rename from cosmos3/action.py rename to cosmos-inference/action.py diff --git a/cosmos3/args.py b/cosmos-inference/args.py similarity index 100% rename from cosmos3/args.py rename to cosmos-inference/args.py diff --git a/cosmos3/args_test.py b/cosmos-inference/args_test.py similarity index 100% rename from cosmos3/args_test.py rename to cosmos-inference/args_test.py diff --git a/cosmos3/common/__init__.py b/cosmos-inference/common/__init__.py similarity index 100% rename from cosmos3/common/__init__.py rename to cosmos-inference/common/__init__.py diff --git a/cosmos3/common/args.py b/cosmos-inference/common/args.py similarity index 100% rename from cosmos3/common/args.py rename to cosmos-inference/common/args.py diff --git a/cosmos3/common/args_test.py b/cosmos-inference/common/args_test.py similarity index 100% rename from cosmos3/common/args_test.py rename to cosmos-inference/common/args_test.py diff --git a/cosmos3/common/checkpoints.py b/cosmos-inference/common/checkpoints.py similarity index 100% rename from cosmos3/common/checkpoints.py rename to cosmos-inference/common/checkpoints.py diff --git a/cosmos3/common/config.py b/cosmos-inference/common/config.py similarity index 100% rename from cosmos3/common/config.py rename to cosmos-inference/common/config.py diff --git a/cosmos3/common/config_test.py b/cosmos-inference/common/config_test.py similarity index 100% rename from cosmos3/common/config_test.py rename to cosmos-inference/common/config_test.py diff --git a/cosmos3/common/inference.py b/cosmos-inference/common/inference.py similarity index 100% rename from cosmos3/common/inference.py rename to cosmos-inference/common/inference.py diff --git a/cosmos3/common/inference_test.py b/cosmos-inference/common/inference_test.py similarity index 100% rename from cosmos3/common/inference_test.py rename to cosmos-inference/common/inference_test.py diff --git a/cosmos3/common/init.py b/cosmos-inference/common/init.py similarity index 100% rename from cosmos3/common/init.py rename to cosmos-inference/common/init.py diff --git a/cosmos3/configs/experiment/action_policy_sft_8b.yaml b/cosmos-inference/configs/experiment/action_policy_sft_8b.yaml similarity index 100% rename from cosmos3/configs/experiment/action_policy_sft_8b.yaml rename to cosmos-inference/configs/experiment/action_policy_sft_8b.yaml diff --git a/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml b/cosmos-inference/configs/experiment/mixed_modality_sft_8b.yaml similarity index 100% rename from cosmos3/configs/experiment/mixed_modality_sft_8b.yaml rename to cosmos-inference/configs/experiment/mixed_modality_sft_8b.yaml diff --git a/cosmos3/dataset_samples.py b/cosmos-inference/dataset_samples.py similarity index 100% rename from cosmos3/dataset_samples.py rename to cosmos-inference/dataset_samples.py diff --git a/cosmos3/defaults/forward_dynamics/sample_args.json b/cosmos-inference/defaults/forward_dynamics/sample_args.json similarity index 100% rename from cosmos3/defaults/forward_dynamics/sample_args.json rename to cosmos-inference/defaults/forward_dynamics/sample_args.json diff --git a/cosmos3/defaults/image2video/sample_args.json b/cosmos-inference/defaults/image2video/sample_args.json similarity index 100% rename from cosmos3/defaults/image2video/sample_args.json rename to cosmos-inference/defaults/image2video/sample_args.json diff --git a/cosmos3/defaults/inverse_dynamics/sample_args.json b/cosmos-inference/defaults/inverse_dynamics/sample_args.json similarity index 100% rename from cosmos3/defaults/inverse_dynamics/sample_args.json rename to cosmos-inference/defaults/inverse_dynamics/sample_args.json diff --git a/cosmos3/defaults/policy/sample_args.json b/cosmos-inference/defaults/policy/sample_args.json similarity index 100% rename from cosmos3/defaults/policy/sample_args.json rename to cosmos-inference/defaults/policy/sample_args.json diff --git a/cosmos3/defaults/prompt_upsampler.txt b/cosmos-inference/defaults/prompt_upsampler.txt similarity index 100% rename from cosmos3/defaults/prompt_upsampler.txt rename to cosmos-inference/defaults/prompt_upsampler.txt diff --git a/cosmos3/defaults/text2image/sample_args.json b/cosmos-inference/defaults/text2image/sample_args.json similarity index 100% rename from cosmos3/defaults/text2image/sample_args.json rename to cosmos-inference/defaults/text2image/sample_args.json diff --git a/cosmos3/defaults/text2video/sample_args.json b/cosmos-inference/defaults/text2video/sample_args.json similarity index 100% rename from cosmos3/defaults/text2video/sample_args.json rename to cosmos-inference/defaults/text2video/sample_args.json diff --git a/cosmos3/defaults/video2video/sample_args.json b/cosmos-inference/defaults/video2video/sample_args.json similarity index 100% rename from cosmos3/defaults/video2video/sample_args.json rename to cosmos-inference/defaults/video2video/sample_args.json diff --git a/cosmos3/defaults/video_captioner.txt b/cosmos-inference/defaults/video_captioner.txt similarity index 100% rename from cosmos3/defaults/video_captioner.txt rename to cosmos-inference/defaults/video_captioner.txt diff --git a/cosmos3/fixtures/__init__.py b/cosmos-inference/fixtures/__init__.py similarity index 100% rename from cosmos3/fixtures/__init__.py rename to cosmos-inference/fixtures/__init__.py diff --git a/cosmos3/fixtures/args.py b/cosmos-inference/fixtures/args.py similarity index 100% rename from cosmos3/fixtures/args.py rename to cosmos-inference/fixtures/args.py diff --git a/cosmos3/fixtures/script.py b/cosmos-inference/fixtures/script.py similarity index 100% rename from cosmos3/fixtures/script.py rename to cosmos-inference/fixtures/script.py diff --git a/cosmos3/flags.py b/cosmos-inference/flags.py similarity index 100% rename from cosmos3/flags.py rename to cosmos-inference/flags.py diff --git a/cosmos3/inference.py b/cosmos-inference/inference.py similarity index 100% rename from cosmos3/inference.py rename to cosmos-inference/inference.py diff --git a/cosmos3/model.py b/cosmos-inference/model.py similarity index 100% rename from cosmos3/model.py rename to cosmos-inference/model.py diff --git a/cosmos3/model_test.py b/cosmos-inference/model_test.py similarity index 100% rename from cosmos3/model_test.py rename to cosmos-inference/model_test.py diff --git a/cosmos3/ray/__init__.py b/cosmos-inference/ray/__init__.py similarity index 100% rename from cosmos3/ray/__init__.py rename to cosmos-inference/ray/__init__.py diff --git a/cosmos3/ray/configs/latency.yaml b/cosmos-inference/ray/configs/latency.yaml similarity index 100% rename from cosmos3/ray/configs/latency.yaml rename to cosmos-inference/ray/configs/latency.yaml diff --git a/cosmos3/ray/configs/throughput.yaml b/cosmos-inference/ray/configs/throughput.yaml similarity index 100% rename from cosmos3/ray/configs/throughput.yaml rename to cosmos-inference/ray/configs/throughput.yaml diff --git a/cosmos3/ray/gradio.py b/cosmos-inference/ray/gradio.py similarity index 100% rename from cosmos3/ray/gradio.py rename to cosmos-inference/ray/gradio.py diff --git a/cosmos3/ray/serve.py b/cosmos-inference/ray/serve.py similarity index 100% rename from cosmos3/ray/serve.py rename to cosmos-inference/ray/serve.py diff --git a/cosmos3/ray/serve_test.py b/cosmos-inference/ray/serve_test.py similarity index 100% rename from cosmos3/ray/serve_test.py rename to cosmos-inference/ray/serve_test.py diff --git a/cosmos3/ray/submit.py b/cosmos-inference/ray/submit.py similarity index 100% rename from cosmos3/ray/submit.py rename to cosmos-inference/ray/submit.py diff --git a/cosmos3/scripts/__init__.py b/cosmos-inference/scripts/__init__.py similarity index 100% rename from cosmos3/scripts/__init__.py rename to cosmos-inference/scripts/__init__.py diff --git a/cosmos3/scripts/_test.py b/cosmos-inference/scripts/_test.py similarity index 100% rename from cosmos3/scripts/_test.py rename to cosmos-inference/scripts/_test.py diff --git a/cosmos3/scripts/_test/convert_model_to_dcp.sh b/cosmos-inference/scripts/_test/convert_model_to_dcp.sh similarity index 100% rename from cosmos3/scripts/_test/convert_model_to_dcp.sh rename to cosmos-inference/scripts/_test/convert_model_to_dcp.sh diff --git a/cosmos3/scripts/_test/convert_model_to_diffusers.sh b/cosmos-inference/scripts/_test/convert_model_to_diffusers.sh similarity index 100% rename from cosmos3/scripts/_test/convert_model_to_diffusers.sh rename to cosmos-inference/scripts/_test/convert_model_to_diffusers.sh diff --git a/cosmos3/scripts/_test/export_config.sh b/cosmos-inference/scripts/_test/export_config.sh similarity index 100% rename from cosmos3/scripts/_test/export_config.sh rename to cosmos-inference/scripts/_test/export_config.sh diff --git a/cosmos3/scripts/_test/export_model.sh b/cosmos-inference/scripts/_test/export_model.sh similarity index 100% rename from cosmos3/scripts/_test/export_model.sh rename to cosmos-inference/scripts/_test/export_model.sh diff --git a/cosmos3/scripts/action_policy_server.py b/cosmos-inference/scripts/action_policy_server.py similarity index 100% rename from cosmos3/scripts/action_policy_server.py rename to cosmos-inference/scripts/action_policy_server.py diff --git a/cosmos3/scripts/caption_from_video.py b/cosmos-inference/scripts/caption_from_video.py similarity index 100% rename from cosmos3/scripts/caption_from_video.py rename to cosmos-inference/scripts/caption_from_video.py diff --git a/cosmos3/scripts/captions_to_sft_jsonl.py b/cosmos-inference/scripts/captions_to_sft_jsonl.py similarity index 100% rename from cosmos3/scripts/captions_to_sft_jsonl.py rename to cosmos-inference/scripts/captions_to_sft_jsonl.py diff --git a/cosmos3/scripts/convert_model_to_dcp.py b/cosmos-inference/scripts/convert_model_to_dcp.py similarity index 100% rename from cosmos3/scripts/convert_model_to_dcp.py rename to cosmos-inference/scripts/convert_model_to_dcp.py diff --git a/cosmos3/scripts/eval_utils.py b/cosmos-inference/scripts/eval_utils.py similarity index 100% rename from cosmos3/scripts/eval_utils.py rename to cosmos-inference/scripts/eval_utils.py diff --git a/cosmos3/scripts/export_config.py b/cosmos-inference/scripts/export_config.py similarity index 100% rename from cosmos3/scripts/export_config.py rename to cosmos-inference/scripts/export_config.py diff --git a/cosmos3/scripts/export_model.py b/cosmos-inference/scripts/export_model.py similarity index 100% rename from cosmos3/scripts/export_model.py rename to cosmos-inference/scripts/export_model.py diff --git a/cosmos3/scripts/export_schemas.py b/cosmos-inference/scripts/export_schemas.py similarity index 100% rename from cosmos3/scripts/export_schemas.py rename to cosmos-inference/scripts/export_schemas.py diff --git a/cosmos3/scripts/export_schemas_test.py b/cosmos-inference/scripts/export_schemas_test.py similarity index 100% rename from cosmos3/scripts/export_schemas_test.py rename to cosmos-inference/scripts/export_schemas_test.py diff --git a/cosmos3/scripts/hydra.py b/cosmos-inference/scripts/hydra.py similarity index 100% rename from cosmos3/scripts/hydra.py rename to cosmos-inference/scripts/hydra.py diff --git a/cosmos3/scripts/inference.py b/cosmos-inference/scripts/inference.py similarity index 100% rename from cosmos3/scripts/inference.py rename to cosmos-inference/scripts/inference.py diff --git a/cosmos3/scripts/prefetch_hf_checkpoints.py b/cosmos-inference/scripts/prefetch_hf_checkpoints.py similarity index 100% rename from cosmos3/scripts/prefetch_hf_checkpoints.py rename to cosmos-inference/scripts/prefetch_hf_checkpoints.py diff --git a/cosmos3/scripts/train.py b/cosmos-inference/scripts/train.py similarity index 100% rename from cosmos3/scripts/train.py rename to cosmos-inference/scripts/train.py diff --git a/cosmos3/scripts/upsample_prompts.py b/cosmos-inference/scripts/upsample_prompts.py similarity index 100% rename from cosmos3/scripts/upsample_prompts.py rename to cosmos-inference/scripts/upsample_prompts.py diff --git a/cosmos3/vision.py b/cosmos-inference/vision.py similarity index 100% rename from cosmos3/vision.py rename to cosmos-inference/vision.py diff --git a/cosmos/__init__.py b/cosmos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/algorithm/__init__.py b/cosmos/algorithm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/algorithm/loss/__init__.py b/cosmos/algorithm/loss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/algorithm/reward/__init__.py b/cosmos/algorithm/reward/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/algorithm/rl/__init__.py b/cosmos/algorithm/rl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/callbacks/__init__.py b/cosmos/callbacks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/checkpoint/__init__.py b/cosmos/checkpoint/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/communicator/__init__.py b/cosmos/communicator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/controller/__init__.py b/cosmos/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/data/__init__.py b/cosmos/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/evaluation/__init__.py b/cosmos/evaluation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/inference/__init__.py b/cosmos/inference/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/launcher/__init__.py b/cosmos/launcher/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/model/__init__.py b/cosmos/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/tools/__init__.py b/cosmos/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/trainer/__init__.py b/cosmos/trainer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/utils/__init__.py b/cosmos/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/workers/__init__.py b/cosmos/workers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/workers/reference/__init__.py b/cosmos/workers/reference/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/workers/reward/__init__.py b/cosmos/workers/reward/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/workers/rollout/__init__.py b/cosmos/workers/rollout/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos/workers/simulations/__init__.py b/cosmos/workers/simulations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tools/.gitkeep b/tools/.gitkeep new file mode 100644 index 00000000..e69de29b From 619256f1551f2ac5783b421f0c2b9ac90a39b2e8 Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Mon, 11 May 2026 21:34:54 -0700 Subject: [PATCH 04/24] Consolidate Cosmos3 content under cosmos-inference/, restore root layout Move all cosmos3-internal-originated files at the repo root (dotfiles, config, ci/, docker/, docs/, examples/, inputs/, schemas/, vllm-cosmos3/, AGENTS.md, ATTRIBUTIONS.md, CHANGELOG.md, CONTRIBUTING.md, Dockerfile, LICENSE, conftest.py, justfile, pyproject.toml, pyrefly.toml, uv.lock) into cosmos-inference/, so the inference package is self-contained. Root now matches the planned framework layout: - cosmos/ (new framework skeleton from prior commit) - cosmos-inference/ (full Cosmos3 codebase) - examples/, docs/, docker/, tests/, tools/ (fresh placeholders) - README.md, RELEASE.md, cosmos-logo-thumbnail.png (originals) Other changes: - Root README.md restored to the original deprecated-notice version. - cosmos-inference/README.md added with the Cosmos3 documentation (previously merged into root README). - cosmos-inference/.gitattributes: drop the stale `cosmos-logo-thumbnail.png` LFS override (the file lives at root, not inside cosmos-inference/). Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 146 +----------------- .../skills/cosmos3-codebase-nav/SKILL.md | 0 .../skills/cosmos3-env-troubleshoot/SKILL.md | 0 .../skills/cosmos3-inference/SKILL.md | 0 .../skills/cosmos3-post-training/SKILL.md | 0 .../.agents}/skills/cosmos3-setup/SKILL.md | 0 .../skills/cosmos3-codebase-nav/SKILL.md | 0 .../skills/cosmos3-env-troubleshoot/SKILL.md | 0 .../skills/cosmos3-inference/SKILL.md | 0 .../skills/cosmos3-post-training/SKILL.md | 0 .../.claude}/skills/cosmos3-setup/SKILL.md | 0 .../.config}/rumdl.toml | 0 .coveragerc => cosmos-inference/.coveragerc | 0 .../.dockerignore | 0 .../.gitattributes | 3 - .../.github}/ISSUE_TEMPLATE/bug_report.md | 0 .../.github}/workflows/pre-commit.yml | 0 .gitignore => cosmos-inference/.gitignore | 0 .../.gitleaks.toml | 0 .../.pre-commit-config.yaml | 0 .pytest.toml => cosmos-inference/.pytest.toml | 0 .../.python-version | 0 .ruff.toml => cosmos-inference/.ruff.toml | 0 AGENTS.md => cosmos-inference/AGENTS.md | 0 .../ATTRIBUTIONS.md | 0 CHANGELOG.md => cosmos-inference/CHANGELOG.md | 0 .../CONTRIBUTING.md | 0 Dockerfile => cosmos-inference/Dockerfile | 0 LICENSE => cosmos-inference/LICENSE | 0 cosmos-inference/README.md | 137 ++++++++++++++++ {ci => cosmos-inference/ci}/.link-check.json | 0 .../ci}/.markdown-toc-creator.toml | 0 .../ci}/.pre-commit-config-base.yaml | 0 {ci => cosmos-inference/ci}/license.txt | 0 {ci => cosmos-inference/ci}/uv_lock.sh | 0 {ci => cosmos-inference/ci}/uv_lock_script.sh | 0 conftest.py => cosmos-inference/conftest.py | 0 .../docker}/ci.Dockerfile | 0 .../docker}/entrypoint.sh | 0 .../docker}/nightly.Dockerfile | 0 {docs => cosmos-inference/docs}/Cosmos3.pdf | Bin .../docs}/action_policy_closed_loop_eval.md | 0 {docs => cosmos-inference/docs}/faq.md | 0 {docs => cosmos-inference/docs}/gallery.md | 0 {docs => cosmos-inference/docs}/inference.md | 0 .../docs}/inference_online.md | 0 {docs => cosmos-inference/docs}/prompting.md | 0 {docs => cosmos-inference/docs}/setup.md | 0 {docs => cosmos-inference/docs}/training.md | 0 .../examples}/_test.py | 0 .../examples}/_test/inference.sh | 0 .../examples}/_test/inference_pipeline.sh | 0 .../examples}/inference.py | 0 .../examples}/inference_pipeline.py | 0 .../action_forward_dynamics_camera_0.json | 0 .../action_forward_dynamics_camera_1.json | 0 .../omni/action_forward_dynamics_robot.json | 0 .../omni/action_inverse_dynamics_av.json | 0 .../inputs}/omni/action_policy_robot.json | 0 .../inputs}/omni/i2v.json | 0 .../inputs}/omni/t2i.json | 0 .../inputs}/omni/t2v.json | 0 .../inputs}/t2v_long_prompts.jsonl | 0 justfile => cosmos-inference/justfile | 0 .../pyproject.toml | 0 pyrefly.toml => cosmos-inference/pyrefly.toml | 0 .../schemas}/OmniSampleOverrides.schema.json | 0 .../schemas}/OmniSampleOverrides.yaml | 0 .../schemas}/OmniSetupOverrides.schema.json | 0 .../schemas}/OmniSetupOverrides.yaml | 0 uv.lock => cosmos-inference/uv.lock | 0 .../vllm-cosmos3}/README.md | 0 .../vllm-cosmos3}/pyproject.toml | 0 .../vllm-cosmos3}/uv.lock | 0 .../vllm-cosmos3}/vllm_cosmos3/__init__.py | 0 .../vllm-cosmos3}/vllm_cosmos3/model.py | 0 docker/.gitkeep | 0 docs/.gitkeep | 0 examples/.gitkeep | 0 79 files changed, 145 insertions(+), 141 deletions(-) rename {.agents => cosmos-inference/.agents}/skills/cosmos3-codebase-nav/SKILL.md (100%) rename {.agents => cosmos-inference/.agents}/skills/cosmos3-env-troubleshoot/SKILL.md (100%) rename {.agents => cosmos-inference/.agents}/skills/cosmos3-inference/SKILL.md (100%) rename {.agents => cosmos-inference/.agents}/skills/cosmos3-post-training/SKILL.md (100%) rename {.agents => cosmos-inference/.agents}/skills/cosmos3-setup/SKILL.md (100%) rename {.claude => cosmos-inference/.claude}/skills/cosmos3-codebase-nav/SKILL.md (100%) rename {.claude => cosmos-inference/.claude}/skills/cosmos3-env-troubleshoot/SKILL.md (100%) rename {.claude => cosmos-inference/.claude}/skills/cosmos3-inference/SKILL.md (100%) rename {.claude => cosmos-inference/.claude}/skills/cosmos3-post-training/SKILL.md (100%) rename {.claude => cosmos-inference/.claude}/skills/cosmos3-setup/SKILL.md (100%) rename {.config => cosmos-inference/.config}/rumdl.toml (100%) rename .coveragerc => cosmos-inference/.coveragerc (100%) rename .dockerignore => cosmos-inference/.dockerignore (100%) rename .gitattributes => cosmos-inference/.gitattributes (89%) rename {.github => cosmos-inference/.github}/ISSUE_TEMPLATE/bug_report.md (100%) rename {.github => cosmos-inference/.github}/workflows/pre-commit.yml (100%) rename .gitignore => cosmos-inference/.gitignore (100%) rename .gitleaks.toml => cosmos-inference/.gitleaks.toml (100%) rename .pre-commit-config.yaml => cosmos-inference/.pre-commit-config.yaml (100%) rename .pytest.toml => cosmos-inference/.pytest.toml (100%) rename .python-version => cosmos-inference/.python-version (100%) rename .ruff.toml => cosmos-inference/.ruff.toml (100%) rename AGENTS.md => cosmos-inference/AGENTS.md (100%) rename ATTRIBUTIONS.md => cosmos-inference/ATTRIBUTIONS.md (100%) rename CHANGELOG.md => cosmos-inference/CHANGELOG.md (100%) rename CONTRIBUTING.md => cosmos-inference/CONTRIBUTING.md (100%) rename Dockerfile => cosmos-inference/Dockerfile (100%) rename LICENSE => cosmos-inference/LICENSE (100%) create mode 100644 cosmos-inference/README.md rename {ci => cosmos-inference/ci}/.link-check.json (100%) rename {ci => cosmos-inference/ci}/.markdown-toc-creator.toml (100%) rename {ci => cosmos-inference/ci}/.pre-commit-config-base.yaml (100%) rename {ci => cosmos-inference/ci}/license.txt (100%) rename {ci => cosmos-inference/ci}/uv_lock.sh (100%) rename {ci => cosmos-inference/ci}/uv_lock_script.sh (100%) rename conftest.py => cosmos-inference/conftest.py (100%) rename {docker => cosmos-inference/docker}/ci.Dockerfile (100%) rename {docker => cosmos-inference/docker}/entrypoint.sh (100%) rename {docker => cosmos-inference/docker}/nightly.Dockerfile (100%) rename {docs => cosmos-inference/docs}/Cosmos3.pdf (100%) rename {docs => cosmos-inference/docs}/action_policy_closed_loop_eval.md (100%) rename {docs => cosmos-inference/docs}/faq.md (100%) rename {docs => cosmos-inference/docs}/gallery.md (100%) rename {docs => cosmos-inference/docs}/inference.md (100%) rename {docs => cosmos-inference/docs}/inference_online.md (100%) rename {docs => cosmos-inference/docs}/prompting.md (100%) rename {docs => cosmos-inference/docs}/setup.md (100%) rename {docs => cosmos-inference/docs}/training.md (100%) rename {examples => cosmos-inference/examples}/_test.py (100%) rename {examples => cosmos-inference/examples}/_test/inference.sh (100%) rename {examples => cosmos-inference/examples}/_test/inference_pipeline.sh (100%) rename {examples => cosmos-inference/examples}/inference.py (100%) rename {examples => cosmos-inference/examples}/inference_pipeline.py (100%) rename {inputs => cosmos-inference/inputs}/omni/action_forward_dynamics_camera_0.json (100%) rename {inputs => cosmos-inference/inputs}/omni/action_forward_dynamics_camera_1.json (100%) rename {inputs => cosmos-inference/inputs}/omni/action_forward_dynamics_robot.json (100%) rename {inputs => cosmos-inference/inputs}/omni/action_inverse_dynamics_av.json (100%) rename {inputs => cosmos-inference/inputs}/omni/action_policy_robot.json (100%) rename {inputs => cosmos-inference/inputs}/omni/i2v.json (100%) rename {inputs => cosmos-inference/inputs}/omni/t2i.json (100%) rename {inputs => cosmos-inference/inputs}/omni/t2v.json (100%) rename {inputs => cosmos-inference/inputs}/t2v_long_prompts.jsonl (100%) rename justfile => cosmos-inference/justfile (100%) rename pyproject.toml => cosmos-inference/pyproject.toml (100%) rename pyrefly.toml => cosmos-inference/pyrefly.toml (100%) rename {schemas => cosmos-inference/schemas}/OmniSampleOverrides.schema.json (100%) rename {schemas => cosmos-inference/schemas}/OmniSampleOverrides.yaml (100%) rename {schemas => cosmos-inference/schemas}/OmniSetupOverrides.schema.json (100%) rename {schemas => cosmos-inference/schemas}/OmniSetupOverrides.yaml (100%) rename uv.lock => cosmos-inference/uv.lock (100%) rename {vllm-cosmos3 => cosmos-inference/vllm-cosmos3}/README.md (100%) rename {vllm-cosmos3 => cosmos-inference/vllm-cosmos3}/pyproject.toml (100%) rename {vllm-cosmos3 => cosmos-inference/vllm-cosmos3}/uv.lock (100%) rename {vllm-cosmos3 => cosmos-inference/vllm-cosmos3}/vllm_cosmos3/__init__.py (100%) rename {vllm-cosmos3 => cosmos-inference/vllm-cosmos3}/vllm_cosmos3/model.py (100%) create mode 100644 docker/.gitkeep create mode 100644 docs/.gitkeep create mode 100644 examples/.gitkeep diff --git a/README.md b/README.md index 698335f8..3c833802 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,13 @@

-NVIDIA Cosmos Logo

+

+ New GitHub page for NVIDIA Cosmos:
+ https://github.com/nvidia-cosmos +

-

🤗 Hugging Face | Paper Draft

- -> **Note:** The initial CES2025 release of NVIDIA Cosmos lives on the [`archived-ces2025`](https://github.com/NVIDIA/Cosmos/tree/archived-ces2025) branch. For other NVIDIA Cosmos projects, visit the [NVIDIA Cosmos](https://github.com/nvidia-cosmos) GitHub organization. - -# Cosmos3 - -## User Guide - -- [Gallery](./docs/gallery.md) -- [Quickstart](#setup) -- [Setup](./docs/setup.md): Installation, environment, checkpoints -- [Prompting](./docs/prompting.md): Prompt engineering, upsampling -- [Inference](./docs/inference.md): Sample arguments, default values, offline batch inference - - [Online Inference](./docs/inference_online.md): Online serving, web UI - - [Action Policy Closed-Loop Evaluation on LIBERO](./docs/action_policy_closed_loop_eval.md): Action policy server/client setup and LIBERO evaluation. The client setup clones and installs the external [Lifelong-Robot-Learning/LIBERO](https://github.com/Lifelong-Robot-Learning/LIBERO) simulator package. -- [Training](./docs/training.md): Post-Training (Supervised Fine-Tuning) -- [FAQ](./docs/faq.md): FAQ, tips, troubleshooting -- [AGENTS.md](./AGENTS.md): AI agent entry point — start here for quick codebase orientation - -## Overview - -**Cosmos3** is a world foundation model that unifies understanding and generation within a single Mixture-of-Transformer (MoT) architecture. Two tightly coupled towers—a **Reasoner** (vision-language model) and a **Generator** (world simulator)—share latent representations so that structured perception directly grounds realistic, temporally consistent simulation. - -

Image

- -One model, many capabilities: - -| Input Modality | Output Modality | Application | EA1 | -| ----------------------- | --------------- | --------------------- | ------------ | -| Video \| Text | Video | Video Generator | ✅ | -| Video \| Text | Text | Vision Language Model | Coming soon! | -| Action \| Video \| Text | Video | World Model | ✅ | -| Video \| Text | Video & Action | Policy Model | ✅ | - -## Supported Features (Cosmos3 EA1 — Robotics Backbone) - -### User Stories - -- **Video Backbone**: Evaluate and benchmark the model’s task understanding and review its architecture to inform codebase decisions. - -### Modalities Supported - -- Text2Image (t2i), Text2Video (t2v), Image2Video (i2v) - -### Base Model Specifications - -| Spec | Value | -| ---------------- | -------------------------------------------------------------------------- | -| Model Size | Nano, Super | -| Resolution | 256p / 480p / 720p | -| Frame Rate (FPS) | 10–30 | -| Num of Frames | Default: 189 (max by resolution: `256p → 400`, `480p → 300`, `720p → 200`) | -| Max Duration | Variable | -| View | Single view only | - -## Setup - -For more details and alternative installation methods, see [Setup](./docs/setup.md#installation). - -Install system dependencies: - -```shell -sudo apt-get update && sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget -``` - -Install the package with `uv`: - -```shell -uv sync --all-extras --group=cu130-train -source .venv/bin/activate && export LD_LIBRARY_PATH= -``` - -## Prompting - -For prompting guidance, see [Prompting](./docs/prompting.md). - -## Inference - -For more details, see [Inference](./docs/inference.md). - -### Offline Batch Inference - -Generate a single sample with 1 GPU: - -```shell -python -m cosmos3.scripts.inference \ - --parallelism-preset=latency \ - -i "inputs/omni/t2v.json" \ - -o outputs/omni_nano \ - --checkpoint-path Cosmos3-Nano \ - --seed=0 -``` - -Generate multiple samples with 8 GPUs (~5 mins): - -```shell -torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ - --parallelism-preset=throughput \ - -i "inputs/omni/*.json" \ - -o outputs/omni_nano \ - --checkpoint-path Cosmos3-Nano \ - --seed=0 -``` - -**Note:** The progress bar only prints on rank 0. - -### Models - -| Model | Arguments | -| ------------- | --------------------------------- | -| Cosmos3-Nano | `--checkpoint-path=Cosmos3-Nano` | -| Cosmos3-Super | `--checkpoint-path=Cosmos3-Super` | - -### Modalities - -| Modality | Example | -| ---------------- | -------------------------------------------------------------------------------------------------- | -| Text2Image | [`-i "inputs/omni/t2i.json"`](inputs/omni/t2i.json) | -| Text2Video | [`-i "inputs/omni/t2v.json"`](inputs/omni/t2v.json) | -| Image2Video | [`-i "inputs/omni/i2v.json"`](inputs/omni/i2v.json) | -| Forward Dynamics | [`-i "inputs/omni/action_forward_dynamics*.json"`](inputs/omni/action_forward_dynamics_robot.json) | -| Inverse Dynamics | [`-i "inputs/omni/action_inverse_dynamics*.json"`](inputs/omni/action_inverse_dynamics_av.json) | -| Policy | [`-i "inputs/omni/action_policy*.json"`](inputs/omni/action_policy_robot.json) | - -To generate all examples, use `-i "inputs/omni/*.json"`. - -For more information regarding action inference, please see [Action Inference](./docs/inference.md#action-inference). - -### CLI Reference - -To see all available arguments: - -```shell -python -m cosmos3.scripts.inference --help -``` +**This repository has been deprecated and is no longer maintained.** To view the initial release of NVIDIA Cosmos from this repository, please check out branch `archived-ces2025`. diff --git a/.agents/skills/cosmos3-codebase-nav/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-codebase-nav/SKILL.md similarity index 100% rename from .agents/skills/cosmos3-codebase-nav/SKILL.md rename to cosmos-inference/.agents/skills/cosmos3-codebase-nav/SKILL.md diff --git a/.agents/skills/cosmos3-env-troubleshoot/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-env-troubleshoot/SKILL.md similarity index 100% rename from .agents/skills/cosmos3-env-troubleshoot/SKILL.md rename to cosmos-inference/.agents/skills/cosmos3-env-troubleshoot/SKILL.md diff --git a/.agents/skills/cosmos3-inference/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-inference/SKILL.md similarity index 100% rename from .agents/skills/cosmos3-inference/SKILL.md rename to cosmos-inference/.agents/skills/cosmos3-inference/SKILL.md diff --git a/.agents/skills/cosmos3-post-training/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md similarity index 100% rename from .agents/skills/cosmos3-post-training/SKILL.md rename to cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md diff --git a/.agents/skills/cosmos3-setup/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-setup/SKILL.md similarity index 100% rename from .agents/skills/cosmos3-setup/SKILL.md rename to cosmos-inference/.agents/skills/cosmos3-setup/SKILL.md diff --git a/.claude/skills/cosmos3-codebase-nav/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-codebase-nav/SKILL.md similarity index 100% rename from .claude/skills/cosmos3-codebase-nav/SKILL.md rename to cosmos-inference/.claude/skills/cosmos3-codebase-nav/SKILL.md diff --git a/.claude/skills/cosmos3-env-troubleshoot/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-env-troubleshoot/SKILL.md similarity index 100% rename from .claude/skills/cosmos3-env-troubleshoot/SKILL.md rename to cosmos-inference/.claude/skills/cosmos3-env-troubleshoot/SKILL.md diff --git a/.claude/skills/cosmos3-inference/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-inference/SKILL.md similarity index 100% rename from .claude/skills/cosmos3-inference/SKILL.md rename to cosmos-inference/.claude/skills/cosmos3-inference/SKILL.md diff --git a/.claude/skills/cosmos3-post-training/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md similarity index 100% rename from .claude/skills/cosmos3-post-training/SKILL.md rename to cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md diff --git a/.claude/skills/cosmos3-setup/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-setup/SKILL.md similarity index 100% rename from .claude/skills/cosmos3-setup/SKILL.md rename to cosmos-inference/.claude/skills/cosmos3-setup/SKILL.md diff --git a/.config/rumdl.toml b/cosmos-inference/.config/rumdl.toml similarity index 100% rename from .config/rumdl.toml rename to cosmos-inference/.config/rumdl.toml diff --git a/.coveragerc b/cosmos-inference/.coveragerc similarity index 100% rename from .coveragerc rename to cosmos-inference/.coveragerc diff --git a/.dockerignore b/cosmos-inference/.dockerignore similarity index 100% rename from .dockerignore rename to cosmos-inference/.dockerignore diff --git a/.gitattributes b/cosmos-inference/.gitattributes similarity index 89% rename from .gitattributes rename to cosmos-inference/.gitattributes index 289b63d0..dba17f50 100644 --- a/.gitattributes +++ b/cosmos-inference/.gitattributes @@ -23,6 +23,3 @@ assets/** filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.tiff filter=lfs diff=lfs merge=lfs -text *.bmp filter=lfs diff=lfs merge=lfs -text - -# Preserved root assets (not LFS) -cosmos-logo-thumbnail.png !filter !diff !merge text=auto diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/cosmos-inference/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to cosmos-inference/.github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/workflows/pre-commit.yml b/cosmos-inference/.github/workflows/pre-commit.yml similarity index 100% rename from .github/workflows/pre-commit.yml rename to cosmos-inference/.github/workflows/pre-commit.yml diff --git a/.gitignore b/cosmos-inference/.gitignore similarity index 100% rename from .gitignore rename to cosmos-inference/.gitignore diff --git a/.gitleaks.toml b/cosmos-inference/.gitleaks.toml similarity index 100% rename from .gitleaks.toml rename to cosmos-inference/.gitleaks.toml diff --git a/.pre-commit-config.yaml b/cosmos-inference/.pre-commit-config.yaml similarity index 100% rename from .pre-commit-config.yaml rename to cosmos-inference/.pre-commit-config.yaml diff --git a/.pytest.toml b/cosmos-inference/.pytest.toml similarity index 100% rename from .pytest.toml rename to cosmos-inference/.pytest.toml diff --git a/.python-version b/cosmos-inference/.python-version similarity index 100% rename from .python-version rename to cosmos-inference/.python-version diff --git a/.ruff.toml b/cosmos-inference/.ruff.toml similarity index 100% rename from .ruff.toml rename to cosmos-inference/.ruff.toml diff --git a/AGENTS.md b/cosmos-inference/AGENTS.md similarity index 100% rename from AGENTS.md rename to cosmos-inference/AGENTS.md diff --git a/ATTRIBUTIONS.md b/cosmos-inference/ATTRIBUTIONS.md similarity index 100% rename from ATTRIBUTIONS.md rename to cosmos-inference/ATTRIBUTIONS.md diff --git a/CHANGELOG.md b/cosmos-inference/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to cosmos-inference/CHANGELOG.md diff --git a/CONTRIBUTING.md b/cosmos-inference/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to cosmos-inference/CONTRIBUTING.md diff --git a/Dockerfile b/cosmos-inference/Dockerfile similarity index 100% rename from Dockerfile rename to cosmos-inference/Dockerfile diff --git a/LICENSE b/cosmos-inference/LICENSE similarity index 100% rename from LICENSE rename to cosmos-inference/LICENSE diff --git a/cosmos-inference/README.md b/cosmos-inference/README.md new file mode 100644 index 00000000..1df0ee7c --- /dev/null +++ b/cosmos-inference/README.md @@ -0,0 +1,137 @@ +

+ NVIDIA Cosmos +

+ +

🤗 Hugging Face | Paper Draft

+ +# Cosmos3 + +## User Guide + +- [Gallery](./docs/gallery.md) +- [Quickstart](#setup) +- [Setup](./docs/setup.md): Installation, environment, checkpoints +- [Prompting](./docs/prompting.md): Prompt engineering, upsampling +- [Inference](./docs/inference.md): Sample arguments, default values, offline batch inference + - [Online Inference](./docs/inference_online.md): Online serving, web UI + - [Action Policy Closed-Loop Evaluation on LIBERO](./docs/action_policy_closed_loop_eval.md): Action policy server/client setup and LIBERO evaluation. The client setup clones and installs the external [Lifelong-Robot-Learning/LIBERO](https://github.com/Lifelong-Robot-Learning/LIBERO) simulator package. +- [Training](./docs/training.md): Post-Training (Supervised Fine-Tuning) +- [FAQ](./docs/faq.md): FAQ, tips, troubleshooting +- [AGENTS.md](./AGENTS.md): AI agent entry point — start here for quick codebase orientation + +## Overview + +**Cosmos3** is a world foundation model that unifies understanding and generation within a single Mixture-of-Transformer (MoT) architecture. Two tightly coupled towers—a **Reasoner** (vision-language model) and a **Generator** (world simulator)—share latent representations so that structured perception directly grounds realistic, temporally consistent simulation. + +

Image

+ +One model, many capabilities: + +| Input Modality | Output Modality | Application | EA1 | +| ----------------------- | --------------- | --------------------- | ------------ | +| Video \| Text | Video | Video Generator | ✅ | +| Video \| Text | Text | Vision Language Model | Coming soon! | +| Action \| Video \| Text | Video | World Model | ✅ | +| Video \| Text | Video & Action | Policy Model | ✅ | + +## Supported Features (Cosmos3 EA1 — Robotics Backbone) + +### User Stories + +- **Video Backbone**: Evaluate and benchmark the model’s task understanding and review its architecture to inform codebase decisions. + +### Modalities Supported + +- Text2Image (t2i), Text2Video (t2v), Image2Video (i2v) + +### Base Model Specifications + +| Spec | Value | +| ---------------- | -------------------------------------------------------------------------- | +| Model Size | Nano, Super | +| Resolution | 256p / 480p / 720p | +| Frame Rate (FPS) | 10–30 | +| Num of Frames | Default: 189 (max by resolution: `256p → 400`, `480p → 300`, `720p → 200`) | +| Max Duration | Variable | +| View | Single view only | + +## Setup + +For more details and alternative installation methods, see [Setup](./docs/setup.md#installation). + +Install system dependencies: + +```shell +sudo apt-get update && sudo apt-get install -y --no-install-recommends curl ffmpeg libx11-dev tree wget +``` + +Install the package with `uv`: + +```shell +uv sync --all-extras --group=cu130-train +source .venv/bin/activate && export LD_LIBRARY_PATH= +``` + +## Prompting + +For prompting guidance, see [Prompting](./docs/prompting.md). + +## Inference + +For more details, see [Inference](./docs/inference.md). + +### Offline Batch Inference + +Generate a single sample with 1 GPU: + +```shell +python -m cosmos3.scripts.inference \ + --parallelism-preset=latency \ + -i "inputs/omni/t2v.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +Generate multiple samples with 8 GPUs (~5 mins): + +```shell +torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ + --parallelism-preset=throughput \ + -i "inputs/omni/*.json" \ + -o outputs/omni_nano \ + --checkpoint-path Cosmos3-Nano \ + --seed=0 +``` + +**Note:** The progress bar only prints on rank 0. + +### Models + +| Model | Arguments | +| ------------- | --------------------------------- | +| Cosmos3-Nano | `--checkpoint-path=Cosmos3-Nano` | +| Cosmos3-Super | `--checkpoint-path=Cosmos3-Super` | + +### Modalities + +| Modality | Example | +| ---------------- | -------------------------------------------------------------------------------------------------- | +| Text2Image | [`-i "inputs/omni/t2i.json"`](inputs/omni/t2i.json) | +| Text2Video | [`-i "inputs/omni/t2v.json"`](inputs/omni/t2v.json) | +| Image2Video | [`-i "inputs/omni/i2v.json"`](inputs/omni/i2v.json) | +| Forward Dynamics | [`-i "inputs/omni/action_forward_dynamics*.json"`](inputs/omni/action_forward_dynamics_robot.json) | +| Inverse Dynamics | [`-i "inputs/omni/action_inverse_dynamics*.json"`](inputs/omni/action_inverse_dynamics_av.json) | +| Policy | [`-i "inputs/omni/action_policy*.json"`](inputs/omni/action_policy_robot.json) | + +To generate all examples, use `-i "inputs/omni/*.json"`. + +For more information regarding action inference, please see [Action Inference](./docs/inference.md#action-inference). + +### CLI Reference + +To see all available arguments: + +```shell +python -m cosmos3.scripts.inference --help +``` diff --git a/ci/.link-check.json b/cosmos-inference/ci/.link-check.json similarity index 100% rename from ci/.link-check.json rename to cosmos-inference/ci/.link-check.json diff --git a/ci/.markdown-toc-creator.toml b/cosmos-inference/ci/.markdown-toc-creator.toml similarity index 100% rename from ci/.markdown-toc-creator.toml rename to cosmos-inference/ci/.markdown-toc-creator.toml diff --git a/ci/.pre-commit-config-base.yaml b/cosmos-inference/ci/.pre-commit-config-base.yaml similarity index 100% rename from ci/.pre-commit-config-base.yaml rename to cosmos-inference/ci/.pre-commit-config-base.yaml diff --git a/ci/license.txt b/cosmos-inference/ci/license.txt similarity index 100% rename from ci/license.txt rename to cosmos-inference/ci/license.txt diff --git a/ci/uv_lock.sh b/cosmos-inference/ci/uv_lock.sh similarity index 100% rename from ci/uv_lock.sh rename to cosmos-inference/ci/uv_lock.sh diff --git a/ci/uv_lock_script.sh b/cosmos-inference/ci/uv_lock_script.sh similarity index 100% rename from ci/uv_lock_script.sh rename to cosmos-inference/ci/uv_lock_script.sh diff --git a/conftest.py b/cosmos-inference/conftest.py similarity index 100% rename from conftest.py rename to cosmos-inference/conftest.py diff --git a/docker/ci.Dockerfile b/cosmos-inference/docker/ci.Dockerfile similarity index 100% rename from docker/ci.Dockerfile rename to cosmos-inference/docker/ci.Dockerfile diff --git a/docker/entrypoint.sh b/cosmos-inference/docker/entrypoint.sh similarity index 100% rename from docker/entrypoint.sh rename to cosmos-inference/docker/entrypoint.sh diff --git a/docker/nightly.Dockerfile b/cosmos-inference/docker/nightly.Dockerfile similarity index 100% rename from docker/nightly.Dockerfile rename to cosmos-inference/docker/nightly.Dockerfile diff --git a/docs/Cosmos3.pdf b/cosmos-inference/docs/Cosmos3.pdf similarity index 100% rename from docs/Cosmos3.pdf rename to cosmos-inference/docs/Cosmos3.pdf diff --git a/docs/action_policy_closed_loop_eval.md b/cosmos-inference/docs/action_policy_closed_loop_eval.md similarity index 100% rename from docs/action_policy_closed_loop_eval.md rename to cosmos-inference/docs/action_policy_closed_loop_eval.md diff --git a/docs/faq.md b/cosmos-inference/docs/faq.md similarity index 100% rename from docs/faq.md rename to cosmos-inference/docs/faq.md diff --git a/docs/gallery.md b/cosmos-inference/docs/gallery.md similarity index 100% rename from docs/gallery.md rename to cosmos-inference/docs/gallery.md diff --git a/docs/inference.md b/cosmos-inference/docs/inference.md similarity index 100% rename from docs/inference.md rename to cosmos-inference/docs/inference.md diff --git a/docs/inference_online.md b/cosmos-inference/docs/inference_online.md similarity index 100% rename from docs/inference_online.md rename to cosmos-inference/docs/inference_online.md diff --git a/docs/prompting.md b/cosmos-inference/docs/prompting.md similarity index 100% rename from docs/prompting.md rename to cosmos-inference/docs/prompting.md diff --git a/docs/setup.md b/cosmos-inference/docs/setup.md similarity index 100% rename from docs/setup.md rename to cosmos-inference/docs/setup.md diff --git a/docs/training.md b/cosmos-inference/docs/training.md similarity index 100% rename from docs/training.md rename to cosmos-inference/docs/training.md diff --git a/examples/_test.py b/cosmos-inference/examples/_test.py similarity index 100% rename from examples/_test.py rename to cosmos-inference/examples/_test.py diff --git a/examples/_test/inference.sh b/cosmos-inference/examples/_test/inference.sh similarity index 100% rename from examples/_test/inference.sh rename to cosmos-inference/examples/_test/inference.sh diff --git a/examples/_test/inference_pipeline.sh b/cosmos-inference/examples/_test/inference_pipeline.sh similarity index 100% rename from examples/_test/inference_pipeline.sh rename to cosmos-inference/examples/_test/inference_pipeline.sh diff --git a/examples/inference.py b/cosmos-inference/examples/inference.py similarity index 100% rename from examples/inference.py rename to cosmos-inference/examples/inference.py diff --git a/examples/inference_pipeline.py b/cosmos-inference/examples/inference_pipeline.py similarity index 100% rename from examples/inference_pipeline.py rename to cosmos-inference/examples/inference_pipeline.py diff --git a/inputs/omni/action_forward_dynamics_camera_0.json b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json similarity index 100% rename from inputs/omni/action_forward_dynamics_camera_0.json rename to cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json diff --git a/inputs/omni/action_forward_dynamics_camera_1.json b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json similarity index 100% rename from inputs/omni/action_forward_dynamics_camera_1.json rename to cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json diff --git a/inputs/omni/action_forward_dynamics_robot.json b/cosmos-inference/inputs/omni/action_forward_dynamics_robot.json similarity index 100% rename from inputs/omni/action_forward_dynamics_robot.json rename to cosmos-inference/inputs/omni/action_forward_dynamics_robot.json diff --git a/inputs/omni/action_inverse_dynamics_av.json b/cosmos-inference/inputs/omni/action_inverse_dynamics_av.json similarity index 100% rename from inputs/omni/action_inverse_dynamics_av.json rename to cosmos-inference/inputs/omni/action_inverse_dynamics_av.json diff --git a/inputs/omni/action_policy_robot.json b/cosmos-inference/inputs/omni/action_policy_robot.json similarity index 100% rename from inputs/omni/action_policy_robot.json rename to cosmos-inference/inputs/omni/action_policy_robot.json diff --git a/inputs/omni/i2v.json b/cosmos-inference/inputs/omni/i2v.json similarity index 100% rename from inputs/omni/i2v.json rename to cosmos-inference/inputs/omni/i2v.json diff --git a/inputs/omni/t2i.json b/cosmos-inference/inputs/omni/t2i.json similarity index 100% rename from inputs/omni/t2i.json rename to cosmos-inference/inputs/omni/t2i.json diff --git a/inputs/omni/t2v.json b/cosmos-inference/inputs/omni/t2v.json similarity index 100% rename from inputs/omni/t2v.json rename to cosmos-inference/inputs/omni/t2v.json diff --git a/inputs/t2v_long_prompts.jsonl b/cosmos-inference/inputs/t2v_long_prompts.jsonl similarity index 100% rename from inputs/t2v_long_prompts.jsonl rename to cosmos-inference/inputs/t2v_long_prompts.jsonl diff --git a/justfile b/cosmos-inference/justfile similarity index 100% rename from justfile rename to cosmos-inference/justfile diff --git a/pyproject.toml b/cosmos-inference/pyproject.toml similarity index 100% rename from pyproject.toml rename to cosmos-inference/pyproject.toml diff --git a/pyrefly.toml b/cosmos-inference/pyrefly.toml similarity index 100% rename from pyrefly.toml rename to cosmos-inference/pyrefly.toml diff --git a/schemas/OmniSampleOverrides.schema.json b/cosmos-inference/schemas/OmniSampleOverrides.schema.json similarity index 100% rename from schemas/OmniSampleOverrides.schema.json rename to cosmos-inference/schemas/OmniSampleOverrides.schema.json diff --git a/schemas/OmniSampleOverrides.yaml b/cosmos-inference/schemas/OmniSampleOverrides.yaml similarity index 100% rename from schemas/OmniSampleOverrides.yaml rename to cosmos-inference/schemas/OmniSampleOverrides.yaml diff --git a/schemas/OmniSetupOverrides.schema.json b/cosmos-inference/schemas/OmniSetupOverrides.schema.json similarity index 100% rename from schemas/OmniSetupOverrides.schema.json rename to cosmos-inference/schemas/OmniSetupOverrides.schema.json diff --git a/schemas/OmniSetupOverrides.yaml b/cosmos-inference/schemas/OmniSetupOverrides.yaml similarity index 100% rename from schemas/OmniSetupOverrides.yaml rename to cosmos-inference/schemas/OmniSetupOverrides.yaml diff --git a/uv.lock b/cosmos-inference/uv.lock similarity index 100% rename from uv.lock rename to cosmos-inference/uv.lock diff --git a/vllm-cosmos3/README.md b/cosmos-inference/vllm-cosmos3/README.md similarity index 100% rename from vllm-cosmos3/README.md rename to cosmos-inference/vllm-cosmos3/README.md diff --git a/vllm-cosmos3/pyproject.toml b/cosmos-inference/vllm-cosmos3/pyproject.toml similarity index 100% rename from vllm-cosmos3/pyproject.toml rename to cosmos-inference/vllm-cosmos3/pyproject.toml diff --git a/vllm-cosmos3/uv.lock b/cosmos-inference/vllm-cosmos3/uv.lock similarity index 100% rename from vllm-cosmos3/uv.lock rename to cosmos-inference/vllm-cosmos3/uv.lock diff --git a/vllm-cosmos3/vllm_cosmos3/__init__.py b/cosmos-inference/vllm-cosmos3/vllm_cosmos3/__init__.py similarity index 100% rename from vllm-cosmos3/vllm_cosmos3/__init__.py rename to cosmos-inference/vllm-cosmos3/vllm_cosmos3/__init__.py diff --git a/vllm-cosmos3/vllm_cosmos3/model.py b/cosmos-inference/vllm-cosmos3/vllm_cosmos3/model.py similarity index 100% rename from vllm-cosmos3/vllm_cosmos3/model.py rename to cosmos-inference/vllm-cosmos3/vllm_cosmos3/model.py diff --git a/docker/.gitkeep b/docker/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/.gitkeep b/examples/.gitkeep new file mode 100644 index 00000000..e69de29b From 4e121d7d1b43e9518c2c913641c8dc216b3908ae Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Mon, 11 May 2026 21:39:49 -0700 Subject: [PATCH 05/24] Add stub pyproject.toml and uv.lock for cosmos/ package at root Minimal hatchling-backed pyproject.toml declaring the cosmos skeleton package (version 0.0.0, no dependencies, python >=3.13), plus a uv.lock generated by `uv lock` resolving the single editable cosmos package. Brings the repo root in line with the planned layout: pyproject.toml, uv.lock, README.md Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 17 +++++++++++++++++ uv.lock | 8 ++++++++ 2 files changed, 25 insertions(+) create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4b142f01 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[project] +name = "cosmos" +version = "0.0.0" +description = "Cosmos framework — main package (skeleton)" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["cosmos"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..3c0b20cf --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "cosmos" +version = "0.0.0" +source = { editable = "." } From 6dfdd92ad8c722e828ff5d9d2b1816f93641ad98 Mon Sep 17 00:00:00 2001 From: "liang.feng" Date: Thu, 14 May 2026 01:55:47 -0700 Subject: [PATCH 06/24] Sync cosmos-inference/ with cosmos3-internal origin/main Replace cosmos-inference/ contents with the exact snapshot of origin/main (99450113) from the cosmos3-internal repo to drop the stale flat-layout files and align with upstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/cosmos3-post-training/SKILL.md | 34 +- .../skills/cosmos3-post-training/SKILL.md | 34 +- .../augmentors/interleaved_video_parsing.py | 223 - cosmos-inference/{ => cosmos3}/__init__.py | 0 .../{ => cosmos3}/_src/imaginaire/__init__.py | 0 .../_src/imaginaire/attention/README.md | 0 .../_src/imaginaire/attention/__init__.py | 0 .../_src/imaginaire/attention/backends.py | 0 .../_src/imaginaire/attention/checks.py | 0 .../_src/imaginaire/attention/docs/README.md | 0 .../_src/imaginaire/attention/docs/apis.md | 0 .../imaginaire/attention/docs/backends.md | 0 .../imaginaire/attention/docs/features.md | 0 .../imaginaire/attention/docs/multi-dim.md | 0 .../imaginaire/attention/flash2/__init__.py | 0 .../imaginaire/attention/flash2/checks.py | 0 .../imaginaire/attention/flash2/functions.py | 0 .../_src/imaginaire/attention/flash2/meta.py | 0 .../_src/imaginaire/attention/flash2/stubs.py | 0 .../imaginaire/attention/flash3/__init__.py | 0 .../imaginaire/attention/flash3/checks.py | 0 .../imaginaire/attention/flash3/functions.py | 0 .../_src/imaginaire/attention/flash3/meta.py | 0 .../_src/imaginaire/attention/flash3/stubs.py | 0 .../_src/imaginaire/attention/frontend.py | 0 .../_src/imaginaire/attention/masks.py | 0 .../imaginaire/attention/natten/__init__.py | 0 .../imaginaire/attention/natten/checks.py | 0 .../imaginaire/attention/natten/functions.py | 0 .../_src/imaginaire/attention/natten/meta.py | 0 .../_src/imaginaire/attention/natten/stubs.py | 0 .../imaginaire/attention/utils/__init__.py | 0 .../imaginaire/attention/utils/determinism.py | 0 .../imaginaire/attention/utils/environment.py | 0 .../attention/utils/safe_ops/__init__.py | 0 .../attention/utils/safe_ops/functools.py | 0 .../attention/utils/safe_ops/log.py | 0 .../imaginaire/attention/utils/version.py | 0 .../_src/imaginaire/attention/varlen.py | 0 .../_src/imaginaire/auxiliary/__init__.py | 0 .../auxiliary/guardrail/__init__.py | 0 .../auxiliary/guardrail/blocklist/__init__.py | 0 .../guardrail/blocklist/blocklist.py | 0 .../guardrail/blocklist/profile_blocklist.py | 0 .../auxiliary/guardrail/blocklist/utils.py | 0 .../auxiliary/guardrail/common/__init__.py | 0 .../auxiliary/guardrail/common/core.py | 0 .../auxiliary/guardrail/common/io_utils.py | 0 .../auxiliary/guardrail/common/presets.py | 0 .../guardrail/face_blur_filter/__init__.py | 0 .../guardrail/face_blur_filter/blur_utils.py | 0 .../face_blur_filter/face_blur_filter.py | 0 .../face_blur_filter/retinaface_utils.py | 0 .../guardrail/llamaGuard3/__init__.py | 0 .../guardrail/llamaGuard3/categories.py | 0 .../guardrail/llamaGuard3/llamaGuard3.py | 0 .../guardrail/qwen3guard/__init__.py | 0 .../guardrail/qwen3guard/categories.py | 0 .../guardrail/qwen3guard/qwen3guard.py | 0 .../video_content_safety_filter/__init__.py | 0 .../video_content_safety_filter/model.py | 0 .../video_content_safety_filter.py | 0 .../vision_encoder.py | 0 .../_src/imaginaire/callbacks/__init__.py | 0 .../_src/imaginaire/callbacks/every_n.py | 0 .../imaginaire/callbacks/image_grad_clip.py | 0 .../_src/imaginaire/callbacks/manual_gc.py | 0 .../_src/imaginaire/checkpointer/__init__.py | 0 .../_src/imaginaire/checkpointer/base.py | 0 .../_src/imaginaire/checkpointer/ddp.py | 0 .../_src/imaginaire/checkpointer/dummy.py | 0 .../imaginaire/checkpointer/s3_filesystem.py | 0 .../imaginaire/checkpointer/safe_broadcast.py | 0 .../_src/imaginaire/checkpointer/tp.py | 0 .../_src/imaginaire/checkpointer/tp_ema.py | 0 .../{ => cosmos3}/_src/imaginaire/config.py | 0 .../_src/imaginaire/configs/lr_scheduler.py | 0 .../_src/imaginaire/datasets/__init__.py | 0 .../datasets/augmentors/merge_datadict.py | 0 .../datasets/augmentors/v3_text_transforms.py | 0 .../imaginaire/datasets/decoders/__init__.py | 0 .../datasets/decoders/json_loader.py | 0 .../datasets/decoders/pkl_loader.py | 0 .../datasets/decoders/video_decoder.py | 0 .../imaginaire/datasets/joint_training.py | 0 .../_src/imaginaire/datasets/mock_dataset.py | 0 .../datasets/webdataset/__init__.py | 0 .../webdataset/augmentors/augmentor.py | 0 .../webdataset/augmentors/geometry/camera.py | 0 .../webdataset/augmentors/geometry/depth.py | 0 .../augmentors/geometry/pointcloud.py | 0 .../webdataset/augmentors/image/__init__.py | 0 .../webdataset/augmentors/image/cropping.py | 0 .../webdataset/augmentors/image/flip.py | 0 .../webdataset/augmentors/image/misc.py | 0 .../webdataset/augmentors/image/normalize.py | 0 .../webdataset/augmentors/image/padding.py | 0 .../webdataset/augmentors/image/resize.py | 0 .../datasets/webdataset/config/schema.py | 0 .../datasets/webdataset/dataloader.py | 0 .../datasets/webdataset/decoders/__init__.py | 0 .../datasets/webdataset/decoders/depth.py | 0 .../datasets/webdataset/decoders/image.py | 0 .../datasets/webdataset/decoders/pickle.py | 0 .../webdataset/distributors/__init__.py | 0 .../datasets/webdataset/distributors/basic.py | 0 .../distributors/multi_aspect_ratio.py | 0 .../distributors/multi_aspect_ratio_v2.py | 0 .../weighted_multi_aspect_ratio.py | 0 .../datasets/webdataset/utils/iterators.py | 0 .../datasets/webdataset/utils/misc.py | 0 .../datasets/webdataset/utils/stream.py | 0 .../unit_test/RetryingStreamDataLoaderTest.py | 0 .../utils/unit_test/RetryingStreamMockTest.py | 0 .../RetryingStreamStatsOverheadBenchmark.py | 0 .../RetryingStreamTarIteratorTest.py | 0 .../utils/unit_test/mpi_rank_worker.py | 0 .../datasets/webdataset/webdataset.py | 0 .../datasets/webdataset/webdataset_ext.py | 0 .../{ => cosmos3}/_src/imaginaire/flags.py | 0 .../_src/imaginaire/flops/__init__.py | 0 .../_src/imaginaire/flops/omni_mot.py | 0 .../_src/imaginaire/flops/wan_vae.py | 0 .../_src/imaginaire/functional/batch_ops.py | 0 .../imaginaire/functional/lr_scheduler.py | 0 .../_src/imaginaire/functional/multi_step.py | 0 .../_src/imaginaire/functional/runge_kutta.py | 0 .../_src/imaginaire/lazy_config/__init__.py | 0 .../_src/imaginaire/lazy_config/file_io.py | 0 .../imaginaire/lazy_config/instantiate.py | 0 .../_src/imaginaire/lazy_config/lazy.py | 0 .../_src/imaginaire/lazy_config/lazy_call.py | 0 .../imaginaire/lazy_config/omegaconf_patch.py | 0 .../_src/imaginaire/lazy_config/registry.py | 0 .../{ => cosmos3}/_src/imaginaire/model.py | 0 .../imaginaire/models/abstract_emb_model.py | 0 .../_src/imaginaire/modules/camera.py | 0 .../imaginaire/modules/denoiser_scaling.py | 0 .../_src/imaginaire/modules/edm_sde.py | 0 .../imaginaire/modules/image_embeddings.py | 0 .../_src/imaginaire/modules/res_sampler.py | 0 .../_src/imaginaire/serialization.py | 0 .../{ => cosmos3}/_src/imaginaire/trainer.py | 0 .../_src/imaginaire/utils/__init__.py | 0 .../_src/imaginaire/utils/callback.py | 0 .../_src/imaginaire/utils/checkpoint_db.py | 0 .../_src/imaginaire/utils/checkpointer.py | 0 .../_src/imaginaire/utils/cluster_env.py | 0 .../_src/imaginaire/utils/config_helper.py | 0 .../_src/imaginaire/utils/context_managers.py | 0 .../_src/imaginaire/utils/context_parallel.py | 0 .../_src/imaginaire/utils/count_params.py | 0 .../_src/imaginaire/utils/dataloader.py | 0 .../_src/imaginaire/utils/dataset_utils.py | 0 .../imaginaire/utils/denoise_prediction.py | 0 .../_src/imaginaire/utils/device.py | 0 .../_src/imaginaire/utils/disabled_train.py | 0 .../_src/imaginaire/utils/distributed.py | 0 .../_src/imaginaire/utils/easy_io/__init__.py | 0 .../utils/easy_io/backends/__init__.py | 0 .../utils/easy_io/backends/auto_auth.py | 0 .../utils/easy_io/backends/base_backend.py | 0 .../utils/easy_io/backends/boto3_backend.py | 0 .../utils/easy_io/backends/boto3_client.py | 0 .../utils/easy_io/backends/http_backend.py | 0 .../utils/easy_io/backends/local_backend.py | 0 .../utils/easy_io/backends/msc_backend.py | 0 .../utils/easy_io/backends/registry_utils.py | 0 .../_src/imaginaire/utils/easy_io/easy_io.py | 0 .../imaginaire/utils/easy_io/file_client.py | 0 .../utils/easy_io/handlers/__init__.py | 0 .../imaginaire/utils/easy_io/handlers/base.py | 0 .../utils/easy_io/handlers/byte_handler.py | 0 .../utils/easy_io/handlers/csv_handler.py | 0 .../utils/easy_io/handlers/gzip_handler.py | 0 .../easy_io/handlers/imageio_video_handler.py | 0 .../utils/easy_io/handlers/json_handler.py | 0 .../utils/easy_io/handlers/jsonl_handler.py | 0 .../utils/easy_io/handlers/np_handler.py | 0 .../utils/easy_io/handlers/pandas_handler.py | 0 .../utils/easy_io/handlers/pickle_handler.py | 0 .../utils/easy_io/handlers/pil_handler.py | 0 .../utils/easy_io/handlers/registry_utils.py | 0 .../utils/easy_io/handlers/tarfile_handler.py | 0 .../utils/easy_io/handlers/torch_handler.py | 0 .../easy_io/handlers/torchjit_handler.py | 0 .../utils/easy_io/handlers/trimesh_handler.py | 0 .../utils/easy_io/handlers/txt_handler.py | 0 .../utils/easy_io/handlers/yaml_handler.py | 0 .../_src/imaginaire/utils/ema.py | 0 .../utils/embedding_concat_strategy.py | 0 .../utils/env_parsers/cred_env_parser.py | 0 .../env_parsers/customization_env_parser.py | 0 .../utils/env_parsers/env_parser.py | 0 .../utils/env_parsers/inference_env_parser.py | 0 .../_src/imaginaire/utils/fsdp_helper.py | 0 .../_src/imaginaire/utils/fused_adam.py | 0 .../_src/imaginaire/utils/fused_nan_to_num.py | 0 .../_src/imaginaire/utils/graph.py | 0 .../imaginaire/utils/high_sigma_strategy.py | 0 .../_src/imaginaire/utils/launch.py | 0 .../_src/imaginaire/utils/log.py | 0 .../_src/imaginaire/utils/misc.py | 0 .../_src/imaginaire/utils/nsys_wrapper.sh | 0 .../_src/imaginaire/utils/object_store.py | 0 .../imaginaire/utils/optim_instantiate.py | 0 .../imaginaire/utils/parallel_state_helper.py | 0 .../_src/imaginaire/utils/primitives.py | 0 .../_src/imaginaire/utils/profiling.py | 0 .../_src/imaginaire/utils/progress_bar.py | 0 .../_src/imaginaire/utils/registry.py | 0 .../_src/imaginaire/utils/replace_bg_color.py | 0 .../_src/imaginaire/utils/s3_utils.py | 0 .../_src/imaginaire/utils/scheduler.py | 0 .../imaginaire/utils/submit_job_helper.py | 0 .../_src/imaginaire/utils/timer.py | 0 .../_src/imaginaire/utils/tone_curve.py | 0 .../_src/imaginaire/utils/training.py | 0 .../_src/imaginaire/utils/validator.py | 0 .../_src/imaginaire/utils/validator_params.py | 0 .../_src/imaginaire/utils/wandb_util.py | 0 .../_src/imaginaire/visualize/__init__.py | 0 .../_src/imaginaire/visualize/img.py | 0 .../_src/imaginaire/visualize/video.py | 0 .../{ => cosmos3}/_src/vfm/__init__.py | 0 .../_src/vfm/callbacks/__init__.py | 0 .../_src/vfm/callbacks/compile_tokenizer.py | 0 .../_src/vfm/callbacks/dataloader_state.py | 0 .../_src/vfm/callbacks/dataloading_monitor.py | 0 .../_src/vfm/callbacks/device_monitor.py | 0 .../every_n_draw_audio_video_sample.py | 0 .../_src/vfm/callbacks/every_n_draw_sample.py | 0 .../callbacks/every_n_dur_fps_draw_sample.py | 0 .../_src/vfm/callbacks/expert_heatmap.py | 0 .../_src/vfm/callbacks/generation.py | 0 .../_src/vfm/callbacks/grad_clip.py | 0 .../_src/vfm/callbacks/heart_beat.py | 0 .../_src/vfm/callbacks/hf_export.py | 6 +- .../_src/vfm/callbacks/iter_speed.py | 0 .../_src/vfm/callbacks/load_pretrained.py | 0 .../_src/vfm/callbacks/low_precision.py | 0 .../{ => cosmos3}/_src/vfm/callbacks/mfu.py | 0 .../callbacks/moe_specialization_callback.py | 0 .../vfm/callbacks/moe_stability_callback.py | 0 .../_src/vfm/callbacks/norm_monitor.py | 0 .../{ => cosmos3}/_src/vfm/callbacks/ofu.py | 0 .../_src/vfm/callbacks/param_count.py | 0 .../_src/vfm/callbacks/per_stream_timing.py | 117 + .../vfm/callbacks/sequence_packing_padding.py | 0 .../_src/vfm/callbacks/sigma_loss_analysis.py | 0 .../_src/vfm/callbacks/skip_nan_step.py | 0 .../termination_signal_checkpoint.py | 178 + .../_src/vfm/callbacks/training_stats.py | 0 .../_src/vfm/callbacks/vlm/grad_clip.py | 0 .../_src/vfm/callbacks/wandb_log.py | 0 .../_src/vfm/callbacks/wandb_log_eval.py | 0 .../_src/vfm/checkpointer/__init__.py | 0 .../_src/vfm/checkpointer/dcp.py | 0 .../{ => cosmos3}/_src/vfm/conditioner.py | 0 .../_src/vfm/configs/__init__.py | 0 .../_src/vfm/configs/base/__init__.py | 0 .../_src/vfm/configs/base/config.py | 0 .../vfm/configs/base/defaults/__init__.py | 0 .../vfm/configs/base/defaults/callbacks.py | 4 + .../vfm/configs/base/defaults/checkpointer.py | 0 .../_src/vfm/configs/base/defaults/cluster.py | 0 .../vfm/configs/base/defaults/conditioner.py | 0 .../_src/vfm/configs/base/defaults/ema.py | 0 .../_src/vfm/configs/base/defaults/model.py | 0 .../vfm/configs/base/defaults/model_config.py | 52 +- .../base/defaults/multiview_dataloader.py | 0 .../vfm/configs/base/defaults/optimizer.py | 0 .../vfm/configs/base/defaults/parallelism.py | 84 + .../vfm/configs/base/defaults/tokenizer.py | 0 .../vfm/configs/base/defaults/unittest.py | 0 .../_src/vfm/configs/base/defaults/vlm.py | 0 .../_src/vfm/configs/base/vlm/__init__.py | 0 .../_src/vfm/configs/base/vlm/config.py | 0 .../vfm/configs/base/vlm/defaults/__init__.py | 0 .../configs/base/vlm/defaults/callbacks.py | 7 +- .../configs/base/vlm/defaults/checkpointer.py | 0 .../vfm/configs/base/vlm/defaults/config.py | 0 .../configs/base/vlm/defaults/dataloader.py | 0 .../vlm/defaults/dataloader_weighted_url.py | 0 .../vfm/configs/base/vlm/defaults/model.py | 5 +- .../configs/base/vlm/defaults/optimizer.py | 0 .../vfm/configs/base/vlm/defaults/training.py | 62 +- .../configs/base/vlm/defaults/vlm_policy.py | 0 .../configs/base/vlm/experiment/__init__.py | 0 .../experiment/pre_exp012_phase2_vlm_smoke.py | 6 +- .../configs/base/vlm/experiment/pre_exp01x.py | 46 +- .../vfm/configs/base/vlm/experiment/utils.py | 0 .../_src/vfm/datasets/__init__.py | 0 .../datasets/action/action_normalization.py | 0 .../_src/vfm/datasets/action/action_spec.py | 0 .../_src/vfm/datasets/action/dataloaders.py | 0 .../_src/vfm/datasets/action/domain_utils.py | 0 .../vfm/datasets/action/json_formatter.py | 0 .../vfm/datasets/action/libero_dataset.py | 0 .../vfm/datasets/action/libero_pose_utils.py | 0 ...bero_native_frame_wise_relative_rot6d.json | 0 .../_src/vfm/datasets/action/pose_utils.py | 0 .../_src/vfm/datasets/action/transforms.py | 0 .../vfm/datasets/action/unified_dataset.py | 0 .../vfm/datasets/action/viewpoint_utils.py | 0 .../_src/vfm/datasets/augmentors/__init__.py | 0 .../augmentors/append_fps_frames_for_image.py | 0 .../vfm/datasets/augmentors/audio_caption.py | 0 .../vfm/datasets/augmentors/audio_parsing.py | 0 .../vfm/datasets/augmentors/caption_filter.py | 0 .../_src/vfm/datasets/augmentors/cropping.py | 0 .../duration_fps_text_timestamps.py | 0 .../augmentors/idle_frames_text_info.py | 0 .../augmentors/image_editing_transform.py | 0 .../augmentors/image_resolution_filter.py | 0 .../augmentors/interleaved_image_transform.py | 0 .../augmentors/interleaved_video_parsing.py | 583 ++ .../vfm/datasets/augmentors/merge_datadict.py | 0 .../vfm/datasets/augmentors/pkl_to_media.py | 0 .../augmentors/resolution_text_info.py | 0 .../vfm/datasets/augmentors/sequence_plan.py | 0 .../augmentors/sound_sequence_plan.py | 0 .../vfm/datasets/augmentors/text_tokenizer.py | 0 .../augmentors/text_transforms_for_image.py | 13 + .../augmentors/text_transforms_for_video.py | 274 +- .../transfer_control_input/__init__.py | 0 .../augmentors/transfer_control_input/blur.py | 0 .../transfer_control_input/control_input.py | 0 .../transfer_control_input/fast_blur.py | 0 .../augmentors/transfer_control_input/seg.py | 0 .../augmentors/transfer_control_transform.py | 0 .../vfm/datasets/augmentors/video_parsing.py | 0 .../_src/vfm/datasets/joint_dataloader.py | 131 +- .../vfm/datasets/local_datasets/__init__.py | 0 .../vfm/datasets/local_datasets/helper.py | 0 .../datasets/local_datasets/sft_dataset.py | 0 .../_src/vfm/datasets/sequence_packing.py | 0 .../{ => cosmos3}/_src/vfm/datasets/utils.py | 0 .../_src/vfm/diffusion/__init__.py | 0 .../_src/vfm/diffusion/rectified_flow.py | 0 .../_src/vfm/diffusion/samplers/__init__.py | 0 .../_src/vfm/diffusion/samplers/edm.py | 0 .../_src/vfm/diffusion/samplers/fixed_step.py | 0 .../diffusion/samplers/fm_solvers_unipc.py | 0 .../_src/vfm/diffusion/samplers/unipc.py | 0 .../_src/vfm/diffusion/samplers/utils.py | 0 .../_src/vfm/evaluation/__init__.py | 0 .../_src/vfm/evaluation/action/__init__.py | 0 .../vfm/evaluation/action/libero/__init__.py | 0 .../action/libero/closed_loop_eval.py | 0 .../libero/dataset_reply_action_server.py | 0 .../{ => cosmos3}/_src/vfm/models/__init__.py | 0 .../{ => cosmos3}/_src/vfm/models/hf_model.py | 0 .../_src/vfm/models/llm/__init__.py | 0 .../_src/vfm/models/llm/qwen3/__init__.py | 0 .../models/llm/qwen3/configs/Qwen3-0.6B.json | 0 .../vfm/models/llm/qwen3/configs/__init__.py | 0 .../models/llm/qwen3/configuration_qwen3.py | 0 .../_src/vfm/models/llm/qwen3/qwen3.py | 0 .../_src/vfm/models/llm/qwen3/test_qwen3.py | 0 .../_src/vfm/models/mot/__init__.py | 0 .../_src/vfm/models/mot/attention.py | 0 .../vfm/models/mot/context_parallel_utils.py | 0 .../vfm/models/mot/cosmos3_vfm_network.py | 0 .../vfm/models/mot/domain_aware_linear.py | 0 .../vfm/models/mot/dot_product_attention.py | 0 .../_src/vfm/models/mot/modeling_utils.py | 0 .../vfm/models/mot/parallelize_unified_mot.py | 0 .../vfm/models/mot/parallelize_vfm_network.py | 0 .../vfm/models/mot/qwen3_vl_unified_mot.py | 0 .../vfm/models/mot/unified_3dmrope_utils.py | 0 .../_src/vfm/models/mot/unified_mot.py | 0 .../_src/vfm/models/omni_mot_model.py | 0 .../_src/vfm/models/parallelize_vlm.py | 23 +- .../_src/vfm/models/utils/__init__.py | 0 .../vfm/models/utils/data_and_condition.py | 0 .../_src/vfm/models/utils/dcp_loader.py | 0 .../vfm/models/utils/load_balancing_loss.py | 0 .../_src/vfm/models/utils/memory.py | 0 .../vfm/models/utils/safetensors_loader.py | 0 .../_src/vfm/models/utils/taylorseer.py | 0 .../_src/vfm/models/vlm/__init__.py | 0 .../vlm/nemotron_3_dense_vl/__init__.py | 0 .../configs/Nemotron-2B-Dense-VL.json | 0 .../configuration_nemotron_3_dense_vl.py | 0 .../nemotron_3_dense_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/__init__.py | 0 .../configs/Qwen3-VL-2B-Instruct.json | 0 .../configs/Qwen3-VL-32B-Instruct.json | 0 .../configs/Qwen3-VL-4B-Instruct.json | 0 .../configs/Qwen3-VL-8B-Instruct.json | 0 .../models/vlm/qwen3_vl/configs/__init__.py | 0 .../vlm/qwen3_vl/configuration_qwen3_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py | 0 .../_src/vfm/models/vlm/qwen3_vl/utils.py | 0 .../vlm/qwen3_vl/video_processing_qwen3_vl.py | 0 .../vfm/models/vlm/qwen3_vl_moe/__init__.py | 0 .../configs/Qwen3-VL-235B-A22B-Instruct.json | 0 .../configs/Qwen3-VL-30B-A3B-Instruct.json | 0 .../vlm/qwen3_vl_moe/configs/__init__.py | 0 .../configuration_qwen3_vl_moe.py | 0 .../_src/vfm/models/vlm/qwen3_vl_moe/moe.py | 0 .../vfm/models/vlm/qwen3_vl_moe/moe_bench.py | 0 .../models/vlm/qwen3_vl_moe/moe_kernels.py | 0 .../models/vlm/qwen3_vl_moe/qwen3_vl_moe.py | 0 .../_src/vfm/models/vlm_model.py | 29 +- .../_src/vfm/tokenizers/__init__.py | 0 .../_src/vfm/tokenizers/interface.py | 0 .../_src/vfm/tokenizers/tokenization_qwen2.py | 0 .../vfm/tokenizers/wan2pt2_vae_4x16x16.py | 12 + .../{ => cosmos3}/_src/vfm/utils/__init__.py | 0 .../_src/vfm/utils/context_parallel.py | 0 .../_src/vfm/utils/data_utils.py | 0 .../_src/vfm/utils/dtensor_helper.py | 0 .../_src/vfm/utils/flash_attn.py | 0 .../_src/vfm/utils/fused_adam.py | 0 .../{ => cosmos3}/_src/vfm/utils/loss.py | 0 .../{ => cosmos3}/_src/vfm/utils/misc.py | 0 .../_src/vfm/utils/model_loader.py | 0 .../_src/vfm/utils/model_weights_stats.py | 0 .../_src/vfm/utils/monkey_patch.py | 0 .../{ => cosmos3}/_src/vfm/utils/optimizer.py | 0 .../_src/vfm/utils/parallelism.py | 0 .../_src/vfm/utils/rand_state.py | 0 .../_src/vfm/utils/tokenizer_benchmarking.py | 0 .../_src/vfm/utils/vlm/__init__.py | 0 .../_src/vfm/utils/vlm/optimizer.py | 0 .../utils/vlm/pretrained_models_downloader.py | 0 cosmos-inference/{ => cosmos3}/_test.py | 8 +- .../{ => cosmos3}/_test/autoregressive.sh | 0 .../{ => cosmos3}/_test/distilled.sh | 0 .../{ => cosmos3}/_test/latency.sh | 0 .../{ => cosmos3}/_test/omni-super.sh | 0 cosmos-inference/{ => cosmos3}/_test/omni.sh | 0 .../{ => cosmos3}/_test/omni_param.sh | 0 cosmos-inference/{ => cosmos3}/_test/sft.sh | 0 cosmos-inference/{ => cosmos3}/action.py | 27 +- cosmos-inference/{ => cosmos3}/args.py | 145 +- cosmos-inference/{ => cosmos3}/args_test.py | 0 .../{ => cosmos3}/common/__init__.py | 0 cosmos-inference/{ => cosmos3}/common/args.py | 12 +- .../{ => cosmos3}/common/args_test.py | 0 .../{ => cosmos3}/common/checkpoints.py | 0 .../{ => cosmos3}/common/config.py | 0 .../{ => cosmos3}/common/config_test.py | 0 .../{ => cosmos3}/common/inference.py | 0 .../{ => cosmos3}/common/inference_test.py | 0 cosmos-inference/{ => cosmos3}/common/init.py | 0 .../experiment/action_policy_sft_8b.yaml | 0 .../experiment/mixed_modality_sft_8b.yaml | 0 .../{ => cosmos3}/dataset_samples.py | 0 .../forward_dynamics/sample_args.json | 0 .../defaults/image2video/sample_args.json | 0 .../inverse_dynamics/sample_args.json | 0 .../defaults/policy/sample_args.json | 0 .../defaults/prompt_upsampler.txt | 0 .../defaults/text2image/sample_args.json | 1 - .../defaults/text2video/sample_args.json | 0 .../defaults/video2video/sample_args.json | 1 - .../defaults/video_captioner.txt | 0 .../{ => cosmos3}/fixtures/__init__.py | 0 .../{ => cosmos3}/fixtures/args.py | 0 .../{ => cosmos3}/fixtures/script.py | 0 cosmos-inference/{ => cosmos3}/flags.py | 0 cosmos-inference/{ => cosmos3}/inference.py | 52 +- cosmos-inference/{ => cosmos3}/model.py | 0 cosmos-inference/{ => cosmos3}/model_test.py | 0 .../{ => cosmos3}/ray/__init__.py | 0 .../{ => cosmos3}/ray/configs/latency.yaml | 0 .../{ => cosmos3}/ray/configs/throughput.yaml | 0 cosmos-inference/{ => cosmos3}/ray/gradio.py | 0 cosmos-inference/{ => cosmos3}/ray/serve.py | 0 .../{ => cosmos3}/ray/serve_test.py | 0 cosmos-inference/{ => cosmos3}/ray/submit.py | 0 .../{ => cosmos3}/scripts/__init__.py | 0 .../{ => cosmos3}/scripts/_test.py | 0 .../scripts/_test/convert_model_to_dcp.sh | 0 .../_test/convert_model_to_diffusers.sh | 0 .../scripts/_test/export_config.sh | 0 .../scripts/_test/export_model.sh | 0 .../scripts/action_policy_server.py | 0 .../scripts/caption_from_video.py | 0 .../scripts/captions_to_sft_jsonl.py | 0 .../scripts/convert_model_to_dcp.py | 0 cosmos-inference/cosmos3/scripts/eval.py | 123 + .../{ => cosmos3}/scripts/eval_utils.py | 0 .../{ => cosmos3}/scripts/export_config.py | 0 .../{ => cosmos3}/scripts/export_model.py | 0 .../{ => cosmos3}/scripts/export_schemas.py | 0 .../scripts/export_schemas_test.py | 0 .../{ => cosmos3}/scripts/hydra.py | 0 .../{ => cosmos3}/scripts/inference.py | 15 +- .../scripts/prefetch_hf_checkpoints.py | 0 .../{ => cosmos3}/scripts/train.py | 0 .../{ => cosmos3}/scripts/upsample_prompts.py | 0 cosmos-inference/{ => cosmos3}/vision.py | 0 cosmos-inference/docs/inference.md | 13 +- cosmos-inference/docs/training.md | 45 +- .../action_forward_dynamics_camera_0.json | 2 +- .../action_forward_dynamics_camera_1.json | 2 +- .../omni/action_forward_dynamics_robot.json | 2 +- .../omni/action_inverse_dynamics_av.json | 2 +- .../inputs/omni/action_policy_robot.json | 2 +- .../packages/transformers-cosmos3/README.md | 16 + .../transformers-cosmos3/pyproject.toml | 25 + .../scripts/inference_example.py | 96 + .../scripts/inference_example.py.lock | 1346 ++++ .../transformers_cosmos3/__init__.py | 20 + .../transformers_cosmos3/model.py | 53 + .../packages/transformers-cosmos3/uv.lock | 705 ++ .../packages/vllm-cosmos3/README.md | 30 + .../vllm-cosmos3/pyproject.toml | 2 +- .../packages/vllm-cosmos3/uv.lock | 5694 +++++++++++++++++ .../vllm-cosmos3/vllm_cosmos3/__init__.py | 12 +- .../vllm-cosmos3/vllm_cosmos3/model.py | 83 +- .../schemas/OmniSampleOverrides.schema.json | 36 +- .../schemas/OmniSampleOverrides.yaml | 5 +- .../schemas/OmniSetupOverrides.schema.json | 56 +- .../schemas/OmniSetupOverrides.yaml | 9 +- cosmos-inference/vllm-cosmos3/README.md | 14 - cosmos-inference/vllm-cosmos3/uv.lock | 8 - 521 files changed, 9886 insertions(+), 699 deletions(-) delete mode 100644 cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py rename cosmos-inference/{ => cosmos3}/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/README.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/backends.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/checks.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/docs/README.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/docs/apis.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/docs/backends.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/docs/features.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/docs/multi-dim.md (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash2/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash2/checks.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash2/functions.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash2/meta.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash2/stubs.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash3/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash3/checks.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash3/functions.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash3/meta.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/flash3/stubs.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/frontend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/masks.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/natten/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/natten/checks.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/natten/functions.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/natten/meta.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/natten/stubs.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/determinism.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/environment.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/safe_ops/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/safe_ops/functools.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/safe_ops/log.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/utils/version.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/attention/varlen.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/common/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/common/core.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/common/io_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/common/presets.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/callbacks/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/callbacks/every_n.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/callbacks/image_grad_clip.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/callbacks/manual_gc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/base.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/ddp.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/dummy.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/s3_filesystem.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/safe_broadcast.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/tp.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/checkpointer/tp_ema.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/config.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/configs/lr_scheduler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/augmentors/merge_datadict.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/augmentors/v3_text_transforms.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/decoders/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/decoders/json_loader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/decoders/pkl_loader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/decoders/video_decoder.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/joint_training.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/mock_dataset.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/config/schema.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/dataloader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/decoders/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/decoders/depth.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/decoders/image.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/decoders/pickle.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/distributors/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/distributors/basic.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/iterators.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/misc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/stream.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/webdataset.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/datasets/webdataset/webdataset_ext.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/flags.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/flops/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/flops/omni_mot.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/flops/wan_vae.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/functional/batch_ops.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/functional/lr_scheduler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/functional/multi_step.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/functional/runge_kutta.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/file_io.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/instantiate.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/lazy.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/lazy_call.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/omegaconf_patch.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/lazy_config/registry.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/models/abstract_emb_model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/modules/camera.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/modules/denoiser_scaling.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/modules/edm_sde.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/modules/image_embeddings.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/modules/res_sampler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/serialization.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/trainer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/callback.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/checkpoint_db.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/checkpointer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/cluster_env.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/config_helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/context_managers.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/context_parallel.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/count_params.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/dataloader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/dataset_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/denoise_prediction.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/device.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/disabled_train.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/distributed.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/auto_auth.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/base_backend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/boto3_backend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/boto3_client.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/http_backend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/local_backend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/msc_backend.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/backends/registry_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/easy_io.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/file_client.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/base.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/byte_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/csv_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/json_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/np_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/pil_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/registry_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/torch_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/txt_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/ema.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/embedding_concat_strategy.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/env_parsers/cred_env_parser.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/env_parsers/customization_env_parser.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/env_parsers/env_parser.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/env_parsers/inference_env_parser.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/fsdp_helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/fused_adam.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/fused_nan_to_num.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/graph.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/high_sigma_strategy.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/launch.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/log.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/misc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/nsys_wrapper.sh (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/object_store.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/optim_instantiate.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/parallel_state_helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/primitives.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/profiling.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/progress_bar.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/registry.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/replace_bg_color.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/s3_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/scheduler.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/submit_job_helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/timer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/tone_curve.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/training.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/validator.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/validator_params.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/utils/wandb_util.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/visualize/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/visualize/img.py (100%) rename cosmos-inference/{ => cosmos3}/_src/imaginaire/visualize/video.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/compile_tokenizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/dataloader_state.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/dataloading_monitor.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/device_monitor.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/every_n_draw_audio_video_sample.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/every_n_draw_sample.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/expert_heatmap.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/generation.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/grad_clip.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/heart_beat.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/hf_export.py (98%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/iter_speed.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/load_pretrained.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/low_precision.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/mfu.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/moe_specialization_callback.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/moe_stability_callback.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/norm_monitor.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/ofu.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/param_count.py (100%) create mode 100644 cosmos-inference/cosmos3/_src/vfm/callbacks/per_stream_timing.py rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/sequence_packing_padding.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/sigma_loss_analysis.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/skip_nan_step.py (100%) create mode 100644 cosmos-inference/cosmos3/_src/vfm/callbacks/termination_signal_checkpoint.py rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/training_stats.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/vlm/grad_clip.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/wandb_log.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/callbacks/wandb_log_eval.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/checkpointer/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/checkpointer/dcp.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/conditioner.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/config.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/callbacks.py (96%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/checkpointer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/cluster.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/conditioner.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/ema.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/model_config.py (88%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/multiview_dataloader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/optimizer.py (100%) create mode 100644 cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/parallelism.py rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/tokenizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/unittest.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/defaults/vlm.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/config.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/callbacks.py (95%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/checkpointer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/config.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/dataloader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/model.py (87%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/optimizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/training.py (68%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/defaults/vlm_policy.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/experiment/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py (95%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py (95%) rename cosmos-inference/{ => cosmos3}/_src/vfm/configs/base/vlm/experiment/utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/action_normalization.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/action_spec.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/dataloaders.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/domain_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/json_formatter.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/libero_dataset.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/libero_pose_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/pose_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/transforms.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/unified_dataset.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/action/viewpoint_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/audio_caption.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/audio_parsing.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/caption_filter.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/cropping.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/idle_frames_text_info.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/image_editing_transform.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/image_resolution_filter.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/interleaved_image_transform.py (100%) create mode 100644 cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/merge_datadict.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/pkl_to_media.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/resolution_text_info.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/sequence_plan.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/sound_sequence_plan.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/text_tokenizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/text_transforms_for_image.py (94%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/text_transforms_for_video.py (65%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_input/blur.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_input/seg.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/transfer_control_transform.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/augmentors/video_parsing.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/joint_dataloader.py (84%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/local_datasets/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/local_datasets/helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/local_datasets/sft_dataset.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/sequence_packing.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/datasets/utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/rectified_flow.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/edm.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/fixed_step.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/fm_solvers_unipc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/unipc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/diffusion/samplers/utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/evaluation/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/evaluation/action/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/evaluation/action/libero/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/evaluation/action/libero/closed_loop_eval.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/hf_model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/configs/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/configuration_qwen3.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/qwen3.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/llm/qwen3/test_qwen3.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/attention.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/context_parallel_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/cosmos3_vfm_network.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/domain_aware_linear.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/dot_product_attention.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/modeling_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/parallelize_unified_mot.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/parallelize_vfm_network.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/qwen3_vl_unified_mot.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/unified_3dmrope_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/mot/unified_mot.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/omni_mot_model.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/parallelize_vlm.py (84%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/data_and_condition.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/dcp_loader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/load_balancing_loss.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/memory.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/safetensors_loader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/utils/taylorseer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/moe.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/models/vlm_model.py (96%) rename cosmos-inference/{ => cosmos3}/_src/vfm/tokenizers/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/tokenizers/interface.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/tokenizers/tokenization_qwen2.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py (98%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/context_parallel.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/data_utils.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/dtensor_helper.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/flash_attn.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/fused_adam.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/loss.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/misc.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/model_loader.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/model_weights_stats.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/monkey_patch.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/optimizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/parallelism.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/rand_state.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/tokenizer_benchmarking.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/vlm/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/vlm/optimizer.py (100%) rename cosmos-inference/{ => cosmos3}/_src/vfm/utils/vlm/pretrained_models_downloader.py (100%) rename cosmos-inference/{ => cosmos3}/_test.py (98%) rename cosmos-inference/{ => cosmos3}/_test/autoregressive.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/distilled.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/latency.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/omni-super.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/omni.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/omni_param.sh (100%) rename cosmos-inference/{ => cosmos3}/_test/sft.sh (100%) rename cosmos-inference/{ => cosmos3}/action.py (89%) rename cosmos-inference/{ => cosmos3}/args.py (88%) rename cosmos-inference/{ => cosmos3}/args_test.py (100%) rename cosmos-inference/{ => cosmos3}/common/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/common/args.py (97%) rename cosmos-inference/{ => cosmos3}/common/args_test.py (100%) rename cosmos-inference/{ => cosmos3}/common/checkpoints.py (100%) rename cosmos-inference/{ => cosmos3}/common/config.py (100%) rename cosmos-inference/{ => cosmos3}/common/config_test.py (100%) rename cosmos-inference/{ => cosmos3}/common/inference.py (100%) rename cosmos-inference/{ => cosmos3}/common/inference_test.py (100%) rename cosmos-inference/{ => cosmos3}/common/init.py (100%) rename cosmos-inference/{ => cosmos3}/configs/experiment/action_policy_sft_8b.yaml (100%) rename cosmos-inference/{ => cosmos3}/configs/experiment/mixed_modality_sft_8b.yaml (100%) rename cosmos-inference/{ => cosmos3}/dataset_samples.py (100%) rename cosmos-inference/{ => cosmos3}/defaults/forward_dynamics/sample_args.json (100%) rename cosmos-inference/{ => cosmos3}/defaults/image2video/sample_args.json (100%) rename cosmos-inference/{ => cosmos3}/defaults/inverse_dynamics/sample_args.json (100%) rename cosmos-inference/{ => cosmos3}/defaults/policy/sample_args.json (100%) rename cosmos-inference/{ => cosmos3}/defaults/prompt_upsampler.txt (100%) rename cosmos-inference/{ => cosmos3}/defaults/text2image/sample_args.json (97%) rename cosmos-inference/{ => cosmos3}/defaults/text2video/sample_args.json (100%) rename cosmos-inference/{ => cosmos3}/defaults/video2video/sample_args.json (96%) rename cosmos-inference/{ => cosmos3}/defaults/video_captioner.txt (100%) rename cosmos-inference/{ => cosmos3}/fixtures/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/fixtures/args.py (100%) rename cosmos-inference/{ => cosmos3}/fixtures/script.py (100%) rename cosmos-inference/{ => cosmos3}/flags.py (100%) rename cosmos-inference/{ => cosmos3}/inference.py (97%) rename cosmos-inference/{ => cosmos3}/model.py (100%) rename cosmos-inference/{ => cosmos3}/model_test.py (100%) rename cosmos-inference/{ => cosmos3}/ray/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/ray/configs/latency.yaml (100%) rename cosmos-inference/{ => cosmos3}/ray/configs/throughput.yaml (100%) rename cosmos-inference/{ => cosmos3}/ray/gradio.py (100%) rename cosmos-inference/{ => cosmos3}/ray/serve.py (100%) rename cosmos-inference/{ => cosmos3}/ray/serve_test.py (100%) rename cosmos-inference/{ => cosmos3}/ray/submit.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/__init__.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/_test.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/_test/convert_model_to_dcp.sh (100%) rename cosmos-inference/{ => cosmos3}/scripts/_test/convert_model_to_diffusers.sh (100%) rename cosmos-inference/{ => cosmos3}/scripts/_test/export_config.sh (100%) rename cosmos-inference/{ => cosmos3}/scripts/_test/export_model.sh (100%) rename cosmos-inference/{ => cosmos3}/scripts/action_policy_server.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/caption_from_video.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/captions_to_sft_jsonl.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/convert_model_to_dcp.py (100%) create mode 100644 cosmos-inference/cosmos3/scripts/eval.py rename cosmos-inference/{ => cosmos3}/scripts/eval_utils.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/export_config.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/export_model.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/export_schemas.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/export_schemas_test.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/hydra.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/inference.py (80%) rename cosmos-inference/{ => cosmos3}/scripts/prefetch_hf_checkpoints.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/train.py (100%) rename cosmos-inference/{ => cosmos3}/scripts/upsample_prompts.py (100%) rename cosmos-inference/{ => cosmos3}/vision.py (100%) create mode 100644 cosmos-inference/packages/transformers-cosmos3/README.md create mode 100644 cosmos-inference/packages/transformers-cosmos3/pyproject.toml create mode 100755 cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py create mode 100644 cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py.lock create mode 100644 cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/__init__.py create mode 100644 cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/model.py create mode 100644 cosmos-inference/packages/transformers-cosmos3/uv.lock create mode 100644 cosmos-inference/packages/vllm-cosmos3/README.md rename cosmos-inference/{ => packages}/vllm-cosmos3/pyproject.toml (97%) create mode 100644 cosmos-inference/packages/vllm-cosmos3/uv.lock rename cosmos-inference/{ => packages}/vllm-cosmos3/vllm_cosmos3/__init__.py (82%) rename cosmos-inference/{ => packages}/vllm-cosmos3/vllm_cosmos3/model.py (50%) delete mode 100644 cosmos-inference/vllm-cosmos3/README.md delete mode 100644 cosmos-inference/vllm-cosmos3/uv.lock diff --git a/cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md b/cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md index 2efe73cd..d1405fd1 100644 --- a/cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md +++ b/cosmos-inference/.agents/skills/cosmos3-post-training/SKILL.md @@ -5,13 +5,19 @@ description: > preparing the example dataset and DCP base checkpoint, editing the experiment config, launching distributed training with `torchrun`, running T2V/I2V/V2V inference with the trained DCP checkpoint, optionally exporting it to - Hugging Face safetensors, and the optional Video Captioning pipeline for - building custom datasets. Use when the user asks "how do I post-train Cosmos3", "how do I - fine-tune on my own video dataset", "how do I export a trained checkpoint", - "how do I caption videos for training", or any question about SFT, the - `cu130-train` / `cu128-train` install groups, the `mixed_modality_sft_8b.yaml` config, - the `convert_model_to_dcp` / `export_model` / `train` / `caption_from_video` - / `captions_to_sft_jsonl` scripts, or where SFT outputs land on disk. + Hugging Face safetensors, running **action evaluation** (`cosmos3.scripts.eval`) + on a held-out dataset for action checkpoints (forward / inverse dynamics, policy) + to score PSNR / action MSE, and the optional Video Captioning pipeline for + building custom datasets. Use when the user asks "how do I post-train Cosmos3", + "how do I fine-tune on my own video dataset", "how do I export a trained + checkpoint", "how do I evaluate an action checkpoint", "how do I run eval.py / + cosmos3.scripts.eval", "how do I caption videos for training", or any question + about SFT, the `cu130-train` / `cu128-train` install groups, the + `mixed_modality_sft_8b.yaml` config, the `convert_model_to_dcp` / `export_model` + / `train` / `eval` / `caption_from_video` / `captions_to_sft_jsonl` scripts, + action-evaluation metrics, or where SFT and action-eval outputs land on disk. + Note: this skill's evaluation coverage is **action-only** — eval.py does not + score non-action (T2V/I2V/V2V) checkpoints; for those, use the inference flow. --- # Cosmos3 Post-Training (SFT) @@ -21,6 +27,7 @@ description: > - User wants to fine-tune Cosmos3-Nano on their own video dataset (SFT) - User asks which fields in `mixed_modality_sft_8b.yaml` to override (lr, FSDP shard, max_iter, jsonl_paths, ...) - User wants to convert a base Hugging Face checkpoint to DCP, or convert a trained DCP back to safetensors +- User wants to score an **action** checkpoint (forward / inverse dynamics, policy) against a held-out dataset with `cosmos3.scripts.eval` — per-sample PSNR / action MSE plus an aggregate. Eval is action-only; do not invoke this skill's eval guidance for T2V/I2V/V2V checkpoints - User wants to caption raw videos with a VLM to build a training dataset - User wants to assemble a JSONL manifest from videos + captions - For installation, `--group=cu130-train` / `cu128-train`, or LD_LIBRARY_PATH issues, hand off to **cosmos3-setup** @@ -45,7 +52,11 @@ The canonical reference is `docs/training.md`. Use this table to route questions | How do I validate the config without actually training? | `docs/training.md` § Step 3 (the `--dry-run` flag) | | How do I export the trained DCP back to safetensors? | `docs/training.md` § Export checkpoint to Hugging Face safetensors | | How do I run inference with the trained checkpoint? | `docs/training.md` § Run inference with trained checkpoint | -| Where do training artifacts land? | `docs/training.md` § Outputs | +| How do I evaluate an action checkpoint (forward/inverse/policy)? | `docs/training.md` § Evaluation | +| How do I run `cosmos3.scripts.eval` / `eval.py`? | `docs/training.md` § Run action evaluation with trained checkpoint | +| Latency vs throughput preset for action eval? | `docs/training.md` § Run action evaluation with trained checkpoint | +| What metrics does action eval report (PSNR, action MSE)? | `docs/training.md` § Evaluation | +| Where do training and action-eval artifacts land? | `docs/training.md` § Outputs | | How do I caption raw videos for SFT? | `docs/training.md` § Video Captioning for Training Data Processing | | How do I serve the captioning VLM? | `docs/training.md` § Server setup | | How do I build a JSONL dataset from captions + videos? | `docs/training.md` § Creating Video Dataset JSONL File for Training | @@ -57,7 +68,8 @@ The canonical reference is `docs/training.md`. Use this table to route questions 3. **Step 2 — Prepare config** — the provided `cosmos3/configs/experiment/mixed_modality_sft_8b.yaml` runs as-is on the example dataset (~100 iterations); override `model.config.parallelism.data_parallel_shard_degree`, `dataloader_train.dataloader.datasets.*.jsonl_paths`, `optimizer.lr`, `trainer.max_iter`, etc. for custom runs. 4. **Step 3 — Run training** — `torchrun --nproc_per_node=8 -m cosmos3.scripts.train -o outputs/train --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml --config-overrides "checkpoint.load_path=$BASE_CHECKPOINT_PATH" "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl"` (use `--dry-run` first when iterating on config). DCP checkpoints land in `outputs/train/job/checkpoints/iter_`. 5. **Inference** — read `outputs/train/job/checkpoints/latest_checkpoint.txt`, point `cosmos3.scripts.inference` at the resulting `outputs/train/job/checkpoints/iter_` DCP path with `--config-file outputs/train/config.yaml`. The example input glob `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` covers T2V, I2V, and V2V (see `cosmos3-inference` skill for presets / input formats). -6. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. +6. **Action evaluation (action checkpoints only)** — `torchrun --nproc-per-node=8 -m cosmos3.scripts.eval --parallelism-preset=throughput -o outputs/train_eval --checkpoint-path $CHECKPOINT_PATH --config-file outputs/train/config.yaml --root-override /path/to/eval/dataset`. Resolves the dataloader from the training config (`val` split, falling back to `dataloader_train`), generates each sample through the inference engine, and scores against GT — `psnr` for video modes (`forward_dynamics`, `policy`) and `action_mse` for action modes (`inverse_dynamics`, `policy`). Per-sample `metrics.json` lives next to each `vision.mp4`; rank-0 aggregate is `outputs/train_eval/metrics_aggregate.json`. Skip this step entirely for T2V/I2V/V2V checkpoints — eval.py only computes action-mode metrics. +7. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. ## Things not obvious from the docs @@ -67,6 +79,10 @@ The canonical reference is `docs/training.md`. Use this table to route questions - **Mixed-modality input glob**: the example uses `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` with a `*` so a single command runs T2V, I2V, and V2V (the dataset has `inference_prompt/`, `inference_prompt_i2v/`, `inference_prompt_v2v/` siblings under `val/`). - **`data_parallel_shard_degree` must equal `WORLD_SIZE`**: it has to match `--nproc_per_node` on the `torchrun` command. Mismatch → FSDP init failure. - **`--dry-run`**: `cosmos3.scripts.train` accepts `--dry-run` to validate the config end-to-end without launching training. Use it whenever iterating on YAML overrides. +- **`eval.py` is action-only**: `cosmos3.scripts.eval` only scores action-mode generations (PSNR for predicted video, MSE for predicted action). It does *not* score the bridge-v2 video SFT walkthrough or any T2V/I2V/V2V checkpoint — those use `cosmos3.scripts.inference` (no GT scoring). Pointing `eval.py` at a non-action dataloader fails with "mode requires GT video/action but data_batch had none". +- **Throughput preset for full-dataset action eval**: `--parallelism-preset` defaults to `latency` (model sharded across all ranks, one sample at a time — required when the checkpoint is too large to fit on a single GPU). For full-dataset action eval when the model fits on one GPU, pass `--parallelism-preset=throughput` so wall-clock scales as `N / num_gpus × per_sample_time` instead of `N × per_sample_time`. +- **Action eval reuses the training dataloader via `--config-file`**: pointing `eval.py` at `outputs/train/config.yaml` resolves the same dataloader the model was trained against (`val` split by default; falls back to `dataloader_train` when there is no `dataloader_val`). Use `--root-override /path/to/eval/dataset` to swap in held-out data without editing the config; alternatives are `--gcs-root-override ` (downloads via `--cache-dir`) and `--gcs-path-map`. Use `--dataset ` when the dataloader has multiple entries. +- **`--model-mode` defaults to `joint`**: every dataset entry is evaluated under all three action modes — total generation count = `len(modes) × ceil(len(val_split) / sample_stride)`, capped by `--num-samples`. Restrict during development with `--num-samples N`, `--sample-stride K`, or `--model-mode `. Mode is also encoded in each sample's name (`//`) and is what the metric dispatcher reads back when scoring. - **Captioning server flags**: `vllm serve ... --allowed-local-media-path /` is required so the VLM can read the `file://` paths the captioning script sends. Use `Qwen/Qwen3-VL-8B-Instruct-FP8` as the recommended model; first launch downloads weights and may take several minutes (server is ready when you see `Application startup complete.`). - **Captioning input modes**: `cosmos3.scripts.caption_from_video` accepts `--video ` (single file or directory of `.mp4`s) or `-i ` where each line has a `vision_path` field — same JSONL format used downstream by training. - **Captioning output layout**: each input video produces a directory containing `caption.txt` and `sample_args.json`; `captions_to_sft_jsonl` then assembles those plus the source videos into a training-ready JSONL. diff --git a/cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md b/cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md index 2efe73cd..d1405fd1 100644 --- a/cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md +++ b/cosmos-inference/.claude/skills/cosmos3-post-training/SKILL.md @@ -5,13 +5,19 @@ description: > preparing the example dataset and DCP base checkpoint, editing the experiment config, launching distributed training with `torchrun`, running T2V/I2V/V2V inference with the trained DCP checkpoint, optionally exporting it to - Hugging Face safetensors, and the optional Video Captioning pipeline for - building custom datasets. Use when the user asks "how do I post-train Cosmos3", "how do I - fine-tune on my own video dataset", "how do I export a trained checkpoint", - "how do I caption videos for training", or any question about SFT, the - `cu130-train` / `cu128-train` install groups, the `mixed_modality_sft_8b.yaml` config, - the `convert_model_to_dcp` / `export_model` / `train` / `caption_from_video` - / `captions_to_sft_jsonl` scripts, or where SFT outputs land on disk. + Hugging Face safetensors, running **action evaluation** (`cosmos3.scripts.eval`) + on a held-out dataset for action checkpoints (forward / inverse dynamics, policy) + to score PSNR / action MSE, and the optional Video Captioning pipeline for + building custom datasets. Use when the user asks "how do I post-train Cosmos3", + "how do I fine-tune on my own video dataset", "how do I export a trained + checkpoint", "how do I evaluate an action checkpoint", "how do I run eval.py / + cosmos3.scripts.eval", "how do I caption videos for training", or any question + about SFT, the `cu130-train` / `cu128-train` install groups, the + `mixed_modality_sft_8b.yaml` config, the `convert_model_to_dcp` / `export_model` + / `train` / `eval` / `caption_from_video` / `captions_to_sft_jsonl` scripts, + action-evaluation metrics, or where SFT and action-eval outputs land on disk. + Note: this skill's evaluation coverage is **action-only** — eval.py does not + score non-action (T2V/I2V/V2V) checkpoints; for those, use the inference flow. --- # Cosmos3 Post-Training (SFT) @@ -21,6 +27,7 @@ description: > - User wants to fine-tune Cosmos3-Nano on their own video dataset (SFT) - User asks which fields in `mixed_modality_sft_8b.yaml` to override (lr, FSDP shard, max_iter, jsonl_paths, ...) - User wants to convert a base Hugging Face checkpoint to DCP, or convert a trained DCP back to safetensors +- User wants to score an **action** checkpoint (forward / inverse dynamics, policy) against a held-out dataset with `cosmos3.scripts.eval` — per-sample PSNR / action MSE plus an aggregate. Eval is action-only; do not invoke this skill's eval guidance for T2V/I2V/V2V checkpoints - User wants to caption raw videos with a VLM to build a training dataset - User wants to assemble a JSONL manifest from videos + captions - For installation, `--group=cu130-train` / `cu128-train`, or LD_LIBRARY_PATH issues, hand off to **cosmos3-setup** @@ -45,7 +52,11 @@ The canonical reference is `docs/training.md`. Use this table to route questions | How do I validate the config without actually training? | `docs/training.md` § Step 3 (the `--dry-run` flag) | | How do I export the trained DCP back to safetensors? | `docs/training.md` § Export checkpoint to Hugging Face safetensors | | How do I run inference with the trained checkpoint? | `docs/training.md` § Run inference with trained checkpoint | -| Where do training artifacts land? | `docs/training.md` § Outputs | +| How do I evaluate an action checkpoint (forward/inverse/policy)? | `docs/training.md` § Evaluation | +| How do I run `cosmos3.scripts.eval` / `eval.py`? | `docs/training.md` § Run action evaluation with trained checkpoint | +| Latency vs throughput preset for action eval? | `docs/training.md` § Run action evaluation with trained checkpoint | +| What metrics does action eval report (PSNR, action MSE)? | `docs/training.md` § Evaluation | +| Where do training and action-eval artifacts land? | `docs/training.md` § Outputs | | How do I caption raw videos for SFT? | `docs/training.md` § Video Captioning for Training Data Processing | | How do I serve the captioning VLM? | `docs/training.md` § Server setup | | How do I build a JSONL dataset from captions + videos? | `docs/training.md` § Creating Video Dataset JSONL File for Training | @@ -57,7 +68,8 @@ The canonical reference is `docs/training.md`. Use this table to route questions 3. **Step 2 — Prepare config** — the provided `cosmos3/configs/experiment/mixed_modality_sft_8b.yaml` runs as-is on the example dataset (~100 iterations); override `model.config.parallelism.data_parallel_shard_degree`, `dataloader_train.dataloader.datasets.*.jsonl_paths`, `optimizer.lr`, `trainer.max_iter`, etc. for custom runs. 4. **Step 3 — Run training** — `torchrun --nproc_per_node=8 -m cosmos3.scripts.train -o outputs/train --config-file cosmos3/configs/experiment/mixed_modality_sft_8b.yaml --config-overrides "checkpoint.load_path=$BASE_CHECKPOINT_PATH" "dataloader_train.dataloader.datasets.video.dataset.jsonl_paths=$DATASET_PATH/train/video_dataset_file.jsonl"` (use `--dry-run` first when iterating on config). DCP checkpoints land in `outputs/train/job/checkpoints/iter_`. 5. **Inference** — read `outputs/train/job/checkpoints/latest_checkpoint.txt`, point `cosmos3.scripts.inference` at the resulting `outputs/train/job/checkpoints/iter_` DCP path with `--config-file outputs/train/config.yaml`. The example input glob `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` covers T2V, I2V, and V2V (see `cosmos3-inference` skill for presets / input formats). -6. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. +6. **Action evaluation (action checkpoints only)** — `torchrun --nproc-per-node=8 -m cosmos3.scripts.eval --parallelism-preset=throughput -o outputs/train_eval --checkpoint-path $CHECKPOINT_PATH --config-file outputs/train/config.yaml --root-override /path/to/eval/dataset`. Resolves the dataloader from the training config (`val` split, falling back to `dataloader_train`), generates each sample through the inference engine, and scores against GT — `psnr` for video modes (`forward_dynamics`, `policy`) and `action_mse` for action modes (`inverse_dynamics`, `policy`). Per-sample `metrics.json` lives next to each `vision.mp4`; rank-0 aggregate is `outputs/train_eval/metrics_aggregate.json`. Skip this step entirely for T2V/I2V/V2V checkpoints — eval.py only computes action-mode metrics. +7. **Export (optional)** — `cosmos3.scripts.export_model` converts the DCP iter to Hugging Face safetensors at `outputs/train/model`. Not required for the standard inference flow above. ## Things not obvious from the docs @@ -67,6 +79,10 @@ The canonical reference is `docs/training.md`. Use this table to route questions - **Mixed-modality input glob**: the example uses `"$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json"` with a `*` so a single command runs T2V, I2V, and V2V (the dataset has `inference_prompt/`, `inference_prompt_i2v/`, `inference_prompt_v2v/` siblings under `val/`). - **`data_parallel_shard_degree` must equal `WORLD_SIZE`**: it has to match `--nproc_per_node` on the `torchrun` command. Mismatch → FSDP init failure. - **`--dry-run`**: `cosmos3.scripts.train` accepts `--dry-run` to validate the config end-to-end without launching training. Use it whenever iterating on YAML overrides. +- **`eval.py` is action-only**: `cosmos3.scripts.eval` only scores action-mode generations (PSNR for predicted video, MSE for predicted action). It does *not* score the bridge-v2 video SFT walkthrough or any T2V/I2V/V2V checkpoint — those use `cosmos3.scripts.inference` (no GT scoring). Pointing `eval.py` at a non-action dataloader fails with "mode requires GT video/action but data_batch had none". +- **Throughput preset for full-dataset action eval**: `--parallelism-preset` defaults to `latency` (model sharded across all ranks, one sample at a time — required when the checkpoint is too large to fit on a single GPU). For full-dataset action eval when the model fits on one GPU, pass `--parallelism-preset=throughput` so wall-clock scales as `N / num_gpus × per_sample_time` instead of `N × per_sample_time`. +- **Action eval reuses the training dataloader via `--config-file`**: pointing `eval.py` at `outputs/train/config.yaml` resolves the same dataloader the model was trained against (`val` split by default; falls back to `dataloader_train` when there is no `dataloader_val`). Use `--root-override /path/to/eval/dataset` to swap in held-out data without editing the config; alternatives are `--gcs-root-override ` (downloads via `--cache-dir`) and `--gcs-path-map`. Use `--dataset ` when the dataloader has multiple entries. +- **`--model-mode` defaults to `joint`**: every dataset entry is evaluated under all three action modes — total generation count = `len(modes) × ceil(len(val_split) / sample_stride)`, capped by `--num-samples`. Restrict during development with `--num-samples N`, `--sample-stride K`, or `--model-mode `. Mode is also encoded in each sample's name (`//`) and is what the metric dispatcher reads back when scoring. - **Captioning server flags**: `vllm serve ... --allowed-local-media-path /` is required so the VLM can read the `file://` paths the captioning script sends. Use `Qwen/Qwen3-VL-8B-Instruct-FP8` as the recommended model; first launch downloads weights and may take several minutes (server is ready when you see `Application startup complete.`). - **Captioning input modes**: `cosmos3.scripts.caption_from_video` accepts `--video ` (single file or directory of `.mp4`s) or `-i ` where each line has a `vision_path` field — same JSONL format used downstream by training. - **Captioning output layout**: each input video produces a directory containing `caption.txt` and `sample_args.json`; `captions_to_sft_jsonl` then assembles those plus the source videos into a training-ready JSONL. diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py b/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py deleted file mode 100644 index c41bf43e..00000000 --- a/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_video_parsing.py +++ /dev/null @@ -1,223 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from typing import Optional - -import numpy as np -import omegaconf -import torch -from torchcodec.decoders import VideoDecoder -from torchvision.transforms.v2 import Resize - -from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size -from cosmos3._src.imaginaire.utils import log -from cosmos3._src.vfm.datasets.augmentors.video_parsing import VideoParsingWithFullFrames - -# Local copies of the torchcodec decoder helpers so this module does not depend on -# private symbols of ``video_parsing.py``. Behavior matches the originals. -_PostDecodeTransforms = list[Callable[[torch.Tensor], torch.Tensor]] | None -_SUPPORTS_VIDEO_DECODER_TRANSFORMS: bool | None = None -_WARNED_POST_DECODE_TRANSFORMS = False - - -def _create_video_decoder( - video: bytes, - seek_mode: str, - num_ffmpeg_threads: int, - transforms: _PostDecodeTransforms = None, -) -> tuple[VideoDecoder, _PostDecodeTransforms]: - global _SUPPORTS_VIDEO_DECODER_TRANSFORMS, _WARNED_POST_DECODE_TRANSFORMS - - kwargs = {"seek_mode": seek_mode, "num_ffmpeg_threads": num_ffmpeg_threads} - if transforms is None: - return VideoDecoder(video, **kwargs), None - - if _SUPPORTS_VIDEO_DECODER_TRANSFORMS is not False: - try: - decoder = VideoDecoder(video, transforms=transforms, **kwargs) - _SUPPORTS_VIDEO_DECODER_TRANSFORMS = True - return decoder, None - except TypeError as e: - if "transforms" not in str(e): - raise - _SUPPORTS_VIDEO_DECODER_TRANSFORMS = False - - if not _WARNED_POST_DECODE_TRANSFORMS: - log.warning( - "Installed torchcodec does not support VideoDecoder(transforms=...); " - "applying video transforms after frame decode.", - rank0_only=False, - ) - _WARNED_POST_DECODE_TRANSFORMS = True - return VideoDecoder(video, **kwargs), transforms - - -def _apply_post_decode_transforms( - frames: torch.Tensor, transforms: _PostDecodeTransforms -) -> torch.Tensor: # frames: [T,C,H,W], returns: [T,C,H,W] - if transforms is None: - return frames - - for transform in transforms: - frames = transform(frames) # [T,C,H,W] - return frames - - -class VideoTransferAlignedFullFramesParsing(VideoParsingWithFullFrames): - """Decode RGB and precomputed control videos with one shared v3 frame plan. - - This is the variable-length counterpart of the fixed-window transfer parser. - The RGB stream determines the sampled stride and frame indices. Any extra - input video streams, such as depth or segmentation, are decoded with the same - frame indices so the control video stays temporally aligned with the target. - """ - - def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: - assert len(input_keys) >= 2, "VideoTransferAlignedFullFramesParsing requires [metas, video, ...]." - super().__init__(input_keys=input_keys[:2], output_keys=output_keys, args=args) - self.input_keys = input_keys - self.control_video_keys = input_keys[2:] - - def _build_rgb_decode_transform(self, data_dict: dict, meta_dict: dict) -> list[Resize] | None: - if not self.perform_resize: - return None - - img_size = obtain_augmentation_size(data_dict, {"size": self.size}) - assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( - f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" - ) - img_w, img_h = img_size - orig_w, orig_h = meta_dict["width"], meta_dict["height"] - - scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) - target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) - assert target_size[0] <= img_h and target_size[1] <= img_w, ( - f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" - ) - return [Resize(target_size)] - - def _sample_frame_indices(self, decoder_len: int) -> tuple[list[int], int]: - stride = self._sample_stride_with_bias(self.max_stride, self.min_stride) - frame_indices = np.arange(0, decoder_len, stride).tolist() - max_num_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) - if max_num_frames < 1: - return [], stride - - # Wan VAE temporal compression expects 1 + 4N video frames. - num_video_frames = 1 + 4 * ((max_num_frames - 1) // 4) - return frame_indices[:num_video_frames], stride - - def _decode_frames_at( - self, - video: bytes, - frame_indices: list[int], - transforms: list[Resize] | None = None, - ) -> torch.Tensor: # returns [C,T,H,W] - video_decoder, post_decode_transforms = _create_video_decoder( - video, - self.seek_mode, - self.video_decode_num_threads, - transforms, - ) - try: - frame_batch = video_decoder.get_frames_at(frame_indices) - frames = frame_batch.data # [T,C,H,W] - frames = _apply_post_decode_transforms(frames, post_decode_transforms) # [T,C,H,W] - frames = frames.permute(1, 0, 2, 3) # [C,T,H,W] - finally: - del video_decoder - return frames # [C,T,H,W] - - def __call__(self, data_dict: dict) -> dict | None: - try: - meta_dict = data_dict[self.meta_key] - video = data_dict[self.video_key] - except Exception: - log.warning( - f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", - rank0_only=False, - ) - return None - - if not self._validate_and_probe(video, meta_dict, data_dict): - return None - - rgb_transform = self._build_rgb_decode_transform(data_dict, meta_dict) - try: - rgb_decoder = VideoDecoder( - video, - seek_mode=self.seek_mode, - num_ffmpeg_threads=self.video_decode_num_threads, - ) - decoder_len = len(rgb_decoder) - del rgb_decoder - - frame_indices, stride = self._sample_frame_indices(decoder_len) - if len(frame_indices) == 0: - log.warning( - f"VideoTransferAlignedFullFramesParsing: no valid frame indices. " - f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", - rank0_only=False, - ) - return None - - video_frames = self._decode_frames_at(video, frame_indices, rgb_transform) # [C,T,H,W] - except Exception as e: - log.warning( - f"Failed to decode RGB video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", - rank0_only=False, - ) - return None - - base_video_info = { - "frame_start": frame_indices[0], - "frame_end": frame_indices[-1], - "frame_indices": frame_indices, - "num_frames": len(frame_indices), - "fps": meta_dict["framerate"], - "conditioning_fps": meta_dict["framerate"] / stride, - "num_multiplier": stride, - "n_orig_video_frames": decoder_len, - } - data_dict[self.video_key] = { - **base_video_info, - "video": video_frames, # [C,T,H,W] - } - - for control_video_key in self.control_video_keys: - control_video = data_dict.get(control_video_key) - if not isinstance(control_video, bytes): - log.warning( - f"VideoTransferAlignedFullFramesParsing: missing bytes for {control_video_key}. " - f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", - rank0_only=False, - ) - return None - try: - control_frames = self._decode_frames_at(control_video, frame_indices) # [C,T,H,W] - except Exception as e: - log.warning( - f"Failed to decode {control_video_key}. " - f"url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", - rank0_only=False, - ) - return None - data_dict[control_video_key] = { - **base_video_info, - "video": control_frames, # [C,T,H,W] - } - - return data_dict diff --git a/cosmos-inference/__init__.py b/cosmos-inference/cosmos3/__init__.py similarity index 100% rename from cosmos-inference/__init__.py rename to cosmos-inference/cosmos3/__init__.py diff --git a/cosmos-inference/_src/imaginaire/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/README.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/README.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/README.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/README.md diff --git a/cosmos-inference/_src/imaginaire/attention/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/backends.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/backends.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/backends.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/backends.py diff --git a/cosmos-inference/_src/imaginaire/attention/checks.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/checks.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/checks.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/checks.py diff --git a/cosmos-inference/_src/imaginaire/attention/docs/README.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/docs/README.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/docs/README.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/docs/README.md diff --git a/cosmos-inference/_src/imaginaire/attention/docs/apis.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/docs/apis.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/docs/apis.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/docs/apis.md diff --git a/cosmos-inference/_src/imaginaire/attention/docs/backends.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/docs/backends.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/docs/backends.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/docs/backends.md diff --git a/cosmos-inference/_src/imaginaire/attention/docs/features.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/docs/features.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/docs/features.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/docs/features.md diff --git a/cosmos-inference/_src/imaginaire/attention/docs/multi-dim.md b/cosmos-inference/cosmos3/_src/imaginaire/attention/docs/multi-dim.md similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/docs/multi-dim.md rename to cosmos-inference/cosmos3/_src/imaginaire/attention/docs/multi-dim.md diff --git a/cosmos-inference/_src/imaginaire/attention/flash2/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash2/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash2/checks.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/checks.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash2/checks.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/checks.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash2/functions.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/functions.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash2/functions.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/functions.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash2/meta.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/meta.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash2/meta.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/meta.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash2/stubs.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/stubs.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash2/stubs.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash2/stubs.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash3/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash3/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash3/checks.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/checks.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash3/checks.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/checks.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash3/functions.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/functions.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash3/functions.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/functions.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash3/meta.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/meta.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash3/meta.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/meta.py diff --git a/cosmos-inference/_src/imaginaire/attention/flash3/stubs.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/stubs.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/flash3/stubs.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/flash3/stubs.py diff --git a/cosmos-inference/_src/imaginaire/attention/frontend.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/frontend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/frontend.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/frontend.py diff --git a/cosmos-inference/_src/imaginaire/attention/masks.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/masks.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/masks.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/masks.py diff --git a/cosmos-inference/_src/imaginaire/attention/natten/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/natten/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/natten/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/natten/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/natten/checks.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/natten/checks.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/natten/checks.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/natten/checks.py diff --git a/cosmos-inference/_src/imaginaire/attention/natten/functions.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/natten/functions.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/natten/functions.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/natten/functions.py diff --git a/cosmos-inference/_src/imaginaire/attention/natten/meta.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/natten/meta.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/natten/meta.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/natten/meta.py diff --git a/cosmos-inference/_src/imaginaire/attention/natten/stubs.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/natten/stubs.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/natten/stubs.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/natten/stubs.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/determinism.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/determinism.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/determinism.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/determinism.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/environment.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/environment.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/environment.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/environment.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/safe_ops/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/__init__.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/functools.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/safe_ops/functools.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/functools.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/safe_ops/log.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/safe_ops/log.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/safe_ops/log.py diff --git a/cosmos-inference/_src/imaginaire/attention/utils/version.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/utils/version.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/utils/version.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/utils/version.py diff --git a/cosmos-inference/_src/imaginaire/attention/varlen.py b/cosmos-inference/cosmos3/_src/imaginaire/attention/varlen.py similarity index 100% rename from cosmos-inference/_src/imaginaire/attention/varlen.py rename to cosmos-inference/cosmos3/_src/imaginaire/attention/varlen.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/blocklist.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/profile_blocklist.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/blocklist/utils.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/core.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/core.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/core.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/io_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/io_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/io_utils.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/presets.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/common/presets.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/common/presets.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/blur_utils.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/face_blur_filter.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/face_blur_filter/retinaface_utils.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/categories.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/llamaGuard3/llamaGuard3.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/categories.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/qwen3guard/qwen3guard.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/__init__.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/model.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/video_content_safety_filter.py diff --git a/cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py b/cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py similarity index 100% rename from cosmos-inference/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py rename to cosmos-inference/cosmos3/_src/imaginaire/auxiliary/guardrail/video_content_safety_filter/vision_encoder.py diff --git a/cosmos-inference/_src/imaginaire/callbacks/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/callbacks/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/callbacks/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/callbacks/__init__.py diff --git a/cosmos-inference/_src/imaginaire/callbacks/every_n.py b/cosmos-inference/cosmos3/_src/imaginaire/callbacks/every_n.py similarity index 100% rename from cosmos-inference/_src/imaginaire/callbacks/every_n.py rename to cosmos-inference/cosmos3/_src/imaginaire/callbacks/every_n.py diff --git a/cosmos-inference/_src/imaginaire/callbacks/image_grad_clip.py b/cosmos-inference/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py similarity index 100% rename from cosmos-inference/_src/imaginaire/callbacks/image_grad_clip.py rename to cosmos-inference/cosmos3/_src/imaginaire/callbacks/image_grad_clip.py diff --git a/cosmos-inference/_src/imaginaire/callbacks/manual_gc.py b/cosmos-inference/cosmos3/_src/imaginaire/callbacks/manual_gc.py similarity index 100% rename from cosmos-inference/_src/imaginaire/callbacks/manual_gc.py rename to cosmos-inference/cosmos3/_src/imaginaire/callbacks/manual_gc.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/__init__.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/base.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/base.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/base.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/base.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/ddp.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/ddp.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/ddp.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/ddp.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/dummy.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/dummy.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/dummy.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/dummy.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/s3_filesystem.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/s3_filesystem.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/s3_filesystem.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/safe_broadcast.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/safe_broadcast.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/safe_broadcast.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/tp.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/tp.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/tp.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/tp.py diff --git a/cosmos-inference/_src/imaginaire/checkpointer/tp_ema.py b/cosmos-inference/cosmos3/_src/imaginaire/checkpointer/tp_ema.py similarity index 100% rename from cosmos-inference/_src/imaginaire/checkpointer/tp_ema.py rename to cosmos-inference/cosmos3/_src/imaginaire/checkpointer/tp_ema.py diff --git a/cosmos-inference/_src/imaginaire/config.py b/cosmos-inference/cosmos3/_src/imaginaire/config.py similarity index 100% rename from cosmos-inference/_src/imaginaire/config.py rename to cosmos-inference/cosmos3/_src/imaginaire/config.py diff --git a/cosmos-inference/_src/imaginaire/configs/lr_scheduler.py b/cosmos-inference/cosmos3/_src/imaginaire/configs/lr_scheduler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/configs/lr_scheduler.py rename to cosmos-inference/cosmos3/_src/imaginaire/configs/lr_scheduler.py diff --git a/cosmos-inference/_src/imaginaire/datasets/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/augmentors/merge_datadict.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/augmentors/merge_datadict.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/augmentors/merge_datadict.py diff --git a/cosmos-inference/_src/imaginaire/datasets/augmentors/v3_text_transforms.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/augmentors/v3_text_transforms.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/augmentors/v3_text_transforms.py diff --git a/cosmos-inference/_src/imaginaire/datasets/decoders/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/decoders/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/decoders/json_loader.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/decoders/json_loader.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/json_loader.py diff --git a/cosmos-inference/_src/imaginaire/datasets/decoders/pkl_loader.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/decoders/pkl_loader.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/pkl_loader.py diff --git a/cosmos-inference/_src/imaginaire/datasets/decoders/video_decoder.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/decoders/video_decoder.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/decoders/video_decoder.py diff --git a/cosmos-inference/_src/imaginaire/datasets/joint_training.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/joint_training.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/joint_training.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/joint_training.py diff --git a/cosmos-inference/_src/imaginaire/datasets/mock_dataset.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/mock_dataset.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/mock_dataset.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/mock_dataset.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/augmentor.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/camera.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/depth.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/geometry/pointcloud.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/cropping.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/flip.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/misc.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/normalize.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/padding.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/augmentors/image/resize.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/config/schema.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/config/schema.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/config/schema.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/dataloader.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/dataloader.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/dataloader.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/depth.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/depth.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/depth.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/image.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/image.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/image.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/pickle.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/decoders/pickle.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/decoders/pickle.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/__init__.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/basic.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/basic.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/basic.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/multi_aspect_ratio_v2.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/distributors/weighted_multi_aspect_ratio.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/iterators.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/iterators.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/iterators.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/misc.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/misc.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/misc.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/stream.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/stream.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/stream.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamDataLoaderTest.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamMockTest.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamStatsOverheadBenchmark.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/RetryingStreamTarIteratorTest.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/utils/unit_test/mpi_rank_worker.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/webdataset.py diff --git a/cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset_ext.py b/cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py similarity index 100% rename from cosmos-inference/_src/imaginaire/datasets/webdataset/webdataset_ext.py rename to cosmos-inference/cosmos3/_src/imaginaire/datasets/webdataset/webdataset_ext.py diff --git a/cosmos-inference/_src/imaginaire/flags.py b/cosmos-inference/cosmos3/_src/imaginaire/flags.py similarity index 100% rename from cosmos-inference/_src/imaginaire/flags.py rename to cosmos-inference/cosmos3/_src/imaginaire/flags.py diff --git a/cosmos-inference/_src/imaginaire/flops/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/flops/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/flops/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/flops/__init__.py diff --git a/cosmos-inference/_src/imaginaire/flops/omni_mot.py b/cosmos-inference/cosmos3/_src/imaginaire/flops/omni_mot.py similarity index 100% rename from cosmos-inference/_src/imaginaire/flops/omni_mot.py rename to cosmos-inference/cosmos3/_src/imaginaire/flops/omni_mot.py diff --git a/cosmos-inference/_src/imaginaire/flops/wan_vae.py b/cosmos-inference/cosmos3/_src/imaginaire/flops/wan_vae.py similarity index 100% rename from cosmos-inference/_src/imaginaire/flops/wan_vae.py rename to cosmos-inference/cosmos3/_src/imaginaire/flops/wan_vae.py diff --git a/cosmos-inference/_src/imaginaire/functional/batch_ops.py b/cosmos-inference/cosmos3/_src/imaginaire/functional/batch_ops.py similarity index 100% rename from cosmos-inference/_src/imaginaire/functional/batch_ops.py rename to cosmos-inference/cosmos3/_src/imaginaire/functional/batch_ops.py diff --git a/cosmos-inference/_src/imaginaire/functional/lr_scheduler.py b/cosmos-inference/cosmos3/_src/imaginaire/functional/lr_scheduler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/functional/lr_scheduler.py rename to cosmos-inference/cosmos3/_src/imaginaire/functional/lr_scheduler.py diff --git a/cosmos-inference/_src/imaginaire/functional/multi_step.py b/cosmos-inference/cosmos3/_src/imaginaire/functional/multi_step.py similarity index 100% rename from cosmos-inference/_src/imaginaire/functional/multi_step.py rename to cosmos-inference/cosmos3/_src/imaginaire/functional/multi_step.py diff --git a/cosmos-inference/_src/imaginaire/functional/runge_kutta.py b/cosmos-inference/cosmos3/_src/imaginaire/functional/runge_kutta.py similarity index 100% rename from cosmos-inference/_src/imaginaire/functional/runge_kutta.py rename to cosmos-inference/cosmos3/_src/imaginaire/functional/runge_kutta.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/__init__.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/file_io.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/file_io.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/file_io.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/file_io.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/instantiate.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/instantiate.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/instantiate.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/instantiate.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/lazy.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/lazy.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/lazy.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/lazy.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/lazy_call.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/lazy_call.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/lazy_call.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/lazy_call.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/omegaconf_patch.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/omegaconf_patch.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/omegaconf_patch.py diff --git a/cosmos-inference/_src/imaginaire/lazy_config/registry.py b/cosmos-inference/cosmos3/_src/imaginaire/lazy_config/registry.py similarity index 100% rename from cosmos-inference/_src/imaginaire/lazy_config/registry.py rename to cosmos-inference/cosmos3/_src/imaginaire/lazy_config/registry.py diff --git a/cosmos-inference/_src/imaginaire/model.py b/cosmos-inference/cosmos3/_src/imaginaire/model.py similarity index 100% rename from cosmos-inference/_src/imaginaire/model.py rename to cosmos-inference/cosmos3/_src/imaginaire/model.py diff --git a/cosmos-inference/_src/imaginaire/models/abstract_emb_model.py b/cosmos-inference/cosmos3/_src/imaginaire/models/abstract_emb_model.py similarity index 100% rename from cosmos-inference/_src/imaginaire/models/abstract_emb_model.py rename to cosmos-inference/cosmos3/_src/imaginaire/models/abstract_emb_model.py diff --git a/cosmos-inference/_src/imaginaire/modules/camera.py b/cosmos-inference/cosmos3/_src/imaginaire/modules/camera.py similarity index 100% rename from cosmos-inference/_src/imaginaire/modules/camera.py rename to cosmos-inference/cosmos3/_src/imaginaire/modules/camera.py diff --git a/cosmos-inference/_src/imaginaire/modules/denoiser_scaling.py b/cosmos-inference/cosmos3/_src/imaginaire/modules/denoiser_scaling.py similarity index 100% rename from cosmos-inference/_src/imaginaire/modules/denoiser_scaling.py rename to cosmos-inference/cosmos3/_src/imaginaire/modules/denoiser_scaling.py diff --git a/cosmos-inference/_src/imaginaire/modules/edm_sde.py b/cosmos-inference/cosmos3/_src/imaginaire/modules/edm_sde.py similarity index 100% rename from cosmos-inference/_src/imaginaire/modules/edm_sde.py rename to cosmos-inference/cosmos3/_src/imaginaire/modules/edm_sde.py diff --git a/cosmos-inference/_src/imaginaire/modules/image_embeddings.py b/cosmos-inference/cosmos3/_src/imaginaire/modules/image_embeddings.py similarity index 100% rename from cosmos-inference/_src/imaginaire/modules/image_embeddings.py rename to cosmos-inference/cosmos3/_src/imaginaire/modules/image_embeddings.py diff --git a/cosmos-inference/_src/imaginaire/modules/res_sampler.py b/cosmos-inference/cosmos3/_src/imaginaire/modules/res_sampler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/modules/res_sampler.py rename to cosmos-inference/cosmos3/_src/imaginaire/modules/res_sampler.py diff --git a/cosmos-inference/_src/imaginaire/serialization.py b/cosmos-inference/cosmos3/_src/imaginaire/serialization.py similarity index 100% rename from cosmos-inference/_src/imaginaire/serialization.py rename to cosmos-inference/cosmos3/_src/imaginaire/serialization.py diff --git a/cosmos-inference/_src/imaginaire/trainer.py b/cosmos-inference/cosmos3/_src/imaginaire/trainer.py similarity index 100% rename from cosmos-inference/_src/imaginaire/trainer.py rename to cosmos-inference/cosmos3/_src/imaginaire/trainer.py diff --git a/cosmos-inference/_src/imaginaire/utils/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/__init__.py diff --git a/cosmos-inference/_src/imaginaire/utils/callback.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/callback.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/callback.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/callback.py diff --git a/cosmos-inference/_src/imaginaire/utils/checkpoint_db.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/checkpoint_db.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/checkpoint_db.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/checkpoint_db.py diff --git a/cosmos-inference/_src/imaginaire/utils/checkpointer.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/checkpointer.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/checkpointer.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/checkpointer.py diff --git a/cosmos-inference/_src/imaginaire/utils/cluster_env.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/cluster_env.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/cluster_env.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/cluster_env.py diff --git a/cosmos-inference/_src/imaginaire/utils/config_helper.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/config_helper.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/config_helper.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/config_helper.py diff --git a/cosmos-inference/_src/imaginaire/utils/context_managers.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/context_managers.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/context_managers.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/context_managers.py diff --git a/cosmos-inference/_src/imaginaire/utils/context_parallel.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/context_parallel.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/context_parallel.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/context_parallel.py diff --git a/cosmos-inference/_src/imaginaire/utils/count_params.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/count_params.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/count_params.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/count_params.py diff --git a/cosmos-inference/_src/imaginaire/utils/dataloader.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/dataloader.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/dataloader.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/dataloader.py diff --git a/cosmos-inference/_src/imaginaire/utils/dataset_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/dataset_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/dataset_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/dataset_utils.py diff --git a/cosmos-inference/_src/imaginaire/utils/denoise_prediction.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/denoise_prediction.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/denoise_prediction.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/denoise_prediction.py diff --git a/cosmos-inference/_src/imaginaire/utils/device.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/device.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/device.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/device.py diff --git a/cosmos-inference/_src/imaginaire/utils/disabled_train.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/disabled_train.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/disabled_train.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/disabled_train.py diff --git a/cosmos-inference/_src/imaginaire/utils/distributed.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/distributed.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/distributed.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/distributed.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/__init__.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/__init__.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/auto_auth.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/auto_auth.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/auto_auth.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/base_backend.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/base_backend.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/base_backend.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_backend.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_backend.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_backend.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_client.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/boto3_client.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/boto3_client.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/http_backend.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/http_backend.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/http_backend.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/local_backend.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/local_backend.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/local_backend.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/msc_backend.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/msc_backend.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/msc_backend.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/backends/registry_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/backends/registry_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/backends/registry_utils.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/easy_io.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/easy_io.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/easy_io.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/file_client.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/file_client.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/file_client.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/file_client.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/__init__.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/base.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/base.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/base.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/byte_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/byte_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/byte_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/csv_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/csv_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/csv_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/gzip_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/imageio_video_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/json_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/json_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/json_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/jsonl_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/np_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/np_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/np_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pandas_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pickle_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pil_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/pil_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/pil_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/registry_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/registry_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/registry_utils.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/tarfile_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torch_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torch_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/torch_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/torchjit_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/trimesh_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/txt_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/txt_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/txt_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/easy_io/handlers/yaml_handler.py diff --git a/cosmos-inference/_src/imaginaire/utils/ema.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/ema.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/ema.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/ema.py diff --git a/cosmos-inference/_src/imaginaire/utils/embedding_concat_strategy.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/embedding_concat_strategy.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/embedding_concat_strategy.py diff --git a/cosmos-inference/_src/imaginaire/utils/env_parsers/cred_env_parser.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/env_parsers/cred_env_parser.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/cred_env_parser.py diff --git a/cosmos-inference/_src/imaginaire/utils/env_parsers/customization_env_parser.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/env_parsers/customization_env_parser.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/customization_env_parser.py diff --git a/cosmos-inference/_src/imaginaire/utils/env_parsers/env_parser.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/env_parsers/env_parser.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/env_parser.py diff --git a/cosmos-inference/_src/imaginaire/utils/env_parsers/inference_env_parser.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/env_parsers/inference_env_parser.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/env_parsers/inference_env_parser.py diff --git a/cosmos-inference/_src/imaginaire/utils/fsdp_helper.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/fsdp_helper.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/fsdp_helper.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/fsdp_helper.py diff --git a/cosmos-inference/_src/imaginaire/utils/fused_adam.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/fused_adam.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/fused_adam.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/fused_adam.py diff --git a/cosmos-inference/_src/imaginaire/utils/fused_nan_to_num.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/fused_nan_to_num.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/fused_nan_to_num.py diff --git a/cosmos-inference/_src/imaginaire/utils/graph.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/graph.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/graph.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/graph.py diff --git a/cosmos-inference/_src/imaginaire/utils/high_sigma_strategy.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/high_sigma_strategy.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/high_sigma_strategy.py diff --git a/cosmos-inference/_src/imaginaire/utils/launch.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/launch.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/launch.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/launch.py diff --git a/cosmos-inference/_src/imaginaire/utils/log.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/log.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/log.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/log.py diff --git a/cosmos-inference/_src/imaginaire/utils/misc.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/misc.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/misc.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/misc.py diff --git a/cosmos-inference/_src/imaginaire/utils/nsys_wrapper.sh b/cosmos-inference/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/nsys_wrapper.sh rename to cosmos-inference/cosmos3/_src/imaginaire/utils/nsys_wrapper.sh diff --git a/cosmos-inference/_src/imaginaire/utils/object_store.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/object_store.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/object_store.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/object_store.py diff --git a/cosmos-inference/_src/imaginaire/utils/optim_instantiate.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/optim_instantiate.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/optim_instantiate.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/optim_instantiate.py diff --git a/cosmos-inference/_src/imaginaire/utils/parallel_state_helper.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/parallel_state_helper.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/parallel_state_helper.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/parallel_state_helper.py diff --git a/cosmos-inference/_src/imaginaire/utils/primitives.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/primitives.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/primitives.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/primitives.py diff --git a/cosmos-inference/_src/imaginaire/utils/profiling.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/profiling.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/profiling.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/profiling.py diff --git a/cosmos-inference/_src/imaginaire/utils/progress_bar.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/progress_bar.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/progress_bar.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/progress_bar.py diff --git a/cosmos-inference/_src/imaginaire/utils/registry.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/registry.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/registry.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/registry.py diff --git a/cosmos-inference/_src/imaginaire/utils/replace_bg_color.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/replace_bg_color.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/replace_bg_color.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/replace_bg_color.py diff --git a/cosmos-inference/_src/imaginaire/utils/s3_utils.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/s3_utils.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/s3_utils.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/s3_utils.py diff --git a/cosmos-inference/_src/imaginaire/utils/scheduler.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/scheduler.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/scheduler.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/scheduler.py diff --git a/cosmos-inference/_src/imaginaire/utils/submit_job_helper.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/submit_job_helper.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/submit_job_helper.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/submit_job_helper.py diff --git a/cosmos-inference/_src/imaginaire/utils/timer.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/timer.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/timer.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/timer.py diff --git a/cosmos-inference/_src/imaginaire/utils/tone_curve.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/tone_curve.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/tone_curve.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/tone_curve.py diff --git a/cosmos-inference/_src/imaginaire/utils/training.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/training.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/training.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/training.py diff --git a/cosmos-inference/_src/imaginaire/utils/validator.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/validator.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/validator.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/validator.py diff --git a/cosmos-inference/_src/imaginaire/utils/validator_params.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/validator_params.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/validator_params.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/validator_params.py diff --git a/cosmos-inference/_src/imaginaire/utils/wandb_util.py b/cosmos-inference/cosmos3/_src/imaginaire/utils/wandb_util.py similarity index 100% rename from cosmos-inference/_src/imaginaire/utils/wandb_util.py rename to cosmos-inference/cosmos3/_src/imaginaire/utils/wandb_util.py diff --git a/cosmos-inference/_src/imaginaire/visualize/__init__.py b/cosmos-inference/cosmos3/_src/imaginaire/visualize/__init__.py similarity index 100% rename from cosmos-inference/_src/imaginaire/visualize/__init__.py rename to cosmos-inference/cosmos3/_src/imaginaire/visualize/__init__.py diff --git a/cosmos-inference/_src/imaginaire/visualize/img.py b/cosmos-inference/cosmos3/_src/imaginaire/visualize/img.py similarity index 100% rename from cosmos-inference/_src/imaginaire/visualize/img.py rename to cosmos-inference/cosmos3/_src/imaginaire/visualize/img.py diff --git a/cosmos-inference/_src/imaginaire/visualize/video.py b/cosmos-inference/cosmos3/_src/imaginaire/visualize/video.py similarity index 100% rename from cosmos-inference/_src/imaginaire/visualize/video.py rename to cosmos-inference/cosmos3/_src/imaginaire/visualize/video.py diff --git a/cosmos-inference/_src/vfm/__init__.py b/cosmos-inference/cosmos3/_src/vfm/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/__init__.py diff --git a/cosmos-inference/_src/vfm/callbacks/__init__.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/__init__.py diff --git a/cosmos-inference/_src/vfm/callbacks/compile_tokenizer.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/compile_tokenizer.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/compile_tokenizer.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/compile_tokenizer.py diff --git a/cosmos-inference/_src/vfm/callbacks/dataloader_state.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/dataloader_state.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/dataloader_state.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/dataloader_state.py diff --git a/cosmos-inference/_src/vfm/callbacks/dataloading_monitor.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/dataloading_monitor.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/dataloading_monitor.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/dataloading_monitor.py diff --git a/cosmos-inference/_src/vfm/callbacks/device_monitor.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/device_monitor.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/device_monitor.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/device_monitor.py diff --git a/cosmos-inference/_src/vfm/callbacks/every_n_draw_audio_video_sample.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/every_n_draw_audio_video_sample.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_draw_audio_video_sample.py diff --git a/cosmos-inference/_src/vfm/callbacks/every_n_draw_sample.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/every_n_draw_sample.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_draw_sample.py diff --git a/cosmos-inference/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/every_n_dur_fps_draw_sample.py diff --git a/cosmos-inference/_src/vfm/callbacks/expert_heatmap.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/expert_heatmap.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/expert_heatmap.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/expert_heatmap.py diff --git a/cosmos-inference/_src/vfm/callbacks/generation.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/generation.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/generation.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/generation.py diff --git a/cosmos-inference/_src/vfm/callbacks/grad_clip.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/grad_clip.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/grad_clip.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/grad_clip.py diff --git a/cosmos-inference/_src/vfm/callbacks/heart_beat.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/heart_beat.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/heart_beat.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/heart_beat.py diff --git a/cosmos-inference/_src/vfm/callbacks/hf_export.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/hf_export.py similarity index 98% rename from cosmos-inference/_src/vfm/callbacks/hf_export.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/hf_export.py index 96ff278b..494d7033 100644 --- a/cosmos-inference/_src/vfm/callbacks/hf_export.py +++ b/cosmos-inference/cosmos3/_src/vfm/callbacks/hf_export.py @@ -55,7 +55,7 @@ AutoTokenizer = None GenerationConfig = None -# Map string dtype names (as stored in TrainConfig.param_dtype) to torch dtypes. +# Map string dtype names (as stored in ParallelismConfig.precision) to torch dtypes. _DTYPE_MAP: dict[str, torch.dtype] = { "float32": torch.float32, "float16": torch.float16, @@ -112,8 +112,8 @@ class HFExportCallback(Callback): Args: dtype: Export weight dtype (e.g. ``"bfloat16"``). Use - ``"${model.config.train.param_dtype}"`` in the Hydra callback config to inherit from - the training precision. + ``"${model.config.policy.parallelism.precision}"`` in the Hydra callback config to + inherit from the training precision. """ # HuggingFace convention: max 4 GB per shard file. diff --git a/cosmos-inference/_src/vfm/callbacks/iter_speed.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/iter_speed.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/iter_speed.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/iter_speed.py diff --git a/cosmos-inference/_src/vfm/callbacks/load_pretrained.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/load_pretrained.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/load_pretrained.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/load_pretrained.py diff --git a/cosmos-inference/_src/vfm/callbacks/low_precision.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/low_precision.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/low_precision.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/low_precision.py diff --git a/cosmos-inference/_src/vfm/callbacks/mfu.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/mfu.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/mfu.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/mfu.py diff --git a/cosmos-inference/_src/vfm/callbacks/moe_specialization_callback.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/moe_specialization_callback.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/moe_specialization_callback.py diff --git a/cosmos-inference/_src/vfm/callbacks/moe_stability_callback.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/moe_stability_callback.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/moe_stability_callback.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/moe_stability_callback.py diff --git a/cosmos-inference/_src/vfm/callbacks/norm_monitor.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/norm_monitor.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/norm_monitor.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/norm_monitor.py diff --git a/cosmos-inference/_src/vfm/callbacks/ofu.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/ofu.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/ofu.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/ofu.py diff --git a/cosmos-inference/_src/vfm/callbacks/param_count.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/param_count.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/param_count.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/param_count.py diff --git a/cosmos-inference/cosmos3/_src/vfm/callbacks/per_stream_timing.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/per_stream_timing.py new file mode 100644 index 00000000..e264e32e --- /dev/null +++ b/cosmos-inference/cosmos3/_src/vfm/callbacks/per_stream_timing.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Per-stream timing callback. + +Logs ``forward``, ``backward`` and ``optimizer_step`` wall-clock time broken +down by the data stream that produced each iteration's batch +(``data_batch["dataset_name"]``). + +Useful for verifying load-balance hypotheses such as "action_data_slow drives +the long ``optimizer_step`` time observed at large node counts". Because +:class:`IterativeJointDataLoader` synchronises stream selection across all +ranks via ``seed + global_id``, every rank processes the same stream at the +same iteration, so logging on rank 0 is representative of the global cost. +""" + +from __future__ import annotations + +from collections import defaultdict + +import torch +import wandb + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import distributed +from cosmos3._src.imaginaire.utils.callback import Callback + +_TIMER_KEYS: tuple[str, ...] = ("forward", "backward", "optimizer_step", "dataloader_train") + + +class PerStreamTiming(Callback): + """Aggregate ``training_timer`` results by data stream and log to wandb. + + Args: + log_freq: Number of iterations between wandb logs. Each log emits + the per-stream mean time for every key in :data:`_TIMER_KEYS` and + the per-stream iteration count, then resets the accumulators. + """ + + def __init__(self, log_freq: int = 100) -> None: + super().__init__() + self.log_freq = log_freq + self._sums: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float)) + self._counts: dict[str, int] = defaultdict(int) + + @staticmethod + def _extract_stream_name(data_batch: dict) -> str | None: + """Return the ``dataset_name`` carried by the packed batch. + + ``IterativeJointDataLoader`` attaches a per-sample ``dataset_name`` + and the collation produces a list of identical names for one stream. + """ + ds = data_batch.get("dataset_name") + if ds is None: + return None + if isinstance(ds, str): + return ds + if isinstance(ds, (list, tuple)) and ds: + first = ds[0] + return first if isinstance(first, str) else None + return None + + @torch.no_grad() + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + del model, output_batch, loss + stream = self._extract_stream_name(data_batch) + if stream is None: + return + + timer_results = self.trainer.training_timer.results + for key in _TIMER_KEYS: + values = timer_results.get(key) + if not values: + continue + self._sums[stream][key] += float(values[-1]) + self._counts[stream] += 1 + + if iteration % self.log_freq != 0 or iteration == 0: + return + if not distributed.is_rank0() or wandb.run is None: + self._reset() + return + + log_dict: dict[str, float] = {} + for stream_name, key_sums in self._sums.items(): + n = self._counts[stream_name] + if n == 0: + continue + log_dict[f"per_stream_iters/{stream_name}"] = float(n) + for key, total in key_sums.items(): + log_dict[f"per_stream_timer/{stream_name}/{key}"] = total / n + + wandb.log(log_dict, step=iteration) + self._reset() + + def _reset(self) -> None: + self._sums = defaultdict(lambda: defaultdict(float)) + self._counts = defaultdict(int) diff --git a/cosmos-inference/_src/vfm/callbacks/sequence_packing_padding.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/sequence_packing_padding.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/sequence_packing_padding.py diff --git a/cosmos-inference/_src/vfm/callbacks/sigma_loss_analysis.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/sigma_loss_analysis.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/sigma_loss_analysis.py diff --git a/cosmos-inference/_src/vfm/callbacks/skip_nan_step.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/skip_nan_step.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/skip_nan_step.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/skip_nan_step.py diff --git a/cosmos-inference/cosmos3/_src/vfm/callbacks/termination_signal_checkpoint.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/termination_signal_checkpoint.py new file mode 100644 index 00000000..cb3b4ce7 --- /dev/null +++ b/cosmos-inference/cosmos3/_src/vfm/callbacks/termination_signal_checkpoint.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Callback that saves an emergency checkpoint before a Slurm job is killed. + +Slurm signal timeline +--------------------- + +**Timeout** (job hits ``--time`` limit):: + + T SIGUSR1 → batch shell (N is 300 via ``--signal=B:SIGUSR1@300``) + T+N sec SIGTERM → all processes + T+N+KillWait SIGKILL → anything still alive (KillWait is 30s) + +**Preemption** (higher-priority job needs the nodes):: + + T SIGUSR1 → batch shell + T+Grace SIGTERM + SIGKILL (GraceTime is 300s in GCP-IAD) + +**User cancel** (``scancel ``):: + + T SIGTERM → all processes (no SIGUSR1) + T+KillWait SIGKILL → anything still alive (KillWait is 30s) + +Implementation +-------------- + +* **How the SIGUSR1 signal is handled:** + + - The Slurm batch script responds to SIGUSR1 by creating a sentinel file + (``$SLURM_LOG_DIR/SIGUSR1_RECEIVED``) on the shared filesystem. + - This callback polls for the presence of this sentinel file at the end of + each training step. + - When detected, it triggers an emergency checkpoint save. + +* **Why the Python process can't receive SIGUSR1 directly:** + + - Pyxis/Enroot containers do not receive SIGUSR1 signals from Slurm (the + signal is sent to the batch shell, not the container). + - Attempted to forward SIGUSR1 with both ``srun`` and + ``scancel --signal`` in batch scripts, proven not working. + +* **Why no SIGTERM handler is needed:** + + - The SIGUSR1 signal is the trigger of preemption and timeout, it + is already able to distinguish them from user cancel. + - Before SIGTERM arrives there are at least 300 s (``GraceTime=300`` for + preemption, ``--signal=B:SIGUSR1@N`` for timeout), which is sufficent + for the poll to detect the sentinel and save a checkpoint. + - SIGTERM and the subsequent SIGKILL are left to terminate the process + naturally after the checkpoint has been saved. + - SIGTERM and SIGUSR1 are logged so we can observe signal delivery into + Pyxis/Enroot containers for debugging purposes. + +To avoid redundant checkpoints, a save is only performed if at least +``save_iter * min_save_fraction`` iterations have elapsed since the last +checkpoint. +""" + +from __future__ import annotations + +import os +import signal +import sys + +import torch + +from cosmos3._src.imaginaire.model import ImaginaireModel +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.imaginaire.utils.callback import Callback + + +class TerminationSignalCheckpoint(Callback): + """Save a checkpoint in response to SIGUSR1 (preemption or timeout). + + Args: + min_save_fraction: Fraction of the regular checkpoint interval (between 0 + and 1) that must have elapsed since the last checkpoint before an + emergency save is allowed. Defaults to 1/3. + """ + + def __init__(self, min_save_fraction: float = 1 / 3): + super().__init__() + self._min_save_fraction = min_save_fraction + self._current_iteration: int = 0 + self._last_checkpoint_iteration: int = 0 + # Captured from on_before_optimizer_step so we can call checkpointer.save(). + self._optimizer: torch.optim.Optimizer | None = None + self._scheduler: torch.optim.lr_scheduler.LRScheduler | None = None + self._grad_scaler: torch.amp.GradScaler | None = None + # Sentinel file created by the batch-shell trap when SIGUSR1 arrives. + # This is the sole detection mechanism because srun/Pyxis does not + # relay SIGUSR1 into the container. + slurm_log_dir = os.environ.get("SLURM_LOG_DIR", "") + self._sigusr1_sentinel = os.path.join(slurm_log_dir, "SIGUSR1_RECEIVED") if slurm_log_dir else "" + + # ------------------------------------------------------------------ + # Lifecycle hooks + # ------------------------------------------------------------------ + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self._current_iteration = iteration + self._last_checkpoint_iteration = iteration + self._install_termination_signal_handlers() + + def on_before_optimizer_step( + self, + model_ddp, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + self._optimizer = optimizer + self._scheduler = scheduler + self._grad_scaler = grad_scaler + + def on_save_checkpoint_success(self, iteration: int = 0, elapsed_time: float = 0) -> None: + self._last_checkpoint_iteration = iteration + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self._current_iteration = iteration + + if not self._sigusr1_sentinel or not os.path.exists(self._sigusr1_sentinel): + return + + log.info("[TerminationSignalCheckpoint] Detected SIGUSR1 sentinel file. Will save checkpoint.") + + # Check if the minimum progress has been reached since the last checkpoint. + min_progress = int(self.config.checkpoint.save_iter * self._min_save_fraction) + if (iteration - self._last_checkpoint_iteration) < min_progress: + log.info( + f"[TerminationSignalCheckpoint] Only {iteration - self._last_checkpoint_iteration} iterations " + f"since last checkpoint (threshold {min_progress}). Skipping checkpoint save." + ) + sys.exit(0) + + assert self._optimizer is not None, ( + "[TerminationSignalCheckpoint] Optimizer reference not set — on_before_optimizer_step was never called" + ) + + log.info(f"[TerminationSignalCheckpoint] Saving checkpoint at iteration {iteration}.") + self.trainer.checkpointer.save(model, self._optimizer, self._scheduler, self._grad_scaler, iteration=iteration) + # Async DCP checkpointing queues the write to a background process. + # We must wait for it to finish before exiting. + self.trainer.checkpointer.finalize() + log.info(f"[TerminationSignalCheckpoint] Checkpoint saved at iteration {iteration}.") + sys.exit(0) + + # ------------------------------------------------------------------ + # Termination signal handlers + # ------------------------------------------------------------------ + + def _install_termination_signal_handlers(self) -> None: + signal.signal(signal.SIGTERM, self._log_sigterm) + log.info("[TerminationSignalCheckpoint] Installed SIGTERM handler.") + + def _log_sigterm(self, signum: int, frame: object) -> None: + log.info(f"[TerminationSignalCheckpoint] Received SIGTERM at iteration {self._current_iteration}.") diff --git a/cosmos-inference/_src/vfm/callbacks/training_stats.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/training_stats.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/training_stats.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/training_stats.py diff --git a/cosmos-inference/_src/vfm/callbacks/vlm/grad_clip.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/vlm/grad_clip.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/vlm/grad_clip.py diff --git a/cosmos-inference/_src/vfm/callbacks/wandb_log.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/wandb_log.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/wandb_log.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/wandb_log.py diff --git a/cosmos-inference/_src/vfm/callbacks/wandb_log_eval.py b/cosmos-inference/cosmos3/_src/vfm/callbacks/wandb_log_eval.py similarity index 100% rename from cosmos-inference/_src/vfm/callbacks/wandb_log_eval.py rename to cosmos-inference/cosmos3/_src/vfm/callbacks/wandb_log_eval.py diff --git a/cosmos-inference/_src/vfm/checkpointer/__init__.py b/cosmos-inference/cosmos3/_src/vfm/checkpointer/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/checkpointer/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/checkpointer/__init__.py diff --git a/cosmos-inference/_src/vfm/checkpointer/dcp.py b/cosmos-inference/cosmos3/_src/vfm/checkpointer/dcp.py similarity index 100% rename from cosmos-inference/_src/vfm/checkpointer/dcp.py rename to cosmos-inference/cosmos3/_src/vfm/checkpointer/dcp.py diff --git a/cosmos-inference/_src/vfm/conditioner.py b/cosmos-inference/cosmos3/_src/vfm/conditioner.py similarity index 100% rename from cosmos-inference/_src/vfm/conditioner.py rename to cosmos-inference/cosmos3/_src/vfm/conditioner.py diff --git a/cosmos-inference/_src/vfm/configs/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/config.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/config.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/config.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/config.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/callbacks.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/callbacks.py similarity index 96% rename from cosmos-inference/_src/vfm/configs/base/defaults/callbacks.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/callbacks.py index 74568cd3..f84d48dc 100644 --- a/cosmos-inference/_src/vfm/configs/base/defaults/callbacks.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/callbacks.py @@ -40,6 +40,7 @@ from cosmos3._src.vfm.callbacks.sequence_packing_padding import SequencePackingPadding from cosmos3._src.vfm.callbacks.sigma_loss_analysis import SigmaLossAnalysis from cosmos3._src.vfm.callbacks.skip_nan_step import SkipNaNStep +from cosmos3._src.vfm.callbacks.termination_signal_checkpoint import TerminationSignalCheckpoint from cosmos3._src.vfm.callbacks.training_stats import TrainingStatsCallback from cosmos3._src.vfm.callbacks.wandb_log import WandbCallback as WandBCallbackMultiplier from cosmos3._src.vfm.callbacks.wandb_log_eval import WandbCallback as WandBCallbackEval @@ -100,6 +101,9 @@ save_s3="${upload_reproducible_setup}", upload_every_n_mul=5, ), + termination_signal_checkpoint=L(TerminationSignalCheckpoint)( + min_save_fraction=1 / 3, + ), ) OPTIMIZATION_CALLBACKS = dict( diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/checkpointer.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/checkpointer.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/checkpointer.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/cluster.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/cluster.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/cluster.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/cluster.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/conditioner.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/conditioner.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/conditioner.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/conditioner.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/ema.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/ema.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/ema.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/ema.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/model.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/model.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/model.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/model.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/model_config.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/model_config.py similarity index 88% rename from cosmos-inference/_src/vfm/configs/base/defaults/model_config.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/model_config.py index 585512c9..a3940df5 100644 --- a/cosmos-inference/_src/vfm/configs/base/defaults/model_config.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/model_config.py @@ -21,6 +21,7 @@ from cosmos3._src.imaginaire.config import Config from cosmos3._src.imaginaire.lazy_config import LazyDict from cosmos3._src.vfm.configs.base.defaults.ema import EMAConfig +from cosmos3._src.vfm.configs.base.defaults.parallelism import ParallelismConfig from cosmos3._src.vfm.configs.base.defaults.vlm import VLMConfig from cosmos3._src.vfm.configs.base.vlm.defaults.training import PolicyConfig, TrainConfig @@ -78,57 +79,6 @@ class DiffusionExpertConfig: unified_3d_mrope_temporal_modality_margin: int = 0 -@attrs.define(slots=False) -class ParallelismConfig: - # Activation checkpointing is used to reduce the memory usage of the model. - # The outputs of each layer are checkpointed, the intermediate results are not saved. - use_activation_checkpointing: bool = False - - # Torch compile is used to compile the model for faster training. - use_torch_compile: bool = False - - # Whether to use CUDA graphs for faster inference. This option does not work during training. - use_cuda_graphs: bool = False - - # Whether the entire Cosmos3 VFM network is compiled, or only a specific region is compiled. - # Use "language" to compile only individual layers in the MOT model. - # Use "all" to compile the the MOT model, as well as encode/decode functions. - compiled_region: Literal["all", "language"] = "language" - - # Whether torch.compile should generate symbolic-shape (dynamic) kernels - # (maps to ``torch.compile(dynamic=...)``). Defaults to True for training, - # which sees varying shapes across batches (sequence length, CP sharding, ...); - # specializing would recompile continuously. See ParallelismOverrides in - # packages/cosmos3/cosmos3/common/args.py for the inference-side rationale - # (where dynamic=False is preferred for stable AR shapes). - compile_dynamic: bool = True - - # Enable autotuning for pointwise/reduction Triton kernels (e.g. RMSNorm). - # Explores 6 candidate configs instead of the default 1, improving kernel performance - # at the cost of longer first-iteration compilation time. - max_autotune_pointwise: bool = False - - # Enable coordinate descent tuning after autotuning. Starts from the best autotuned - # config and explores nearby configs by adjusting one parameter at a time. - # Requires max_autotune_pointwise=True to have effect on reduction kernels. - coordinate_descent_tuning: bool = False - - # Whether to enable inference mode. - enable_inference_mode: bool = False - - # Number of ranks for sharding the model weights. - data_parallel_shard_degree: int = 1 - - # Number of ranks for context parallelism. - context_parallel_shard_degree: int = 1 - - # Number of ranks for CFG parallelism. - cfg_parallel_shard_degree: int = 1 - - # Precision for the model. - precision: str = "bfloat16" - - @attrs.define(slots=False) class LBLConfig: # For load balancing loss computation. diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/multiview_dataloader.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/multiview_dataloader.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/multiview_dataloader.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/optimizer.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/optimizer.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/optimizer.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/optimizer.py diff --git a/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/parallelism.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/parallelism.py new file mode 100644 index 00000000..eeaa90cb --- /dev/null +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/parallelism.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared user-facing parallelism schema for VFM and VLM. + +Both project trees (vfm/, vfm/configs/base/vlm/) instantiate the same +ParallelDims runtime at vfm/utils/parallelism.py. They now also share this +single user-facing config schema. Trainer-side translation from the long +descriptive field names here to the short ParallelDims constructor kwargs +happens at the read site (see vfm/models/{omni_mot_model,vlm_model}.py). +""" + +from typing import Literal + +import attrs + + +@attrs.define(slots=False) +class ParallelismConfig: + # Activation checkpointing is used to reduce the memory usage of the model. + # The outputs of each layer are checkpointed, the intermediate results are not saved. + use_activation_checkpointing: bool = False + + # Torch compile is used to compile the model for faster training. + use_torch_compile: bool = False + + # Whether to use CUDA graphs for faster inference. This option does not work during training. + use_cuda_graphs: bool = False + + # Whether the entire Cosmos3 VFM network is compiled, or only a specific region is compiled. + # Use "language" to compile only individual layers in the MOT model. + # Use "all" to compile the the MOT model, as well as encode/decode functions. + compiled_region: Literal["all", "language"] = "language" + + # Whether torch.compile should generate symbolic-shape (dynamic) kernels + # (maps to ``torch.compile(dynamic=...)``). Defaults to True for training, + # which sees varying shapes across batches (sequence length, CP sharding, ...); + # specializing would recompile continuously. See ParallelismOverrides in + # packages/cosmos3/cosmos3/common/args.py for the inference-side rationale + # (where dynamic=False is preferred for stable AR shapes). + compile_dynamic: bool = True + + # Enable autotuning for pointwise/reduction Triton kernels (e.g. RMSNorm). + # Explores 6 candidate configs instead of the default 1, improving kernel performance + # at the cost of longer first-iteration compilation time. + max_autotune_pointwise: bool = False + + # Enable coordinate descent tuning after autotuning. Starts from the best autotuned + # config and explores nearby configs by adjusting one parameter at a time. + # Requires max_autotune_pointwise=True to have effect on reduction kernels. + coordinate_descent_tuning: bool = False + + # Whether to enable inference mode. + enable_inference_mode: bool = False + + # Number of ranks for sharding the model weights (FSDP). The default -1 + # auto-infers to world_size at runtime via ParallelDims. + data_parallel_shard_degree: int = -1 + + # Number of ranks for replicating the model weights (HSDP outer dim). + # data_parallel_replicate_degree x data_parallel_shard_degree must divide + # world_size when both are explicitly set. + data_parallel_replicate_degree: int = 1 + + # Number of ranks for context parallelism. + context_parallel_shard_degree: int = 1 + + # Number of ranks for CFG parallelism. + cfg_parallel_shard_degree: int = 1 + + # Precision for the model. + precision: str = "bfloat16" diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/tokenizer.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/tokenizer.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/tokenizer.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/unittest.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/unittest.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/unittest.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/unittest.py diff --git a/cosmos-inference/_src/vfm/configs/base/defaults/vlm.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/vlm.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/defaults/vlm.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/defaults/vlm.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/config.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/config.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/config.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/config.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/callbacks.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py similarity index 95% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/callbacks.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py index 7fe35e15..15992c48 100644 --- a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/callbacks.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/callbacks.py @@ -26,12 +26,12 @@ from cosmos3._src.vfm.callbacks.dataloader_state import DataLoaderStateCallback from cosmos3._src.vfm.callbacks.dataloading_monitor import DetailedDataLoadingSpeedMonitor from cosmos3._src.vfm.callbacks.hf_export import HFExportCallback +from cosmos3._src.vfm.callbacks.low_precision import LowPrecisionCallback from cosmos3._src.vfm.callbacks.vlm.grad_clip import GradClip from cosmos3._src.vfm.configs.base.defaults.callbacks import JOB_MONITOR_CALLBACKS from projects.cosmos3.vlm.callbacks.data_stats import DataStatsCallback from projects.cosmos3.vlm.callbacks.iter_speed import IterSpeed from projects.cosmos3.vlm.callbacks.log_tensor_shape import LogTensorShapeCallback -from projects.cosmos3.vlm.callbacks.low_precision import LowPrecisionCallback from projects.cosmos3.vlm.callbacks.param_count import ParamCount from projects.cosmos3.vlm.callbacks.wandb_log import WandbCallback as WandBCallbackMultiplier from projects.cosmos3.vlm.callbacks.wandb_log_eval import WandbCallback as WandBCallbackEval @@ -64,8 +64,7 @@ def register_callbacks(): update_iter=1, config=PLACEHOLDER, trainer=PLACEHOLDER, - param_torch_dtype="${model.config.policy.param_torch_dtype}", - ), # use model + ), # reads model.precision; no extra kwarg needed # nvtx=L(NVTXCallback)(synchronize=True), ) @@ -120,7 +119,7 @@ def register_callbacks(): HF_EXPORT_CALLBACKS = dict( hf_export=L(HFExportCallback)( - dtype="${model.config.train.param_dtype}", + dtype="${model.config.policy.parallelism.precision}", ), ) cs.store(group="callbacks", package="trainer.callbacks", name="hf_export", node=HF_EXPORT_CALLBACKS) diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/checkpointer.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/checkpointer.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/checkpointer.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/config.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/config.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/config.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/dataloader_weighted_url.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/model.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py similarity index 87% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/model.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py index 2fae36e5..47b1d1b4 100644 --- a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/model.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/model.py @@ -17,7 +17,6 @@ from cosmos3._src.imaginaire.lazy_config import LazyCall as L from cosmos3._src.vfm.configs.base.defaults.model_config import VLMModelConfig -from cosmos3._src.vfm.configs.base.vlm.defaults.training import TrainConfig from cosmos3._src.vfm.models.vlm_model import VLMModel VLM_FSDP_CONFIG = dict( @@ -25,9 +24,7 @@ distributed_parallelism="fsdp", ), model=L(VLMModel)( - config=VLMModelConfig( - train=TrainConfig(param_dtype="bfloat16"), - ), + config=VLMModelConfig(), checkpoint="${checkpoint}", ), ) diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/optimizer.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/optimizer.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/optimizer.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/training.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py similarity index 68% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/training.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py index 7d613bad..9946aff6 100644 --- a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/training.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/training.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os from dataclasses import MISSING, field from typing import Union @@ -21,6 +20,7 @@ import torch from cosmos3._src.imaginaire.config import make_freezable +from cosmos3._src.vfm.configs.base.defaults.parallelism import ParallelismConfig def skip_ui_field(*, default=MISSING, default_factory=MISSING, **kwargs): @@ -50,13 +50,8 @@ class FP8: @make_freezable @attrs.define(slots=False) class TrainConfig: - # Whether to use async tensor parallelism - async_tp_enabled: bool = False - # Whether to use torch.compile - compile: bool = False - - # The data type for parameters and activations - param_dtype: str = "bfloat16" + # Master parameter dtype for FSDP. Activation / model dtype lives on the + # shared ParallelismConfig as policy.parallelism.precision. master_dtype: str = "float32" # The data type for reduction in FSDP @@ -79,22 +74,9 @@ class TrainConfig: fp8: FP8 = FP8() deterministic: bool = True - def __post_init__(self): - self.ckpt.__post_init__() - if self.async_tp_enabled and not self.compile: - raise ValueError("Async tensor parallelism requires torch.compile to be enabled") - def key_values(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} - @property - def param_torch_dtype(self): - return { - "bfloat16": torch.bfloat16, - "float16": torch.float16, - "float32": torch.float32, - }[self.param_dtype] - @property def master_torch_dtype(self): return { @@ -108,41 +90,6 @@ def fsdp_reduce_torch_dtype(self): return {"float32": torch.float32}[self.fsdp_reduce_dtype] -@make_freezable -@attrs.define(slots=False) -class ParallelismConfig: - # Number of initial replicas to be created - n_init_replicas: int = 1 - # Tensor parallelism size - tp_size: int = 1 - # Context parallelism size - cp_size: int = 1 - # Expert parallelism size - ep_size: int = 1 - # Data Parallelism size in sharded mode - dp_shard_size: int = -1 - # Pipeline parallelism size - pp_size: int = 1 - # Pipeline parallelism dynamic shape - pp_dynamic_shape: bool = False - # Pipeline parallelism micro batch size - pp_micro_batch_size: int = 1 - # Data Parallelism size in replicate mode - dp_replicate_size: int = 1 - # The method to rotate kv shards during context parallelism - cp_rotate_method: str = "allgather" - - @property - def world_size(self): - world_size = os.environ.get("WORLD_SIZE", 1) - return int(world_size) - - @property - def local_world_size(self): - local_world_size = os.environ.get("LOCAL_WORLD_SIZE", 1) - return int(local_world_size) - - # Why we does not make this freezable? # Because we need to path the cache model dir as model_name_or_path to the cosmos-rl model to use the # model weights downloaded from s3. If cosmos-rl support reading model from s3 directly, we can make it freezable. @@ -154,11 +101,8 @@ class PolicyConfig: model_name_or_path: str = "Qwen/Qwen2.5-VL-3B-Instruct" # The maximum length for training, longer than this will be ignored for training stability model_max_length: int = 16000 - # Whether to use gradient checkpointing - model_gradient_checkpointing: bool = True # The maximum length for video tokens, only applied to qwen model qwen_max_video_token_length: int = 8000 - param_torch_dtype: str = "bfloat16" # Pretrain weights (Optional) pretrain_weights_path_vlm: str = "" diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/defaults/vlm_policy.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/defaults/vlm_policy.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/defaults/vlm_policy.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/__init__.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/experiment/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/__init__.py diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py similarity index 95% rename from cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py index b906615b..f42a25d6 100644 --- a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py @@ -31,7 +31,7 @@ # --------------------------------------------------------------------------- # Smoke test: 4-GPU FSDP2 with qwen3_vl_2b, debug data, 10 iterations. # Exercises: HFModel meta-init → parallelize() → forward → CE loss → backward -# Requires: trainable_params (Phase 2 mandatory), dp_shard_size=4 +# Requires: trainable_params (Phase 2 mandatory), data_parallel_shard_degree=4 # --------------------------------------------------------------------------- pre_exp012_000_phase2_vlm_smoke_4gpu_8b = LazyDict( dict( @@ -65,8 +65,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=4, - dp_replicate_size=-1, + data_parallel_shard_degree=4, + data_parallel_replicate_degree=-1, ), ), ), diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py similarity index 95% rename from cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py index b49627f8..57f012d8 100644 --- a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py +++ b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/pre_exp01x.py @@ -151,8 +151,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -200,8 +200,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -251,8 +251,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -299,8 +299,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -465,8 +465,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -516,8 +516,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -580,8 +580,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -630,8 +630,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), ), ), @@ -690,8 +690,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), monkey_patch_for_text_only_data=True, pretrain_weights_path_llm="Qwen/Qwen3-8B", @@ -758,8 +758,8 @@ config=dict( policy=dict( parallelism=dict( - dp_shard_size=8, - dp_replicate_size=-1, + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, ), monkey_patch_for_text_only_data=True, ), @@ -808,10 +808,8 @@ config=dict( policy=dict( parallelism=dict( - tp_size=1, - ep_size=2, - dp_shard_size=2, - dp_replicate_size=1, + data_parallel_shard_degree=2, + data_parallel_replicate_degree=1, ), ), ), diff --git a/cosmos-inference/_src/vfm/configs/base/vlm/experiment/utils.py b/cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py similarity index 100% rename from cosmos-inference/_src/vfm/configs/base/vlm/experiment/utils.py rename to cosmos-inference/cosmos3/_src/vfm/configs/base/vlm/experiment/utils.py diff --git a/cosmos-inference/_src/vfm/datasets/__init__.py b/cosmos-inference/cosmos3/_src/vfm/datasets/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/__init__.py diff --git a/cosmos-inference/_src/vfm/datasets/action/action_normalization.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/action_normalization.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/action_normalization.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/action_normalization.py diff --git a/cosmos-inference/_src/vfm/datasets/action/action_spec.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/action_spec.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/action_spec.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/action_spec.py diff --git a/cosmos-inference/_src/vfm/datasets/action/dataloaders.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/dataloaders.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/dataloaders.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/dataloaders.py diff --git a/cosmos-inference/_src/vfm/datasets/action/domain_utils.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/domain_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/domain_utils.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/domain_utils.py diff --git a/cosmos-inference/_src/vfm/datasets/action/json_formatter.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/json_formatter.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/json_formatter.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/json_formatter.py diff --git a/cosmos-inference/_src/vfm/datasets/action/libero_dataset.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/libero_dataset.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/libero_dataset.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/libero_dataset.py diff --git a/cosmos-inference/_src/vfm/datasets/action/libero_pose_utils.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/libero_pose_utils.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/libero_pose_utils.py diff --git a/cosmos-inference/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json b/cosmos-inference/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/normalizers/libero_native_frame_wise_relative_rot6d.json diff --git a/cosmos-inference/_src/vfm/datasets/action/pose_utils.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/pose_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/pose_utils.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/pose_utils.py diff --git a/cosmos-inference/_src/vfm/datasets/action/transforms.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/transforms.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/transforms.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/transforms.py diff --git a/cosmos-inference/_src/vfm/datasets/action/unified_dataset.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/unified_dataset.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/unified_dataset.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/unified_dataset.py diff --git a/cosmos-inference/_src/vfm/datasets/action/viewpoint_utils.py b/cosmos-inference/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/action/viewpoint_utils.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/action/viewpoint_utils.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/__init__.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/__init__.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/append_fps_frames_for_image.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/audio_caption.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/audio_caption.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/audio_caption.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/audio_parsing.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/audio_parsing.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/audio_parsing.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/caption_filter.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/caption_filter.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/caption_filter.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/cropping.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/cropping.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/cropping.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/cropping.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/duration_fps_text_timestamps.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/idle_frames_text_info.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/idle_frames_text_info.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/idle_frames_text_info.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/image_editing_transform.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/image_editing_transform.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/image_editing_transform.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/image_resolution_filter.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/image_resolution_filter.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/image_resolution_filter.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/interleaved_image_transform.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/interleaved_image_transform.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_image_transform.py diff --git a/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py new file mode 100644 index 00000000..44d10892 --- /dev/null +++ b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/interleaved_video_parsing.py @@ -0,0 +1,583 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from collections.abc import Callable +from typing import Optional + +import numpy as np +import omegaconf +import torch +from einops import rearrange +from torchcodec.decoders import VideoDecoder +from torchvision.transforms.v2 import Resize, UniformTemporalSubsample + +from cosmos3._src.imaginaire.datasets.webdataset.augmentors.image.misc import obtain_augmentation_size +from cosmos3._src.imaginaire.utils import log +from cosmos3._src.vfm.datasets.augmentors.video_parsing import VideoParsingWithFullFrames + +# Local copies of the torchcodec decoder helpers so this module does not depend on +# private symbols of ``video_parsing.py``. Behavior matches the originals. +_PostDecodeTransforms = list[Callable[[torch.Tensor], torch.Tensor]] | None +_SUPPORTS_VIDEO_DECODER_TRANSFORMS: bool | None = None +_WARNED_POST_DECODE_TRANSFORMS = False + + +def _create_video_decoder( + video: bytes, + seek_mode: str, + num_ffmpeg_threads: int, + transforms: _PostDecodeTransforms = None, +) -> tuple[VideoDecoder, _PostDecodeTransforms]: + global _SUPPORTS_VIDEO_DECODER_TRANSFORMS, _WARNED_POST_DECODE_TRANSFORMS + + kwargs = {"seek_mode": seek_mode, "num_ffmpeg_threads": num_ffmpeg_threads} + if transforms is None: + return VideoDecoder(video, **kwargs), None + + if _SUPPORTS_VIDEO_DECODER_TRANSFORMS is not False: + try: + decoder = VideoDecoder(video, transforms=transforms, **kwargs) + _SUPPORTS_VIDEO_DECODER_TRANSFORMS = True + return decoder, None + except TypeError as e: + if "transforms" not in str(e): + raise + _SUPPORTS_VIDEO_DECODER_TRANSFORMS = False + + if not _WARNED_POST_DECODE_TRANSFORMS: + log.warning( + "Installed torchcodec does not support VideoDecoder(transforms=...); " + "applying video transforms after frame decode.", + rank0_only=False, + ) + _WARNED_POST_DECODE_TRANSFORMS = True + return VideoDecoder(video, **kwargs), transforms + + +def _apply_post_decode_transforms( + frames: torch.Tensor, transforms: _PostDecodeTransforms +) -> torch.Tensor: # frames: [T,C,H,W], returns: [T,C,H,W] + if transforms is None: + return frames + + for transform in transforms: + frames = transform(frames) # [T,C,H,W] + return frames + + +class VideoTransferAlignedFullFramesParsing(VideoParsingWithFullFrames): + """Decode RGB and precomputed control videos with one shared v3 frame plan. + + This is the variable-length counterpart of the fixed-window transfer parser. + The RGB stream determines the sampled stride and frame indices. Any extra + input video streams, such as depth or segmentation, are decoded with the same + frame indices so the control video stays temporally aligned with the target. + """ + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + assert len(input_keys) >= 2, "VideoTransferAlignedFullFramesParsing requires [metas, video, ...]." + super().__init__(input_keys=input_keys[:2], output_keys=output_keys, args=args) + self.input_keys = input_keys + self.control_video_keys = input_keys[2:] + self.min_stride_key = self.args.get("min_stride_key", "_full_frames_min_stride") + + def _build_rgb_decode_transform(self, data_dict: dict, meta_dict: dict) -> list[Resize] | None: + if not self.perform_resize: + return None + + img_size = obtain_augmentation_size(data_dict, {"size": self.size}) + assert isinstance(img_size, (tuple, omegaconf.listconfig.ListConfig)), ( + f"Arg size in resize should be a tuple, get {type(img_size)}, {img_size}" + ) + img_w, img_h = img_size + orig_w, orig_h = meta_dict["width"], meta_dict["height"] + + scaling_ratio = min((img_w / orig_w), (img_h / orig_h)) + target_size = (int(scaling_ratio * orig_h + 0.5), int(scaling_ratio * orig_w + 0.5)) + assert target_size[0] <= img_h and target_size[1] <= img_w, ( + f"Resize error. orig {(orig_w, orig_h)} desire {img_size} compute {target_size}" + ) + return [Resize(target_size)] + + def _sample_frame_indices(self, decoder_len: int, min_stride_override: int | None = None) -> tuple[list[int], int]: + min_stride = int(min_stride_override) if min_stride_override is not None else self.min_stride + max_stride = max(self.max_stride, min_stride) + stride = self._sample_stride_with_bias(max_stride, min_stride) + frame_indices = np.arange(0, decoder_len, stride).tolist() + max_num_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) + if max_num_frames < 1: + return [], stride + + # Wan VAE temporal compression expects 1 + 4N video frames. + num_video_frames = 1 + 4 * ((max_num_frames - 1) // 4) + return frame_indices[:num_video_frames], stride + + def _probe_video_len(self, video: bytes) -> int: + video_decoder = VideoDecoder( + video, + seek_mode=self.seek_mode, + num_ffmpeg_threads=self.video_decode_num_threads, + ) + try: + return len(video_decoder) + finally: + del video_decoder + + def _decode_frames_at( + self, + video: bytes, + frame_indices: list[int], + transforms: list[Resize] | None = None, + ) -> torch.Tensor: # returns [C,T,H,W] + video_decoder, post_decode_transforms = _create_video_decoder( + video, + self.seek_mode, + self.video_decode_num_threads, + transforms, + ) + try: + frame_batch = video_decoder.get_frames_at(frame_indices) + frames = frame_batch.data # [T,C,H,W] + frames = _apply_post_decode_transforms(frames, post_decode_transforms) # [T,C,H,W] + frames = frames.permute(1, 0, 2, 3) # [C,T,H,W] + finally: + del video_decoder + return frames # [C,T,H,W] + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + rgb_transform = self._build_rgb_decode_transform(data_dict, meta_dict) + control_videos: dict[str, bytes] = {} + try: + decoder_len = self._probe_video_len(video) + + control_decoder_lens = [] + for control_video_key in self.control_video_keys: + control_video = data_dict.get(control_video_key) + if not isinstance(control_video, bytes): + log.warning( + f"VideoTransferAlignedFullFramesParsing: missing bytes for {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + control_videos[control_video_key] = control_video + control_decoder_lens.append(self._probe_video_len(control_video)) + + # Precomputed control streams can be one frame shorter than RGB; sample + # only frames present in every stream to keep all modalities aligned. + aligned_decoder_len = min([decoder_len, *control_decoder_lens]) if control_decoder_lens else decoder_len + + min_stride_override = data_dict.pop(self.min_stride_key, None) + frame_indices, stride = self._sample_frame_indices( + aligned_decoder_len, min_stride_override=min_stride_override + ) + if len(frame_indices) == 0: + log.warning( + f"VideoTransferAlignedFullFramesParsing: no valid frame indices. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + video_frames = self._decode_frames_at(video, frame_indices, rgb_transform) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode RGB video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + base_video_info = { + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "frame_indices": frame_indices, + "num_frames": len(frame_indices), + "fps": meta_dict["framerate"], + "conditioning_fps": meta_dict["framerate"] / stride, + "num_multiplier": stride, + "n_orig_video_frames": decoder_len, + } + data_dict[self.video_key] = { + **base_video_info, + "video": video_frames, # [C,T,H,W] + } + + for control_video_key, control_video in control_videos.items(): + try: + control_frames = self._decode_frames_at(control_video, frame_indices) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + data_dict[control_video_key] = { + **base_video_info, + "video": control_frames, # [C,T,H,W] + } + + return data_dict + + +class VideoTransferAlignedLegacyChunkParsing(VideoTransferAlignedFullFramesParsing): + """Decode legacy caption-window transfer streams with shared RGB/control frame indices.""" + + def __init__(self, input_keys: list, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys=input_keys, output_keys=output_keys, args=args) + self.key_for_caption = self.args["key_for_caption"] + assert self.key_for_caption in [ + "t2w_windows", + "i2w_windows_later_frames", + ], "key_for_caption must be either t2w_windows or i2w_windows_later_frames" + self.min_duration = self.args["min_duration"] + self.num_frames = self.args["num_video_frames"] + self.use_native_fps = self.args["use_native_fps"] + self.use_original_fps = self.args["use_original_fps"] + self.use_dynamic_fps = self.args.get("use_dynamic_fps", False) + self.low_fps_bias = self.args.get("low_fps_bias", 0.5) + assert 0.0 <= self.low_fps_bias <= 1.0, f"low_fps_bias must be in [0, 1], got {self.low_fps_bias}" + mode_count = sum([self.use_dynamic_fps, self.use_native_fps, self.use_original_fps]) + assert mode_count <= 1, ( + f"Only one FPS mode can be enabled at a time. Got: " + f"use_dynamic_fps={self.use_dynamic_fps}, " + f"use_native_fps={self.use_native_fps}, " + f"use_original_fps={self.use_original_fps}" + ) + self.allowed_num_multiplers = self.args.get("allowed_num_multiplers", list(range(1, 100))) + if self.num_frames > 0: + self.sampler = UniformTemporalSubsample(self.num_frames) + + def _sample_legacy_stride_with_bias(self, max_stride: int) -> int: + if max_stride == 1: + return 1 + + strides = np.arange(1, max_stride + 1) + weights = np.linspace(1 - self.low_fps_bias, self.low_fps_bias, max_stride) + weights = np.maximum(weights, 0.01) + probs = weights / weights.sum() + return int(np.random.choice(strides, p=probs)) + + def _decode_all_streams_at( + self, + video: bytes, + control_videos: dict[str, bytes], + frame_indices: list[int], + rgb_transform: list[Resize] | None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + video_frames = self._decode_frames_at(video, frame_indices, rgb_transform) # [C,T,H,W] + control_frames_by_key = { + control_video_key: self._decode_frames_at(control_video, frame_indices) # [C,T,H,W] + for control_video_key, control_video in control_videos.items() + } + return video_frames, control_frames_by_key + + def _subsample_all_streams( + self, video_frames: torch.Tensor, control_frames_by_key: dict[str, torch.Tensor] + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + video_frames = rearrange( + self.sampler(rearrange(video_frames, "c t h w -> t c h w")), "t c h w -> c t h w" + ) # [C,T,H,W] + control_frames_by_key = { + key: rearrange(self.sampler(rearrange(frames, "c t h w -> t c h w")), "t c h w -> c t h w") + for key, frames in control_frames_by_key.items() + } # [C,T,H,W] + return video_frames, control_frames_by_key + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + control_videos: dict[str, bytes] = {} + control_decoder_lens: list[int] = [] + try: + decoder_len = self._probe_video_len(video) + for control_video_key in self.control_video_keys: + control_video = data_dict.get(control_video_key) + if not isinstance(control_video, bytes): + log.warning( + f"VideoTransferAlignedLegacyChunkParsing: missing bytes for {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + control_videos[control_video_key] = control_video + control_decoder_lens.append(self._probe_video_len(control_video)) + except Exception as e: + log.warning( + f"Failed to probe video streams. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + aligned_decoder_len = min([decoder_len, *control_decoder_lens]) if control_decoder_lens else decoder_len + options: list = list((i, item) for i, item in enumerate(meta_dict[self.key_for_caption])) + if len(options) > 1: + options = options[:-1] + random.shuffle(options) + + rgb_transform = self._build_rgb_decode_transform(data_dict, meta_dict) + video_frames = None + control_frames_by_key: dict[str, torch.Tensor] = {} + dynamic_conditioning_fps = None + num_multiplier: float | int = 1 + frame_indices: list[int] = [] + chunk_index = 0 + start_frame = 0 + end_frame = 0 + + for chunk_index, option in options: + start_frame = int(option["start_frame"]) + end_frame = min(int(option["end_frame"]), aligned_decoder_len) + if (end_frame - start_frame) < self.min_duration * meta_dict["framerate"]: + continue + if self.use_native_fps or self.use_original_fps or self.use_dynamic_fps: + if "alpamayo" in data_dict["__url__"].root: + start_frame += 5 + if (end_frame - start_frame) < self.num_frames: + continue + total_frames = end_frame - start_frame + if self.use_dynamic_fps: + max_stride = total_frames // self.num_frames + if max_stride < 1: + continue + num_multiplier = self._sample_legacy_stride_with_bias(max_stride) + dynamic_conditioning_fps = meta_dict["framerate"] / num_multiplier + elif self.use_native_fps: + num_multiplier = total_frames // self.num_frames + if num_multiplier not in self.allowed_num_multiplers: + continue + else: + num_multiplier = 1 + + expected_length = self.num_frames * int(num_multiplier) + if total_frames < expected_length: + continue + frame_start = start_frame + (total_frames - expected_length) // 2 + frame_end = frame_start + expected_length + frame_indices = list(range(frame_start, frame_end, int(num_multiplier))) + else: + frame_indices = list(range(start_frame, end_frame)) + if "alpamayo" in data_dict["__url__"].root: + if len(frame_indices) < 5: + continue + frame_indices = frame_indices[5:] + start_frame += 5 + + try: + video_frames, control_frames_by_key = self._decode_all_streams_at( + video, control_videos, frame_indices, rgb_transform + ) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode aligned video streams. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + break + + if video_frames is None: + log.warning( + f"No valid video frames found, return None. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + if self.num_frames > 0 and not (self.use_dynamic_fps or self.use_native_fps or self.use_original_fps): + video_frames, control_frames_by_key = self._subsample_all_streams( + video_frames, control_frames_by_key + ) # [C,T,H,W] + num_multiplier = (end_frame - start_frame) / self.num_frames + + + # variable-length fields like ``frame_indices`` here -- ``video_flatten_keys`` in + # ``get_video_transfer_augmentor`` lists ``frame_indices``, and surfacing a + # per-sample list there would crash ``custom_collate_fn`` (default_collate requires + # equal-size elements across the batch). + base_video_info = { + "fps": meta_dict["framerate"], + "n_orig_video_frames": meta_dict["nb_frames"], + "chunk_index": chunk_index, + "frame_start": start_frame, + "frame_end": end_frame, + "num_frames": end_frame - start_frame, + "num_multiplier": num_multiplier, + "conditioning_fps": dynamic_conditioning_fps or meta_dict["framerate"] / num_multiplier, + } + data_dict[self.video_key] = { + **base_video_info, + "video": video_frames, # [C,T,H,W] + } + for control_video_key, control_frames in control_frames_by_key.items(): + data_dict[control_video_key] = { + **base_video_info, + "video": control_frames, # [C,T,H,W] + } + return data_dict + + +class VideoTransferAlignedChunkedFramesParsing(VideoTransferAlignedFullFramesParsing): + """Decode RGB and aligned control videos for a selected caption chunk.""" + + def _sample_frame_indices_for_chunk( + self, + decoder_len: int, + chunk_start: int, + chunk_end: int, + min_stride_override: int | None = None, + ) -> tuple[list[int], int]: + chunk_start = max(0, min(chunk_start, decoder_len)) + chunk_end = max(chunk_start, min(chunk_end, decoder_len)) + if chunk_end <= chunk_start: + return [], 0 + + min_stride = int(min_stride_override) if min_stride_override is not None else self.min_stride + max_stride = max(self.max_stride, min_stride) + stride = self._sample_stride_with_bias(max_stride, min_stride) + frame_indices = np.arange(chunk_start, chunk_end, stride).tolist() + max_num_frames = min(len(frame_indices), self.args.get("max_num_frames", 1000)) + if max_num_frames < 1: + return [], stride + + # Wan VAE temporal compression expects 1 + 4N video frames. + num_video_frames = 1 + 4 * ((max_num_frames - 1) // 4) + return frame_indices[:num_video_frames], stride + + def __call__(self, data_dict: dict) -> dict | None: + try: + meta_dict = data_dict[self.meta_key] + video = data_dict[self.video_key] + except Exception: + log.warning( + f"Cannot find video. url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + if not self._validate_and_probe(video, meta_dict, data_dict): + return None + + if "chunk_start_frame" not in data_dict or "chunk_end_frame" not in data_dict: + log.warning( + f"VideoTransferAlignedChunkedFramesParsing: missing chunk_start_frame/chunk_end_frame. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + chunk_start = int(data_dict["chunk_start_frame"]) + chunk_end = int(data_dict["chunk_end_frame"]) + + rgb_transform = self._build_rgb_decode_transform(data_dict, meta_dict) + control_videos: dict[str, bytes] = {} + try: + decoder_len = self._probe_video_len(video) + + control_decoder_lens = [] + for control_video_key in self.control_video_keys: + control_video = data_dict.get(control_video_key) + if not isinstance(control_video, bytes): + log.warning( + f"VideoTransferAlignedChunkedFramesParsing: missing bytes for {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + control_videos[control_video_key] = control_video + control_decoder_lens.append(self._probe_video_len(control_video)) + + # Clamp the caption chunk to frames available in every loaded stream. + aligned_decoder_len = min([decoder_len, *control_decoder_lens]) if control_decoder_lens else decoder_len + min_stride_override = data_dict.pop(self.min_stride_key, None) + frame_indices, stride = self._sample_frame_indices_for_chunk( + aligned_decoder_len, + chunk_start, + chunk_end, + min_stride_override=min_stride_override, + ) + if len(frame_indices) == 0: + log.warning( + f"VideoTransferAlignedChunkedFramesParsing: empty chunk after clamping/stride. " + f"chunk=[{chunk_start},{chunk_end}), aligned_decoder_len={aligned_decoder_len}, " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}", + rank0_only=False, + ) + return None + + video_frames = self._decode_frames_at(video, frame_indices, rgb_transform) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode RGB video. url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + + base_video_info = { + "frame_start": frame_indices[0], + "frame_end": frame_indices[-1], + "frame_indices": frame_indices, + "num_frames": len(frame_indices), + "fps": meta_dict["framerate"], + "conditioning_fps": meta_dict["framerate"] / stride if stride > 0 else meta_dict["framerate"], + "num_multiplier": stride, + "n_orig_video_frames": decoder_len, + } + data_dict[self.video_key] = { + **base_video_info, + "video": video_frames, # [C,T,H,W] + } + + for control_video_key, control_video in control_videos.items(): + try: + control_frames = self._decode_frames_at(control_video, frame_indices) # [C,T,H,W] + except Exception as e: + log.warning( + f"Failed to decode {control_video_key}. " + f"url: {data_dict['__url__']}, key: {data_dict['__key__']}, error: {e}", + rank0_only=False, + ) + return None + data_dict[control_video_key] = { + **base_video_info, + "video": control_frames, # [C,T,H,W] + } + + data_dict.pop("chunk_start_frame", None) + data_dict.pop("chunk_end_frame", None) + return data_dict diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/merge_datadict.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/merge_datadict.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/merge_datadict.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/pkl_to_media.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/pkl_to_media.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/pkl_to_media.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/resolution_text_info.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/resolution_text_info.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/resolution_text_info.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/sequence_plan.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/sequence_plan.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/sequence_plan.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/sound_sequence_plan.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/sound_sequence_plan.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/sound_sequence_plan.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/text_tokenizer.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/text_tokenizer.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_tokenizer.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_image.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py similarity index 94% rename from cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_image.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py index 452e646d..fb2f147e 100644 --- a/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_image.py +++ b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_image.py @@ -178,6 +178,19 @@ def __call__(self, data_dict: dict) -> dict: selected_caption_type = random.choice(_AVAILABLE_QWEN3_30B_A3B_CAPTIONS) data_dict["ai_caption"] = decoded_captions_ai[selected_caption_type] data_dict["selected_caption_type"] = selected_caption_type + elif caption_type == "qwen3_235b_a22b_v0": + # Synthetic scene-text data ingested via + # pipelines/image/text_rendering/ingest_webdataset.py stores captions as a flat + # dict {"caption_short": ..., "caption_long": ...} (no "caption"/"captions" + # nesting), so we index decoded_captions_ai directly. + available = [k for k in _AVAILABLE_CAPTIONS_V2 if k in decoded_captions_ai] + if not available: + raise KeyError( + f"No known caption keys for {caption_type} in {list(decoded_captions_ai.keys())}" + ) + selected_caption_type = random.choice(available) + data_dict["ai_caption"] = decoded_captions_ai[selected_caption_type] + data_dict["selected_caption_type"] = selected_caption_type elif any( caption_type in _AVAILABLE_QWEN_CAPTIONS for caption_type in decoded_captions_ai[caption_key].keys() ): diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_video.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py similarity index 65% rename from cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_video.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py index 001b3c8a..b2fa283c 100644 --- a/cosmos-inference/_src/vfm/datasets/augmentors/text_transforms_for_video.py +++ b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/text_transforms_for_video.py @@ -312,38 +312,42 @@ def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: O assert len(input_keys) >= 1, "TextTransformForVideoTransferFullFrames requires a metadata input key" self.meta_key = input_keys[0] self.args = args or {} - self.caption_prefix = self.args.get("caption_prefix", None) self.keep_metas = self.args.get("keep_metas", False) self.caption_options = self._normalize_caption_config(self.args["caption_config"]) + # This fixes transfer datasets that mix caption chunks with different + # lengths. Each caption source needs its own stride so the sampled video + # stays within the token budget while matching the selected caption. + self.min_stride_key = self.args.get("min_stride_key", "_full_frames_min_stride") @staticmethod - def _normalize_caption_config(caption_config: dict | list) -> list[tuple[str, float]]: + def _normalize_caption_config(caption_config: dict | list) -> list[tuple[str, float, dict]]: if isinstance(caption_config, dict): - options: list[tuple[str, float]] = [] + options: list[tuple[str, float, dict]] = [] for caption_key, config in caption_config.items(): - ratio = config.get("ratio", 1.0) if isinstance(config, dict) else float(config) - options.append((caption_key, float(ratio))) + if isinstance(config, dict): + ratio = config.get("ratio", 1.0) + # Keep more than the sampling ratio because source-specific + # settings, like min_stride, are part of how caption/video + # alignment is preserved. + options.append((caption_key, float(ratio), dict(config))) + else: + options.append((caption_key, float(config), {})) return options options = [] for item in caption_config: if isinstance(item, str): - options.append((item, 1.0)) + options.append((item, 1.0, {})) elif isinstance(item, dict): caption_key = item.get("key") or item.get("caption_key") or item.get("caption_type") or item.get("name") if caption_key is None: raise ValueError(f"Caption config entry is missing a caption key: {item}") - options.append((caption_key, float(item.get("ratio", 1.0)))) + options.append((caption_key, float(item.get("ratio", 1.0)), dict(item))) else: caption_key, ratio = item - options.append((caption_key, float(ratio))) + options.append((caption_key, float(ratio), {})) return options - def _apply_caption_prefix(self, data_dict: dict) -> None: - if not self.caption_prefix or not isinstance(data_dict.get("ai_caption"), str): - return - data_dict["ai_caption"] = self.caption_prefix + " " + data_dict["ai_caption"].lstrip() - def _lookup_caption_dict(self, data_dict: dict, meta_dict: dict | None, caption_key: str) -> dict | None: candidate = data_dict.get(caption_key) if candidate is None and isinstance(meta_dict, dict): @@ -355,27 +359,28 @@ def _lookup_caption_dict(self, data_dict: dict, meta_dict: dict | None, caption_ def __call__(self, data_dict: dict) -> dict | None: meta_dict = data_dict.get(self.meta_key) - available_options: list[tuple[str, float]] = [] - for key, ratio in self.caption_options: + available_options: list[tuple[str, float, dict]] = [] + for key, ratio, option in self.caption_options: if ratio <= 0: continue if self._lookup_caption_dict(data_dict, meta_dict, key) is not None: - available_options.append((key, ratio)) + available_options.append((key, ratio, option)) if not available_options: log.warning( f"TextTransformForVideoTransferFullFrames: none of the configured caption keys " - f"{[key for key, _ in self.caption_options]} hold a caption dict in metadata/sample keys. " + f"{[key for key, _, _ in self.caption_options]} hold a caption dict in metadata/sample keys. " f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", rank0_only=False, ) return None sampled_caption_key = random.choices( - [key for key, _ in available_options], - weights=[ratio for _, ratio in available_options], + [key for key, _, _ in available_options], + weights=[ratio for _, ratio, _ in available_options], k=1, )[0] + sampled_caption_option = next(option for key, _, option in available_options if key == sampled_caption_key) caption_dict = self._lookup_caption_dict(data_dict, meta_dict, sampled_caption_key) if caption_dict is None or self.CAPTION_FIELD not in caption_dict: log.warning( @@ -399,11 +404,121 @@ def __call__(self, data_dict: dict) -> dict | None: data_dict["ai_caption"] = json.dumps(structured) data_dict["sampled_caption_style"] = sampled_caption_key - self._apply_caption_prefix(data_dict) + if "min_stride" in sampled_caption_option: + # Without this override, 200-frame and 400-frame caption sources + # would share one stride and could either waste context or overflow + # the intended token length. + data_dict[self.min_stride_key] = int(sampled_caption_option["min_stride"]) if not self.keep_metas: data_dict.pop(self.meta_key, None) - for caption_key, _ in self.caption_options: + for caption_key, _, _ in self.caption_options: + if caption_key in data_dict: + del data_dict[caption_key] + return data_dict + + +class TextTransformForVideoTransferChunkedFrames(TextTransformForVideoTransferFullFrames): + """Read structured captions and sample one chunk for transfer training. + + This keeps the full-frame caption-source sampling behavior, including + per-source options such as ``min_stride``, but emits the sampled chunk's + frame range so the downstream parser can decode the matching RGB/control + frames. + """ + + def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: Optional[dict] = None) -> None: + super().__init__(input_keys, output_keys, args) + # The parser still needs metadata for fps/resolution after this transform. + self.keep_metas = self.args.get("keep_metas", True) + self.min_num_frames = int(self.args.get("min_num_frames", 5)) + + def __call__(self, data_dict: dict) -> dict | None: + meta_dict = data_dict.get(self.meta_key) + + available_options: list[tuple[str, float, dict]] = [] + for key, ratio, option in self.caption_options: + if ratio <= 0: + continue + if self._lookup_caption_dict(data_dict, meta_dict, key) is not None: + available_options.append((key, ratio, option)) + + if not available_options: + log.warning( + f"TextTransformForVideoTransferChunkedFrames: none of the configured caption keys " + f"{[key for key, _, _ in self.caption_options]} hold a caption dict in metadata/sample keys. " + f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + sampled_caption_key = random.choices( + [key for key, _, _ in available_options], + weights=[ratio for _, ratio, _ in available_options], + k=1, + )[0] + sampled_caption_option = next(option for key, _, option in available_options if key == sampled_caption_key) + caption_dict = self._lookup_caption_dict(data_dict, meta_dict, sampled_caption_key) + if caption_dict is None or self.CAPTION_FIELD not in caption_dict: + log.warning( + f"TextTransformForVideoTransferChunkedFrames: caption dict for {sampled_caption_key} is missing the " + f"hardcoded {self.CAPTION_FIELD} field. url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + try: + chunks = json.loads(caption_dict[self.CAPTION_FIELD]) + if not isinstance(chunks, dict) or len(chunks) == 0: + log.warning( + f"TextTransformForVideoTransferChunkedFrames: empty chunk dict for {sampled_caption_key}. " + f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + eligible_chunk_keys: list[str] = [] + for chunk_key, chunk in chunks.items(): + try: + start_frame = int(chunk["start_frame"]) + end_frame = int(chunk["end_frame"]) + except (KeyError, TypeError, ValueError): + continue + if end_frame - start_frame >= self.min_num_frames: + eligible_chunk_keys.append(chunk_key) + + if not eligible_chunk_keys: + log.warning( + f"TextTransformForVideoTransferChunkedFrames: no chunks with >= {self.min_num_frames} frames " + f"in {sampled_caption_key}. url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}", + rank0_only=False, + ) + return None + + sampled_chunk_key = random.choice(eligible_chunk_keys) + sampled_chunk = chunks[sampled_chunk_key] + chunk_start_frame = int(sampled_chunk["start_frame"]) + chunk_end_frame = int(sampled_chunk["end_frame"]) + structured = json.loads(sampled_chunk["caption"]) + except Exception as e: + log.warning( + f"TextTransformForVideoTransferChunkedFrames: failed to decode {sampled_caption_key}.{self.CAPTION_FIELD}. " + f"url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}, error: {e}", + rank0_only=False, + ) + return None + + data_dict["chunk_start_frame"] = chunk_start_frame + data_dict["chunk_end_frame"] = chunk_end_frame + data_dict["ai_caption"] = json.dumps(structured) + data_dict["sampled_caption_style"] = sampled_caption_key + data_dict["sampled_chunk_key"] = sampled_chunk_key + if "min_stride" in sampled_caption_option: + data_dict[self.min_stride_key] = int(sampled_caption_option["min_stride"]) + + if not self.keep_metas: + data_dict.pop(self.meta_key, None) + for caption_key, _, _ in self.caption_options: if caption_key in data_dict: del data_dict[caption_key] return data_dict @@ -429,8 +544,98 @@ def __init__(self, input_keys: dict, output_keys: Optional[list] = None, args: O ) self.meta_key = input_keys[0] self.video_key = input_keys[1] - self.args = args - self.keep_metas = args.get("keep_metas", False) if args else False + self.args = args or {} + self.keep_metas = self.args.get("keep_metas", False) + self.caption_key = self.args.get("caption_key", "caption") + + @staticmethod + def _json_dict_or_none(raw_caption: object) -> dict | None: + if isinstance(raw_caption, dict): + return raw_caption + if not isinstance(raw_caption, str) or len(raw_caption) == 0: + return None + try: + parsed_caption = json.loads(raw_caption) + except (json.JSONDecodeError, TypeError): + return None + return parsed_caption if isinstance(parsed_caption, dict) else None + + @staticmethod + def _frame_count_or_large_bound(meta_dict: dict) -> int: + nb_frames = meta_dict.get("nb_frames") + if isinstance(nb_frames, int) and nb_frames > 0: + return nb_frames + length = meta_dict.get("length") + framerate = meta_dict.get("framerate") + if isinstance(length, (float, int)) and isinstance(framerate, (float, int)) and length > 0 and framerate > 0: + return max(1, int(round(length * framerate))) + # VideoParsingChunkedFrames clamps to the decoder length, so a large end frame is safe. + return 10**9 + + def _find_audio_caption(self, meta_dict: dict) -> str | None: + audio_caption = meta_dict.get("caption_audio") + if isinstance(audio_caption, str) and len(audio_caption) > 0: + return audio_caption + for value in meta_dict.values(): + if isinstance(value, dict): + caption_sound = value.get("caption_sound") + if isinstance(caption_sound, str) and len(caption_sound) > 0: + return caption_sound + return None + + def _parse_legacy_full_video_caption(self, meta_dict: dict) -> dict[str, dict] | None: + """Build one full-video chunk from older caption schemas that do not have ``caption``.""" + caption_json = self._json_dict_or_none(meta_dict.get("caption_structured")) + if caption_json is None: + for caption_key in ( + "caption_rewrite_dense", + "caption_dense", + "caption_descriptive", + "caption_base", + "caption_temporal", + "caption_short", + ): + caption_text = meta_dict.get(caption_key) + if isinstance(caption_text, str) and len(caption_text) > 0: + caption_json = {"description": caption_text} + break + if caption_json is None: + return None + + end_frame = self._frame_count_or_large_bound(meta_dict) + return { + f"chunk_0_{end_frame}": { + "start_frame": 0, + "end_frame": end_frame, + "caption_json": caption_json, + } + } + + def _parse_audio_caption_chunks(self, meta_dict: dict) -> dict[str, dict] | None: + """Build chunk metadata from nested audio-caption metas when visual captions are absent.""" + chunks: dict[str, dict] = {} + for key, value in meta_dict.items(): + if not isinstance(key, str) or not isinstance(value, dict): + continue + caption_sound = value.get("caption_sound") + if not isinstance(caption_sound, str) or len(caption_sound) == 0: + continue + + try: + start_frame, end_frame = [int(part) for part in key.split("_", maxsplit=1)] + except ValueError: + start_frame = value.get("start_frame") + end_frame = value.get("end_frame") + if not isinstance(start_frame, int) or not isinstance(end_frame, int): + continue + + chunks[key] = { + "start_frame": start_frame, + "end_frame": end_frame, + "caption_json": {"audio_description": caption_sound}, + } + + return chunks or None def __call__(self, data_dict: dict) -> dict | None: r"""Performs text transformation. @@ -451,7 +656,21 @@ def __call__(self, data_dict: dict) -> dict | None: try: meta_dict = data_dict[self.meta_key] - caption = json.loads(meta_dict["caption"]) + raw_caption = meta_dict.get(self.caption_key) + if raw_caption is not None: + caption = self._json_dict_or_none(raw_caption) + if caption is None: + raise ValueError(f"{self.caption_key} is not a JSON object") + else: + # Some sound midtrain shards use older full-video visual captions + # (caption_base/caption_structured/...) instead of the chunked + # ``caption`` field. Prefer those visual captions when present; + # otherwise fall back to nested audio-only chunks. + caption = self._parse_legacy_full_video_caption(meta_dict) + if caption is None: + caption = self._parse_audio_caption_chunks(meta_dict) + if caption is None: + raise KeyError(self.caption_key) # Contents of caption # caption = { @@ -481,7 +700,10 @@ def __call__(self, data_dict: dict) -> dict | None: data_dict["chunk_start_frame"] = int(sampled_chunk["start_frame"]) data_dict["chunk_end_frame"] = int(sampled_chunk["end_frame"]) - caption_json = json.loads(sampled_chunk["caption"]) + if "caption_json" in sampled_chunk: + caption_json = sampled_chunk["caption_json"] + else: + caption_json = json.loads(sampled_chunk["caption"]) except Exception as e: log.warning( f"TextTransformForVideoJsonCaption dataloader error -- url: {data_dict.get('__url__')}, key: {data_dict.get('__key__')}\n error {e}", @@ -497,7 +719,7 @@ def __call__(self, data_dict: dict) -> dict | None: # Inject audio caption from metas as a new field when available. Added after the field # dropout above so it is preserved whenever upstream metadata provides it. - audio_caption = meta_dict.get("caption_audio") + audio_caption = self._find_audio_caption(meta_dict) if isinstance(audio_caption, str) and len(audio_caption) > 0: caption_json["audio_description"] = audio_caption diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/__init__.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/blur.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/blur.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/blur.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/control_input.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/fast_blur.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/seg.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_input/seg.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_input/seg.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_transform.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/transfer_control_transform.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/transfer_control_transform.py diff --git a/cosmos-inference/_src/vfm/datasets/augmentors/video_parsing.py b/cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/augmentors/video_parsing.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/augmentors/video_parsing.py diff --git a/cosmos-inference/_src/vfm/datasets/joint_dataloader.py b/cosmos-inference/cosmos3/_src/vfm/datasets/joint_dataloader.py similarity index 84% rename from cosmos-inference/_src/vfm/datasets/joint_dataloader.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/joint_dataloader.py index 2a69f5ca..0bd49ad9 100644 --- a/cosmos-inference/_src/vfm/datasets/joint_dataloader.py +++ b/cosmos-inference/cosmos3/_src/vfm/datasets/joint_dataloader.py @@ -60,14 +60,26 @@ def custom_collate_fn(batch): elem = batch[0] if isinstance(elem, dict): + # Some Action datasets add optional metadata keys (for example + # ``additional_view_description`` for concat-view captions) only for a + # subset of samples. PyTorch can batch such samples together when + # DataLoader batch_size > 1; collating only elem's keys and indexing + # every sample by that key turns the optional field into a fatal + # KeyError. Use the union of keys and skip optional keys that are not + # present in every sample. Required training keys still fail loudly via + # downstream assertions if actually missing. result = {} - for key in elem: + keys = set().union(*(d.keys() for d in batch)) + for key in keys: if key in _TIMING_KEYS: continue + values = [d.get(key) for d in batch] + if any(value is None for value in values): + continue if key in list_collate_keys: - result[key] = [d[key] for d in batch] + result[key] = values else: - result[key] = default_collate([d[key] for d in batch]) + result[key] = default_collate(values) result.update(_aggregate_worker_timing(batch)) return result else: @@ -135,6 +147,8 @@ class JointDataLoader(webdataset.WebLoader): A joint dataloader that supports loading both images and videos. """ + _DEFAULT_LOOKAHEAD_LIMIT: ClassVar[int] = 10 + def __init__( self, dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], @@ -145,6 +159,9 @@ def __init__( max_samples_per_batch: int | None, sound_latent_fps: float = 0, audio_sample_rate: int = 48000, + prewarm: bool = True, + default_lookahead_limit: int = _DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, ): """ Initialize the JointDataLoader with multiple datasets. @@ -163,6 +180,9 @@ def __init__( sound_latent_fps: Sound tokenizer latent rate in Hz (e.g. 25). If 0, sound tokens are not counted. audio_sample_rate: Audio sample rate in Hz (e.g. 48000). Used with sound_latent_fps to estimate sound token count. + default_lookahead_limit: Packing-loop look-ahead fallback for dataloaders not in + ``lookahead_limits``. + lookahead_limits: Optional ``{dataset_name: int}`` per-dataloader override. Example: joint_loader = IterativeJointDataLoader( @@ -179,6 +199,7 @@ def __init__( ) """ self.dataloader_list, self.dataset_name_list, self.data_ratios = [], [], [] + self.lookahead_limits: list[int] = [] self.tokenizer_spatial_compression_factor = tokenizer_spatial_compression_factor self.tokenizer_temporal_compression_factor = tokenizer_temporal_compression_factor self.patch_spatial = patch_spatial @@ -186,11 +207,16 @@ def __init__( self.max_samples_per_batch = max_samples_per_batch self.sound_latent_fps = sound_latent_fps self.audio_sample_rate = audio_sample_rate + self.default_lookahead_limit = int(default_lookahead_limit) assert (self.max_sequence_length is None) != (self.max_samples_per_batch is None), ( "Exactly one of max_sequence_length or max_samples_per_batch must be None, but not both." ) + _lookahead_overrides: Dict[str, int] = dict(lookahead_limits) if lookahead_limits else {} + unknown = set(_lookahead_overrides) - set(dataloaders) + assert not unknown, f"lookahead_limits references unknown dataloaders {unknown}; valid: {sorted(dataloaders)}" + for dataset_name, dataloader_data in dataloaders.items(): assert set(dataloader_data.keys()) == {"dataloader", "ratio"}, f"Invalid config: {dataloader_data}" if dataloader_data["ratio"] <= 0: @@ -198,6 +224,7 @@ def __init__( self.dataset_name_list.append(dataset_name) self.dataloader_list.append(instantiate(dataloader_data["dataloader"], collate_fn=custom_collate_fn)) self.data_ratios.append(dataloader_data["ratio"]) + self.lookahead_limits.append(int(_lookahead_overrides.get(dataset_name, self.default_lookahead_limit))) self.global_id = 0 self.ratio_sum = sum(self.data_ratios) @@ -214,6 +241,84 @@ def __init__( for data in self.dataloader_list: self.data_len += len(data) + # Pre-warm all dataloaders: force worker process spawning and first + # batch loading so that slow dataset initialisation (e.g. action + # datasets with spawn workers) happens here rather than mid-training + # where it would cause NCCL collective timeouts. + if prewarm: + self._prewarm_dataloaders() + else: + log.info( + "JointDataLoader: prewarm DISABLED (debug mode); first iteration may incur per-stream cold-load cost" + ) + + def _prewarm_dataloaders(self) -> None: + """Force all dataloader iterators to spawn workers and produce one batch. + + The first ``next()`` call on an ``InfiniteDataLoader`` iterator triggers + ``DataLoader.__iter__()`` which spawns worker processes. For action + dataloaders using ``multiprocessing_context='spawn'``, each worker must + fully initialise heavy datasets (BridgeOrigLeRobotDataset, EMBODIMENT_A, etc.) + from scratch. If this happens lazily during training, the resulting + delay (potentially minutes) causes NCCL collective timeouts when faster + ranks enter the forward pass while slower ranks are still loading data. + + By pulling one batch from every dataloader here — before any training + iteration — we ensure all workers are alive and warmed up. The fetched + samples are pushed into the per-dataloader buffer so they are consumed + normally by the first iteration that selects that dataloader. + + A ``dist.barrier()`` at the end synchronises all ranks so that training + only begins once every rank has finished pre-warming. + """ + import time + + for i, (name, dl_iter) in enumerate(zip(self.dataset_name_list, self.dataloaders)): + t0 = time.monotonic() + try: + batch = next(dl_iter) + except StopIteration: + log.warning(f"Pre-warm: dataloader {name!r} is empty, skipping") + continue + elapsed = time.monotonic() - t0 + + # Split the collated batch into individual samples and push them + # into the buffer — identical to the splitting logic in + # _get_next_sample — so the samples are not wasted. + is_image_batch = "images" in batch + input_images_or_videos = batch["images" if is_image_batch else "video"] + batch_size = len(input_images_or_videos) + + for j in range(batch_size): + sample = {} + for k, v in batch.items(): + if k in _BATCH_TIMING_KEYS: + sample[k] = v + elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS: + elem = v[j] + if isinstance(elem, list): + sample[k] = elem + else: + sample[k] = v[j : j + 1] + elif isinstance(v, list): + sample[k] = v[j] + elif isinstance(v, torch.Tensor) and v.dim() > 0: + sample[k] = v[j : j + 1] + else: + sample[k] = v[j : j + 1] + self.buffers[i].append(sample) + + log.info( + f"Pre-warm: dataloader {name!r} ready — {batch_size} samples buffered in {elapsed:.1f}s", + rank0_only=False, + ) + + # Synchronise so training only starts once every rank is warmed up. + if torch.distributed.is_initialized(): + log.info("Pre-warm: waiting at barrier for all ranks …") + torch.distributed.barrier() + log.info("Pre-warm: all ranks ready") + def _compute_num_tokens_per_sample(self, data_batch: dict) -> int: """ This function computes the number of tokens per sample in the data batch. @@ -423,6 +528,9 @@ def __init__( sound_latent_fps: float = 0, audio_sample_rate: int = 48000, seed: int | None = 42, + prewarm: bool = True, + default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, ): super().__init__( dataloaders, @@ -433,6 +541,9 @@ def __init__( max_samples_per_batch, sound_latent_fps=sound_latent_fps, audio_sample_rate=audio_sample_rate, + prewarm=prewarm, + default_lookahead_limit=default_lookahead_limit, + lookahead_limits=lookahead_limits, ) self.seed = seed # Calculate probabilities for random sampling @@ -451,7 +562,7 @@ def __iter__(self): metrics = _PackingMetrics() output_batch = dict() skipped_samples = deque() - lookahead_limit = 10 + lookahead_limit = self.lookahead_limits[index_id] lookahead_count = 0 while True: @@ -672,6 +783,7 @@ def __init__( sound_latent_fps: float = 0, audio_sample_rate: int = 48000, dataset_name: str = "default", + lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, ): """ Args: @@ -686,6 +798,7 @@ def __init__( sound_latent_fps: Sound tokenizer latent rate in Hz. If 0, sound tokens are not counted. audio_sample_rate: Audio sample rate in Hz. dataset_name: Name tag attached to every sample in the output batch. + lookahead_limit: Packing-loop look-ahead for the wrapped dataloader. """ wrapped = {dataset_name: {"dataloader": dataloader, "ratio": 1}} super().__init__( @@ -697,6 +810,7 @@ def __init__( max_samples_per_batch=max_samples_per_batch, sound_latent_fps=sound_latent_fps, audio_sample_rate=audio_sample_rate, + lookahead_limits={dataset_name: int(lookahead_limit)}, ) def __iter__(self): @@ -709,7 +823,8 @@ def __iter__(self): output_batch: dict = {} skipped_samples: deque = deque() - lookahead_limit = 10 + # PackingDataLoader wraps a single dataloader, so lookahead_limits has one entry. + lookahead_limit = self.lookahead_limits[0] lookahead_count = 0 while True: @@ -785,6 +900,8 @@ def __init__( max_samples_per_batch: int | None = None, sound_latent_fps: float = 0, audio_sample_rate: int = 48000, + default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, ): super().__init__( dataloaders, @@ -795,6 +912,8 @@ def __init__( max_samples_per_batch, sound_latent_fps=sound_latent_fps, audio_sample_rate=audio_sample_rate, + default_lookahead_limit=default_lookahead_limit, + lookahead_limits=lookahead_limits, ) # Convert data ratios to probabilities @@ -807,7 +926,7 @@ def __iter__(self): metrics = _PackingMetrics() output_batch = dict() skipped_samples = deque() - lookahead_limit = 10 + lookahead_limit = self.lookahead_limits[index_id] lookahead_count = 0 while True: diff --git a/cosmos-inference/_src/vfm/datasets/local_datasets/__init__.py b/cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/local_datasets/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/__init__.py diff --git a/cosmos-inference/_src/vfm/datasets/local_datasets/helper.py b/cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/helper.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/local_datasets/helper.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/helper.py diff --git a/cosmos-inference/_src/vfm/datasets/local_datasets/sft_dataset.py b/cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/local_datasets/sft_dataset.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/local_datasets/sft_dataset.py diff --git a/cosmos-inference/_src/vfm/datasets/sequence_packing.py b/cosmos-inference/cosmos3/_src/vfm/datasets/sequence_packing.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/sequence_packing.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/sequence_packing.py diff --git a/cosmos-inference/_src/vfm/datasets/utils.py b/cosmos-inference/cosmos3/_src/vfm/datasets/utils.py similarity index 100% rename from cosmos-inference/_src/vfm/datasets/utils.py rename to cosmos-inference/cosmos3/_src/vfm/datasets/utils.py diff --git a/cosmos-inference/_src/vfm/diffusion/__init__.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/__init__.py diff --git a/cosmos-inference/_src/vfm/diffusion/rectified_flow.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/rectified_flow.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/rectified_flow.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/rectified_flow.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/__init__.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/__init__.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/edm.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/edm.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/edm.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/edm.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/fixed_step.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/fixed_step.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/fixed_step.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/fm_solvers_unipc.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/fm_solvers_unipc.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/fm_solvers_unipc.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/unipc.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/unipc.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/unipc.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/unipc.py diff --git a/cosmos-inference/_src/vfm/diffusion/samplers/utils.py b/cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/utils.py similarity index 100% rename from cosmos-inference/_src/vfm/diffusion/samplers/utils.py rename to cosmos-inference/cosmos3/_src/vfm/diffusion/samplers/utils.py diff --git a/cosmos-inference/_src/vfm/evaluation/__init__.py b/cosmos-inference/cosmos3/_src/vfm/evaluation/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/evaluation/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/evaluation/__init__.py diff --git a/cosmos-inference/_src/vfm/evaluation/action/__init__.py b/cosmos-inference/cosmos3/_src/vfm/evaluation/action/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/evaluation/action/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/evaluation/action/__init__.py diff --git a/cosmos-inference/_src/vfm/evaluation/action/libero/__init__.py b/cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/evaluation/action/libero/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/__init__.py diff --git a/cosmos-inference/_src/vfm/evaluation/action/libero/closed_loop_eval.py b/cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py similarity index 100% rename from cosmos-inference/_src/vfm/evaluation/action/libero/closed_loop_eval.py rename to cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/closed_loop_eval.py diff --git a/cosmos-inference/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py b/cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py similarity index 100% rename from cosmos-inference/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py rename to cosmos-inference/cosmos3/_src/vfm/evaluation/action/libero/dataset_reply_action_server.py diff --git a/cosmos-inference/_src/vfm/models/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/__init__.py diff --git a/cosmos-inference/_src/vfm/models/hf_model.py b/cosmos-inference/cosmos3/_src/vfm/models/hf_model.py similarity index 100% rename from cosmos-inference/_src/vfm/models/hf_model.py rename to cosmos-inference/cosmos3/_src/vfm/models/hf_model.py diff --git a/cosmos-inference/_src/vfm/models/llm/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/__init__.py diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/__init__.py diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configs/Qwen3-0.6B.json diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/configs/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/configs/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configs/__init__.py diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/configuration_qwen3.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/configuration_qwen3.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/configuration_qwen3.py diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/qwen3.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/qwen3.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/qwen3.py diff --git a/cosmos-inference/_src/vfm/models/llm/qwen3/test_qwen3.py b/cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py similarity index 100% rename from cosmos-inference/_src/vfm/models/llm/qwen3/test_qwen3.py rename to cosmos-inference/cosmos3/_src/vfm/models/llm/qwen3/test_qwen3.py diff --git a/cosmos-inference/_src/vfm/models/mot/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/__init__.py diff --git a/cosmos-inference/_src/vfm/models/mot/attention.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/attention.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/attention.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/attention.py diff --git a/cosmos-inference/_src/vfm/models/mot/context_parallel_utils.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/context_parallel_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/context_parallel_utils.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/context_parallel_utils.py diff --git a/cosmos-inference/_src/vfm/models/mot/cosmos3_vfm_network.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/cosmos3_vfm_network.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/cosmos3_vfm_network.py diff --git a/cosmos-inference/_src/vfm/models/mot/domain_aware_linear.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/domain_aware_linear.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/domain_aware_linear.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/domain_aware_linear.py diff --git a/cosmos-inference/_src/vfm/models/mot/dot_product_attention.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/dot_product_attention.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/dot_product_attention.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/dot_product_attention.py diff --git a/cosmos-inference/_src/vfm/models/mot/modeling_utils.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/modeling_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/modeling_utils.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/modeling_utils.py diff --git a/cosmos-inference/_src/vfm/models/mot/parallelize_unified_mot.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/parallelize_unified_mot.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/parallelize_unified_mot.py diff --git a/cosmos-inference/_src/vfm/models/mot/parallelize_vfm_network.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/parallelize_vfm_network.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/parallelize_vfm_network.py diff --git a/cosmos-inference/_src/vfm/models/mot/qwen3_vl_unified_mot.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/qwen3_vl_unified_mot.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/qwen3_vl_unified_mot.py diff --git a/cosmos-inference/_src/vfm/models/mot/unified_3dmrope_utils.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/unified_3dmrope_utils.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/unified_3dmrope_utils.py diff --git a/cosmos-inference/_src/vfm/models/mot/unified_mot.py b/cosmos-inference/cosmos3/_src/vfm/models/mot/unified_mot.py similarity index 100% rename from cosmos-inference/_src/vfm/models/mot/unified_mot.py rename to cosmos-inference/cosmos3/_src/vfm/models/mot/unified_mot.py diff --git a/cosmos-inference/_src/vfm/models/omni_mot_model.py b/cosmos-inference/cosmos3/_src/vfm/models/omni_mot_model.py similarity index 100% rename from cosmos-inference/_src/vfm/models/omni_mot_model.py rename to cosmos-inference/cosmos3/_src/vfm/models/omni_mot_model.py diff --git a/cosmos-inference/_src/vfm/models/parallelize_vlm.py b/cosmos-inference/cosmos3/_src/vfm/models/parallelize_vlm.py similarity index 84% rename from cosmos-inference/_src/vfm/models/parallelize_vlm.py rename to cosmos-inference/cosmos3/_src/vfm/models/parallelize_vlm.py index 5d72703f..cde4518a 100644 --- a/cosmos-inference/_src/vfm/models/parallelize_vlm.py +++ b/cosmos-inference/cosmos3/_src/vfm/models/parallelize_vlm.py @@ -31,11 +31,18 @@ from cosmos3._src.vfm.models.hf_model import HFModel from cosmos3._src.vfm.utils.parallelism import ParallelDims +_PRECISION_TO_TORCH_DTYPE: dict[str, torch.dtype] = { + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float32": torch.float32, +} + def parallelize( model: HFModel, parallel_dims: ParallelDims, - train_config, + precision: str, + fsdp_offload: bool = False, ) -> None: """Apply FSDP2 to an HFModel in-place. @@ -53,9 +60,11 @@ def parallelize( model: HFModel instance (``_model`` attribute must be on meta or CPU device). parallel_dims: ParallelDims with meshes already built via :meth:`ParallelDims.build_meshes`. - train_config: Train sub-config — provides ``param_torch_dtype`` and - ``fsdp_offload``. Pass the TRAIN sub-config object, - not the full config. + precision: Activation / parameter dtype for FSDP MixedPrecisionPolicy + (one of ``"bfloat16"``, ``"float16"``, ``"float32"``). + Sourced from ``policy.parallelism.precision``. + fsdp_offload: If True, wrap with CPUOffloadPolicy. Sourced from + ``train.fsdp_offload``. """ if not parallel_dims.dp_shard_enabled: # No shard axis: dp_shard <= 1. FSDP2 (fully_shard) has nothing to do. @@ -65,7 +74,7 @@ def parallelize( return mp_policy = MixedPrecisionPolicy( - param_dtype=train_config.param_torch_dtype, + param_dtype=_PRECISION_TO_TORCH_DTYPE[precision], reduce_dtype=torch.float32, ) @@ -90,9 +99,7 @@ def parallelize( # Wrap the full inner model to cover remaining parameters # (embed_tokens, final layer norm, lm_head, visual projector stem, etc.) - cpu_offload_policy = None - if getattr(train_config, "fsdp_offload", False): - cpu_offload_policy = CPUOffloadPolicy() + cpu_offload_policy = CPUOffloadPolicy() if fsdp_offload else None fully_shard(inner, offload_policy=cpu_offload_policy, **fsdp_kwargs) log.info("parallelize: FSDP2 applied to HFModel._model") diff --git a/cosmos-inference/_src/vfm/models/utils/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/__init__.py diff --git a/cosmos-inference/_src/vfm/models/utils/data_and_condition.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/data_and_condition.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/data_and_condition.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/data_and_condition.py diff --git a/cosmos-inference/_src/vfm/models/utils/dcp_loader.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/dcp_loader.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/dcp_loader.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/dcp_loader.py diff --git a/cosmos-inference/_src/vfm/models/utils/load_balancing_loss.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/load_balancing_loss.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/load_balancing_loss.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/load_balancing_loss.py diff --git a/cosmos-inference/_src/vfm/models/utils/memory.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/memory.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/memory.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/memory.py diff --git a/cosmos-inference/_src/vfm/models/utils/safetensors_loader.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/safetensors_loader.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/safetensors_loader.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/safetensors_loader.py diff --git a/cosmos-inference/_src/vfm/models/utils/taylorseer.py b/cosmos-inference/cosmos3/_src/vfm/models/utils/taylorseer.py similarity index 100% rename from cosmos-inference/_src/vfm/models/utils/taylorseer.py rename to cosmos-inference/cosmos3/_src/vfm/models/utils/taylorseer.py diff --git a/cosmos-inference/_src/vfm/models/vlm/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json diff --git a/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py diff --git a/cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configs/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/configuration_qwen3_vl.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/qwen3_vl.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/utils.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/utils.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/utils.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl/video_processing_qwen3_vl.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configs/__init__.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_bench.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/moe_kernels.py diff --git a/cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py similarity index 100% rename from cosmos-inference/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm/qwen3_vl_moe/qwen3_vl_moe.py diff --git a/cosmos-inference/_src/vfm/models/vlm_model.py b/cosmos-inference/cosmos3/_src/vfm/models/vlm_model.py similarity index 96% rename from cosmos-inference/_src/vfm/models/vlm_model.py rename to cosmos-inference/cosmos3/_src/vfm/models/vlm_model.py index e8b1c55a..22d455d8 100644 --- a/cosmos-inference/_src/vfm/models/vlm_model.py +++ b/cosmos-inference/cosmos3/_src/vfm/models/vlm_model.py @@ -260,6 +260,8 @@ def __init__(self, config: VLMModelConfig, checkpoint): from cosmos3._src.vfm.utils.flash_attn import init_flash_attn_meta self.config = config + # Expose model.precision so LowPrecisionCallback can read it (mirrors OmniMoTModel). + self.precision = getattr(torch, config.policy.parallelism.precision) init_flash_attn_meta(config.train.deterministic) self._init_vlm(config.policy, checkpoint, config.train) @@ -330,17 +332,7 @@ def _init_vlm(self, policy, checkpoint, train) -> None: # consume FSDP rank slots, so dp_replicate * dp_shard == world_size # alone. The VLM HFModel doesn't have a CP-aware attention path. world_size = int(os.environ.get("WORLD_SIZE", 1)) - _tp = policy.parallelism.tp_size - _ep = policy.parallelism.ep_size - _pp = policy.parallelism.pp_size - if _tp != 1 or _ep != 1 or _pp != 1: - raise NotImplementedError( - "VLMModel: tp/ep/pp parallelism is not supported in the unified ParallelDims " - f"design (got tp={_tp}, ep={_ep}, pp={_pp}). Set policy.parallelism.tp_size=1, " - f"ep_size=1, pp_size=1, and use dp_shard_size + dp_replicate_size for FSDP/HSDP." - ) - _dp_shard = policy.parallelism.dp_shard_size - _dp_replicate = policy.parallelism.dp_replicate_size + _dp_replicate = policy.parallelism.data_parallel_replicate_degree # Single-process run: force dp_replicate=1 so ParallelDims doesn't # auto-infer it to world_size (which would equal 1 anyway, but guards # against environments where WORLD_SIZE is unset/inconsistent). @@ -349,9 +341,9 @@ def _init_vlm(self, policy, checkpoint, train) -> None: parallel_dims = ParallelDims( world_size=world_size, - dp_shard=_dp_shard, + dp_shard=policy.parallelism.data_parallel_shard_degree, dp_replicate=_dp_replicate, - cp=getattr(policy.parallelism, "cp_size", 1), + cp=policy.parallelism.context_parallel_shard_degree, enable_inference_mode=False, ) @@ -376,7 +368,12 @@ def _init_vlm(self, policy, checkpoint, train) -> None: # ── d. Apply FSDP2 ── if torch.distributed.is_initialized(): - parallelize(hf_model, parallel_dims, train) + parallelize( + hf_model, + parallel_dims, + policy.parallelism.precision, + train.fsdp_offload, + ) # ── e. Materialize meta tensors ── # FSDP2 fully_shard does NOT auto-materialize meta tensors. We must @@ -388,7 +385,7 @@ def _init_vlm(self, policy, checkpoint, train) -> None: # load_weights() can write into a real (non-meta) CPU tensor; FSDP # will move them to GPU on first forward. # Reference: vlm/train.py:188-192 (same guard; offload handled by post_to_empty_hook). - if getattr(train, "fsdp_offload", False): + if train.fsdp_offload: hf_model._model._apply( lambda t: torch.empty_like(t, device="cpu") if t.device.type == "meta" else t, recurse=True, @@ -444,7 +441,7 @@ def _init_vlm(self, policy, checkpoint, train) -> None: log.info(f"VLMModel: overlaid {len(lm_loaded)} language-model params from {llm_path}") # ── i. Gradient checkpointing ── - if policy.model_gradient_checkpointing: + if policy.parallelism.use_activation_checkpointing: hf_model.apply_gradient_checkpointing() self.model = hf_model diff --git a/cosmos-inference/_src/vfm/tokenizers/__init__.py b/cosmos-inference/cosmos3/_src/vfm/tokenizers/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/tokenizers/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/tokenizers/__init__.py diff --git a/cosmos-inference/_src/vfm/tokenizers/interface.py b/cosmos-inference/cosmos3/_src/vfm/tokenizers/interface.py similarity index 100% rename from cosmos-inference/_src/vfm/tokenizers/interface.py rename to cosmos-inference/cosmos3/_src/vfm/tokenizers/interface.py diff --git a/cosmos-inference/_src/vfm/tokenizers/tokenization_qwen2.py b/cosmos-inference/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py similarity index 100% rename from cosmos-inference/_src/vfm/tokenizers/tokenization_qwen2.py rename to cosmos-inference/cosmos3/_src/vfm/tokenizers/tokenization_qwen2.py diff --git a/cosmos-inference/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py b/cosmos-inference/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py similarity index 98% rename from cosmos-inference/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py rename to cosmos-inference/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py index a6b8c01d..1c837fb5 100644 --- a/cosmos-inference/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py +++ b/cosmos-inference/cosmos3/_src/vfm/tokenizers/wan2pt2_vae_4x16x16.py @@ -1028,6 +1028,18 @@ def _video_vae( model.load_state_dict(ckpt, assign=TRAINING) else: model.to_empty(device=device) + # Ensure all params/buffers are contiguous on every rank before + # `sync_model_states` performs its shape+stride verification. + # `assign=TRAINING` on rank 0 replaces params with loaded tensor objects, + # whose storage may have different strides than the `to_empty`-initialized + # tensors on other ranks. Without this, `_verify_param_shape_across_processes` + # raises: "params[N] ... appears not to match strides of the same param in process 0". + for p in model.parameters(): + if not p.is_contiguous(): + p.data = p.data.contiguous() + for b in model.buffers(): + if not b.is_contiguous(): + b.data = b.data.contiguous() sync_model_states(model) return model diff --git a/cosmos-inference/_src/vfm/utils/__init__.py b/cosmos-inference/cosmos3/_src/vfm/utils/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/utils/__init__.py diff --git a/cosmos-inference/_src/vfm/utils/context_parallel.py b/cosmos-inference/cosmos3/_src/vfm/utils/context_parallel.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/context_parallel.py rename to cosmos-inference/cosmos3/_src/vfm/utils/context_parallel.py diff --git a/cosmos-inference/_src/vfm/utils/data_utils.py b/cosmos-inference/cosmos3/_src/vfm/utils/data_utils.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/data_utils.py rename to cosmos-inference/cosmos3/_src/vfm/utils/data_utils.py diff --git a/cosmos-inference/_src/vfm/utils/dtensor_helper.py b/cosmos-inference/cosmos3/_src/vfm/utils/dtensor_helper.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/dtensor_helper.py rename to cosmos-inference/cosmos3/_src/vfm/utils/dtensor_helper.py diff --git a/cosmos-inference/_src/vfm/utils/flash_attn.py b/cosmos-inference/cosmos3/_src/vfm/utils/flash_attn.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/flash_attn.py rename to cosmos-inference/cosmos3/_src/vfm/utils/flash_attn.py diff --git a/cosmos-inference/_src/vfm/utils/fused_adam.py b/cosmos-inference/cosmos3/_src/vfm/utils/fused_adam.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/fused_adam.py rename to cosmos-inference/cosmos3/_src/vfm/utils/fused_adam.py diff --git a/cosmos-inference/_src/vfm/utils/loss.py b/cosmos-inference/cosmos3/_src/vfm/utils/loss.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/loss.py rename to cosmos-inference/cosmos3/_src/vfm/utils/loss.py diff --git a/cosmos-inference/_src/vfm/utils/misc.py b/cosmos-inference/cosmos3/_src/vfm/utils/misc.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/misc.py rename to cosmos-inference/cosmos3/_src/vfm/utils/misc.py diff --git a/cosmos-inference/_src/vfm/utils/model_loader.py b/cosmos-inference/cosmos3/_src/vfm/utils/model_loader.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/model_loader.py rename to cosmos-inference/cosmos3/_src/vfm/utils/model_loader.py diff --git a/cosmos-inference/_src/vfm/utils/model_weights_stats.py b/cosmos-inference/cosmos3/_src/vfm/utils/model_weights_stats.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/model_weights_stats.py rename to cosmos-inference/cosmos3/_src/vfm/utils/model_weights_stats.py diff --git a/cosmos-inference/_src/vfm/utils/monkey_patch.py b/cosmos-inference/cosmos3/_src/vfm/utils/monkey_patch.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/monkey_patch.py rename to cosmos-inference/cosmos3/_src/vfm/utils/monkey_patch.py diff --git a/cosmos-inference/_src/vfm/utils/optimizer.py b/cosmos-inference/cosmos3/_src/vfm/utils/optimizer.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/optimizer.py rename to cosmos-inference/cosmos3/_src/vfm/utils/optimizer.py diff --git a/cosmos-inference/_src/vfm/utils/parallelism.py b/cosmos-inference/cosmos3/_src/vfm/utils/parallelism.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/parallelism.py rename to cosmos-inference/cosmos3/_src/vfm/utils/parallelism.py diff --git a/cosmos-inference/_src/vfm/utils/rand_state.py b/cosmos-inference/cosmos3/_src/vfm/utils/rand_state.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/rand_state.py rename to cosmos-inference/cosmos3/_src/vfm/utils/rand_state.py diff --git a/cosmos-inference/_src/vfm/utils/tokenizer_benchmarking.py b/cosmos-inference/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/tokenizer_benchmarking.py rename to cosmos-inference/cosmos3/_src/vfm/utils/tokenizer_benchmarking.py diff --git a/cosmos-inference/_src/vfm/utils/vlm/__init__.py b/cosmos-inference/cosmos3/_src/vfm/utils/vlm/__init__.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/vlm/__init__.py rename to cosmos-inference/cosmos3/_src/vfm/utils/vlm/__init__.py diff --git a/cosmos-inference/_src/vfm/utils/vlm/optimizer.py b/cosmos-inference/cosmos3/_src/vfm/utils/vlm/optimizer.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/vlm/optimizer.py rename to cosmos-inference/cosmos3/_src/vfm/utils/vlm/optimizer.py diff --git a/cosmos-inference/_src/vfm/utils/vlm/pretrained_models_downloader.py b/cosmos-inference/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py similarity index 100% rename from cosmos-inference/_src/vfm/utils/vlm/pretrained_models_downloader.py rename to cosmos-inference/cosmos3/_src/vfm/utils/vlm/pretrained_models_downloader.py diff --git a/cosmos-inference/_test.py b/cosmos-inference/cosmos3/_test.py similarity index 98% rename from cosmos-inference/_test.py rename to cosmos-inference/cosmos3/_test.py index 3a6dfaa2..e29a0bb2 100644 --- a/cosmos-inference/_test.py +++ b/cosmos-inference/cosmos3/_test.py @@ -24,9 +24,9 @@ from cosmos3.args import ( IMAGE_ONLY_RESOLUTIONS, - ActionMode, AspectRatio, InferenceResolution, + ModelMode, OmniSampleArgs, OmniSampleOverrides, _load_modality_defaults, @@ -210,10 +210,8 @@ def _check_action_golden(sample_outputs: SampleOutputs, sample_dir: Path) -> lis from cosmos3.scripts.eval_utils import compute_sample_metrics - mode = ActionMode(sample_outputs.args["action_mode"]) - gt_video = ( - _load_canonical_gt_video(sample_dir) if mode in (ActionMode.FORWARD_DYNAMICS, ActionMode.POLICY) else None - ) + mode = ModelMode(sample_outputs.args["model_mode"]) + gt_video = _load_canonical_gt_video(sample_dir) if mode in (ModelMode.FORWARD_DYNAMICS, ModelMode.POLICY) else None gt_action = _load_gt_action(extra["golden_action_path"], sample_dir) if mse_max is not None else None # `compute_sample_metrics` parses mode from `name.split("/")[-2]`. metrics = compute_sample_metrics( diff --git a/cosmos-inference/_test/autoregressive.sh b/cosmos-inference/cosmos3/_test/autoregressive.sh similarity index 100% rename from cosmos-inference/_test/autoregressive.sh rename to cosmos-inference/cosmos3/_test/autoregressive.sh diff --git a/cosmos-inference/_test/distilled.sh b/cosmos-inference/cosmos3/_test/distilled.sh similarity index 100% rename from cosmos-inference/_test/distilled.sh rename to cosmos-inference/cosmos3/_test/distilled.sh diff --git a/cosmos-inference/_test/latency.sh b/cosmos-inference/cosmos3/_test/latency.sh similarity index 100% rename from cosmos-inference/_test/latency.sh rename to cosmos-inference/cosmos3/_test/latency.sh diff --git a/cosmos-inference/_test/omni-super.sh b/cosmos-inference/cosmos3/_test/omni-super.sh similarity index 100% rename from cosmos-inference/_test/omni-super.sh rename to cosmos-inference/cosmos3/_test/omni-super.sh diff --git a/cosmos-inference/_test/omni.sh b/cosmos-inference/cosmos3/_test/omni.sh similarity index 100% rename from cosmos-inference/_test/omni.sh rename to cosmos-inference/cosmos3/_test/omni.sh diff --git a/cosmos-inference/_test/omni_param.sh b/cosmos-inference/cosmos3/_test/omni_param.sh similarity index 100% rename from cosmos-inference/_test/omni_param.sh rename to cosmos-inference/cosmos3/_test/omni_param.sh diff --git a/cosmos-inference/_test/sft.sh b/cosmos-inference/cosmos3/_test/sft.sh similarity index 100% rename from cosmos-inference/_test/sft.sh rename to cosmos-inference/cosmos3/_test/sft.sh diff --git a/cosmos-inference/action.py b/cosmos-inference/cosmos3/action.py similarity index 89% rename from cosmos-inference/action.py rename to cosmos-inference/cosmos3/action.py index 3f86c69c..678372d8 100644 --- a/cosmos-inference/action.py +++ b/cosmos-inference/cosmos3/action.py @@ -20,9 +20,8 @@ from typing import Any import torch -from typing_extensions import assert_never -from cosmos3.args import ActionMode +from cosmos3.args import ModelMode from cosmos3.vision import read_media_frames from cosmos3._src.vfm.datasets.action.domain_utils import get_domain_id from cosmos3._src.vfm.datasets.action.transforms import ( @@ -36,26 +35,24 @@ def _load_actions( action_path: Path | str | None, - action_mode: ActionMode, + model_mode: ModelMode, action_chunk_size: int, max_action_dim: int, raw_action_dim: int | None, ) -> tuple[torch.Tensor, int]: """Load actions from JSON (or zeros for policy mode and inverse dynamics mode). Returns ``(padded_action, raw_dim)``.""" - match action_mode: - case ActionMode.FORWARD_DYNAMICS: + match model_mode: + case ModelMode.FORWARD_DYNAMICS: assert action_path is not None, "action_path is required for forward_dynamics mode" p = Path(str(action_path)) raw = torch.tensor(json.loads(p.read_text()), dtype=torch.float32) raw_dim = raw.shape[-1] return pad_action_to_max_dim(raw, max_action_dim), raw_dim - case ActionMode.POLICY | ActionMode.INVERSE_DYNAMICS: + case ModelMode.POLICY | ModelMode.INVERSE_DYNAMICS: assert raw_action_dim is not None, "raw_action_dim is required for policy and inverse_dynamics modes" return torch.zeros(action_chunk_size, max_action_dim, dtype=torch.float32), raw_action_dim - case ActionMode.IMAGE2VIDEO: - assert False case _: - assert_never(action_mode) + raise ValueError(f"Unsupported action model_mode: {model_mode}") def build_action_batch( @@ -65,7 +62,7 @@ def build_action_batch( raw_action_dim: int, prompt: str, domain_name: str, - action_mode: ActionMode, + model_mode: ModelMode, action_chunk_size: int, fps: int, resolution: str | None = None, @@ -95,7 +92,7 @@ def build_action_batch( padded_image_size = pad_dict["image_size"] sequence_plan = build_sequence_plan_from_mode( - mode=action_mode, + mode=model_mode.value, video_length=target_frames, action_length=action_chunk_size, has_text=True, @@ -112,7 +109,7 @@ def build_action_batch( input_video_key: [[video_padded]] * batch_size, "action": [[action]] * batch_size, "raw_action_dim": [torch.tensor(raw_action_dim, dtype=torch.long)] * batch_size, - "mode": [action_mode.value] * batch_size, + "mode": [model_mode.value] * batch_size, "ai_caption": [ai_caption] * batch_size, "prompt": [prompt] * batch_size, "conditioning_fps": [torch.tensor(fps, dtype=torch.long)] * batch_size, @@ -128,7 +125,7 @@ def get_action_sample_data( batch_size: int, prompt: str, vision_path: Path, - action_mode: ActionMode, + model_mode: ModelMode, action_path: Path | None, domain_name: str, resolution: str, @@ -146,7 +143,7 @@ def get_action_sample_data( assert action_path is not None or raw_action_dim is not None, ( "Either action_path or raw_action_dim must be provided" ) - action, raw_action_dim = _load_actions(action_path, action_mode, action_chunk_size, max_action_dim, raw_action_dim) + action, raw_action_dim = _load_actions(action_path, model_mode, action_chunk_size, max_action_dim, raw_action_dim) return build_action_batch( video=frames, @@ -154,7 +151,7 @@ def get_action_sample_data( raw_action_dim=raw_action_dim, prompt=prompt, domain_name=domain_name, - action_mode=action_mode, + model_mode=model_mode, action_chunk_size=action_chunk_size, fps=fps, resolution=resolution, diff --git a/cosmos-inference/args.py b/cosmos-inference/cosmos3/args.py similarity index 88% rename from cosmos-inference/args.py rename to cosmos-inference/cosmos3/args.py index 0df882e7..5bacbd64 100644 --- a/cosmos-inference/args.py +++ b/cosmos-inference/cosmos3/args.py @@ -120,11 +120,6 @@ def _build_sampling(self, model_config: "OmniMoTModelConfig", sample_meta: "Samp } -class ModelVariant(StrEnum): - VFM = "vfm" - ACTION = "action" - - ModelSize = Literal["0.6B", "2B", "8B", "30B-A3B", "32B", "235B-A22B"] PromptUpsamplerVariant = Literal["8B", "32B"] @@ -141,24 +136,34 @@ class ModelMode(StrEnum): INVERSE_DYNAMICS = "inverse_dynamics" POLICY = "policy" + @property + def is_action(self) -> bool: + return self in ACTION_MODEL_MODES + + +# Image-output modes: ``num_frames`` defaults to 1 and the output is saved as a still image. +_IMAGE_OUTPUT_MODES: frozenset[ModelMode] = frozenset({ModelMode.TEXT2IMAGE, ModelMode.IMAGE2IMAGE}) + +# Modes that produce action tensors and require a model with ``action_gen=True``. +ACTION_MODEL_MODES: frozenset[ModelMode] = frozenset( + {ModelMode.FORWARD_DYNAMICS, ModelMode.INVERSE_DYNAMICS, ModelMode.POLICY} +) + class VisionMode(StrEnum): IMAGE = "image" VIDEO = "video" + @classmethod + def from_model_mode(cls, model_mode: ModelMode) -> Self: + return cls.IMAGE if model_mode in _IMAGE_OUTPUT_MODES else cls.VIDEO + class ConditionVisionMode(StrEnum): IMAGE = "image" VIDEO = "video" -class ActionMode(StrEnum): - POLICY = "policy" - FORWARD_DYNAMICS = "forward_dynamics" - INVERSE_DYNAMICS = "inverse_dynamics" - IMAGE2VIDEO = "image2video" - - class NegativeMetadataMode(StrEnum): NONE = "none" SAME = "same" @@ -226,11 +231,9 @@ class BlurTransferOverrides(TransferOverrides): class SampleMeta(pydantic.BaseModel): - model_variant: ModelVariant model_mode: ModelMode vision_mode: VisionMode condition_vision_mode: ConditionVisionMode | None - action_mode: ActionMode | None = None RESOLUTION_ADAPTER = pydantic.TypeAdapter(Resolution) @@ -293,11 +296,6 @@ def _build_text_data(self, model_config: "OmniMoTModelConfig", sample_meta: Samp class _VisionDataBase: - @property - def vision_mode(self) -> VisionMode: - self = cast(VisionDataOverrides, self) - return VisionMode.IMAGE if self.num_frames == 1 else VisionMode.VIDEO - @property def condition_vision_mode(self) -> ConditionVisionMode | None: self = cast(VisionDataOverrides, self) @@ -330,25 +328,29 @@ def duration(self) -> float: @property def vision_size(self) -> tuple[int, int]: - """Vision size (width, height) in pixels.""" + """Vision size (width, height) in pixels. + + Per the VisionDataOverrides.aspect_ratio docstring, ``None`` means + "default to 16:9 for all modes except image_edit". This property is + only reached by non-image_edit code paths (image_edit branches off in + cosmos3/inference.py:_get_image_edit_sample_data before this is + consulted), so the documented legacy default applies here. Callers + that want native aspect-ratio preservation (e.g. transfer inference) + autodetect via cosmos3.vision.read_and_resize_media before reaching + this property and never observe the fallback. + """ from cosmos3._src.vfm.datasets.utils import IMAGE_RES_SIZE_INFO, VIDEO_RES_SIZE_INFO assert self.resolution - assert self.aspect_ratio + aspect_ratio: AspectRatio = self.aspect_ratio or "16,9" if self.num_frames == 1: - return IMAGE_RES_SIZE_INFO[self.resolution][self.aspect_ratio] + return IMAGE_RES_SIZE_INFO[self.resolution][aspect_ratio] else: - return VIDEO_RES_SIZE_INFO[self.resolution][self.aspect_ratio] + return VIDEO_RES_SIZE_INFO[self.resolution][aspect_ratio] @property def vision_extension(self) -> str: - match self.vision_mode: - case VisionMode.IMAGE: - return ".jpg" - case VisionMode.VIDEO: - return ".mp4" - case _: - assert_never(self.vision_mode) + return ".jpg" if self.num_frames == 1 else ".mp4" class VisionDataOverrides(OverridesBase, _VisionDataBase): @@ -403,6 +405,10 @@ def _build_vision_data(self, model_config: "OmniMoTModelConfig", sample_meta: Sa if self.resolution is None: self.resolution = RESOLUTION_ADAPTER.validate_python(model_config.resolution) + # Image-output modes always emit a single frame; infer it so callers don't + # have to set ``num_frames=1`` in every text2image / image2image preset. + if self.num_frames is None and sample_meta.vision_mode == VisionMode.IMAGE: + self.num_frames = 1 assert self.num_frames is not None if self.fps is not None and (self.fps < 10 or self.fps > 30): log.warning(f"FPS {self.fps} is outside the recommended range [10, 30]. Quality may be degraded.") @@ -435,7 +441,6 @@ class SoundDataOverrides(OverridesBase): class ActionDataArgs(ArgsBase): - action_mode: ActionMode | None = None action_path: ResolvedFilePath | None = None domain_name: str = "" image_size: pydantic.PositiveInt = 256 @@ -446,8 +451,6 @@ class ActionDataArgs(ArgsBase): class ActionDataOverrides(OverridesBase): """Action data overrides.""" - action_mode: Training[ActionMode | None] = None - """Action mode. When set, activates Action batch construction.""" action_path: Training[ResolvedFilePathOrUrl | None] = None """Path to action JSON file. Required for forward_dynamics mode.""" domain_name: Training[str | None] = None @@ -465,21 +468,6 @@ def download(self, output_dir: Path): self.action_path = download_file(self.action_path, output_dir, "action") def _build_action_data(self, model_config: "OmniMoTModelConfig", sample_meta: SampleMeta): - if self.action_mode is not None: - match self.action_mode: - case ActionMode.IMAGE2VIDEO: - pass - case ActionMode.FORWARD_DYNAMICS: - if self.action_path is None: - raise ValueError("'action_path' is required") - case ActionMode.INVERSE_DYNAMICS | ActionMode.POLICY: - if self.raw_action_dim is None: - raise ValueError("'raw_action_dim' is required") - case _: - assert_never(self.action_mode) - if self.action_path and "://" in self.action_path: - raise ValueError("Must call `download()` before building action data") - if self.domain_name is None: self.domain_name = "" if self.image_size is None: @@ -487,40 +475,49 @@ def _build_action_data(self, model_config: "OmniMoTModelConfig", sample_meta: Sa if self.action_chunk_size is None: self.action_chunk_size = 16 + mode = sample_meta.model_mode + if not mode.is_action: + return + if not model_config.action_gen: + raise ValueError( + f"model_mode={mode.value!r} requires a model with action support " + "(model.config.action_gen=True), but the loaded checkpoint has action_gen=False" + ) + match mode: + case ModelMode.FORWARD_DYNAMICS: + if self.action_path is None: + raise ValueError(f"'action_path' is required for model_mode={mode.value!r}") + case ModelMode.INVERSE_DYNAMICS | ModelMode.POLICY: + if self.raw_action_dim is None: + raise ValueError(f"'raw_action_dim' is required for model_mode={mode.value!r}") + case _: + assert_never(mode) + + if self.action_path and "://" in self.action_path: + raise ValueError("Must call `download()` before building action data") + class _SampleDataBase: @property - def model_variant(self) -> ModelVariant: + def resolved_model_mode(self) -> ModelMode: + """Return ``model_mode`` if set, else infer the VFM modality from ``vision_path`` and ``num_frames``.""" self = cast(SampleDataOverrides, self) - if self.action_mode is not None: - return ModelVariant.ACTION - return ModelVariant.VFM - - @property - def model_mode(self) -> ModelMode: - self = cast(SampleDataOverrides, self) - match self.model_variant: - case ModelVariant.VFM: - input_mode = self.condition_vision_mode or "text" - output_mode = self.vision_mode - return ModelMode(f"{input_mode}2{output_mode}") - case ModelVariant.ACTION: - assert self.action_mode - return ModelMode(self.action_mode.value) - case _: - assert_never(self.model_variant) + if self.model_mode is not None: + return self.model_mode + input_mode = self.condition_vision_mode.value if self.condition_vision_mode else "text" + output_mode = VisionMode.IMAGE.value if self.num_frames == 1 else VisionMode.VIDEO.value + return ModelMode(f"{input_mode}2{output_mode}") @property def sample_meta(self) -> SampleMeta: self = cast(SampleDataOverrides, self) + mode = self.resolved_model_mode return SampleMeta( - model_variant=self.model_variant, - model_mode=self.model_mode, - vision_mode=self.vision_mode, + model_mode=mode, + vision_mode=VisionMode.from_model_mode(mode), condition_vision_mode=self.condition_vision_mode, - action_mode=self.action_mode, ) @@ -530,7 +527,8 @@ class SampleDataArgs( VisionDataArgs, SoundDataArgs, ActionDataArgs, -): ... +): + model_mode: ModelMode class SampleDataOverrides( @@ -542,6 +540,10 @@ class SampleDataOverrides( ): """Sample data arguments for 'OmniMoTModel.generate_samples'.""" + model_mode: ModelMode | None = None + """Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and + ``num_frames``; action modes must be set explicitly.""" + class OmniSampleArgs(SampleArgs, SamplingArgs, SampleDataArgs): ... @@ -587,6 +589,7 @@ def build_sample(self, *, model_config: Any) -> OmniSampleArgs: merged = type(self).model_validate(merged_data) self.__dict__.update(merged.__dict__) + self.model_mode = sample_meta.model_mode self._build_sample() self._build_sampling(model_config=model_config, sample_meta=sample_meta) diff --git a/cosmos-inference/args_test.py b/cosmos-inference/cosmos3/args_test.py similarity index 100% rename from cosmos-inference/args_test.py rename to cosmos-inference/cosmos3/args_test.py diff --git a/cosmos-inference/common/__init__.py b/cosmos-inference/cosmos3/common/__init__.py similarity index 100% rename from cosmos-inference/common/__init__.py rename to cosmos-inference/cosmos3/common/__init__.py diff --git a/cosmos-inference/common/args.py b/cosmos-inference/cosmos3/common/args.py similarity index 97% rename from cosmos-inference/common/args.py rename to cosmos-inference/cosmos3/common/args.py index eb3809c9..909f1f9a 100644 --- a/cosmos-inference/common/args.py +++ b/cosmos-inference/cosmos3/common/args.py @@ -597,6 +597,7 @@ class GuardrailOverrides(OverridesBase): class SetupArgs(ABC, CheckpointArgs, ParallelismArgs, GuardrailArgs): output_dir: ResolvedPath keep_going: bool + skip_invalid_samples: bool debug: bool profile: bool benchmark: bool @@ -635,6 +636,10 @@ class SetupOverrides(ABC, CheckpointOverrides, ParallelismOverrides, GuardrailOv """Output directory.""" keep_going: bool = False """If True, catch and log errors instead of raising them.""" + skip_invalid_samples: bool = False + """If True, skip samples whose modality (e.g. action, sound) is not supported by the + loaded model and emit a ``status='skip'`` output instead of raising. Useful for tests + and examples that exercise multiple modalities against checkpoints with varying support.""" debug: bool = False """If True, enable debug outputs.""" profile: bool = False @@ -838,10 +843,11 @@ class SampleOutputs(ArgsBase): args: dict """Sample arguments.""" - status: Literal["success", "error"] = "success" - """Generation status.""" + status: Literal["success", "error", "skip"] = "success" + """Generation status. ``skip`` indicates the sample was bypassed because the loaded + model does not support the requested modality (e.g. action/sound).""" message: str | None = None - """Generation error message.""" + """Generation error or skip reason message.""" stack_trace: str | None = None """Generation error stack trace.""" diff --git a/cosmos-inference/common/args_test.py b/cosmos-inference/cosmos3/common/args_test.py similarity index 100% rename from cosmos-inference/common/args_test.py rename to cosmos-inference/cosmos3/common/args_test.py diff --git a/cosmos-inference/common/checkpoints.py b/cosmos-inference/cosmos3/common/checkpoints.py similarity index 100% rename from cosmos-inference/common/checkpoints.py rename to cosmos-inference/cosmos3/common/checkpoints.py diff --git a/cosmos-inference/common/config.py b/cosmos-inference/cosmos3/common/config.py similarity index 100% rename from cosmos-inference/common/config.py rename to cosmos-inference/cosmos3/common/config.py diff --git a/cosmos-inference/common/config_test.py b/cosmos-inference/cosmos3/common/config_test.py similarity index 100% rename from cosmos-inference/common/config_test.py rename to cosmos-inference/cosmos3/common/config_test.py diff --git a/cosmos-inference/common/inference.py b/cosmos-inference/cosmos3/common/inference.py similarity index 100% rename from cosmos-inference/common/inference.py rename to cosmos-inference/cosmos3/common/inference.py diff --git a/cosmos-inference/common/inference_test.py b/cosmos-inference/cosmos3/common/inference_test.py similarity index 100% rename from cosmos-inference/common/inference_test.py rename to cosmos-inference/cosmos3/common/inference_test.py diff --git a/cosmos-inference/common/init.py b/cosmos-inference/cosmos3/common/init.py similarity index 100% rename from cosmos-inference/common/init.py rename to cosmos-inference/cosmos3/common/init.py diff --git a/cosmos-inference/configs/experiment/action_policy_sft_8b.yaml b/cosmos-inference/cosmos3/configs/experiment/action_policy_sft_8b.yaml similarity index 100% rename from cosmos-inference/configs/experiment/action_policy_sft_8b.yaml rename to cosmos-inference/cosmos3/configs/experiment/action_policy_sft_8b.yaml diff --git a/cosmos-inference/configs/experiment/mixed_modality_sft_8b.yaml b/cosmos-inference/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml similarity index 100% rename from cosmos-inference/configs/experiment/mixed_modality_sft_8b.yaml rename to cosmos-inference/cosmos3/configs/experiment/mixed_modality_sft_8b.yaml diff --git a/cosmos-inference/dataset_samples.py b/cosmos-inference/cosmos3/dataset_samples.py similarity index 100% rename from cosmos-inference/dataset_samples.py rename to cosmos-inference/cosmos3/dataset_samples.py diff --git a/cosmos-inference/defaults/forward_dynamics/sample_args.json b/cosmos-inference/cosmos3/defaults/forward_dynamics/sample_args.json similarity index 100% rename from cosmos-inference/defaults/forward_dynamics/sample_args.json rename to cosmos-inference/cosmos3/defaults/forward_dynamics/sample_args.json diff --git a/cosmos-inference/defaults/image2video/sample_args.json b/cosmos-inference/cosmos3/defaults/image2video/sample_args.json similarity index 100% rename from cosmos-inference/defaults/image2video/sample_args.json rename to cosmos-inference/cosmos3/defaults/image2video/sample_args.json diff --git a/cosmos-inference/defaults/inverse_dynamics/sample_args.json b/cosmos-inference/cosmos3/defaults/inverse_dynamics/sample_args.json similarity index 100% rename from cosmos-inference/defaults/inverse_dynamics/sample_args.json rename to cosmos-inference/cosmos3/defaults/inverse_dynamics/sample_args.json diff --git a/cosmos-inference/defaults/policy/sample_args.json b/cosmos-inference/cosmos3/defaults/policy/sample_args.json similarity index 100% rename from cosmos-inference/defaults/policy/sample_args.json rename to cosmos-inference/cosmos3/defaults/policy/sample_args.json diff --git a/cosmos-inference/defaults/prompt_upsampler.txt b/cosmos-inference/cosmos3/defaults/prompt_upsampler.txt similarity index 100% rename from cosmos-inference/defaults/prompt_upsampler.txt rename to cosmos-inference/cosmos3/defaults/prompt_upsampler.txt diff --git a/cosmos-inference/defaults/text2image/sample_args.json b/cosmos-inference/cosmos3/defaults/text2image/sample_args.json similarity index 97% rename from cosmos-inference/defaults/text2image/sample_args.json rename to cosmos-inference/cosmos3/defaults/text2image/sample_args.json index 7ba35c2c..0b495097 100644 --- a/cosmos-inference/defaults/text2image/sample_args.json +++ b/cosmos-inference/cosmos3/defaults/text2image/sample_args.json @@ -14,7 +14,6 @@ "negative_prompt_keep_metadata": true, "aspect_ratio": "16,9", "fps": 24, - "num_frames": 1, "video_save_quality": 10, "image_save_quality": 95, "enable_sound": false diff --git a/cosmos-inference/defaults/text2video/sample_args.json b/cosmos-inference/cosmos3/defaults/text2video/sample_args.json similarity index 100% rename from cosmos-inference/defaults/text2video/sample_args.json rename to cosmos-inference/cosmos3/defaults/text2video/sample_args.json diff --git a/cosmos-inference/defaults/video2video/sample_args.json b/cosmos-inference/cosmos3/defaults/video2video/sample_args.json similarity index 96% rename from cosmos-inference/defaults/video2video/sample_args.json rename to cosmos-inference/cosmos3/defaults/video2video/sample_args.json index 80699c38..5537f245 100644 --- a/cosmos-inference/defaults/video2video/sample_args.json +++ b/cosmos-inference/cosmos3/defaults/video2video/sample_args.json @@ -12,7 +12,6 @@ "inverse_duration_template": "The video is not {duration:.1f} seconds long and is not of {fps:.0f} FPS.", "inverse_resolution_template": "This video is not of {height}x{width} resolution.", "negative_prompt_keep_metadata": true, - "aspect_ratio": "16,9", "fps": 24, "num_frames": 189, "video_save_quality": 10, diff --git a/cosmos-inference/defaults/video_captioner.txt b/cosmos-inference/cosmos3/defaults/video_captioner.txt similarity index 100% rename from cosmos-inference/defaults/video_captioner.txt rename to cosmos-inference/cosmos3/defaults/video_captioner.txt diff --git a/cosmos-inference/fixtures/__init__.py b/cosmos-inference/cosmos3/fixtures/__init__.py similarity index 100% rename from cosmos-inference/fixtures/__init__.py rename to cosmos-inference/cosmos3/fixtures/__init__.py diff --git a/cosmos-inference/fixtures/args.py b/cosmos-inference/cosmos3/fixtures/args.py similarity index 100% rename from cosmos-inference/fixtures/args.py rename to cosmos-inference/cosmos3/fixtures/args.py diff --git a/cosmos-inference/fixtures/script.py b/cosmos-inference/cosmos3/fixtures/script.py similarity index 100% rename from cosmos-inference/fixtures/script.py rename to cosmos-inference/cosmos3/fixtures/script.py diff --git a/cosmos-inference/flags.py b/cosmos-inference/cosmos3/flags.py similarity index 100% rename from cosmos-inference/flags.py rename to cosmos-inference/cosmos3/flags.py diff --git a/cosmos-inference/inference.py b/cosmos-inference/cosmos3/inference.py similarity index 97% rename from cosmos-inference/inference.py rename to cosmos-inference/cosmos3/inference.py index a0f4b78c..53ba2dbe 100644 --- a/cosmos-inference/inference.py +++ b/cosmos-inference/cosmos3/inference.py @@ -29,10 +29,9 @@ from PIL import Image from torch.utils._pytree import tree_map_only from torch.utils.data import Dataset -from typing_extensions import Self, assert_never +from typing_extensions import Self from cosmos3.args import ( - ActionMode, ModelMode, NegativeMetadataMode, OmniSampleArgs, @@ -312,33 +311,28 @@ def get_sample_data( ) -> dict: """Create a sample batch for generation.""" - match sample_args.action_mode: - case ActionMode.FORWARD_DYNAMICS | ActionMode.POLICY | ActionMode.INVERSE_DYNAMICS: - from cosmos3.action import get_action_sample_data - - assert sample_args.vision_path is not None - return get_action_sample_data( - model_config=model, - batch_size=sample_args.num_outputs, - prompt=sample_args.prompt, - vision_path=sample_args.vision_path, - action_mode=sample_args.action_mode, - action_path=sample_args.action_path, - domain_name=sample_args.domain_name, - resolution=str(sample_args.image_size), - aspect_ratio=sample_args.aspect_ratio, - action_chunk_size=sample_args.action_chunk_size, - max_action_dim=model.config.max_action_dim, - raw_action_dim=sample_args.raw_action_dim, - duration_template=sample_args.duration_template, - resolution_template=sample_args.resolution_template, - fps=sample_args.fps, - device=device, - ) - case ActionMode.IMAGE2VIDEO | None: - pass - case _: - assert_never(sample_args.action_mode) + if sample_args.model_mode.is_action: + from cosmos3.action import get_action_sample_data + + assert sample_args.vision_path is not None + return get_action_sample_data( + model_config=model, + batch_size=sample_args.num_outputs, + prompt=sample_args.prompt, + vision_path=sample_args.vision_path, + model_mode=sample_args.model_mode, + action_path=sample_args.action_path, + domain_name=sample_args.domain_name, + resolution=str(sample_args.image_size), + aspect_ratio=sample_args.aspect_ratio, + action_chunk_size=sample_args.action_chunk_size, + max_action_dim=model.config.max_action_dim, + raw_action_dim=sample_args.raw_action_dim, + duration_template=sample_args.duration_template, + resolution_template=sample_args.resolution_template, + fps=sample_args.fps, + device=device, + ) if sample_args.model_mode == ModelMode.IMAGE2IMAGE: diff --git a/cosmos-inference/model.py b/cosmos-inference/cosmos3/model.py similarity index 100% rename from cosmos-inference/model.py rename to cosmos-inference/cosmos3/model.py diff --git a/cosmos-inference/model_test.py b/cosmos-inference/cosmos3/model_test.py similarity index 100% rename from cosmos-inference/model_test.py rename to cosmos-inference/cosmos3/model_test.py diff --git a/cosmos-inference/ray/__init__.py b/cosmos-inference/cosmos3/ray/__init__.py similarity index 100% rename from cosmos-inference/ray/__init__.py rename to cosmos-inference/cosmos3/ray/__init__.py diff --git a/cosmos-inference/ray/configs/latency.yaml b/cosmos-inference/cosmos3/ray/configs/latency.yaml similarity index 100% rename from cosmos-inference/ray/configs/latency.yaml rename to cosmos-inference/cosmos3/ray/configs/latency.yaml diff --git a/cosmos-inference/ray/configs/throughput.yaml b/cosmos-inference/cosmos3/ray/configs/throughput.yaml similarity index 100% rename from cosmos-inference/ray/configs/throughput.yaml rename to cosmos-inference/cosmos3/ray/configs/throughput.yaml diff --git a/cosmos-inference/ray/gradio.py b/cosmos-inference/cosmos3/ray/gradio.py similarity index 100% rename from cosmos-inference/ray/gradio.py rename to cosmos-inference/cosmos3/ray/gradio.py diff --git a/cosmos-inference/ray/serve.py b/cosmos-inference/cosmos3/ray/serve.py similarity index 100% rename from cosmos-inference/ray/serve.py rename to cosmos-inference/cosmos3/ray/serve.py diff --git a/cosmos-inference/ray/serve_test.py b/cosmos-inference/cosmos3/ray/serve_test.py similarity index 100% rename from cosmos-inference/ray/serve_test.py rename to cosmos-inference/cosmos3/ray/serve_test.py diff --git a/cosmos-inference/ray/submit.py b/cosmos-inference/cosmos3/ray/submit.py similarity index 100% rename from cosmos-inference/ray/submit.py rename to cosmos-inference/cosmos3/ray/submit.py diff --git a/cosmos-inference/scripts/__init__.py b/cosmos-inference/cosmos3/scripts/__init__.py similarity index 100% rename from cosmos-inference/scripts/__init__.py rename to cosmos-inference/cosmos3/scripts/__init__.py diff --git a/cosmos-inference/scripts/_test.py b/cosmos-inference/cosmos3/scripts/_test.py similarity index 100% rename from cosmos-inference/scripts/_test.py rename to cosmos-inference/cosmos3/scripts/_test.py diff --git a/cosmos-inference/scripts/_test/convert_model_to_dcp.sh b/cosmos-inference/cosmos3/scripts/_test/convert_model_to_dcp.sh similarity index 100% rename from cosmos-inference/scripts/_test/convert_model_to_dcp.sh rename to cosmos-inference/cosmos3/scripts/_test/convert_model_to_dcp.sh diff --git a/cosmos-inference/scripts/_test/convert_model_to_diffusers.sh b/cosmos-inference/cosmos3/scripts/_test/convert_model_to_diffusers.sh similarity index 100% rename from cosmos-inference/scripts/_test/convert_model_to_diffusers.sh rename to cosmos-inference/cosmos3/scripts/_test/convert_model_to_diffusers.sh diff --git a/cosmos-inference/scripts/_test/export_config.sh b/cosmos-inference/cosmos3/scripts/_test/export_config.sh similarity index 100% rename from cosmos-inference/scripts/_test/export_config.sh rename to cosmos-inference/cosmos3/scripts/_test/export_config.sh diff --git a/cosmos-inference/scripts/_test/export_model.sh b/cosmos-inference/cosmos3/scripts/_test/export_model.sh similarity index 100% rename from cosmos-inference/scripts/_test/export_model.sh rename to cosmos-inference/cosmos3/scripts/_test/export_model.sh diff --git a/cosmos-inference/scripts/action_policy_server.py b/cosmos-inference/cosmos3/scripts/action_policy_server.py similarity index 100% rename from cosmos-inference/scripts/action_policy_server.py rename to cosmos-inference/cosmos3/scripts/action_policy_server.py diff --git a/cosmos-inference/scripts/caption_from_video.py b/cosmos-inference/cosmos3/scripts/caption_from_video.py similarity index 100% rename from cosmos-inference/scripts/caption_from_video.py rename to cosmos-inference/cosmos3/scripts/caption_from_video.py diff --git a/cosmos-inference/scripts/captions_to_sft_jsonl.py b/cosmos-inference/cosmos3/scripts/captions_to_sft_jsonl.py similarity index 100% rename from cosmos-inference/scripts/captions_to_sft_jsonl.py rename to cosmos-inference/cosmos3/scripts/captions_to_sft_jsonl.py diff --git a/cosmos-inference/scripts/convert_model_to_dcp.py b/cosmos-inference/cosmos3/scripts/convert_model_to_dcp.py similarity index 100% rename from cosmos-inference/scripts/convert_model_to_dcp.py rename to cosmos-inference/cosmos3/scripts/convert_model_to_dcp.py diff --git a/cosmos-inference/cosmos3/scripts/eval.py b/cosmos-inference/cosmos3/scripts/eval.py new file mode 100644 index 00000000..6cbe17e4 --- /dev/null +++ b/cosmos-inference/cosmos3/scripts/eval.py @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Evaluation entrypoint for (Action) models with local dataset.""" + +from cosmos3.common.init import init_script, is_rank0 + +init_script() + +import json + +import pydantic +import torch +import tyro + +from cosmos3.args import OmniSetupOverrides +from cosmos3.common.args import SampleOutputs, SetupOverrides, tyro_cli +from cosmos3.common.init import init_output_dir +from cosmos3.dataset import DatasetArgs, create_dataset +from cosmos3.scripts.eval_utils import aggregate_metrics, compute_sample_metrics, extract_gt_action, extract_gt_video +from cosmos3._src.imaginaire.utils import log + + +class EvalArgs(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid", use_attribute_docstrings=True) + + setup: SetupOverrides = OmniSetupOverrides.model_construct() + """Model and parallelism configuration.""" + dataset: DatasetArgs = DatasetArgs() + """Dataset loading configuration.""" + compute_metrics: bool = True + """Compute per-sample metrics and write metrics.json sidecars + metrics_aggregate.json.""" + + +def eval_dataset(args: EvalArgs) -> list[SampleOutputs]: + """Run dataset inference: load dataset in memory, run inference, save outputs.""" + if args.setup.output_dir is None: + raise ValueError("'output_dir' is required") + + setup = args.setup.build_setup() + init_output_dir(setup.output_dir) + log.debug(f"{args.__class__.__name__}({args})") + + samples = create_dataset( + args.dataset, + config_args=setup, + ) + log.info(f"Loaded {len(samples)} samples in memory") + + pipe = setup.get_inference_cls().create(setup) + + output_dir = setup.output_dir + all_outputs: list[SampleOutputs] = [] + for i, (sample_args, data_batch) in enumerate(samples): + assert sample_args.name + sample_args.output_dir = output_dir / sample_args.name + sample_args = sample_args.build_sample(model_config=pipe.model_config) + log.info(f"[{i + 1}/{len(samples)}] Processing: {sample_args.name}") + + gt_video: torch.Tensor | None = None + gt_action: torch.Tensor | None = None + if args.compute_metrics: + gt_video = extract_gt_video(data_batch) + gt_action = extract_gt_action(data_batch) + + batch_outputs = pipe.generate_batch([sample_args], data_batch) + all_outputs.extend(batch_outputs) + + if args.compute_metrics and batch_outputs: + sample_output = batch_outputs[0] + if sample_output.status == "success": + metrics = compute_sample_metrics( + sample_args.name, + gt_video, + gt_action, + sample_output, + sample_args.output_dir, + sample_args.vision_extension, + ) + (sample_args.output_dir / "metrics.json").write_text(json.dumps(metrics, indent=2, sort_keys=True)) + log.info(f"Metrics for {sample_args.name}: {metrics}") + + if setup.benchmark and is_rank0(): + benchmark_file = output_dir / "benchmark.json" + benchmark_file.write_text(json.dumps(pipe.get_timer_results(), indent=2, sort_keys=True)) + log.success(f"Saved benchmark to '{benchmark_file}'") + + if args.compute_metrics and is_rank0(): + aggregate = aggregate_metrics(output_dir) + aggregate_file = output_dir / "metrics_aggregate.json" + aggregate_file.write_text(json.dumps(aggregate, indent=2, sort_keys=True)) + log.success(f"Saved aggregated metrics to '{aggregate_file}'") + + return all_outputs + + +def main() -> None: + args = tyro_cli( + EvalArgs, + description=__doc__, + config=( + tyro.conf.OmitArgPrefixes, + tyro.conf.CascadeSubcommandArgs, + tyro.conf.OmitSubcommandPrefixes, + ), + ) + eval_dataset(args) + + +if __name__ == "__main__": + main() diff --git a/cosmos-inference/scripts/eval_utils.py b/cosmos-inference/cosmos3/scripts/eval_utils.py similarity index 100% rename from cosmos-inference/scripts/eval_utils.py rename to cosmos-inference/cosmos3/scripts/eval_utils.py diff --git a/cosmos-inference/scripts/export_config.py b/cosmos-inference/cosmos3/scripts/export_config.py similarity index 100% rename from cosmos-inference/scripts/export_config.py rename to cosmos-inference/cosmos3/scripts/export_config.py diff --git a/cosmos-inference/scripts/export_model.py b/cosmos-inference/cosmos3/scripts/export_model.py similarity index 100% rename from cosmos-inference/scripts/export_model.py rename to cosmos-inference/cosmos3/scripts/export_model.py diff --git a/cosmos-inference/scripts/export_schemas.py b/cosmos-inference/cosmos3/scripts/export_schemas.py similarity index 100% rename from cosmos-inference/scripts/export_schemas.py rename to cosmos-inference/cosmos3/scripts/export_schemas.py diff --git a/cosmos-inference/scripts/export_schemas_test.py b/cosmos-inference/cosmos3/scripts/export_schemas_test.py similarity index 100% rename from cosmos-inference/scripts/export_schemas_test.py rename to cosmos-inference/cosmos3/scripts/export_schemas_test.py diff --git a/cosmos-inference/scripts/hydra.py b/cosmos-inference/cosmos3/scripts/hydra.py similarity index 100% rename from cosmos-inference/scripts/hydra.py rename to cosmos-inference/cosmos3/scripts/hydra.py diff --git a/cosmos-inference/scripts/inference.py b/cosmos-inference/cosmos3/scripts/inference.py similarity index 80% rename from cosmos-inference/scripts/inference.py rename to cosmos-inference/cosmos3/scripts/inference.py index 7ca018c9..d525beb5 100644 --- a/cosmos-inference/scripts/inference.py +++ b/cosmos-inference/cosmos3/scripts/inference.py @@ -25,7 +25,7 @@ import tyro from cosmos3.args import OmniSetupOverrides -from cosmos3.common.args import SetupOverrides, tyro_cli +from cosmos3.common.args import SampleOutputs, SetupOverrides, tyro_cli from cosmos3.common.init import init_output_dir from cosmos3._src.imaginaire.utils import log @@ -62,7 +62,18 @@ def inference(args: InferenceArgs): sample_overrides.download(sample_overrides.output_dir / "inputs") pipe = setup_args.get_inference_cls().create(setup_args) - sample_args_list = [overrides.build_sample(model_config=pipe.model_config) for overrides in sample_overrides_list] + sample_args_list = [] + for overrides in sample_overrides_list: + try: + sample_args_list.append(overrides.build_sample(model_config=pipe.model_config)) + except ValueError as e: + if not setup_args.skip_invalid_samples: + raise + msg = f"Skipping sample '{overrides.name}': {e}" + log.warning(msg) + overrides.output_dir.mkdir(parents=True, exist_ok=True) + skip_output = SampleOutputs(args=overrides.model_dump(mode="json"), status="skip", message=msg) + (overrides.output_dir / "sample_outputs.json").write_text(skip_output.model_dump_json()) pipe.generate(sample_args_list) if setup_args.benchmark and is_rank0(): diff --git a/cosmos-inference/scripts/prefetch_hf_checkpoints.py b/cosmos-inference/cosmos3/scripts/prefetch_hf_checkpoints.py similarity index 100% rename from cosmos-inference/scripts/prefetch_hf_checkpoints.py rename to cosmos-inference/cosmos3/scripts/prefetch_hf_checkpoints.py diff --git a/cosmos-inference/scripts/train.py b/cosmos-inference/cosmos3/scripts/train.py similarity index 100% rename from cosmos-inference/scripts/train.py rename to cosmos-inference/cosmos3/scripts/train.py diff --git a/cosmos-inference/scripts/upsample_prompts.py b/cosmos-inference/cosmos3/scripts/upsample_prompts.py similarity index 100% rename from cosmos-inference/scripts/upsample_prompts.py rename to cosmos-inference/cosmos3/scripts/upsample_prompts.py diff --git a/cosmos-inference/vision.py b/cosmos-inference/cosmos3/vision.py similarity index 100% rename from cosmos-inference/vision.py rename to cosmos-inference/cosmos3/vision.py diff --git a/cosmos-inference/docs/inference.md b/cosmos-inference/docs/inference.md index 1677d847..5fef742b 100644 --- a/cosmos-inference/docs/inference.md +++ b/cosmos-inference/docs/inference.md @@ -68,10 +68,11 @@ python -m cosmos3.scripts.inference \ General sample arguments: -| Argument | Description | -| -------- | ---------------------------------------------- | -| `name` | Output subfolder name (inside `--output-dir`). | -| `seed` | Random seed for reproducibility. | +| Argument | Description | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `model_mode` | Generation modality. One of `text2image`, `text2video`, `image2image`, `image2video`, `video2video`, `forward_dynamics`, `inverse_dynamics`, `policy`. Also selects the matching default preset. When omitted, the VFM modality is inferred from `vision_path` and `num_frames`; action modes must be set explicitly. | +| `name` | Output subfolder name (inside `--output-dir`). | +| `seed` | Random seed for reproducibility. | ### Condition @@ -99,11 +100,11 @@ Provide an image or video via `vision_path`. #### Action -Action inference is enabled by setting `action_mode` in the sample argument file. The examples live in [`inputs/omni/`](../inputs/omni/). +Action inference is enabled by setting `model_mode` to one of the action tasks in the sample argument file. The examples live in [`inputs/omni/`](../inputs/omni/). | Argument | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| `action_mode` | Selects the action task: `forward_dynamics`, `inverse_dynamics`, or `policy`. This also selects the matching default preset. | +| `model_mode` | Selects the action task: `forward_dynamics`, `inverse_dynamics`, or `policy`. This also selects the matching default preset. | | `vision_path` | Observation image or video. URLs are downloaded into the sample output's `inputs/` directory before generation. | | `prompt` | Text instruction or scene/task description used as the action sample caption. | | `domain_name` | Domain name passed to the action domain registry, such as `libero` or `av`. Use the domain used by the checkpoint's action training data. | diff --git a/cosmos-inference/docs/training.md b/cosmos-inference/docs/training.md index 7ef398da..c525a6aa 100644 --- a/cosmos-inference/docs/training.md +++ b/cosmos-inference/docs/training.md @@ -20,6 +20,8 @@ ______________________________________________________________________ - [Run inference with trained checkpoint](#run-inference-with-trained-checkpoint) - [Result Comparison](#result-comparison) - [Export checkpoint to Hugging Face safetensors](#export-checkpoint-to-hugging-face-safetensors) +- [Evaluation](#evaluation) + - [Run evaluation with trained checkpoint](#run-evaluation-with-trained-checkpoint) - [Outputs](#outputs) - [Video Captioning for Training Data Processing](#video-captioning-for-training-data-processing) - [Server setup](#server-setup) @@ -34,6 +36,7 @@ Fine-tune a pre-trained Cosmos3 model on your own video dataset using supervised 1. **Training** (4 steps): prepare data, prepare the checkpoint, prepare the config, and launch distributed training with `torchrun`. 2. **Inference** (2 steps): run inference with the trained checkpoint, and optionally export it to Hugging Face safetensors. +3. **Evaluation** (1 step): run evaluation with the trained checkpoint. Currently supported modalities are Forward Dynamics, Inverse Dynamics, and Policy. This guide was tested on the following environments: @@ -172,7 +175,7 @@ CHECKPOINT_PATH=outputs/train/job/checkpoints/$CHECKPOINT_ITER Run Text2Video (T2V), Image2Video (I2V), and Video2Video (V2V) inference for a single clip with the output checkpoint: ```shell -torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ +COSMOS_TRAINING=1 torchrun --nproc-per-node=8 -m cosmos3.scripts.inference \ --parallelism-preset=latency \ -i "$DATASET_PATH/val/inference_prompt*/episode_049683_clip000.json" \ -o outputs/train_inference \ @@ -226,6 +229,30 @@ torchrun -m cosmos3.scripts.export_model \ -o outputs/train/model ``` +## Evaluation + +Supported modalities: Forward Dynamics, Inverse Dynamics, Policy. + +`cosmos3.scripts.eval` scores checkpoints on a held-out dataset — `psnr` for predicted video, `action_mse` for predicted action. + +### Run evaluation with trained checkpoint + +```shell +torchrun --nproc-per-node=8 -m cosmos3.scripts.eval \ + -o outputs/train_eval \ + --checkpoint-path $CHECKPOINT_PATH \ + --config-file outputs/train/config.yaml \ + --root-override /path/to/eval/dataset +``` + +- `--config-file` reuses the training dataloader (`val` split, falling back to `dataloader_train`). +- `--root-override` swaps the dataset's baked-in `root` for a local path. Alternatives: `--gcs-root-override ` + `--cache-dir` to download, or `--dataset ` to pick one entry from a multi-dataset config. +- `--model-mode` defaults to `joint` (every entry evaluated under all three modes); pass a single mode (e.g. `forward_dynamics`) to evaluate only that mode. +- `--num-samples N` caps the number of dataset entries evaluated; `--sample-stride K` evaluates every K-th entry (defaults to `1`, i.e. every entry). +- For a quick debug run, shrink the workload with `--num-samples N`, `--sample-stride K`, or `--model-mode `. +- Per-sample scores land in `outputs/train_eval////metrics.json`; rank-0 aggregate is `outputs/train_eval/metrics_aggregate.json`. +- Optional flags: `--no-compute-metrics` (generation only), `--benchmark` (per-stage timings), `--debug` (raw tensors + pickled debug data). + ## Outputs The training output directory contains: @@ -238,13 +265,15 @@ The training output directory contains: 1. `iter_/`: DCP checkpoints saved every `{checkpoint.save_iter}` iterations. 1. `/`: Callback outputs. -| Artifact | Path | -| ------------------------------------------------------- | ---------------------------------------- | -| Example dataset | `$DATASET_PATH` | -| Base checkpoint (DCP) | `$BASE_CHECKPOINT_PATH` | -| Trained checkpoint (DCP) | `outputs/train/job/checkpoints/iter_` | -| Trained checkpoint (Hugging Face safetensors, optional) | `outputs/train/model` | -| Inference video from trained model | `outputs/train_inference` | +| Artifact | Path | +| --------------------------------------------------------- | ----------------------------------------------- | +| Example dataset | `$DATASET_PATH` | +| Base checkpoint (DCP) | `$BASE_CHECKPOINT_PATH` | +| Trained checkpoint (DCP) | `outputs/train/job/checkpoints/iter_` | +| Trained checkpoint (Hugging Face safetensors, optional) | `outputs/train/model` | +| Inference video from trained model | `outputs/train_inference` | +| Action evaluation per-sample outputs (action checkpoints) | `outputs/train_eval////` | +| Action evaluation aggregate metrics (action checkpoints) | `outputs/train_eval/metrics_aggregate.json` | ______________________________________________________________________ diff --git a/cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json index ab9f3799..3054fdd6 100644 --- a/cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json +++ b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_0.json @@ -1,5 +1,5 @@ { - "action_mode": "forward_dynamics", + "model_mode": "forward_dynamics", "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/mountain_720.png", "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/camera_action_44.json", "image_size": 480, diff --git a/cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json index 8cac7e4d..333e1e8c 100644 --- a/cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json +++ b/cosmos-inference/inputs/omni/action_forward_dynamics_camera_1.json @@ -1,5 +1,5 @@ { - "action_mode": "forward_dynamics", + "model_mode": "forward_dynamics", "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/solar_720.png", "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/camera_action_44.json", "image_size": 480, diff --git a/cosmos-inference/inputs/omni/action_forward_dynamics_robot.json b/cosmos-inference/inputs/omni/action_forward_dynamics_robot.json index c925f85b..e2f180b2 100644 --- a/cosmos-inference/inputs/omni/action_forward_dynamics_robot.json +++ b/cosmos-inference/inputs/omni/action_forward_dynamics_robot.json @@ -1,5 +1,5 @@ { - "action_mode": "forward_dynamics", + "model_mode": "forward_dynamics", "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.mp4", "action_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.json", "image_size": 480, diff --git a/cosmos-inference/inputs/omni/action_inverse_dynamics_av.json b/cosmos-inference/inputs/omni/action_inverse_dynamics_av.json index 19d9b495..1c18429d 100644 --- a/cosmos-inference/inputs/omni/action_inverse_dynamics_av.json +++ b/cosmos-inference/inputs/omni/action_inverse_dynamics_av.json @@ -1,5 +1,5 @@ { - "action_mode": "inverse_dynamics", + "model_mode": "inverse_dynamics", "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/av_vision_25_73d01c91-51f0-46cf-9b76-5682a76fb349.mp4", "image_size": 480, "fps": 10, diff --git a/cosmos-inference/inputs/omni/action_policy_robot.json b/cosmos-inference/inputs/omni/action_policy_robot.json index 7ab7e4bb..fda87a55 100644 --- a/cosmos-inference/inputs/omni/action_policy_robot.json +++ b/cosmos-inference/inputs/omni/action_policy_robot.json @@ -1,5 +1,5 @@ { - "action_mode": "policy", + "model_mode": "policy", "vision_path": "https://github.com/nvidia-cosmos/cosmos-dependencies/raw/refs/heads/assets/cosmos3/inputs/action/bridge_0.mp4", "image_size": 480, "fps": 5, diff --git a/cosmos-inference/packages/transformers-cosmos3/README.md b/cosmos-inference/packages/transformers-cosmos3/README.md new file mode 100644 index 00000000..61e95f06 --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/README.md @@ -0,0 +1,16 @@ +# Cosmos3 transformers Plugin + +Run example inference: + +```shell +cd packages/transformers-cosmos3 +./scripts/inference_example.py +``` + +See [Cosmos-Reason2](https://github.com/nvidia-cosmos/cosmos-reason2) for more details. + +For more details, see: + +- [Cosmos-Reason2 repository](https://github.com/nvidia-cosmos/cosmos-reason2) +- [Qwen3-VL repository](https://github.com/QwenLM/Qwen3-VL#using--transformers-to-chat) +- [Qwen3-VL transformers](https://huggingface.co/docs/transformers/en/model_doc/qwen3_vl) diff --git a/cosmos-inference/packages/transformers-cosmos3/pyproject.toml b/cosmos-inference/packages/transformers-cosmos3/pyproject.toml new file mode 100644 index 00000000..daadfa0a --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/pyproject.toml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "transformers-cosmos3" +version = "0.1.0" +description = "transformers plugin that loads Cosmos3 checkpoints" +requires-python = ">=3.10" +dependencies = ["transformers>=4.57"] diff --git a/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py b/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py new file mode 100755 index 00000000..a7f2987c --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py @@ -0,0 +1,96 @@ +#!/usr/bin/env -S uv run --script +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "accelerate==1.13.0", +# "torch==2.11.0", +# "torchvision", +# "transformers==5.8.0", +# "transformers-cosmos3", +# ] +# [tool.uv.sources] +# transformers-cosmos3 = { path = "../", editable = true } +# /// + +"""Minimal example of inference with Cosmos-Reason2.""" + +# Source: https://github.com/QwenLM/Qwen3-VL?tab=readme-ov-file#new-qwen-vl-utils-usage + +import warnings + +warnings.filterwarnings("ignore") + + +import torch +import transformers +from transformers import AutoProcessor +from transformers_cosmos3 import Cosmos3ForConditionalGeneration + +SEPARATOR = "-" * 20 + + +def main(): + # Ensure reproducibility + transformers.set_seed(0) + + # Load model + model_name = "nvidia/Cosmos3-Nano" + model = Cosmos3ForConditionalGeneration.from_pretrained( + model_name, trust_remote_code=True, dtype=torch.float16, device_map="auto", attn_implementation="sdpa" + ) + processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) + + # Create inputs + conversation = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Give me a short introduction to large language model."}, + ], + }, + ] + + # Process inputs + inputs = processor.apply_chat_template( + conversation, + tokenize=True, + add_generation_prompt=True, + return_dict=True, + return_tensors="pt", + processor_kwargs={"fps": 4}, + ) + inputs = inputs.to(model.device) + + # Run inference + generated_ids = model.generate(**inputs, max_new_tokens=4096) + generated_ids_trimmed = [ + out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids, strict=False) + ] + output_text = processor.batch_decode( + generated_ids_trimmed, + skip_special_tokens=True, + clean_up_tokenization_spaces=False, + ) + print(SEPARATOR) + print(output_text[0]) + print(SEPARATOR) + + +if __name__ == "__main__": + main() diff --git a/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py.lock b/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py.lock new file mode 100644 index 00000000..c3899f54 --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/scripts/inference_example.py.lock @@ -0,0 +1,1346 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[manifest] +requirements = [ + { name = "accelerate", specifier = "==1.13.0" }, + { name = "torch", specifier = "==2.11.0" }, + { name = "torchvision" }, + { name = "transformers", specifier = "==5.8.0" }, + { name = "transformers-cosmos3", editable = "../" }, +] + +[[package]] +name = "accelerate" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, +] + +[[package]] +name = "idna" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, + { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/36/390075693b76d4fb4a2bea360fb6080347763bd1f1147c49ed0ed938778c/transformers-5.8.0.tar.gz", hash = "sha256:6cc9a1f0291d16b1c1b735bad775e78ebefff7722701d4e28f98aaaa2bd6fb91", size = 8528141, upload-time = "2026-05-05T16:50:04.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/7b/5621d08b34ac35deb9fa14b58d27d124d21ef125ee1c64bc724ca47dfb63/transformers-5.8.0-py3-none-any.whl", hash = "sha256:e9d2cae6d195a7e1e05164c5ebf26142a7044e4dc4267274f4809204f92827e4", size = 10630279, upload-time = "2026-05-05T16:50:01.026Z" }, +] + +[[package]] +name = "transformers-cosmos3" +version = "0.1.0" +source = { editable = "../" } +dependencies = [ + { name = "transformers" }, +] + +[package.metadata] +requires-dist = [{ name = "transformers", specifier = ">=4.57" }] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/__init__.py b/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/__init__.py new file mode 100644 index 00000000..f4120457 --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/__init__.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""transformers shim: load Cosmos3 checkpoints into Qwen3-VL understanding tower.""" + +from transformers_cosmos3.model import Cosmos3ForConditionalGeneration + +__all__ = ["Cosmos3ForConditionalGeneration"] diff --git a/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/model.py b/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/model.py new file mode 100644 index 00000000..40315622 --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/transformers_cosmos3/model.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Load the understanding tower of a Cosmos3 checkpoint.""" + +import logging + +from transformers import Qwen3VLForConditionalGeneration + +logger = logging.getLogger(__name__) + +_DROP_PATTERNS: tuple[str, ...] = ( + r"_moe_gen", + r"^llm2vae\.", + r"^vae2llm\.", + r"^time_embedder\.", +) +"""Generation-tower drop patterns (regex, matched via `re.search`).""" + +_KEY_MAPPING: dict[str, str] = { + # Flat Qwen3 -> nested HF Qwen3-VL. Negative lookahead skips already-nested keys. + r"^model\.(?!language_model\.)(.+)$": r"model.language_model.\1", +} + + +class Cosmos3ForConditionalGeneration(Qwen3VLForConditionalGeneration): + # Drop-pattern keys don't match any model parameter after rename -- the + # loader skips them; these patterns silence the resulting warning. + _keys_to_ignore_on_load_unexpected = list(_DROP_PATTERNS) + # Drop once a future checkpoint ships `model.visual.*`. + _keys_to_ignore_on_load_missing = [r"^model\.visual\."] + + @classmethod + def from_pretrained(cls, *args, **kwargs): + # `_checkpoint_conversion_mapping` is a global model_type -> mapping + # registry, so subclassing doesn't register new renames. Inject via + # the per-call `key_mapping=` kwarg instead, letting callers override. + merged = {**_KEY_MAPPING, **(kwargs.pop("key_mapping", None) or {})} + kwargs["key_mapping"] = merged + logger.info("Cosmos3 transformers shim: applying key_mapping=%s", merged) + return super().from_pretrained(*args, **kwargs) diff --git a/cosmos-inference/packages/transformers-cosmos3/uv.lock b/cosmos-inference/packages/transformers-cosmos3/uv.lock new file mode 100644 index 00000000..2e6f35f0 --- /dev/null +++ b/cosmos-inference/packages/transformers-cosmos3/uv.lock @@ -0,0 +1,705 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/36/390075693b76d4fb4a2bea360fb6080347763bd1f1147c49ed0ed938778c/transformers-5.8.0.tar.gz", hash = "sha256:6cc9a1f0291d16b1c1b735bad775e78ebefff7722701d4e28f98aaaa2bd6fb91", size = 8528141, upload-time = "2026-05-05T16:50:04.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/7b/5621d08b34ac35deb9fa14b58d27d124d21ef125ee1c64bc724ca47dfb63/transformers-5.8.0-py3-none-any.whl", hash = "sha256:e9d2cae6d195a7e1e05164c5ebf26142a7044e4dc4267274f4809204f92827e4", size = 10630279, upload-time = "2026-05-05T16:50:01.026Z" }, +] + +[[package]] +name = "transformers-cosmos3" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "transformers" }, +] + +[package.metadata] +requires-dist = [{ name = "transformers", specifier = ">=4.57" }] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/cosmos-inference/packages/vllm-cosmos3/README.md b/cosmos-inference/packages/vllm-cosmos3/README.md new file mode 100644 index 00000000..5607ab8e --- /dev/null +++ b/cosmos-inference/packages/vllm-cosmos3/README.md @@ -0,0 +1,30 @@ +# Cosmos3 vLLM Plugin + +Start the vLLM server: + +```shell +cd packages/vllm-cosmos3 +VLLM_USE_DEEP_GEMM=0 uvx --torch-backend=auto --with-editable . vllm@latest serve nvidia/Cosmos3-Nano \ + --trust-remote-code \ + --port 8000 +``` + +Wait for the server to start (takes ~5 minutes). You will see `Application startup complete.` in the log. + +In a separate terminal, submit a request: + +```shell +curl -s http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "nvidia/Cosmos3-Nano", + "messages": [{"role": "user", "content": "Give me a short introduction to large language model."}], + "max_tokens": 4096 + }' | jq -r '.choices[0].message.content' +``` + +For more details, see: + +- [Cosmos-Reason2 repository](https://github.com/nvidia-cosmos/cosmos-reason2) +- [Qwen3-VL repository](https://github.com/QwenLM/Qwen3-VL#online-serving) +- [Qwen3-VL vLLM](https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen3-VL.html) diff --git a/cosmos-inference/vllm-cosmos3/pyproject.toml b/cosmos-inference/packages/vllm-cosmos3/pyproject.toml similarity index 97% rename from cosmos-inference/vllm-cosmos3/pyproject.toml rename to cosmos-inference/packages/vllm-cosmos3/pyproject.toml index 0a98a896..91468aec 100644 --- a/cosmos-inference/vllm-cosmos3/pyproject.toml +++ b/cosmos-inference/packages/vllm-cosmos3/pyproject.toml @@ -22,7 +22,7 @@ name = "vllm-cosmos3" version = "0.1.0" description = "vLLM plugin that loads Cosmos3 checkpoints" requires-python = ">=3.10" -dependencies = [] +dependencies = ["vllm>=0.19"] [project.entry-points."vllm.general_plugins"] register_cosmos3 = "vllm_cosmos3:register" diff --git a/cosmos-inference/packages/vllm-cosmos3/uv.lock b/cosmos-inference/packages/vllm-cosmos3/uv.lock new file mode 100644 index 00000000..c05f2464 --- /dev/null +++ b/cosmos-inference/packages/vllm-cosmos3/uv.lock @@ -0,0 +1,5694 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, + { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, + { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.101.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/cb/9d0123243e749ac3a579972b2c398971bce1dc57bcc4efb08066df610360/anthropic-0.101.0.tar.gz", hash = "sha256:1116a6a87c55757e0fbe3e1ba40804fbd04de7963601a6dd6b539a889f18de3e", size = 758603, upload-time = "2026-05-11T15:46:33.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/b2/74ff06762d005ecf1658929a292df0acb786d025f6a6c54fcb30e2dc7761/anthropic-0.101.0-py3-none-any.whl", hash = "sha256:cc3cc6576989471e2aa9132258034ad0ff0d8fe500b04ac499e4e46ed68c5ed0", size = 753594, upload-time = "2026-05-11T15:46:32.216Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "apache-tvm-ffi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/3d/4594c14de64e92697a91eec8ac6518ad72a3f30776aff432e68c2c6d9d3d/apache_tvm_ffi-0.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d911cbbc83bf12a0d9ec03e5315ff1bb92d95702fe912cd7a050393274382e71", size = 2068752, upload-time = "2026-02-27T19:27:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/0a/827e4f9ae85e1be3037818abd59566d906ba1fe27295c6938b12cc482151/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1c8dd4018420c0d14bace688594710909ce198056ff8ac2ad1cd462b30fe1bdd", size = 2231204, upload-time = "2026-02-27T19:27:04.734Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b6/f1ec5c528918c4dae03885ec472663072a984431d7d7fb04ca0798a2e13c/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f6bc8846d570b8ce38692fc91b530b44cd6ae092c805a844da23970e81b12c0", size = 2323684, upload-time = "2026-02-27T19:27:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/818836fbc0f198da1597896f82d7e6556bf5678cd5150d633214bf14b718/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3ec9149f207a7af3ea3531cad7a0b0d04ded06df4f51a547479d5eb489428dd", size = 2160066, upload-time = "2026-02-27T19:27:07.897Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/2e7d73d055523c2fb31394cd3d55593969a0680619e1c939c2128c2fdd36/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefcd17f61bf503ff0f4ad429e03ef6c241c7d13682f58281d883218b854c9bd", size = 2307014, upload-time = "2026-02-27T19:27:10.287Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/9b99efbeaaed4c78a2b7cfeda6b8fc7d6249616938c05ae0248aa0bf0d56/apache_tvm_ffi-0.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:dd58da01331826fbe6c064d6f0c9bbc2d62883b78df8d15baa8ea21d37507e4d", size = 1993158, upload-time = "2026-02-27T19:27:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/44/130571cede8704b1412e48b3dd78de41b4d31b68241f954743d1a9925bd9/apache_tvm_ffi-0.1.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:932d94e29595a47109f0ef6e0b4209a934451582954ea8b426e758d6b3e307e3", size = 2070368, upload-time = "2026-02-27T19:27:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/9f2cfd6d49b03c5d4ec5c12548d911e2e01265be783f343103b4df716765/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c0449fc3802987c3652bea266ffda2934a6f69c80bba791a3f55b91040656a18", size = 2231154, upload-time = "2026-02-27T19:27:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/43/63faedea83494e99122466a993bcdccd31cf93c7e8a0d56731120e82e2b9/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f16d73a82a9e68a439b7d233d48b1b929be17fe92df4bbf1ee2274e573144a3", size = 2323130, upload-time = "2026-02-27T19:27:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/d735bc4c528efaf0a8a954076963c727aad2dde8577641aa9025ec4f2d52/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01ebb1308b2666c206aa9a4015eb48f03a5d98ea2e9cfb002bd5e2ca0b9c7ef3", size = 2159854, upload-time = "2026-02-27T19:27:18.789Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/6cfc82a3ab5d9e501bbcee5df36eebe09da1c384461d7a55e2a17776d117/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21365abd2a2a1a6d3b4e6e4f048309651125becfa795440c3607f3cc27d30ac7", size = 2307140, upload-time = "2026-02-27T19:27:20.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/3ffe1fe3190e12807a12b72ed0d291c7f66569c2e7c3571fde18175f19e1/apache_tvm_ffi-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:9ee710a9fba3d9ff9747870bbd7e2175eb8d5b9c791f17fd645f35f6dab3f8aa", size = 1993218, upload-time = "2026-02-27T19:27:22.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/f2/b8c4b151169f6d7ba8773c8af68b2e0c1013d7fb3f1bdf87573f47157ce9/apache_tvm_ffi-0.1.9-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:49e52350b0470654847de752e65603b604a4d3323e7e9f5e8a982f44acc4c143", size = 2041756, upload-time = "2026-02-27T19:27:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/1b7dc5f0807f83098183a57db6ee85b2c93b646d74a6e03781c9208aaeb0/apache_tvm_ffi-0.1.9-cp312-abi3-win_amd64.whl", hash = "sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b", size = 1973200, upload-time = "2026-02-27T19:27:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1e/991ae65e64ce132c1ba665562db6638f5696d6133f580e20c653de33b9af/apache_tvm_ffi-0.1.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3349f72ddb8ce206472d0380a729f213017a2180707096f8d57114b81097dd1", size = 2072944, upload-time = "2026-02-27T19:27:34.261Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/1e0643949e683fb3cfababd87058c0cfef122d1a3bb6ce703f719051b842/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d1f4d2b7ec7b1213632e9a104e9330bfc3dec48decffa62114c33aa188c9f43a", size = 2215954, upload-time = "2026-02-27T19:27:35.872Z" }, + { url = "https://files.pythonhosted.org/packages/d6/06/5016191ab61d2db4c3a7d754a3c1184e0836f575a7d08491669738c5e4b9/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4f01d16ba53fe118e363f7257253f07003797e4abe6fc9567f23b6a930dbff2", size = 2307291, upload-time = "2026-02-27T19:27:37.527Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f5/40bf0667330938efbfc0a51743cc53c79e41b4ece1a8abad3076192c9674/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c0581dd6bfbce7b017ef85cfda08bbe38891cc4b3afbcfaa8bc2d383728e426", size = 2143850, upload-time = "2026-02-27T19:27:40.437Z" }, + { url = "https://files.pythonhosted.org/packages/72/4a/421cbd4ed32e8bad3b88af3e8fa145c1f6f493bdd05be15b6f2d9b3cb7d6/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dfa14be2a49347791ef21222a8225ce7f99bfec17104a676cb4f1bf3a107088", size = 2289038, upload-time = "2026-02-27T19:27:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/c8923d819b49872a612033b90d29299c0be73a7cbed1ddb3dc78dfe5e9f1/apache_tvm_ffi-0.1.9-cp314-cp314t-win_amd64.whl", hash = "sha256:a42d7ca27dce83efbdce7ec970fe3e773a69c31d928730ee5d9badb1229d106c", size = 2039007, upload-time = "2026-02-27T19:27:43.618Z" }, +] + +[[package]] +name = "astor" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "blake3" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a0/fbe66cf17f72cab1600246b90db6cb39b52a88335b9bd2821688379d8dde/blake3-1.0.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8956bb9aec47b6c37ccce935a943588f1f5e6e2e85d43bb7cb76a574238f8a9b", size = 350634, upload-time = "2025-10-14T06:45:09.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/f4b88873054aa87b8c36398775713bf674807e7449a9c7fefe35d3cf1dc5/blake3-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7adbbee5dd0c302218eb8acdfd82b7006930eb5798f56f79f9cca89f6f192662", size = 328382, upload-time = "2025-10-14T06:45:11.137Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/4c37ced9358cece71f2f380a57f77a449f6e87cc6d9f450613237b7a3078/blake3-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:859cd57bac097a2cd63cb36d64c2f6f16c9edece5590f929e70157478e46dc9e", size = 371337, upload-time = "2025-10-14T06:45:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/d1/df/0825da1cde7ca63a8bcdc785ca7f8647b025e9497eef18c75bb9754dbd26/blake3-1.0.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e1d70bf76c02846d0868a3d413eb6c430b76a315e12f1b2e59b5cf56c1f62a3", size = 374945, upload-time = "2025-10-14T06:45:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/43f10c623179dce789ca9e3b8f4064fb6312e99f05c1aae360d07ad95bb0/blake3-1.0.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3fe26f145fcb82931d1820b55c0279f72f8f8e49450dd9d74efbfd409b28423", size = 448766, upload-time = "2025-10-14T06:45:15.471Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/9431bf5fe0eedeb2aadb4fe81fb18945cf8d49adad98e7988fb3cdac76c2/blake3-1.0.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c076d58ee37eb5b2d8d91bb9db59c5a008fd59c71845dc57fe438aeeabaf10", size = 507107, upload-time = "2025-10-14T06:45:17.055Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/3712cdaebaefa8d5acec46f8df7861ba1832e1e188bc1333dd5acd31f760/blake3-1.0.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78731ce7fca46f776ae45fb5271a2a76c4a92c9687dd4337e84b2ae9a174b28f", size = 393955, upload-time = "2025-10-14T06:45:18.718Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d0/add0441e7aaa6b358cac0ddc9246f0799b60d25f06bd542b554afe19fd85/blake3-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65e373c8b47174b969ee61a89ee56922f722972eb650192845c8546df8d9db9", size = 387577, upload-time = "2025-10-14T06:45:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/e4a61f5c0cad4d51a886e8f4367e590caaead8a4809892292bf724c4421d/blake3-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db54946792d2b8c6fa4be73e6e334519f13c1b52e7ff346b3e2ec8ad3eb59401", size = 550515, upload-time = "2025-10-14T06:45:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/c7/90c01091465628acff96534e82d4b3bc16ca22c515f69916d2715273c0e3/blake3-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:67d9c42c42eb1c7aedcf901591c743266009fcf48babf6d6f8450f567cb94a84", size = 554650, upload-time = "2025-10-14T06:45:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/812d7125c6e99e5e0e841a9af2c4161ac811c027e08886353df76eae7b96/blake3-1.0.8-cp310-cp310-win32.whl", hash = "sha256:444215a1e5201f8fa4e5c7352e938a7070cd33d66aeb1dd9b1103a64b6920f9e", size = 228695, upload-time = "2025-10-14T06:45:24.255Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/ab9b5c4b650ff397d347451bfb1ad7e6e53dc06c945e2fd091f27a76422e/blake3-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:725c52c4d393c7bd1a10682df322d480734002a1389b320366c660568708846b", size = 215660, upload-time = "2025-10-14T06:45:25.381Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e1/1df74c915fde3c48940247ad64984f40f5968191d7b5230bcc7b31402e7c/blake3-1.0.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9a8946cb6b1d2b2096daaaa89856f39887bce2b78503fa31b78173e3a86fa281", size = 350481, upload-time = "2025-10-14T06:45:26.625Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/7c47ae1f5f8d60783ce6234a8b31db351fc62be243006a6276284ca3d40d/blake3-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:adccc3a139207e02bb7d7bb0715fe0b87069685aad5f3afff820b2f829467904", size = 328039, upload-time = "2025-10-14T06:45:32.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/515209b0c282c360e249b89cd85350d97cfd55fadbb4df736c67b77b27a1/blake3-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fcfe81b3ae3fb5d2e88be0d3259603ff95f0d5ed69f655c28fdaef31e49a470", size = 371092, upload-time = "2025-10-14T06:45:34.062Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/9d342a2bf5817f006bbe947335e5d387327541ea47590854947befd01251/blake3-1.0.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ce8d45a5bb5326482de72ea1969a378634236186a970fef63058a5b7b8b435", size = 374859, upload-time = "2025-10-14T06:45:35.262Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fc/ea4bef850a7ec9fbb383503fd3c56056dd9fa44e10c3bc61050ab7b2bac0/blake3-1.0.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83605dbf43f581d8b7175b7f3bfe5388bad5a7c6ac175c9c11d669da31133f4b", size = 448585, upload-time = "2025-10-14T06:45:36.542Z" }, + { url = "https://files.pythonhosted.org/packages/a5/67/167a65a4c431715407d07b1b8b1367698a3ad88e7260edb85f0c5293f08a/blake3-1.0.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b5573b052777142b2cecc453d022c3f21aa4aba75011258410bb98f41c1a727", size = 507519, upload-time = "2025-10-14T06:45:37.814Z" }, + { url = "https://files.pythonhosted.org/packages/32/e2/0886e192d634b264c613b0fbf380745b39992b424a0effc00ef08783644e/blake3-1.0.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe1b02ab49bfd969ef50b9f17482a2011c77536654af21807ba5c2674e0bb2a0", size = 393645, upload-time = "2025-10-14T06:45:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3b/7fb2fe615448caaa5f6632b2c7551117b38ccac747a3a5769181e9751641/blake3-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7780666dc6be809b49442d6d5ce06fdbe33024a87560b58471103ec17644682", size = 387640, upload-time = "2025-10-14T06:45:40.546Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8c/2bfc942c6c97cb3d20f341859343bb86ee20af723fedfc886373e606079b/blake3-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af394b50c6aa0b1b957a99453d1ee440ef67cd2d1b5669c731647dc723de8a3a", size = 550316, upload-time = "2025-10-14T06:45:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/75/0252be37620699b79dbaa799c9b402d63142a131d16731df4ef09d135dd7/blake3-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c63ece266a43014cf29e772a82857cd8e90315ae3ed53e3c5204851596edd5f2", size = 554463, upload-time = "2025-10-14T06:45:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6d/d698ae2d5ddd25976fd2c11b079ca071334aecbba6414da8c9cc8e19d833/blake3-1.0.8-cp311-cp311-win32.whl", hash = "sha256:44c2815d4616fad7e2d757d121c0a11780f70ffc817547b3059b5c7e224031a7", size = 228375, upload-time = "2025-10-14T06:45:44.425Z" }, + { url = "https://files.pythonhosted.org/packages/34/d7/33b01e27dc3542dc9ec44132684506f880cd0257b04da0bf7f4b2afa41c8/blake3-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:8f2ef8527a7a8afd99b16997d015851ccc0fe2a409082cebb980af2554e5c74c", size = 215733, upload-time = "2025-10-14T06:45:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a0/b7b6dff04012cfd6e665c09ee446f749bd8ea161b00f730fe1bdecd0f033/blake3-1.0.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b", size = 347983, upload-time = "2025-10-14T06:45:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/264091cac31d7ae913f1f296abc20b8da578b958ffb86100a7ce80e8bf5c/blake3-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865", size = 325415, upload-time = "2025-10-14T06:45:48.482Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" }, + { url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" }, + { url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/55/d1/ca74aa450cbe10e396e061f26f7a043891ffa1485537d6b30d3757e20995/blake3-1.0.8-cp312-cp312-win32.whl", hash = "sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b", size = 228343, upload-time = "2025-10-14T06:46:01.533Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/bbd02647169e3fbed27558555653ac2578c6f17ccacf7d1956c58ef1d214/blake3-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7", size = 215704, upload-time = "2025-10-14T06:46:02.79Z" }, + { url = "https://files.pythonhosted.org/packages/55/b8/11de9528c257f7f1633f957ccaff253b706838d22c5d2908e4735798ec01/blake3-1.0.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943", size = 347771, upload-time = "2025-10-14T06:46:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/f7668be55c909678b001ecacff11ad7016cd9b4e9c7cc87b5971d638c5a9/blake3-1.0.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc", size = 325431, upload-time = "2025-10-14T06:46:06.136Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/e8a85fa261894bf7ce7af928ff3408aab60287ab8d58b55d13a3f700b619/blake3-1.0.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d", size = 370994, upload-time = "2025-10-14T06:46:07.398Z" }, + { url = "https://files.pythonhosted.org/packages/62/cd/765b76bb48b8b294fea94c9008b0d82b4cfa0fa2f3c6008d840d01a597e4/blake3-1.0.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f54cff7f15d91dc78a63a2dd02a3dccdc932946f271e2adb4130e0b4cf608ba", size = 374372, upload-time = "2025-10-14T06:46:08.698Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/32084eadbb28592bb07298f0de316d2da586c62f31500a6b1339a7e7b29b/blake3-1.0.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7e12a777f6b798eb8d06f875d6e108e3008bd658d274d8c676dcf98e0f10537", size = 447627, upload-time = "2025-10-14T06:46:10.002Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f4/3788a1d86e17425eea147e28d7195d7053565fc279236a9fd278c2ec495e/blake3-1.0.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddfc59b0176fb31168f08d5dd536e69b1f4f13b5a0f4b0c3be1003efd47f9308", size = 507536, upload-time = "2025-10-14T06:46:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/fe/01/4639cba48513b94192681b4da472cdec843d3001c5344d7051ee5eaef606/blake3-1.0.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2336d5b2a801a7256da21150348f41610a6c21dae885a3acb1ebbd7333d88d8", size = 394105, upload-time = "2025-10-14T06:46:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/21/ae/6e55c19c8460fada86cd1306a390a09b0c5a2e2e424f9317d2edacea439f/blake3-1.0.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4072196547484c95a5a09adbb952e9bb501949f03f9e2a85e7249ef85faaba8", size = 386928, upload-time = "2025-10-14T06:46:16.284Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6c/05b7a5a907df1be53a8f19e7828986fc6b608a44119641ef9c0804fbef15/blake3-1.0.8-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0eab3318ec02f8e16fe549244791ace2ada2c259332f0c77ab22cf94dfff7130", size = 550003, upload-time = "2025-10-14T06:46:17.791Z" }, + { url = "https://files.pythonhosted.org/packages/b4/03/f0ea4adfedc1717623be6460b3710fcb725ca38082c14274369803f727e1/blake3-1.0.8-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a33b9a1fb6d1d559a8e0d04b041e99419a6bb771311c774f6ff57ed7119c70ed", size = 553857, upload-time = "2025-10-14T06:46:19.088Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6f/e5410d2e2a30c8aba8389ffc1c0061356916bf5ecd0a210344e7b69b62ab/blake3-1.0.8-cp313-cp313-win32.whl", hash = "sha256:e171b169cb7ea618e362a4dddb7a4d4c173bbc08b9ba41ea3086dd1265530d4f", size = 228315, upload-time = "2025-10-14T06:46:20.391Z" }, + { url = "https://files.pythonhosted.org/packages/79/ef/d9c297956dfecd893f29f59e7b22445aba5b47b7f6815d9ba5dcd73fcae6/blake3-1.0.8-cp313-cp313-win_amd64.whl", hash = "sha256:3168c457255b5d2a2fc356ba696996fcaff5d38284f968210d54376312107662", size = 215477, upload-time = "2025-10-14T06:46:21.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/ba/eaa7723d66dd8ab762a3e85e139bb9c46167b751df6e950ad287adb8fb61/blake3-1.0.8-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4d672c24dc15ec617d212a338a4ca14b449829b6072d09c96c63b6e6b621aed", size = 347289, upload-time = "2025-10-14T06:46:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/6957f6ee27f0d5b8c4efdfda68a1298926a88c099f4dd89c711049d16526/blake3-1.0.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1af0e5a29aa56d4fba904452ae784740997440afd477a15e583c38338e641f41", size = 324444, upload-time = "2025-10-14T06:46:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/13/da/722cebca11238f3b24d3cefd2361c9c9ea47cfa0ad9288eeb4d1e0b7cf93/blake3-1.0.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef153c5860d5bf1cc71aece69b28097d2a392913eb323d6b52555c875d0439fc", size = 370441, upload-time = "2025-10-14T06:46:26.29Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d5/2f7440c8e41c0af995bad3a159e042af0f4ed1994710af5b4766ca918f65/blake3-1.0.8-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ae3689f0c7bfa6ce6ae45cab110e4c3442125c4c23b28f1f097856de26e4d1", size = 374312, upload-time = "2025-10-14T06:46:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6c/fb6a7812e60ce3e110bcbbb11f167caf3e975c589572c41e1271f35f2c41/blake3-1.0.8-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb83532f7456ddeb68dae1b36e1f7c52f9cb72852ac01159bbcb1a12b0f8be0", size = 447007, upload-time = "2025-10-14T06:46:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/13/3b/c99b43fae5047276ea9d944077c190fc1e5f22f57528b9794e21f7adedc6/blake3-1.0.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae7754c7d96e92a70a52e07c732d594cf9924d780f49fffd3a1e9235e0f5ba7", size = 507323, upload-time = "2025-10-14T06:46:30.661Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bb/ba90eddd592f8c074a0694cb0a744b6bd76bfe67a14c2b490c8bdfca3119/blake3-1.0.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bacaae75e98dee3b7da6c5ee3b81ee21a3352dd2477d6f1d1dbfd38cdbf158a", size = 393449, upload-time = "2025-10-14T06:46:31.805Z" }, + { url = "https://files.pythonhosted.org/packages/25/ed/58a2acd0b9e14459cdaef4344db414d4a36e329b9720921b442a454dd443/blake3-1.0.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9456c829601d72852d8ba0af8dae0610f7def1d59f5942efde1e2ef93e8a8b57", size = 386844, upload-time = "2025-10-14T06:46:33.195Z" }, + { url = "https://files.pythonhosted.org/packages/4a/04/fed09845b18d90862100c8e48308261e2f663aab25d3c71a6a0bdda6618b/blake3-1.0.8-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:497ef8096ec4ac1ffba9a66152cee3992337cebf8ea434331d8fd9ce5423d227", size = 549550, upload-time = "2025-10-14T06:46:35.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/1859fddfabc1cc72548c2269d988819aad96d854e25eae00531517925901/blake3-1.0.8-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:511133bab85ff60ed143424ce484d08c60894ff7323f685d7a6095f43f0c85c3", size = 553805, upload-time = "2025-10-14T06:46:36.532Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c7/2969352017f62378e388bb07bb2191bc9a953f818dc1cd6b9dd5c24916e1/blake3-1.0.8-cp313-cp313t-win32.whl", hash = "sha256:9c9fbdacfdeb68f7ca53bb5a7a5a593ec996eaf21155ad5b08d35e6f97e60877", size = 228068, upload-time = "2025-10-14T06:46:37.826Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fc/923e25ac9cadfff1cd20038bcc0854d0f98061eb6bc78e42c43615f5982d/blake3-1.0.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3cec94ed5676821cf371e9c9d25a41b4f3ebdb5724719b31b2749653b7cc1dfa", size = 215369, upload-time = "2025-10-14T06:46:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2a/9f13ea01b03b1b4751a1cc2b6c1ef4b782e19433a59cf35b59cafb2a2696/blake3-1.0.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2c33dac2c6112bc23f961a7ca305c7e34702c8177040eb98d0389d13a347b9e1", size = 347016, upload-time = "2025-10-14T06:46:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/06/8e/8458c4285fbc5de76414f243e4e0fcab795d71a8b75324e14959aee699da/blake3-1.0.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c445eff665d21c3b3b44f864f849a2225b1164c08654beb23224a02f087b7ff1", size = 324496, upload-time = "2025-10-14T06:46:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/49/fa/b913eb9cc4af708c03e01e6b88a8bb3a74833ba4ae4b16b87e2829198e06/blake3-1.0.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47939f04b89c5c6ff1e51e883e5efab1ea1bf01a02f4d208d216dddd63d0dd8", size = 370654, upload-time = "2025-10-14T06:46:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/245e0800c33b99c8f2b570d9a7199b51803694913ee4897f339648502933/blake3-1.0.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73e0b4fa25f6e3078526a592fb38fca85ef204fd02eced6731e1cdd9396552d4", size = 374693, upload-time = "2025-10-14T06:46:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a6/8cb182c8e482071dbdfcc6ec0048271fd48bcb78782d346119ff54993700/blake3-1.0.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0543c57eb9d6dac9d4bced63e9f7f7b546886ac04cec8da3c3d9c8f30cbbb7", size = 447673, upload-time = "2025-10-14T06:46:46.358Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/1cbbb5574d2a9436d1b15e7eb5b9d82e178adcaca71a97b0fddaca4bfe3a/blake3-1.0.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed972ebd553c0c25363459e9fc71a38c045d8419e365b59acd8cd791eff13981", size = 507233, upload-time = "2025-10-14T06:46:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/9c/45/b55825d90af353b3e26c653bab278da9d6563afcf66736677f9397e465be/blake3-1.0.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bafdec95dfffa3f6571e529644744e280337df15ddd9728f224ba70c5779b23", size = 393852, upload-time = "2025-10-14T06:46:49.511Z" }, + { url = "https://files.pythonhosted.org/packages/34/73/9058a1a457dd20491d1b37de53d6876eff125e1520d9b2dd7d0acbc88de2/blake3-1.0.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d78f06f3fb838b34c330e2987090376145cbe5944d8608a0c4779c779618f7b", size = 386442, upload-time = "2025-10-14T06:46:51.205Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/561d537ffc17985e276e08bf4513f1c106f1fdbef571e782604dc4e44070/blake3-1.0.8-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:dd03ff08d1b6e4fdda1cd03826f971ae8966ef6f683a8c68aa27fb21904b5aa9", size = 549929, upload-time = "2025-10-14T06:46:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/03/2f/dbe20d2c57f1a67c63be4ba310bcebc707b945c902a0bde075d2a8f5cd5c/blake3-1.0.8-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4e02a3c499e35bf51fc15b2738aca1a76410804c877bcd914752cac4f71f052a", size = 553750, upload-time = "2025-10-14T06:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/6b/da/c6cb712663c869b2814870c2798e57289c4268c5ac5fb12d467fce244860/blake3-1.0.8-cp314-cp314-win32.whl", hash = "sha256:a585357d5d8774aad9ffc12435de457f9e35cde55e0dc8bc43ab590a6929e59f", size = 228404, upload-time = "2025-10-14T06:46:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/c7dcd8bc3094bba1c4274e432f9e77a7df703532ca000eaa550bd066b870/blake3-1.0.8-cp314-cp314-win_amd64.whl", hash = "sha256:9ab5998e2abd9754819753bc2f1cf3edf82d95402bff46aeef45ed392a5468bf", size = 215460, upload-time = "2025-10-14T06:46:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/75/3c/6c8afd856c353176836daa5cc33a7989e8f54569e9d53eb1c53fc8f80c34/blake3-1.0.8-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2df12f295f95a804338bd300e8fad4a6f54fd49bd4d9c5893855a230b5188a8", size = 347482, upload-time = "2025-10-14T06:47:00.189Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/92cd5501ce8e1f5cabdc0c3ac62d69fdb13ff0b60b62abbb2b6d0a53a790/blake3-1.0.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:63379be58438878eeb76ebe4f0efbeaabf42b79f2cff23b6126b7991588ced67", size = 324376, upload-time = "2025-10-14T06:47:01.413Z" }, + { url = "https://files.pythonhosted.org/packages/11/33/503b37220a3e2e31917ef13722efd00055af51c5e88ae30974c733d7ece6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88d527c247f9609dc1d45a08fd243e39f0d5300d54c57e048de24d4fa9240ebb", size = 370220, upload-time = "2025-10-14T06:47:02.573Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/fe817843adf59516c04d44387bd643b422a3b0400ea95c6ede6a49920737/blake3-1.0.8-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506a47897a11ebe8f3cdeb52f1365d6a2f83959e98ccb0c830f8f73277d4d358", size = 373454, upload-time = "2025-10-14T06:47:03.784Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/90a2a623575373dfc9b683f1bad1bf017feafa5a6d65d94fb09543050740/blake3-1.0.8-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5122a61b3b004bbbd979bdf83a3aaab432da3e2a842d7ddf1c273f2503b4884", size = 447102, upload-time = "2025-10-14T06:47:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/93/ff/4e8ce314f60115c4c657b1fdbe9225b991da4f5bcc5d1c1f1d151e2f39d6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0171e85d56dec1219abdae5f49a0ed12cb3f86a454c29160a64fd8a8166bba37", size = 506791, upload-time = "2025-10-14T06:47:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/44/88/2963a1f18aab52bdcf35379b2b48c34bbc462320c37e76960636b8602c36/blake3-1.0.8-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:003f61e8c41dd9931edddf1cc6a1bb680fb2ac0ad15493ef4a1df9adc59ce9df", size = 393717, upload-time = "2025-10-14T06:47:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/45/d1/a848ed8e8d4e236b9b16381768c9ae99d92890c24886bb4505aa9c3d2033/blake3-1.0.8-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c3151955efb09ba58cd3e1263521e15e9e3866a40d6bd3556d86fc968e8f95", size = 386150, upload-time = "2025-10-14T06:47:10.363Z" }, + { url = "https://files.pythonhosted.org/packages/96/09/e3eb5d60f97c01de23d9f434e6e1fc117efb466eaa1f6ddbbbcb62580d6e/blake3-1.0.8-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:5eb25bca3cee2e0dd746a214784fb36be6a43640c01c55b6b4e26196e72d076c", size = 549120, upload-time = "2025-10-14T06:47:11.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/ad/3d9661c710febb8957dd685fdb3e5a861aa0ac918eda3031365ce45789e2/blake3-1.0.8-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ab4e1dea4fa857944944db78e8f20d99ee2e16b2dea5a14f514fb0607753ac83", size = 553264, upload-time = "2025-10-14T06:47:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/11/55/e332a5b49edf377d0690e95951cca21a00c568f6e37315f9749efee52617/blake3-1.0.8-cp314-cp314t-win32.whl", hash = "sha256:67f1bc11bf59464ef092488c707b13dd4e872db36e25c453dfb6e0c7498df9f1", size = 228116, upload-time = "2025-10-14T06:47:14.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5c/dbd00727a3dd165d7e0e8af40e630cd7e45d77b525a3218afaff8a87358e/blake3-1.0.8-cp314-cp314t-win_amd64.whl", hash = "sha256:421b99cdf1ff2d1bf703bc56c454f4b286fce68454dd8711abbcb5a0df90c19a", size = 215133, upload-time = "2025-10-14T06:47:16.069Z" }, +] + +[[package]] +name = "cachetools" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e2/85f227594656000ff4d8adadae91a21f536d4a84c6c716a86bd6685874be/cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b", size = 40202, upload-time = "2026-05-03T20:00:29.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" }, +] + +[[package]] +name = "cbor2" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/31/74b54539251d2c617875bee73df0b0473dc0b2eabcafa274a85eb69227f9/cbor2-6.1.0.tar.gz", hash = "sha256:7a988431ef0e0e24dab2c701d78b48d23389c24b96a7c82dbd90353af335e141", size = 85870, upload-time = "2026-05-12T21:09:56.047Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/16/053cf8163062ecc29232b5cdfc7ce9df0b3dd61e26a35ae6ea792aba0b37/cbor2-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b0472da47777fef7b5b81c1773172026be47871f0ff8ba7258fc4b85f0c1630", size = 411199, upload-time = "2026-05-12T21:08:30.945Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/88a214490d8c6e803ea7e6ef12f9b7f9952e069d844c65368d7c6e4e20dd/cbor2-6.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4294246096ff528981db137473ea90f99475f25be7e5e1e1c402bb468bc972fb", size = 458770, upload-time = "2026-05-12T21:08:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/5f945365f868525b21d600c4f0f599053fc818f36360433775c072d8f20e/cbor2-6.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c6c3f1134bcfac88a6af9e90f8984b7b241843e884b4a6e257f62e43b85a72b1", size = 468932, upload-time = "2026-05-12T21:08:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/6347c1da74f46b0d2e6f960b7cc4453cb753f48967133a0093ffc877e7fc/cbor2-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ce5408ae99aa44eecb039437cf2c8370a4a129add990025782b1b1225f1f296e", size = 523724, upload-time = "2026-05-12T21:08:36.329Z" }, + { url = "https://files.pythonhosted.org/packages/68/cb/b5ff5d20be10df024dcfbfc0a48b5d8fecb9162f40812fcdc67797624e09/cbor2-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a5101ab7f0fac57b77c34def107e809759320549180bd5ed8662c67b2d708675", size = 535179, upload-time = "2026-05-12T21:08:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/daaa0b6f3d2ab46418738164c661ca6d29164881c0fe02e9025577192b6c/cbor2-6.1.0-cp310-cp310-win32.whl", hash = "sha256:e54834a413553b34c628264c8bbbc073f96390e278b01a1711308565eb761208", size = 284181, upload-time = "2026-05-12T21:08:39.987Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/deadc9f5b90f83345d3f1d5278e46565d45d97ed354b1155229500417417/cbor2-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:13191298a8ef34b35be4c81add189b055771cfbc51dddb94b7c475b4b085111d", size = 300336, upload-time = "2026-05-12T21:08:41.551Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c0/9f2ce8f19113e0e8e24df42769ca5989745e391146c01006014c9ca8f9fc/cbor2-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:74458adcf2d5681b202c99009d81f7e815a1ebb65ba79ff7e386f21b4a761726", size = 291506, upload-time = "2026-05-12T21:08:43.164Z" }, + { url = "https://files.pythonhosted.org/packages/97/88/61652404b18e9db45d17d286f04e004a9a16dbd877377f57ed668424f6ac/cbor2-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb87ed40533434f229c5f9f6cdcbd54230adcae5205e219736beb72d9b2b10f", size = 411151, upload-time = "2026-05-12T21:08:44.738Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/163973dde484a1fa6c8a8daa68a36dc56b61194b84b6ed8e7d02090f59aa/cbor2-6.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:42b48ac73494634de9e0b778f5048ead49f291eda793a9b95d0238053172c3f9", size = 457282, upload-time = "2026-05-12T21:08:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/0e/94/78c5944a02fe86c0b9dc46150f098e240e142a19b96c2fc4f896bc77d74b/cbor2-6.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9eb17decdb225f245ec404b718cf6b01dc95447aff6182c0a6c822a6c1e5a048", size = 468848, upload-time = "2026-05-12T21:08:48.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9c/2b371ab2341ff51006ad637175adb8d1940c2baa5d17f03b737314a681d7/cbor2-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:344966e8899f548bfd31bf3c7f880b05a64369ace7ff74dec89f6687e2b74909", size = 523301, upload-time = "2026-05-12T21:08:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/726421471d4f782a399738906193c5cba26f96d45283dc42ba260d5d1d55/cbor2-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:618c24f8b578d84dab663770d4da6c6dd3b8a87a75bec48c8bfbd264c883a745", size = 535237, upload-time = "2026-05-12T21:08:52.073Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b8/cc4769f30a437b5f4e2b359b28eaa7bfe75a6d04c010f748539ef1f9d296/cbor2-6.1.0-cp311-cp311-win32.whl", hash = "sha256:7c93f391dac11fb61c68f33e7fd5c23d96d0eb37fb45db83e381f9a9fd726979", size = 284296, upload-time = "2026-05-12T21:08:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/a9b74e184295496bab7126767cd003489ad1816a9b78851f1316b1427141/cbor2-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:962d6379710e801ada58c47f13df8bdba41022db9fe77a9e7a59a421823cb773", size = 300412, upload-time = "2026-05-12T21:08:55.36Z" }, + { url = "https://files.pythonhosted.org/packages/6c/37/f09d626f2edce335ce488a2f020a6f0c148a1aa198a4cf0749b0410c52c9/cbor2-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:1f200815fa95cb119410655fda5f428f13a5b1877e710ca92c158fae98022483", size = 291302, upload-time = "2026-05-12T21:08:57.25Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/4d82bd835c130825b3ce0fa66339379d417d0379272e03773c230842c765/cbor2-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f9013b20e13441d8b9f535631d38b1ff654af7f45af6423c4b7ddd8c494a967", size = 409460, upload-time = "2026-05-12T21:08:59.097Z" }, + { url = "https://files.pythonhosted.org/packages/95/a4/bf5a54cc6ccda2d5e8043f38f34c1eefd814cebf40b5812a40a39d1318c4/cbor2-6.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:386758adf697172aa40fe4c81172ba858f32a6409e7dffa8541fdd790505b372", size = 453687, upload-time = "2026-05-12T21:09:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/39/13/3089fb602f574c407c7047751162ec9c0dda74bc601332d2cf4ef5b65c99/cbor2-6.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1e905a4bb44c2a5eb465cfef374e3d6866d5b1b8923f4d387e5fae5eb985ccfd", size = 466931, upload-time = "2026-05-12T21:09:03.633Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/0b67f0269583a0f6af9c9d22914b503515a74c78a40f737785cdf0fad00b/cbor2-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ebf05e8086b5afa6d829dd19d3fd1680217f043ded24a49c510a064df02fc4ed", size = 520757, upload-time = "2026-05-12T21:09:05.234Z" }, + { url = "https://files.pythonhosted.org/packages/de/57/8a6f64811d5b425ed93c247ae7415d9243a864462939c825763588fb4af3/cbor2-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba05594132277b4cadbb2e321ea6d1c84cc1cf1a38deaf2deb1a401f2e567d7f", size = 534182, upload-time = "2026-05-12T21:09:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/5c/52/6914f4652cd0a14e96622688b54611519925d2dbf0f372bee225560317e3/cbor2-6.1.0-cp312-cp312-win32.whl", hash = "sha256:927b7effc7eabdccca64240f87248e41792430efb5cc9b578b793d0a7b3616e1", size = 282531, upload-time = "2026-05-12T21:09:09.129Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/713b2a59349b20915de7c9dbb0ded3a6ddeaedf541885d7b5ffd46799de4/cbor2-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:190dd9631e800900ecab301745fd4278c530fdb1e7fb0a56000e568e272dd13b", size = 300184, upload-time = "2026-05-12T21:09:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/423e1705946cf50f4c5e2b6bb1a806e0d28cd5e68561fb32c65fd79db151/cbor2-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23737b6a04a7cc0c216686c580cba70bc944ae8fcc13639c237489b4d693223c", size = 288383, upload-time = "2026-05-12T21:09:12.552Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/ec95a68b26199e218937836b3700177382a17dc9dc355fc6c687a3d8ebcd/cbor2-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54d11fafdf6adf02c08c0ef20bdd11097c1a2c9a20b300c7fc66b07a85eb1a12", size = 408619, upload-time = "2026-05-12T21:09:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d5/686b6d04157aec18a13a3bc185b68e5200e5cd93b78ad1f3b0dbc9dd5cdb/cbor2-6.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0d3a629eb6747964e20132d78581177b3df381c345156751cc4cb4176a058ea4", size = 453023, upload-time = "2026-05-12T21:09:15.953Z" }, + { url = "https://files.pythonhosted.org/packages/95/f7/2b84ead95cd71c30b85975e85bdfdcb70ac09679c52ea5af2354d3538ef7/cbor2-6.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:17f874920590da73d564092b813267c47493eec96e6765b72b5499500b9bff35", size = 465962, upload-time = "2026-05-12T21:09:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/74/39/89b749a9031b1b00c1373560a891bb25b2d282057fb3b157385dbd8744ed/cbor2-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a6f72aafa65184363e208aea5aacd81beafa07c9949e1c4d402ff46a76beb627", size = 520230, upload-time = "2026-05-12T21:09:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/97/3e/0ee1f52f83195f9a5e35fe7897360da1822c696712cdf265c44030db03ee/cbor2-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:032f06d4c04143db657c598470c3069406f897cd817c24c3b6aefb87cd1329b1", size = 533467, upload-time = "2026-05-12T21:09:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/30/13/8605f161ad89da2a67fcc03405a51bfbfd57d5bf234f1797703ba66a25f8/cbor2-6.1.0-cp313-cp313-win32.whl", hash = "sha256:dbe926724deb339f35b247f6e24d82966076ef2c41c00066576f4c3d3630d731", size = 281867, upload-time = "2026-05-12T21:09:23.715Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5b/bad7006957b99ce12a17708cf3941feb75e39551da15b4dfd52ed93945f8/cbor2-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9c35a71bf2f1d1405704839bfa590d1ea82125a2afda963e834c68548cad83f", size = 299569, upload-time = "2026-05-12T21:09:25.195Z" }, + { url = "https://files.pythonhosted.org/packages/b0/50/4bd8574f8a6c79ba328ca22b870274d83e57b1a8ec61c37bf7ad876ae9bd/cbor2-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e713bbbd025683a096f5dbcd958b1b172a224d7f0bdcab45c138870f712bf281", size = 287174, upload-time = "2026-05-12T21:09:26.717Z" }, + { url = "https://files.pythonhosted.org/packages/db/aa/f0640584752ea4f6c575acc4b3e04d5e074e02e2aa8cd59b3bdc538f35d3/cbor2-6.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:634b6b9bd1197946a88fcc10e49eb205a3fe663dcbffc068949444bd4d1c3522", size = 407377, upload-time = "2026-05-12T21:09:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3f/5db67547c6fb3b1fb74711d30cf0ea970b085295c4477959340f3e36ab6c/cbor2-6.1.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a313df1367d7f57259e59a86fa77ad6eaff957097738c12e860bee391558fe6a", size = 452597, upload-time = "2026-05-12T21:09:30.069Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e6/38c5504a7d00a5803df70ebf840459693e94999c42790bf5ca18f9212bf7/cbor2-6.1.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:780c5c769e8cdca5ea26e1b0835e7a339fa9bd514ccdcecc948c4b189d8bf426", size = 464480, upload-time = "2026-05-12T21:09:31.923Z" }, + { url = "https://files.pythonhosted.org/packages/6e/eb/54f5a2bbbf1eedbb4fec829978f5797b5950969f1aa8665c43b3572cd3c2/cbor2-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32ca627b48114a47af97dc6f2ca55905835b8b1813e33efa4f9448cdfe5f9ec3", size = 519943, upload-time = "2026-05-12T21:09:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/21/3a/15a988a7ac392dee5a654e89937f9b36477595c0d76e7718c90d2ace8c19/cbor2-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75ab21a66189c99f0007685c9c40c3e2f37aed7e9d5c9015b39e20a2df2f4be4", size = 531899, upload-time = "2026-05-12T21:09:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/6d1273a854467d4c06e390afd3984cb400a49fcf46db4da6e7eff2944e43/cbor2-6.1.0-cp314-cp314-win32.whl", hash = "sha256:b4ad2a13d11760045b32d74b662e3088635d81b3597c20e7ad41f4fd7da9271a", size = 285593, upload-time = "2026-05-12T21:09:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/b8/32/fe8bd1e196673378782f635566e7c62fd54503b1c0d13dd4dd42ba9bbd2f/cbor2-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:893acbe1e1de7f1e3ba172d8cf0d37e7a631167cf40bd760280a7e01ed60344f", size = 308902, upload-time = "2026-05-12T21:09:39.176Z" }, + { url = "https://files.pythonhosted.org/packages/34/61/d6fc33c736d6cb992b5e177d654444a743f867c424549ef493bee7e1754f/cbor2-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:29113addf431fd376fe6c269d2a7d16ef558f8575a450b83b3f433ed42b3d029", size = 299404, upload-time = "2026-05-12T21:09:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/47/7f/ffef5ce6e7eba7c663e1cb4dc37ec6552151026259e336d5053dde2dc302/cbor2-6.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5be6ec0c838e429d02062b1252dca91086ace8849a6c61cfd3200e79192fc238", size = 402607, upload-time = "2026-05-12T21:09:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/d3afb0db49cf98b0079a69f03cd757028392ca6f63fa7c106c0d1fdc04f2/cbor2-6.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b001f272431555f9e61c93fd8fb76ced431e01fc6a672efc339a646d18a08fa5", size = 445869, upload-time = "2026-05-12T21:09:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/74/37/258163912225532acac508c2c0a6a534d2ad4cd14a29768b6006569bcc35/cbor2-6.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9b691ae53eb6f708bc760685a72cc472fe21e899324d7a4bb0cd1a4aa64932cb", size = 459396, upload-time = "2026-05-12T21:09:46.218Z" }, + { url = "https://files.pythonhosted.org/packages/07/c2/82df8d95f9bd0eed8154f2929df25ee5c488a8ba3067bc213ec0f349634b/cbor2-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:75532e16e8cd8f0637881ac254e689805f38cd7d08a1cbeeb09c21cb6a10d4f9", size = 511265, upload-time = "2026-05-12T21:09:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/ac09e9b2b115f788eb0e2af06c931e77ddbfd002200e071e8caccfd3ec99/cbor2-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7310f218712795cb33432933d4c44cd0b2d455e4f822b59ce78d96a6a9714383", size = 526573, upload-time = "2026-05-12T21:09:49.582Z" }, + { url = "https://files.pythonhosted.org/packages/cf/28/221315e7ee92413de1dd2cb5962a1a589d8d6657ee12c2798d0e5049b0ba/cbor2-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:c427f262ce7c3096a259c7dc599581d0a6648d84b268070cd4eaf8b3d0230753", size = 279481, upload-time = "2026-05-12T21:09:51.559Z" }, + { url = "https://files.pythonhosted.org/packages/77/ca/0b2fa62b1e30b0c5f0fc7f3691b69bf57ced35425c314285e055b99a5b02/cbor2-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ba12e97579d430f1169f853fa8e7b930cd7011abcdae63eb015f42856d0ea92f", size = 302218, upload-time = "2026-05-12T21:09:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a1/48d9483ada705f60342c75522e3e37831e8eca6b18bd076a701dc0c7ad55/cbor2-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3e8829dc8b3cd2000d5501018b1668162435eb56a5b77f5110c23b48547bd925", size = 289979, upload-time = "2026-05-12T21:09:54.774Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "compressed-tensors" +version = "0.15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "loguru" }, + { name = "pydantic" }, + { name = "torch" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/1b/c3c4a98ec5f2727656336f07a0c35862195c310d8eb0b2fa5b4be6848680/compressed_tensors-0.15.0.1.tar.gz", hash = "sha256:a8e93054e8a5ec49c980b09ed36c4c1249b4a8ee167920a8e461c4da26e78d99", size = 229412, upload-time = "2026-04-10T14:23:54.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/52/93833dc1610e017ac5b7dcd59b8304d8ef67d1114c2d124e728a2cbbea12/compressed_tensors-0.15.0.1-py3-none-any.whl", hash = "sha256:e1b1f322e82e475715e242bad46925a304ea8e5c98b5055a15b8eb22fb6bfea9", size = 194260, upload-time = "2026-04-10T14:23:53.098Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ea/81999d01375645f34596c76eb046b4b36d58cc6fe2bddb2410f8a7b7a827/cuda_bindings-13.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:845025438a1b9e20718b9fb42add3e0eb72e85458bcab3eeb80bfd8f0a9dab33", size = 5600047, upload-time = "2026-03-11T00:12:34.848Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/29/5a/0ce1731c48bcd9f40996a4ef1abbf634f1a7fe4a15c5050b1e75ce3a7acf/cuda_bindings-13.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:debb51b211d246f8326f6b6e982506a5d0d9906672c91bc478b66addc7ecc60a", size = 5631363, upload-time = "2026-03-11T00:12:41.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a5/d7f01a415e134546248cef612adad8153c9f1eb10ec79505a7cd8294370b/cuda_bindings-13.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:45815daeb595bf3b405c52671a2542b1f8e9329f3b029494acbfcc74aeaa1f2d", size = 5840830, upload-time = "2026-03-11T00:12:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c4/84/d3b6220b51cbc02ca14db7387e97445126b4ff5125aaa6c5dd7dcb75e679/cuda_bindings-13.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8cebe3ce4aeeca5af9c490e175f76c4b569bbf4a35a62294b777bc77bf7ac4d8", size = 5796512, upload-time = "2026-03-11T00:12:54.483Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/e3/73/98bcb069778fe420226db75aff54b5dd6c3ecfd0912edabab723326e80b7/cuda_bindings-13.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd658bb5c0e55b7b3e5dd0ed509c6addb298c665db26a9bfba35e1e626000ba2", size = 5938605, upload-time = "2026-03-11T00:13:01.639Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/4e01cc06447d39476e138d1b1adec8d35c0d04eccd2c8d69befc08cd66e8/cuda_bindings-13.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ccf14e0c1def3b7200100aafff3a9f7e210ecb6e409329e92dcf6cd2c00d5c7", size = 6662637, upload-time = "2026-03-11T00:13:07.881Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-python" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings" }, + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/da/b4dbe129f941afe1c24a09ba53521b78875626763d96414798a74763282f/cuda_python-13.2.0-py3-none-any.whl", hash = "sha256:2f092b0ec13a860115fa595411889ee939ad203450ea4f91e9461b174ea7b084", size = 8145, upload-time = "2026-03-11T13:55:19.143Z" }, +] + +[[package]] +name = "cuda-tile" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/c8/1687a83d0739151ca410a5716b5f1dfb6f8feb6381c7c695d72264f17e1c/cuda_tile-1.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c55616c648f06f84808648a521c67f2d7c790574d6b53ddf8c3bfbc995d36d45", size = 245438, upload-time = "2026-04-20T15:51:18.862Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d0/0e4790cc7a536a685a961d2e04f26be54f86f0974e1eaea71c1b64ec4032/cuda_tile-1.3.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:8c71c2fd9b96c054c126a218f9927c8c8dde72441a532464551b865b416d452a", size = 246915, upload-time = "2026-04-20T15:51:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/3d/74/055b786579909475528d599bc5a2729e49edb6acc5f06cfa3073f4343250/cuda_tile-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:339769f95b3a5453b7f416da6d1285f24d0daf3a700a895b68dee3fa6fc93e8f", size = 240739, upload-time = "2026-04-20T15:52:14.007Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d6/753aecb3e8fcee80d20f9d32b4504276691c2f77fc10abbbd8e82197e24c/cuda_tile-1.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:59d9843fa723ceb4d680ec246e12e3ded857266e4c2bf5c5d21e530d6d765060", size = 245441, upload-time = "2026-04-20T15:51:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/8b416239413bf11d17d42ccee43258f3787da13bcea7b2e42e8bbf04b3da/cuda_tile-1.3.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:2888d6b89fae053a53ca7bb703c508a5cf90671d266934573c5b6c25978022c4", size = 246706, upload-time = "2026-04-20T15:51:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/68303196d577e497ddf3cef0fd92785d83f47f6239543a5b19dc4076e487/cuda_tile-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:791b363251fbc64db4402d92153ba3d14bc0aaa4d218cea66562af02a7a76bd9", size = 240640, upload-time = "2026-04-20T15:52:15.428Z" }, + { url = "https://files.pythonhosted.org/packages/f3/49/4592bc94ca05a07c7947ea114fd12734c8497f2daffee9faa79a03e39fb5/cuda_tile-1.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:375316b64c51ee7cfadb2f170a30c1547bc41eb39f1e233a6556713857d2e81f", size = 245744, upload-time = "2026-04-20T15:52:09.621Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/84cb68be463c827bf79da9fa0aa5140838de6455ef6f438bbe0ffa75d378/cuda_tile-1.3.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:e4865acbff1172aaee304bf9c550586088d8b4545a384423597a590899386709", size = 247301, upload-time = "2026-04-20T15:51:04.042Z" }, + { url = "https://files.pythonhosted.org/packages/db/6f/d2fd16c2b0d878021dc703eea5f8fe09599d6b04bdc2531a36fc617751fd/cuda_tile-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:93e20ed31e46e5bf704fb31d13e1c08338d2177838798876f7ee9ec4384b75ba", size = 240923, upload-time = "2026-04-20T15:52:14.939Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/ee943554f83d6a143d9e0a5cf27cd7f5f8f6ef447c7e8366d9ad6a5d1bf2/cuda_tile-1.3.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:8a9bd4dae193cddf438f55d617b6f25b4b0b0fcf4ac4acde7d2695898e396c30", size = 245750, upload-time = "2026-04-20T15:52:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/e1daea2dc4e094290ba727750f8342095ae857ff3ba4f81c489f48688613/cuda_tile-1.3.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:a44a81e255fdb7bf8e1f7511fe3a019e6045024574509ea8548e0f71f25f8473", size = 247300, upload-time = "2026-04-20T15:51:03.072Z" }, + { url = "https://files.pythonhosted.org/packages/2b/77/c13afad1a06824c1c942afd0205e78ff17f0ee06fc1a943f6e2135cf4112/cuda_tile-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:efcb93c25563fe23d6aa083c22893fd703122eaf684b0d36874982d28a6dad0b", size = 240925, upload-time = "2026-04-20T15:52:21.283Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "depyf" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astor" }, + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/65/4df6936130b56e1429114e663e7c1576cf845f3aef1b2dd200c0a5d19dba/depyf-0.20.0-py3-none-any.whl", hash = "sha256:d31effad4261cebecb58955d832e448ace88f432328f95f82fd99c30fd9308d4", size = 39381, upload-time = "2025-10-13T12:33:33.647Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/57/cee8e91b83f39e75ae5562a2237261442a8179dcb3b631c7398113157398/fastapi_cloud_cli-0.17.1.tar.gz", hash = "sha256:0baece208fa88063bec46dccb5fb512f3199162092165e57654b44e64adbc44d", size = 47409, upload-time = "2026-04-27T13:38:07.094Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/a0/e252b68cf155409afabea037ab2971f41509481838847f6503fe890884ea/fastapi_cloud_cli-0.17.1-py3-none-any.whl", hash = "sha256:325e0199bdac7cb86f5df4f4a1d2070054095588088ef7b923a60cec458dcd63", size = 34046, upload-time = "2026-04-27T13:38:08.319Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4a/0d79fe52243a4130aa41d0a3a9eea22e00427db761e1a6782ee817c50222/fastar-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7c906ad371ca365591ebcb7630009923f3eceb20956814494d15591a78e9e46", size = 709786, upload-time = "2026-04-13T17:09:53.974Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/77c94eaafc035e39f5ce5176e32743da4e3fe890f28790e708e53d8f75cd/fastar-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6919497b35fa5bd978d2c26ee117cf1771b90ee5073f7518e44b9bc364b57715", size = 632127, upload-time = "2026-04-13T17:09:39.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f6/97658dd992f4e45747d35adb24c0b100f6b6d451490685ae3fe8a3a2ee1b/fastar-0.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:56b50206aeedd99e22b83289e6fb3ff8f7d7da4407d2419902e4716b4f90585a", size = 869608, upload-time = "2026-04-13T17:09:08.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/fc/81c1ec4d8146a437399e7b95631b51be312f323a9ce64569f932db6c3914/fastar-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a1811a69ae81d469720df0c8af3f84f834a93b5e4f8be0e0e8bde6a52fa11f2", size = 762925, upload-time = "2026-04-13T17:07:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/49baf480ecb197aea7ce2515c503a2f25061958dd3b4c98e98a3a11cdcc7/fastar-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10486238c55589a3947c38f9cfb88a67d8a608eb8dddc722038237d0278a41d7", size = 759913, upload-time = "2026-04-13T17:08:07.324Z" }, + { url = "https://files.pythonhosted.org/packages/94/eb/946f1980267f2824efb7d7c518d47a49b89c0e9cd7c449301f5a7531558a/fastar-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1555ef9992d368a6ec39092276990cef8d329c39a1d86ebd847eaa3b10efd472", size = 926054, upload-time = "2026-04-13T17:08:22.196Z" }, + { url = "https://files.pythonhosted.org/packages/0c/19/d5eb611085ce054382570d8d4e24a5e2ff23cd6d2404528a6643841d6059/fastar-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1f4aca0a9620b76988bbf6225cdea6678a392902444ca18bb8a51495b165a89", size = 818594, upload-time = "2026-04-13T17:08:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/4a/52/18e8d55c0d3d917713f381cb2d0cb793da00c209c802e011d8dc72018cd5/fastar-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75beeecac7d11a666a6c4a0b7f7e80842ae5cf523f2f890b99c78fc82b403545", size = 823005, upload-time = "2026-04-13T17:09:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/0fecdcf33e5aaffe777b96a1c10a3204fe0b05bf18e971033a0bfedafc1c/fastar-0.11.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a08cdf5d16daa401c65c9c7493a18db7dc515c52155a17071ec7098bb07da9d3", size = 887115, upload-time = "2026-04-13T17:08:37.385Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/2a6ad1c2523eb72a4595a9331162fc67ce0f0aee3348728598026c516986/fastar-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6e210375e5a7ba53586cbd6017aa417d2d2ceacbe8671682470281bd0a15e8ef", size = 973595, upload-time = "2026-04-13T17:10:09.258Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/2aa48843228673feacc2b80876b8924e63ea9c5f5f607bd7a72416b86bae/fastar-0.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a2988eb2604b8e15670f355425e8c800e4dcd4edfbcbfe194397f8f17b7eb19e", size = 1036988, upload-time = "2026-04-13T17:10:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/92/ac/3dd14b21c323e8484f47c910110d1d93139ba44621ac2c4c597dbe9fcdb7/fastar-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34abc857b46068fdf91d157bd0203bfd6791dc7a432d1ed180f5af6c2f5bcce9", size = 1078267, upload-time = "2026-04-13T17:10:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/de/a1/3f89e58d6fa99160c9e7e17220c8ab5040b5cc017c4fac2356c6ed18453d/fastar-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0d884be84e37a01053776395441fc960031974e0265801ce574efc3d05e0cdaf", size = 1032551, upload-time = "2026-04-13T17:11:00.667Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ea/24dd3cfc2096933d7d2a80c926e79602cff1fa481124ed2165b60c1dd9ef/fastar-0.11.0-cp310-cp310-win32.whl", hash = "sha256:c721c1ad758e3e4c2c1fd9e96911a0fa58c0a6be5668f1bcfd0b741e72c7cb63", size = 456022, upload-time = "2026-04-13T17:11:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/82/ef/6eb39ee9cdd59822d1c7337c4d28fdc948885bdf455af9e70efa9879e06f/fastar-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba4180b7c3080f55f9035fdd7d8c39fe0e1485087a68ff615bb4784a10b8106b", size = 488392, upload-time = "2026-04-13T17:11:27.486Z" }, + { url = "https://files.pythonhosted.org/packages/11/7a/fb367bdaf4efa2c7952a45aeab2e87a564293ecffe150af673ec8edfda46/fastar-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b82fd6f996e65a86f67a6bd64dd22ef3e8ae2dcaed0ae3b550e71f7e1bbb1df5", size = 709869, upload-time = "2026-04-13T17:09:55.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/5872b28c72c27ec1a00760eace6ff35f714f41ebbd5208cf016b12e29250/fastar-0.11.0-cp311-cp311-win32.whl", hash = "sha256:030b2580fc394f2c9b7890b6735810404e9b9ed5e0344db150b945965b5482b7", size = 457368, upload-time = "2026-04-13T17:11:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/ce6832a16193eb4466f4108be8809c249b51cb1f89dd7894545700d079d5/fastar-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:83ab57ae067969cd0b483ac3b6dccc4b595fc77f5c820760998648d4c42822b5", size = 488605, upload-time = "2026-04-13T17:11:29.161Z" }, + { url = "https://files.pythonhosted.org/packages/15/5a/9cfb80661cf38fd7b0889224beb7d2746784d4ade2a931ed9775a18d8602/fastar-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:27b1a4cee2298b704de8151d310462ee7335ed036011ca9aa6e784b30b6c73a9", size = 464580, upload-time = "2026-04-13T17:11:18.583Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/9bbeffbf1905391446dd98aa520422ce7affde5c9a7c22d757cc5d7c1397/fastar-0.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1266d6a004f427b0d61bd6c7b544d84cc964691b2232c2f4d635a1b75f2f6d5e", size = 711644, upload-time = "2026-04-13T17:10:07.663Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, +] + +[[package]] +name = "fastsafetensors" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/69/e34a1e86a02b255896c57263bf0dfbae45b4708fd609b937f783c2202e7b/fastsafetensors-0.3.1.tar.gz", hash = "sha256:b7eb039a564d77280d17e5d63b27e9963ba5158ad02d2a3c1772c62072a81a53", size = 55665, upload-time = "2026-05-06T08:48:59.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/77/0913cd907585085b2f10f2d7eef2aa52c9445241dc66238de25ee24c8241/fastsafetensors-0.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:acc0971b006f381598d32c5f7aae3a07d9a10d87c701f26b65215083e15a4733", size = 1808292, upload-time = "2026-05-06T08:48:51.12Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/eaa10409a526242253926fe6981c652dfdb8aa4ec0d4cba4077a9376a1fd/fastsafetensors-0.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32cf4b531b5d77de41d777106ea69036a16bdea80062646dabc93a11b4cf88ee", size = 1828304, upload-time = "2026-05-06T08:48:53.15Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/909871d673bacd6dfc7fee5e59bcd4ec9fbd19775bafe567ad236a3adced/fastsafetensors-0.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac76f33e47959b7c31658fbbda1805df7540819828a3ce6a94eb34b4db0b1fa7", size = 1854825, upload-time = "2026-05-06T08:48:54.452Z" }, + { url = "https://files.pythonhosted.org/packages/84/f0/a9ec204e866b52ce323bafc31f4dbf15581b3c998d597ec53c62012716ff/fastsafetensors-0.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c9f8d3e18969090b5d66581cb6e9f6c9057158c151ed87e9c309dead5c64442", size = 1854843, upload-time = "2026-05-06T08:48:55.921Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/00acc9e8f8209f513bf504371f0c40f4073b90240ef3e73507461d2390b9/fastsafetensors-0.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:348814ad027891317ad8aff90a3c62f8f704a0861d089ea33cff088980602355", size = 1852255, upload-time = "2026-05-06T08:48:57.442Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flashinfer-cubin" +version = "0.6.8.post1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b7/5e3b1a8c67031b421a8bd29c2bc29b900a550bb3392e8bda18bb15b5e476/flashinfer_cubin-0.6.8.post1-py3-none-any.whl", hash = "sha256:43636d4cd39e694a83d76a89f87fefcdf4cecb4c4f7dd22dac25ec368c1e901f", size = 295154113, upload-time = "2026-04-18T18:28:21.738Z" }, +] + +[[package]] +name = "flashinfer-python" +version = "0.6.8.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "click" }, + { name = "cuda-tile" }, + { name = "einops" }, + { name = "ninja" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl" }, + { name = "nvidia-ml-py" }, + { name = "packaging" }, + { name = "requests" }, + { name = "tabulate" }, + { name = "torch" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/1e/2760fef9e74abc4480961048e5790b4c9e955872fb4d7d97900cfddced5a/flashinfer_python-0.6.8.post1.tar.gz", hash = "sha256:b18e4121baf9b93fa9a9f368ba9b981a0342895f50ab9dddc224aeb964ed346f", size = 6675885, upload-time = "2026-04-18T18:28:13.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/6d/1e8a8533913e33a50a486332ce0673f4fdb860f6eb9ed450327c5c1762cb/flashinfer_python-0.6.8.post1-py3-none-any.whl", hash = "sha256:818f9b8cc2fe66c42a1f6264be4841ac8821ada703685a02cfccb2b5124a710b", size = 9385316, upload-time = "2026-04-18T18:28:10.285Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "gguf" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ae/17f1308ae45cd7b08ebb521747d5b23f4efc4d172038a4e228dd5106c3ff/gguf-0.19.0.tar.gz", hash = "sha256:dbadcd6cc7ccd44256f2229fe7c2dff5e8aa5cf0612ab987fd2b1a57e428923f", size = 111220, upload-time = "2026-05-06T13:04:03.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/bb/d71d6da82763528c2c2ed6b59a9d6142c6595545a4c448e2085d155e88c2/gguf-0.19.0-py3-none-any.whl", hash = "sha256:70bcd10edfe697fb2dad6e40af2234b9d8ece9a41a99761405121ebda1c3c1cd", size = 118475, upload-time = "2026-05-06T13:04:02.588Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "ijson" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/32/21c1b47a1afb7319944d0b9685c0997a9d574a77b030c82f6a1ac2cef4eb/ijson-3.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea8dcac10d86adaeead454bc25c97b68d0bda573d5fd6f86f5e21cf8f7906f88", size = 88935, upload-time = "2026-02-24T03:56:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/6ac7ebbb3cd767c87cdcbb950a6754afd1c0977756347bfe03eb8e5b866d/ijson-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92b0495bbb2150bbf14fc5d98fb6d76bcd1c526605a172709e602e6fedc96495", size = 60567, upload-time = "2026-02-24T03:56:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/c4/98/1140de9ae872468a8bc2e87c171228e25e58b1eb696b7fb430f7590fea44/ijson-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af0c4c8943be8b09a4e57bdc1da6001dae7b36526d4154fe5c8224738d0921f", size = 60620, upload-time = "2026-02-24T03:56:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/60/e1/67dfe0774e4c7ca6ec8702e280e8764d356f3db54358999818cda6df7679/ijson-3.5.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45887d5e84ff0d2b138c926cebd9071830733968afe8d9d12080b3c178c7f918", size = 126558, upload-time = "2026-02-24T03:56:43.922Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ef/23d614fc773d428caeb6e197218b7e32adcc668ff5b98777039149571208/ijson-3.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a70b575be8e57a28c80e90ed349ad3a851c3478524c70e36e07d6092ecd12c9", size = 133091, upload-time = "2026-02-24T03:56:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b8/80/99727603cd8a1d32edafa4392f4056b2420bf48c15afd34481c68a2d4435/ijson-3.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2adeecd45830bfd5580ca79a584154713aabef0b9607e16249133df5d2859813", size = 130249, upload-time = "2026-02-24T03:56:46.333Z" }, + { url = "https://files.pythonhosted.org/packages/0b/94/3a3d623ca80768e834be8a834ef05960e3b9e79af1a911704ff10c9e8792/ijson-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d873e72889e7fc5962ab58909f1adff338d7c2f49e450e5b5fe844eff8155a14", size = 133501, upload-time = "2026-02-24T03:56:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f6/df2c14ad340834eccee379046f155e4b66a16ddafd445429dee7b3323614/ijson-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9a88c559456a79708592234d697645d92b599718f4cbbeaa6515f83ac63ca0ae", size = 128438, upload-time = "2026-02-24T03:56:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/9ff5b8b5fee113f5607bc4149b707382a898eeb545153189b075e5ec8d59/ijson-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf83f58ad50dc0d39a2105cb26d4f359b38f42cef68b913170d4d47d97d97ba5", size = 131116, upload-time = "2026-02-24T03:56:49.737Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/954ce0d440d7cf72a3d8361b14406f9cdbf624b1625c10f8488857c769d6/ijson-3.5.0-cp310-cp310-win32.whl", hash = "sha256:aec4580a7712a19b1f95cd41bed260fc6a31266d37ef941827772a4c199e8143", size = 52724, upload-time = "2026-02-24T03:56:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/24/33/ece87d60502c6115642cbabeb8c122fa982212b392bc4f4ff5aab8e02dac/ijson-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a9c4c70501e23e8eb1675330686d1598eebfa14b6f0dbc8f00c2e081cc628fa", size = 55125, upload-time = "2026-02-24T03:56:51.942Z" }, + { url = "https://files.pythonhosted.org/packages/65/da/644343198abca5e0f6e2486063f8d8f3c443ca0ef5e5c890e51ef6032e33/ijson-3.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5616311404b858d32740b7ad8b9a799c62165f5ecb85d0a8ed16c21665a90533", size = 88964, upload-time = "2026-02-24T03:56:53.099Z" }, + { url = "https://files.pythonhosted.org/packages/5b/63/8621190aa2baf96156dfd4c632b6aa9f1464411e50b98750c09acc0505ea/ijson-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9733f94029dd41702d573ef64752e2556e72aea14623d6dbb7a44ca1ccf30fd", size = 60582, upload-time = "2026-02-24T03:56:54.261Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/6a3f041fdd17dacff33b7d7d3ba3df6dca48740108340c6042f974b2ad20/ijson-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db8398c6721b98412a4f618da8022550c8b9c5d9214040646071b5deb4d4a393", size = 60632, upload-time = "2026-02-24T03:56:55.159Z" }, + { url = "https://files.pythonhosted.org/packages/e4/68/474541998abbdecfd46a744536878335de89aceb9f085bff1aaf35575ceb/ijson-3.5.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c061314845c08163b1784b6076ea5f075372461a32e6916f4e5f211fd4130b64", size = 131988, upload-time = "2026-02-24T03:56:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/e05ff8b72a44fe9d192f41c5dcbc35cfa87efc280cdbfe539ffaf4a7535e/ijson-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1111a1c5ac79119c5d6e836f900c1a53844b50a18af38311baa6bb61e2645aca", size = 138669, upload-time = "2026-02-24T03:56:57.555Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/955a83b031102c7a602e2c06d03aff0a0e584212f09edb94ccc754d203ac/ijson-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e74aff8c681c24002b61b1822f9511d4c384f324f7dbc08c78538e01fdc9fcb", size = 135093, upload-time = "2026-02-24T03:56:59.267Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f2/30250cfcb4d2766669b31f6732689aab2bb91de426a15a3ebe482df7ee48/ijson-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:739a7229b1b0cc5f7e2785a6e7a5fc915e850d3fed9588d0e89a09f88a417253", size = 138715, upload-time = "2026-02-24T03:57:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/a2/05/785a145d7e75e04e04480d59b6323cd4b1d9013a6cd8643fa635fbc93490/ijson-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ef88712160360cab3ca6471a4e5418243f8b267cf1fe1620879d1b5558babc71", size = 133194, upload-time = "2026-02-24T03:57:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/eb/80d6f8a748dead4034cea0939494a67d10ccf88d6413bf6e860393139676/ijson-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ca0d1b6b5f8166a6248f4309497585fb8553b04bc8179a0260fad636cfdb798", size = 135588, upload-time = "2026-02-24T03:57:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/bbc21f9400ebdbca48fab272593e0d1f875691be1e927d264d90d48b8c47/ijson-3.5.0-cp311-cp311-win32.whl", hash = "sha256:966039cf9047c7967febf7b9a52ec6f38f5464a4c7fbb5565e0224b7376fefff", size = 52721, upload-time = "2026-02-24T03:57:04.365Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2e/4e8c0208b8f920ee80c88c956f93e78318f2cfb646455353b182738b490c/ijson-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bad6a1634cb7c9f3f4c7e52325283b35b565f5b6cc27d42660c6912ce883422", size = 55121, upload-time = "2026-02-24T03:57:05.498Z" }, + { url = "https://files.pythonhosted.org/packages/aa/17/9c63c7688025f3a8c47ea717b8306649c8c7244e49e20a2be4e3515dc75c/ijson-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ebefbe149a6106cc848a3eaf536af51a9b5ccc9082de801389f152dba6ab755", size = 88536, upload-time = "2026-02-24T03:57:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/e15c2400244c117b06585452ebc63ae254f5a6964f712306afd1422daae0/ijson-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e30d9f00f82e64de689c0b8651b9cfed879c184b139d7e1ea5030cec401c21", size = 60499, upload-time = "2026-02-24T03:57:09.155Z" }, + { url = "https://files.pythonhosted.org/packages/77/a9/bf4fe3538a0c965f16b406f180a06105b875da83f0743e36246be64ef550/ijson-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a04a33ee78a6f27b9b8528c1ca3c207b1df3b8b867a4cf2fcc4109986f35c227", size = 60330, upload-time = "2026-02-24T03:57:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/a7254a065933c0e2ffd3586f46187d84830d3d7b6f41cfa5901820a4f87d/ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c", size = 53079, upload-time = "2026-02-24T03:57:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/2edca79b359fc9f95d774616867a03ecccdf333797baf5b3eea79733918c/ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944", size = 55500, upload-time = "2026-02-24T03:57:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/d67e764a712c3590627480643a3b51efcc3afa4ef3cb54ee4c989073c97e/ijson-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e9cedc10e40dd6023c351ed8bfc7dcfce58204f15c321c3c1546b9c7b12562a4", size = 88544, upload-time = "2026-02-24T03:57:21.293Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/f1c299371686153fa3cf5c0736b96247a87a1bee1b7145e6d21f359c505a/ijson-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3647649f782ee06c97490b43680371186651f3f69bebe64c6083ee7615d185e5", size = 60495, upload-time = "2026-02-24T03:57:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/16/94/b1438e204d75e01541bebe3e668fe3e68612d210e9931ae1611062dd0a56/ijson-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90e74be1dce05fce73451c62d1118671f78f47c9f6be3991c82b91063bf01fc9", size = 60325, upload-time = "2026-02-24T03:57:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/30/e2/4aa9c116fa86cc8b0f574f3c3a47409edc1cd4face05d0e589a5a176b05d/ijson-3.5.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78e9ad73e7be2dd80627504bd5cbf512348c55ce2c06e362ed7683b5220e8568", size = 138774, upload-time = "2026-02-24T03:57:24.683Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/738b88752a70c3be1505faa4dcd7110668c2712e582a6a36488ed1e295d4/ijson-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9577449313cc94be89a4fe4b3e716c65f09cc19636d5a6b2861c4e80dddebd58", size = 149820, upload-time = "2026-02-24T03:57:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/0b3ab9f393ca8f72ea03bc896ba9fdc987e90ae08cdb51c32a4ee0c14d5e/ijson-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e4c1178fb50aff5f5701a30a5152ead82a14e189ce0f6102fa1b5f10b2f54ff", size = 149747, upload-time = "2026-02-24T03:57:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/b0037119f75131b78cb00acc2657b1a9d0435475f1f2c5f8f5a170b66b9c/ijson-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0eb402ab026ffb37a918d75af2b7260fe6cfbce13232cc83728a714dd30bd81d", size = 151027, upload-time = "2026-02-24T03:57:28.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/a0/cb344de1862bf09d8f769c9d25c944078c87dd59a1b496feec5ad96309a4/ijson-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b08ee08355f9f729612a8eb9bf69cc14f9310c3b2a487c6f1c3c65d85216ec4", size = 142996, upload-time = "2026-02-24T03:57:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/ca/32/a8ffd67182e02ea61f70f62daf43ded4fa8a830a2520a851d2782460aba8/ijson-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bda62b6d48442903e7bf56152108afb7f0f1293c2b9bef2f2c369defea76ab18", size = 152068, upload-time = "2026-02-24T03:57:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/3578df8e75d446aab0ae92e27f641341f586b85e1988536adebc65300cb4/ijson-3.5.0-cp313-cp313-win32.whl", hash = "sha256:8d073d9b13574cfa11083cc7267c238b7a6ed563c2661e79192da4a25f09c82c", size = 53065, upload-time = "2026-02-24T03:57:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a2/f7cdaf5896710da3e69e982e44f015a83d168aa0f3a89b6f074b5426779d/ijson-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:2419f9e32e0968a876b04d8f26aeac042abd16f582810b576936bbc4c6015069", size = 55499, upload-time = "2026-02-24T03:57:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/13e2492d17e19a2084523e18716dc2809159f2287fd2700c735f311e76c4/ijson-3.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4d4b0cd676b8c842f7648c1a783448fac5cd3b98289abd83711b3e275e143524", size = 93019, upload-time = "2026-02-24T03:57:33.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/483fc97ece0c3f1cecabf48f6a7a36e89d19369eec462faaeaa34c788992/ijson-3.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:252dec3680a48bb82d475e36b4ae1b3a9d7eb690b951bb98a76c5fe519e30188", size = 62714, upload-time = "2026-02-24T03:57:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/793fe020a0fe9d9eed4c285cf4a5cfdb0a935708b3bde0d72f35c794b513/ijson-3.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:aa1b5dca97d323931fde2501172337384c958914d81a9dac7f00f0d4bfc76bc7", size = 62460, upload-time = "2026-02-24T03:57:35.874Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/f1a2690aa8d4df1f4e262b385e65a933ffdc250b091531bac9a449c19e16/ijson-3.5.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7a5ec7fd86d606094bba6f6f8f87494897102fa4584ef653f3005c51a784c320", size = 199273, upload-time = "2026-02-24T03:57:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/f1346d5299e79b988ab472dc773d5381ec2d57c23cb2f1af3ede4a810e62/ijson-3.5.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:009f41443e1521847701c6d87fa3923c0b1961be3c7e7de90947c8cb92ea7c44", size = 216884, upload-time = "2026-02-24T03:57:38.346Z" }, + { url = "https://files.pythonhosted.org/packages/28/3c/8b637e869be87799e6c2c3c275a30a546f086b1aed77e2b7f11512168c5a/ijson-3.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4c3651d1f9fe2839a93fdf8fd1d5ca3a54975349894249f3b1b572bcc4bd577", size = 207306, upload-time = "2026-02-24T03:57:39.718Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/18b1c1df6951ca056782d7580ec40cea4ff9a27a0947d92640d1cc8c4ae3/ijson-3.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:945b7abcfcfeae2cde17d8d900870f03536494245dda7ad4f8d056faa303256c", size = 211364, upload-time = "2026-02-24T03:57:40.953Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/e795812e82851574a9dba8a53fde045378f531ef14110c6fb55dbd23b443/ijson-3.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0574b0a841ff97495c13e9d7260fbf3d85358b061f540c52a123db9dbbaa2ed6", size = 200608, upload-time = "2026-02-24T03:57:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/013c85b4749b57a4cb4c2670014d1b32b8db4ab1a7be92ea7aeb5d7fe7b5/ijson-3.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f969ffb2b89c5cdf686652d7fb66252bc72126fa54d416317411497276056a18", size = 205127, upload-time = "2026-02-24T03:57:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7c/faf643733e3ab677f180018f6a855c4ef70b7c46540987424c563c959e42/ijson-3.5.0-cp313-cp313t-win32.whl", hash = "sha256:59d3f9f46deed1332ad669518b8099920512a78bda64c1f021fcd2aff2b36693", size = 55282, upload-time = "2026-02-24T03:57:44.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/22/94ddb47c24b491377aca06cd8fc9202cad6ab50619842457d2beefde21ea/ijson-3.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c2839fa233746d8aad3b8cd2354e441613f5df66d721d59da4a09394bd1db2b", size = 58016, upload-time = "2026-02-24T03:57:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/7a/93/0868efe753dc1df80cc405cf0c1f2527a6991643607c741bff8dcb899b3b/ijson-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25a5a6b2045c90bb83061df27cfa43572afa43ba9408611d7bfe237c20a731a9", size = 89094, upload-time = "2026-02-24T03:57:46.115Z" }, + { url = "https://files.pythonhosted.org/packages/24/94/fd5a832a0df52ef5e4e740f14ac8640725d61034a1b0c561e8b5fb424706/ijson-3.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8976c54c0b864bc82b951bae06567566ac77ef63b90a773a69cd73aab47f4f4f", size = 60715, upload-time = "2026-02-24T03:57:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/1b9a90af5732491f9eec751ee211b86b11011e1158c555c06576d52c3919/ijson-3.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:859eb2038f7f1b0664df4241957694cc35e6295992d71c98659b22c69b3cbc10", size = 60638, upload-time = "2026-02-24T03:57:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/2c551ea980fe56f68710a8d5389cfbd015fc45aaafd17c3c52c346db6aa1/ijson-3.5.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c911aa02991c7c0d3639b6619b93a93210ff1e7f58bf7225d613abea10adc78e", size = 140667, upload-time = "2026-02-24T03:57:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/27b887879ba6a5bc29766e3c5af4942638c952220fd63e1e442674f7883a/ijson-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903cbdc350173605220edc19796fbea9b2203c8b3951fb7335abfa8ed37afda8", size = 149850, upload-time = "2026-02-24T03:57:50.329Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/23e10e1bc04bf31193b21e2960dce14b17dbd5d0c62204e8401c59d62c08/ijson-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4549d96ded5b8efa71639b2160235415f6bdb8c83367615e2dbabcb72755c33", size = 149206, upload-time = "2026-02-24T03:57:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/8e/90/e552f6495063b235cf7fa2c592f6597c057077195e517b842a0374fd470c/ijson-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b2dcf6349e6042d83f3f8c39ce84823cf7577eba25bac5aae5e39bbbbbe9c1c", size = 150438, upload-time = "2026-02-24T03:57:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/5c/18/45bf8f297c41b42a1c231d261141097babd953d2c28a07be57ae4c3a1a02/ijson-3.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e44af39e6f8a17e5627dcd89715d8279bf3474153ff99aae031a936e5c5572e5", size = 144369, upload-time = "2026-02-24T03:57:53.22Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/deb9772bb2c0cead7ad64f00c3598eec9072bdf511818e70e2c512eeabbe/ijson-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9260332304b7e7828db56d43f08fc970a3ab741bf84ff10189361ea1b60c395b", size = 151352, upload-time = "2026-02-24T03:57:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/67f4d80cd58ad7eab0cd1af5fe28b961886338956b2f88c0979e21914346/ijson-3.5.0-cp314-cp314-win32.whl", hash = "sha256:63bc8121bb422f6969ced270173a3fa692c29d4ae30c860a2309941abd81012a", size = 53610, upload-time = "2026-02-24T03:57:55.655Z" }, + { url = "https://files.pythonhosted.org/packages/70/d3/263672ea22983ba3940f1534316dbc9200952c1c2a2332d7a664e4eaa7ae/ijson-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:01b6dad72b7b7df225ef970d334556dfad46c696a2c6767fb5d9ed8889728bca", size = 56301, upload-time = "2026-02-24T03:57:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/86f7fac35e0835faa188085ae0579e813493d5261ce056484015ad533445/ijson-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2ea4b676ec98e374c1df400a47929859e4fa1239274339024df4716e802aa7e4", size = 93069, upload-time = "2026-02-24T03:57:57.849Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/e7366ed9c6e60228d35baf4404bac01a126e7775ea8ce57f560125ed190a/ijson-3.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:014586eec043e23c80be9a923c56c3a0920a0f1f7d17478ce7bc20ba443968ef", size = 62767, upload-time = "2026-02-24T03:57:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/3e703e8cc4b3ada79f13b28070b51d9550c578f76d1968657905857b2ddd/ijson-3.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5b8b886b0248652d437f66e7c5ac318bbdcb2c7137a7e5327a68ca00b286f5f", size = 62467, upload-time = "2026-02-24T03:58:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/0c91af32c1ee8a957fdac2e051b5780756d05fd34e4b60d94a08d51bac1d/ijson-3.5.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:498fd46ae2349297e43acf97cdc421e711dbd7198418677259393d2acdc62d78", size = 200447, upload-time = "2026-02-24T03:58:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/796ea0e391b7e2d45c5b1b451734bba03f81c2984cf955ea5eaa6c4920ad/ijson-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a51b4f9b81f12793731cf226266d1de2112c3c04ba4a04117ad4e466897e05", size = 217820, upload-time = "2026-02-24T03:58:02.598Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9636c710dc4ac4a281baa266a64f323b4cc165cec26836af702c44328b59a515", size = 208310, upload-time = "2026-02-24T03:58:04.759Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ad/8b3105a78774fd4a65e534a21d975ef3a77e189489fe3029ebcaeba5e243/ijson-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7168a39e8211107666d71b25693fd1b2bac0b33735ef744114c403c6cac21e1", size = 211843, upload-time = "2026-02-24T03:58:05.836Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/a2739f6072d6e1160581bc3ed32da614c8cced023dcd519d9c5fa66e0425/ijson-3.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8696454245415bc617ab03b0dc3ae4c86987df5dc6a90bad378fe72c5409d89e", size = 200906, upload-time = "2026-02-24T03:58:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5e/e06c2de3c3d4a9cfb655c1ad08a68fb72838d271072cdd3196576ac4431a/ijson-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c21bfb61f71f191565885bf1bc29e0a186292d866b4880637b833848360bdc1b", size = 205495, upload-time = "2026-02-24T03:58:09.163Z" }, + { url = "https://files.pythonhosted.org/packages/7c/11/778201eb2e202ddd76b36b0fb29bf3d8e3c167389d8aa883c62524e49f47/ijson-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:a2619460d6795b70d0155e5bf016200ac8a63ab5397aa33588bb02b6c21759e6", size = 56280, upload-time = "2026-02-24T03:58:10.116Z" }, + { url = "https://files.pythonhosted.org/packages/23/28/96711503245339084c8086b892c47415895eba49782d6cc52d9f4ee50301/ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854", size = 58965, upload-time = "2026-02-24T03:58:11.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3b/d31ecfa63a218978617446159f3d77aab2417a5bd2885c425b176353ff78/ijson-3.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d64c624da0e9d692d6eb0ff63a79656b59d76bf80773a17c5b0f835e4e8ef627", size = 57715, upload-time = "2026-02-24T03:58:24.545Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/b170e646d378e8cccf9637c05edb5419b00c2c4df64b0258c3af5355608e/ijson-3.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:876f7df73b7e0d6474f9caa729b9cdbfc8e76de9075a4887dfd689e29e85c4ca", size = 57205, upload-time = "2026-02-24T03:58:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/ef/83/44dbd0231b0a8c6c14d27473d10c4e27dfbce7d5d9a833c79e3e6c33eb40/ijson-3.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e7dbff2c8d9027809b0cde663df44f3210da10ea377121d42896fb6ee405dd31", size = 71229, upload-time = "2026-02-24T03:58:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/cf84048b7c6cec888826e696a31f45bee7ebcac15e532b6be1fc4c2c9608/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4217a1edc278660679e1197c83a1a2a2d367792bfbb2a3279577f4b59b93730d", size = 71217, upload-time = "2026-02-24T03:58:28.021Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0a/e34c729a87ff67dc6540f6bcc896626158e691d433ab57db0086d73decd2/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04f0fc740311388ee745ba55a12292b722d6f52000b11acbb913982ba5fbdf87", size = 68618, upload-time = "2026-02-24T03:58:28.918Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0f/e849d072f2e0afe49627de3995fc9dae54b4c804c70c0840f928d95c10e1/ijson-3.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fdeee6957f92e0c114f65c55cf8fe7eabb80cfacab64eea6864060913173f66d", size = 55369, upload-time = "2026-02-24T03:58:29.839Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "interegular" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9d/8b6dde58a028a3962ce17e84d5fe73758df61378e00ef8ac3d85da34b0ff/interegular-0.3.3.tar.gz", hash = "sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600", size = 24705, upload-time = "2024-01-06T23:01:22.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/01/72d6472f80651673716d1deda2a5bbb633e563ecf94f4479da5519d69d25/interegular-0.3.3-py37-none-any.whl", hash = "sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c", size = 23635, upload-time = "2024-01-06T23:01:20.829Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, + { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, + { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + +[[package]] +name = "llguidance" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/48/3f7a9d3ff1b36bba92b5107a3a21286821227afe9ea464736133994d61fb/llguidance-1.3.0.tar.gz", hash = "sha256:861249afd51dc325646834462ea827e57a5c2b2042e108e6aae7059fdad9104d", size = 1070460, upload-time = "2025-10-20T19:58:44.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/33/be5acb85cd8cdc4afde33d9c234eece9f318e087920255af3c05864cd3e7/llguidance-1.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7685222660a762e481ac633d49cc559c64980fe2ee59c8f932a5bb5cbc0c2c2", size = 3220647, upload-time = "2025-10-20T19:58:42.542Z" }, + { url = "https://files.pythonhosted.org/packages/82/e6/b48bda5b15efeaeb62bd0dba8fc6a01d4ae5457a85dbb5d18632385fe15c/llguidance-1.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:098030ff0687261a3f1bd54cf21fe951fc861d56d37a0671250dd36677eaf224", size = 3099830, upload-time = "2025-10-20T19:58:40.826Z" }, + { url = "https://files.pythonhosted.org/packages/aa/11/44389d3d1526d7a5c38ffd587a5ebc61d7bee443ac1dea95f2089ad58f5f/llguidance-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f6caca5d78db7f76e1fbb0fff8607b861c32d47fa3d5dee2fc49de27ee269df", size = 2835242, upload-time = "2025-10-20T19:58:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/1ff2bedb8f9acb46a2d2d603415d272bb622c142ea86f5b95445cc6e366c/llguidance-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc17e9dd602c3879bf91664a64bf72f54c74dbfbeb24ccfab6a5fe435b12f7aa", size = 3033133, upload-time = "2025-10-20T19:58:38.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/809349638231f469b9056c0e1bfd924d5ef5558b3b3ec72d093b6fad33b1/llguidance-1.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:1d1cd1c8618d1a13605d3e057c978651e551c8c469b481ee4041f1d6c436002d", size = 2789946, upload-time = "2025-10-20T19:58:45.958Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[package]] +name = "lm-format-enforcer" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/ef/11292bb0b85cf4c93447cab5a29f64576ed14d3ab4280e35ddd23486594a/lm_format_enforcer-0.11.3-py3-none-any.whl", hash = "sha256:cf586350875def1ae7a8fba84fcbbfc8371424b6c9d05c1fcba70aa233fbf06f", size = 45418, upload-time = "2025-08-24T19:37:46.325Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistral-common" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydantic-extra-types", extra = ["pycountry"] }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/12167a1bea9714582e5b4f539f9c019323363e314a499c72855ff0e5ad43/mistral_common-1.11.2.tar.gz", hash = "sha256:79f68fc2d1190f28637f40e053f919c8c2697e00b2aa679ddee562a95183f4ad", size = 6357845, upload-time = "2026-05-04T19:47:40.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/f0/6a5d604b972e442b9d36c117d01788feddad099e4965699e3516ee6fefc3/mistral_common-1.11.2-py3-none-any.whl", hash = "sha256:ebb42062cd705a0aa2bc69b4cde2b83d446ae58150b7e29322c90cb08fcfca6c", size = 6531968, upload-time = "2026-05-04T19:47:37.718Z" }, +] + +[package.optional-dependencies] +image = [ + { name = "opencv-python-headless" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "model-hosting-container-standards" +version = "0.1.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "jmespath" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "starlette" }, + { name = "supervisor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/5a/d669bdeb5ba96db42c6ef010835a25119b05f8c35ee5f1c3f715626625fe/model_hosting_container_standards-0.1.15.tar.gz", hash = "sha256:ae8dd74d3250545c14f0a7068186c7b0f0ab6563d31e7137f556b6b660c8a6a9", size = 93994, upload-time = "2026-05-05T18:22:29.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/26/c7aea197f1719f31d0dd686eb4475982fe9efd7668ce259cb52b62c676b6/model_hosting_container_standards-0.1.15-py3-none-any.whl", hash = "sha256:849e08c4732203ee861c8c24966b4e916ea4420fa324b430f7f74a1e1fe8811a", size = 125418, upload-time = "2026-05-05T18:22:27.819Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/38/d591d9f66d43d897ecbd249f2833665823d19c8b043f16619bc8343e23df/msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064", size = 195172, upload-time = "2026-04-12T21:43:45.062Z" }, + { url = "https://files.pythonhosted.org/packages/69/1a/6899188b5982ec1324e0c629b7801eed2db987f6634fab58abd9fc82d317/msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238", size = 188316, upload-time = "2026-04-12T21:43:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" }, + { url = "https://files.pythonhosted.org/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/27021d1c3e5da837743092a7b7a5e8818397e1f4c05ee8b068bd7d1fd78a/msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449", size = 188392, upload-time = "2026-04-12T21:43:53.384Z" }, + { url = "https://files.pythonhosted.org/packages/80/2b/daf7a8d6d7cf00e0dcd0439178b284ade701234abdcadf3385601da04fbd/msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e", size = 174191, upload-time = "2026-04-12T21:43:54.6Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554, upload-time = "2026-04-12T21:44:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517, upload-time = "2026-04-12T21:44:05.66Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "numba" +version = "0.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/61/7299643b9c18d669e04be7c5bcb64d985070d07553274817b45b049e7bfe/numba-0.65.0.tar.gz", hash = "sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b", size = 2764131, upload-time = "2026-04-01T03:52:01.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9b/e8453d93d5cb3f53cc956f135024be09d52f4f99643acaf8fdca090a8f3c/numba-0.65.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:dff9fd5fbc9a35c517359c5823ea705d9b65f01fb46e42e35a2eabe5a52c2e96", size = 2680537, upload-time = "2026-04-01T03:51:17.325Z" }, + { url = "https://files.pythonhosted.org/packages/07/95/d6a2f0625e1092624228301eea11cdaff21ddcaf917ef3d631846a38b2f4/numba-0.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4c894c94afa5ffd627c7e3b693df10cb0d905bd5eb06de3dfc31775140cf4f89", size = 3739444, upload-time = "2026-04-01T03:51:19.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/fe518c97af035e4ec670c2edc3f0ff7a518cbed2f0b5053124d7c979bd8a/numba-0.65.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7325b1aab88f0339057288ee32f39dc660e14f93872a6fda14fa6eb9f95b047", size = 3446390, upload-time = "2026-04-01T03:51:21.55Z" }, + { url = "https://files.pythonhosted.org/packages/d0/06/5010939854249c290c6217e3fb7404914f4ed953f9923e340c3e166bcaf0/numba-0.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:71e72e9ca2f619df4768f9c3962bfec60191a5a26fe2b6a8c6a07532b6146169", size = 2747200, upload-time = "2026-04-01T03:51:23.674Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ce/d67c499703eb5479ce02420e8ccd65c5753d87d2e16d563f152d71405346/numba-0.65.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:28e547d0b18024f19cbaf9de02fc5c145790213d9be8a2c95b43f93ec162b9e4", size = 2680228, upload-time = "2026-04-01T03:51:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a7/11e2b24251d57cf41fc9ad83f378d890d61a890e3f8eb6338b39833f67a4/numba-0.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:032b0b8e879512cd424d79eed6d772a1399c6387ded184c2cf3cc22c08d750a6", size = 3744674, upload-time = "2026-04-01T03:51:27.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0b/7c63eb742859a6243f42288441f65ac9dac96ea59f409e43b713aafbe867/numba-0.65.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af143d823624033a128b5950c0aaf9ffc2386dfe954eb757119cf0432335534c", size = 3450620, upload-time = "2026-04-01T03:51:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/1371cbbe955be340a46093a10b61462437e0fadc7a63290473a0e584cb03/numba-0.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:15d159578e59a39df246b83480f78d7794b0fca40153b5684d3849a99c48a0fb", size = 2747081, upload-time = "2026-04-01T03:51:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2f/8bd31a1ea43c01ac215283d83aa5f8d5acbe7a36c85b82f1757bfe9ccb31/numba-0.65.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa", size = 2680705, upload-time = "2026-04-01T03:51:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/88406bd58600cc696417b8e5dd6a056478da808f3eaf48d18e2421e0c2d9/numba-0.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f", size = 3801411, upload-time = "2026-04-01T03:51:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/61/ce753a1d7646dd477e16d15e89473703faebb8995d2f71d7ad69a540b565/numba-0.65.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e", size = 3501622, upload-time = "2026-04-01T03:51:36.348Z" }, + { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/eee0f1ff456218db036bfc9023995ec1f85a9dc8f2422f1594f6a87829e0/numba-0.65.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c6334094563a456a695c812e6846288376ca02327cf246cdcc83e1bb27862367", size = 2680679, upload-time = "2026-04-01T03:51:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8f/3d116e4b8e92f6abace431afa4b2b944f4d65bdee83af886f5c4b263df95/numba-0.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b8a9008411615c69d083d1dcf477f75a5aa727b30beb16e139799e2be945cdfd", size = 3809537, upload-time = "2026-04-01T03:51:41.42Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/6a3ca4128e253cb67affe06deb47688f51ce968f5111e2a06d010e6f1fa6/numba-0.65.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af96c0cba53664efcb361528b8c75e011a6556c859c7e08424c2715201c6cf7a", size = 3508615, upload-time = "2026-04-01T03:51:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/96/0e/267f9a36fb282c104a971d7eecb685b411c47dce2a740fe69cf5fc2945d9/numba-0.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:6254e73b9c929dc736a1fbd3d6f5680789709a5067cae1fa7198707385129c04", size = 2749938, upload-time = "2026-04-01T03:51:45.218Z" }, + { url = "https://files.pythonhosted.org/packages/56/a4/90edb01e9176053578e343d7a7276bc28356741ee67059aed8ed2c1a4e59/numba-0.65.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:ee336b398a6fca51b1f626034de99f50cb1bd87d537a166275158a3cee744b82", size = 2680878, upload-time = "2026-04-01T03:51:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/e12d6ff4b9119db3cbf7b2db1ce257576441bd3c76388c786dea74f20b02/numba-0.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05c0a9fdf75d85f57dee47b719e8d6415707b80aae45d75f63f9dc1b935c29f7", size = 3778456, upload-time = "2026-04-01T03:51:48.552Z" }, + { url = "https://files.pythonhosted.org/packages/17/89/abcd83e76f6a773276fe76244140671bcc5bf820f6e2ae1a15362ae4c8c9/numba-0.65.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:583680e0e8faf124d362df23b4b593f3221a8996341a63d1b664c122401bec2f", size = 3478464, upload-time = "2026-04-01T03:51:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/fbce55ce3d933afbc7ade04df826853e4a846aaa47d58d2fbb669b8f2d08/numba-0.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:add297d3e1c08dd884f44100152612fa41e66a51d15fdf91307f9dde31d06830", size = 2752012, upload-time = "2026-04-01T03:51:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/af705f4257d9388fb2fd6d7416573e98b6ca9c786e8b58f02720978557bd/numba-0.65.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:194a243ba53a9157c8538cbb3166ec015d785a8c5d584d06cdd88bee902233c7", size = 2683961, upload-time = "2026-04-01T03:51:54.281Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e5/8267b0adb0c01b52b553df5062fbbb42c30ed5362d08b85cc913a36f838f/numba-0.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7fa502960f7a2f3f5cb025bc7bff888a3551277b92431bfdc5ba2f11a375749", size = 3816373, upload-time = "2026-04-01T03:51:56.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f5/b8397ca360971669a93706b9274592b6864e4367a37d498fbbcb62aa2d48/numba-0.65.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5046c63f783ca3eb6195f826a50797465e7c4ce811daa17c9bea47e310c9b964", size = 3532782, upload-time = "2026-04-01T03:51:58.387Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1e73fa16bf0393ebb74c5bb208d712152ffdfc84600a8e93a3180317856e/numba-0.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46fd679ae4f68c7a5d5721efbd29ecee0b0f3013211591891d79b51bfdf73113", size = 2757611, upload-time = "2026-04-01T03:52:00.083Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cudnn-frontend" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/be/f5a1e633c524c13c0182213ab27dab42dca29a3c785be5ff74d2d185aed1/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:baa6fbc8e7c55f1c78c0374ed9a890e1cf81acaca0c92d6135d18a8e3c985244", size = 2023500, upload-time = "2026-01-27T23:31:34.747Z" }, + { url = "https://files.pythonhosted.org/packages/82/a7/765a17c6a9496196c34f269d17dfb902b6c618c0261c0962511e95302e81/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4bcca42259e358002c8867e3624a558f66cd5dff2cc6c3aafd860ef2f41730", size = 2154278, upload-time = "2026-01-27T23:06:55.784Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/7caae2243540bc60e47eae95f0fd913c9baa05cf94df0471914f70d45158/nvidia_cudnn_frontend-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:06252021ef1e5a7256f1e70429a426b01792636c05cc547fe8e64c6885a9652e", size = 1590158, upload-time = "2026-01-27T23:08:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9a/83d3d080118de4a7810fa019349edec634b8b37b9cafaacd05719de62dd6/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6d4d0b88d617b233a503c84980b54d840b60b2734497d1a7a071ec5293daec2", size = 2023709, upload-time = "2026-01-27T23:32:10.912Z" }, + { url = "https://files.pythonhosted.org/packages/13/c7/c3624b3ed77b102618f26295e816b27f1c3ebb1143730237a9f51d403c3f/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382ea063b92cbfd5b442cb75ff8422932d78276aecf139e46713ed1ad3d07af4", size = 2155568, upload-time = "2026-01-27T23:07:13.277Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/8613dfd029d076b86a8a87efe3f4bb4ab73cec15fa8fc27e665098f4d167/nvidia_cudnn_frontend-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:baa509effc4d299d3f04e549d4188f88bca8a8b527f483cbd2f66bc18f13a8b1", size = 1591244, upload-time = "2026-01-27T23:08:44.691Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b4/604e230378680ee117849a4e1045baca092f93161a829291a84d5acce70c/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:310b417f2848a83d1437203fcaeea320a74fb7f28af20bf42bf5afc9c01f1c12", size = 2027408, upload-time = "2026-01-27T23:32:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/c6/52/08f98262e77b1cbcc834cc1a5db494d0661ea1dbdea58c2e2d51a57fdaca/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c023539ca6de99234cf5102c3ec0d6af817f5396fc93028a22ba5b834a35b8a", size = 2159245, upload-time = "2026-01-27T23:07:32.664Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/751a5a8cfdc95fb4dc556192d37369ae488c30c473fe9a3ec720b23d07ea/nvidia_cudnn_frontend-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:e13f7dd46cdb4762dde87f181f06d1c5e15e9478bbdd547bfa74d9b11f415aae", size = 1591041, upload-time = "2026-01-27T23:09:04.118Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/db791a26ebb6a6e1268f518e18c82d8ad18546f7008f4b0d5bde15f927de/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a6e2b7bd43705ffa4af3b187374fdd5e7d09fc228a4d65fc8b4b0a537a8e605", size = 2027249, upload-time = "2026-01-27T23:33:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/19/74/3038cf496d5de7cfdff730f5202e438c17d9123de507059340e02ddff9d7/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0544206b02cae9da4f044ca3fe7416b99e0c8a8052285dd3e5a8fc445d34f9c", size = 2160001, upload-time = "2026-01-27T23:07:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5e/148cc6609dba326e620e4d949246020dfba05ca07d0387442e62b71d19b6/nvidia_cudnn_frontend-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:7eefa5f10cc003df5f3593f82f1ee6c001fc3412bdc78430c751914dfceefd7f", size = 1591270, upload-time = "2026-01-27T23:09:21.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0a/515209dd2afc6027bf1112bf415f575bfe9628d18877abe7424cb597dd7b/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b489da1b30f1d7da822b37b89cc4f68afd80e020eb57e4ab24921f8b57f6e946", size = 2028689, upload-time = "2026-02-11T21:32:04.235Z" }, + { url = "https://files.pythonhosted.org/packages/ab/57/52d18e1f50979eeabfafb408ec73068afc5a1e1ccd21636240317cd456d4/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37688c81a34ac590aff9de4c34d2968bab949411af707baa327616ebd4b34ae1", size = 2160182, upload-time = "2026-02-11T21:25:18.437Z" }, + { url = "https://files.pythonhosted.org/packages/67/53/df2810b56d259ef96fa6beaa1381bd14c29fbe82836b409516e864c5e177/nvidia_cudnn_frontend-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:5053b473fa74168b5fbf35934cd6187f88aa03b8447b9f2cd417332d5e5c9569", size = 1592759, upload-time = "2026-02-11T21:32:33.87Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cutlass-dsl-libs-base" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/a3/46fdf77d373b06bc65a0eda6c921c746985fb3e496c90a09be476291ea80/nvidia_cutlass_dsl-4.5.0-py3-none-any.whl", hash = "sha256:3b051fe02ca69422ab840e64d9865667aba288a3984a7ca4ccd038a82aef1344", size = 10178, upload-time = "2026-05-06T01:17:33.592Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4d/1d0dc5f36f929885417acfff02af94f61d49e6d34acb480c080d4d887555/nvidia_cutlass_dsl_libs_base-4.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c78b18f2b44ca10a91bc76380ebd65bb7b86aa97a9330bae9b73eb0a1bc51d55", size = 75636580, upload-time = "2026-05-06T01:23:19.17Z" }, + { url = "https://files.pythonhosted.org/packages/fe/81/1229637e8a14e1129475b8260a6ce66058148fa85faf10c94f9f95de5ef6/nvidia_cutlass_dsl_libs_base-4.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5cfdf52bea8feede5e512a094484956693cb87adaafa310991d2876653b1a88e", size = 74505679, upload-time = "2026-05-06T01:26:23.41Z" }, + { url = "https://files.pythonhosted.org/packages/b4/39/155dcbcf942b2c170aa0d1115ef5f2d358d9916ddc7200ab6e70541b97a0/nvidia_cutlass_dsl_libs_base-4.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f8635ad1e0a670323cc729f167067fa880cb56577ec2e79afb80a35ab371912e", size = 75635889, upload-time = "2026-05-06T01:25:20.572Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/2c2b3fc81a45a1bbbdcfd10c6d9793fd28848e6fefa6d4ed7c7c477f7d2a/nvidia_cutlass_dsl_libs_base-4.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7bb6de91b00a2b392cd834fec174a1461bf0f10a9b6d28086c8f4885aed27218", size = 74505494, upload-time = "2026-05-06T01:27:29.616Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d0/924048cfa43e1cb546735cb332b05a4fb92c63c1a1ac566f06445f9eca58/nvidia_cutlass_dsl_libs_base-4.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f7c133d31fa82ae7db697fd6943a5f9a2c97c8a40ee1056c67ef29fe00974d8", size = 75630723, upload-time = "2026-05-06T01:24:49.842Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8b/2c187400d85f7d2acb328f20499b7b05745dca8485cf6ad247d5f2b434cf/nvidia_cutlass_dsl_libs_base-4.5.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bd18322d9247f8c033a10ed4e519c4985ca6b4fb578ade382e5a264422ebd915", size = 74505487, upload-time = "2026-05-06T01:26:52.755Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2c/21d5fc62e030a43c0f1a3dab6749fb632026a27d6a60f59975cd29a5d165/nvidia_cutlass_dsl_libs_base-4.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:90a4d802a03963fa36eb287fbc9b40a1374590fc7e8cc1b9673dee8872f75713", size = 75632646, upload-time = "2026-05-06T01:24:19.623Z" }, + { url = "https://files.pythonhosted.org/packages/1c/79/0dca3b465711ffb4c44b4252940cc5f51d2d4905e405707e5c6c2a83d3d6/nvidia_cutlass_dsl_libs_base-4.5.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8e58b016da5bb09bd1d809d0c025433edb36b279adfbcd107e96361b214bd8bc", size = 74505936, upload-time = "2026-05-06T01:25:52.728Z" }, + { url = "https://files.pythonhosted.org/packages/59/85/2799e4de2fe7070cc4126ac501443d1cd7796b07ed880118e31956ae266a/nvidia_cutlass_dsl_libs_base-4.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2e121b20f0a48122c9b48227d00a7d681189e1de2fd4d211f9661a4e1658f066", size = 75634241, upload-time = "2026-05-06T01:22:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/04/c6/5aaa2dff6dfc615a83687df4462a91dad2ac1af85d6a9c91d9a6b9760a02/nvidia_cutlass_dsl_libs_base-4.5.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0a60dfce3349984315306ef719ed1edf0e225527158f26019a5cf266e06cc45d", size = 74504851, upload-time = "2026-05-06T01:23:48.613Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.595.45" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/49/c29f6e30d8662d2e94fef17739ea7309cc76aba269922ae999e4cc07f268/nvidia_ml_py-13.595.45.tar.gz", hash = "sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079", size = 50780, upload-time = "2026-03-19T16:59:44.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/24/fc256107d23597fa33d319505ce77160fa1a2349c096d01901ffc7cb7fc4/nvidia_ml_py-13.595.45-py3-none-any.whl", hash = "sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376", size = 51776, upload-time = "2026-03-19T16:59:43.603Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "openai" +version = "2.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a1/4d5e84cf51720fc1526cc49e10ac1961abcccb55b0efb3d970db1e9a2728/openai-2.36.0.tar.gz", hash = "sha256:139dea0edd2f1b30c33d46ae1a6929e03906254140318e4608e98fe8c566f2e7", size = 753003, upload-time = "2026-05-07T17:33:17.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/1c/5d43735b2553baae2a5e899dcbcd0670a86930d993184d72ca909bf11c9b/openai-2.36.0-py3-none-any.whl", hash = "sha256:143f6194b548dbc2c921af1f1b03b9f14c85fed8a75b5b516f5bcc11a2a50c63", size = 1302361, upload-time = "2026-05-07T17:33:15.063Z" }, +] + +[[package]] +name = "openai-harmony" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/c6/2502f416d46be3ec08bb66d696cccffb57781a499e3ff2e4d7c174af4e8f/openai_harmony-0.0.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:029ec25ca74abe48fdb58eb9fdd2a8c1618581fc33ce8e5653f8a1ffbfbd9326", size = 2627806, upload-time = "2025-11-05T19:06:57.063Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d2/ce6953ca87db9cae3e775024184da7d1c5cb88cead19a2d75b42f00a959c/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4f709815924ec325b9a890e6ab2bbb0ceec8e319a4e257328eb752cf36b2efc", size = 2948463, upload-time = "2025-11-05T19:06:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/b553c9651662d6ce102ca7f3629d268b23df1abe5841e24bed81e8a8e949/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cfcfd963b50a41fc656c84d3440ca6eecdccd6c552158ce790b8f2e33dfb5a9", size = 2704083, upload-time = "2025-11-05T19:06:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/9b/af/4eec8f9ab9c27bcdb444460c72cf43011d176fc44c79d6e113094ca1e152/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a3a16972aa1cee38ea958470cd04ac9a2d5ac38fdcf77ab686611246220c158", size = 2959765, upload-time = "2025-11-05T19:06:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/11/3c/33f3374e4624e0e776f6b13b73c45a7ead7f9c4529f8369ed5bfcaa30cac/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4d5cfa168e74d08f8ba6d58a7e49bc7daef4d58951ec69b66b0d56f4927a68d", size = 3427031, upload-time = "2025-11-05T19:06:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/4327dbf87f75ae813405fd9a9b4a5cde63d506ffed0a096a440a4cabd89c/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cbaa3bda75ef0d8836e1f8cc84af62f971b1d756d740efc95c38c3e04c0bfde2", size = 2932931, upload-time = "2025-11-05T19:07:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c8/1774eec4f6f360ef57618fb8f52e3d3af245b2491bd0297513aa09eec04b/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:772922a9bd24e133950fad71eb1550836f415a88e8c77870e12d0c3bd688ddc2", size = 2996140, upload-time = "2025-11-05T19:07:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/3d1e01e2dba517a91760e4a03e4f20ffc75039a6fe584d0e6f9b5c78fd15/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:007b0476a1f331f8130783f901f1da6f5a7057af1a4891f1b6a31dec364189b5", size = 3205080, upload-time = "2025-11-05T19:07:05.078Z" }, + { url = "https://files.pythonhosted.org/packages/14/63/119de431572d7c70a7bf1037034a9be6ed0a7502a7498ba7302bca5b3242/openai_harmony-0.0.8-cp38-abi3-win32.whl", hash = "sha256:a9b5f893326b28d9e935ade14b4f655f5a840942473bc89b201c25f7a15af9cf", size = 2082457, upload-time = "2025-11-05T19:07:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/c83cf5a206c263ee70448a5ae4264682555f4d0b5bed0d2cc6ca1108103d/openai_harmony-0.0.8-cp38-abi3-win_amd64.whl", hash = "sha256:39d44f0d8f466bd56698e7ead708bead3141e27b9b87e3ab7d5a6d0e4a869ee5", size = 2438369, upload-time = "2025-11-05T19:07:08.1Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/84/d55baf8e1a222f40282956083e67de9fa92d5fa451108df4839505fa2a24/opentelemetry_exporter_otlp-1.41.1.tar.gz", hash = "sha256:299a2f0541ca175df186f5ac58fd5db177ba1e9b72b0826049062f750d55b47f", size = 6152, upload-time = "2026-04-24T13:15:40.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/d5/ea4aa7dfc458fd537bd9519ea0e7226eef2a6212dfe952694984167daaba/opentelemetry_exporter_otlp-1.41.1-py3-none-any.whl", hash = "sha256:db276c5a80c02b063994e80950d00ca1bfddcf6520f608335b7dc2db0c0eb9c6", size = 7025, upload-time = "2026-04-24T13:15:17.839Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, +] + +[[package]] +name = "outlines-core" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/04/4a0812eb27c086cfd2e66e7ec9150f33e105912a9b7f8b335e3479f03a06/outlines_core-0.2.14.tar.gz", hash = "sha256:64808deed1591ca3029ff64346ceb974cd5d780c916ea82504951fe83523039e", size = 191539, upload-time = "2026-01-09T15:59:10.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d0/e7719044b3ed57fde6f700211be67682eec4a2735fcf8d4199400ac8f7a3/outlines_core-0.2.14-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:056f656ea6e4807338963377afb50b9d936593ba3545a819f1aba56fd6e14920", size = 2050100, upload-time = "2026-01-09T15:58:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ff/f41c933bc5b69b7080e763af375d7632d419859b6cd4492c5a12d5c89c80/outlines_core-0.2.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:c76f28feb6ea71b1ff4b0ba5901dc383273a32b156213dc1bc753fc634645a1e", size = 2200812, upload-time = "2026-01-09T15:58:08.876Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/05b3e5b9fa4f7f40da459022b2292c6bca48492df920eebd3572844e7fe6/outlines_core-0.2.14-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e604925d6525f669253160568397df6d6c8124b2e01f1fde553e3b9f28ce9e21", size = 2050299, upload-time = "2026-01-09T15:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/abcee1d35c7a7fde984ce4299de2db72add3c8a88a0f6e9e7f8c4836151d/outlines_core-0.2.14-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:f0e5037153b5b3abfb617f6dfdc3ff28b6fab50f0de5936ea6995f5675d23e0b", size = 2197822, upload-time = "2026-01-09T15:58:11.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/cc/6f5d1b92e79b30c2ac187b23ab66b0365e06007a9405b35709d02e94615b/outlines_core-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e4b5b7c8e50489bea444b095692ddb5d8fb92ea6b949c4a6a3381eca9b691a7", size = 2339071, upload-time = "2026-01-09T15:58:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/66/f7/252c0d4ecc5020d698b23257b9860fbab020c1120c15417ed02a9fd82d19/outlines_core-0.2.14-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b582b5d2f773cff966f37d7a5680d97506792647c93fb2e522283e8a14726e9d", size = 2236310, upload-time = "2026-01-09T15:58:13.942Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/5599fef9e4184a99684c9cc285081a1b5f4a741b2d6c676466a0ca365d39/outlines_core-0.2.14-cp310-cp310-win32.whl", hash = "sha256:f753edd430ac27e6dcde5a614665888db72b78c666aa160c478afd1eb986fb8b", size = 1841873, upload-time = "2026-01-09T15:58:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5f/da4daf605ab9c26e854a741c3567eb717e8622d4d17bcd751da5238b039f/outlines_core-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:060a0174a6262bfd378763f210e374e52011776849a3a767df9863fb6839c142", size = 2136404, upload-time = "2026-01-09T15:58:16.939Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/a67f0be9546776f71c5df373f38ce6db965abc9845fbcd291b393a20712e/outlines_core-0.2.14-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7770b5e0497e6f4548a8923299d4438d7dd61dc17c2f58acfd5df4d3101bb991", size = 2050098, upload-time = "2026-01-09T15:58:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/f2e19cc32ea97c1bac4882dbfa693671175a330ad5a735af5b97c2258056/outlines_core-0.2.14-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a2795dc2047821b229457f941a303639e0c14e4c3c5718797540a27b529a062e", size = 2200792, upload-time = "2026-01-09T15:58:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ea/19e859d4cfcbeceace30ad490f5369c87eab81767238593e20c17f55a390/outlines_core-0.2.14-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:4daa22d677dc6a74c44f9266ec9e3151332dcea4250dd019ea0c75b98ae32938", size = 2050363, upload-time = "2026-01-09T15:58:20.981Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/188aecb87008ddd293b8d315f26017750a1d7f9e95b8e2756d4a3af08196/outlines_core-0.2.14-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:813b28813b22025c3d079b3b8a20cf5a28c6d5ba29ec21c5b1093442aa5d4e91", size = 2197869, upload-time = "2026-01-09T15:58:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/e0be45d4c8ad7d301cdc9917d22ff39211da1e830f92fb07b29c9221b5c4/outlines_core-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:615566bf8257d2bba8ac192cdfc29d1c4357f57b53672fbd622e821215e4f1bd", size = 2338968, upload-time = "2026-01-09T15:58:23.317Z" }, + { url = "https://files.pythonhosted.org/packages/f2/67/9dab90313460eb250f926e7985d62cebfc33c7580197be8a496de6e9f7c4/outlines_core-0.2.14-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:81d01cfae29de5671bc5013fd6b2008621157bec3d8be284da7da2dc0672745c", size = 2236169, upload-time = "2026-01-09T15:58:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/289996bae3457cf3917ff21e0082e4950cf27a101d0870e16fee94c917e0/outlines_core-0.2.14-cp311-cp311-win32.whl", hash = "sha256:8a5e5f34961fe4d04c389d00f92d624c6318ab3ff00467fbf7c93324458886d9", size = 1841978, upload-time = "2026-01-09T15:58:26.309Z" }, + { url = "https://files.pythonhosted.org/packages/be/65/2d59be2f8c0cca118a6235ab2286615e3c1b2fa9d6768c4ea4b86b556204/outlines_core-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:babf97a54662330c55a79fdcab8994f96faa6dcb71b458d4b18c4fb538f5d461", size = 2136353, upload-time = "2026-01-09T15:58:27.443Z" }, + { url = "https://files.pythonhosted.org/packages/66/93/30b9188648a479b32be429a24166db47a7bfdb0f9a8aac4c6dcf569e0a52/outlines_core-0.2.14-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:95e6476d9702d2fcc4e85370dbbfb6933a46c816e9c90107f6ce36eb68b5d64a", size = 2049651, upload-time = "2026-01-09T15:58:28.549Z" }, + { url = "https://files.pythonhosted.org/packages/0d/06/f3557daa8e87d5b95f64de269a301d73ec3c2202ab897c3e1f1cb93eb1db/outlines_core-0.2.14-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f04731a5e29a190e2cc9f692a1f3fb2414a645355ca7d01b83df43439c38bea8", size = 2201046, upload-time = "2026-01-09T15:58:29.958Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/d8acf778990964c951080d568284e858d466f27dfd6f2674781927faba1c/outlines_core-0.2.14-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:0e4c69f0a8565edb56464c4c9b6c291a10805f3a96dff84182980e90ae1a5e2f", size = 2049558, upload-time = "2026-01-09T15:58:31.003Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/0320b14b49b8379ced1ab195ecf5875dbd2267b90148847541f43bfde6c1/outlines_core-0.2.14-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:63f53cfd9614e754499ae86dd699f3abcecf42d6a4e58d80fd80347881d85960", size = 2197854, upload-time = "2026-01-09T15:58:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/29/29/3a04944407207a5d214879ca5ca33c2bd3e65199a4e927051c1bdaaa4d50/outlines_core-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bb2060c240c4507f334965a8948dbeeb22007560d797f6debd92346c0b620cb", size = 2341426, upload-time = "2026-01-09T15:58:33.553Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/a77f746272504bac3f628047d56ea1731b61549a3e1d9bbfd226f2968246/outlines_core-0.2.14-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1de34681c7e0e7e1551fc9036e4fa3c57986336c905a10536591ceb6d869c258", size = 2236941, upload-time = "2026-01-09T15:58:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/9f599d938923ab8ceeff26fdf2f9ea53bea3c962085c4927a08338a32349/outlines_core-0.2.14-cp312-cp312-win32.whl", hash = "sha256:870e8e038853818cb202ccc8cde92251f300f96805bfcc3be1c883adda7b5297", size = 1842940, upload-time = "2026-01-09T15:58:36.544Z" }, + { url = "https://files.pythonhosted.org/packages/f8/df/0f145c52ebd156d80273e2f5278227ea57e0275b2aa863bed33f44f77923/outlines_core-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:87b42440478764cce1353a87d8560ef82f3b39b9d753bfe93195ea3584f369e3", size = 2137266, upload-time = "2026-01-09T15:58:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/13/9d/e6c81c975c123f0639d5f6909c987e510d43e07c2e1e6495b21639c4dec6/outlines_core-0.2.14-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8b3e8d668188282a1f7666732bb8a01958ab134db35bb792e7442a40e55ff1e7", size = 2049297, upload-time = "2026-01-09T15:58:39.184Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d1/5ce55ef724aed0915edc877b6dd610d39b3169e4341154bb53daa022065a/outlines_core-0.2.14-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:66e695b375b180725fb534d9adf298531c152ec3d881e3b9e01c82b5dd269f52", size = 2200944, upload-time = "2026-01-09T15:58:40.257Z" }, + { url = "https://files.pythonhosted.org/packages/32/e3/60ad781251eedcf1496317ecd58eb2e4488717ba63b10494ab49dfd05e5d/outlines_core-0.2.14-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6bd166d3b07acef2f60d4ede44592a26d3f7d8712876bfc8e22150045def5857", size = 2049607, upload-time = "2026-01-09T15:58:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2d/662d6a76face5b4b3481f888900d00856c37aa2927341a023866457da212/outlines_core-0.2.14-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:9d45462d7548aa0e17176a691ae73447f3e6bed9658a0cd96fe72eadf7474475", size = 2197755, upload-time = "2026-01-09T15:58:42.861Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9a/4b62903de006d991b58674ff033c1b6fb92be5767360376fc961f6771bdb/outlines_core-0.2.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6453e23f01d98ec48e3a4141d7112792ce77001dfb28d91d6fd89f47009f91ef", size = 2341051, upload-time = "2026-01-09T15:58:44.415Z" }, + { url = "https://files.pythonhosted.org/packages/50/36/1532f7d9ab16c676812d94528e89964aa0d15f12adcb285e6ed86f86f2fe/outlines_core-0.2.14-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7deef6df74cb247f2a3a62f03438ba967456504b0555ec7029f8db834e054448", size = 2236778, upload-time = "2026-01-09T15:58:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/dfd94f15f4c04e691e7fdf30cf8b9b22bf2cbc426b3ef270af3e200596d5/outlines_core-0.2.14-cp313-cp313-win32.whl", hash = "sha256:bb008c7ecc034bcfda0ddc10a4d1f2181a4b61ec1643ee56183dd6fa64139c9d", size = 1842727, upload-time = "2026-01-09T15:58:46.723Z" }, + { url = "https://files.pythonhosted.org/packages/34/35/e24ab5d2116812464380587435297d8ece2f0218c2ba8afc9f541e3a6911/outlines_core-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:eb27e92204b296a063ac58f361153be4e78c8103a96e0b1c085b22d4fc3534cf", size = 2137108, upload-time = "2026-01-09T15:58:47.784Z" }, + { url = "https://files.pythonhosted.org/packages/1b/28/22fe8ee3bdf9cf13ab88a9d9b96729d9966c791c25227d0b7ca45c8d118f/outlines_core-0.2.14-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:69410e5b55bcbaad8c865d94bd01e7bff8a57996dcd2251b7d50dec70d7d9a63", size = 2050470, upload-time = "2026-01-09T15:58:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3e/30ce0b13e4c4c82de606c8bbf60775ac6fca1978efa54cd553893795fd0b/outlines_core-0.2.14-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:adf96395759d7fdf6efeb8a67d3f36f520c1546bfd4df0752306db8c7cb7d6c5", size = 2202138, upload-time = "2026-01-09T15:58:50.281Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/bd2ff9e90b28fa0dcc345c9196731ed9126e366733c8ccbc559149e34893/outlines_core-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b02bb0fc21c5e23e2ff9b2d1459db2c1c3e813a7646c9d5db091c6931edb9c85", size = 2050325, upload-time = "2026-01-09T15:58:51.596Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/fc0ae7d04345d17267d4dd5c693ed9e86c7f44419cc04ad92348472781be/outlines_core-0.2.14-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:e75395b1cccecdf85d8d8265aba28841ddeb1e8da406f4b1e0135df5a6e9960f", size = 2199081, upload-time = "2026-01-09T15:58:53.17Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/dfa000239e46f17b47e6dc9bec3aab8a8136fe400312f1916320e02c8f38/outlines_core-0.2.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1776ae984574461f249fe590314a439992eb9b883f4091b8fa7fc56f29f3717", size = 2343210, upload-time = "2026-01-09T15:58:54.282Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/0e63da06c6054f154ef22b5ef3c6b9030cb22da9c03d2d2dd82836a1e795/outlines_core-0.2.14-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7eba2b41dac03d6e6e8d5ea0aecbbc03dacb4c57de3b1fc944d0bafb022941f7", size = 2238206, upload-time = "2026-01-09T15:58:55.705Z" }, + { url = "https://files.pythonhosted.org/packages/74/4e/382271ab5ffe768055f11dddb50e82a0c46487f3766bf08a06cfcd35388b/outlines_core-0.2.14-cp314-cp314-win32.whl", hash = "sha256:0cd8ce3ce61df44fd9c5450d9744e2280586c2a6e6e3dfefa0dab1944764b424", size = 1845364, upload-time = "2026-01-09T15:58:56.795Z" }, + { url = "https://files.pythonhosted.org/packages/0d/11/13adf2d02c681b599c1eb550b0dbd763d1b818a106a13bd693019bdb5637/outlines_core-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:3e67fc23b1a3ac9562488fb50f409c171538b76f64aa5f7e25d9b0bf14770204", size = 2139979, upload-time = "2026-01-09T15:58:57.984Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "partial-json-parser" +version = "0.2.1.1.post7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, + { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, + { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, + { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/16d7af6fae7803f4c691856bc0d8d433ccf30e106432e2ef7707ee19a38a/pybase64-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f63aa7f29139b8a05ce5f97cdb7fad63d29071e5bdc8a638a343311fe996112a", size = 38241, upload-time = "2025-12-06T13:22:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3e/268beb8d2240ab55396af4d1b45d2494935982212549b92a5f5b57079bd3/pybase64-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5943ec1ae87a8b4fe310905bb57205ea4330c75e2c628433a7d9dd52295b588", size = 31672, upload-time = "2025-12-06T13:22:28.854Z" }, + { url = "https://files.pythonhosted.org/packages/80/14/4365fa33222edcc46b6db4973f9e22bda82adfb6ab2a01afff591f1e41c8/pybase64-1.4.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5f2b8aef86f35cd5894c13681faf433a1fffc5b2e76544dcb5416a514a1a8347", size = 65978, upload-time = "2025-12-06T13:22:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/1c/22/e89739d8bc9b96c68ead44b4eec42fe555683d9997e4ba65216d384920fc/pybase64-1.4.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6ec7e53dd09b0a8116ccf5c3265c7c7fce13c980747525be76902aef36a514a", size = 68903, upload-time = "2025-12-06T13:22:31.29Z" }, + { url = "https://files.pythonhosted.org/packages/77/e1/7e59a19f8999cdefe9eb0d56bfd701dd38263b0f6fb4a4d29fce165a1b36/pybase64-1.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7528604cd69c538e1dbaafded46e9e4915a2adcd6f2a60fcef6390d87ca922ea", size = 57516, upload-time = "2025-12-06T13:22:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/42/ad/f47dc7e6fe32022b176868b88b671a32dab389718c8ca905cab79280aaaf/pybase64-1.4.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4ec645f32b50593879031e09158f8681a1db9f5df0f72af86b3969a1c5d1fa2b", size = 54533, upload-time = "2025-12-06T13:22:33.457Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/7ab312b5a324833953b00e47b23eb4f83d45bd5c5c854b4b4e51b2a0cf5b/pybase64-1.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:634a000c5b3485ccc18bb9b244e0124f74b6fbc7f43eade815170237a7b34c64", size = 57187, upload-time = "2025-12-06T13:22:34.566Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/80acab1fcbaaae103e6b862ef5019192c8f2cd8758433595a202179a0d1d/pybase64-1.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:309ea32ad07639a485580af1be0ad447a434deb1924e76adced63ac2319cfe15", size = 57730, upload-time = "2025-12-06T13:22:35.581Z" }, + { url = "https://files.pythonhosted.org/packages/1f/24/84256d472400ea3163d7d69c44bb7e2e1027f0f1d4d20c47629a7dc4578e/pybase64-1.4.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d10d517566b748d3f25f6ac7162af779360c1c6426ad5f962927ee205990d27c", size = 53036, upload-time = "2025-12-06T13:22:36.621Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/33aecbed312ee0431798a73fa25e00dedbffdd91389ee23121fed397c550/pybase64-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74cc0f4d835400857cc5c6d27ec854f7949491e07a04e6d66e2137812831f4c", size = 56321, upload-time = "2025-12-06T13:22:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/a341b050746658cbec8cab3c733aeb3ef52ce8f11e60d0d47adbdf729ebf/pybase64-1.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b591d774ac09d5eb73c156a03277cb271438fbd8042bae4109ff3a827cd218c", size = 50114, upload-time = "2025-12-06T13:22:38.752Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/f7e6680ae6dc4ddff39112ad66e0fa6b2ec346e73881bafc08498c560bc0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5eb588d35a04302ef6157d17db62354a787ac6f8b1585dd0b90c33d63a97a550", size = 66570, upload-time = "2025-12-06T13:22:40.221Z" }, + { url = "https://files.pythonhosted.org/packages/4c/71/774748eecc7fe23869b7e5df028e3c4c2efa16b506b83ea3fa035ea95dc2/pybase64-1.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df8b122d5be2c96962231cc4831d9c2e1eae6736fb12850cec4356d8b06fe6f8", size = 55700, upload-time = "2025-12-06T13:22:41.289Z" }, + { url = "https://files.pythonhosted.org/packages/b3/91/dd15075bb2fe0086193e1cd4bad80a43652c38d8a572f9218d46ba721802/pybase64-1.4.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31b7a85c661fc591bbcce82fb8adaebe2941e6a83b08444b0957b77380452a4b", size = 52491, upload-time = "2025-12-06T13:22:42.628Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/f357d63ea3774c937fc47160e040419ed528827aa3d4306d5ec9826259c0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e6d7beaae65979fef250e25e66cf81c68a8f81910bcda1a2f43297ab486a7e4e", size = 53957, upload-time = "2025-12-06T13:22:44.615Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/243693771701a54e67ff5ccbf4c038344f429613f5643169a7befc51f007/pybase64-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a6276bc3a3962d172a2b5aba544d89881c4037ea954517b86b00892c703d007", size = 68422, upload-time = "2025-12-06T13:22:45.641Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/f987081bf6bc1d1eda3012dae1b06ad427732ef9933a632cb8b58f9917f8/pybase64-1.4.3-cp310-cp310-win32.whl", hash = "sha256:4bdd07ef017515204ee6eaab17e1ad05f83c0ccb5af8ae24a0fe6d9cb5bb0b7a", size = 33622, upload-time = "2025-12-06T13:22:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/c169a769fe90128f16d394aad87b2096dd4bf2f035ae0927108a46b617df/pybase64-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:5db0b6bbda15110db2740c61970a8fda3bf9c93c3166a3f57f87c7865ed1125c", size = 35799, upload-time = "2025-12-06T13:22:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/bdbe6af0bd4f3fe5bc70e77ead7f7d523bb9d3ca3ad50ac42b9adbb9ca14/pybase64-1.4.3-cp310-cp310-win_arm64.whl", hash = "sha256:f96367dfc82598569aa02b1103ebd419298293e59e1151abda2b41728703284b", size = 31158, upload-time = "2025-12-06T13:22:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cf/6e712491bd665ea8633efb0b484121893ea838d8e830e06f39f2aae37e58/pybase64-1.4.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94cf50c36bb2f8618982ee5a978c4beed9db97d35944fa96e8586dd953c7994a", size = 38007, upload-time = "2025-12-06T13:26:32.804Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/9272cae1c49176337dcdbd97511e2843faae1aaf5a5fb48569093c6cd4ce/pybase64-1.4.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:01bc3ff5ca1341685c6d2d945b035f442f7b9c3b068a5c6ee8408a41fda5754e", size = 31538, upload-time = "2025-12-06T13:26:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/17546f97befe429c73f622bbd869ceebb518c40fdb0dec4c4f98312e80a5/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03d0aa3761a99034960496280c02aa063f856a3cc9b33771bc4eab0e4e72b5c2", size = 40682, upload-time = "2025-12-06T13:26:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/464b36d5dfb61f3da17858afaeaa876a9342d58e9f17803ce7f28b5de9e8/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ca5b1ce768520acd6440280cdab35235b27ad2faacfcec064bc9c3377066ef1", size = 41306, upload-time = "2025-12-06T13:26:36.351Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/a748dfc0969a8d960ecf1e82c8a2a16046ffec22f8e7ece582aa3b1c6cf9/pybase64-1.4.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3caa1e2ddad1c50553ffaaa1c86b74b3f9fbd505bea9970326ab88fc68c4c184", size = 35452, upload-time = "2025-12-06T13:26:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/4d37bd3577d1aa6c732dc099087fe027c48873e223de3784b095e5653f8b/pybase64-1.4.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bd47076f736b27a8b0f9b30d93b6bb4f5af01b0dc8971f883ed3b75934f39a99", size = 36125, upload-time = "2025-12-06T13:26:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, +] + +[[package]] +name = "pycountry" +version = "26.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[package.optional-dependencies] +pycountry = [ + { name = "pycountry" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "quack-kernels" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "einops" }, + { name = "nvidia-cutlass-dsl" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/58/58b82e91b236539f424ff5681e7095b1f2860ddfb7778fe0be14d8fb58de/quack_kernels-0.4.1.tar.gz", hash = "sha256:9d7d6ba412bc0c8a9b1331c52a73db76280adb9dc2f2750df4851ddabef1466b", size = 274766, upload-time = "2026-04-30T14:37:55.65Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e4/a6c3bbbe3d4242fa412454b8e8069a079e500be331aecf8f2aa666164e9c/quack_kernels-0.4.1-py3-none-any.whl", hash = "sha256:c1c8df2935bf5156ec47d2c5384ac08b411fd0ee702d80ae916dbf6d6f5ae813", size = 260827, upload-time = "2026-04-30T14:37:54.584Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "requests" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/77/45030521529b1c4e34d4dbbdfb6dd81cd39e44539b122ef55ade5dab071d/rich_toolkit-0.19.8.tar.gz", hash = "sha256:4cfd2bcb34299442168c983af22e74c881e055e8b67417f577307bf0eaa4d0af", size = 196485, upload-time = "2026-05-12T13:01:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4c/998d76e1c35fb70493d4300fe625ad933b40fd225de58882411fd4dc30e8/rich_toolkit-0.19.8-py3-none-any.whl", hash = "sha256:97126e170b95ca357e034bf729da408f9eb37c5627183807962bc18f18771033", size = 33174, upload-time = "2026-05-12T13:01:48.185Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.59.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/e0/9bf5e5fc7442b10880f3ec0eff0ef4208b84a099606f343ec4f5445227fb/sentry_sdk-2.59.0.tar.gz", hash = "sha256:cd265808ef8bf3f3edf69b527c0a0b2b6b1322762679e55b8987db2e9584aec1", size = 447331, upload-time = "2026-05-04T12:19:06.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/00/b8cc413748fb6383d1582e7cda51314f99743351c462a92dc690d5b5853b/sentry_sdk-2.59.0-py2.py3-none-any.whl", hash = "sha256:abcf65ee9a9d9cdebf9ad369782408ecca9c1c792686ef06ba34f5ab233527fe", size = 468432, upload-time = "2026-05-04T12:19:04.741Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "supervisor" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/b5/37e7a3706de436a8a2d75334711dad1afb4ddffab09f25e31d89e467542f/supervisor-4.3.0.tar.gz", hash = "sha256:4a2bf149adf42997e1bb44b70c43b613275ec9852c3edacca86a9166b27e945e", size = 468912, upload-time = "2025-08-23T18:25:02.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tilelang" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "cloudpickle" }, + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "psutil" }, + { name = "setuptools", marker = "sys_platform == 'darwin'" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext", marker = "python_full_version < '3.14'" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "z3-solver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/70/5051f65821baa30a3d61fc48f8ba10c776490315e8c90f82559b92089756/tilelang-0.1.9.tar.gz", hash = "sha256:287f727c913bb648fcf6c1968809ba3390e55eeed257a5c6bb9a80bc05966af4", size = 93395292, upload-time = "2026-04-22T09:19:11.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/db/4dd76da8c8585c605639a21bc098d504e317fe324a72f01ce3c7370250b4/tilelang-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:00ed594fdeb229c5505b9ffa895c3c5daeb28641c78f783fa1f724cf1e08cecd", size = 36599020, upload-time = "2026-04-22T09:14:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/f7/8a/1cbeee79d62abaa02441c2d00621554e41aa62dbf3b94a4feb3867184b01/tilelang-0.1.9-cp38-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bbccfe9035aed775ffafb6dc25a5994504b24e2c5d95d0f39643edfafa7bf12", size = 45419374, upload-time = "2026-04-22T09:15:56.014Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a7/f4bfb86f87e107703146e703204cec2c0eae2492b633e0052b0ace3febb6/tilelang-0.1.9-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:77ab0ee2f40f66ea015b6b21426d482751e28cbc635ef9d1198cbd6502454a7c", size = 42110365, upload-time = "2026-04-22T09:17:18.292Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/49/67a66932ab2fcdda3c5a4dcf606e713d86883a4a9a99a3bb832815b52b8e/torch_c_dlpack_ext-0.1.5-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e0f6c197d5293884898b9ebf13d07501de39cb94799b374ed43f91731087d557", size = 7056755, upload-time = "2026-01-12T11:24:31.817Z" }, + { url = "https://files.pythonhosted.org/packages/ae/28/d2d6bf90e01a1f4da3277c9a56d9ecac648b6d6adaa8e20c17f802deb7fb/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba3d88f0f7d5e1d9c3d4a3179037fc8e261c3b77ac1fad23edc0d3a9214ef193", size = 432066, upload-time = "2026-01-12T11:24:33.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/a1f9584a3af4ac6ae5ad5cf86927d8c3a9b6bb50d54e54d19313411216a0/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7468df84ec152d930fbc3acf460c44a60b3462b95af3d3a676d133629c7e176", size = 879488, upload-time = "2026-01-12T11:24:34.837Z" }, + { url = "https://files.pythonhosted.org/packages/6c/08/478cfcb5814e29f9b720111bdef315fc2fbc8b276e4b1183c8b9c9414a4f/torch_c_dlpack_ext-0.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:78dd4904bd26170a2dd7c0eab56367756ee0a15672ce9b84146169e68f0c6ddc", size = 1461437, upload-time = "2026-01-12T11:24:36.385Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/c12a9bb3a5ddc0962c00467891bf1ffdda39a4d4780bf0fbbf54523ff34e/torch_c_dlpack_ext-0.1.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:56bd25a2af19280bf8a06aa62cff5510106f43235b9327d8561b3e9a659c4d84", size = 5076782, upload-time = "2026-01-12T11:24:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/64e1e579d107064785549e70758e38a42376ab7e73d86897ed4beab10e74/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fba674110e1fab0b176bb5a28223e157db65c90767d4ba74abdbee9f537b0e9d", size = 440949, upload-time = "2026-01-12T11:24:39.716Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/3e1382a620824f92920ab3fae132d8fb4e85898284c99e0c6a7764e452ce/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3448c4f0d64104d0b2e58080a7efa72304a04960c18f338024b80b13cd3eca26", size = 897768, upload-time = "2026-01-12T11:24:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/76ea1006b9038b496d01e916c91efd17cb782abde2491a261cf203f57e30/torch_c_dlpack_ext-0.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:74676474e0afa9a4216c4755ea7cf05e8158be1d168f6bda669ba91097c263f2", size = 1479088, upload-time = "2026-01-12T11:24:42.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/67/10d236698525d7b7db4d74ec0a4b01f5b2db33968995fdd9ac6b4635e327/torch_c_dlpack_ext-0.1.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c0f2bd51fcd99c0e5b50314e1985f2728c4941bfa821f065e6c30951d1f995ca", size = 5291237, upload-time = "2026-01-12T11:24:44.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e6/7d7a97a3953208d6d6ce749180c34d1dab48464ded9a76cecabe9d021ce6/torch_c_dlpack_ext-0.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:670fbbab70123cc228bed41693a3720757af57a0ad22669063c9db25321e8f55", size = 1482855, upload-time = "2026-01-12T11:24:49.581Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/65346a201d921b616731311fc9941f15137672b444cebdad702cb52ccee0/torch_c_dlpack_ext-0.1.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:74acea2ed395cadda63342845b9e9ee7cd4537846223dacfb4431b4610109265", size = 1993243, upload-time = "2026-01-12T11:24:51.079Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/faf10be09a5812b1c5ec9922b53fb5def5fc4080b81a653b9347bb169ebb/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f1e99d13c64e22dac0a34a1560e9e5a398a49a9fa81df83053e04fde6ec5bd", size = 443798, upload-time = "2026-01-12T11:24:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/2d/68/f434b48700f3e04f33882f54d8d3910327b935f55e14ec49da7d607bf470/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:debe62e5ef93e631065d6b9f6e60d3d39bae6b89fa1b25d9523f40b3efbf8aba", size = 755004, upload-time = "2026-01-12T11:24:54.004Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/cc64e563f05ea99bd79bdb43f71f0f46452d3acd734da4843ede5fc73a35/torch_c_dlpack_ext-0.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:30e3eab616dbc81dfdb7492aca557be551a9163ba9b585f97394a42b336b113a", size = 999126, upload-time = "2026-01-12T11:24:55.44Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/449324ca8e81573e650b6851fc31c1038f750d1de85d0b185d788e1c7a3a/torch_c_dlpack_ext-0.1.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:cac94a4905d391889e679a8da31e46dc325af5d55d13b7c70c0ce3d71d1ced6d", size = 1982154, upload-time = "2026-01-12T11:24:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/20/62/11c05b99f69aa5152bca0313e0dfa6d125a020cf890dc888ef009aa7891c/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a58fdf45fb0bda7bc459632cec891570f31c11636d5851c825cf308ec8b73c2", size = 163825, upload-time = "2026-01-12T11:24:59.474Z" }, + { url = "https://files.pythonhosted.org/packages/15/b5/be613cd8e71c9982bd07af530f86c5a7f30df7831d14cec5414857af7149/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b985a324c68241cf83a9474b28015524b66775b12a91930dd4c0760aa628d01", size = 171740, upload-time = "2026-01-12T11:25:00.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/11/52e291f1659e2ec70a09f5ca4ad27e015eb4f0a1371ae68d23a9fbd1c704/torch_c_dlpack_ext-0.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:d794e19fa3f330ab7a29987c07e031fc08e4953aec516d35701d0827863e356b", size = 277086, upload-time = "2026-01-12T11:25:01.901Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d9/357eb5fe4e19a861e6fa1af4d9f535e8fa8692336e6cf436e8a21262e054/torchaudio-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ebb59c694909eccb5d61b7cc199d297692012c43286e36d92983aa7bad7586d", size = 684145, upload-time = "2026-03-23T18:13:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/90de77e73f395bba2fe477f8e82e4ae1d14d6452a706838765e850a5e80c/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be7ad472acb16d16e98c005f0219b0db06a47dfe8f7b4d177062e1638f871e3b", size = 1626521, upload-time = "2026-03-23T18:13:40.98Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/5757ed7d8d11a6c14336bcb54e63980979f00005555fec80fb4aa4de5eff/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5847fe2022b17c6580aeb39c8797a443411cc09edfd9183cd50ac1a3b8ccf97c", size = 1771929, upload-time = "2026-03-23T18:13:43.432Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f4/8ce2417eac66296e45b7aaa69858403fb6a52b1323f8635ec37b4b0f1fa3/torchaudio-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e2da1df4f6fe885c46db350a0dc90a0dff4b54541dff8846faa904d255e2bfe", size = 328661, upload-time = "2026-03-23T18:13:45.77Z" }, + { url = "https://files.pythonhosted.org/packages/94/77/0eec7f175d88f312296bd5b11c23bd58da37c1021f53da3db4df449ce3ee/torchaudio-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:492dd64645e9d0bb843e94f1d9a4d1e31426262ffc594fafecc1697df9df5eb9", size = 684142, upload-time = "2026-03-23T18:13:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f9/6f7ebe071b44592c85269762b55b63ab0a091b5f479f73544738f7564a1e/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:73dab4841f94d888bc7c2aed7b5547c643edc974306919fe1adfb65d57cccf4b", size = 1626527, upload-time = "2026-03-23T18:13:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/17408e0d154d0c894537a88dcbadc48e8ad3b6e1ef4a1dabda5d40245ee0/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1a07ec72fd6f26a588c39b5f029e0130d16bb40bc4221635580bf8fb18fcbc80", size = 1771930, upload-time = "2026-03-23T18:13:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/c9/75/b6d03fc75b409bdaec597274d1bdd4213db716ed16f6801386b31d59c551/torchaudio-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb59ba4452bbbe95d75ad3ef18df9824955625f36698ce9a5998a4a9f3c1ba1d", size = 328658, upload-time = "2026-03-23T18:13:44.545Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/77658817acacd01a72b714440c62f419efc4d90170e704e8e7a2c0918988/torchaudio-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cf1acc883bee9cb906a933572fed6a8a933f86ef34e9ea7d803f72317e8c1b", size = 684226, upload-time = "2026-03-23T18:13:40.023Z" }, + { url = "https://files.pythonhosted.org/packages/78/28/c7adc053039f286c2aca0038b766cbe3294e66fec6b29a820e95128f9ede/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bc653defca1c16154398517a1adc98d0fb7f1dd08e58ced217558d213c2c6e29", size = 1626670, upload-time = "2026-03-23T18:13:42.162Z" }, + { url = "https://files.pythonhosted.org/packages/88/d8/d6d0f896e064aa67377484efef4911cdcc07bce2929474e1417cc0af18c2/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6503c0bdb29daf2e6281bb70ea2dfe2c3553b782b619eb5d73bdadd8a3f7cecf", size = 1771992, upload-time = "2026-03-23T18:13:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/941277ecc39f7a0a169d554302a1f1afd87c1d94a8aec828891916cea59a/torchaudio-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:478110f981e5d40a8d82221732c57a56c85a1d5895fb8fe646e86ee15eded3bd", size = 328663, upload-time = "2026-03-23T18:13:19.218Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9e/f76fcd9877c8c78f258ee34e0fb8291fdb91e6218d582d9ca66b1e4bd4ae/torchaudio-2.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e3f9696a9ef1d49acc452159b052370c636406d072e9d8f10895fda87b591ea9", size = 679904, upload-time = "2026-03-23T18:13:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/249c1498ebdad3e7752866635ec0855fc0dcf898beccda5a9d2b9df8e4d0/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b034d7672f1c415434f48ef17807f2cce47f29e8795338c751d4e596c9fbe8b5", size = 1618523, upload-time = "2026-03-23T18:13:15.703Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/be13fe35d9aa5c26381c0e453c828a789d15c007f8f7d08c95341d19974d/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1c1101c1243ef0e4063ec63298977e2d3655c15cf88d9eb0a1bd4fe2db9f47ea", size = 1771992, upload-time = "2026-03-23T18:13:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8b/2bbb3dca6ff28cba0de250874d5ef4fc2822c47a934b59b3974cff3219ef/torchaudio-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:986f4df5ed17b003dc52489468601720090e65f964f8bebccf90eb45bba75744", size = 328662, upload-time = "2026-03-23T18:13:18.308Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ce/52c652d30af7d6e96c8f1735d26131e94708e3f38d852b8fa97958804dd8/torchaudio-2.11.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:bda09ea630ae7207384fb0f28c35e4f8c0d82dd6eba020b6b335ad0caa9fed49", size = 680814, upload-time = "2026-03-23T18:13:17.08Z" }, + { url = "https://files.pythonhosted.org/packages/06/95/1ad1507482e7263e556709a3f5f87fecd375a0742cdaf238806c8e72eaad/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9fe3083c62e035646483a14e180d33561bdc2eed436c9ab1259c137fb7120b4a", size = 1618546, upload-time = "2026-03-23T18:13:29.686Z" }, + { url = "https://files.pythonhosted.org/packages/98/4c/480328ba07487eb9890406720304d0d460dd7a6a64098614f5aa53b662ca/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:13cff988697ccbad539987599f9dc672f40c417bed67570b365e4e5002bbd096", size = 1771991, upload-time = "2026-03-23T18:13:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/3e/98/5d4790e2d6548768999acd34999d5aeefce8bcc23a07afaa5f03e723f557/torchaudio-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ed404c4399ad7f172c86a47c1b25293d322d1d58e26b10b0456a86cf67d37d84", size = 328661, upload-time = "2026-03-23T18:13:34.359Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/ffa618b4f0d9732d7df7a2fa2bd48657d896599bc224e5af3c70d46c546b/torchaudio-2.11.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:cc09cd1f6015b8549e7fe255fb1be5346b57e7fee06541d3f3dbb012d8c4715f", size = 679901, upload-time = "2026-03-23T18:13:25.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/54/f414d7b92dd0b3094a2409c95a97bd6c49aa0620da722a0e55462f9bd9cb/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:79fb3cb99169fd41bd9719647261402a164da0d105a4d81f42a3260844ec5e79", size = 1618527, upload-time = "2026-03-23T18:13:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/bf2e1f6ce24c990192400ae49b4acc1a0d0295b6c6a06bceecdc46ce08de/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:00e9f71ab9c656f0abdb40c515bd65d4658ab0ad380dee27a2efd7d51dabd3d6", size = 1771995, upload-time = "2026-03-23T18:13:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/83/6f/b0efb44e0bfe8dd4d78d76ae3be280354e1fb5c8631c782785d74cd8a7b1/torchaudio-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1424638adb8bb40087bc7b6eb103e8e4fe398210f09076f33b7b5e61501b5d66", size = 328662, upload-time = "2026-03-23T18:13:32.243Z" }, + { url = "https://files.pythonhosted.org/packages/60/84/1c792b0b700eac9a96772cfd9f96c097b17bca3234a2fde3c64b8063660d/torchaudio-2.11.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:da2725e250866da42a12934c9a6552f65a18b7187fd7a6221387f0e605fb3b96", size = 679926, upload-time = "2026-03-23T18:13:24.452Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a0/62a5842062f739239691f2e57523e0570dd06704ad987755f7644a3afa23/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1be3767064364ae82705bdf2b15c1e8b41fea82c4cd04d47428a8684b634b6ed", size = 1618552, upload-time = "2026-03-23T18:13:21.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/c293d818f9f899db93bf291b42401c05ae29acfb2e53d5341c30ea703e62/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67f6edac29ed004652c11db5c19d9debb5d835695930574f564efc8bdd061bba", size = 1771986, upload-time = "2026-03-23T18:13:22.153Z" }, + { url = "https://files.pythonhosted.org/packages/93/f7/ee5da8c03f1a3c7662c6c6a119f24a4b3e646da94be56dce3201e3a6ee9b/torchaudio-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:88fb5e29f670a33d9bac6aabb1d2734460cf6e461bde5cdc352826035851b16d", size = 328661, upload-time = "2026-03-23T18:13:20.1Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, + { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/36/390075693b76d4fb4a2bea360fb6080347763bd1f1147c49ed0ed938778c/transformers-5.8.0.tar.gz", hash = "sha256:6cc9a1f0291d16b1c1b735bad775e78ebefff7722701d4e28f98aaaa2bd6fb91", size = 8528141, upload-time = "2026-05-05T16:50:04.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/7b/5621d08b34ac35deb9fa14b58d27d124d21ef125ee1c64bc724ca47dfb63/transformers-5.8.0-py3-none-any.whl", hash = "sha256:e9d2cae6d195a7e1e05164c5ebf26142a7044e4dc4267274f4809204f92827e4", size = 10630279, upload-time = "2026-05-05T16:50:01.026Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "vllm" +version = "0.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anthropic" }, + { name = "apache-tvm-ffi" }, + { name = "blake3" }, + { name = "cachetools" }, + { name = "cbor2" }, + { name = "cloudpickle" }, + { name = "compressed-tensors" }, + { name = "depyf" }, + { name = "diskcache" }, + { name = "einops" }, + { name = "fastapi", extra = ["standard"] }, + { name = "fastsafetensors" }, + { name = "filelock" }, + { name = "flashinfer-cubin" }, + { name = "flashinfer-python" }, + { name = "gguf" }, + { name = "ijson" }, + { name = "lark" }, + { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 'x86_64'" }, + { name = "lm-format-enforcer" }, + { name = "mcp" }, + { name = "mistral-common", extra = ["image"] }, + { name = "model-hosting-container-standards" }, + { name = "msgspec" }, + { name = "ninja" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl" }, + { name = "openai" }, + { name = "openai-harmony" }, + { name = "opencv-python-headless" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, + { name = "outlines-core" }, + { name = "partial-json-parser" }, + { name = "pillow" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "pyzmq" }, + { name = "quack-kernels" }, + { name = "regex" }, + { name = "requests" }, + { name = "sentencepiece" }, + { name = "setproctitle" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "six", marker = "python_full_version >= '3.12'" }, + { name = "tiktoken" }, + { name = "tilelang" }, + { name = "tokenizers" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, + { name = "xgrammar", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/23/4a5dc23600be07407835e404d06a2fcd587c8dcaebeab314b5c3593509ce/vllm-0.20.2.tar.gz", hash = "sha256:58809377798c5335c6e2fe30092abda54d9200b5b8a717b3735a63f5daa0e383", size = 33526607, upload-time = "2026-05-10T07:39:27.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/d9/7bef6cf9d7508b4313237602e96e10054c7491f12f48403813c9c2d6f6f1/vllm-0.20.2-cp38-abi3-manylinux_2_35_aarch64.whl", hash = "sha256:76ccf4c0554556c06f6b0fb1643742d4cf97dcc69f6ef3f04556d0764126035a", size = 235788487, upload-time = "2026-05-10T07:38:37.747Z" }, + { url = "https://files.pythonhosted.org/packages/c5/aa/4488d49c481a2184e6e285b8d3f937905205f52cd5ac30fb348770494b6e/vllm-0.20.2-cp38-abi3-manylinux_2_35_x86_64.whl", hash = "sha256:22a7dd06eb03371298e13d6100f3dedbf307352342aaf08e87c929c60aae9b4d", size = 244425988, upload-time = "2026-05-10T07:39:04.327Z" }, +] + +[[package]] +name = "vllm-cosmos3" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "vllm" }, +] + +[package.metadata] +requires-dist = [{ name = "vllm", specifier = ">=0.19" }] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "xgrammar" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pydantic" }, + { name = "torch" }, + { name = "transformers" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/54/7e593fc41ffcaf5ac7c0379e0aec0cf03e53a742d1a91f64c6c7e79a6ac1/xgrammar-0.2.0.tar.gz", hash = "sha256:c4f0238a89869343171d43d069b8c5da874f3c2c25f408f20cd5987219a6adef", size = 2421093, upload-time = "2026-05-01T18:33:54.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/43/e19db659e9e56d88f3769f4052a955213cf6ad2341b7f1d583da83e361eb/xgrammar-0.2.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:fc4fb16e99f807d5c8ca1926a43024a09d82331ae56e0fe14a85f365fc12f2f1", size = 23150211, upload-time = "2026-05-01T18:31:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/9c/eb/a265fcd2f18e5c2cf343079b857d081889d1dc080c524b9fdffda4040f16/xgrammar-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa89d9eb2d989b90b19ff0d0fa9cf9be05e970e56ad2e979315772496f7fe8bb", size = 23055199, upload-time = "2026-05-01T18:32:00.337Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/f5da0311502ed14b4a25988210bf366d470a2e3c91ecb8fb4499400ef7f6/xgrammar-0.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6130eacd86161c2eae8bd46a7108c0856757bbc8e5efd207eba36fc82db541f", size = 44155197, upload-time = "2026-05-01T18:32:03.69Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b0/d776e41054932b9fcee8204fa44c9ffe42469b6414410270ccc09662142d/xgrammar-0.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e5daf9ab32850fce44eed894a2704ca91d537bf1e41943f8d11145c235eaa87", size = 44616365, upload-time = "2026-05-01T18:32:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/f5/17/90074d932da98424476ce5e65f1243e9d5ddd95b03060d02fd125b2df18a/xgrammar-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8a0aaa6cb71efeeb6223f06fa0b16829d8e7f9e5af51803899c2cca8ea8c7ba7", size = 7400272, upload-time = "2026-05-01T18:32:12.02Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/82fa277ac2336bb21f2b3b6117e73081f184e6244544e61b57ba5b5f13c3/xgrammar-0.2.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c606b0e6deb328f9e7cfd2ae15232efca58a88c8dbe56447daccc63f89078c4f", size = 23150194, upload-time = "2026-05-01T18:32:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f2/166a5afbf6a236a5044a5f3f56a271781417cbd4c406aad7427c5f6da8a0/xgrammar-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:47815edecad20eed1b1084ca97ebc2f8c2ba054bd0ee3cb2d5dd5681e781b632", size = 23055210, upload-time = "2026-05-01T18:32:17.183Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f8/2122b33a44be20ee1466360c6916816b9a79ac38f430cd56676484614443/xgrammar-0.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:001e2177bd80bb7c49dca3a70a8c2a645c664afc03c3cad7abffc9340c9a4eff", size = 44155235, upload-time = "2026-05-01T18:32:21.288Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bd/4c1598e93e1e9a6dcc650e57600a80b52d6d759f8f53b902ea34727bd6fe/xgrammar-0.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f03bcbd6cfd96864d59d8acd18e9e5a3f1656beedcdc55a553bf078120758ac", size = 44616355, upload-time = "2026-05-01T18:32:25.174Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/6607c78f1c9aca915a109d6dae582da0f24da2de7f6d0ee426d2c6ee17af/xgrammar-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6c01a536e2317f8e2e21762567f504d54c198bdf936cf89abc7cc1448268b", size = 7400485, upload-time = "2026-05-01T18:32:28.796Z" }, + { url = "https://files.pythonhosted.org/packages/23/3c/c76e711834600226831666d6a54b3a139bf0512c90268b44b6fda69aaee2/xgrammar-0.2.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:fd525fbff2cd0541720c304cf9b2299c52e9d0645eb623816b411b8dcea64d1f", size = 23150208, upload-time = "2026-05-01T18:32:31.413Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/5a5faa8ac7d6b6dd4dcfcc31bb1ab4c5a2f10ee90224fb3891181c76e24e/xgrammar-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f70f6c2f062535f54836851794d11f96f2a15d8e42a4b21dfbc14767d52c096e", size = 23055200, upload-time = "2026-05-01T18:32:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1c/92eac0cd125ba195e3f1e3e25e89aedcaecbf99a4034ab12b7655ac07453/xgrammar-0.2.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddad831bc7da41d52ed34b7e1050c9a37d3f5f2314eaed8e658cbd2a34625e31", size = 44155238, upload-time = "2026-05-01T18:32:38.679Z" }, + { url = "https://files.pythonhosted.org/packages/7e/30/99f4e83821db16d58dd41249ba46038ed47bce274c57ad5567030775fc62/xgrammar-0.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a36c744d24d93e178c138486aa02b390a80326b64ff11e222e063a028dd65849", size = 44616361, upload-time = "2026-05-01T18:32:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5c/35a84b53a057b637d60ee039872589204724c92579c1ded1bd7f8f1b449e/xgrammar-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bf78d76afcb94372b5c3a05da7f80ad74a8973d971d43a0d4961ea672a8f5fb", size = 7400441, upload-time = "2026-05-01T18:32:45.831Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a1/ce7a1c2ebe89ee2715885935721169ecb9fbd13ab791d8f4dc0a86b87259/xgrammar-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c7d88530100b6bbb0bc3ccfa7fb50385c938ce15e9219b47bb91fd5b63c2788", size = 23055213, upload-time = "2026-05-01T18:32:48.649Z" }, + { url = "https://files.pythonhosted.org/packages/36/22/18bfae3275613493f0fcbd274f2fa169f85c333ffa9581fca83c25669b8a/xgrammar-0.2.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ea1451a1df7aeb39ef97f7b4b8860b7f80424251943563aac48fa98b7b7e939", size = 44155210, upload-time = "2026-05-01T18:32:52.201Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b5/0e4d77b7a91be685e7e388d06c7215cbb7c241402f64b4366d8a4a7a847e/xgrammar-0.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91b3cd498713042ae51c458e2357954e54df0abaea217d6e4297e8065f31a258", size = 44616344, upload-time = "2026-05-01T18:32:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/974cb6cf9f2b62ec32d525af160cdea1b99f902c54b5e44c2c659a96ffc9/xgrammar-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c85eef94b3905216fecd2c07d9b0c49d963280dfb7905636b44458b8add42ce0", size = 7400474, upload-time = "2026-05-01T18:32:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/bfdb217b4c65c7019286b404ebe69f134e20851d040fe97aab06e8562330/xgrammar-0.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99f62252522d4774a54efaec179df27d8e19430bf8b7ea2535aaa9af91197085", size = 23150351, upload-time = "2026-05-01T18:33:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/17/2d/72b7437ac170983e2245e96044bce9836585307f83265cfd424f62ef96aa/xgrammar-0.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82ae4aeee4165274e8899f1dbfbc4e59cb1a69ef658c074b374cc1addf23c345", size = 23055214, upload-time = "2026-05-01T18:33:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3a/58a7524c130d7596e20da10ae0683567005e9a5eea5811849cb48b1ee261/xgrammar-0.2.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f26458f7fbfa8c2489a4f29d3d1d7026da114078a0cb96110b4e0a1bb2a1b6e", size = 44155212, upload-time = "2026-05-01T18:33:08.93Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/4dba577b8d729d0f400d35d12194ff9754db4d15dd443b4e2a3f1f4653da/xgrammar-0.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe904ebf9bfa46003fd098d9fb0696a4e37d85c170f435ee14dfaeab00f956ce", size = 44616380, upload-time = "2026-05-01T18:33:13.09Z" }, + { url = "https://files.pythonhosted.org/packages/51/c5/f1639358ab7074fe0ee98bfb8023d370090ac00f280715b72f33a0d68d3a/xgrammar-0.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:f13dc94f001d24abf101b08e8cd63366dc8ecafc474edb76fc17c54efbcc68e8", size = 7492601, upload-time = "2026-05-01T18:33:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/88/47/9e98845a9ee060174f6c786de654de8880ef4e99e65e8405c9b67d52e84a/xgrammar-0.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eeb96f8a709318c73b1fa0bd46cb9dbdc57e564e18e705ccc194456f18ed28b4", size = 23150338, upload-time = "2026-05-01T18:33:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/30/ec/3f2baa04c80e9a289b1f27ea96662ada212630c6c058ce4873069e1abb8e/xgrammar-0.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d3d4df5bdaa9d945f2b435c60c14af692b64435ab598c739813ab1fb3146e2f0", size = 23055166, upload-time = "2026-05-01T18:33:22.358Z" }, + { url = "https://files.pythonhosted.org/packages/ff/64/243ce8250877ee9b8f3f9745e2f6d5c8dc2e13ad71e875d09204b9f031aa/xgrammar-0.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8675ca4512eb2a58a9314a022bf4e7089e1161edb9ef2b2c87390f84078611b8", size = 44155253, upload-time = "2026-05-01T18:33:26.026Z" }, + { url = "https://files.pythonhosted.org/packages/32/4c/507e35a290ce2bfb013efcf199e430b269282c9bb571df7788594ae9203a/xgrammar-0.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b17d98dd62c96aedd5b0ff0643cc2343eebe40782d469a14e650a3c7402d749", size = 44616337, upload-time = "2026-05-01T18:33:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/5c/31/9fe0123c482b4eb85b3feb44957d1e5b6596b1b07b85cd6d0decf3f8da8c/xgrammar-0.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ce4e7603c26e486994dc882b1cba7d79774cc75fd0a7e998f9110035f336ab4", size = 7492694, upload-time = "2026-05-01T18:33:33.389Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "z3-solver" +version = "4.15.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/8e/0c8f17309549d2e5cde9a3ccefa6365437f1e7bafe71878eaf9478e47b18/z3_solver-4.15.4.0.tar.gz", hash = "sha256:928c29b58c4eb62106da51c1914f6a4a55d0441f8f48a81b9da07950434a8946", size = 5018600, upload-time = "2025-10-29T18:12:03.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/33/a3d5d2eaeb0f7b3174d57d405437eabb2075d4d50bd9ea0957696c435c7b/z3_solver-4.15.4.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:407e825cc9211f95ef46bdc8d151bf630e7ab2d62a21d24cd74c09cc5b73f3aa", size = 37052538, upload-time = "2025-10-29T18:11:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/fd7ffac1551cd9f8d44fe41358f738be670fc4c24dfd514fab503f2cf3e7/z3_solver-4.15.4.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:00bd10c5a6a5f6112d3a9a810d0799227e52f76caa860dafa5e00966bb47eb13", size = 39807925, upload-time = "2025-10-29T18:11:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/c9/bb51a96af0091324c81b803f16c49f719f9f6ea0b0bb52200f5c97ec4892/z3_solver-4.15.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e103a6f203f505b8b8b8e5c931cc407c95b61556512d4921c1ddc0b3f41b08e", size = 29268352, upload-time = "2025-10-29T18:11:53.032Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/0b49f7e4e53817cfb09a0f6585012b782dfe0b666e8abefcb4fac0570606/z3_solver-4.15.4.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:62c7e9cbdd711932301f29919ad9158de9b2f58b4d281dd259bbcd0a2f408ba1", size = 27226534, upload-time = "2025-10-29T18:11:55.59Z" }, + { url = "https://files.pythonhosted.org/packages/26/91/33de49538444d4aafbe47415c450c2f9abab1733e1226f276b496672f46c/z3_solver-4.15.4.0-py3-none-win32.whl", hash = "sha256:be3bc916545c96ffbf89e00d07104ff14f78336e55db069177a1bfbcc01b269d", size = 13191672, upload-time = "2025-10-29T18:11:58.424Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/a0b135e4419df475177ae78fc93c422430b0fd8875649486f9a5989772e6/z3_solver-4.15.4.0-py3-none-win_amd64.whl", hash = "sha256:00e35b02632ed085ea8199fb230f6015e6fc40554a6680c097bd5f060e827431", size = 16259597, upload-time = "2025-10-29T18:12:01.14Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/cosmos-inference/vllm-cosmos3/vllm_cosmos3/__init__.py b/cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/__init__.py similarity index 82% rename from cosmos-inference/vllm-cosmos3/vllm_cosmos3/__init__.py rename to cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/__init__.py index 9d6cfd6c..bbecf35c 100644 --- a/cosmos-inference/vllm-cosmos3/vllm_cosmos3/__init__.py +++ b/cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/__init__.py @@ -13,8 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""vLLM plugin: register Cosmos3 models.""" -# See https://docs.vllm.ai/en/latest/design/plugin_system +"""vLLM plugin: register Cosmos3 models. + +See https://docs.vllm.ai/en/latest/design/plugin_system +""" + +import logging + +logger = logging.getLogger(__name__) def register(): @@ -22,7 +28,7 @@ def register(): arch = "Cosmos3ForConditionalGeneration" if arch not in ModelRegistry.get_supported_archs(): - print(f"Registering architecture {arch}") + logger.info("Registering architecture %s", arch) ModelRegistry.register_model( arch, "vllm_cosmos3.model:Cosmos3ForConditionalGeneration", diff --git a/cosmos-inference/vllm-cosmos3/vllm_cosmos3/model.py b/cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/model.py similarity index 50% rename from cosmos-inference/vllm-cosmos3/vllm_cosmos3/model.py rename to cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/model.py index 1e13fe0a..d3339c3b 100644 --- a/cosmos-inference/vllm-cosmos3/vllm_cosmos3/model.py +++ b/cosmos-inference/packages/vllm-cosmos3/vllm_cosmos3/model.py @@ -13,43 +13,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Subclass of Qwen3VLForConditionalGeneration that strips a configurable -prefix from checkpoint weight names before loading. +"""Load the understanding tower of a Cosmos3 checkpoint.""" -The prefix is read from the env var COSMOS3_CKPT_PREFIX (e.g. "my_wrapper."). -""" - -import os +import logging +import re from collections.abc import Iterable import torch from vllm.model_executor.models.qwen3_vl import Qwen3VLForConditionalGeneration -_COSMOS3_DROP_TOKENS: tuple[str, ...] = ( - "_moe_gen", - "llm2vae", - "vae2llm", - "time_embedder", +logger = logging.getLogger(__name__) + +_DROP_PATTERNS: tuple[str, ...] = ( + r"_moe_gen", + r"^llm2vae\.", + r"^vae2llm\.", + r"^time_embedder\.", +) +"""Generation-tower drop patterns (regex, matched via `re.search`).""" +_DROP_RE = re.compile("|".join(_DROP_PATTERNS)) + +_KEY_MAPPING: dict[str, str] = { + # Flat Qwen3 -> nested HF Qwen3-VL. Negative lookahead skips already-nested keys. + r"^model\.(?!language_model\.)(.+)$": r"model.language_model.\1", +} +_KEY_MAPPING_RES: tuple[tuple[re.Pattern[str], str], ...] = tuple( + (re.compile(src), tgt) for src, tgt in _KEY_MAPPING.items() ) def _is_und_tower_weight(name: str) -> bool: - """Return True if `name` belongs to the understanding (text) tower.""" - return not any(token in name for token in _COSMOS3_DROP_TOKENS) - - -def _to_qwen3vl_hf_name(name: str) -> str: - """Wrap flat Qwen3 keys into the nested HF Qwen3-VL namespace. - - Cosmos3's understanding tower is saved with the standalone Qwen3 layout - (``model.layers.*``, ``model.embed_tokens.*``, ``model.norm.*``, - ``lm_head.*``), but vLLM's ``Qwen3VLForConditionalGeneration`` expects HF - Qwen3-VL naming (``model.language_model.layers.*`` etc.). Its - ``hf_to_vllm_mapper`` then rewrites ``model.language_model.`` to - ``language_model.model.`` for the runtime module tree. - """ - if name.startswith("model.") and not name.startswith("model.language_model."): - return "model.language_model." + name[len("model.") :] + return _DROP_RE.search(name) is None + + +def _to_hf_name(name: str) -> str: + for pat, repl in _KEY_MAPPING_RES: + name = pat.sub(repl, name) return name @@ -66,23 +65,29 @@ def load_weights( self, weights: Iterable[tuple[str, torch.Tensor]], ) -> set[str]: - prefix = os.environ.get("COSMOS3_CKPT_PREFIX", "") - if prefix: - prefix = prefix.rstrip(".") + "." + # vLLM's per-weight callback: filter generation-tower entries and + # rename understanding-tower entries before delegating to the base. + kept = 0 + skipped = 0 def _iter() -> Iterable[tuple[str, torch.Tensor]]: + nonlocal kept, skipped for name, tensor in weights: - stripped = name.removeprefix(prefix) if prefix else name - if _is_und_tower_weight(stripped): - yield _to_qwen3vl_hf_name(stripped), tensor + if _is_und_tower_weight(name): + kept += 1 + yield _to_hf_name(name), tensor + else: + skipped += 1 loaded = super().load_weights(_iter()) - - # The cosmos3 checkpoint has no `visual.*` weights — they don't exist - # in the OmniMoT understanding tower. Treat them as intentionally - # uninitialized so vLLM 0.19+ doesn't hard-error on missing weights. - # This means the ViT runs with random init; safe iff prompts are - # text-only (the vision pathway is then never invoked). + logger.info( + "Cosmos3 vllm shim: kept %d understanding weights, skipped %d generation weights", + kept, + skipped, + ) + + # Drop once a future checkpoint ships `visual.*`. Mark the params as + # loaded so vLLM 0.19+ doesn't error on missing weights. for name, _ in self.named_parameters(): if name.startswith("visual."): loaded.add(name) diff --git a/cosmos-inference/schemas/OmniSampleOverrides.schema.json b/cosmos-inference/schemas/OmniSampleOverrides.schema.json index b2ec920b..2039248d 100644 --- a/cosmos-inference/schemas/OmniSampleOverrides.schema.json +++ b/cosmos-inference/schemas/OmniSampleOverrides.schema.json @@ -1,13 +1,17 @@ { "$defs": { - "ActionMode": { + "ModelMode": { "enum": [ - "policy", + "text2image", + "text2video", + "image2image", + "image2video", + "video2video", "forward_dynamics", "inverse_dynamics", - "image2video" + "policy" ], - "title": "ActionMode", + "title": "ModelMode", "type": "string" }, "NegativeMetadataMode": { @@ -22,18 +26,6 @@ }, "additionalProperties": false, "properties": { - "action_mode": { - "anyOf": [ - { - "$ref": "#/$defs/ActionMode" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Action mode. When set, activates Action batch construction." - }, "action_path": { "anyOf": [ { @@ -345,6 +337,18 @@ "description": "Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.", "title": "Negative Prompt Keep Metadata" }, + "model_mode": { + "anyOf": [ + { + "$ref": "#/$defs/ModelMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and\n``num_frames``; action modes must be set explicitly." + }, "num_steps": { "anyOf": [ { diff --git a/cosmos-inference/schemas/OmniSampleOverrides.yaml b/cosmos-inference/schemas/OmniSampleOverrides.yaml index b8fc4158..2202ff15 100644 --- a/cosmos-inference/schemas/OmniSampleOverrides.yaml +++ b/cosmos-inference/schemas/OmniSampleOverrides.yaml @@ -15,8 +15,6 @@ # Number of action steps to predict. action_chunk_size: null -# Action mode. When set, activates Action batch construction. -action_mode: null # Path to action JSON file. Required for forward_dynamics mode. action_path: null # Vision aspect ratio. When None, image_edit preserves the input image's native @@ -48,6 +46,9 @@ inverse_duration_template: null inverse_resolution_template: null # Model name. model: null +# Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and +# ``num_frames``; action modes must be set explicitly. +model_mode: null # Name of the sample. name: null # Negative prompt metadata mode: 'none', 'same', or 'inverse'. diff --git a/cosmos-inference/schemas/OmniSetupOverrides.schema.json b/cosmos-inference/schemas/OmniSetupOverrides.schema.json index 6a96b7f2..2ddfb709 100644 --- a/cosmos-inference/schemas/OmniSetupOverrides.schema.json +++ b/cosmos-inference/schemas/OmniSetupOverrides.schema.json @@ -1,15 +1,5 @@ { "$defs": { - "ActionMode": { - "enum": [ - "policy", - "forward_dynamics", - "inverse_dynamics", - "image2video" - ], - "title": "ActionMode", - "type": "string" - }, "CheckpointDirHf": { "additionalProperties": false, "description": "Config for checkpoint directory on Hugging Face.", @@ -75,6 +65,20 @@ "title": "ConfigFileType", "type": "string" }, + "ModelMode": { + "enum": [ + "text2image", + "text2video", + "image2image", + "image2video", + "video2video", + "forward_dynamics", + "inverse_dynamics", + "policy" + ], + "title": "ModelMode", + "type": "string" + }, "NegativeMetadataMode": { "enum": [ "none", @@ -87,18 +91,6 @@ "OmniSampleOverrides": { "additionalProperties": false, "properties": { - "action_mode": { - "anyOf": [ - { - "$ref": "#/$defs/ActionMode" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Action mode. When set, activates Action batch construction." - }, "action_path": { "anyOf": [ { @@ -410,6 +402,18 @@ "description": "Compatibility flag. If True and mode is 'none', mode is promoted to 'same'.", "title": "Negative Prompt Keep Metadata" }, + "model_mode": { + "anyOf": [ + { + "$ref": "#/$defs/ModelMode" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and\n``num_frames``; action modes must be set explicitly." + }, "num_steps": { "anyOf": [ { @@ -863,6 +867,12 @@ "title": "Keep Going", "type": "boolean" }, + "skip_invalid_samples": { + "default": false, + "description": "If True, skip samples whose modality (e.g. action, sound) is not supported by the\nloaded model and emit a ``status='skip'`` output instead of raising. Useful for tests\nand examples that exercise multiple modalities against checkpoints with varying support.", + "title": "Skip Invalid Samples", + "type": "boolean" + }, "debug": { "default": false, "description": "If True, enable debug outputs.", @@ -926,7 +936,6 @@ "sample_overrides": { "$ref": "#/$defs/OmniSampleOverrides", "default": { - "action_mode": null, "action_path": null, "domain_name": null, "image_size": null, @@ -949,6 +958,7 @@ "inverse_duration_template": null, "inverse_resolution_template": null, "negative_prompt_keep_metadata": null, + "model_mode": null, "num_steps": null, "guidance": null, "guidance_interval": null, diff --git a/cosmos-inference/schemas/OmniSetupOverrides.yaml b/cosmos-inference/schemas/OmniSetupOverrides.yaml index 1fe16ff6..19fff90f 100644 --- a/cosmos-inference/schemas/OmniSetupOverrides.yaml +++ b/cosmos-inference/schemas/OmniSetupOverrides.yaml @@ -73,8 +73,6 @@ profile: false sample_overrides: # Number of action steps to predict. action_chunk_size: null - # Action mode. When set, activates Action batch construction. - action_mode: null # Path to action JSON file. Required for forward_dynamics mode. action_path: null # Vision aspect ratio. When None, image_edit preserves the input image's native @@ -106,6 +104,9 @@ sample_overrides: inverse_resolution_template: null # Model name. model: null + # Generation modality. When omitted, the VFM modality is inferred from ``vision_path`` and + # ``num_frames``; action modes must be set explicitly. + model_mode: null # Name of the sample. name: null # Negative prompt metadata mode: 'none', 'same', or 'inverse'. @@ -154,6 +155,10 @@ sample_overrides: # Path or URL to conditioning image/video. vision_path: null sampler: unipc +# If True, skip samples whose modality (e.g. action, sound) is not supported by the +# loaded model and emit a ``status='skip'`` output instead of raising. Useful for tests +# and examples that exercise multiple modalities against checkpoints with varying support. +skip_invalid_samples: false tp_size: 1 use_cuda_graphs: false # If True, use EMA weights. Otherwise, use regular weights. diff --git a/cosmos-inference/vllm-cosmos3/README.md b/cosmos-inference/vllm-cosmos3/README.md deleted file mode 100644 index fa1101e2..00000000 --- a/cosmos-inference/vllm-cosmos3/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cosmos3 vLLM Plugin - -See [Cosmos-Reason2](https://github.com/nvidia-cosmos/cosmos-reason2) for inference examples. - -```shell -VLLM_USE_DEEP_GEMM=0 uvx --torch-backend=auto --with-editable ./vllm-cosmos3 vllm@latest serve nvidia/Cosmos3-Nano-Internal \ - --revision spectralflight/vllm-shim \ - --trust-remote-code \ - --allowed-local-media-path "$(pwd)" \ - --max-model-len 16384 \ - --media-io-kwargs '{"video": {"num_frames": -1}}' \ - --reasoning-parser qwen3 \ - --port 8000 -``` diff --git a/cosmos-inference/vllm-cosmos3/uv.lock b/cosmos-inference/vllm-cosmos3/uv.lock deleted file mode 100644 index eaa8b513..00000000 --- a/cosmos-inference/vllm-cosmos3/uv.lock +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" - -[[package]] -name = "vllm-cosmos3" -version = "0.1.0" -source = { editable = "." } From c82be1cf574820041186c192db0225061639a1fa Mon Sep 17 00:00:00 2001 From: yangyangt Date: Thu, 14 May 2026 09:27:07 -0700 Subject: [PATCH 07/24] Add minimal release --- cosmos_training/configs/base/__init__.py | 0 .../base/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 231 bytes .../base/__pycache__/config.cpython-312.pyc | Bin 0 -> 5536 bytes .../configs/base/base_config_test.py | 217 ++ cosmos_training/configs/base/config.py | 138 + .../configs/base/defaults/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 240 bytes .../__pycache__/callbacks.cpython-312.pyc | Bin 0 -> 5739 bytes .../__pycache__/checkpointer.cpython-312.pyc | Bin 0 -> 3766 bytes .../__pycache__/cluster.cpython-312.pyc | Bin 0 -> 1894 bytes .../defaults/__pycache__/ema.cpython-312.pyc | Bin 0 -> 1225 bytes .../__pycache__/model.cpython-312.pyc | Bin 0 -> 1377 bytes .../__pycache__/model_config.cpython-312.pyc | Bin 0 -> 10479 bytes .../open_source_dataloader.cpython-312.pyc | Bin 0 -> 5984 bytes .../__pycache__/optimizer.cpython-312.pyc | Bin 0 -> 2593 bytes .../__pycache__/parallelism.cpython-312.pyc | Bin 0 -> 1945 bytes .../__pycache__/tokenizer.cpython-312.pyc | Bin 0 -> 2947 bytes .../defaults/__pycache__/vlm.cpython-312.pyc | Bin 0 -> 22509 bytes .../configs/base/defaults/callbacks.py | 142 + .../configs/base/defaults/checkpointer.py | 132 + .../configs/base/defaults/cluster.py | 58 + .../configs/base/defaults/conditioner.py | 194 ++ cosmos_training/configs/base/defaults/ema.py | 40 + .../configs/base/defaults/model.py | 54 + .../configs/base/defaults/model_config.py | 360 ++ .../base/defaults/multiview_dataloader.py | 162 + .../base/defaults/open_source_dataloader.py | 209 ++ .../configs/base/defaults/optimizer.py | 112 + .../configs/base/defaults/parallelism.py | 85 + .../configs/base/defaults/tokenizer.py | 61 + .../configs/base/defaults/unittest.py | 43 + cosmos_training/configs/base/defaults/vlm.py | 985 ++++++ cosmos_training/configs/base/vlm/__init__.py | 0 .../vlm/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 235 bytes cosmos_training/configs/base/vlm/config.py | 73 + .../configs/base/vlm/defaults/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 244 bytes .../__pycache__/training.cpython-312.pyc | Bin 0 -> 4546 bytes .../configs/base/vlm/defaults/callbacks.py | 120 + .../configs/base/vlm/defaults/checkpointer.py | 87 + .../configs/base/vlm/defaults/config.py | 82 + .../configs/base/vlm/defaults/dataloader.py | 92 + .../vlm/defaults/dataloader_weighted_url.py | 570 ++++ .../configs/base/vlm/defaults/model.py | 35 + .../configs/base/vlm/defaults/optimizer.py | 65 + .../configs/base/vlm/defaults/training.py | 117 + .../configs/base/vlm/defaults/vlm_policy.py | 170 + .../configs/base/vlm/experiment/__init__.py | 0 .../experiment/pre_exp012_phase2_vlm_smoke.py | 105 + .../configs/base/vlm/experiment/pre_exp01x.py | 852 +++++ .../configs/base/vlm/experiment/utils.py | 28 + cosmos_training/cosmos/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 225 bytes cosmos_training/cosmos/callbacks/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 235 bytes .../compile_tokenizer.cpython-312.pyc | Bin 0 -> 4939 bytes .../device_monitor.cpython-312.pyc | Bin 0 -> 10821 bytes .../__pycache__/every_n.cpython-312.pyc | Bin 0 -> 3726 bytes .../every_n_draw_sample.cpython-312.pyc | Bin 0 -> 23509 bytes .../expert_heatmap.cpython-312.pyc | Bin 0 -> 5728 bytes .../__pycache__/grad_clip.cpython-312.pyc | Bin 0 -> 14704 bytes .../__pycache__/heart_beat.cpython-312.pyc | Bin 0 -> 5438 bytes .../__pycache__/iter_speed.cpython-312.pyc | Bin 0 -> 5175 bytes .../load_pretrained.cpython-312.pyc | Bin 0 -> 1247 bytes .../__pycache__/low_precision.cpython-312.pyc | Bin 0 -> 2579 bytes .../__pycache__/manual_gc.cpython-312.pyc | Bin 0 -> 2162 bytes .../callbacks/__pycache__/mfu.cpython-312.pyc | Bin 0 -> 12103 bytes ...oe_specialization_callback.cpython-312.pyc | Bin 0 -> 10078 bytes .../moe_stability_callback.cpython-312.pyc | Bin 0 -> 9853 bytes .../__pycache__/norm_monitor.cpython-312.pyc | Bin 0 -> 18286 bytes .../callbacks/__pycache__/ofu.cpython-312.pyc | Bin 0 -> 12440 bytes .../__pycache__/param_count.cpython-312.pyc | Bin 0 -> 2143 bytes .../sequence_packing_padding.cpython-312.pyc | Bin 0 -> 3153 bytes .../sigma_loss_analysis.cpython-312.pyc | Bin 0 -> 18534 bytes .../__pycache__/skip_nan_step.cpython-312.pyc | Bin 0 -> 4307 bytes ...mination_signal_checkpoint.cpython-312.pyc | Bin 0 -> 8169 bytes .../training_stats.cpython-312.pyc | Bin 0 -> 15494 bytes .../__pycache__/wandb_log.cpython-312.pyc | Bin 0 -> 11074 bytes .../wandb_log_eval.cpython-312.pyc | Bin 0 -> 7357 bytes .../cosmos/callbacks/compile_tokenizer.py | 118 + .../cosmos/callbacks/data_stats.py | 219 ++ .../cosmos/callbacks/dataloader_state.py | 119 + .../cosmos/callbacks/dataloading_monitor.py | 538 +++ .../cosmos/callbacks/device_monitor.py | 201 ++ cosmos_training/cosmos/callbacks/every_n.py | 85 + .../cosmos/callbacks/every_n_draw_sample.py | 438 +++ .../cosmos/callbacks/expert_heatmap.py | 105 + cosmos_training/cosmos/callbacks/grad_clip.py | 331 ++ .../cosmos/callbacks/heart_beat.py | 106 + cosmos_training/cosmos/callbacks/hf_export.py | 471 +++ .../cosmos/callbacks/iter_speed.py | 121 + .../cosmos/callbacks/load_pretrained.py | 29 + .../cosmos/callbacks/log_tensor_shape.py | 41 + .../cosmos/callbacks/low_precision.py | 70 + cosmos_training/cosmos/callbacks/manual_gc.py | 50 + cosmos_training/cosmos/callbacks/mfu.py | 318 ++ .../callbacks/moe_specialization_callback.py | 203 ++ .../callbacks/moe_stability_callback.py | 203 ++ .../cosmos/callbacks/norm_monitor.py | 345 ++ cosmos_training/cosmos/callbacks/ofu.py | 293 ++ .../cosmos/callbacks/param_count.py | 50 + .../callbacks/sequence_packing_padding.py | 70 + .../cosmos/callbacks/sigma_loss_analysis.py | 356 ++ .../cosmos/callbacks/skip_nan_step.py | 96 + .../termination_signal_checkpoint.py | 178 + .../cosmos/callbacks/training_stats.py | 307 ++ cosmos_training/cosmos/callbacks/wandb_log.py | 239 ++ .../cosmos/callbacks/wandb_log_eval.py | 153 + .../cosmos/callbacks/wandb_log_simgple.py | 167 + cosmos_training/cosmos/callbacks/wandb_vis.py | 106 + cosmos_training/cosmos/checkpoint/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 236 bytes .../__pycache__/base.cpython-312.pyc | Bin 0 -> 8472 bytes .../__pycache__/dcp.cpython-312.pyc | Bin 0 -> 41087 bytes .../__pycache__/dummy.cpython-312.pyc | Bin 0 -> 1791 bytes .../__pycache__/s3_filesystem.cpython-312.pyc | Bin 0 -> 16837 bytes cosmos_training/cosmos/checkpoint/base.py | 185 + cosmos_training/cosmos/checkpoint/dcp.py | 855 +++++ cosmos_training/cosmos/checkpoint/dummy.py | 47 + .../cosmos/checkpoint/s3_filesystem.py | 330 ++ .../cosmos/communicator/__init__.py | 0 cosmos_training/cosmos/data/__init__.py | 0 .../data/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 230 bytes .../joint_dataloader.cpython-312.pyc | Bin 0 -> 43340 bytes .../sequence_packing.cpython-312.pyc | Bin 0 -> 120890 bytes .../vfm/__pycache__/utils.cpython-312.pyc | Bin 0 -> 5427 bytes .../cosmos/data/vfm/action/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 241 bytes .../__pycache__/domain_utils.cpython-312.pyc | Bin 0 -> 1614 bytes .../data/vfm/action/action_normalization.py | 61 + .../vfm/action/action_normalization_test.py | 75 + .../cosmos/data/vfm/action/action_spec.py | 247 ++ .../vfm/action/agibotworld_beta_dataset.py | 366 ++ .../action/agibotworld_beta_dataset_config.py | 200 ++ .../cosmos/data/vfm/action/av_dataset.py | 1006 ++++++ .../vfm/action/bridge_orig_lerobot_dataset.py | 269 ++ .../cosmos/data/vfm/action/camera_dataset.py | 608 ++++ .../data/vfm/action/camera_dataset_sharded.py | 665 ++++ .../data/vfm/action/compute_action_stats.py | 1428 ++++++++ .../data/vfm/action/compute_pose_stats.py | 637 ++++ .../data/vfm/action/cosmos3_action_lerobot.py | 1000 ++++++ .../cosmos/data/vfm/action/dataloaders.py | 160 + .../cosmos/data/vfm/action/domain_utils.py | 47 + .../data/vfm/action/droid_lerobot_dataset.py | 466 +++ .../action/droid_lerobot_dataset_config.py | 97 + .../cosmos/data/vfm/action/dummy_dataset.py | 112 + .../data/vfm/action/embodiment_b_dataset.py | 74 + .../vfm/action/embodiment_b_dataset_config.py | 14 + .../data/vfm/action/embodiment_c_dataset.py | 655 ++++ .../vfm/action/embodiment_c_dataset_config.py | 150 + .../cosmos/data/vfm/action/embodiment_c_fk.py | 496 +++ .../data/vfm/action/embodiment_c_fk_test.py | 464 +++ .../data/vfm/action/embodiment_c_spec.py | 142 + .../data/vfm/action/filter_bridge_dataset.py | 679 ++++ .../cosmos/data/vfm/action/fractal.py | 192 ++ .../data/vfm/action/hand_pose_dataset.py | 1359 ++++++++ .../vfm/action/hand_pose_dataset_config.py | 100 + .../cosmos/data/vfm/action/json_formatter.py | 291 ++ .../vfm/action/libero_action_stats_10k.json | 101 + .../cosmos/data/vfm/action/libero_dataset.py | 623 ++++ .../data/vfm/action/libero_pose_utils.py | 81 + .../data/vfm/action/normalizers/README.md | 79 + ...orig_lerobot_backward_framewise_rot6d.json | 33 + ...roid_lerobot_backward_framewise_rot6d.json | 33 + .../vfm/action/normalizers/embodiment_b.json | 25 + ...nt_c_gripper_backward_framewise_rot6d.json | 33 + .../fractal_backward_framewise_rot6d.json | 33 + .../hand_pose_backward_framewise_rot6d.json | 35 + ...bero_native_frame_wise_relative_rot6d.json | 37 + ...-franka-dual_backward_framewise_rot6d.json | 33 + ...omind-franka_backward_framewise_rot6d.json | 33 + .../robomind-ur_backward_framewise_rot6d.json | 33 + ...a_umi_single_task_anchored_normalizer.json | 85 + ...gle_task_frame_wise_no_rot_normalizer.json | 85 + ...umi_single_task_frame_wise_normalizer.json | 85 + .../uva_umi_single_task_normalizer.json | 85 + .../cosmos/data/vfm/action/pose_utils.py | 759 +++++ .../cosmos/data/vfm/action/pose_utils_test.py | 206 ++ .../cosmos/data/vfm/action/pusht_dataset.py | 249 ++ .../vfm/action/robomind_dataset_config.py | 333 ++ .../vfm/action/robomind_franka_dataset.py | 290 ++ .../data/vfm/action/robomind_ur_dataset.py | 265 ++ .../cosmos/data/vfm/action/transforms.py | 678 ++++ .../cosmos/data/vfm/action/transforms_test.py | 322 ++ .../data/vfm/action/umi/data_classes.py | 94 + .../cosmos/data/vfm/action/umi/data_utils.py | 35 + .../vfm/action/umi/imagecodecs_numcodecs.py | 1344 ++++++++ .../cosmos/data/vfm/action/umi/normalizer.py | 295 ++ .../cosmos/data/vfm/action/umi/pose_utils.py | 283 ++ .../cosmos/data/vfm/action/umi/transforms.py | 77 + .../cosmos/data/vfm/action/umi_dataset.py | 1223 +++++++ .../cosmos/data/vfm/action/umi_dataset.yaml | 213 ++ .../vfm/action/umi_dataset_multi_task.yaml | 171 + .../cosmos/data/vfm/action/unified_dataset.py | 594 ++++ .../data/vfm/action/unified_dataset_test.py | 534 +++ .../G1_omnipicker_calibrated.urdf | 1350 ++++++++ .../vfm/action/urdf_visualizer/__init__.py | 0 .../action/urdf_visualizer/contracts_test.py | 302 ++ .../droid_franka_robotiq_2f85.xml | 372 ++ .../vfm/action/urdf_visualizer/ik_solver.py | 849 +++++ .../urdf_visualizer/robot_scene_model.py | 213 ++ .../action/urdf_visualizer/unified_action.py | 532 +++ .../urdf_visualizer/unified_renderer.py | 1004 ++++++ .../urdf_visualizer/ur5e_robotiq_2f85.xml | 321 ++ .../vfm/action/urdf_visualizer/urdf_loader.py | 1106 ++++++ .../data/vfm/action/urdf_visualizer/viewer.py | 1016 ++++++ .../cosmos/data/vfm/action/viewpoint_utils.py | 111 + .../cosmos/data/vfm/joint_dataloader.py | 989 ++++++ .../data/vfm/local_datasets/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 249 bytes .../__pycache__/helper.cpython-312.pyc | Bin 0 -> 9781 bytes .../__pycache__/sft_dataset.cpython-312.pyc | Bin 0 -> 30422 bytes .../cosmos/data/vfm/local_datasets/helper.py | 265 ++ .../data/vfm/local_datasets/sft_dataset.py | 691 ++++ .../cosmos/data/vfm/processors/__init__.py | 176 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 8120 bytes ...nemotron3densevl_processor.cpython-312.pyc | Bin 0 -> 13411 bytes .../nemotronvl_processor.cpython-312.pyc | Bin 0 -> 25039 bytes .../qwen3vl_processor.cpython-312.pyc | Bin 0 -> 14136 bytes .../processors/nemotron3densevl_processor.py | 286 ++ .../vfm/processors/nemotronvl_processor.py | 590 ++++ .../data/vfm/processors/qwen3vl_processor.py | 350 ++ .../cosmos/data/vfm/sequence_packing.py | 2987 ++++++++++++++++ cosmos_training/cosmos/data/vfm/utils.py | 185 + .../data/vlm/processors/qwen3vl_processor.py | 325 ++ cosmos_training/cosmos/launcher/__init__.py | 0 cosmos_training/cosmos/model/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 231 bytes .../model/__pycache__/_base.cpython-312.pyc | Bin 0 -> 6330 bytes cosmos_training/cosmos/model/_base.py | 130 + .../cosmos/model/attention/__init__.py | 37 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 628 bytes .../__pycache__/backends.cpython-312.pyc | Bin 0 -> 12411 bytes .../__pycache__/checks.cpython-312.pyc | Bin 0 -> 26464 bytes .../__pycache__/frontend.cpython-312.pyc | Bin 0 -> 32629 bytes .../__pycache__/masks.cpython-312.pyc | Bin 0 -> 755 bytes .../__pycache__/varlen.cpython-312.pyc | Bin 0 -> 9425 bytes .../cosmos/model/attention/backends.py | 418 +++ .../cosmos/model/attention/checks.py | 634 ++++ .../cosmos/model/attention/cudnn/__init__.py | 83 + .../cosmos/model/attention/cudnn/checks.py | 144 + .../model/attention/cudnn/cudnn_forward.py | 402 +++ .../cosmos/model/attention/cudnn/functions.py | 269 ++ .../cosmos/model/attention/cudnn/meta.py | 62 + .../cosmos/model/attention/cudnn/stubs.py | 47 + .../cosmos/model/attention/flash2/__init__.py | 88 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 2304 bytes .../flash2/__pycache__/checks.cpython-312.pyc | Bin 0 -> 4055 bytes .../__pycache__/functions.cpython-312.pyc | Bin 0 -> 7216 bytes .../flash2/__pycache__/meta.cpython-312.pyc | Bin 0 -> 2021 bytes .../flash2/__pycache__/stubs.cpython-312.pyc | Bin 0 -> 1497 bytes .../cosmos/model/attention/flash2/checks.py | 136 + .../model/attention/flash2/functions.py | 198 ++ .../cosmos/model/attention/flash2/meta.py | 64 + .../cosmos/model/attention/flash2/stubs.py | 47 + .../cosmos/model/attention/flash3/__init__.py | 74 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 2377 bytes .../flash3/__pycache__/checks.cpython-312.pyc | Bin 0 -> 4627 bytes .../flash3/__pycache__/meta.cpython-312.pyc | Bin 0 -> 1996 bytes .../flash3/__pycache__/stubs.cpython-312.pyc | Bin 0 -> 1497 bytes .../cosmos/model/attention/flash3/checks.py | 138 + .../model/attention/flash3/functions.py | 213 ++ .../cosmos/model/attention/flash3/meta.py | 64 + .../cosmos/model/attention/flash3/stubs.py | 47 + .../cosmos/model/attention/frontend.py | 769 +++++ .../cosmos/model/attention/masks.py | 61 + .../cosmos/model/attention/natten/__init__.py | 127 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3488 bytes .../natten/__pycache__/checks.cpython-312.pyc | Bin 0 -> 17399 bytes .../natten/__pycache__/meta.cpython-312.pyc | Bin 0 -> 2387 bytes .../natten/__pycache__/stubs.cpython-312.pyc | Bin 0 -> 2767 bytes .../cosmos/model/attention/natten/checks.py | 521 +++ .../model/attention/natten/functions.py | 487 +++ .../cosmos/model/attention/natten/meta.py | 67 + .../cosmos/model/attention/natten/stubs.py | 82 + .../cosmos/model/attention/utils/__init__.py | 86 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3262 bytes .../__pycache__/determinism.cpython-312.pyc | Bin 0 -> 2213 bytes .../__pycache__/environment.cpython-312.pyc | Bin 0 -> 5282 bytes .../utils/__pycache__/version.cpython-312.pyc | Bin 0 -> 1696 bytes .../attention/utils/backend_filter_test.py | 230 ++ .../model/attention/utils/determinism.py | 56 + .../model/attention/utils/determinism_test.py | 222 ++ .../model/attention/utils/environment.py | 134 + .../attention/utils/safe_ops/__init__.py | 26 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 614 bytes .../__pycache__/functools.cpython-312.pyc | Bin 0 -> 2038 bytes .../safe_ops/__pycache__/log.cpython-312.pyc | Bin 0 -> 2442 bytes .../attention/utils/safe_ops/functools.py | 68 + .../model/attention/utils/safe_ops/log.py | 64 + .../utils/safe_ops/safe_lru_cache_test.py | 188 + .../cosmos/model/attention/utils/version.py | 50 + .../model/attention/utils/version_test.py | 155 + .../cosmos/model/attention/varlen.py | 225 ++ .../omni_mot_model.cpython-312.pyc | Bin 0 -> 144944 bytes .../model/vfm/algorithm/loss/__init__.py | 33 + .../loss/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 927 bytes .../__pycache__/cross_entropy.cpython-312.pyc | Bin 0 -> 4284 bytes .../__pycache__/flow_matching.cpython-312.pyc | Bin 0 -> 5360 bytes .../load_balancing.cpython-312.pyc | Bin 0 -> 3255 bytes .../__pycache__/time_weight.cpython-312.pyc | Bin 0 -> 1336 bytes .../model/vfm/algorithm/loss/cross_entropy.py | 110 + .../model/vfm/algorithm/loss/flow_matching.py | 108 + .../vfm/algorithm/loss/load_balancing.py | 82 + .../model/vfm/algorithm/loss/time_weight.py | 40 + .../rectified_flow.cpython-312.pyc | Bin 0 -> 10159 bytes .../model/vfm/diffusion/rectified_flow.py | 174 + .../model/vfm/diffusion/samplers/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 254 bytes .../samplers/__pycache__/edm.cpython-312.pyc | Bin 0 -> 13730 bytes .../__pycache__/fixed_step.cpython-312.pyc | Bin 0 -> 6741 bytes .../fm_solvers_unipc.cpython-312.pyc | Bin 0 -> 35407 bytes .../__pycache__/unipc.cpython-312.pyc | Bin 0 -> 5905 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 3892 bytes .../model/vfm/diffusion/samplers/edm.py | 295 ++ .../vfm/diffusion/samplers/fixed_step.py | 143 + .../diffusion/samplers/fm_solvers_unipc.py | 768 +++++ .../model/vfm/diffusion/samplers/unipc.py | 124 + .../model/vfm/diffusion/samplers/utils.py | 67 + .../cosmos/model/vfm/mot/__init__.py | 0 .../mot/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes .../mot/__pycache__/attention.cpython-312.pyc | Bin 0 -> 15029 bytes .../context_parallel_utils.cpython-312.pyc | Bin 0 -> 16067 bytes .../cosmos3_vfm_network.cpython-312.pyc | Bin 0 -> 48904 bytes .../domain_aware_linear.cpython-312.pyc | Bin 0 -> 3774 bytes .../modeling_utils.cpython-312.pyc | Bin 0 -> 21841 bytes .../parallelize_unified_mot.cpython-312.pyc | Bin 0 -> 3804 bytes .../parallelize_vfm_network.cpython-312.pyc | Bin 0 -> 7429 bytes .../unified_3dmrope_utils.cpython-312.pyc | Bin 0 -> 8622 bytes .../__pycache__/unified_mot.cpython-312.pyc | Bin 0 -> 40803 bytes .../cosmos/model/vfm/mot/attention.py | 430 +++ .../model/vfm/mot/context_parallel_utils.py | 427 +++ .../model/vfm/mot/cosmos3_vfm_network.py | 1118 ++++++ .../model/vfm/mot/domain_aware_linear.py | 90 + .../cosmos/model/vfm/mot/modeling_utils.py | 407 +++ .../model/vfm/mot/parallelize_unified_mot.py | 89 + .../model/vfm/mot/parallelize_vfm_network.py | 172 + .../model/vfm/mot/unified_3dmrope_utils.py | 206 ++ .../cosmos/model/vfm/mot/unified_mot.py | 1041 ++++++ .../cosmos/model/vfm/omni_mot_model.py | 3023 +++++++++++++++++ .../__pycache__/flux_vae_8x8.cpython-312.pyc | Bin 0 -> 26899 bytes .../__pycache__/interface.cpython-312.pyc | Bin 0 -> 9273 bytes .../tokenization_qwen2.cpython-312.pyc | Bin 0 -> 16104 bytes .../wan2pt1_vae_4x8x8.cpython-312.pyc | Bin 0 -> 40495 bytes .../wan2pt2_vae_4x16x16.cpython-312.pyc | Bin 0 -> 70448 bytes .../cosmos/model/vfm/tokenizers/interface.py | 236 ++ .../vfm/tokenizers/tokenization_qwen2.py | 340 ++ .../vfm/tokenizers/wan2pt2_vae_4x16x16.py | 1694 +++++++++ .../cosmos/model/vfm/utils/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 241 bytes .../data_and_condition.cpython-312.pyc | Bin 0 -> 8302 bytes .../utils/__pycache__/memory.cpython-312.pyc | Bin 0 -> 4865 bytes .../safetensors_loader.cpython-312.pyc | Bin 0 -> 48538 bytes .../model/vfm/utils/data_and_condition.py | 181 + .../cosmos/model/vfm/utils/memory.py | 115 + .../model/vfm/utils/safetensors_loader.py | 1160 +++++++ .../cosmos/model/vfm/vlm/__init__.py | 0 .../vlm/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes .../vfm/vlm/nemotron_3_dense_vl/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 259 bytes ...ration_nemotron_3_dense_vl.cpython-312.pyc | Bin 0 -> 3701 bytes .../nemotron_3_dense_vl.cpython-312.pyc | Bin 0 -> 12034 bytes .../configs/Nemotron-2B-Dense-VL.json | 31 + .../configuration_nemotron_3_dense_vl.py | 97 + .../nemotron_3_dense_vl.py | 165 + .../cosmos/model/vfm/vlm/qwen3_vl/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 248 bytes .../configuration_qwen3_vl.cpython-312.pyc | Bin 0 -> 13493 bytes .../__pycache__/qwen3_vl.cpython-312.pyc | Bin 0 -> 84609 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 13607 bytes .../configs/Qwen3-VL-2B-Instruct.json | 63 + .../configs/Qwen3-VL-32B-Instruct.json | 62 + .../configs/Qwen3-VL-4B-Instruct.json | 63 + .../configs/Qwen3-VL-8B-Instruct.json | 62 + .../vlm/qwen3_vl/configuration_qwen3_vl.py | 298 ++ .../cosmos/model/vfm/vlm/qwen3_vl/qwen3_vl.py | 1651 +++++++++ .../cosmos/model/vfm/vlm/qwen3_vl/utils.py | 348 ++ .../model/vfm/vlm/qwen3_vl_moe/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 252 bytes ...configuration_qwen3_vl_moe.cpython-312.pyc | Bin 0 -> 15569 bytes .../__pycache__/moe.cpython-312.pyc | Bin 0 -> 13482 bytes .../__pycache__/moe_kernels.cpython-312.pyc | Bin 0 -> 6772 bytes .../__pycache__/qwen3_vl_moe.cpython-312.pyc | Bin 0 -> 105495 bytes .../configs/Qwen3-VL-235B-A22B-Instruct.json | 68 + .../configs/Qwen3-VL-30B-A3B-Instruct.json | 68 + .../configuration_qwen3_vl_moe.py | 330 ++ .../cosmos/model/vfm/vlm/qwen3_vl_moe/moe.py | 261 ++ .../model/vfm/vlm/qwen3_vl_moe/moe_kernels.py | 216 ++ .../vfm/vlm/qwen3_vl_moe/qwen3_vl_moe.py | 2041 +++++++++++ cosmos_training/cosmos/tools/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 231 bytes .../cosmos/tools/flops/__init__.py | 30 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 615 bytes .../__pycache__/omni_mot.cpython-312.pyc | Bin 0 -> 18702 bytes .../flops/__pycache__/wan_vae.cpython-312.pyc | Bin 0 -> 5840 bytes .../cosmos/tools/flops/omni_mot.py | 498 +++ cosmos_training/cosmos/tools/flops/wan_vae.py | 134 + .../__pycache__/video.cpython-312.pyc | Bin 0 -> 3900 bytes .../cosmos/tools/visualize/video.py | 96 + cosmos_training/cosmos/trainer/__init__.py | 460 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 27642 bytes cosmos_training/cosmos/utils/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 231 bytes .../__pycache__/callback.cpython-312.pyc | Bin 0 -> 32879 bytes .../__pycache__/checkpoint_db.cpython-312.pyc | Bin 0 -> 16770 bytes .../__pycache__/checkpointer.cpython-312.pyc | Bin 0 -> 24106 bytes .../__pycache__/cluster_env.cpython-312.pyc | Bin 0 -> 7572 bytes .../utils/__pycache__/config.cpython-312.pyc | Bin 0 -> 23048 bytes .../__pycache__/config_helper.cpython-312.pyc | Bin 0 -> 10416 bytes .../context_managers.cpython-312.pyc | Bin 0 -> 3445 bytes .../__pycache__/count_params.cpython-312.pyc | Bin 0 -> 1040 bytes .../utils/__pycache__/device.cpython-312.pyc | Bin 0 -> 7110 bytes .../__pycache__/distributed.cpython-312.pyc | Bin 0 -> 22332 bytes .../utils/__pycache__/ema.cpython-312.pyc | Bin 0 -> 19676 bytes .../utils/__pycache__/flags.cpython-312.pyc | Bin 0 -> 3227 bytes .../utils/__pycache__/launch.cpython-312.pyc | Bin 0 -> 10325 bytes .../utils/__pycache__/log.cpython-312.pyc | Bin 0 -> 7171 bytes .../utils/__pycache__/misc.cpython-312.pyc | Bin 0 -> 35274 bytes .../__pycache__/object_store.cpython-312.pyc | Bin 0 -> 19137 bytes .../optim_instantiate.cpython-312.pyc | Bin 0 -> 3804 bytes .../__pycache__/profiling.cpython-312.pyc | Bin 0 -> 11496 bytes .../__pycache__/progress_bar.cpython-312.pyc | Bin 0 -> 2399 bytes .../__pycache__/serialization.cpython-312.pyc | Bin 0 -> 17426 bytes .../utils/__pycache__/timer.cpython-312.pyc | Bin 0 -> 11884 bytes .../__pycache__/validator.cpython-312.pyc | Bin 0 -> 25405 bytes .../__pycache__/wandb_util.cpython-312.pyc | Bin 0 -> 7010 bytes cosmos_training/cosmos/utils/callback.py | 618 ++++ cosmos_training/cosmos/utils/checkpoint_db.py | 446 +++ cosmos_training/cosmos/utils/checkpointer.py | 504 +++ cosmos_training/cosmos/utils/cluster_env.py | 166 + cosmos_training/cosmos/utils/config.py | 600 ++++ cosmos_training/cosmos/utils/config_helper.py | 219 ++ .../__pycache__/lr_scheduler.cpython-312.pyc | Bin 0 -> 676 bytes .../cosmos/utils/configs/lr_scheduler.py | 26 + .../cosmos/utils/context_managers.py | 78 + cosmos_training/cosmos/utils/count_params.py | 23 + cosmos_training/cosmos/utils/device.py | 152 + cosmos_training/cosmos/utils/distributed.py | 491 +++ .../cosmos/utils/easy_io/README.md | 3 + .../cosmos/utils/easy_io/__init__.py | 14 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes .../__pycache__/easy_io.cpython-312.pyc | Bin 0 -> 38173 bytes .../__pycache__/file_client.cpython-312.pyc | Bin 0 -> 19274 bytes .../cosmos/utils/easy_io/backends/__init__.py | 23 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 960 bytes .../__pycache__/auto_auth.cpython-312.pyc | Bin 0 -> 2944 bytes .../__pycache__/base_backend.cpython-312.pyc | Bin 0 -> 7159 bytes .../__pycache__/boto3_backend.cpython-312.pyc | Bin 0 -> 35994 bytes .../__pycache__/boto3_client.cpython-312.pyc | Bin 0 -> 31573 bytes .../__pycache__/http_backend.cpython-312.pyc | Bin 0 -> 11489 bytes .../__pycache__/local_backend.cpython-312.pyc | Bin 0 -> 23736 bytes .../__pycache__/msc_backend.cpython-312.pyc | Bin 0 -> 43848 bytes .../registry_utils.cpython-312.pyc | Bin 0 -> 4653 bytes .../utils/easy_io/backends/auto_auth.py | 70 + .../utils/easy_io/backends/base_backend.py | 147 + .../utils/easy_io/backends/boto3_backend.py | 862 +++++ .../utils/easy_io/backends/boto3_client.py | 640 ++++ .../utils/easy_io/backends/http_backend.py | 198 ++ .../utils/easy_io/backends/local_backend.py | 599 ++++ .../utils/easy_io/backends/msc_backend.py | 1075 ++++++ .../utils/easy_io/backends/registry_utils.py | 134 + .../cosmos/utils/easy_io/easy_io.py | 1117 ++++++ .../cosmos/utils/easy_io/easy_io_test.py | 82 + .../cosmos/utils/easy_io/file_client.py | 459 +++ .../cosmos/utils/easy_io/handlers/__init__.py | 29 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 734 bytes .../handlers/__pycache__/base.cpython-312.pyc | Bin 0 -> 1760 bytes .../__pycache__/byte_handler.cpython-312.pyc | Bin 0 -> 1278 bytes .../__pycache__/csv_handler.cpython-312.pyc | Bin 0 -> 2102 bytes .../__pycache__/gzip_handler.cpython-312.pyc | Bin 0 -> 1466 bytes .../imageio_video_handler.cpython-312.pyc | Bin 0 -> 6055 bytes .../__pycache__/json_handler.cpython-312.pyc | Bin 0 -> 2227 bytes .../__pycache__/jsonl_handler.cpython-312.pyc | Bin 0 -> 3676 bytes .../__pycache__/np_handler.cpython-312.pyc | Bin 0 -> 3822 bytes .../pandas_handler.cpython-312.pyc | Bin 0 -> 1244 bytes .../pickle_handler.cpython-312.pyc | Bin 0 -> 2098 bytes .../__pycache__/pil_handler.cpython-312.pyc | Bin 0 -> 3859 bytes .../registry_utils.cpython-312.pyc | Bin 0 -> 3857 bytes .../tarfile_handler.cpython-312.pyc | Bin 0 -> 1948 bytes .../__pycache__/torch_handler.cpython-312.pyc | Bin 0 -> 1257 bytes .../torchjit_handler.cpython-312.pyc | Bin 0 -> 1330 bytes .../trimesh_handler.cpython-312.pyc | Bin 0 -> 1469 bytes .../__pycache__/txt_handler.cpython-312.pyc | Bin 0 -> 1238 bytes .../__pycache__/yaml_handler.cpython-312.pyc | Bin 0 -> 1579 bytes .../cosmos/utils/easy_io/handlers/base.py | 44 + .../utils/easy_io/handlers/byte_handler.py | 39 + .../utils/easy_io/handlers/csv_handler.py | 42 + .../utils/easy_io/handlers/gzip_handler.py | 33 + .../easy_io/handlers/imageio_video_handler.py | 168 + .../utils/easy_io/handlers/json_handler.py | 49 + .../utils/easy_io/handlers/jsonl_handler.py | 80 + .../utils/easy_io/handlers/np_handler.py | 89 + .../utils/easy_io/handlers/pandas_handler.py | 31 + .../utils/easy_io/handlers/pickle_handler.py | 42 + .../utils/easy_io/handlers/pil_handler.py | 96 + .../utils/easy_io/handlers/registry_utils.py | 93 + .../utils/easy_io/handlers/tarfile_handler.py | 39 + .../utils/easy_io/handlers/torch_handler.py | 34 + .../easy_io/handlers/torchjit_handler.py | 34 + .../utils/easy_io/handlers/trimesh_handler.py | 36 + .../utils/easy_io/handlers/txt_handler.py | 34 + .../utils/easy_io/handlers/yaml_handler.py | 38 + cosmos_training/cosmos/utils/ema.py | 366 ++ .../cred_env_parser.cpython-312.pyc | Bin 0 -> 3059 bytes .../__pycache__/env_parser.cpython-312.pyc | Bin 0 -> 6381 bytes .../utils/env_parsers/cred_env_parser.py | 83 + .../env_parsers/customization_env_parser.py | 31 + .../cosmos/utils/env_parsers/env_parser.py | 127 + .../utils/env_parsers/inference_env_parser.py | 45 + cosmos_training/cosmos/utils/flags.py | 98 + .../__pycache__/batch_ops.cpython-312.pyc | Bin 0 -> 1923 bytes .../__pycache__/lr_scheduler.cpython-312.pyc | Bin 0 -> 9469 bytes .../__pycache__/multi_step.cpython-312.pyc | Bin 0 -> 1989 bytes .../__pycache__/runge_kutta.cpython-312.pyc | Bin 0 -> 12761 bytes .../cosmos/utils/functional/batch_ops.py | 61 + .../cosmos/utils/functional/lr_scheduler.py | 178 + .../cosmos/utils/functional/multi_step.py | 60 + .../cosmos/utils/functional/runge_kutta.py | 333 ++ cosmos_training/cosmos/utils/fused_adam.py | 383 +++ cosmos_training/cosmos/utils/launch.py | 179 + .../cosmos/utils/lazy_config/__init__.py | 70 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3013 bytes .../__pycache__/file_io.cpython-312.pyc | Bin 0 -> 617 bytes .../__pycache__/instantiate.cpython-312.pyc | Bin 0 -> 4686 bytes .../__pycache__/lazy.cpython-312.pyc | Bin 0 -> 19799 bytes .../__pycache__/lazy_call.cpython-312.pyc | Bin 0 -> 3167 bytes .../omegaconf_patch.cpython-312.pyc | Bin 0 -> 2479 bytes .../__pycache__/registry.cpython-312.pyc | Bin 0 -> 2418 bytes .../cosmos/utils/lazy_config/file_io.py | 25 + .../cosmos/utils/lazy_config/instantiate.py | 120 + .../cosmos/utils/lazy_config/lazy.py | 377 ++ .../cosmos/utils/lazy_config/lazy_call.py | 81 + .../utils/lazy_config/omegaconf_patch.py | 65 + .../cosmos/utils/lazy_config/registry.py | 91 + cosmos_training/cosmos/utils/log.py | 156 + cosmos_training/cosmos/utils/misc.py | 689 ++++ cosmos_training/cosmos/utils/object_store.py | 417 +++ .../cosmos/utils/one_logger/README.md | 97 + .../one_logger_override_utils.cpython-312.pyc | Bin 0 -> 2643 bytes .../one_logger/one_logger_context_managers.py | 60 + .../one_logger/one_logger_global_vars.py | 86 + .../one_logger/one_logger_override_utils.py | 60 + .../utils/one_logger/one_logger_utils.py | 1445 ++++++++ .../cosmos/utils/optim_instantiate.py | 87 + cosmos_training/cosmos/utils/profiling.py | 188 + cosmos_training/cosmos/utils/progress_bar.py | 76 + cosmos_training/cosmos/utils/serialization.py | 447 +++ cosmos_training/cosmos/utils/timer.py | 297 ++ .../cosmos/utils/training_telemetry/README.md | 122 + .../utils/training_telemetry/__init__.py | 13 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 250 bytes .../__pycache__/telemetry.cpython-312.pyc | Bin 0 -> 4657 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 7358 bytes .../utils/training_telemetry/callback.py | 325 ++ .../training_telemetry/context_managers.py | 56 + .../utils/training_telemetry/telemetry.py | 95 + .../cosmos/utils/training_telemetry/utils.py | 158 + cosmos_training/cosmos/utils/validator.py | 512 +++ .../__pycache__/data_utils.cpython-312.pyc | Bin 0 -> 4152 bytes .../dtensor_helper.cpython-312.pyc | Bin 0 -> 3977 bytes .../model_weights_stats.cpython-312.pyc | Bin 0 -> 3135 bytes .../vfm/__pycache__/optimizer.cpython-312.pyc | Bin 0 -> 29968 bytes .../__pycache__/parallelism.cpython-312.pyc | Bin 0 -> 14451 bytes .../__pycache__/rand_state.cpython-312.pyc | Bin 0 -> 7541 bytes .../cosmos/utils/vfm/data_utils.py | 123 + .../cosmos/utils/vfm/dtensor_helper.py | 66 + .../cosmos/utils/vfm/flash_attn.py | 57 + .../cosmos/utils/vfm/fused_adam.py | 385 +++ .../cosmos/utils/vfm/model_weights_stats.py | 64 + .../cosmos/utils/vfm/monkey_patch.py | 200 ++ cosmos_training/cosmos/utils/vfm/optimizer.py | 587 ++++ .../cosmos/utils/vfm/parallelism.py | 273 ++ .../cosmos/utils/vfm/rand_state.py | 152 + .../cosmos/utils/vfm/vlm/__init__.py | 0 .../vlm/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes ...etrained_models_downloader.cpython-312.pyc | Bin 0 -> 11774 bytes .../cosmos/utils/vfm/vlm/optimizer.py | 336 ++ .../vfm/vlm/pretrained_models_downloader.py | 276 ++ cosmos_training/cosmos/utils/vlm/__init__.py | 0 .../cosmos/utils/vlm/compute_flops_qwen3vl.py | 681 ++++ .../vlm/configs_defaults/checkpointer.py | 116 + cosmos_training/cosmos/utils/vlm/constant.py | 29 + .../cosmos/utils/vlm/create_position_ids.py | 152 + .../cosmos/utils/vlm/dcp_checkpointer.py | 517 +++ .../cosmos/utils/vlm/distributed.py | 427 +++ .../cosmos/utils/vlm/flop_calculator.py | 219 ++ .../cosmos/utils/vlm/fused_adam.py | 408 +++ .../cosmos/utils/vlm/model_wrapper.py | 43 + cosmos_training/cosmos/utils/vlm/optimizer.py | 322 ++ cosmos_training/cosmos/utils/vlm/planner.py | 169 + .../utils/vlm/pretrained_models_downloader.py | 221 ++ .../cosmos/utils/vlm/video_preprocess.py | 48 + cosmos_training/cosmos/utils/wandb_util.py | 172 + .../exp001_text2image_qwen2b.py | 771 +++++ .../mixed_modality_sft_8b.cpython-312.pyc | Bin 0 -> 8183 bytes .../experiments/sft/mixed_modality_sft_8b.py | 302 ++ .../launch_mixed_modality_sft_8b.sh | 79 + .../run_mixed_modality_sft_trace.sh | 146 + .../scripts/__pycache__/train.cpython-312.pyc | Bin 0 -> 9910 bytes cosmos_training/scripts/train.py | 234 ++ 600 files changed, 100686 insertions(+) create mode 100644 cosmos_training/configs/base/__init__.py create mode 100644 cosmos_training/configs/base/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/configs/base/__pycache__/config.cpython-312.pyc create mode 100644 cosmos_training/configs/base/base_config_test.py create mode 100644 cosmos_training/configs/base/config.py create mode 100644 cosmos_training/configs/base/defaults/__init__.py create mode 100644 cosmos_training/configs/base/defaults/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/callbacks.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/checkpointer.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/cluster.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/ema.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/model.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/model_config.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/open_source_dataloader.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/optimizer.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/parallelism.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/tokenizer.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/__pycache__/vlm.cpython-312.pyc create mode 100644 cosmos_training/configs/base/defaults/callbacks.py create mode 100644 cosmos_training/configs/base/defaults/checkpointer.py create mode 100644 cosmos_training/configs/base/defaults/cluster.py create mode 100644 cosmos_training/configs/base/defaults/conditioner.py create mode 100644 cosmos_training/configs/base/defaults/ema.py create mode 100644 cosmos_training/configs/base/defaults/model.py create mode 100644 cosmos_training/configs/base/defaults/model_config.py create mode 100644 cosmos_training/configs/base/defaults/multiview_dataloader.py create mode 100644 cosmos_training/configs/base/defaults/open_source_dataloader.py create mode 100644 cosmos_training/configs/base/defaults/optimizer.py create mode 100644 cosmos_training/configs/base/defaults/parallelism.py create mode 100644 cosmos_training/configs/base/defaults/tokenizer.py create mode 100644 cosmos_training/configs/base/defaults/unittest.py create mode 100644 cosmos_training/configs/base/defaults/vlm.py create mode 100644 cosmos_training/configs/base/vlm/__init__.py create mode 100644 cosmos_training/configs/base/vlm/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/configs/base/vlm/config.py create mode 100644 cosmos_training/configs/base/vlm/defaults/__init__.py create mode 100644 cosmos_training/configs/base/vlm/defaults/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/configs/base/vlm/defaults/__pycache__/training.cpython-312.pyc create mode 100644 cosmos_training/configs/base/vlm/defaults/callbacks.py create mode 100644 cosmos_training/configs/base/vlm/defaults/checkpointer.py create mode 100644 cosmos_training/configs/base/vlm/defaults/config.py create mode 100644 cosmos_training/configs/base/vlm/defaults/dataloader.py create mode 100644 cosmos_training/configs/base/vlm/defaults/dataloader_weighted_url.py create mode 100644 cosmos_training/configs/base/vlm/defaults/model.py create mode 100644 cosmos_training/configs/base/vlm/defaults/optimizer.py create mode 100644 cosmos_training/configs/base/vlm/defaults/training.py create mode 100644 cosmos_training/configs/base/vlm/defaults/vlm_policy.py create mode 100644 cosmos_training/configs/base/vlm/experiment/__init__.py create mode 100644 cosmos_training/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py create mode 100644 cosmos_training/configs/base/vlm/experiment/pre_exp01x.py create mode 100644 cosmos_training/configs/base/vlm/experiment/utils.py create mode 100644 cosmos_training/cosmos/__init__.py create mode 100644 cosmos_training/cosmos/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__init__.py create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/compile_tokenizer.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/device_monitor.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/every_n.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/every_n_draw_sample.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/expert_heatmap.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/grad_clip.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/heart_beat.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/iter_speed.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/load_pretrained.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/low_precision.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/manual_gc.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/mfu.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/moe_specialization_callback.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/moe_stability_callback.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/norm_monitor.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/ofu.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/param_count.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/sequence_packing_padding.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/sigma_loss_analysis.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/skip_nan_step.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/termination_signal_checkpoint.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/training_stats.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/wandb_log.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/__pycache__/wandb_log_eval.cpython-312.pyc create mode 100644 cosmos_training/cosmos/callbacks/compile_tokenizer.py create mode 100644 cosmos_training/cosmos/callbacks/data_stats.py create mode 100644 cosmos_training/cosmos/callbacks/dataloader_state.py create mode 100644 cosmos_training/cosmos/callbacks/dataloading_monitor.py create mode 100644 cosmos_training/cosmos/callbacks/device_monitor.py create mode 100644 cosmos_training/cosmos/callbacks/every_n.py create mode 100644 cosmos_training/cosmos/callbacks/every_n_draw_sample.py create mode 100644 cosmos_training/cosmos/callbacks/expert_heatmap.py create mode 100644 cosmos_training/cosmos/callbacks/grad_clip.py create mode 100644 cosmos_training/cosmos/callbacks/heart_beat.py create mode 100644 cosmos_training/cosmos/callbacks/hf_export.py create mode 100644 cosmos_training/cosmos/callbacks/iter_speed.py create mode 100644 cosmos_training/cosmos/callbacks/load_pretrained.py create mode 100644 cosmos_training/cosmos/callbacks/log_tensor_shape.py create mode 100644 cosmos_training/cosmos/callbacks/low_precision.py create mode 100644 cosmos_training/cosmos/callbacks/manual_gc.py create mode 100644 cosmos_training/cosmos/callbacks/mfu.py create mode 100644 cosmos_training/cosmos/callbacks/moe_specialization_callback.py create mode 100644 cosmos_training/cosmos/callbacks/moe_stability_callback.py create mode 100644 cosmos_training/cosmos/callbacks/norm_monitor.py create mode 100644 cosmos_training/cosmos/callbacks/ofu.py create mode 100644 cosmos_training/cosmos/callbacks/param_count.py create mode 100644 cosmos_training/cosmos/callbacks/sequence_packing_padding.py create mode 100644 cosmos_training/cosmos/callbacks/sigma_loss_analysis.py create mode 100644 cosmos_training/cosmos/callbacks/skip_nan_step.py create mode 100644 cosmos_training/cosmos/callbacks/termination_signal_checkpoint.py create mode 100644 cosmos_training/cosmos/callbacks/training_stats.py create mode 100644 cosmos_training/cosmos/callbacks/wandb_log.py create mode 100644 cosmos_training/cosmos/callbacks/wandb_log_eval.py create mode 100644 cosmos_training/cosmos/callbacks/wandb_log_simgple.py create mode 100644 cosmos_training/cosmos/callbacks/wandb_vis.py create mode 100644 cosmos_training/cosmos/checkpoint/__init__.py create mode 100644 cosmos_training/cosmos/checkpoint/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/checkpoint/__pycache__/base.cpython-312.pyc create mode 100644 cosmos_training/cosmos/checkpoint/__pycache__/dcp.cpython-312.pyc create mode 100644 cosmos_training/cosmos/checkpoint/__pycache__/dummy.cpython-312.pyc create mode 100644 cosmos_training/cosmos/checkpoint/__pycache__/s3_filesystem.cpython-312.pyc create mode 100644 cosmos_training/cosmos/checkpoint/base.py create mode 100644 cosmos_training/cosmos/checkpoint/dcp.py create mode 100644 cosmos_training/cosmos/checkpoint/dummy.py create mode 100644 cosmos_training/cosmos/checkpoint/s3_filesystem.py create mode 100644 cosmos_training/cosmos/communicator/__init__.py create mode 100644 cosmos_training/cosmos/data/__init__.py create mode 100644 cosmos_training/cosmos/data/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/__pycache__/joint_dataloader.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/__pycache__/sequence_packing.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/__pycache__/utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/action/__init__.py create mode 100644 cosmos_training/cosmos/data/vfm/action/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/action/__pycache__/domain_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/action/action_normalization.py create mode 100644 cosmos_training/cosmos/data/vfm/action/action_normalization_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/action_spec.py create mode 100644 cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/av_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/bridge_orig_lerobot_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/camera_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/camera_dataset_sharded.py create mode 100644 cosmos_training/cosmos/data/vfm/action/compute_action_stats.py create mode 100644 cosmos_training/cosmos/data/vfm/action/compute_pose_stats.py create mode 100644 cosmos_training/cosmos/data/vfm/action/cosmos3_action_lerobot.py create mode 100644 cosmos_training/cosmos/data/vfm/action/dataloaders.py create mode 100644 cosmos_training/cosmos/data/vfm/action/domain_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/dummy_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_c_fk.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_c_fk_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/embodiment_c_spec.py create mode 100644 cosmos_training/cosmos/data/vfm/action/filter_bridge_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/fractal.py create mode 100644 cosmos_training/cosmos/data/vfm/action/hand_pose_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/hand_pose_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/json_formatter.py create mode 100644 cosmos_training/cosmos/data/vfm/action/libero_action_stats_10k.json create mode 100644 cosmos_training/cosmos/data/vfm/action/libero_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/libero_pose_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/README.md create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/bridge_orig_lerobot_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/droid_lerobot_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_b.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_c_gripper_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/fractal_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/hand_pose_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/libero_native_frame_wise_relative_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka-dual_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/robomind-ur_backward_framewise_rot6d.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_anchored_normalizer.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_no_rot_normalizer.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_normalizer.json create mode 100644 cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_normalizer.json create mode 100644 cosmos_training/cosmos/data/vfm/action/pose_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/action/pose_utils_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/pusht_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/robomind_dataset_config.py create mode 100644 cosmos_training/cosmos/data/vfm/action/robomind_franka_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/robomind_ur_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/transforms.py create mode 100644 cosmos_training/cosmos/data/vfm/action/transforms_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/data_classes.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/data_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/imagecodecs_numcodecs.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/normalizer.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/pose_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi/transforms.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/umi_dataset.yaml create mode 100644 cosmos_training/cosmos/data/vfm/action/umi_dataset_multi_task.yaml create mode 100644 cosmos_training/cosmos/data/vfm/action/unified_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/action/unified_dataset_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/G1_omnipicker_calibrated.urdf create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/__init__.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/contracts_test.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/droid_franka_robotiq_2f85.xml create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ik_solver.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/robot_scene_model.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_action.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_renderer.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ur5e_robotiq_2f85.xml create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/urdf_loader.py create mode 100644 cosmos_training/cosmos/data/vfm/action/urdf_visualizer/viewer.py create mode 100644 cosmos_training/cosmos/data/vfm/action/viewpoint_utils.py create mode 100644 cosmos_training/cosmos/data/vfm/joint_dataloader.py create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/__init__.py create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/helper.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/sft_dataset.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/helper.py create mode 100644 cosmos_training/cosmos/data/vfm/local_datasets/sft_dataset.py create mode 100644 cosmos_training/cosmos/data/vfm/processors/__init__.py create mode 100644 cosmos_training/cosmos/data/vfm/processors/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/processors/__pycache__/nemotron3densevl_processor.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/processors/__pycache__/nemotronvl_processor.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/processors/__pycache__/qwen3vl_processor.cpython-312.pyc create mode 100644 cosmos_training/cosmos/data/vfm/processors/nemotron3densevl_processor.py create mode 100644 cosmos_training/cosmos/data/vfm/processors/nemotronvl_processor.py create mode 100644 cosmos_training/cosmos/data/vfm/processors/qwen3vl_processor.py create mode 100644 cosmos_training/cosmos/data/vfm/sequence_packing.py create mode 100644 cosmos_training/cosmos/data/vfm/utils.py create mode 100644 cosmos_training/cosmos/data/vlm/processors/qwen3vl_processor.py create mode 100644 cosmos_training/cosmos/launcher/__init__.py create mode 100644 cosmos_training/cosmos/model/__init__.py create mode 100644 cosmos_training/cosmos/model/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/__pycache__/_base.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/_base.py create mode 100644 cosmos_training/cosmos/model/attention/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/backends.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/checks.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/frontend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/masks.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/__pycache__/varlen.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/backends.py create mode 100644 cosmos_training/cosmos/model/attention/checks.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/checks.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/cudnn_forward.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/functions.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/meta.py create mode 100644 cosmos_training/cosmos/model/attention/cudnn/stubs.py create mode 100644 cosmos_training/cosmos/model/attention/flash2/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/flash2/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash2/__pycache__/checks.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash2/__pycache__/functions.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash2/__pycache__/meta.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash2/__pycache__/stubs.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash2/checks.py create mode 100644 cosmos_training/cosmos/model/attention/flash2/functions.py create mode 100644 cosmos_training/cosmos/model/attention/flash2/meta.py create mode 100644 cosmos_training/cosmos/model/attention/flash2/stubs.py create mode 100644 cosmos_training/cosmos/model/attention/flash3/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/flash3/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash3/__pycache__/checks.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash3/__pycache__/meta.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash3/__pycache__/stubs.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/flash3/checks.py create mode 100644 cosmos_training/cosmos/model/attention/flash3/functions.py create mode 100644 cosmos_training/cosmos/model/attention/flash3/meta.py create mode 100644 cosmos_training/cosmos/model/attention/flash3/stubs.py create mode 100644 cosmos_training/cosmos/model/attention/frontend.py create mode 100644 cosmos_training/cosmos/model/attention/masks.py create mode 100644 cosmos_training/cosmos/model/attention/natten/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/natten/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/natten/__pycache__/checks.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/natten/__pycache__/meta.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/natten/__pycache__/stubs.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/natten/checks.py create mode 100644 cosmos_training/cosmos/model/attention/natten/functions.py create mode 100644 cosmos_training/cosmos/model/attention/natten/meta.py create mode 100644 cosmos_training/cosmos/model/attention/natten/stubs.py create mode 100644 cosmos_training/cosmos/model/attention/utils/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/__pycache__/determinism.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/__pycache__/environment.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/__pycache__/version.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/backend_filter_test.py create mode 100644 cosmos_training/cosmos/model/attention/utils/determinism.py create mode 100644 cosmos_training/cosmos/model/attention/utils/determinism_test.py create mode 100644 cosmos_training/cosmos/model/attention/utils/environment.py create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/__init__.py create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/__pycache__/functools.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/__pycache__/log.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/functools.py create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/log.py create mode 100644 cosmos_training/cosmos/model/attention/utils/safe_ops/safe_lru_cache_test.py create mode 100644 cosmos_training/cosmos/model/attention/utils/version.py create mode 100644 cosmos_training/cosmos/model/attention/utils/version_test.py create mode 100644 cosmos_training/cosmos/model/attention/varlen.py create mode 100644 cosmos_training/cosmos/model/vfm/__pycache__/omni_mot_model.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/cross_entropy.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/flow_matching.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/load_balancing.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/time_weight.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/cross_entropy.py create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/flow_matching.py create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/load_balancing.py create mode 100644 cosmos_training/cosmos/model/vfm/algorithm/loss/time_weight.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/__pycache__/rectified_flow.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/rectified_flow.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/edm.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/fixed_step.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/fm_solvers_unipc.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/unipc.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/edm.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/fixed_step.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/fm_solvers_unipc.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/unipc.py create mode 100644 cosmos_training/cosmos/model/vfm/diffusion/samplers/utils.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/attention.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/context_parallel_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/cosmos3_vfm_network.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/domain_aware_linear.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/modeling_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_unified_mot.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_vfm_network.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/unified_3dmrope_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/__pycache__/unified_mot.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/mot/attention.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/context_parallel_utils.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/cosmos3_vfm_network.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/domain_aware_linear.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/modeling_utils.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/parallelize_unified_mot.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/parallelize_vfm_network.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/unified_3dmrope_utils.py create mode 100644 cosmos_training/cosmos/model/vfm/mot/unified_mot.py create mode 100644 cosmos_training/cosmos/model/vfm/omni_mot_model.py create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/flux_vae_8x8.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/interface.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/tokenization_qwen2.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt1_vae_4x8x8.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt2_vae_4x16x16.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/interface.py create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/tokenization_qwen2.py create mode 100644 cosmos_training/cosmos/model/vfm/tokenizers/wan2pt2_vae_4x16x16.py create mode 100644 cosmos_training/cosmos/model/vfm/utils/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/utils/__pycache__/data_and_condition.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/utils/__pycache__/memory.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/utils/__pycache__/safetensors_loader.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/utils/data_and_condition.py create mode 100644 cosmos_training/cosmos/model/vfm/utils/memory.py create mode 100644 cosmos_training/cosmos/model/vfm/utils/safetensors_loader.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/configuration_nemotron_3_dense_vl.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/nemotron_3_dense_vl.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/configuration_qwen3_vl.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/qwen3_vl.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/configuration_qwen3_vl.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/qwen3_vl.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/utils.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__init__.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/configuration_qwen3_vl_moe.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/moe.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/moe_kernels.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/qwen3_vl_moe.cpython-312.pyc create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe_kernels.py create mode 100644 cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/qwen3_vl_moe.py create mode 100644 cosmos_training/cosmos/tools/__init__.py create mode 100644 cosmos_training/cosmos/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/tools/flops/__init__.py create mode 100644 cosmos_training/cosmos/tools/flops/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/tools/flops/__pycache__/omni_mot.cpython-312.pyc create mode 100644 cosmos_training/cosmos/tools/flops/__pycache__/wan_vae.cpython-312.pyc create mode 100644 cosmos_training/cosmos/tools/flops/omni_mot.py create mode 100644 cosmos_training/cosmos/tools/flops/wan_vae.py create mode 100644 cosmos_training/cosmos/tools/visualize/__pycache__/video.cpython-312.pyc create mode 100644 cosmos_training/cosmos/tools/visualize/video.py create mode 100644 cosmos_training/cosmos/trainer/__init__.py create mode 100644 cosmos_training/cosmos/trainer/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__init__.py create mode 100644 cosmos_training/cosmos/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/callback.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/checkpoint_db.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/checkpointer.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/cluster_env.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/config.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/config_helper.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/context_managers.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/count_params.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/device.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/distributed.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/ema.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/flags.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/launch.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/log.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/misc.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/object_store.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/optim_instantiate.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/profiling.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/progress_bar.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/serialization.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/timer.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/validator.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/__pycache__/wandb_util.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/callback.py create mode 100644 cosmos_training/cosmos/utils/checkpoint_db.py create mode 100644 cosmos_training/cosmos/utils/checkpointer.py create mode 100644 cosmos_training/cosmos/utils/cluster_env.py create mode 100644 cosmos_training/cosmos/utils/config.py create mode 100644 cosmos_training/cosmos/utils/config_helper.py create mode 100644 cosmos_training/cosmos/utils/configs/__pycache__/lr_scheduler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/configs/lr_scheduler.py create mode 100644 cosmos_training/cosmos/utils/context_managers.py create mode 100644 cosmos_training/cosmos/utils/count_params.py create mode 100644 cosmos_training/cosmos/utils/device.py create mode 100644 cosmos_training/cosmos/utils/distributed.py create mode 100644 cosmos_training/cosmos/utils/easy_io/README.md create mode 100644 cosmos_training/cosmos/utils/easy_io/__init__.py create mode 100644 cosmos_training/cosmos/utils/easy_io/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/__pycache__/easy_io.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/__pycache__/file_client.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__init__.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/auto_auth.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/base_backend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/boto3_backend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/boto3_client.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/http_backend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/local_backend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/msc_backend.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/__pycache__/registry_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/auto_auth.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/base_backend.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/boto3_backend.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/boto3_client.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/http_backend.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/local_backend.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/msc_backend.py create mode 100644 cosmos_training/cosmos/utils/easy_io/backends/registry_utils.py create mode 100644 cosmos_training/cosmos/utils/easy_io/easy_io.py create mode 100644 cosmos_training/cosmos/utils/easy_io/easy_io_test.py create mode 100644 cosmos_training/cosmos/utils/easy_io/file_client.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__init__.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/base.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/byte_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/csv_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/gzip_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/imageio_video_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/json_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/jsonl_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/np_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pandas_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pickle_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pil_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/registry_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/tarfile_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torch_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torchjit_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/trimesh_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/txt_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/yaml_handler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/base.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/byte_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/csv_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/gzip_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/imageio_video_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/json_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/jsonl_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/np_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/pandas_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/pickle_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/pil_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/registry_utils.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/tarfile_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/torch_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/torchjit_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/trimesh_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/txt_handler.py create mode 100644 cosmos_training/cosmos/utils/easy_io/handlers/yaml_handler.py create mode 100644 cosmos_training/cosmos/utils/ema.py create mode 100644 cosmos_training/cosmos/utils/env_parsers/__pycache__/cred_env_parser.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/env_parsers/__pycache__/env_parser.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/env_parsers/cred_env_parser.py create mode 100644 cosmos_training/cosmos/utils/env_parsers/customization_env_parser.py create mode 100644 cosmos_training/cosmos/utils/env_parsers/env_parser.py create mode 100644 cosmos_training/cosmos/utils/env_parsers/inference_env_parser.py create mode 100644 cosmos_training/cosmos/utils/flags.py create mode 100644 cosmos_training/cosmos/utils/functional/__pycache__/batch_ops.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/functional/__pycache__/lr_scheduler.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/functional/__pycache__/multi_step.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/functional/__pycache__/runge_kutta.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/functional/batch_ops.py create mode 100644 cosmos_training/cosmos/utils/functional/lr_scheduler.py create mode 100644 cosmos_training/cosmos/utils/functional/multi_step.py create mode 100644 cosmos_training/cosmos/utils/functional/runge_kutta.py create mode 100644 cosmos_training/cosmos/utils/fused_adam.py create mode 100644 cosmos_training/cosmos/utils/launch.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/__init__.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/file_io.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/instantiate.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/lazy.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/lazy_call.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/omegaconf_patch.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/__pycache__/registry.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/lazy_config/file_io.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/instantiate.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/lazy.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/lazy_call.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/omegaconf_patch.py create mode 100644 cosmos_training/cosmos/utils/lazy_config/registry.py create mode 100644 cosmos_training/cosmos/utils/log.py create mode 100644 cosmos_training/cosmos/utils/misc.py create mode 100644 cosmos_training/cosmos/utils/object_store.py create mode 100644 cosmos_training/cosmos/utils/one_logger/README.md create mode 100644 cosmos_training/cosmos/utils/one_logger/__pycache__/one_logger_override_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/one_logger/one_logger_context_managers.py create mode 100644 cosmos_training/cosmos/utils/one_logger/one_logger_global_vars.py create mode 100644 cosmos_training/cosmos/utils/one_logger/one_logger_override_utils.py create mode 100644 cosmos_training/cosmos/utils/one_logger/one_logger_utils.py create mode 100644 cosmos_training/cosmos/utils/optim_instantiate.py create mode 100644 cosmos_training/cosmos/utils/profiling.py create mode 100644 cosmos_training/cosmos/utils/progress_bar.py create mode 100644 cosmos_training/cosmos/utils/serialization.py create mode 100644 cosmos_training/cosmos/utils/timer.py create mode 100644 cosmos_training/cosmos/utils/training_telemetry/README.md create mode 100644 cosmos_training/cosmos/utils/training_telemetry/__init__.py create mode 100644 cosmos_training/cosmos/utils/training_telemetry/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/training_telemetry/__pycache__/telemetry.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/training_telemetry/__pycache__/utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/training_telemetry/callback.py create mode 100644 cosmos_training/cosmos/utils/training_telemetry/context_managers.py create mode 100644 cosmos_training/cosmos/utils/training_telemetry/telemetry.py create mode 100644 cosmos_training/cosmos/utils/training_telemetry/utils.py create mode 100644 cosmos_training/cosmos/utils/validator.py create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/data_utils.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/dtensor_helper.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/model_weights_stats.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/optimizer.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/parallelism.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/__pycache__/rand_state.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/data_utils.py create mode 100644 cosmos_training/cosmos/utils/vfm/dtensor_helper.py create mode 100644 cosmos_training/cosmos/utils/vfm/flash_attn.py create mode 100644 cosmos_training/cosmos/utils/vfm/fused_adam.py create mode 100644 cosmos_training/cosmos/utils/vfm/model_weights_stats.py create mode 100644 cosmos_training/cosmos/utils/vfm/monkey_patch.py create mode 100644 cosmos_training/cosmos/utils/vfm/optimizer.py create mode 100644 cosmos_training/cosmos/utils/vfm/parallelism.py create mode 100644 cosmos_training/cosmos/utils/vfm/rand_state.py create mode 100644 cosmos_training/cosmos/utils/vfm/vlm/__init__.py create mode 100644 cosmos_training/cosmos/utils/vfm/vlm/__pycache__/__init__.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/vlm/__pycache__/pretrained_models_downloader.cpython-312.pyc create mode 100644 cosmos_training/cosmos/utils/vfm/vlm/optimizer.py create mode 100644 cosmos_training/cosmos/utils/vfm/vlm/pretrained_models_downloader.py create mode 100644 cosmos_training/cosmos/utils/vlm/__init__.py create mode 100644 cosmos_training/cosmos/utils/vlm/compute_flops_qwen3vl.py create mode 100644 cosmos_training/cosmos/utils/vlm/configs_defaults/checkpointer.py create mode 100644 cosmos_training/cosmos/utils/vlm/constant.py create mode 100644 cosmos_training/cosmos/utils/vlm/create_position_ids.py create mode 100644 cosmos_training/cosmos/utils/vlm/dcp_checkpointer.py create mode 100644 cosmos_training/cosmos/utils/vlm/distributed.py create mode 100644 cosmos_training/cosmos/utils/vlm/flop_calculator.py create mode 100644 cosmos_training/cosmos/utils/vlm/fused_adam.py create mode 100644 cosmos_training/cosmos/utils/vlm/model_wrapper.py create mode 100644 cosmos_training/cosmos/utils/vlm/optimizer.py create mode 100644 cosmos_training/cosmos/utils/vlm/planner.py create mode 100644 cosmos_training/cosmos/utils/vlm/pretrained_models_downloader.py create mode 100644 cosmos_training/cosmos/utils/vlm/video_preprocess.py create mode 100644 cosmos_training/cosmos/utils/wandb_util.py create mode 100644 cosmos_training/experiments/pretraining/image_only_256p/exp001_text2image_qwen2b.py create mode 100644 cosmos_training/experiments/sft/__pycache__/mixed_modality_sft_8b.cpython-312.pyc create mode 100644 cosmos_training/experiments/sft/mixed_modality_sft_8b.py create mode 100755 cosmos_training/launch_mixed_modality_sft_8b.sh create mode 100755 cosmos_training/run_mixed_modality_sft_trace.sh create mode 100644 cosmos_training/scripts/__pycache__/train.cpython-312.pyc create mode 100644 cosmos_training/scripts/train.py diff --git a/cosmos_training/configs/base/__init__.py b/cosmos_training/configs/base/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/configs/base/__pycache__/__init__.cpython-312.pyc b/cosmos_training/configs/base/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b42594497ee0e25106cba923f4b4d6cce04be644 GIT binary patch literal 231 zcmZ8b!41MN3`{7M5K;%>gBL)IzzR_bX_czPk(~&20ajoJW?>8_K;_jJi9~r|>Fo2# zck-jE)*@AWmtCIE-2Q7^Cgmmuv5|+npheood^264j~vKA!x!*Kp~2oFgGN55;R!pV z!@R3w%|->Pt+TF$$c7k(+O-+sYf|WqV~nKDto$4rU1M8@oC8Xwn4r|w57TOQ##8Lg Rp33x=OyEpjg_M$t#6CI-MOFX+ literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/__pycache__/config.cpython-312.pyc b/cosmos_training/configs/base/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04198159aa7e397ee9a1f62dc04253707eaa75d1 GIT binary patch literal 5536 zcma(V+in}jb(Y-abxGatlC^RyTZ|+sH?)c3x^eDlV&&q*ZHT7R6=z7Tyvt=~mzL#< zr~)Xk{ZLdWP{i#^-J(DnI6(U!?StGXFujP70BIibra%r72DFh~tJ;wV8``)5Mld zY9@7XiP;ve>cxwCnF34toK2|7mov)G0opwImJsqt-Jbp>bgdyeC2b0=^E2r!>(v0n`s1w^Qn{u?X2rEW3lE^ zXI|i(_Vx;`U`xM@FByc|*tIVLZqiEGU7FX<>6ZJG&&w+%S; z96y9Jx2jjl{Lc+0VY^Bzj$LC#8d91wd{cAg_&PAi4JKb*Wh#gwx)A`JUg+LMU&=8h zvD=BJX13(J;}SlW0P=xGl@FxQM+-49y*P<*(>scvL^sgB z83~%CsUhHpc`~0z;@40~BFctZNRv2;f%mD`&}FHRIgfG)FXr1Xct*UW7hRjJdMYkR!F(WmV3T<%7%N_^+S~{u&cj}^2!Uh~ zLWh%+Jq7H*D0*tJq%ck|y=*UnWmgQiC@)o}5QhALD7AD?soE>BQBt_AkfLivuRK(B zAM(9ikUGHpDr6raUgTyD^D^aP6&pIi<;8~{c)&|tGA$66+aXAHvFc)XwMxAxDZ=sT z)vCdJbJ7Q5q}L#cHn90pWJOM1}UW zoV1XOLg25#m3axU69Wu}dgGWFPE~h{OFULl zGq`IUGC*dMx%GKTjEko3KxA@wrqtFMzhm||bj|AxC;bqJ&Mx{CAhA zR73DUqkZ@V2IM8mmy_YSFgARXlE)nqdB(5?>{d#3l`A~ zN7I{`@M)vy`u44@FuhuZz%TZPtWhhvH3kxJ|F?7v+;j=Dn!BX?66%nc9ZjgUl_2*m z=hPU4LkbDipe6wcYpw<<3euvxWD`dS=h_-!#w9AYlG^_&nSMgKP|N#x221wIpC% z`NCRr5%vQ68pD$}CjT(~yXh|@ zon5JW(e9Dh`t#Sw=EK*GJ=CqlzM6_Cb6+RY%9LLNh?V)>6ydMg;s9|O_x=ekz2C*5 z%n$xS5)T4d9i9N-=7%^xZqi8f2oAoBXhc4OqZvM0p#V5}opYdJF12VxIh*zqk z&0s@fhtI;z5JcifaHPXUN#Y2Oro2bVhMOUc@FdaX4_8JAb6XRQuYtMI`B;*q%ucJT zF2S=ZNz!Jw)l-jHz4fTTRkP3Puge0?m;=^eoj=i%Bx?>?!}XYeJIoPlv>vy{>Is2& zn#Zj1deYMB{MnWyUFL*!yq*?tw|Sp+q7F~E2wID^5*J{RHZ0nP#oMq%8z!}3sWvRx zhAC~B+=ivwFtvq&{K%ZFXVwr@X*~-$#Sc6aaCz)L=)6u|k(ss*&kMgI_p^INvqfu`pTIfToO`eYZ1Fcs$F{wp5(Hdp&gZWf>_200>tdbUNfFY#C{(FlJa`{@ zN}1WUsweXYm=_No&t6>A&QK7^qN?nHl&P1?P$y#oIwTYdCqZ!yJ6>(Bb=n{@G9X_{`zshuZ^tlTVp!Jr$h1Q*!f`ahVcc@N~!E_Ube5JhO4G(LEG^FE+X! z3&0=!yKfZ07v6bcW1%tgpby@WCvM9V+j6ckr1_L>xxXT*7?Yd<-w!-(QWzIUAc2(;jY{Z5(VIiZTaM07AX%l((3y2eqGe##lShi+x+rI6gp3H zohEJm8yU9+zXVkMXqvCNhUxePU^XBHZq9frXFubUhxaoFWi?)+CcKUq|2^U;cN4}R z>W=4lF9r_Fe~II%RYV@4(*J&o!c2I9pJ8jj$7?WW1-jj+BuQUuNE-VLsXJ)=b2Pq# zzQ2Q>-9huaF%-$J_gx)*XLR$KZE5)HNK%^FMbPa None: + super().validate() + self._dispatch_model_config_validate() + + def _dispatch_model_config_validate(self) -> None: + """Run model-family validation on the composed model.config. + + validate() runs before instantiate(), so self.model.config is a + DictConfig wrapping the structured schema rather than the attrs class. + DictConfig surfaces fields but not methods, so to drive the typed + isinstance dispatch the schema must first be materialized via + OmegaConf.to_object. + """ + materialized = OmegaConf.to_object(self.model.config) + if isinstance(materialized, ModelConfig): + materialized.validate(self) + + +def make_config() -> Config: + c = Config( + model=None, + optimizer=None, + scheduler=None, + dataloader_train=None, + dataloader_val=None, + ) + + # Specifying values through instances of attrs + c.job.project = "cosmos3_vfm" + c.job.group = "debug" + c.job.name = "delete_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + c.trainer.type = Trainer + c.trainer.straggler_detection.enabled = False + c.trainer.max_iter = 400_000 + c.trainer.logging_iter = 20 + c.trainer.validation_iter = 100 + c.trainer.run_validation = False + c.trainer.callbacks = None + + c.upload_reproducible_setup = False + + from configs.base.defaults.callbacks import register_callbacks + from configs.base.defaults.checkpointer import register_checkpoint, register_ckpt_type + from configs.base.defaults.cluster import register_cluster + from configs.base.defaults.ema import register_ema + + # from configs.base.defaults.data import register_data + from configs.base.defaults.model import register_model + from configs.base.defaults.optimizer import register_optimizer, register_scheduler + from configs.base.defaults.tokenizer import register_sound_tokenizer, register_tokenizer + from configs.base.defaults.vlm import register_vlm + from configs.base.defaults.open_source_dataloader import register_open_source_dataloaders + + # Call this function to register config groups for advanced overriding. the order follows the default config groups + # register_data() + register_model() + register_checkpoint() + register_ckpt_type() + register_optimizer() + register_scheduler() + register_callbacks() + register_tokenizer() + register_sound_tokenizer() + register_ema() + register_cluster() + register_vlm() + register_open_source_dataloaders() + + # Register only the mixed_modality_sft_8b experiment. + # Importing the module triggers its top-level cs.store(...) call. + # To register more experiments later, add explicit imports here + # (or switch to import_all_modules_from_package("experiments", reload=True) + # for auto-discovery of every .py under experiments/). + import experiments.sft.mixed_modality_sft_8b # noqa: F401 + return c diff --git a/cosmos_training/configs/base/defaults/__init__.py b/cosmos_training/configs/base/defaults/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/configs/base/defaults/__pycache__/__init__.cpython-312.pyc b/cosmos_training/configs/base/defaults/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdbe1d3374b53b1f9d3d9a2097d32292c1c0a6d3 GIT binary patch literal 240 zcmZ8b!41MN3~VTs5K;%>Ll!`czzR`l(<)UOS9T)QDR?pivoHn|pz`VqN1{BibiO;w zKKW6W+l(u|i!Sjqwf~ydJh^3KwiBm&hAVbc+L^D?MG9zSLl-#!h6bDkk`++TG$3@Q z3X4}~O-2RO)?44oNCruc+P4Yed!%46o*-gnmdWp;0^it{WXh4XX>@cPt`z45rS`)( XZw@`w(U~I_{GVV@60ckcfjO%W0h37P literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/defaults/__pycache__/callbacks.cpython-312.pyc b/cosmos_training/configs/base/defaults/__pycache__/callbacks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6254248aa148c491107f71e959b8d51db7d2abfa GIT binary patch literal 5739 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/defaults/__pycache__/cluster.cpython-312.pyc b/cosmos_training/configs/base/defaults/__pycache__/cluster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfaa7421aecb3dc28804aad382041e5808f01041 GIT binary patch literal 1894 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 B$fy7S literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/defaults/__pycache__/optimizer.cpython-312.pyc b/cosmos_training/configs/base/defaults/__pycache__/optimizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33be7b4a04c6e20ec0f67a58d7392e5b361a2a8c GIT binary patch literal 2593 zcmcgt&2Jk;6rXjx{*0XuJ6}yp+=!OAZL&$)rXNC$N=S)NR9Zkn@?|yN8E-b-^;)y* z1VgrJ8|MO<1C2M!#l5>;F{^dH~?m5PeQYE(p8grEXMLQ;Wx;?3I5N7WCwF!KE7 z&G^lGvv1z-%~y#;3_&Ze%hT5r2>mD~_2&NrleG^3E+K+)h)5)0N>(5jkc8X-azifY z%Ry7NLb;GH%VyY$`CWjCWu;i68R*j@2WS&7HbOr9JYpLl8V`SF)w8kxj%l`(9Z;;%8^>?^L# zXg0uG^JT*%s>xJGo1&y_QpTIAYa5>Q{;{3E-Wl03s(tBoZ+o*o^h1+xK ztAC8@$L9y{T&#?rc=c2^!UHDbjkDCyr(Bg#4Oh6Fr!IDQ11&jRE|eWgc=8;rII3%_ z4mIKXc@hp`!8aNu)1b`Zy~J>E-lXbQLu$ch>JU*&m|@Ggdn1g{&((|E5LQO-e)6t_ zvLT+B#mrL6B^9pB_p4e)5!|a5ew~L4u#B0@6Iw+BFD5PO?v%q@XDG|tj^S2RqX<_$ zgH5gOL5y+e4ba#A2DpS=2?oDS2kO%1xL6b4L~o#Jd2_ukLAFEg2apTjQEhH5dl6c! zStb#;&KX1+Nbuu;+gR8BRJtfF$&112TzZ_B(0}j}e!Q0o%0^G~=thF@n17>~U~85~ zPl?+-2{NRYY}tgGJh`E&x|I^W66CVZ>~e{ROISOHb;^T9YzYK)E>{i7!p3V?P&Uj& z1yw;g)n(Wy8bw`!?_rKo0g8$4);6hji54BZ%rshG+1T-9>he}bbwiXA zuv23+Q{b#ep*t4=&ZAq=^mox+-$ZxajP{&=&I^X#n|OP|OLlq9-Cm;0OZ3d2oqy|Q zqGtsKld)T^yS(nbt1^h|A=K2f8U|@4isZyPiprstG?K$x_opAWzxQGL?}`0ua3Zvg z2Ticx^B@T!{y%uIodrh&`SWTl~dIBlt)_7q5x%=f5gT z&Goltt4Cl*3$SCeo;I4;$`foFMT(&=PWrP+e`fpwAv8r8QRb(e_-Xz9-@o|euP;A8 z!CGMnGQS%J-m}z-t!%{i z!w!puM_TR@njxQytWRiW1?&?b-cp~2>;Onr&anXyx1{+-JeWxFGeYk(fG4CmWP0!X z*>9V6-%Rx0YTxZWb-?THxxClw&feIu&ui;mnDA2V3wwN6Z;EWBd;(HF^FYc!($?khXPbCfRf(;sD(|cokAbpln9hI+Ppn$yeKiuI zCi~IhJGLoSb<`!Fv%T4SkP>;r9c2@ph_XIUHYZNiWBf z3&9Kfmm`hKvE*{9Z6$=#?EroK>w8k^zzx*Ax({`9d#R3vy6) XN9TGX+yRQIKhteeXl*bfRpa;%C(f?J literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/defaults/__pycache__/parallelism.cpython-312.pyc b/cosmos_training/configs/base/defaults/__pycache__/parallelism.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75b7f44e5d3f451c282985cd7e3bef32c97b62ff GIT binary patch literal 1945 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 "Video2WorldCondition": + """ + Sets the video conditioning frames for video-to-video generation. + + This method creates a conditioning mask for the input video frames that determines + which frames will be used as context frames for generating new frames. The method + handles both image batches (T=1) and video batches (T>1) differently. + + Args: + gt_frames: A tensor of ground truth frames with shape [B, C, T, H, W], where: + B = batch size + C = number of channels + T = number of frames + H = height + W = width + + random_min_num_conditional_frames: Minimum number of frames to use for conditioning + when randomly selecting a number of conditioning frames. + + random_max_num_conditional_frames: Maximum number of frames to use for conditioning + when randomly selecting a number of conditioning frames. + + num_conditional_frames: Optional; If provided, all examples in the batch will use + exactly this many frames for conditioning. If None, a random number of frames + between random_min_num_conditional_frames and random_max_num_conditional_frames + will be selected for each example in the batch. + + conditional_frames_probs: Optional; Dictionary mapping number of frames to probabilities. + If provided, overrides the random_min/max_num_conditional_frames with weighted sampling. + Example: {0: 0.5, 1: 0.25, 2: 0.25} for 50% chance of 0 frames, 25% for 1, 25% for 2. + + Returns: + A new Video2WorldCondition object with the gt_frames and conditioning mask set. + The conditioning mask (condition_video_input_mask) is a binary tensor + of shape [B, 1, T, H, W] where 1 indicates frames used for conditioning and 0 + indicates frames to be generated. + + Notes: + - For image batches (T=1), no conditioning frames are used (num_conditional_frames_B = 0). + - For video batches: + - If num_conditional_frames is provided, all examples use that fixed number of frames. + - Otherwise, each example randomly uses between random_min_num_conditional_frames and + random_max_num_conditional_frames frames. + - The mask marks the first N frames as conditioning frames (set to 1) for each example. + """ + kwargs = self.to_dict(skip_underscore=False) + kwargs["gt_frames"] = gt_frames + + # condition_video_input_mask + B, _, T, H, W = gt_frames.shape + condition_video_input_mask = torch.zeros(B, 1, T, H, W, dtype=gt_frames.dtype, device=gt_frames.device) + if T == 1: # handle image batch + num_conditional_frames_B = torch.zeros(B, dtype=torch.int32) + else: # handle video batch + if num_conditional_frames is not None: + num_conditional_frames_B = torch.ones(B, dtype=torch.int32) * num_conditional_frames + elif conditional_frames_probs is not None: + # Use weighted sampling based on provided probabilities + frames_options = list(conditional_frames_probs.keys()) + weights = list(conditional_frames_probs.values()) + num_conditional_frames_B = torch.tensor( + random.choices(frames_options, weights=weights, k=B), dtype=torch.int32 + ) + else: + num_conditional_frames_B = torch.randint( + random_min_num_conditional_frames, random_max_num_conditional_frames + 1, size=(B,) + ) + for idx in range(B): + condition_video_input_mask[idx, :, : num_conditional_frames_B[idx], :, :] += 1 + + kwargs["condition_video_input_mask"] = condition_video_input_mask + return type(self)(**kwargs) + + def edit_for_inference( + self, is_cfg_conditional: bool = True, num_conditional_frames: int = 1 + ) -> "Video2WorldCondition": + _condition = self.set_video_condition( + gt_frames=self.gt_frames, + random_min_num_conditional_frames=0, + random_max_num_conditional_frames=0, + num_conditional_frames=num_conditional_frames, + ) + if not is_cfg_conditional: + # Do not use classifier free guidance on conditional frames. + # YB found that it leads to worse results. + _condition.use_video_condition.fill_(True) + return _condition + + def broadcast(self, process_group: torch.distributed.ProcessGroup) -> "Video2WorldCondition": + if self.is_broadcasted: + return self + # extra efforts + gt_frames = self.gt_frames + condition_video_input_mask = self.condition_video_input_mask + kwargs = self.to_dict(skip_underscore=False) + kwargs["gt_frames"] = None + kwargs["condition_video_input_mask"] = None + new_condition = Text2WorldCondition.broadcast( + type(self)(**kwargs), + process_group, + ) + + kwargs = new_condition.to_dict(skip_underscore=False) + _, _, T, _, _ = gt_frames.shape + if process_group is not None: + if T > 1 and process_group.size() > 1: + gt_frames = broadcast_split_tensor(gt_frames, seq_dim=2, process_group=process_group) + condition_video_input_mask = broadcast_split_tensor( + condition_video_input_mask, seq_dim=2, process_group=process_group + ) + kwargs["gt_frames"] = gt_frames + kwargs["condition_video_input_mask"] = condition_video_input_mask + return type(self)(**kwargs) + + +class Video2WorldConditioner(GeneralConditioner): + def forward( + self, + batch: Dict, + override_dropout_rate: Optional[Dict[str, float]] = None, + ) -> Video2WorldCondition: + output = super()._forward(batch, override_dropout_rate) + return Video2WorldCondition(**output) + + +_SHARED_CONFIG = dict( + fps=L(ReMapkey)( + input_key="fps", + output_key="fps", + dropout_rate=0.0, + dtype=None, + ), + use_video_condition=L(BooleanFlag)( + input_key="fps", + output_key="use_video_condition", + dropout_rate=0.2, + ), +) + +VideoPredictionConditioner: LazyDict = L(Video2WorldConditioner)( + **_SHARED_CONFIG, +) + + +def register_conditioner(): + cs = ConfigStore.instance() + cs.store( + group="conditioner", + package="model.config.conditioner", + name="video_prediction_conditioner", + node=VideoPredictionConditioner, + ) diff --git a/cosmos_training/configs/base/defaults/ema.py b/cosmos_training/configs/base/defaults/ema.py new file mode 100644 index 00000000..2ea0b65d --- /dev/null +++ b/cosmos_training/configs/base/defaults/ema.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import attrs +from hydra.core.config_store import ConfigStore + + +@attrs.define(slots=False) +class EMAConfig: + """ + Config for the EMA. + """ + + enabled: bool = True + rate: float = 0.1 + iteration_shift: int = 0 + + +PowerEMAConfig: EMAConfig = EMAConfig( + enabled=True, + rate=0.10, + iteration_shift=0, +) + + +def register_ema(): + cs = ConfigStore.instance() + cs.store(group="ema", package="model.config.ema", name="power", node=PowerEMAConfig) diff --git a/cosmos_training/configs/base/defaults/model.py b/cosmos_training/configs/base/defaults/model.py new file mode 100644 index 00000000..3264c92b --- /dev/null +++ b/cosmos_training/configs/base/defaults/model.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyCall as L +from configs.base.defaults.model_config import ( + OmniMoTModelConfig, + ParallelismConfig, +) +from cosmos.model.vfm.omni_mot_model import OmniMoTModel + +MOT_DDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="ddp", + ), + model=L(OmniMoTModel)( + config=OmniMoTModelConfig(), + _recursive_=False, + ), +) + + +MOT_FSDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="fsdp", + ), + model=L(OmniMoTModel)( + config=OmniMoTModelConfig( + parallelism=ParallelismConfig( + data_parallel_shard_degree=8, + ), + ), + _recursive_=False, + ), +) + + +def register_model(): + cs = ConfigStore.instance() + cs.store(group="model", package="_global_", name="mot_ddp", node=MOT_DDP_CONFIG) + cs.store(group="model", package="_global_", name="mot_fsdp", node=MOT_FSDP_CONFIG) diff --git a/cosmos_training/configs/base/defaults/model_config.py b/cosmos_training/configs/base/defaults/model_config.py new file mode 100644 index 00000000..3b4d5cb1 --- /dev/null +++ b/cosmos_training/configs/base/defaults/model_config.py @@ -0,0 +1,360 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Any + +import attrs + +from cosmos.utils.config import Config +from cosmos.utils.lazy_config import LazyDict +from configs.base.defaults.ema import EMAConfig +from configs.base.defaults.parallelism import ParallelismConfig +from configs.base.defaults.vlm import VLMConfig +from configs.base.vlm.defaults.training import PolicyConfig, TrainConfig + + +@attrs.define(slots=False) +class ModelConfig: + """Typed base for project model configs. + + Subclasses override validate to add family-specific checks. The receiver is + a fresh attrs copy from OmegaConf.to_object, so mutations to self are + discarded; write through root_config for any propagating side effects. + + The ema field is required (disabled by default) so trainer/callback reads + stay as model.config.ema.enabled across every family; subclasses that opt in + (e.g. OmniMoTModelConfig) override with their own EMAConfig. + """ + + ema: EMAConfig = EMAConfig(enabled=False) + + def validate(self, root_config: Config) -> None: + return + + +@attrs.define(slots=False) +class DiffusionExpertConfig: + # This determines the range of timesteps before the fourier feature embedding is applied. + timestep_range: float = 1.0 + # Whether to load the generation pathway weights from pretrained LLM/VLM weights. + load_weights_from_pretrained: bool = True + + patch_spatial: int = 2 + max_vae_latent_side_after_patchify: int = ( + 20 # Max dimension (h or w) of the VAE latent after patchification (320/(8*2)) + ) + # Position embedding type for vision tokens: + # - "3d_rope": Additive 3D RoPE embeddings (VideoRopePosition3DEmb) + 1D position IDs for attention + # - "flattened_sin_cos": Additive flattened sin/cos embeddings + 1D position IDs for attention + # - "unified_3d_mrope": No additive embedding + 3D position IDs for Qwen3VL-style mRoPE attention + position_embedding_type: str = "3d_rope" + # When finetuning from lower resolution to higher resolution, the spatial resolution of videos increase. + # So, we need to adjust the position embedding. + # We use NTK based RoPE extrapolation to adjust the position embedding. + # Reference: (https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/) + # Design adapted from Cosmos2.5 (https://arxiv.org/pdf/2511.00062) + # extrapolation_ratio here is how the base of the RoPE is scaled + # b' = b * extrapolation_ratio^(dim / (dim - 2)) + rope_h_extrapolation_ratio: float = 1.0 + rope_w_extrapolation_ratio: float = 1.0 + rope_t_extrapolation_ratio: float = 1.0 + enable_fps_modulation: bool = False + base_fps: int = 24 + # For unified_3d_mrope: whether spatial (H, W) indices reset to 0 for each vision segment + unified_3d_mrope_reset_spatial_ids: bool = True + # Setting the temporal gap on the boundary of the different modalities, default is 0, using a value greater than 0 will add an additional offset on the accumulated temporal offset. + unified_3d_mrope_temporal_modality_margin: int = 0 + + +@attrs.define(slots=False) +class LBLConfig: + # For load balancing loss computation. + # - "local": Use the fraction of tokens routed to each expert only for the local rank. + # - "global": Use the fraction of tokens routed to each expert across all ranks. + method: str = "local" + + # Coefficients for the load balancing loss. + # - "und": Coefficient for the load balancing loss for the "und" pathway. + # - "gen": Coefficient for the load balancing loss for the "gen" pathway. + coeff_und: float | None = None + coeff_gen: float | None = None + + +@attrs.define(slots=False) +class RectifiedFlowTrainingConfig: + shift: Any = 5 # Training time shift. If dict, maps resolution (str) to shift value (int) + use_dynamic_shift: bool = False # Whether to use dynamic shifting + train_time_image_distribution: str = "logitnormal" # Training time distribution for images + train_time_video_distribution: str = "logitnormal" # Training time distribution for videos + train_time_action_distribution: str = "logitnormal" # Training time distribution for actions + train_time_sound_distribution: str = "logitnormal" # Training time distribution for sound + train_time_weight: str = "uniform" # Training time weight + loss_scale: float = 1.0 # Loss scale + image_loss_scale: float | None = None # If set, overrides loss_scale for images + sound_loss_scale: float | None = None # If set, overrides loss_scale for sound + use_high_sigma_strategy: bool = False # Whether to use high sigma strategy + high_sigma_ratio: float = 0.05 # Ratio of using high sigmas + high_sigma_timesteps_min: int = 995 # Minimum timestep for high sigma + high_sigma_timesteps_max: int = 1000 # Maximum timestep for high sigma + use_discrete_rf: bool = False # Whether to use discrete formulation of rectified flow + + # user: please adjust this value according to loss_scale to balance the action loss with the video loss. + # default is 10.0 to align with previous training settings. + action_loss_weight: float = 10.0 + + # Independent noise schedule for action. When False (default), action shares the sigma + # sampled from the vision RF on every step — legacy behavior. When True, action samples + # its own sigma from `rectified_flow_action` using `shift_action` and + # `use_high_sigma_strategy_action`. Action always uses a shared scalar sigma per sample + # ([B,1]), independent of vision's DF mode. If action opts in to the high-sigma strategy, + # it reuses the global ratio / min / max. + independent_action_schedule: bool = False + shift_action: int | None = None # must be int; None → inherit `shift` (which must also be int) + use_high_sigma_strategy_action: bool = False + + # Independent noise schedule for sound. When False (default), sound shares the vision + # sigma schedule, reindexed to the dense audio-bearing subset. When True, sound samples + # its own scalar sigma per sample ([B,1]) from `rectified_flow_sound` using `shift_sound` + # and `use_high_sigma_strategy_sound`. + independent_sound_schedule: bool = False + shift_sound: int | None = None # must be int; None → inherit `shift` (which must also be int) + use_high_sigma_strategy_sound: bool = False + + # When True, per-instance flow-matching loss is normalized by the count of + # active (noisy) elements rather than all elements — preserves sum/active_count + # semantics so conditioning-heavy samples (e.g. I2V, forward_dynamics, diffusion + # forcing, AR rollout teacher-forcing) contribute gradient on par with K=0 + # samples. With .mean() the gradient of a K-conditioned sample is scaled by + # (T-K)/T, which undertrains the attend-to-clean-history dynamics. Kept + # False by default to preserve legacy loss magnitudes; enable for AR/DF training. + normalize_loss_by_active: bool = False + + +@attrs.define(slots=False) +class RectifiedFlowInferenceConfig: + scheduler_type: str = "unipc" # Scheduler type + num_train_timesteps: int = 1000 + shift: int = 1 + use_dynamic_shifting: bool = False + + +@attrs.define(slots=False) +class FixedStepSamplerConfig: + """Config for the fixed-step sampler used by distilled models. + + Uses a fixed sigma schedule instead of a smooth multi-step solver. + + Mirrors the constructor args of ``FixedStepSampler``. + """ + + # Discrete noise-level schedule (descending, excluding the final 0.0 step). + # Convention: exclude the final 0.0 step — FixedStepSampler appends it automatically. + # Values must be descending. Using 0.999 instead of 1.0 avoids numeric edge cases at sigma=1. + t_list: list[float] = [0.999, 0.75, 0.5, 0.25] + # Integrator type: "ode" (deterministic Euler) or "sde" (stochastic re-noising at each step). + sample_type: str = "ode" + + +# Don't have any defaults and init only in config file. +@attrs.define(slots=False) +class OmniMoTModelConfig(ModelConfig): + """ + Config for Omni MoT model. + """ + + tokenizer: LazyDict = None + net: LazyDict = None + ema: EMAConfig = EMAConfig() + parallelism: ParallelismConfig = ParallelismConfig() + + # LoRA (parameter-efficient fine-tuning). When `lora_enabled=True`, + # `OmniMoTModel.build_net` injects custom LoRA adapters BEFORE FSDP wrap on + # the meta-device network, then re-initializes lora_A/lora_B after + # to_empty + init_weights. Pair with `optimizer.keys_to_select=["lora_"]` + # and `checkpoint.keys_to_skip_loading=[..., "lora_"]`. + lora_enabled: bool = False + lora_rank: int = 16 + lora_alpha: int = 32 + lora_target_modules: str = "q_proj_moe_gen,k_proj_moe_gen,v_proj_moe_gen,o_proj_moe_gen" + + # Rectified flow configs + rectified_flow_training_config: RectifiedFlowTrainingConfig = RectifiedFlowTrainingConfig() + rectified_flow_inference_config: RectifiedFlowInferenceConfig = RectifiedFlowInferenceConfig() + + # Optional fixed-step sampler for distilled models (None for base models). + fixed_step_sampler_config: FixedStepSamplerConfig | None = None + + # Model configs + vlm_config: VLMConfig = VLMConfig() + diffusion_expert_config: DiffusionExpertConfig = DiffusionExpertConfig() + # Training data keys + input_video_key: str = "video" + input_image_key: str = "images" # key to fetch input image from data_batch + input_caption_key: str = "ai_caption" # Key used to fetch input captions + + # State and sequence shapes + state_ch: int = 16 # for latent model, ref to the latent channel number + state_t: int = 8 # for latent model, ref to the latent number of frames + latent_downsample_factor: int = 8 + resolution: str = "512" + max_num_tokens_after_packing: int = 13312 # Final num tokens after sequence packing + + # Attention implementation for joint understanding + generation + # Note "two_way" and "three_way" disallow and remove "End-of-Vision" or other text token in the generation tower. + # "three_way" must only be used when introducing sparsity + joint_attn_implementation: str = ( + "two_way" # "two_way", "three_way" or "flex" (NOTICE: We are planning to remove "flex" soon) + ) + + # Per-layer NATTEN parameters + # Must use "three_way" attention if used. + # If None, all attention layers remain dense. + # If not None, must be a list exactly the size of number of layers, and each layer can be either + # None (dense) or a dictionary, with at least 'kernel_size' or 'kernel_size_float' keys + # specifying sparsity. NATTEN parameters 'dilation' and 'stride' may also be specified either as + # static integers, or as floating point values that will be mapped to their domain during + # runtime. Integer parameters should never be mixed with floating point ones. + # + # Floating point parameters are highly recommended, unless the use case will have a fixed token + # layout (input resolution). + # + # Examples: + # Interleaved sliding window layers, "GPT-OSS"-style, with static window size: + # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size": (8, 8)}] + # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices + # will use a static sliding window size of 8x8. + # + # Interleaved sliding window layers, "GPT-OSS"-style, with input-dependent window size: + # natten_parameter_list = [None if layer_idx % 2 != 0 else {"kernel_size_float": (0.5, 0.5)}] + # Layers with odd indices ("None"s) will use dense attention, and layers with an even indices + # will use a dynamic window size that is 50% of the input along each of the two dimensions. + # + # Interleaved sliding window and dilated layers, "DiNAT"-style: + # natten_parameter_list = [ + # { + # "kernel_size_float": (0.5, 0.5), + # "dilation_float": (1.0, 1.0), + # } if layer_idx % 2 != 0 else { + # "kernel_size_float": (0.5, 0.5), + # } + # ] + # All layers will use a dynamic window size that is 50% of the input along each of the two + # dimensions. Layers with odd indices will also dilate to the maximum level possible. + # + natten_parameter_list: list | None = None + + # Temporal causality for training autoregressive video generation models. + # When enabled, applies temporal causal attention to generation supertokens. + # Each supertoken is num_action_tokens_per_supertoken action tokens followed + # by H*W vision tokens; the value is stamped onto the packed sequence by the + # temporal-causal packer and read by attention/KV-cache code unchanged. + # Only supports image2video modes (with or without actions). + # Requires joint_attn_implementation="three_way". + video_temporal_causal: bool = False + # "none": standard joint denoising (shared σ, no clean context) + # "teacher_forcing": all frames noised with shared σ; clean history via cross-attention + # "diffusion_forcing": each latent frame gets independent σ ~ Uniform[0,1] + # "teacher_forcing_dcm": replayed teacher-forcing discrete-time consistency distillation + causal_training_strategy: str = attrs.field( + default="none", + validator=attrs.validators.in_({"none", "teacher_forcing", "diffusion_forcing", "teacher_forcing_dcm"}), + ) + + # Load balancing loss config. + lbl: LBLConfig = LBLConfig() + + # vision configs + vision_gen: bool = True # whether to use vision related parameters and condition/generate vision tokens + + # action configs + action_gen: bool = False # whether to use action related parameters and condition/generate action tokens + max_action_dim: int = 32 # maximum dimension of the action space, we need to pad the data to this dimension. + num_embodiment_domains: int = 32 # number of domains for the domain-aware linear layer + + # sound configs + sound_gen: bool = False # whether to use sound related parameters and condition/generate sound tokens + sound_tokenizer: LazyDict | None = None # Sound tokenizer config (e.g., AVAE) + sound_dim: int | None = None # Sound latent channel size (e.g., 64 for AVAE 48kHz) + sound_latent_fps: int = 25 # Sound tokenizer's latent rate (e.g., 48kHz / 1920 hop = 25 Hz) + + log_enc_time_every_n: int = 100 # Frequency of logging encoding time to W&B + + def validate(self, root_config: Config) -> None: + """Skip pretrained loading if a training checkpoint exists. + + Mutates root_config.model.config.* directly because the receiver self + is a fresh attrs copy from OmegaConf.to_object and its writes would be + dropped. + """ + from cosmos.utils import log + from cosmos.checkpoint.dcp import DistributedCheckpointer + + # There are three cases to consider: + # 1. Model is being trained from scratch (using weights from Hugging Face). + # (both _read_latest_checkpoint_file() and load_path are None). + # In this case, we should load the understanding pathway weights from HF weights, + # Additionally, we must copy the understanding pathway weights to the generation + # pathway. + # + # 2. Model is being trained from a previous checkpoint. + # (_read_latest_checkpoint_file() is not None and load_path can be None or not). + # In this case, the model weights have been already loaded from DCP checkpoint + # (checkpointer/dcp.py). We must skip both loading understanding pathway weights, + # and copying the understanding pathway weights to the generation pathway. + + # 3. Model is being warm-started from a load_path (but no previous checkpoint exists). + # (_read_latest_checkpoint_file() is None and load_path is not None). + # In this case, the model weights have been already loaded from DCP checkpoint + # due to load_path being specified (checkpointer/dcp.py). However, we must still + # load the understanding weights from HF weights (since the understanding model + # may be moved from Qwen3-VL to Cosmos-Reason2 for example). We should not copy + # the understanding pathway weights to the generation pathway (since the generation + # pathway has already been pretrained using the previous model weights, for example, + # the Qwen3-VL weights). But the understanding weights are always kept unchanged. + + if not self.vlm_config.load_pretrained and not self.diffusion_expert_config.load_weights_from_pretrained: + # Neither if branch below is taken; no need to create checkpointer. + return + + checkpointer = DistributedCheckpointer( + root_config.checkpoint, root_config.job, callbacks=None, disable_async=True + ) + + if self.vlm_config.load_pretrained: + if checkpointer._read_latest_checkpoint_file() is not None: + log.info( + "Checkpoint found: disabling pretrained model loading to avoid double loading. " + "Model weights will be loaded from checkpoint instead of safetensors." + ) + root_config.model.config.vlm_config.load_pretrained = False + + if self.diffusion_expert_config.load_weights_from_pretrained: + if checkpointer.load_path is not None: + log.info( + "Load path found: disabling pretrained model loading for generation pathway. " + "Generation pathway weights will be loaded from load_path instead of safetensors." + ) + root_config.model.config.diffusion_expert_config.load_weights_from_pretrained = False + + +@attrs.define(slots=False) +class VLMModelConfig(ModelConfig): + """ + Config for VLM model. + """ + + policy: PolicyConfig = PolicyConfig() + train: TrainConfig = TrainConfig() diff --git a/cosmos_training/configs/base/defaults/multiview_dataloader.py b/cosmos_training/configs/base/defaults/multiview_dataloader.py new file mode 100644 index 00000000..deb3f87b --- /dev/null +++ b/cosmos_training/configs/base/defaults/multiview_dataloader.py @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Hydra ConfigStore registration for multiview dataloaders. + +Registers named dataloader configs that can be referenced via Hydra overrides +(e.g. ``{override /data_train: video_control_mads_multiview_0823_gcs_720p_10fps_93frames_7views}``) +or used as templates for inline ``L(get_multiview_video_loader)(...)`` in +experiment configs. + +Two naming conventions: + + **Transfer** (with control signal): + ``video_control_{dataset}_{store}_{res}_{fps}_{frames}_{views}`` + + **Predict** (no control signal): + ``video_{dataset}_{store}_{res}_{fps}_{frames}_{views}`` +""" + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.data.vfm.multiview.multiview_data_source import ( + DEFAULT_CAMERAS, + INDEX_TO_CAMERA_MAPPING, + TRANSFER_CAPTION_KEY_MAPPING, + TRANSFER_CONTROL_KEY_MAPPING, + TRANSFER_VIDEO_KEY_MAPPING, +) +from cosmos.data.vfm.multiview.multiview_dataset import ( + MultiviewAugmentationConfig, + get_multiview_video_loader, +) + +# --------------------------------------------------------------------------- +# Camera view subsets +# --------------------------------------------------------------------------- + +CAMERA_VIEW_CONFIGS: dict[str, tuple[str, ...]] = { + "7views": DEFAULT_CAMERAS, + "1view_front": ("camera_front_wide_120fov",), + "4views": ( + "camera_front_wide_120fov", + "camera_cross_right_120fov", + "camera_rear_tele_30fov", + "camera_cross_left_120fov", + ), +} + +# --------------------------------------------------------------------------- +# Grid dimensions +# --------------------------------------------------------------------------- + +_TRANSFER_DATASETS = ["mads_multiview_0823"] +_OBJECT_STORES = ["gcs"] + +_RESOLUTIONS: list[tuple[str, tuple[int, int]]] = [ + ("720p", (720, 1280)), +] + +_FPS: list[tuple[str, int]] = [ + ("10fps", 1), # MADS transfer data is already at 10 fps +] + +_NUM_VIDEO_FRAMES: list[tuple[str, int]] = [ + ("29frames", 29), + ("61frames", 61), + ("93frames", 93), +] + + +def register_multiview_dataloaders() -> None: + """Register all multiview dataloader configs with Hydra ConfigStore.""" + + cs = ConfigStore.instance() + + # ----- Transfer dataloaders (with control signals) ----- + for dataset in _TRANSFER_DATASETS: + for object_store in _OBJECT_STORES: + for resolution_str, resolution_hw in _RESOLUTIONS: + for fps_str, downsample_factor in _FPS: + for num_frames_str, num_frames in _NUM_VIDEO_FRAMES: + for views_str, camera_keys in CAMERA_VIEW_CONFIGS.items(): + name = ( + f"video_control_{dataset}_{object_store}_{resolution_str}_" + f"{fps_str}_{num_frames_str}_{views_str}" + ) + cs.store( + group="data_train", + package="dataloader_train", + name=name, + node=L(get_multiview_video_loader)( + dataset_name=dataset, + is_train=True, + augmentation_config=L(MultiviewAugmentationConfig)( + resolution_hw=resolution_hw, + fps_downsample_factor=downsample_factor, + num_video_frames=num_frames, + camera_keys=camera_keys, + camera_video_key_mapping=TRANSFER_VIDEO_KEY_MAPPING, + camera_caption_key_mapping=TRANSFER_CAPTION_KEY_MAPPING, + camera_control_key_mapping=TRANSFER_CONTROL_KEY_MAPPING, + position_to_camera_mapping=INDEX_TO_CAMERA_MAPPING, + single_caption_camera_name="camera_front_wide_120fov", + ), + ), + ) + + # ----- Predict dataloaders (no control signals, for future use) ----- + # These use named keys (video_camera_front_wide_120fov, etc.) and need + # different datasets (e.g. alpamayo_dec2024) with 30 fps native data. + # Uncomment and add predict datasets to the catalog when needed. + # + # _PREDICT_DATASETS = ["alpamayo_dec2024"] + # _PREDICT_FPS = [("10fps", 3), ("15fps", 2)] # 30 fps native → downsample + # for dataset in _PREDICT_DATASETS: + # for object_store in _OBJECT_STORES: + # for resolution_str, resolution_hw in _RESOLUTIONS: + # for fps_str, downsample_factor in _PREDICT_FPS: + # for num_frames_str, num_frames in _NUM_VIDEO_FRAMES: + # for views_str, camera_keys in CAMERA_VIEW_CONFIGS.items(): + # name = ( + # f"video_{dataset}_{object_store}_{resolution_str}_" + # f"{fps_str}_{num_frames_str}_{views_str}" + # ) + # cs.store( + # group="data_train", + # package="dataloader_train", + # name=name, + # node=L(get_multiview_video_loader)( + # dataset_name=dataset, + # is_train=True, + # augmentation_config=L(MultiviewAugmentationConfig)( + # resolution_hw=resolution_hw, + # fps_downsample_factor=downsample_factor, + # num_video_frames=num_frames, + # camera_keys=camera_keys, + # camera_video_key_mapping=PREDICT_VIDEO_KEY_MAPPING, + # camera_caption_key_mapping=PREDICT_CAPTION_KEY_MAPPING, + # camera_control_key_mapping=None, + # position_to_camera_mapping=None, + # single_caption_camera_name=None, + # ), + # ), + # ) + + +# Auto-register on import +register_multiview_dataloaders() diff --git a/cosmos_training/configs/base/defaults/open_source_dataloader.py b/cosmos_training/configs/base/defaults/open_source_dataloader.py new file mode 100644 index 00000000..b95ba821 --- /dev/null +++ b/cosmos_training/configs/base/defaults/open_source_dataloader.py @@ -0,0 +1,209 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Hydra ConfigStore registration for the open-source SFT dataloader. + +This mirrors the inline ``dataloader_train`` block in +``configs/experiment/mixed_modality_sft_8b.yaml`` (cosmos-inference) so users +can pick it up via the Hydra defaults group:: + + defaults: + - data_train: open_source_sft_video_256p + +or as a base to override in their own experiment configs:: + + L(get_open_source_sft_dataloader)( + jsonl_paths=["/path/to/data.jsonl"], + resolution="256", + max_sequence_length=45056, + ) + +Original YAML reference target paths use the ``cosmos3._src.vfm.*`` namespace +(the OSS-release form of ``projects.cosmos3.vfm.*``); inside this released +tree the same modules live under ``cosmos.data.vfm.*``. +""" + +from hydra.core.config_store import ConfigStore + +from cosmos.data.vfm.joint_dataloader import ( + PackingDataLoader, + RankPartitionedDataLoader, +) +from cosmos.data.vfm.local_datasets.sft_dataset import get_sft_dataset +from cosmos.utils.lazy_config import LazyCall as L +from configs.base.defaults.vlm import create_qwen2_tokenizer_with_download + + +# --------------------------------------------------------------------------- +# Inner: SFT video dataset (matches the inline ``get_sft_dataset`` call in the +# reference YAML). +# --------------------------------------------------------------------------- + +def get_sft_video_dataset( + *, + jsonl_paths: list[str], + resolution: str = "256", + pretrained_model_name: str = "Qwen/Qwen3-VL-8B-Instruct", + tokenizer_config_variant: str = "hf", + num_video_frames: int = -1, + temporal_compression_factor: int = 4, + temporal_interval_mode: str = "max_30fps", + min_short_edge: int = 0, + frame_selection_mode: str = "first", + sample_by_window: bool = False, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + use_system_prompt: bool = False, + caption_suffix: str = "", + cfg_dropout_rate: float = 0.1, + cfg_dropout_keep_metadata: bool = False, + conditioning_config: dict[int, float] | None = None, + conditioning_fps: int = -1, + conditioning_fps_noise_std: float = 0.0, +): + """LazyCall'd version of ``get_sft_dataset`` matching the reference YAML. + + Defaults reproduce ``mixed_modality_sft_8b.yaml``: 70% T2V / 20% I2V / + 10% V2V conditioning mix at 256p with the Qwen3-VL-8B tokenizer. + """ + if conditioning_config is None: + # 0: T2V (text-to-video) — 70% + # 1: I2V (first-frame conditioning) — 20% + # 2: V2V (first 5 frames → 2 latent frames) — 10% + conditioning_config = {0: 0.7, 1: 0.2, 2: 0.1} + + return L(get_sft_dataset)( + jsonl_paths=jsonl_paths, + resolution=resolution, + num_video_frames=num_video_frames, + temporal_compression_factor=temporal_compression_factor, + temporal_interval_mode=temporal_interval_mode, + min_short_edge=min_short_edge, + frame_selection_mode=frame_selection_mode, + sample_by_window=sample_by_window, + append_duration_fps_timestamps=append_duration_fps_timestamps, + append_resolution_info=append_resolution_info, + use_system_prompt=use_system_prompt, + caption_suffix=caption_suffix, + cfg_dropout_rate=cfg_dropout_rate, + cfg_dropout_keep_metadata=cfg_dropout_keep_metadata, + conditioning_config=conditioning_config, + conditioning_fps=conditioning_fps, + conditioning_fps_noise_std=conditioning_fps_noise_std, + tokenizer_config=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name=pretrained_model_name, + config_variant=tokenizer_config_variant, + ), + ) + + +# --------------------------------------------------------------------------- +# Outer: full PackingDataLoader → RankPartitionedDataLoader → SFT dataset +# pipeline. This is the registered config_store node. +# --------------------------------------------------------------------------- + +def get_open_source_sft_dataloader( + *, + jsonl_paths: list[str] | None = None, + resolution: str = "256", + batch_size: int = 1, + max_sequence_length: int = 45056, + max_samples_per_batch: int | None = None, + num_workers: int = 4, + prefetch_factor: int = 4, + audio_sample_rate: int = 48000, + patch_spatial: int = 2, + tokenizer_spatial_compression_factor: int = 16, + tokenizer_temporal_compression_factor: int = 4, + sound_latent_fps: int = 0, + dataset_name: str = "default", + video_stream_ratio: float = 1.0, +): + """Build the full open-source SFT dataloader (PackingDataLoader at top). + + ``jsonl_paths`` defaults to a Hydra-MISSING marker (``"???"``) so the + user MUST override it at experiment time:: + + ... dataloader_train.dataloader.datasets.video.dataset.jsonl_paths='[".../data.jsonl"]' + """ + if jsonl_paths is None: + # Hydra/OmegaConf "mandatory" sentinel — must be overridden. + jsonl_paths = "???" # type: ignore[assignment] + + return L(PackingDataLoader)( + dataloader=L(RankPartitionedDataLoader)( + batch_size=batch_size, + datasets=dict( + video=dict( + dataset=get_sft_video_dataset( + jsonl_paths=jsonl_paths, + resolution=resolution, + ), + ratio=video_stream_ratio, + ), + ), + in_order=True, + num_workers=num_workers, + persistent_workers=True, + pin_memory=True, + prefetch_factor=prefetch_factor, + sampler=None, + ), + audio_sample_rate=audio_sample_rate, + dataset_name=dataset_name, + max_samples_per_batch=max_samples_per_batch, + max_sequence_length=max_sequence_length, + patch_spatial=patch_spatial, + sound_latent_fps=sound_latent_fps, + tokenizer_spatial_compression_factor=tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor=tokenizer_temporal_compression_factor, + ) + + +# --------------------------------------------------------------------------- +# ConfigStore registration. +# --------------------------------------------------------------------------- + +def register_open_source_dataloaders() -> None: + """Register named dataloader configs under the ``data_train`` Hydra group. + + Pick them via experiment config:: + + defaults: + - data_train: open_source_sft_video_256p + - data_train: open_source_sft_video_480p + - data_train: open_source_sft_video_720p + """ + cs = ConfigStore.instance() + + for res_str, max_seq in [ + ("256", 45056), + ("480", 45056), + ("720", 45056), + ]: + cs.store( + group="data_train", + package="dataloader_train", + name=f"open_source_sft_video_{res_str}p", + node=get_open_source_sft_dataloader( + resolution=res_str, + max_sequence_length=max_seq, + ), + ) + + +# Auto-register on import. +register_open_source_dataloaders() diff --git a/cosmos_training/configs/base/defaults/optimizer.py b/cosmos_training/configs/base/defaults/optimizer.py new file mode 100644 index 00000000..bb59849b --- /dev/null +++ b/cosmos_training/configs/base/defaults/optimizer.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Copied from https://gitlab-master.nvidia.com/dir/imaginaire4/-/blob/d0921eb675d1251e73c4b19acdd78e6ad936ae3b/projects/cosmos/reason2/configs/base/defaults/optimizer.py without changes +""" + +from cosmos.utils.lazy_config import PLACEHOLDER +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.config_helper import ConfigStore +from cosmos.utils.vfm.optimizer import build_lr_scheduler, build_optimizer + +optimizer_kwargs = dict( + # Learning rate for the optimizer. + lr=1e-4, + # Weight decay for the optimizer. + weight_decay=0.1, + # Beta1 and beta2 for the optimizer. + betas=[0.9, 0.99], + # Epsilon for the optimizer. + eps=1e-8, + # Whether to use fuse updates to all parameters. + fused=True, + # Keys to select for the optimizer. + keys_to_select=[], + # Per-key LR multipliers. Maps parameter name patterns to LR multipliers. + # E.g. {"sound2llm": 5.0, "llm2sound": 5.0} gives those params 5x the base LR. + lr_multipliers={}, + # Whether to disable weight decay for one-dimensional params such as norm weights and biases. + # Default is False to preserve historical optimizer behavior. + disable_weight_decay_for_1d_params=False, +) + +lr_scheduler_kwargs = dict( + warm_up_steps=[2000], + f_min=[0.0], + f_max=[1.0], + f_start=[0.0], + cycle_lengths=[100000], + verbosity_interval=0, +) + + +def register_optimizer(): + cs = ConfigStore.instance() + cs.store( + group="optimizer", + package="optimizer", + name="fusedadamw", + node=L(build_optimizer)( + model=PLACEHOLDER, + optimizer_type="FusedAdam", + **optimizer_kwargs, + ), + ) + cs.store( + group="optimizer", + package="optimizer", + name="adamw", + node=L(build_optimizer)( + model=PLACEHOLDER, + optimizer_type="AdamW", + **optimizer_kwargs, + ), + ) + + +def register_scheduler(): + cs = ConfigStore.instance() + cs.store( + group="scheduler", + package="scheduler", + name="lambdalinear", + node=L(build_lr_scheduler)( + optimizer=PLACEHOLDER, + lr_scheduler_type="LambdaLinear", + warm_up_steps=[1000], + cycle_lengths=[10000000000000], + f_start=[1.0e-6], + f_max=[1.0], + f_min=[1.0], + ), + ) + + # Cosine scheduler that works with any optimizer (including fusedadamw) + cs.store( + group="scheduler", + package="scheduler", + name="lambdacosine", + node=L(build_lr_scheduler)( + optimizer=PLACEHOLDER, + lr_scheduler_type="LambdaCosine", + warm_up_steps=[2000], + cycle_lengths=[100000], + f_start=[0.0], + f_max=[1.0], + f_min=[0.0], + verbosity_interval=0, + ), + ) diff --git a/cosmos_training/configs/base/defaults/parallelism.py b/cosmos_training/configs/base/defaults/parallelism.py new file mode 100644 index 00000000..8f7edd9b --- /dev/null +++ b/cosmos_training/configs/base/defaults/parallelism.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared user-facing parallelism schema for VFM and VLM. + +Both project trees (vfm/, vfm/configs/base/vlm/) instantiate the same +ParallelDims runtime at vfm/utils/parallelism.py. They now also share this +single user-facing config schema. Trainer-side translation from the long +descriptive field names here to the short ParallelDims constructor kwargs +happens at the read site (see vfm/models/{omni_mot_model,vlm_model}.py). +""" + +import attrs + + +@attrs.define(slots=False) +class ParallelismConfig: + # Activation checkpointing is used to reduce the memory usage of the model. + # The outputs of each layer are checkpointed, the intermediate results are not saved. + use_activation_checkpointing: bool = False + + # Torch compile is used to compile the model for faster training. + use_torch_compile: bool = False + + # Whether to use CUDA graphs for faster inference. This option does not work during training. + use_cuda_graphs: bool = False + + # Whether the entire Cosmos3 VFM network is compiled, or only a specific region is compiled. + # Use "language" to compile only individual layers in the MOT model. + # Use "all" to compile the the MOT model, as well as encode/decode functions. + compiled_region: str = attrs.field( + default="language", + validator=attrs.validators.in_({"all", "language"}), + ) + + # Whether torch.compile should generate symbolic-shape (dynamic) kernels + # (maps to ``torch.compile(dynamic=...)``). Defaults to True for training, + # which sees varying shapes across batches (sequence length, CP sharding, ...); + # specializing would recompile continuously. See ParallelismOverrides in + # packages/cosmos3/cosmos3/common/args.py for the inference-side rationale + # (where dynamic=False is preferred for stable AR shapes). + compile_dynamic: bool = True + + # Enable autotuning for pointwise/reduction Triton kernels (e.g. RMSNorm). + # Explores 6 candidate configs instead of the default 1, improving kernel performance + # at the cost of longer first-iteration compilation time. + max_autotune_pointwise: bool = False + + # Enable coordinate descent tuning after autotuning. Starts from the best autotuned + # config and explores nearby configs by adjusting one parameter at a time. + # Requires max_autotune_pointwise=True to have effect on reduction kernels. + coordinate_descent_tuning: bool = False + + # Whether to enable inference mode. + enable_inference_mode: bool = False + + # Number of ranks for sharding the model weights (FSDP). The default -1 + # auto-infers to world_size at runtime via ParallelDims. + data_parallel_shard_degree: int = -1 + + # Number of ranks for replicating the model weights (HSDP outer dim). + # data_parallel_replicate_degree x data_parallel_shard_degree must divide + # world_size when both are explicitly set. + data_parallel_replicate_degree: int = 1 + + # Number of ranks for context parallelism. + context_parallel_shard_degree: int = 1 + + # Number of ranks for CFG parallelism. + cfg_parallel_shard_degree: int = 1 + + # Precision for the model. + precision: str = "bfloat16" diff --git a/cosmos_training/configs/base/defaults/tokenizer.py b/cosmos_training/configs/base/defaults/tokenizer.py new file mode 100644 index 00000000..1dfcef2e --- /dev/null +++ b/cosmos_training/configs/base/defaults/tokenizer.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import PLACEHOLDER, LazyDict +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.model.vfm.tokenizers.wan2pt2_vae_4x16x16 import Wan2pt2VAEInterface + +PRETRAINED_TOKENIZER_WAN2PT1_VAE_PTH = "pretrained/tokenizers/video/wan2pt1/Wan2.1_VAE.pth" +PRETRAINED_TOKENIZER_WAN2PT2_VAE_PTH = "pretrained/tokenizers/video/wan2pt2/Wan2.2_VAE.pth" +PRETRAINED_TOKENIZER_FLUX_VAE_PTH = "pretrained/tokenizers/image/flux/ae.safetensors" + +# UniAE checkpoint paths +PRETRAINED_TOKENIZER_UNIAE_4X16X16_C48_T8TO24_64TO512P_FPS_ALL_ENCODER_NONCAUSAL_DECODER_NONCAUSAL_NOGAN_BEST_S1_VAE_PTH = "pretrained/tokenizers/video/cosmos/uniae4x16x16_c48_t8to24_64to512p_fps_all_encoder_noncausal_decoder_noncausal_nogan_best_s1.pt" + +# DCAE checkpoint paths +PRETRAINED_TOKENIZER_DCAE_PTH = "pretrained/tokenizers/video/cosmos/dc-ae-v-1.0-f32t4c64-cosmos-encoder-causal-decoder-chunk-causal-4-frame-120-pad-7-no-gan.pt" +PRETRAINED_TOKENIZER_DCAE_4X32X32_C64_T120_256P_FPS_ALL_ENCODER_CAUSAL_DECODER_CHUNKCAUSAL4_NOGAN_COSMOS_PAD_7_V0PT2_PTH = "pretrained/tokenizers/video/cosmos/dcae4x32x32_c64_t120_256p_fps_all_encoder_causal_decoder_chunk_causal_4_nogan_cosmos_pad_7_v0.2.pt" + +# AVAE (Audio VAE) checkpoint paths +PRETRAINED_TOKENIZER_AVAE_PTH = "pretrained/tokenizers/audio/avae/model_unwrap.ckpt" +PRETRAINED_TOKENIZER_AVAE_44K_NONCAUSAL = "pretrained/tokenizers/audio/avae/avae_44k_noncausal_21hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_44K_CAUSAL = "pretrained/tokenizers/audio/avae/avae_44k_causal_21hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_48K_25HZ = "pretrained/tokenizers/audio/avae/avae_48k_noncausal_25hz_64ch.ckpt" +PRETRAINED_TOKENIZER_AVAE_48K_6HZ = "pretrained/tokenizers/audio/avae/avae_48k_noncausal_6hz_64ch.ckpt" + + + +Wan2pt2VAEConfig: LazyDict = L(Wan2pt2VAEInterface)( + bucket_name=PLACEHOLDER, + object_store_credential_path_pretrained=PLACEHOLDER, + vae_path=PRETRAINED_TOKENIZER_WAN2PT2_VAE_PTH, + spatial_compression_factor=16, + temporal_compression_factor=4, +) + + + +def register_tokenizer(): + cs = ConfigStore.instance() + + # Wan2pt2 tokenizer (the only one the cosmos release needs) + cs.store(group="tokenizer", package="model.config.tokenizer", name="wan2pt2_tokenizer", node=Wan2pt2VAEConfig) + + +def register_sound_tokenizer(): + """Register sound tokenizers in Hydra ConfigStore under model.config.sound_tokenizer.""" + cs = ConfigStore.instance() diff --git a/cosmos_training/configs/base/defaults/unittest.py b/cosmos_training/configs/base/defaults/unittest.py new file mode 100644 index 00000000..0de21c7f --- /dev/null +++ b/cosmos_training/configs/base/defaults/unittest.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import attrs + +# from configs.base.defaults.cluster import GCPIADGB200Config + +# We are hardcoding the unittest assets in this file. + +# CLUSTER_CONFIG = GCPIADGB200Config + +# add codeowner for cosmos/model/vfm/tokenizers + + +@attrs.define(slots=False) +class SwfitStackPDXrConfig: + """ + Config for the cluster specific information. + Everything cluster specific should be here. + """ + + object_store_bucket_data: str + object_store_credential_data: str + + +UNITTEST_CONFIG = SwfitStackPDXrConfig( + object_store_bucket_data="unittest", + object_store_credential_data="credentials/pdx_dir.secret", +) + +TOKENIZER_RECONSTRUCTION_VIDEO_PATH = "tokenizer/video/panda70m_test_0000039_00000.mp4" +AVAE_RECONSTRUCTION_AUDIO_PATH = "tokenizer/audio/test_audio.wav" diff --git a/cosmos_training/configs/base/defaults/vlm.py b/cosmos_training/configs/base/defaults/vlm.py new file mode 100644 index 00000000..9fbb8434 --- /dev/null +++ b/cosmos_training/configs/base/defaults/vlm.py @@ -0,0 +1,985 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Configs for VLM / LLM models + +import os + +import attrs +import torch.distributed as dist + +from cosmos.utils.flags import INTERNAL +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.lazy_config import LazyDict +from cosmos.utils.lazy_config import instantiate as lazy_instantiate +from cosmos.utils import log +from cosmos.utils.config_helper import ConfigStore +from cosmos.utils.easy_io import easy_io +from cosmos.model.vfm.mot.unified_mot import ( + Nemotron3DenseVLTextConfig, + Nemotron3DenseVLTextForCausalLM, + Qwen3VLMoeTextConfig, + Qwen3VLMoeTextForCausalLM, + Qwen3VLTextConfig, + Qwen3VLTextForCausalLM, +) +from cosmos.data.vfm.processors import build_processor_lazy +from cosmos.model.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer + + +def create_vlm_config(base_config: LazyDict, **overrides): + vlm_config = lazy_instantiate(base_config) + for key, value in overrides.items(): + setattr(vlm_config, key, value) + return vlm_config + + +def get_rank_safe() -> int: + if dist.is_available() and dist.is_initialized(): + return dist.get_rank() + return 0 # default to rank 0 when not in distributed mode + + +################################################################################ +# Download tokenizer files from s3 +# Download to ~/.cache/imaginaire4/tokenizer_files/{model_name} and then load from there. +def download_tokenizer_files(model_name: str, config_variant: str) -> str: + if config_variant == "hf": + return model_name + + if config_variant == "s3": + ckpt_bucket = "checkpoints-us-east-1" + credentials = "credentials/s3_checkpoint.secret" + elif config_variant == "gcp": + ckpt_bucket = "nv-00-10206-checkpoint" + credentials = "credentials/gcp_checkpoint.secret" + else: + raise ValueError(f"Invalid config variant: {config_variant}") + + model_path = f"s3://{ckpt_bucket}/cosmos3/pretrained/huggingface/{model_name}" + if not INTERNAL: + from cosmos.utils.checkpoint_db import download_checkpoint_v2 + + model_path = download_checkpoint_v2(model_path) + if "://" not in model_path: + return model_path + + imaginaire_cache_dir = os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire4")) + destination_dir = os.path.join(imaginaire_cache_dir, f"tokenizer_files/{model_name}/rank_{get_rank_safe()}") + s3_backend_args = { + "backend": "s3", + "s3_credential_path": credentials, + } + + extensions = ["json", "txt", "jinja"] + for extension in extensions: + for file_path in easy_io.list_dir_or_file( + model_path, + list_dir=False, + list_file=True, + suffix=extension, + recursive=False, + backend_args=s3_backend_args, + ): + full_path = easy_io.join_path(model_path, file_path, backend_args=s3_backend_args) + local_path = f"{destination_dir}/{file_path}" + if os.path.exists(local_path): + log.debug(f"Skipping already downloaded tokenizer file: {local_path}") + continue + log.info(f"Downloading tokenizer file: {full_path} to {local_path}, cwd: {os.getcwd()}") + # Download the file + file_data = easy_io.get(full_path, backend_args=s3_backend_args) + easy_io.put(file_data, local_path) + return destination_dir + + +def create_qwen2_tokenizer_with_download(pretrained_model_name: str, config_variant: str, **_unused_kwargs): + # **_unused_kwargs absorbs extras (e.g. tokenizer_type) that OmegaConf + # merges in from a vlm_config preset's tokenizer block when an experiment + # overrides the tokenizer with this function but doesn't fully replace + # the preset's kwarg dict. + destination_dir = download_tokenizer_files(pretrained_model_name, config_variant) + return Qwen2Tokenizer.from_pretrained(destination_dir) + + +@attrs.define(slots=False) +class VLMConfig: + # Name of the huggingface model + model_name: str = "" + + # Langugage model class to instantiate + model_instance: LazyDict | None = None + + # Tokenizer / processor used by data augmentors. + # For VLM configs this is a unified processor (build_processor); for LLM-only configs a raw + # tokenizer factory. Code that needs the raw tokenizer should check is_unified_processor. + tokenizer: LazyDict | None = None + + # Path to the checkpoint + checkpoint_path: str = "" + + # Path to the credential file + credential_path: str = "" # Path to the credential file + + # Whether to enable GCS patch in boto3 for DCP loading from GCS + enable_gcs_patch_in_boto3: bool = False + + # Whether to load the pretrained LLM / VLM + load_pretrained: bool = True + + # Layer module to use. We override the decoder layer in huggingface model with this class. + # This is needed as we need to initialize MoT layers. + layer_module: str = "Qwen2MoTDecoderLayer" + + # Whether to use QK normalization for text expert + qk_norm_for_text: bool = False + + # Whether to use QK normalization for diffusion expert + qk_norm_for_diffusion: bool = True # Whether to use QK normalization for diffusion expert + + # If True, use the same word embedding matrices for input and outut embedding layers. + tie_word_embeddings: bool = False + + # Whether to prepend a system prompt during text tokenization. + # Checkpoints trained with system prompt enabled require this to be True at inference time. + use_system_prompt: bool = False + + # If set, forces safetensors weight remapping ("qwen3" vs "nemotron_3_dense_vl"/"nemotron_3_llm"). None = auto-detect. + vlm_checkpoint_format: str | None = None + + +# Configs for LLM models +Qwen3MoT_LLM_0p6b_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-0.6B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/llm/qwen3/configs/Qwen3-0.6B.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-0.6B", + config_variant="hf", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-0.6B/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3MoT_LLM_0p6b_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-0.6B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/llm/qwen3/configs/Qwen3-0.6B.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-0.6B", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-0.6B/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, +) + +Nemotron3_LLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/NVIDIA-Nemotron-3-2B-BF16", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Nemotron/NVIDIA-Nemotron-3-2B-BF16", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Nemotron/NVIDIA-Nemotron-3-2B-BF16/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_llm", +) + +# Configs for VL instruct models + +# Config for Qwen3VL 30B A3B Instruct model +# Qwen3VLMoE uses Qwen2Tokenizer +Qwen3VLMoT_VLM_30b_a3b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-30B-A3B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + + +Qwen3VLMoT_VLM_30b_a3b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-30B-A3B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-30B-A3B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_30b_a3b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-30B-A3B-Private", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-30B-A3B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-30B-A3B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 235B A22B Instruct model +# Qwen3VLMoE uses Qwen2Tokenizer +Qwen3VLMoT_VLM_235b_a22b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-235B-A22B-Instruct", + model_instance=L(Qwen3VLMoeTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLMoeTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json" + ), + layer_module="Qwen3VLMoeTextMoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-235B-A22B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-235B-A22B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + + +# Config for Qwen3VL 2B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_2b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-2B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_2b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-2B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Qwen3VLMoT_VLM_2b_Instruct_HF_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-2B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="hf", + ), + load_pretrained=True, +) + +Nemotron3DenseVL_VLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Nemotron-3-Dense-VL-2B-BF16-Alignment", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_dense_vl", +) + +Cosmos3Reasoner_Nemotron_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-2B-Private", + model_instance=L(Nemotron3DenseVLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Nemotron3DenseVLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=False, + qk_norm_for_diffusion=True, + tie_word_embeddings=False, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Nemotron/NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/nvidia/Cosmos3-Reasoner-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, + vlm_checkpoint_format="nemotron_3_dense_vl", +) + +CosmosReason2_VLM_2b_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-2B", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-2B/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-2B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_2b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-2B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-2B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-2B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-2B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 4B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_4b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-4B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-4B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-4B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_4b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-4B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-4B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-4B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-4B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 8B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_8b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-8B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-8B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_8b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-8B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-8B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_8b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-8B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-8B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_8b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-8B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-8B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3NanoReasoner_VLM_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Nano-Reasoner", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-8B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-8B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Nano-Reasoner/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +# Config for Qwen3VL 32B Instruct model +# Qwen3VL uses Qwen2Tokenizer +Qwen3VLMoT_VLM_32b_Instruct_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-32B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-32B-Instruct", + config_variant="s3", + ), + checkpoint_path="s3://checkpoints-us-east-1/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-32B-Instruct/", + credential_path="credentials/s3_training.secret", + load_pretrained=True, +) + +Qwen3VLMoT_VLM_32b_Instruct_GCP_Config: VLMConfig = VLMConfig( + model_name="Qwen/Qwen3-VL-32B-Instruct", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Qwen/Qwen3-VL-32B-Instruct/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +CosmosReason2_VLM_32b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos-Reason2-32B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos-Reason2-32B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3Reasoner_VLM_32b_Private_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Reasoner-32B-Private", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(build_processor_lazy)( + tokenizer_type="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Reasoner-32B-Private/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + +Cosmos3SuperReasoner_VLM_GCP_Config: VLMConfig = VLMConfig( + model_name="nvidia/Cosmos3-Super-Reasoner", + model_instance=L(Qwen3VLTextForCausalLM)( + config=L(create_vlm_config)( + base_config=L(Qwen3VLTextConfig.from_json_file)( + json_file="cosmos/model/vfm/vlm/qwen3_vl/configs/Qwen3-VL-32B-Instruct.json" + ), + layer_module="MoTDecoderLayer", + qk_norm_for_text=True, + qk_norm_for_diffusion=True, + tie_word_embeddings=True, + freeze_und=False, + ), + ), + tokenizer=L(create_qwen2_tokenizer_with_download)( + pretrained_model_name="Qwen/Qwen3-VL-32B-Instruct", + config_variant="gcp", + ), + checkpoint_path="s3://nv-00-10206-checkpoint/cosmos3/pretrained/huggingface/Cosmos-Reason/Cosmos3-Super-Reasoner/", + credential_path="credentials/gcp_checkpoint.secret", + load_pretrained=True, + enable_gcs_patch_in_boto3=True, +) + + +def register_vlm(): + cs = ConfigStore.instance() + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_mot_0p6b", + node=Qwen3MoT_LLM_0p6b_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_mot_0p6b_gcp", + node=Qwen3MoT_LLM_0p6b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="nemotron_3_llm_2b_gcp", + node=Nemotron3_LLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_30b_a3b_instruct", + node=Qwen3VLMoT_VLM_30b_a3b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_30b_a3b_instruct_gcp", + node=Qwen3VLMoT_VLM_30b_a3b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_235b_a22b_instruct_gcp", + node=Qwen3VLMoT_VLM_235b_a22b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct", + node=Qwen3VLMoT_VLM_2b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct_gcp", + node=Qwen3VLMoT_VLM_2b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_2b_instruct_hf", + node=Qwen3VLMoT_VLM_2b_Instruct_HF_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="nemotron_3_dense_vl_2b_gcp", + node=Nemotron3DenseVL_VLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_nemotron_vlm_2b_private_gcp", + node=Cosmos3Reasoner_Nemotron_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_2b_gcp", + node=CosmosReason2_VLM_2b_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_2b_private_gcp", + node=CosmosReason2_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_2b_private_gcp", + node=Cosmos3Reasoner_VLM_2b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_8b_private_gcp", + node=CosmosReason2_VLM_8b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_8b_private_gcp", + node=Cosmos3Reasoner_VLM_8b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_nano_reasoner_vlm_gcp", + node=Cosmos3NanoReasoner_VLM_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_32b_private_gcp", + node=CosmosReason2_VLM_32b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_reasoner_vlm_32b_private_gcp", + node=Cosmos3Reasoner_VLM_32b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos3_super_reasoner_vlm_gcp", + node=Cosmos3SuperReasoner_VLM_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="cosmos_reason2_vlm_30b_a3b_private_gcp", + node=CosmosReason2_VLM_30b_a3b_Private_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_4b_instruct", + node=Qwen3VLMoT_VLM_4b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_4b_instruct_gcp", + node=Qwen3VLMoT_VLM_4b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_8b_instruct", + node=Qwen3VLMoT_VLM_8b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_8b_instruct_gcp", + node=Qwen3VLMoT_VLM_8b_Instruct_GCP_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_32b_instruct", + node=Qwen3VLMoT_VLM_32b_Instruct_Config, + ) + cs.store( + group="vlm_config", + package="model.config.vlm_config", + name="qwen3_vl_mot_vlm_32b_instruct_gcp", + node=Qwen3VLMoT_VLM_32b_Instruct_GCP_Config, + ) diff --git a/cosmos_training/configs/base/vlm/__init__.py b/cosmos_training/configs/base/vlm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/configs/base/vlm/__pycache__/__init__.cpython-312.pyc b/cosmos_training/configs/base/vlm/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2f9e3b2d3d8e42ece97e38c92b663f76f12d252 GIT binary patch literal 235 zcmZ8b!41MN3~VTs5K;%>gBL)IzzR_bX_YFCD?0&o1r}fiW?>8_K;_jJi9~r|>Fo2# zck-hw*IBIaF4{Dosr}cujLIz=vW+<0Gg`!LOgGaNY+z3sYQ8{^y@Rv74bC@!HK+bAq3@-eE@FUM%VxV literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/vlm/config.py b/cosmos_training/configs/base/vlm/config.py new file mode 100644 index 00000000..8abc1881 --- /dev/null +++ b/cosmos_training/configs/base/vlm/config.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import log +from cosmos.utils.config_helper import import_all_modules_from_package +from configs.base.vlm.defaults.callbacks import register_callbacks +from configs.base.vlm.defaults.checkpointer import register_checkpoint, register_ckpt_type +from configs.base.vlm.defaults.config import Config +from configs.base.vlm.defaults.dataloader import register_data_debug +from configs.base.vlm.defaults.dataloader_weighted_url import ( + register_data_recipe, + register_data_weighted_url, + register_data_weighted_url_with_text, +) +from configs.base.vlm.defaults.model import register_model +from configs.base.vlm.defaults.optimizer import register_optimizer, register_scheduler +from configs.base.vlm.defaults.vlm_policy import register_vlm_policy + + +def make_config() -> Config: + c = Config( + model=None, + optimizer=None, + scheduler=None, + dataloader_train=None, + dataloader_val=None, + ) + + # Specifying values through instances of attrs + c.job.project = "cosmos_reason2" + c.job.group = "debug" + c.job.name = "delete_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + # Unified path: ImaginaireTrainer drives both VLM and VFM. + c.trainer.type = ImaginaireTrainer + c.trainer.straggler_detection.enabled = False + c.trainer.max_iter = 400_000 + c.trainer.logging_iter = 20 + c.trainer.validation_iter = 100 + c.trainer.run_validation = False + c.trainer.callbacks = None + c.trainer.cudnn.benchmark = False + c.upload_reproducible_setup = True + + # Call this function to register config groups for advanced overriding. the order follows the default config groups + register_model() + register_vlm_policy() + # Register dataloader configs + register_data_weighted_url() + register_data_recipe() + register_data_weighted_url_with_text() + register_data_debug() + log.info("Registering optimizer, scheduler, checkpoint, ckpt type, and callbacks") + register_optimizer() + register_scheduler() + register_checkpoint() + register_ckpt_type() + register_callbacks() + import_all_modules_from_package("experiments.vlm", reload=True) + return c diff --git a/cosmos_training/configs/base/vlm/defaults/__init__.py b/cosmos_training/configs/base/vlm/defaults/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/configs/base/vlm/defaults/__pycache__/__init__.cpython-312.pyc b/cosmos_training/configs/base/vlm/defaults/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d01d89b0acfa3e0a74fa56c8cbaa7bde226506ac GIT binary patch literal 244 zcmZ8b%MHRX3~eY#2&sc`!3!WpV1=l(Y1Jx?D?0&o4i3z~ER4YfsGNGjktheA^!$19 zv;D}jO(F>IRU7j&hJTDpQEthQY}N6e&_cQ~&P-RZfjt?h_yRqWuh2HgP#|}bdcxKe ze(v=tXQMskjcZ&DfekSXxvL}n*QBvGjxkVSX65H7&{a*%5cj~@c2Jrs4^|FnEjzu| ayMCC~`wq)s&4E_(qY`o~Ua3-wQmH;f8%k&Z literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/vlm/defaults/__pycache__/training.cpython-312.pyc b/cosmos_training/configs/base/vlm/defaults/__pycache__/training.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7041b72bb39040efcf6f59f7fb4d866d71989448 GIT binary patch literal 4546 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 UJB#R{M>Yw76uqZFPoB$v0jj4w=>Px# literal 0 HcmV?d00001 diff --git a/cosmos_training/configs/base/vlm/defaults/callbacks.py b/cosmos_training/configs/base/vlm/defaults/callbacks.py new file mode 100644 index 00000000..b9787876 --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/callbacks.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dataloader config options. +Based on projects/cosmos/ar/v1/configs/registry.py +""" + +from hydra.core.config_store import ConfigStore + +from cosmos.callbacks.manual_gc import ManualGarbageCollection +from cosmos.utils.lazy_config import PLACEHOLDER +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.callback import WandBCallback +from cosmos.callbacks.data_stats import DataStatsCallback +from cosmos.callbacks.dataloader_state import DataLoaderStateCallback +from cosmos.callbacks.dataloading_monitor import DetailedDataLoadingSpeedMonitor +from cosmos.callbacks.grad_clip import GradClip +from cosmos.callbacks.hf_export import HFExportCallback +from cosmos.callbacks.learning_rate_logger import LearningRateLogger +from cosmos.callbacks.low_precision import LowPrecisionCallback +from configs.base.defaults.callbacks import JOB_MONITOR_CALLBACKS + +# from cosmos.utils.callback import NVTXCallback + + +def register_callbacks(): + cs = ConfigStore.instance() + BASIC_CALLBACKS = dict( + iter_speed=L(IterSpeed)( # does not use model or optimizer + every_n="${trainer.logging_iter}", + save_s3="${upload_reproducible_setup}", + save_s3_every_log_n=500, + hit_thres=50, + ), + manual_gc=L(ManualGarbageCollection)(every_n=5), # does not use model or optimizer + wandb=L(WandBCallback)(), + param_count=L(ParamCount)( # use model + save_s3="${upload_reproducible_setup}", + ), + dataloader_speed=L(DetailedDataLoadingSpeedMonitor)( + every_n=100, + save_s3="${upload_reproducible_setup}", + ), + grad_clip=L(GradClip)(clip_norm=1.0, force_finite=False), # use model + learning_rate_logger=L(LearningRateLogger)(every_n=10), + low_precision=L(LowPrecisionCallback)( + update_iter=1, + config=PLACEHOLDER, + trainer=PLACEHOLDER, + ), # reads model.precision; no extra kwarg needed + + # nvtx=L(NVTXCallback)(synchronize=True), + ) + + PER_DATASET_PERN_CALLBACKS = dict( + wandb_10x=L(WandBCallbackMultiplier)( + logging_iter_multipler=10, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_2x=L(WandBCallbackMultiplier)( + logging_iter_multipler=2, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + data_stats=L(DataStatsCallback)( + logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_val=L(WandBCallbackEval)( + save_s3="${upload_reproducible_setup}", + ), + ) + + SIMPLE_LOG_CALLBACKS = dict( + wandb_10x=L(WandBCallbackMultiplierSimple)( + logging_iter_multipler=10, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + wandb_2x=L(WandBCallbackMultiplierSimple)( + logging_iter_multipler=2, + save_logging_iter_multipler=1, + save_s3="${upload_reproducible_setup}", + ), + log_tensor_shape=L(LogTensorShapeCallback)(num_log=10), + dataloader_state=L(DataLoaderStateCallback)( + distributor_type="${data_setting.distributor_type}", + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="basic_vlm", node=BASIC_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="per_dataset", node=PER_DATASET_PERN_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="simple_log", node=SIMPLE_LOG_CALLBACKS) + cs.store(group="callbacks", package="trainer.callbacks", name="job_monitor", node=JOB_MONITOR_CALLBACKS) + + DATA_VIS_CALLBACKS_QWEN = dict( + wandb_vis=L(VisualizationLoggingCallback)( + every_n=500, + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="data_vis_qwen", node=DATA_VIS_CALLBACKS_QWEN) + + HF_EXPORT_CALLBACKS = dict( + hf_export=L(HFExportCallback)( + dtype="${model.config.policy.parallelism.precision}", + ), + ) + cs.store(group="callbacks", package="trainer.callbacks", name="hf_export", node=HF_EXPORT_CALLBACKS) diff --git a/cosmos_training/configs/base/vlm/defaults/checkpointer.py b/cosmos_training/configs/base/vlm/defaults/checkpointer.py new file mode 100644 index 00000000..56027666 --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/checkpointer.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from hydra.core.config_store import ConfigStore + +from cosmos.utils import config +from cosmos.checkpoint.dummy import Checkpointer as DummyCheckpointer +from cosmos.utils.config import CheckpointConfig +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.checkpoint.dcp import DistributedCheckpointer + +local_object_store = config.ObjectStoreConfig( + enabled=False, +) + +pdx_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/pdx_vfm_checkpoint.secret", + bucket="checkpoints", +) + +s3_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training.secret", + bucket="checkpoints-us-east-1", +) + +s3_eu_object_store = config.ObjectStoreConfig( + enabled=True, + credentials="credentials/s3_training_eu.secret", + bucket="checkpoints-eu-west-3", +) + +CHECKPOINT_LOCAL = CheckpointConfig( + save_to_object_store=local_object_store, + load_from_object_store=local_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_PDX = CheckpointConfig( + save_to_object_store=pdx_object_store, + load_from_object_store=pdx_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + +CHECKPOINT_S3 = CheckpointConfig( + save_to_object_store=s3_object_store, + load_from_object_store=s3_object_store, + save_iter=5000, + broadcast_via_filesystem=True, + dcp_async_mode_enabled=True, +) + + +def register_checkpoint() -> None: + cs = ConfigStore.instance() + cs.store(group="checkpoint", package="checkpoint", name="local", node=CHECKPOINT_LOCAL) + cs.store(group="checkpoint", package="checkpoint", name="pdx", node=CHECKPOINT_PDX) + cs.store(group="checkpoint", package="checkpoint", name="s3", node=CHECKPOINT_S3) + + +DUMMY_CHECKPOINTER: Dict[str, str] = L(DummyCheckpointer)() +DISTRIBUTED_CHECKPOINTER: Dict[str, str] = L(DistributedCheckpointer)() + + +def register_ckpt_type() -> None: + cs = ConfigStore.instance() + cs.store(group="ckpt_type", package="checkpoint.type", name="dummy", node=DUMMY_CHECKPOINTER) + cs.store(group="ckpt_type", package="checkpoint.type", name="dcp", node=DISTRIBUTED_CHECKPOINTER) diff --git a/cosmos_training/configs/base/vlm/defaults/config.py b/cosmos_training/configs/base/vlm/defaults/config.py new file mode 100644 index 00000000..bb4a04ed --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/config.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, List + +import attrs + +from cosmos.utils import config +from configs.base.vlm.defaults.training import PolicyConfig, TrainConfig + + +@attrs.define(slots=False) +class DataSetting: + """Configuration for data. + + Attributes: + qwen_max_video_token_length: Maximum video token length. + qwen_target_fps: Target fps for video sampling. + text_chat_order: Order of text items in user messages. + distributor_type: "with_replace" (WeightedShardlistBasic) or "no_replace" (NoReplaceShardlistBasic). + distributor_seed: Seed for the distributor. + """ + + qwen_max_video_token_length: int = 8192 + qwen_max_image_token_length: int = 8192 + qwen_target_fps: float = 4.0 + text_chat_order: str = attrs.field( + default="text_end", + validator=attrs.validators.in_({"text_end", "text_start", "random"}), + ) + temporal_localization_output_format: str = attrs.field( + default="random", + validator=attrs.validators.in_({"dense_video_caption", "temporal_localization", "temporal_caption", "random"}), + ) + temporal_localization_fps: float = 1.0 + # For packed dataset + max_batch_size: int = 1 + max_tokens: int = 16000 + # "with_replace" (WeightedShardlistBasic) or "no_replace" (NoReplaceShardlistBasic). + distributor_type: str = attrs.field( + default="with_replace", + validator=attrs.validators.in_({"with_replace", "no_replace"}), + ) + distributor_seed: int = 1993 + webdataset_detshuffle: bool = False + num_data_workers: int = 8 + data_prefetch_factor: int = 1 + val_split_ratio: float = 0.0 + + +@attrs.define(slots=False) +class Config(config.Config): + train: TrainConfig = TrainConfig() + policy: PolicyConfig = PolicyConfig() + data_setting: DataSetting = DataSetting() + defaults: List[Any] = attrs.field( + factory=lambda: [ + "_self_", + {"model": "vlm_fsdp"}, + {"vlm_policy": None}, + {"data_train": None}, + {"data_val": None}, + {"optimizer": "fusedadamw"}, + {"scheduler": "warmup_cosine_lr"}, + {"checkpoint": "s3"}, + {"ckpt_type": "dcp"}, + {"callbacks": ["basic_vlm"]}, + {"experiment": None}, + ] + ) diff --git a/cosmos_training/configs/base/vlm/defaults/dataloader.py b/cosmos_training/configs/base/vlm/defaults/dataloader.py new file mode 100644 index 00000000..47a22104 --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/dataloader.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from torch.utils.data import DataLoader + +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.config_helper import ConfigStore +from cosmos.data.vfm.vlm.collate_fn import custom_collate +from cosmos.data.vfm.vlm.debug_data_qwen import DebugQwenDataset +from cosmos.data.vfm.vlm.dummy_data_qwen import DummyQwenDataset +from cosmos.data.vfm.processors import build_processor + + +# Debug dataset +def create_debug_dataloader_config_qwen( + num_images, loss_on_completion_only: bool = True, use_dummy_image: bool = False +): + return L(DataLoader)( + dataset=L(DebugQwenDataset)( + tokenizer=L(build_processor_lazy)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", + ), + num_images=num_images, + seq_len="${model.config.policy.model_max_length}", + image_token_len="${model.config.policy.qwen_max_video_token_length}", + # use_dummy_image=use_dummy_image, + ), + num_workers=8, + prefetch_factor=4, + batch_size=1, + sampler=None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ) + + +def create_dummy_dataloader_config_qwen(): + return L(DataLoader)( + dataset=L(DummyQwenDataset)( + tokenizer=L(build_processor_lazy)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", + ), + num_visual_tokens="${model.config.policy.qwen_max_video_token_length}", + total_tokens="${model.config.policy.model_max_length}", + batch_size="${dataloader_train.batch_size}", + ), + num_workers=8, + prefetch_factor=4, + batch_size=1, + sampler=None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ) + + +def register_data_debug(): + cs = ConfigStore.instance() + for split in ["train", "val"]: + cs.store( + group=f"data_{split}", + package=f"dataloader_{split}", + name="debug_image_data_qwen", # This data is from pixtral model output, expected to have low loss ~1.4 + node=create_debug_dataloader_config_qwen(1), + ) + cs.store( + group=f"data_{split}", + package=f"dataloader_{split}", + name="dummy_image_data_qwen", + node=create_dummy_dataloader_config_qwen(), + ) + + +def register_data(): + register_data_debug() diff --git a/cosmos_training/configs/base/vlm/defaults/dataloader_weighted_url.py b/cosmos_training/configs/base/vlm/defaults/dataloader_weighted_url.py new file mode 100644 index 00000000..3ec45ebb --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/dataloader_weighted_url.py @@ -0,0 +1,570 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.data.vfm.augmentors.vlm.bytes_to_media import BytesToMedia +from cosmos.data.vfm.augmentors.vlm.filter_output_key import FilterOutputKey +from cosmos.data.vfm.augmentors.vlm.filter_seq_length import FilterSeqLength +from cosmos.data.vfm.augmentors.vlm.floating_number_format import FloatingNumberFormat +from cosmos.data.vfm.augmentors.vlm.format_describe_anything import FormatDescribeAnything +from cosmos.data.vfm.augmentors.vlm.prompt_format import PromptFormat +from cosmos.data.vfm.augmentors.vlm.shuffle_text_media_order import ShuffleTextMediaOrder +from cosmos.data.vfm.augmentors.vlm.timestamp import TimeStamp +from cosmos.data.vfm.augmentors.vlm.timestamp_with_subject_tracking import ( + TimeStampWithSubjectTracking, +) +from cosmos.data.vfm.augmentors.vlm.timestamp_without_augment_message import ( + TimeStampWithoutAugmentMessage, +) +from cosmos.data.vfm.augmentors.vlm.timestamp_without_end_time import TimeStampWithoutEndTime +from cosmos.data.vfm.augmentors.vlm.tokenize_data import TokenizeData +from cosmos.data.vfm.vlm.collate_fn import custom_collate +from cosmos.data.vfm.vlm.dataset_provider_sft import get_vlm_dataset +from cosmos.data.vfm.vlm.distributor_with_weight import ( + NoReplaceShardlistBasic, + WeightedShardlistBasic, +) +from cosmos.data.vfm.vlm.joint_dataloader import IterativeJointDataLoader +from cosmos.data.vfm.vlm.joint_dataset_dynamic_batch_webloader import ( + JointDatasetDynamicBatchingWebLoader, +) +from cosmos.data.vfm.processors import build_processor + + +def create_distributor_config( + distributor_type: str, + data_weight_dict: dict, + url_to_category_fn, + shuffle: bool = True, + split_by_node: bool = False, + split_by_worker: bool = False, + resume_flag: bool = False, + verbose: bool = True, + is_infinite_loader: bool = True, + seed: int = 1993, + subsample_config: dict | None = None, + split: str = "train", +): + """ + Return a LazyCall to the distributor class based on distributor_type. + + Args: + distributor_type: "with_replace" -> WeightedShardlistBasic, "no_replace" -> NoReplaceShardlistBasic + data_weight_dict: category -> weight (or repetitions) mapping + url_to_category_fn: maps URL path to category key + split: "train" or "val" (used by NoReplaceShardlistBasic) + Other args: passed to the distributor constructor. + + Returns: + L(WeightedShardlistBasic)(...) or L(NoReplaceShardlistBasic)(...) — a LazyCall to the distributor class. + """ + common = dict( + data_weight_dict=data_weight_dict, + url_to_category_fn=url_to_category_fn, + shuffle=shuffle, + split_by_node=split_by_node, + split_by_worker=split_by_worker, + resume_flag=resume_flag, + verbose=verbose, + is_infinite_loader=is_infinite_loader, + subsample_config=subsample_config, + split=split, + ) + if distributor_type == "with_replace": + return L(WeightedShardlistBasic)(**common) + if distributor_type == "no_replace": + return L(NoReplaceShardlistBasic)(seed=seed, **common) + + raise ValueError(f"distributor_type must be in ['with_replace', 'no_replace'], got {distributor_type!r}") + + +def create_data_augmentor_config(): + return { + "bytes_to_media": L(BytesToMedia)( + input_key="media", + output_key="media", + min_fps_thres=2, + max_fps_thres=60, + target_fps="${data_setting.qwen_target_fps}", # type: ignore + max_video_token_length="${data_setting.qwen_max_video_token_length}", # type: ignore + processor=processor, + is_input_pickle_byptes=False, # If True, it means the input "media" is pickled bytes that needs to be unpickled first; if False, it means the input "media" is raw bytes that can be directly decoded to image/video. Set to False for most cases, and only set to True for some special datasets where media is stored as pickled bytes. + ), # takes "videos" and output "videos" + "prompt_format": L(PromptFormat)( # takes text_keys and output "conversation" + input_keys=["texts"], + text_chat_order="${data_setting.text_chat_order}", + ), + "shuffle_text_media_order": L(ShuffleTextMediaOrder)(), + # ============================ + # TL data augmentation + # ============================ + "timestamp": L(TimeStamp)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="temporal_localization", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "av_reasoning_localization_20250627", + "tl_activitynet_20250630", + "tl_agibot_fisheye_20250630", + "tl_2dvlm_20250627", + "tl_2dvlm_20251121", + "tl_youcook2_20250716", + "tl_yt_cctv_warehouse_20250724", + ], + processor=processor, + ), + "TL_recaption": L(TimeStamp)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="caption", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_2dvlm_recaption_20251121", + "tl_2dvlm_recaption_20250627", + ], + processor=processor, + ), + # Special augmentors: + # timestamp_without_end_time: nexar data does not contain end time + # timestamp_with_subject_trackig: plm data has subject id + mask, and it's video data + # format_describe_anything: dam data has subject id + mask + category label, and it's image data (does not need timestampt) + # timestamp_without_augment_message: rft tl data require timestamp augmentation to video, but keep original text + "timestamp_without_end_time": L(TimeStampWithoutEndTime)( + input_key="media", + # output_format="${data_setting.temporal_localization_output_format}", + output_format="temporal_localization", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_nexar_20250708", + "mimicgen_temporal_localization", + ], + processor=processor, + ), + "timestamp_with_subject_trackig": L(TimeStampWithSubjectTracking)( + input_key="media", + output_format="temporal_location_subject", # Only use temporal_localization tasks to keep the caption style of base model + urls_needs_timestamp=[ + "tl_plm_sav_20250714", + ], + processor=processor, + ), + "floating_number_format": L(FloatingNumberFormat)( + input_key="conversation", + decimal_places=2, + urls_needs_format=[ + "3d_grounding_av", + ], + ), + "format_describe_anything": L(FormatDescribeAnything)( + input_key="media", + urls_needs_timestamp=[ + "describe-anything-dataset", + ], + ), + "timestamp_without_augment_message": L(TimeStampWithoutAugmentMessage)( + input_key="media", + output_format="${data_setting.temporal_localization_output_format}", + urls_needs_timestamp=[ + "rl_distill_tl_0729", + ], + processor=processor, + ), + # ============================ + # End of TL data augmentation + # ============================ + "tokenize_data": L(TokenizeData)( + processor=processor, + max_video_token_length="${data_setting.qwen_max_video_token_length}", + max_image_token_length="${data_setting.qwen_max_image_token_length}", + add_system_prompt_if_missing=True, + text_only=False, + ), + "filter_output_keys": L(FilterOutputKey)( + text_only=False, + ), + "filter_seq_length": L(FilterSeqLength)( + max_token_length="${data_setting.max_tokens}", + processor=processor, + ), + } + + +processor = L(build_processor_lazy)( + tokenizer_type="${model.config.policy.model_name_or_path}", + credentials="${checkpoint.load_from_object_store.credentials}", + bucket="${checkpoint.load_from_object_store.bucket}", +) + + +def get_vlm_dataset_from_module( + data_module: str, + split: str = "train", + distributor_split: str = "train", + object_store: str = "s3", + augmentor_config: dict | None = None, + distributor_type: str = "with_replace", + distributor_seed: int = 1993, + buffer_size: int = 2, + detshuffle: bool = False, +): + """Resolve data module at instantiation time instead of config registration time. + + This defers importlib.import_module to when the config is actually used (training time), + avoiding the ~10+ minute startup penalty from eagerly importing all registered dataset + modules during Hydra config store population. + """ + data_weight_attr = data_module.split(".")[-1] + module_path = ".".join(data_module.split(".")[:-1]) + data_weight_module = importlib.import_module(module_path) + + full_datainfo = data_weight_module.DATAINFO + data_weight_dict = getattr(data_weight_module, data_weight_attr) + url_to_category = data_weight_module.url_to_category + subsample_config = getattr(data_weight_module, "subsample_config", None) + + distributor_config = create_distributor_config( + distributor_type=distributor_type, + data_weight_dict=data_weight_dict, + url_to_category_fn=url_to_category, + split=distributor_split, + seed=distributor_seed, + subsample_config=subsample_config, + ) + + return get_vlm_dataset( + full_datainfo=full_datainfo, + url_to_category_fn=url_to_category, + buffer_size=buffer_size, + object_store=object_store, + data_weight_dict=data_weight_dict, + split=split, + augmentor_config=augmentor_config, + distributor_config=distributor_config, + detshuffle=detshuffle, + ) + + +def create_dataloader_config( + data_module: str, + split: str = "train", + distributor_split: str = "train", + object_store: str = "s3", +): + """Create a lazy dataloader config that defers dataset module import to instantiation time. + + Args: + data_module: Full dotted path to the data weight dict, e.g. + "cosmos.data.vfm.data_sources.vlm.data_sources_nanov2.data_weight.stage_1.data_weight_repeat". + The module should export DATAINFO, url_to_category, and the named weight dict. + split: Dataset split ("train" or "val"). + distributor_split: Distributor split for train/val sharding. + object_store: Object store backend ("s3", "s3_vlmdb", "pdx", "neb_eu"). + + Returns: + L(get_vlm_dataset_from_module): a LazyCall that resolves the module at instantiation time. + """ + return L(get_vlm_dataset_from_module)( + data_module=data_module, + split=split, + distributor_split=distributor_split, + object_store=object_store, + augmentor_config=create_data_augmentor_config(), + distributor_type="${data_setting.distributor_type}", + distributor_seed="${data_setting.distributor_seed}", + detshuffle="${data_setting.webdataset_detshuffle}", + ) + + +def register_data_weighted_url(): + cs = ConfigStore.instance() + # This will register dataset: + # reason1_v01_understanding_only_pdx + # reason1_v01_understanding_only_s3 + # reason1_v01_understanding_only_neb_eu + # eagle_v01_sft_no_text_only_pdx + # eagle_v01_sft_no_text_only_s3 + # eagle_v01_sft_no_text_only_neb_eu + # eagle_v02_grounding_2d_pdx + # eagle_v02_grounding_2d_s3 + # eagle_v02_grounding_2d_neb_eu + # eagle_v03_grounding_2d_v1_2_pdx + # eagle_v03_grounding_2d_v1_2_s3 + # eagle_v03_grounding_2d_v1_2_neb_eu + # eagle_v04_sft_no_text_only_no_grounding_2d_pdx + # eagle_v04_sft_no_text_only_no_grounding_2d_s3 + # eagle_v04_sft_no_text_only_no_grounding_2d_neb_eu + # joint_v01_cr1_understanding_eagle_sft_pdx + # joint_v01_cr1_understanding_eagle_sft_s3 + # joint_v01_cr1_understanding_eagle_sft_neb_eu + for dataset_id, data_module in { + "01": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.understanding_only.data_weight_default", # 01_reason1_understanding_only_default_s3 + "02": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full.data_weight_default", # 02_eagle_sft_full_default_s3 + "03": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.grounding_2d_v1_1.data_weight_default", # 03_eagle_grounding_2d_v1_1_default_s3 + "04": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.grounding_2d_v1_2.data_weight_default", # 04_eagle_grounding_2d_v1_2_default_s3 + "05": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_full_5v5", # 05_joint_cr1_eagle_sft_full_5v5_s3 + "06": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_2d_5v5", # 06_joint_cr1_eagle_sft_2d_5v5_s3 + "07": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.pretrain.data_weight_default", # 07_eagle_pretrain_default_s3 + "08": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_default", # 08_eagle_sft_full_mul_repeat_default_s3 + "09": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_debug", # 09_eagle_sft_full_mul_repeat_debug_s3 + "10": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.cr1_eagle_sft.data_weight_full_5v5_mul_repeat", # 10_joint_cr1_eagle_sft_full_5v5_mul_repeat_s3 + "11": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_single", # 11_eagle_sft_full_mul_repeat_single_s3 + "12": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.understanding_only.data_weight_default", # 12_reason1_understanding_only_default_s3 + "13": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1.data_weight_mix_5v5", # 13_reason1p0_1p1_mix_5v5_s3 + "14": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason1_2.data_weight_mix_5v5v5", # 14_joint_reason1_2_mix_5v5v5_s3 + "15": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1.data_weight_debug_tl", # 15_reason1_reason1p0_1p1_debug_tl_s3 + "16": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason1_2.data_weight_mix_all_zero", # 16_joint_reason1_2_mix_all_zero_s3 + "17": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_only_vatex_subset", # 17_eagle_sft_full_mul_repeat_only_vatex_subset_s3 + "18": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason1_2.data_weight_understand", # 18_joint_reason1_2_data_weight_understand + "19": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_mix_5v5", # 19_reason1_reason1p0_1p1_721_mix_5v5_s3 + "20": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_debug_2dvlm", # 20_reason1_reason1p0_1p1_721_debug_2dvlm_s3 + "21": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_debug_av", # 21_reason1_reason1p0_1p1_721_debug_av_s3 + "22": "cosmos.data.vfm.data_sources.vlm.data_sources_reason1.data_weight.reason1p0_1p1_721.data_weight_no_rft", # 22_reason1_reason1p0_1p1_721_no_rft_s3 + "23": "cosmos.data.vfm.data_sources.vlm.data_sources_reason2.data_weight.reason2.data_weight_all_zero", # 23_reason2_reason2_all_zero_s3 + # 24: Reason 2 data count 5% of total + "24": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason1p0_1p1_2_721.data_weight_mix_475v475v005", # 24_joint_reason1p0_1p1_2_721_mix_475v475v005_s3 + "25": "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.grounding_2d_v1_1.data_weight_no_robospatial", # 25_eagle_grounding_2d_v1_1_no_robospatial_s3 + # via exploration + "26": "cosmos.data.vfm.data_sources.vlm.data_sources_via.data_weight.default.data_weight_spatial_suc_only", # 26_via_default_spatial_suc_only_s3 + "27": "cosmos.data.vfm.data_sources.vlm.data_sources_via.data_weight.default.data_weight_spatial_suc_only_round4", # 27_via_default_spatial_suc_only_round4_s3 + "28": "cosmos.data.vfm.data_sources.vlm.data_sources_via.data_weight.default.data_weight_90_suc_only_round2", # + "29": "cosmos.data.vfm.data_sources.vlm.data_sources_via.data_weight.default.data_weight_all_zeros", # 29_via_default_all_zeros_s3 + # reason2 release + "54": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2_release.data_weight_joint", # 54_joint_reason2_release_joint_s3 + "55": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2_release.data_weight_with_recaption", # 55_joint_reason2_release_joint_with_recaption_s3 + "56": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2_release.data_weight_with_recaption_wo_human", # 56_joint_reason2_release_joint_with_recaption_wo_human_s3 + "57": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2p0_2p1.data_weight_joint", # 57_joint_reason2p0_2p1_joint_s3 + "101": "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2_release.data_weight_debug_recaption", # 101_joint_reason2_release_debug_recaption_s3 + # # taxonomy distillation + # "100": "cosmos.data.vfm.data_sources.vlm_taxonomy_distill.data_weight.taxonomy_distill.data_weight_default", # 100_taxonomy_distill_taxonomy_distill_default_s3 + # # interleave document scoring distillation + # "102": "cosmos.data.vfm.data_sources.vlm_interleave_scoring.data_weight.interleave_scoring.data_weight_default", # 102_interleave_scoring_interleave_scoring_default_s3 + # video taxonomy distillation + "103": "cosmos.data.vfm.data_sources.vlm.data_sources_video_taxonomy.data_weight.video_taxonomy.data_weight_default", # 103_video_taxonomy_video_taxonomy_default_s3 + # nanov2 pre/post-training + "200": "cosmos.data.vfm.data_sources.vlm.data_sources_nanov2.data_weight.stage_1_0218_34m_uniform_pretrain.data_weight_repeat", # 200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb + "201": "cosmos.data.vfm.data_sources.vlm.data_sources_nanov2.data_weight.stage_1_0218_34m_uniform_posttrain.data_weight_repeat", # 201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb + # Data ablation configs (below is a dummy example, do not uncomment) + "202": "cosmos.data.vfm.data_sources.vlm.data_sources_nanov2.data_weight.new_category_data_mixture.data_weight_repeat", # 202_nanov2_new_category_data_mixture_repeat_s3_vlmdb + }.items(): + data_source_name = data_module.split("data_sources_")[-1].split(".")[0] + dataset_file_name = data_module.split(".")[-2] + data_weight_name = data_module.split("data_weight_")[-1] + for distributor_split in ["train", "val"]: + for object_store in ["pdx", "s3", "s3_vlmdb", "neb_eu"]: + dataset_name = f"{dataset_id}_{data_source_name}_{dataset_file_name}_{data_weight_name}_{object_store}" + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=dataset_name, + node=L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers="${data_setting.num_data_workers}" if distributor_split == "train" else 0, + sampler=None, + prefetch_factor="${data_setting.data_prefetch_factor}" + if distributor_split == "train" + else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + ) + + +def register_data_weighted_url_with_text(): + cs = ConfigStore.instance() + + # This will register dataset: + + for dataset_id, data_modules in { + "m01": { + "with_visual": ( + 5, + "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_default", + ), + "text_only": ( + 1, + "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_text_only", + ), + }, # m01_visual_5_mix_text_1__eagle_sft_full_mul_repeat_default_s3 + "m02": { + "with_visual": ( + 5, + "cosmos.data.vfm.data_sources.vlm.data_sources_joint.data_weight.reason2p0_2p1.data_weight_joint", + ), + "text_only": ( + 1, + "cosmos.data.vfm.data_sources.vlm.data_sources_eagle.data_weight.sft_full_mul_repeat.data_weight_text_only", + ), + }, # m02_visual_5_mix_text_1__joint_reason2p0_2p1_joint_s3 + }.items(): + ratio_with_visual, data_module_with_visual = data_modules["with_visual"] + ratio_text_only, data_module_text_only = data_modules["text_only"] + data_source_name = data_module_with_visual.split("data_sources_")[-1].split(".")[0] + dataset_file_name = data_module_with_visual.split(".")[-2] + data_weight_name = data_module_with_visual.split("data_weight_")[-1] + + for distributor_split in ["train", "val"]: + for object_store in ["pdx", "s3", "neb_eu"]: + dataset_name = f"{dataset_id}_visual_{ratio_with_visual}_mix_text_{ratio_text_only}__{data_source_name}_{dataset_file_name}_{data_weight_name}_{object_store}" + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=dataset_name, + node=L(IterativeJointDataLoader)( + dataloaders={ + "with_visual": { + "ratio": ratio_with_visual, + "dataloader": L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module_with_visual, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers="${data_setting.num_data_workers}" + if distributor_split == "train" + else 0, + sampler=None, + prefetch_factor="${data_setting.data_prefetch_factor}" + if distributor_split == "train" + else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + }, + "text_only": { + "ratio": ratio_text_only, + "dataloader": L(JointDatasetDynamicBatchingWebLoader)( + datasets_cfg={ + "default": { + "dataset": create_dataloader_config( + data_module=data_module_text_only, + split="train", + distributor_split=distributor_split, + object_store=object_store, + ), + "ratio": 1, + } + }, + # Arguments for the joint dataset + pool_size=16, + max_batch_size="${data_setting.max_batch_size}", + max_tokens="${data_setting.max_tokens}", + model_name_or_path="${model.config.policy.model_name_or_path}", # "Qwen/Qwen3-VL-2B-Init" + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + # Arguments for the webloader + batch_size=1, # This is not the real batch size, it wont be used + num_workers=2, + sampler=None, + prefetch_factor=1, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + }, + } + ), + ) + + +def register_data_recipe(): + from cosmos.data.vfm.vlm.recipe_dataloader import VLMRecipeDataLoader + + cs = ConfigStore.instance() + # This will register recipe-based dataloaders using VLMRecipeDataLoader. + # Recipe names and storage types are stored in the PostgreSQL recipe database. + # Registered configs: + # cosmos_reason2_s3_vlmdb + for recipe_name, storage_type in [ + ("cosmos_reason2_s3_vlmdb", "s3_vlmdb"), # cosmos_reason2_s3_vlmdb_recipe + ( + "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb", + "s3_vlmdb", + ), # nemotron_nanov2_stage_1_0218_34m_uniform_s3_vlmdb__v1_recipe + ( + "nemotron_nanov2_stage_1_0218_34m_uniform_posttrain_s3_vlmdb", + "s3_vlmdb", + ), # nemotron_nanov2_stage_1_0218_34m_uniform_posttrain_s3_vlmdb__v1_recipe + ]: + config_name = f"{recipe_name.replace('/', '__')}_recipe" + for distributor_split in ["train", "val"]: + cs.store( + group=f"data_{distributor_split}", + package=f"dataloader_{distributor_split}", + name=config_name, + node=L(VLMRecipeDataLoader)( + recipe_name=recipe_name, + storage_type=storage_type, + model_name_or_path="${model.config.policy.model_name_or_path}", + max_tokens="${data_setting.max_tokens}", + max_batch_size="${data_setting.max_batch_size}", + pool_size=16, + long_threshold=6400, + length_key="input_ids", + batching_strategy="prefer_closest", + augmentor_config=create_data_augmentor_config(), + distributor_type="${data_setting.distributor_type}", + detshuffle="${data_setting.webdataset_detshuffle}", + split="train", # use train split of the dataset + distributor_split=distributor_split, # split training dataset into train and val splits + val_split_ratio="${data_setting.val_split_ratio}", + distributor_seed="${data_setting.distributor_seed}", + num_workers="${data_setting.num_data_workers}" if distributor_split == "train" else 0, + prefetch_factor="${data_setting.data_prefetch_factor}" if distributor_split == "train" else None, + persistent_workers=False, + pin_memory=True, + collate_fn=custom_collate, + ), + ) diff --git a/cosmos_training/configs/base/vlm/defaults/model.py b/cosmos_training/configs/base/vlm/defaults/model.py new file mode 100644 index 00000000..d97feb0c --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/model.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyCall as L +from configs.base.defaults.model_config import VLMModelConfig +from cosmos.model.vfm.vlm_model import VLMModel + +VLM_FSDP_CONFIG = dict( + trainer=dict( + distributed_parallelism="fsdp", + ), + model=L(VLMModel)( + config=VLMModelConfig(), + checkpoint="${checkpoint}", + ), +) + + +def register_model(): + cs = ConfigStore.instance() + cs.store(group="model", package="_global_", name="vlm_fsdp", node=VLM_FSDP_CONFIG) diff --git a/cosmos_training/configs/base/vlm/defaults/optimizer.py b/cosmos_training/configs/base/vlm/defaults/optimizer.py new file mode 100644 index 00000000..3900beca --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/optimizer.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.utils.lazy_config import PLACEHOLDER +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.config_helper import ConfigStore +from cosmos.utils.vfm.vlm.optimizer import OptimizerConfig, build_lr_schedulers, build_optimizers + + +def register_optimizer(): + cs = ConfigStore.instance() + cs.store( + group="optimizer", + package="optimizer", + name="fusedadamw", + node=L(build_optimizers)( + model_parts=PLACEHOLDER, + model_part_names=PLACEHOLDER, + config=L(OptimizerConfig)( + name="FusedAdam", + ), + ), + ) + cs.store( + group="optimizer", + package="optimizer", + name="adamw", + node=L(build_optimizers)( + model_parts=PLACEHOLDER, + model_part_names=PLACEHOLDER, + config=L(OptimizerConfig)( + name="AdamW", + ), + ), + ) + + +def register_scheduler(): + cs = ConfigStore.instance() + cs.store( + group="scheduler", + package="scheduler", + name="warmup_cosine_lr", + node=L(build_lr_schedulers)( + optimizers=PLACEHOLDER, + name="warmup_cosine_lr", + warmup_iters=1000, + lr_decay_iters="${trainer.max_iter}", + lr="${optimizer.config.lr}", + init_lr="${optimizer.config.init_lr}", + end_lr="${optimizer.config.end_lr}", + ), + ) diff --git a/cosmos_training/configs/base/vlm/defaults/training.py b/cosmos_training/configs/base/vlm/defaults/training.py new file mode 100644 index 00000000..8e2da5af --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/training.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import MISSING, field +from typing import Union + +import attrs +import torch + +from cosmos.utils.config import make_freezable +from configs.base.defaults.parallelism import ParallelismConfig + + +def skip_ui_field(*, default=MISSING, default_factory=MISSING, **kwargs): + metadata = kwargs.pop("metadata", {}) + metadata["skip_ui"] = True + if default_factory is not MISSING: + return field(default_factory=default_factory, metadata=metadata, **kwargs) + elif default is not MISSING: + return field(default=default, metadata=metadata, **kwargs) + else: + raise ValueError("Must provide either default or default_factory.") + + +@make_freezable +@attrs.define(slots=False) +class TrainPolicyConfig: + mini_batch: int = 1 + type: str = "sft" + + +@make_freezable +@attrs.define(slots=False) +class FP8: + enable_fp8: bool = False + + +@make_freezable +@attrs.define(slots=False) +class TrainConfig: + # Master parameter dtype for FSDP. Activation / model dtype lives on the + # shared ParallelismConfig as policy.parallelism.precision. + master_dtype: str = "float32" + + # The data type for reduction in FSDP + fsdp_reduce_dtype: str = "float32" + + # Whether to offload the model to CPU if using FSDP + fsdp_offload: bool = False + + # Reshard the param after forward pass in FSDP + fsdp_reshard_after_forward: str = "default" + + # The batch size for training per iteration in one replica, this is the local batch size for each gradient accumulation step + train_batch_per_replica: int = 1 + + # The interval of train step for synchronizing weights between replicas. + sync_weight_interval: int = 1 + + # Train policy + train_policy: TrainPolicyConfig = TrainPolicyConfig() + fp8: FP8 = FP8() + deterministic: bool = True + + def key_values(self): + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + + @property + def master_torch_dtype(self): + return { + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float32": torch.float32, + }[self.master_dtype] + + @property + def fsdp_reduce_torch_dtype(self): + return {"float32": torch.float32}[self.fsdp_reduce_dtype] + + +# Why we does not make this freezable? +# Because we need to path the cache model dir as model_name_or_path to the cosmos-rl model to use the +# model weights downloaded from s3. If cosmos-rl support reading model from s3 directly, we can make it freezable. +@attrs.define(slots=False) +class PolicyConfig: + # Parallelism configuration + parallelism: ParallelismConfig = ParallelismConfig() + # The model name or path, compatible with huggingface model name or local path + model_name_or_path: str = "Qwen/Qwen2.5-VL-3B-Instruct" + # The maximum length for training, longer than this will be ignored for training stability + model_max_length: int = 16000 + # The maximum length for video tokens, only applied to qwen model + qwen_max_video_token_length: int = 8000 + + # Pretrain weights (Optional) + pretrain_weights_path_vlm: str = "" + pretrain_weights_path_llm: str = "" + pretrain_weights_path_vit: str = "" + pretrain_weights_cred: str = "credentials/s3_training.secret" + + # Extra model config + lora: Union[str, None] = None + enable_liger_kernel: bool = False + trainable_map: Union[str, None] = None + monkey_patch_for_text_only_data: bool = False diff --git a/cosmos_training/configs/base/vlm/defaults/vlm_policy.py b/cosmos_training/configs/base/vlm/defaults/vlm_policy.py new file mode 100644 index 00000000..578f6d66 --- /dev/null +++ b/cosmos_training/configs/base/vlm/defaults/vlm_policy.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from configs.base.vlm.defaults.training import PolicyConfig + +# Each entry replaces cfg.model.config.policy via package="model.config.policy". +# Sibling to the VFM vlm_config group at +# configs/base/defaults/vlm.py: that group binds +# VLMConfig SKUs onto OmniMoTModelConfig.vlm_config; this group binds +# PolicyConfig SKUs onto VLMModelConfig.policy. The two schemas are kept +# separate today because the loader contracts diverge (VFM uses a +# registry-label + LazyDict model_instance with MoTDecoderLayer +# substitution; VLM uses a literal HF cache path fed to from_pretrained). +# Convergence onto a single SKU schema is tracked as L6 in +# config_unification_plan.v10.md. + +qwen2_5_vl_7b = PolicyConfig(model_name_or_path="Qwen/Qwen2.5-VL-7B-Instruct") + +eagle_er_1p7b = PolicyConfig( + model_name_or_path="eagle_er_qwen3_1p7b_siglip_400m", + model_max_length=16000, +) + +internvl3_5_1b = PolicyConfig( + model_name_or_path="OpenGVLab/InternVL3_5-1B-HF", + model_max_length=16000, # 40960 is the max length by default. +) + +internvl3_5_2b = PolicyConfig( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF", + model_max_length=16000, # 40960 is the max length by default. +) + +qwen3_vl_2b = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Init") + +qwen3_vl_30b_a3b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-30B-A3B-Instruct") + +qwen3_vl_30b_a3b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-30B-A3B-Thinking") + +qwen3_vl_235b_a22b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-235B-A22B-Thinking") + +qwen3_vl_8b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-8B-Thinking") + +qwen3_vl_8b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-8B-Instruct") + +qwen3_vl_2b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Instruct") + +qwen3_vl_2b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-2B-Thinking") + +qwen3_vl_4b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-4B-Instruct") + +qwen3_vl_4b_thinking = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-4B-Thinking") + +qwen3_vl_32b_instruct = PolicyConfig(model_name_or_path="Qwen/Qwen3-VL-32B-Instruct") + +nemotron_nano_12b_v2_vl_bf16 = PolicyConfig(model_name_or_path="nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16") + + +def register_vlm_policy(): + cs = ConfigStore.instance() + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen2_5_vl_7b", + node=qwen2_5_vl_7b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="eagle_er_1p7b", + node=eagle_er_1p7b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="internvl3_5_1b", + node=internvl3_5_1b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="internvl3_5_2b", + node=internvl3_5_2b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b", + node=qwen3_vl_2b, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_30b_a3b_instruct", + node=qwen3_vl_30b_a3b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_30b_a3b_thinking", + node=qwen3_vl_30b_a3b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_235b_a22b_thinking", + node=qwen3_vl_235b_a22b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_8b_thinking", + node=qwen3_vl_8b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_8b_instruct", + node=qwen3_vl_8b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b_instruct", + node=qwen3_vl_2b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_2b_thinking", + node=qwen3_vl_2b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_4b_instruct", + node=qwen3_vl_4b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_4b_thinking", + node=qwen3_vl_4b_thinking, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="qwen3_vl_32b_instruct", + node=qwen3_vl_32b_instruct, + ) + cs.store( + group="vlm_policy", + package="model.config.policy", + name="nemotron_nano_12b_v2_vl_bf16", + node=nemotron_nano_12b_v2_vl_bf16, + ) diff --git a/cosmos_training/configs/base/vlm/experiment/__init__.py b/cosmos_training/configs/base/vlm/experiment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py b/cosmos_training/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py new file mode 100644 index 00000000..44fcff31 --- /dev/null +++ b/cosmos_training/configs/base/vlm/experiment/pre_exp012_phase2_vlm_smoke.py @@ -0,0 +1,105 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA CORPORATION +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- +"""Phase 2 VFM VLMModel smoke test experiments. + +These configs exercise the Phase 2 HFModel/FSDP2 path end-to-end on 4 GPUs. +They are NOT training runs — max_iter=10 is intentionally minimal. + +Usage: + torchrun --nproc_per_node=4 --master_port=12341 -m scripts.train \\ + --config=configs/base/vlm/config.py \\ + -- experiment=pre_exp012_000_phase2_vlm_smoke_4gpu +""" + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyDict + +cs = ConfigStore.instance() + +# --------------------------------------------------------------------------- +# Smoke test: 4-GPU FSDP2 with qwen3_vl_2b, debug data, 10 iterations. +# Exercises: HFModel meta-init → parallelize() → forward → CE loss → backward +# Requires: trainable_params (Phase 2 mandatory), data_parallel_shard_degree=4 +# --------------------------------------------------------------------------- +pre_exp012_000_phase2_vlm_smoke_4gpu_8b = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /data_val": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="phase2_smoke", + ), + trainer=dict( + max_iter=10, + run_validation=False, + logging_iter=1, + ), + optimizer=dict( + config=dict( + # Phase 2 REQUIRED: trainable_params regex list. + # ".*" trains all parameters — appropriate for smoke test. + trainable_params=[".*"], + lr=1e-5, + fused=True, + ), + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=4, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + # dataloader_train=dict( + # enable_flop_based_batching=True, + # target_runtime_seconds=3.0, # Used when max_batch_size > 1 + # ), + checkpoint=dict( + # Don't save checkpoints during smoke test + save_iter=100000, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + webdataset_detshuffle=True, + ), + ) +) + + +for _item in [ + pre_exp012_000_phase2_vlm_smoke_4gpu_8b, +]: + experiment_name = [name.lower() for name, value in globals().items() if value is _item][0] + if "job" not in _item: + _item["job"] = dict(name=experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}") + else: + _item["job"]["name"] = experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + cs.store(group="experiment", package="_global_", name=experiment_name, node=_item) diff --git a/cosmos_training/configs/base/vlm/experiment/pre_exp01x.py b/cosmos_training/configs/base/vlm/experiment/pre_exp01x.py new file mode 100644 index 00000000..f459a372 --- /dev/null +++ b/cosmos_training/configs/base/vlm/experiment/pre_exp01x.py @@ -0,0 +1,852 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hydra.core.config_store import ConfigStore + +from cosmos.utils.lazy_config import LazyDict + +cs = ConfigStore.instance() + +""" +Bundled VLM training experiments registered under the ``experiment`` group. + +Usage: +torchrun --nproc_per_node=8 --master_port=12341 -m scripts.train \ + --config=configs/base/vlm/config.py \ + -- experiment=pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader +""" + + +pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "eagle_er_1p7b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + max_iter=32000, + ), + checkpoint=dict(save_iter=5000), + ) +) + +pre_exp010_010_internvl3_5_1b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_1b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + ), + checkpoint=dict(save_iter=2000), + ) +) + +pre_exp010_020_internvl3_5_2b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=100)), + max_iter=32000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_000_qwen3_vl_2b +""" +pre_exp011_000_qwen3_vl_2b = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + checkpoint=dict(save_iter=2000), + ) +) + + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_020_qwen3_vl_2b_vit2k8k +""" +pre_exp011_020_qwen3_vl_2b_vit2k8k = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 +""" +pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + max_batch_size=8, + max_tokens=16000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8 +""" +pre_exp011_040_qwen3_vl_2b_vit2k8k_mbs8_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=8, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + checkpoint=dict(save_iter=2000), + ) +) + +pre_exp011_041_qwen3_vl_2b_vit2k8k_mbs8_flop3s_mix_text_only = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=200_000, + ), + optimizer=dict( + config=dict( + lr=5e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=8, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + dataloaders=dict( + with_visual=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + text_only=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + ) + ), + checkpoint=dict(save_iter=2000), + ) +) + + +pre_exp011_100_qwen3_vl_2b_align = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + optimizer=dict( + config=dict( + freeze_vision_encoder=True, + freeze_llm=True, + lr=1e-5, + init_lr=1e-7, + end_lr=5e-6, + ), + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + checkpoint=dict(save_iter=2000), + ) +) + +# reinit the llm and/or projector weights for internvl3_5_2b +pre_exp011_300_internvl3_5_2b_reinit_align = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + optimizer=dict( + config=dict( + freeze_vision_encoder=True, + freeze_llm=True, + lr=1e-5, + init_lr=1e-7, + end_lr=5e-6, + ), + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + model=dict( + config=dict( + policy=dict( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF-ReinitLLMProj", + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + + +# reinit the llm and/or projector weights for internvl3_5_2b +pre_exp011_400_internvl3_5_2b_reinit_e2e = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "07_eagle_pretrain_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "internvl3_5_2b"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + model=dict( + config=dict( + policy=dict( + model_name_or_path="OpenGVLab/InternVL3_5-2B-HF-ReinitLLMProj", + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp014_000_qwen3_vl_8b_instruct_vit2k8k_mbs8_flop3s +- reduce lr to 2e-6 cause this is SFT +""" +pre_exp015_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "dummy_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only data_train=m02_visual_5_mix_text_1__joint_reason2p0_2p1_joint_s3 +""" +pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=10000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + dataloaders=dict( + with_visual=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + text_only=dict( + dataloader=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, + ), + ), + ) + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp014_000_qwen3_vl_8b_instruct_vit2k8k_mbs8_flop3s +- reduce lr to 2e-6 cause this is SFT +""" +pre_exp016_000_qwen3_vl_8b_thinking_vit2k8k_mbs1_flop3s = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "08_eagle_sft_full_mul_repeat_default_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_thinking"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp017_000_nemotron_vl_12b_mbs1 +""" +pre_exp017_000_nemotron_vl_12b_mbs1 = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "51_joint_reason1p0_1p1_2_1126_1126_s3"}, + {"override /data_val": "debug_image_data_qwen"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "nemotron_nano_12b_v2_vl_bf16"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + ), + ), + data_setting=dict( + max_tokens=16000, + max_batch_size=1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + ), + ), + ), + checkpoint=dict(save_iter=2000), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain +""" +pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb"}, + {"override /data_val": "200_nanov2_stage_1_0218_34m_uniform_pretrain_repeat_s3_vlmdb"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=60000, + run_validation=True, + validation_iter=40, + max_val_iter=1, + ), + optimizer=dict( + config=dict( + lr=2e-5, + fused=True, + weight_decay=0.05, + betas=[0.9, 0.999], + freeze_vision_encoder=True, + freeze_mm_projector=True, + ), + ), + scheduler=dict( + warmup_iters=6000, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + monkey_patch_for_text_only_data=True, + pretrain_weights_path_llm="Qwen/Qwen3-8B", + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict( + save_iter=2000, + ), + ) +) + +""" +torchrun --nproc_per_node=8 --master_port=12341 -m projects.cosmos3.vlm.train --config=projects/cosmos3/vlm/configs/base/config.py -- experiment=pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain +""" +pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb"}, + {"override /data_val": "201_nanov2_stage_1_0218_34m_uniform_posttrain_repeat_s3_vlmdb"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_8b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict( + group="debug", + ), + trainer=dict( + callbacks=dict(log_tensor_shape=dict(num_log=2)), + max_iter=8000, + run_validation=True, + validation_iter=40, + max_val_iter=1, + ), + optimizer=dict( + config=dict( + lr=1e-5, + fused=True, + weight_decay=0.05, + betas=[0.9, 0.999], + freeze_vision_encoder=True, + freeze_mm_projector=True, + ), + ), + scheduler=dict( + warmup_iters=800, + ), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1996, + val_split_ratio=0.05, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=8, + data_parallel_replicate_degree=-1, + ), + monkey_patch_for_text_only_data=True, + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict( + save_iter=1000, + load_path="cosmos_reason2/pre_exp015/pre_exp015_288_34m_v1352_repeat_uniform_1_epoch_n64/checkpoints/iter_000060000/", + ), + ) +) + + +pre_exp020_001_qwen3_vl_30b_a3b_instruct_ep = LazyDict( + dict( + defaults=[ + {"override /checkpoint": "s3"}, + {"override /data_train": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /data_val": "nemotron_nanov2_stage_1_0218_34m_uniform_pretrain_s3_vlmdb_recipe"}, + {"override /model": "vlm_fsdp"}, + {"override /vlm_policy": "qwen3_vl_30b_a3b_instruct"}, + {"override /callbacks": ["basic_vlm", "simple_log"]}, + "_self_", + ], + job=dict(group="debug"), + trainer=dict( + max_iter=8000, + logging_iter=1, + ), + optimizer=dict(config=dict(lr=5e-5, fused=True)), + data_setting=dict( + qwen_max_video_token_length=8192, + qwen_max_image_token_length=2048, + max_tokens=16000, + max_batch_size=1, + distributor_type="no_replace", + distributor_seed=1993, + val_split_ratio=0.1, + webdataset_detshuffle=True, + ), + model=dict( + config=dict( + policy=dict( + parallelism=dict( + data_parallel_shard_degree=2, + data_parallel_replicate_degree=1, + ), + ), + ), + ), + dataloader_train=dict( + enable_flop_based_batching=True, + target_runtime_seconds=3.0, # Used when max_batch_size > 1 + ), + checkpoint=dict(save_iter=2000), + ) +) + + +for _item in [ + pre_exp010_000_eagle_er_1p7b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp010_010_internvl3_5_1b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp010_020_internvl3_5_2b_joint_reasoner_tl_722_5vs5_no_predict2_s3_webloader, + pre_exp011_000_qwen3_vl_2b, + pre_exp011_020_qwen3_vl_2b_vit2k8k, + pre_exp011_030_qwen3_vl_2b_vit2k8k_mbs8, + pre_exp011_040_qwen3_vl_2b_vit2k8k_mbs8_flop3s, + pre_exp011_041_qwen3_vl_2b_vit2k8k_mbs8_flop3s_mix_text_only, + pre_exp011_100_qwen3_vl_2b_align, + pre_exp011_300_internvl3_5_2b_reinit_align, + pre_exp011_400_internvl3_5_2b_reinit_e2e, + pre_exp015_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s, + pre_exp015_001_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_mix_text_only, + pre_exp016_000_qwen3_vl_8b_thinking_vit2k8k_mbs1_flop3s, + pre_exp017_000_nemotron_vl_12b_mbs1, + pre_exp018_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_pretrain, + pre_exp019_000_qwen3_vl_8b_instruct_vit2k8k_mbs1_flop3s_posttrain, + pre_exp020_001_qwen3_vl_30b_a3b_instruct_ep, +]: + experiment_name = [name.lower() for name, value in globals().items() if value is _item][0] + if "job" not in _item: + _item["job"] = dict(name=experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}") + else: + _item["job"]["name"] = experiment_name + "_${now:%Y-%m-%d}_${now:%H-%M-%S}" + + cs.store(group="experiment", package="_global_", name=experiment_name, node=_item) diff --git a/cosmos_training/configs/base/vlm/experiment/utils.py b/cosmos_training/configs/base/vlm/experiment/utils.py new file mode 100644 index 00000000..676fd475 --- /dev/null +++ b/cosmos_training/configs/base/vlm/experiment/utils.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass, field +from typing import Dict, List + + +@dataclass +class Experiment: + job_exp: str + nnode: int + command_args: List[str] + job_name: str = None + init_command: str = "" + job_group: str = None + extra_env_vars: Dict[str, str] = field(default_factory=dict) diff --git a/cosmos_training/cosmos/__init__.py b/cosmos_training/cosmos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a8fdc487a1204e5c84466da40ee36d798703eed GIT binary patch literal 225 zcmX@j%ge<81SMZsvq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!YS7OqEiNfa)lVxf z*DuH~DoM-F$;>a-PtGsS1yTh?`B|ySB`|(`Qetsxd`VGaW?p7qx_)VKYEiL%Wnx}B z2$w)L#pf5K<`w6c7A2>`MBqB0#>U434Je6^*DI*}#bJ}1pHiBWYFESxbPyvD7lRld MnHd=wiLl!`czzR_s(yAqmBRdi53M{}3%)%HU$ad`X$Z!?6G2KizZUTAKyrwI6fW8LP0&xY@$Ken< zt^Bf!N|F{HWouj8@IV@g57IUXel`hk+7bj*dQpCkIaWzyDyc5Sg@{pJh`k%8&7p^L TF#3o^e2qm=BJZ3rhIwRPc@Rem literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/compile_tokenizer.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/compile_tokenizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d9bdcfaba0f47d961ca8b2546c4c73be947c5d7 GIT binary patch literal 4939 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/every_n.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/every_n.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53eed217c89083d74bf95afd8c1f3730c6b986b2 GIT binary patch literal 3726 zcmcf@O>Y~=b(ZAvbNL~WvLecs)-qyyRhzO~7zGSRK#|mm>sA3;0WFxO>m_$2Ew$XG zXP36c(m@YC$Up(Zms&v()IQmTFDX!<`3V7fAq@v2wl-=Y?IE{yi9kSzm{dqgEFr;{dEL z$Eq=)#dN+Iuf{zZuP2(xYSP2wda9YOrkj~+#+xT}p_#2_IT9i75;b{?s41F#9QEZ@ zb8As4{WVS`&dDh?%VtX5Zc=;2RDqIwzh!Htq38=R$zN(J%bKBRjQ-42sSc!(ZPFrB zG=nlJ;$&0}*l4x3O;snVo6E@PZAI5>N`1BN3-)0?m~72*cxy_koJy*Z6!Cv*L5^G~GC~Q-F^zMLZ7a|GFb{@Of1L z*(g&`vmjHTSv5y<|2L^QHGeDiIJj0dzs5F;pO5~V=znj zI|*5fNxbq14!&&~5Pj{sZ8C8Q{#SfUU8$tgVlaNhmaX%{84?2{iYGP0md=Y88NJ%3 zMtx1ZuGuT%^%bqYBG!Wl7Bx!*Yl>={X~wb$38NW+RCX~OIwNHH3vi5RE{Pg=M8TXA zaf?_t+lE~c-=RxNTerotySeQ1qj<7rnmTCs%N0mb%0$}~SCng1RKy)gCkE$4&;KG- zKzn+F*4rp|Ti7CN(^JSnxn^prXlqSswry*-3OnQGAn9lc#TG#su|}afpegX4L$eI; zc2UtSy3L$Lz;&Ie(DGnZivk~0JW~D-caeXeCPd&$%_)0(TB zRueugW;STu4(D=BvFK3kxwb`_HMgc1%lNm0L$ZnYGuy0ALrQqjkZl;e)?8rlN^1>o z0-913Np4vS7UjOWSy)&0wLVm(^BJdgcZRAxnlFhkEQa%RfbAzMbUynUaP7My9T#tR3 zoY(^P#NnIRN}RN+*a}?MuCF)(L}9CK2eX)NT9%U<=1qNA0N^+}$tzj|uYtU_O#Kji z^mkarnA&V?O&8fLCd)z08V8C;@PUAc179+0y;&$0p7~YTJlY^BJOd`OHVn+~&0G!OSZq^jtt3yuOS~1(YijCUB*zgJk0)~$nr*u%^ z>P!YHd(h%JIA;A50Hp6I5emb+9ULm()9>o*?+(hx?%NMPdhk)dd~W?R&aQr3c~YK# zw77YCzJGfD8xkq!28GED^^f|;dcQFHNa+{m+$hP8zl@VY`B|ZOFLgJyap}JL(0E{M z9)GKU{H@-b^S${Gd%w|oA2xdB)u#o0P$+E{j`j;j@1J;d?z7p?^)KG({rrQcgo3 zY=KR~!s11V^CDL&__33gWhi%T9h!$D$nw>;q6e)6m2{%;`fy^{;n_IM88!_krfPND zi9*Y8czDx5eT*++wP)-&?!hZEOlVI>28D|F2Eou_nGj$MT5m!E-6C$B%p4y~lm`Hh z%)HEu^U{lviH&1-kGMSGuv{8<6Nn|rVbM(?mL}tqF79*%lFn^q5t}FF$(PwNe)dIv z%H`n;C&wl>iW?L|rZ+L$FMZEVAUz4EH-2!Re<(ZQy;Ih2t{tU}@?HFzqG~OYWDJLOSZYM_LByLMSbZvm8pxBUS)u z-;4mZ6$NQ8QJQZ-G@g`qqDrmqnc0ckYb|&TFmx811K^DMUAy8nv5MEMeXr$$g|Moi zha4dw!aGd27zUYNW5#yUD`Bfh($ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/every_n_draw_sample.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/every_n_draw_sample.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a119564a2b96cdd4fd87e02380e964779a6e120 GIT binary patch literal 23509 zcmdsf3ve6Bb>IyCUlJq$f*|+-NqtC)BB}phTK&aciSn)_?~0NZ3t@&NAbhC7P!zd9 zR!%l&$ljc&I5|;X?TOm>ZXvJk%)PkIbjp>`xl~2I^QC44%0MREvs~WP-Co^QQ7gF| z`#!$+dN2S;K~kKot5P>4dU~e2U%&2t-Ti(&|H*1KQE-hs)`ot1n4*4)56WfEM;`qF zO;Oh${N=-h+)$f87ZMZh#?st=Sx^P{@)9)d1 zeYif-(BDAfhHxX)VU0BPH$}YtUh>WuZjQ9{w~)9g+!|Tdzl_Aq;pLGP{VO2uW-P2Z z)TeYo@y=M;Hm0I{+JKI!d|TV!&e-0j`a75^2v;$6Of{=7pWh8&89Tspz|Z*%xF)6s z;C8C0gB0U>n__BN&xdLmr~cJrbi3zMeCTVZC3AEn91g^yJbRIcNByv$s{3q}TgG0t}($gud*77qsltdH^Ye&1O? z9~hFfK>*K6ns{)Wm2@1-k8sg|N^T4ZwGd+C9;G3;MkS~adA&F|4LOpkq&lfd(0slt zC@P^&XgU$U^WIT^ zlsW4S_@iFv?BTF~tOQ0N#&K+b507<`I@-08Ha_GZW+ipRe^F8gqr7D0V_ab9LNE@a z>pOrQi*>adI2)kNRY4%pK8YTZ=ut__`GE)|ZIm7L!GPdxG>*;02H^F}KkN>Vz=*Tm z1M$)B;TXpc#KOT?ygLw!N8mQh#X^9O@(d#zXMH^94@QI0!S0bb%f-9LfUxk#=L`8_ z!)!Dj8{q=1@}^KC4>sTrhtK*0=i=Qg5~DBbV>th)FYb>FhuN;-G07sg3Wy9yi1Px* zE8!PE2Eiq2!9Z0yuAIAk?n?(R?Y?wA)ts}~{$Sn2`N`(5Z4oW?m-f%wYbHh}&tDz; z+RksRn;J=BjCrG=+4u;+Qh_|VV4T7=l91*X zA##o43uy_8FOV>=!K`@kI{6N<0uk}bY6@$mLW%?0QbOu$*GVQA$7r1QM*}QZ13)D0 zKse^-B{d9+q&XXlg(VF?HjEQjd%+(bVdIF3mutejmSEgB%mpJs9%znhhgWOh7jJ~% z5;bpjPOvuu-`H@c^$!2-<1@?txL;^^A!FVxXm)eWkY!PWjqrjrSYG;{*Wlue;I<{G zebf~?pcd2bXLS&9UPn={x#J*8vu88l1{1s z`@*V|C=dsyN>InBG3ZH}v~d(dh>;SbRJ*Eg+!~EVR~H7nUC*^bb0kd|*Ly8L0&|BD zfuv!A0bX*z?f_)L`}kOqN?=vy*+{%y&9z}!`hrBCi{rqw*UoY=B673Id=d zebvPB8_T|-ootxXugtC4_@l~TdiVD_XU~7H`-lC)YbWPUzcF`qKsYsc|H#msZQ0bz z_iZchRR#q^aNcO2*neaFoY9-xwEa78e)Q%aC$p8KxB2w>>E<6=gcD!2qhg^LgJDZ7Em9oh4bi6OX#fAfg84M5# zbN=7M6uU+ZQn2L!X`lHXZZ8Q{LR)ToVrbmk$Du5v8b>11CFrN*R4>V?FHV9`DINe1 zuTWu4k)O#soTf`I*zWYtuQQ=X9APtiZaWe>%sEilUXzBWv`0m_8qK+VKrilunvr9U zgn`jA+TT^>LtR2$h?DmNS}X@pHKD=^S!`0mpez_P_0FGasN$9%vJ`VEv4n=v-O($P z9yVu#-wVV{_RcT(8F^!l z-8lmL80@lLCIXOb<^v?;VK-2Z8_`_W2QiI5>{`c+@+{1({AjG_15N z0-HA=;2rieh)SLXf%>tcJo`j z>GbaH^d9f@9_sYIdPZ(N!Xa~XtM|B{8)Sh|3Nlax+60J}HkywuE-_Rn0oV@?@y{tD zPyMalzLCgT;4J`MSP_Bs@rS(w9P}KamvbyT>>Y?FISH&zZ!8*?8N7iY2c08Vu2h7B0gN%QQ!JD3*jEUjT++9Di*yPD zOga;B&JUj4^NTu{kMaJnuh7M0!RwT}Hs95s{@-8y>Q|p4AM+_he)TD`6nz{%-=-xs z5DsiD4tRo11$j+L6^%+Jh4TGjNez`tI{z^6c_1agENVA$Yau@eJ1NDj#{f4E$toi$ zb8&IPph`x32h1ZZ!iDIlWLcCGS2^rba>=YtHDt)c@xko%L8aV5$o2yK;{P22U<;Q~ zPS>R)xhmIZlv%qfrG4P6n>=yz)b&&8>gzty**RS$I@YF)x$>{)UG>*4UcH!gb%?Hx ztgB0Ob>%Eo6TOooQzIX|dF##TwYT09n>Nn4#il*8y>~~1S5IeOJuAL?RtT`-s{`WE z0U^kVM>)YBf23B`no<_vQ!Q0jwq4#fasKko$=&zN4O1)9Yu@k3c~*Xt{^pvre_EFw z`V;z3)(CCeMbD1Z;Rgn1)=)1R>L*X%H*_qxpcRj65yqCwTPDt=={a+Upy`mAH)wL< zLS_)JM*}dDUxvMlrBGV$S21+Ino%*ZHZ$5miqXAKGkV6r81sK7#>^N8DnJ6(fV8Ut zX;;Hk!p{c3Dpt$t$O;cqEy%7KkX`j4yXu)5__^R$%NiIrysu;R1C1$afMz^zTl;;L|tfE*;s7^++G-R4TMCyYJa#VznbzO2PEF07EHl@Usd%x|u zrCN)n`m3Ib#WDp8W_d~eYNnC3vj*&i0Tr|2ZB4%;MO~!(ov_oiNj6!K-UpJ)F(SD5 zTR`e0YXaFS6d@|^e3WaU5 z{=ynP5{6>1Aj%>nNnKZVLC%X+-ZqdJ+hKWx<$Hi*&yTRtz?c_h(4k-emR5LOz!D|Z z=!MuhT#1yq7w6ew9}#mA5G-bjPC*8V$wbm6D`hFPF7G~ez^|aYrU)?#3WM>yphvJ< z1QZus)(@DGO%oQaV6?5YM5mlUP--u1v!g-S5MdL7K zZUlIZk}(SoM*MLfP{`6cmQrTPxR5ef7xHIFT+gU*eX%wiYF~Wav(YD{gLJWOG2#dO z;dlw=AfmGo|Fb|XnS{6kka+;5BB+*oCU|I=FUk%Adk83lwk9&nKdUaeCzJxv`B>nY z6$}i=%gj@mPa1}SjI+3|!GeNHFaHH3K3oV0+q4+)nvHS~b6a6O3dy!~qDxgk8jB~B zWKalQvMr@vsSW@F1fwODAvDq= z&JVFTs(Y0+Qn_D}^*C=p(ztsmkJ6@_LP?->Y>3(6W?|0E-* ztcW5z%HdG=1qx&hPe|1G{OABom8TgILWOAoIrCRioluw4ySxilFHjy9CWMk7R7})+ zIK>LnsHo;Er<+g+lu?}nRU5aLFA#RQDFrnal$gaD8TB2FqTQe=zChX`M=_5QV_@=> zg8^8pK7_u(3l<3~glgs`7qqG{L1EhH`XsuW$b-j0AT+QSfl2Vi&W$_w=Jobly=1}j zF+pytx7{k~U=J7w4od1!?5t!O_VYtNl7S0AMUt_QnamK5!I!F7Trx!bz;1$E%;91o zX@Kzs0UDz=VnF*!+Xwm6MC}a~fu}w^)l3V+`XERhR>i`Kh{sp&?lTy6sB7 zRmmG#ro@VO#(1aLf3}1zw6d6{#2B;ECRm$_&|tM&dL>Muj-~HFV{S>SDSBEY>OY6z>Tq&~$$939T=_S7OPU=hRhEt}cF)Qa*%S^pcC;ro0Gk zS7OC`q6X$g9wVhUlCFe3w5ym$i6vZ04jA9k`CO?$;HseAyOsRKd!j}unXE}vmW5pt z+FQ)8#FRZVv|o8syenx6WUvqXR6{)ly73+8_ zT{j+#j5iR^g{J`}lANRoj4*xxuVdH?-~vMU2FWv7#D1b9|s$838XFxwJ$d02T@q z0O43X&fzrVzJviT=b)A!9^sIwkqjtiqLc~(G%+7=NqF5}M;IzePdFu(Bb=3FBAqDv z?^t3ZJm@L&-aHyo3E{ldynisCGZ1F|QK$i&H?Sm!tQ3bEres9508w{JCIXKr?IfKn zQA4$$d>e|5a+ff(1-m8|@gaGzk`_o5y2Gk8%_keUNl8h6Q5MPp5>dKx?;(H+;+Qfp z>>G{wz#&R9$t)*wqH)}xkvS}X$tElbX0BqwKsqN z&C6_Bn^J#bFkdlVHVV}nW|!YL9D>vuPu96cbgr51$T)WZA8BvM+Pg%1SIUsHy0X?b z(b|?$J=Rec`@{yJZugvdPp)NUw&jS}a^x-}9DhydKP|j|Hq#OiElsK2sj+C=9k3HFJ-z9-(|9W5wR~K44)VK&V$LqTJfNwGIe%AtFl%C?VNk7PSXf}EC>YG!0J~(s>SlyE|MhIq4WcR-& z!oTZe$}(?rUNc=aO>Px!JJQZ<`*sojZ97uhTy0&-I$zs7wIWm7{uyO5uDN^&nmdxK z@l1xrnvV1ljB`!axn6Xx zPaVkTovci2r$&XE?wS4fEqmr&Em_wF(Y4{*{_li73eDEstAT!Fx$)-q>)YSmnf0s|J*zXG z?)#RW1t*}rP(vAQS1K-7ygoOdkS!hMEloA7`MGx2*fI$JBU%6Tt z6Qr5*c`WZ4m5~A_uU&ysmR+|p6?Y1(9}Nb(gkI_W4YF#X3?t6{+vh;FErtp>cPm&F z??l@PKA+(1qWcg8V7!QDs5MyuqRg}Lu6m+|GU6H64>kA5kV`|Rny7DHdd_%Mlw<#u z-T@}HjBrp$1vc`A`WfR1E;TT!&ccWl9A2I~Vl_{+d_!s5H*r>@12pOd?G zN$zI~FiO7=M{RVT%&%3@+N}!qRAq&bf`S9qA2+N&b;&AlU-BfXP{11p zkp~nXx`dIbzoAN21AUaE@9`3W>jeeB;{7da!uT$Ar$K2&0^yXOxAHhQzNbOC?X720 zRI(ybT~5T|3xX4WP{E-n_A259td;hFiugW)LN#e;nh1q?->OX5k$Gd9L9Ee$Hk~}d zO6P!;PL(W(ComF4A@c>Lp5py2+ppQ?7P-wOP|GP1K;XL5s`Mk^a7d{!R7RM3mIy^t zV~TGjtlpzkEBMXE=~3{uJPGRMc&pMh>f6g75LI?2uJ)K%zzPTf4h~83&DL_qetjB^vH`A!WR#dk=cd z_k#MfRQln52LWq|=rgYE3dLj5@fzZ~1yy%?6=Z_oG<|?W)x~&KF;!t`tte=9f;N%Z zWr=VG&Y#FMbKk@OQR8q+28+j$WBcXGY(+S7v&JnlnEX@Is2^B+kmJx$iSB49SrX5L z0Whw_hh$eDEinQTaiCo_ZqExE-GkxSS(J&uM9pFEw8P7oL6HU{*bE#EFbqchXTvPRX-K0$M)t)6;F8WEVI~?Jp=j$p#`=2|zu~j6{ZsMw_UsxqpVqCouRn1}x^$ z@!&oWuG=5s1GwRWW3K)zoV^Hw@Tw(l2_UeKM5PL_6cX1h<*Wqp&XaT_!$3430YQI) z-}_e)F3OZdVw23^rpAwf>r-HeZFk7}@LQNVh{5|9pp?RW0|Oja=vVoG2=}k>6(L2` zhH<#hDPkEa-6bP*IUeX37%q~AWX+SboFEyAY>4JdE{^5tVB7)sa$=P%R9Q+XbiCmS z$*MH@K0t~GVTdkK56qQUwtZz=&R%@>oK8w zccx~~<%6HpG^Y+eaCwUI*u2|2M4Ef3WKtq3Nn`AOGI^>BDm?b}wkDs_GKwV-SR#6$>V+X?4!s^03lSV@jD8 zsws=@%J#2ppWG+g2d4LB*Y}F+d+*iv&V#6M`10YLtu9yHILY4}yFNB~F6~T@&Dhg1 zVaG9$?y|>&;_;vmiU`M|LNqS$$pu*M={+jA@$_E2?NuGZTUD>>u~u!Zcx#)r*zwm^$5&52q*{!gT%|o*x$Iu$vU!{Rn(?Yps9Qa={J!mlT-Vxfg>Q#*4o}YE z&N&)$4lh{fa*l>aR(*H1Xj?U5oZLM1#m6?P%7Nu3yQkUvw(U8)>)Q6;-#)ePgWg-c z(^c>H%{x5T4qrX|psnXymA5N1ZCi3HIzNcqiu_S5*S_|lUgNHV&hf0A;y)O>H8yo_ zx@vme^z!MkS@+#_vz6c^S9{{IRqw2MTuoVRM2f3vn?7;hvMpEdz4_AhmvS~&&gR74 zveoBoj-1U6_%&3djsRkEfr;p}^S))%CwU>TX45Q)bT7`s06;gt)A#fJH1xt_F#e&N z+*h{DH!PcaRp{C|Ys@sfnCi>5wq;v4iLIMvOmnS!uj}u+QoT7#<;2SqV-x2lc8Qjj z)ZPaxdvYy354CDXMQR^Vi_MYm(Vh+8F@I#9=@njiLwMOg*Atk~fOW@s-8k{)RP)r{ zsSRnh;OrD_s~>n;Z|=IjE92>$FhDD=nXj7XUJuMMgTl~ohB=>ko%>OJTsYZ3nMm(V zZA;v)Fw_X0tV~pLSHS*agI^UxC#(78O75pXrsZ_7%>bL9v@7f<*k{UG zpTNna8oNYcYQc(GRnAI<`4SFfj~H?WVo4cHLq@;)o$Zl$A*wIT5^4ue=aAkD`_QTQ&zZ!vHm{vJz9m!2*@xK-RLe<&T6ORhw{QU@Geb84t$L$aPPSlSW|N@~K2N`^e_=ZsLI z;qxAwcm9J0>}cGJMaZ9 z)qAJ=dOe>!cbPx2cx*tfFhrFCL+6_}gv|{Tpf;>bLs1A4AAJfS|NV z+)v@fuhW?0&=~F$1ogKN{ALjT^#8yypwA286M%5M*!#76)9WVfXpfp&`(A@syZ0Br zv4(K}1-l8m9mwk^7`H+2kLvwbmypuV^!`chP4jj0)SmY$Mc2;XU}OHP(irZ4{9{}3 z8q(1JM0md`8sa|v{IET_gn)LXPk=$wbKNub%6m;>?Jl{`j~{5)65m_Npg4L#-kH17~*xZFMw<;|82zkcOU@IgJZ;^OjKY*F(+w=jt*C~_7)Ch9ocR7 zB*y*%g9rpt1sp8}Lph6{6>yRd7SEDJS87yJmn^wT?kaK`2(&sx1Lix&j>#&YYX~0| zEZmndNMUdp0{9d}Fv{J=mP7DrWundELiNSFQ zBv0N+0}eZomc#K1I1nJKq-2lge$0!^A@>ywzJ$S7G2k({2|;`F-$M~4>z^dZE>$k+ z2JT-Yqz^FoItDLeCNhqsMUoXXWf3??6!hVVft|JFkRpe?s_er}ly-{JOYT;wGGAWlWy!hth3vJ$aQ_aW;Gl4S z3_-jZf^v$nMem4~jHn97T~FEE+HflTXV`NuQJ*-MPlaaaSx0vFE8^}~=A6e;rbT)$ za42Y6F~xomz7-bM_ugHfY5am<@Z`*m0vsFc-jnTq`Cj+SxyIIPV~^O_lWjaIHXaqe z&@Y_!XHUn))A39rpE{cB-kEw)aIF$8o%8Ko->SO}nwh4oZMkS$KIIo}ZRzf8$1bsB z*DNh|?76!UtlJ$GsTU`HOSG&2)yd!2@uAi;xjyUZ6g{2kP_}EY*tIv~+6M%mu(T3ZOJrl{fshL zmrafx*R7vXR%3_;X9j0o*11u1ZbW^W zyE*4xo~vuj*7b;WJ-N=FZ0Ejvo%?R>ySsee>Atai%9!3Voe)>;&D4Pd#r_2=RkP_4 zrLU<-9aykY4Xsm~L1CIYGHf55w@y!Y-(S8* zST-bBnjwVGu7Iy|#=d&G{+@k@VA&zqca%13@9c@Y`-NjCGoJpe=MB;GM#ghC>lqL| z0~ycIeM@k`gY9_eh2HoCL9NW#+tP>c**6Q8&4PV%DX5*Z+S!r2Q0_$5b4v7_%6QIX zJ$}*S&v*j&EesT&xcIdlQ&6}^u=EJ_o?_wj(2RCwWERRE$$E~7o?`^aNzns%otm?p zeo)`^q^CwgIDSTW-7g$FE4Txxikznj`mc5JEjYtlA$0E&+%KU2MpM?=Av!z2aig3o zhhT93;c zV2XD<_p79j!)r!IK9+=>I-;=TVr1=;7G=?-ro_dAD(B*nD0~zNzK((42BY9R4^#ra z{4hYi3;`cg0lkWRLApV~1HOR~E1ae$YXt;M5(a95Z#1vdkfT9Wp_)r;t@DSRz zmRvx2S~vl3yHlm;vfymO=iT$hr}X@@u|+9*$;EF_UchOHYGn+;0YM96YFAnSbZR^X z2LQ{>xo6Gnq?u7A%!~uSQv-J`e&=Nnot6ra6gQLc#;6xQH|OQOL*7yE z>YXIo24ACThl9f2(RS{;SeVE-G&!tzoI8ib%;0QHzG4>|9()1@4^T>A96)!6C>I;W z?+_A=60q}tpAIJ0LK0M@4jsTB)mteQ16f%6MpuOEAyH%r# zD{xNI!l_I!!xO>ie<9{CV}Lv7QlmZYF|xV-G{#=TU>Jk*7;q3s8gvi_XX_wG4tBDb z@B<9~Bc_^5zO*N4;Y&SX@X^L2Yd-uAPyXaDvEp-H$lqQeOF~4TAff;Y1yUslBk1!E zttOv@ktH0EQQ*5WG9a7mtSrAF7+D5_kUj6yOe6~UP#k{0M=qf99ha*3A&$+1jLRO< z74W^ryjKAZTpT-Kk-(Av!WW8dWln}FZsM0v=J<11Bj7lkz2=52dA~Z{Jm+4Yb#E2j zTW8kJoX@y-i}u~K$HeMGDSfW4`R3~DtJAbt*OB&%b=|4Tc~@PL#m~65q%65=_t%>z z&rF6V&j@uLV)d$&9we5UrpZyE?NG+q3tSTz{-0)5Z&+ecuS)L(6Te{FhUZQXT|M+O zy~c`mdaw)c`pT|JKHIcjY+66fXEz-dHyys$bU4?t;)AYRT|)C3uu<;>mR)Gt3!+5c zZ~BJnrvAGAX65xtp>1=fcFXL(`xda}H%x}0mRZeT8oqCU7Z2*%1b2JdBe*s|dw}_d zlYYRaPMuHJrz-^KCJ@M=3GlI&N&2fhZ?HFm?*{+6vT?qy70mEo-#wX}vP>og@I-K| z&NWg9k&4V!0Nv**DeEch6qVRNQ|oiKf3t45RZjXgIH zUO#yA==G!N7c=#nW|!T!9h|p$vbJW?29gdKqC4-Lp6QrnGOJ&@+b_KC6T-uSt#i(H zexZ|cGzyl6g?7r*@_zHwnW^y98KGsZShsFMkN)Zn#PJwh)|XGHa?4kwRcZfi{dD!T z|DzgV*=E7k3?ux0^ZPG{Tpj*UulKY-h#rbIR z;W?KPm|s3n`S{Mm`HcMOZoSXP!~$?K9Jxpi*=bo}vI1ksadB^8V8q}8253=+ZzREo zwm5l>(noPh`x)6I=tr36BMgw)CfX9onm<+^V8dab4^Z<%QXCn#7i4unFJ{F6k&T~+ zl)rnMdZ@B#4GSGqohMh{kn^_Ynp-7rHw-|oV^yxUA?NYtT=ltS?Kw|t&hE@Lv^;EX z)w&*3ITo~VhY@zT7W5c1P_DWKBgVjMqjABEF$-1Sv|zD}A%G0z^ zg|TXCdE0^=V-BjiX2FRukdEC8E{xSuRn7%B#_FiXmIV*S>Zuy{LIcJcsfwzFCX9J0 zm*;1Q*TWZSy|!cO=mG_|Sv9%c-3P*_wgalt*)ARba{rh51>1^@c4bamed)xP&wT03 z@Aw|7y0i!AM`}vv#Gq|KgU<_Z(3H+z%<~0W+f4E-s4%*~s7fGqWVEXus!q__I?U3! zK%xs#)kdvj=HgxTcauM-AWEo)i~)=fe6tul)X2Wt-o;(P(ZT(L96v@?RycT8_AE1! z4`jjjPU5oXnGQUq&_$5Y5fT9CpTL1^jC%)Rps^6vaQwVhSIIYbmpkdnFx|4pFp-p*)fhx3;6mZenGa2yzk0?JeWg? zP^!--@z_~ipjjhdxMLv(TOx*e=ieo!lnqHu$IFe+t2Znx^STUW&H-4Q2e8YWbLI_%YS|Q>yxJ zD9gu`_s3L|KsEghRsCaXi$HB5FYEu7TJLNI+suYP+y33{K;87F&m1(phyKi9pqn1nG|{Uj`KkRM9KLl}Y+AEG!ShTj nxy=U1O*r;N`R-$~zH_4Q+#}5McprUSOFJem{EWgoY2W_?9u!|o literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/expert_heatmap.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/expert_heatmap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6e4f385eeebaa6b973df4944acffe89bf5ba024 GIT binary patch literal 5728 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 E3dKv*RsaA1 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/heart_beat.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/heart_beat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..784d39129584b02ff92e8318635ecac741d7fbd1 GIT binary patch literal 5438 zcmcf_TWl29_0I0j?(FWY*TxIB1H{9_gyrF-p=}aENJtH}11xQzrQLpvhMjAB%% zOW~vGmG|Cr&pr2?bI(1GJN|nj5hKvb55zr?$4NFVxRS0p)G-Pa;P8RdluaTwIa1KZjG|^Rdc@MH38=0X z>NwMkB4vP#>xS(x<7C;PI!q%}vnN%<%6p=`KIu;u|11#l3ph0;IYB3ILIF;f(gQT4 z2jLfjpGd_~=`0x)^zfNPE=(g6V8NA5U#&-IRF{B0s>epddi;!d)hEiuqQqa9X`IS5 zF|lstenLhAeSp*x&!p&RzrQJ4=DH?9*W^=l?WCPt+y93)9eUT9P}34!U$G z)|jnm$~aP<1WKhqZCe|o3V@7`l}(K)M$w_{v}W3hW5Gl>^5B2oc4r&_bS-lH4`Mx%_rSEj}MBnp3j+{llE z5ZY20_!`C2)+wqFHrdz?EvdFqp`6hU#VR?5Rn$zMB`zy`dDLRdxD6^pbW|&w4nNhm zS{%zI3@^2DXaLlLQG{q|FQGvPswp0wt)MJ0EJWiNm)W%La-lEpYV2r9x3$w$wRbC9 zPg<6_>^LXJDOdvzVJS$ooH0lTp#=>Oj}*stFBf$XZjX9WAmTmKyV0ic+`gxt+Ku=s z1LlOiqh`h3?nFCE~#7wP~?RJ_g#VP!0S^xC)MkO(0b=fxA|;iSP;pOshm~ zQdWa7Yf7$$VAkB>G}07ln{onIA$ehgJy8#W1cLl{vPaA$Nr$rO0E+|6Wauj0`wNtT zgJ%v~wz|JKMonrTOvhc(E|(~CB^CU^a8%Wes%qZUY#Ts87PSHeD1;+RAh!WWpQZm?uKZq%^Val^s%8+JoBHAh?WTYK3mU|{1}F;T z;j?XI3p>M|Sj8MMSej&Apjc&9GrQ{;}6n2yX#y|X=^jnt&xo6^38 zRPUwi#q90W_W9KIv!V0Rh4kBJLpP-@3q3EwRKC;QbLowXZ`3w?_eS?CfRGmY0RM5v zA}}PbSirgl_Jef9J>3m6>XcbAoA(PPk`c5S_sxTF|KORv7Z5VRmzQ^q)C*qGTmVn? zy|#%0fK{EQAGY_}327<42RD3h0$prH(}l+B2f=Ef8q@>74SW!C(0zEDKp$Gss!(`A zzcwZIH$)HpF4&5fQKac7MuFYGw!QfG5gX(W+v@ zn+4)DBVjrW)nK((4Ir0?fXa^|0sShNJeOT1wxvKYiJgB?1jmqw8-_XK5%`vThyh{}skI>1uadx6G203D>V zdMD{jUHI_ahYRaBoE5va?_3Ez?H5qlG_$KQ5E1Pd`8=i;1f#EOW!wW!hX!_7h z^p?D_9whRJ!0nu0n!lU%;NQUZ`OW*xdhnBPupR{P^~CEz@ELhL_`LOiU#Xa73Rr<{ zz-|+C6_KZCHUMxsRrazAT&gj+a&Sy|QNdulk6p>Q)}mLHO8nL-=LJ-@jh9ieaVNrCLREl3`Y5 zJCIulzLFg`2#*+7bSws?JA549+KiVThK|Nw!4Bhz?ZGVx|BE6|O;c4tJN^ZL`o86b z9^`hyu)_eg&%w&qXUKzqBuWobVX=QUSSK)i<_)#uC-}5BcIpwLp@N0GZi{Q@#kCIt zQSpbu?9n>G@jB4s`1#ORd|V8Y@VYuDsc$4NtbLG>#S_B4mda9`lTKA|ho2kRXQQ=zN8$aLt6`ykk4><`hJN$CeNv~XDu3_-< zA?yHjZm;)2HQ4;18uZH^3*quAf0=*c4Dzox?oDoozu-R{xaYXCk1WH#S!~Y@Q%g~U z{y+7+$VHc#Bc6u%5`HZ0^*nta%fb{r%tkk?2L(Y`REQwoCF{Q=@qZBcUu64TGI*El ZxJx$QC0iecj|jq1;g{h@gae%Ce*khcvj+eG literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/iter_speed.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/iter_speed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cda66341173daa95bb9d254d236507d47a7308e3 GIT binary patch literal 5175 zcma(VTWlN0agXGYNAmd4)1vk8*_Lflawz!`r&3&}LEPAKQWb?=t7gz#Dc;Et~~ z$2cO+K|YLD3fwwDQCNl(SV02Bfdf?es{2)c2IvoII}m%70Ru@t{8P}e4Fvh>%#uec zs*-j=?at2Z%h3Z9PyBxujmv&I;Ma5j@MWpjqAhBT1LoAEJeqL78lr^++6o}+Ux1swJcbS+D zl`MKK@_P>T;y&ABsM_>K0iF~Xt?ivX zha=qr8!>#)p#e+MTi;$B)(VD6)!}L5-0+-E&1qfB=*DnbH*zq{Q9Y}sO=m7or3_Uz zF{FSI4;KuT8pHFc{8{`n>rApfhe4`9)2f4MaMZ=7Q<^rFO3xa@=qa*+ZkCvvr|qz_ zVr#?m63#{K#%-U0?whyBnh;v_m4&XQ{*n-1-hEq$SK7N4JQw|y?w$qDU7@QI+X2&X zWlJ9nVrAzc#7eN}^2ECntL;PO_90xp*1P@MnJZ_mzOvx?LfHBUgcF<3VF0xQO3F;Z zSZWeTg0sOU&Vd}7;=Kw{ z-^4SrmK7bSIXY`W|0Z5m1n*D;km7$os02O@-l*RK2lw;_x7kg>i9lc4B{x+3zxrh~ zcGk>>Z)aN_TyxBJG+_sFPQif}n~faCV$wbqYg`ahJQjbW9u;t1T%0Hd(12O^mx_@w z={Q9Hx_6IB4=}db+;Gt^S<=g3Ok>iw?!>`qHa`V^%;x*2ZO(X+3a~aFqNuv&)_54( zlgUr(wpUTj6xgQ+Y%xc1_jD^#c~hr|P8XDv9W>_i=^3i$GewoQA-P@8H~d#NOm$9H z^Ge)DyHS9I-FEDDV7C*ye(W}e4cdp;{m>Qrn%=sZ=n+_BtU(8!Ta6Mix*F^*2fLS) zYxyhr+rhnSV&u~C#p9LMo~1KOFI5RQAXTLPRcWLwjjZe|O9xBao?iLIiuso}KYO!u z=tWp?jEhxbz1O<0bYJaVjSZD!Lw90NT~C*~_5<-iYz=6!E3vEH%jW8y(K7sEqc?Y! zx{lmTRekQ3=eeptdbU3adPCuAn1otZ#h%+@&oWmQw|^n_tZm=*aqNTGhusTD?}~k2 zi7l5-ES^{uca+5)cg6mS*jkT1{mZ|+e&VKhOZj~K3*j`~1_!i#EXt`g-7q=8;uEq^ za0L4A3j}X%ZKk}<%-=CYNGu?(*1#kiY4V47&igFi=3xTeaPt6B3^K|lDGn*;1+#I& zES_&ev%C;sI7?7m;NotJ|Dj-#ZG@4g5a+at=c?y@Ug1CWHUos|l%|bjd){wiMZuco zQhd%@I$^o8X!0AH<-SqBxAOrjK>N&wq>?tC5sD}>gziDFn9V-;zwWRU0}h$wE83^lFi z6+?pRs#&l$0xXTgs#-L)!x=-)!1t0`Pw3Hg6WM?;X-8krD5_o~*Dzrtm7CL4r$mdp ziczfF=(^&C{s>gS=adISYOUOJ*#RsQYCC0Nv?v+}#)gM!A+KcUaM4@ix7{P7N|8^@ zr*c}ngGF-qDr4n+-uAN)jr*eLb966uPhppU&h{vov}yZhQbx)&DSaAoZcWYGkxU+o zmfGQF)p-X|I~yYSbE&*CWxHWFw(l(D5){xV&?&?Q6ko{AQMBQBxRxOf0Q|ja!WPm6 zDzmE@L-sCs4!M4JteA3ckZqJ6LRrsXhnHtyp}>mLM#r*@E!OV>4l}0}JDcfdJ^mO< zxD1XBS*9Jb%%#!A(NeHyt*hsn|BC-=aN+31ag5$=TP}~jJNn0G7lcY@-*P*~`f}&s zb>+|6CtA5Tq|S|O#?zi)i9tmtV#yWzvy_TZN>`!t|fXwf6c?1qmP zI*e1yuB;THSkO`oigX-0J5>L4N~@YC%b=W9F&O-RQmbFEaHZH`HDkDd^34VE(8cot zR0bpaZuOVa>eoczed zMJu-HPe|9O_ekO%*?W)l X+#|#H$iQRoD;(GQj`s<{kqP||07s^W literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/load_pretrained.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/load_pretrained.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d333240731a2b1d7a8a324b9dc77565d49dcc86b GIT binary patch literal 1247 zcmZ8h&1)1f6i;S$wzJh;#Si?(AYN8*r-(-pp#>4N3VM*egmjW=Q?oN`lA&(#&_luY zAm~AQ6>s7{;L)St$%7~Zo;-O|T2VatGBdkY1M~9oUf%aF^RC^VMzHdoVEIJ@p)c+* zUO~-lUjSwcam2Ag1Kh{h@t*Pqe&5H)Lst>^*AZ_B|CL|a^c$m~+x+SdU0m$DHeib~ zWm1b985asf;Ve^XfyMXY%AlfC^{pJRGfcsOoQ8z3)*E7a%X<;>!so*i*6~JZZzU?zWp5ZNIC(2Fy5Iu&9U)2(f zM!4&(zMdxay0at=iR-M1NZEoChUA9Wj3@|6hLf0N;Ho4mLM|>@Lu5iyA-LeZiLB2#H4Q*u>slIUze3`>L+smXNZA+u!|P&1_kBSJ%uEz#W&qnz zXUihC<1<}gM$oduGF^;vBeaP|EM0V;t&M0lbm`|h7Guf8qINrOr-@t#>E90ah9ljA z%vO*5JI&s>l*6Dj`+@vgLp#%`)!CeSA0GV>9^VETDuQwXMN3+tnUz^uG+{cqPUqtq zq3#+$x5mR+V7Aahyu2GN?gp$~hyXCp19(sWmn|)$QJK(c&Rt)-SEW0S>;qz4P{5Ji z3wzPY(yDjVSf=gjssC#HSAEjiJ5OuqQ`r6(o_`;n{}4vH4WcgAt^`0m%s6EOL1{5f zT{^jP{9a1$=S)?avZ2~#QuqKBeb9038VRvb3IKxj9*`Q>g)3#Ux(RGa4mpK;H^0`= zju!+czISji^Ze#F1lt?G+}_Upbf+@6(rCUL6bGtd_sS;qO6XKs9Ih3)mCE$SSkUkr wsv^Ebl_S@*-{GaIs>|9n#3g8s0r}=*jDHY>qtEEvPP2t)p4|O`U@J}k03-fS2LJ#7 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/low_precision.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/low_precision.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bb9828603deb22afebd9a1ec7f5b311cdbf57eb GIT binary patch literal 2579 zcmaJ?O>7fK6rQzr{U0ZU5E4krG$|-cW2Zpi02OKr4I)5PA>d%CT`%6TvthmK&WuSN z8I@KXazJW0rM*$9QbFmVy-}%Bua$at@YG)_a|F2OLI@uvV76y~Sw;pjBnuUMDitJ`u5Y3n8>*QjpVgn}|^ITav% zm!0{KC@GoDbnJ7wW!=)qDf?=j3|)}nJ9)rVn$>k zj!-#f#O^7DxREyEpidZ)vTP(llW0WL3>7)+zexzCFg(|7vuqLrTh4s07RG@~K_Iy7 zP<&(R3f64{-?)4wpYn{Z%{$n%O>XLzxuA2199JEKu+!dd4xe-EvRTO))Vxh>JO+m9 zXv;&X1iMQS+WtEKI}r zw7||}m=TW6YLytK&WY8)W$IKhpCy4i-#CY_)@^Q9NzSxcjg-8I#~?2V%c+`t_ptu| zV1sZ%t58hCKeJdAS77DaL5jSDXUEpKsJh0uPI<8y+~NCm?&J)U={GHKF;*7SHoN?pc0ff5g9ZuXm|GDZ?*zcmH zWOzltZhr#HGHOaqqygtY%?L=)BY_kJDbkkYFZwz3ng8&xKT%U&3BG}^+b1m1#y;w9RM|L?#Xp4UAuXHDT4E zdB{YW%}>-E%FB*rI&7lkuqu!mb>`r?z$xr$Jn12fLvspWYgSjHUY{yerhtWkaq*jp#}cr-XPn7W+vT*b|6No3KdK|M^r*0WIKm-_k28G4*#H`G6mS33Mt&?yS8XrRoV_g6^ zZo9@FvgXxpH$JN~Z^TmYhp_>I>=JQh%Y=auHJ~CyH|~M$iQ`lh-c6J!Ks)re8)XgV z#sduUM>Y`xb6|Arlq=8bFry+lRiUC>uHr!;<2Aq{PWOYNGq`k**w4O>j|-!_ zC7OTzqes#+Wva#>L(uKtjHx3;78>N%u zi_@Fk{YwiE&-^H_b)WoY-><_z53hAkZT9S4&OFGh4qsgFncf^d^8J}_&n(GLdvGfb zMlVxn5I;;ky7Z)PZQ$(U#f`l~n?3uMvk$U@bUo-2B=sQGipy_iw~*YIZ6Ud9N@^ug z&y=*C^mtF5LZjo}rewniOzh>s6v@Zc5YOw(6GPGn)2kUAKarxTYCS zNz9PnLp9^Gc@!JU40qLN#m;rPyB;`dQScY-&&o_ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/manual_gc.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/manual_gc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13759373ca65ba4c549c7c9e6daed75c521acbd9 GIT binary patch literal 2162 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/moe_specialization_callback.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/moe_specialization_callback.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00bcac613c298e6d7e19274685a634556aed1e3d GIT binary patch literal 10078 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/ofu.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/ofu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71b3a76e90cb38a921cc6b59ede290d0a13cc254 GIT binary patch literal 12440 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/param_count.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/param_count.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8553fff1b1eec07a9de1ce690cfbd5e71feffae3 GIT binary patch literal 2143 zcmZuyO>7fK6rR~XukFpR6Y8h|+%|$5s9lw+2Na4_{+dFg9sr@Gbi3Idhc)|?&g>RB zbt(=ygbEapDg_mgI0mIsIi@#y>YIyVlYDXutbNVE0C-BD^>KXIQNyj%Gj6M!*-XCexbO+`1DWNPYu({(3I>ny{X z6-0h$Oqtm9$Sp`S*!0i7Z3MnsW6~5dNzHHqt7*4M)z)V1a|RXw0y?yTE~J62+7O8q z!4_s^T|%;*wne0%6jD*TI?y`Ru?#U>5jeFal(l7BsVH`8R$gl9=vqxFW@zRU?%aOv zCruIaOJtDUme6qz$2>A?A2PB4>aY%swzv+eow_Z4Rcb4=0Xx--F90Xs95+bxyjd{V|YV7{4<9U|{Fnft_>GrSuw; z11fvGrfAv;5jI0sa#X=6jIcj$wd2P0czMLxI01P!LPnck9fs{pEVU4RBTRaV-i)}?FU z1}oR^#l#=hds1C;;Ec@bM%$>*cX%}HNx0SC1g)=0>CPx&F5&)P>DOhOTiBH7tgqLA zt<)7pQ#a&wuWGC!Edj>=`y42$7kAS-Y6Pj>f~ES&V$F+8~r{!db{uX zr;D%MFn%2#y_es6VeD~!b>QakqWyz&%UL|N43|fi zH!VjiTMypX4mD(;<^O$_1Dp2}9-;60KiH-g;h`eLy!MM-xQ{I~V;HQhBA1U8%`m=* zOg9-Y6G7nOL8gRx@nYUnn2#;Ig%K+FtpC`QXGf{%_#xiLxVHf2B^5^xvlap|;q6UM zFft1o3K-h*RF>skkNO7J_}b_OO7BylOFj&59$YLhzk5gcIsH@m$9*e1KDs+{qye~O z<>aXru+Kb;IcsA3Skol8R_v!rSes&VEcTYFVSueOCEv$8n3VQ6M^!1FBc*uo#eaxD zLVPrqqR??kTu;p+8|>tlk_m@-ozS6;vW{0Qu2P!cAn$ku<7m=>4dT`_7uRkxZf`Lg z>#=;PjyALOK7Nl0_$VS=qai{FUlafwdIAQYfYM)}xC(Zxf_GLyc@@0*T-gK JfUog&{{cHV58nU) literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/sequence_packing_padding.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/sequence_packing_padding.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4aa707cbc741f2fd68e07f100dbbca7684ffda4d GIT binary patch literal 3153 zcmb7G-ER~}67SiW*_~a_ei$&AAccY40SjbT36T&4Mdvd%I0A!$A*a<{TI1ENU&c{zn#?!$RXBJz+wfk&i!*}c;#s}4w!66ut;MJ6Q5(^b#T`Zl(? za4k)Bb^W?(s=I2c=I_~T2Ek}N(ifIYgua%G{?KD)tqRQhh#;bPsM4F2iW038HPuNd zsHR0!ujq=z6JD~Gs-z-b^^97&l8$)I%hZM{LkdcuGl=Ln5J}S19kutKin**5Q{T$P zbWst>iABnmC6}M7*>kRMyNsR>2=zcT{9n-wvt6Gur)Q)`bMa#BufY5se5XkITS*Y5 zqLM60P_3G{iK+@wZ)7Vv(P)zBGzH!mG)*%LI|q&R_-bM|FeLS}JB+RMNt$XTLx!q3 zV&2dy=1ugbQpsLI#hloAiC$|^-=P<6=c?<^U9<^-Y2{(Yf-!D;-YfuCIB$m*w-+h5 zJnGMd^V|vpYiZtf=BRUXv#x0vss}9B}DOoe~F1K928nBujx`A&kxt?du zQmer!iCzT8oS;^3gw&cmci|$pNZ?b8G#L0|h4PkxDP^L)V{DF}j5z5cm65`}b;$L@ z;z?_g^*mUHcZBn?hfe<(;09~>o9#+NA{Co*5(5!P0xxr9?I@5xD(|Ck9lVujqIV!q zn+j1#f~cQqceEa;pk-9lr@vKTIXP3*m;sDP^N`jtcUPzwp0ycsDZ_R(1PVS069~3L z%tM=n#e~p#qfVJHFov^-7#CHcaq3lLTNpc@%{j(gdS_YJfBaL~Yw(cKa+NQY>j4X^ zf#(Li>;$|9Q=J71)Cv1*JZp1``!`#LKrmijw*5Kzgs~D1>eS~!gE_P>S=Wfo_OF(g z`=Q76UM9gDrBbQB%<|yl%HFM0t<*o^e%QpH0rJCjbl=F{O0|vA)m@*D-5Y8B&Tbi_ z|1xIR07W7dIRTfW*W@QDGQF%<4I9j#c2d$( zJY=njVjM!i1|z&MmN5~K$x)901EcNssXy` z7|ldlU@evp#d0%}XV>M~Se}z|d5gWV(65Dk0nE)*Gx=HmPJFZSOM!2tFkRGEUf4Q6 z$~F59Dt#i+z4FQ40tY}C3kb$a5$mRf^|>IHMfG%G+}vmKI*NFqBp+a<}UB=Xx3 z2JFPdbwXi4(Xu@^T;^#2Wao(n+a|Y}aSwvbse3EWZGF9^!A-veHU1dLb@XT(GKb$i z@rM)NOkDnAqTZgU-ygs9#kkiV_wH|!JWT7OxtnLZCK}y)^K2)-clA^|U+x@uv2|?x z9|ul8`z0tzg+OM(qjMkp;uibh|Lw$v;8k8#vn3t*K67v@`PT zV_nVWx<`;X!nVWFi;DXQiVpnFK)dwq8pT*-Fouh6c#=0UzSgk47)fDF0tdt@{4zwX ziOCMYWLXzl6eATHsnElKK}Si>$R5a8S;E=4?>ZEU3oa^b7^GYlykA8DWU{YBL`I#z z4djO#=wTwM8xISL?kKD3cSz3NEFB$yw>i+ z?HxzDX(`X3U3+dHTh%`{KQdc;j<$C`-yM>26Xkcj>)rCYS-Hxg9lN@D$rVs0_x&(% z(FH{w6|?NP^iF1sfZr|oTTqg%v$R;Pl{S4xnuLT37&|C6Mq`cjj^9s1CVQvY7Q<0X zEtKZ@u^b1@J%?cQC>Yx)%Re3Oa)qj9PitykC>EaiprLqNK!q9|(?QnFtn^WW&@ cf1;Nkpd$}Z=>aoe+1dj-efpg5ob#RUyuMfa-`3QaC;{Jbv z!~(@qtSU%_RHG^trqw}pNHeM-X-!Z&s)amFP#4mV8VY&zsFCEEMor{t4p~NP3T3sU zwUDO`T0?cCbtF$0tcP+-sA05$yz7IukbTrn(uQDTsA;qb(zPtj83O|filyh6nl-+q z8*SlKYU*iI`N%=!h9_n(f694l!FzxY2Bt6fGQ9kTb6;Q#49)5_!{TCowpu}QXpnx}I7Xzze)iI#@4CE-7$5k;^ zK$)c?B$OA@Cn&dOX837NWF~oz<;MJBj%7~!!vb^KD?l<5W`dD%FYgy8LVh1$Jo4w|-TU_AX>IF$ zfTMgEDM4FE-2VY27ATGyRk75lnpKTzSTn2Uw6Hccur_t9_AP2u&+0fspiZe=db0ZJ z`D0qv@Rk<#63v;aLsZWlQ@29nWM61U&C@2gMY4KMAZ3mMVGekG6P$M!OzwVW%s<7k z{epjnW6nmoD94ONc*X|+FdYn`CF2dVuvP?t5pfoHVZdZ!Ae?|j!3i?VASs`O{UPAW zI`FMi0|e&92`qFLa_1hzqmNXG1X)fH z8GzyaoIo1$@*Lxhijk04EUZ|b3q{Vsk`*IN2v(2?KT??tC+H9P#R9JT`Eg;GG=i9u zfs($4nPc85e<&KNh8nag;24=gt4I}29+zY)wc#t@v6v?NdJ7~LsF*57d5Y5m5sz{b0<@AU zJ+Bvfifdx({|~)tVabt8zJw8`0yS&j@hzrNa8<$72WlULs9H-~UTaEK?Qn}}0(jC^ z1clx+fQwJQDh(fE9sND|O>K zs8x+7kF151dZ!4IDYV-TQkC2fDZDGu95UC2Taj~r14Eg zB{iIfxOOD^^jI+B6^C~4ZTPD9PQn$<^6gl09p)g1zy;-_OcUh7l4de8>DH5N&ocJdVl2FNY=h5Va(f_7xv8Wx%61h)}66+XKg(>n>%B3 z-&p_tz_o#_ZFhpc1IMLx)|#W+Gjw}$dYN|P0a`m-o1@z@bX#&_nce_7IokCp?MfNn zvtG4knV}3l1ZUE!p0e6)MhymbcVh9GM--S=uv}_T<@RO9$lSEX=1CCUq>=y))CjGuyp8 zW7&P%!mJv;u9M~6$J4dj(j+FwB#-C8nv);7NOC+51L@#+qymsp8H6KZ@kkBefm#v9 zDY1AX1d2yu3FwAO;mH47?To2UP-Sfu50Fxy)kngrZ)g-;;;OH0p~~^OrUf{?$EGK+q`Lu_bzOn!}Eue?3Kvn$kNcVeS5-)^>@$jPM*B-+U3`l z>X&U>VS89xlD%nbXPWLLOAE$-FKqyc!GhdL*c;+J9?9hKgd%Jdcy5nm@gN5tER+~L z9ya2GYz?q(yc4h`OhAE9CkeC3BR2x_M|3jC@gSs9*C=@vBU{5aDG`!WI1bTU)aP1S zM}KZt>l#yA?^E#jg4XDIR!ud!;bh|#*Jal#1)Yh`fGyz zYzeqHNA0ky98&!WMR3_)5a_(WgR_RbfY^fK1niJ1+Aa{`4gXs-N z`~t@WxpN@46IljsuJa%-h_Z}>w@yJyI62`LK=zR(Nd`nrkS7_~%>5uNir%mfgrf(l zf&}Ch8P-2G#_=Gm6grh44OA&d+iY5ZD1#PUs`e0XqH&TabUeTtRWbv)<%GsDaQ zp2N90=NI7A^3M=hW-=H-xfR=9(+1NU0>uZcJ|+b8^uvKg1YH>icc}-#dyex6TZfs$ zW6ViD3i2-I!1SDFQ2&GkJh7D!BQiDy8kf>E6l70B0Y}MPhOA^Lv`g;y5nuTgjW-DX z?%##z`zla36_1cvkPyBH41mIo1IMZ<7w(7vi*-qe!DTlCmpgFMy>Q?fj(}21srgrM z4MNRgzYpQEk+rM{Pzx>%If(U2iq+gG$`$}&RPrARQM{;uhtCE9W3c<9<)8nl+ zLAbbNUX-w*#g(rd2mCHT0K`_5HQZ*9HKVW-mUdB zyEmyXEtIi#){voFmT5=6xgBI?i}J8Kk}qbg-HWPEt?o2^F1_ho7GkSA@9arvm+6+g zy=7r!9{6Wuo1vodu5;rOyX@RUN>_?6x9Jx7j@%A?@+hjWk-hPvBx^4X3|hIOGp4!@ zS0@H}HSwxxl+253O)kS&Sz*pk(XGVGLLYI>I3VmK^&wm*89}sAl<(T=V-h>R?jxPqn%e@ zptX<}uV2%`h6h_j|!QzgXBY@{tMNrm0-w*r7fonH_vPVTwpkmFj*1b?xgvDdOA{1d;WY`Ob z#G*DSFkAr)uS%O=1EsVU)Z#7!)0P9DYL;?PGtdgFnK8pCoiRh;u_}EyW41DSK3)Z) zG*eA%^Nki|cftBL1ol4&uZ%M8Dr@)22jODP%8Xj6cw5XBz}v1O#GJ7{g;Mjw(Y)@HhhCi@Z!#C*F@O8bGo#!JD;-{=UILT2BKtkYoB~^OHbjmp5Oj(O3 zOAtz`1PSxSoPg6=MNa7RN~--l<=!CknM7fF506N<`W|w)_wX7*8}OKa9F0-;kU0g* z4uXus55nrcd#KR{AT1gr$A54c!-xAM9mtp*FKN-#b?^T`KpW7xkUxyEcgn4m^by_< z)@abW3X@<;sgv)4&Z& zI$tCh;U&#jNaTHpfGoN*y(oa409KlFOS%J*XjqIQmqJkJ4RPKu^Ro{?Kx5rA8wj#% z@M6w`jao+cL{Wx239LBj0fxxk>z4^h^I&B}c}~)!+~nomn|R#Sd^bdrj%ciTWUD|K z9TX*6h@K|G95Dn->hL6wXR>4lHGmiy=e?mVtfT>wOL~7u0Et&pos>+VIKWCl|7oBT z4p)#2FuV~Uy+o6Y(_S9zV4$a)266n1AX$s)p!YNv6y(Aw@=^el(?$do{bG=lj71zI znuW<0VG&8X84e_SundB2oC7s@cp4lJfO?V!XeVjHn8MDrsEz=o*?E6>O49iSETCb2 zB0MP_=lyH}hcX1GFuevr0Vhndi2m^j(G&DeN1`Gu2viYBI`oGCgnS?@aqbRTcqs}1 zKv`mh4^K)401!No(7;Xrl&6L;+5+7Z2weG+X&MIYJLAD6UQ7ekW&0rk0ZPN75hOhr ztH(KbPRmcVKLlxc6sl<$G9ezBKu@8H$|ISf4KS91)DiSb|3A>{MYyEKq%=*cwnWE*;O4Fj2mfo#L(gyF8E z?MmO}zSP;pj-?lu9godg6Pnr4gl}%^mwKwHcTt^f+>|gu51qYW8A+Jtj@-33Ej&B_ zY|6BFb3MUp5>13Ah>=`pVXsY#qjL#+k2B;pPJib zd~543j?RweEe#i+nSCa2bL4HUc{|uZ=8vU@7f)Y(d};XROxkv2rQVsV@66P9=IS?Q z>NhT_Zt6caeQ5f7OS*nzw*JtnnW}BMPw8t-3H6t?l+kk0GHaQ8K6&6a%>eoh_66s> zGda5GSUR4q-Yu7Tp`%VG! z{4%|6O|81Vgbp)mW^3lSW!eSZI9#Y~PE9P0F1J20TbnS?9bai`SvWI)Ce^!mB-^y} zW=E!JPr{5CHO`G*T9+Ei)ORQJD|HPE^gNwx%GNOn-HNsT;?(R^QlGW1OK4Z>o9514 zYWspR7&{XC^Ua+pZ>D)e;+ed)`C@D~cHuAb&d$`XjI%H29LhL{vd*n@x;gJWop*Gk zdNYnqImckeF_?7>&1vV3|Bd0Uv+c@`%R6M46+pP>a*tfHYK0zF?Nm!=sw=f+?hT;T z{-q}u+cV8OZw_Ue_9o0YTEnbi&YXPt@6|T{-I}3W^OokMDP!qMomw2utlx3-&8clzP zO$Q^Z8p`PSl2RK@t2E9d%u>2}<1)QTA-+5j&=^ix+O>I^9?IA4UON|n2~?$L={bAy zM1o$XJFs^7J3tKW`K)=*qAj^KDJ0#OE$QYx35=9}W>OugC)3Rvmg!!|lDkVDU94Z+ zl6DQ;Y)I42WqPjyJheRsg(|o*3TQah)(Z}Eo2a-Q^{|Vql*6L&a zSlx@lQI$z}kJjTDp#O@X@NZSxQa?)XQx*Q3wr{LJ23{YDkNF$I7d1VldJk{EOmeEG ziFl+J30pL>K1i28sa>@qA1Ljzns<$8^7Lk5F&*#*eNiyLaEuTQg}k5_FH^RoMT1*oH|1LGJ?4^%!;EYTM0L8{(psC2Y-}kh*s&tA9p&|W5guHLCL_QE zfH4M33x19TYX{bOp15h2`H5lXLBEfLB;|c%aE-F zjKRyYmtgH4Ks!V~aiA<%Jy@uPW+i!4P!g8SGIE=xxMWb^(E@|b0{;cxUr`p!2VNw1 znDz23_y!DF(erp(%*bWx5zFhs-P+*fFk<<9wKyT{tG6eq3J z1Yz~$!S4sF2O6;mq08)1f;R+qk*Wxg+K=p80GkmsFUSTp0sLCM?AlpJT>43;$V|c< zfQKA9_m$?GiJS%(Q}7vlct!72$t&<1Rph{UMJ67fOz-y1Ahv*!069r;Idu~(`WQZP z8to8Zru2>ih|I6!0w{PdV5aQVgRFfa1&qC! z%yo#+s~{nq9a0io-9|nH2c@Kee)zwJ6#o`^g*JE|Q+m0k>?c4gF1-aGiqf^W)^VcN z1D?mg{|g>Gn}iwWM5R%AU@gRdgsz0Yg$P*djnumC)S>qdUp@SzXLH@#GTqy<-8-`F zJ6GB|Q`_I$eRcPb_Pn?6>b`8(wrtz>mDcv8cxC$X^gFL-TQ{w=w1FSR#N~;10@)V# zBj21%%Ko17s`E!}S?AzNv+Lbc=xecf@ckp#j$A)_vpd_iFWdaYLkm8)nd(fdl*wo! z_81V(&|g(D7OkA%sm+5U_!Lrl91Wblg0dV2Nl~bwAdJBbT%QY+A50aGc9bj{GO!j2 z)%=2#i!M%sT8cQ}&1v|T#o`rR&1q6BUa+yEX=Gd_78HJOyb7UWmi%* zgEGxjP^N(y0}L!B9I9QkM&+sl<$SA_ikqQEX{4yWs0cB0tf(djm5~|xvlRLR=(y$4 zMhxi1PI|GFx$c=29O5;x8di($i*suJqcY5^m|kKvWt1u!TS~~Tp;Yaku`O%0d6Ti` zcYz8Ly`UxaUq5YhSzcfz88{lUXyA-%sU93K4R!m-C zIN3%~#AIlXH^!S{P4c`7stYx-hN`A6I}3)JHA-k(Rk+4VxC=YLn9%@OJyW(edlbA% z&yooX%tEETTn(~lG>f;yngd|?sSK=^m>P63%{vsU`=OecbEvAy{^nSVLSZl@=>n*G ztO&8zz+godyejmDbHQ=Z>C^Z@k^Cx%2IL%dfRP6K*2f(0P(RefoR`!WtY8w<#GP@6 zvSQ#=(efL>v0~`K*1=#@!pl`={Bo7mcKszCbH(IQ8j7QYnXL@`cY#iiGI_N;vQ+m6 zj~~{XGJdgWHj6pMqDd@(Q&ABV`tzy_9We)?)p~_iEd8z~-X`u-ssx@;Ql)3CjWq%l zs~D!p4B$MNVrtg>j+V9j(4h1N)K^tAR4MZc<+`Goz*IC7Sc<7qo{}Q7TPQE2-8HiN zwWRe$SudCj&>)IV*yn()C-#Gx+ClkaiUKn&sRm~n2FqnCqD2ILh=HoIo3&16dMw~} zG&snEAD5pWlysiMpyih|n|HG_+JVV4L3bB%u|)kbe6vxD?xg1!Xw46$PqhY)jxjAmx+bQYXqTGToSA@`D_OLJ*K> z4l?+8j8LqHvA~yVJX*Z!$Gk%8A&oTN1M1XF1EP~7_=)GmvbIByXAipY^ zO0-b&(X_+=80*5fCo`a2R)NA83dkd-$#ibXc5chi>px;X<@A>f$A5TC3((<}*&skR5yRMwM zd}gutqxByTd^nIk{^D}`OLOMCwx+q)v$n33_fy-3{LUwHJD<+%d^)@H@Z8aK$JQTj zO}(D&+M2d+%WvQJ6VC^p?DnVTo=r1DKkiBeel(c2Z@GKu$gSb*p%*@Lz4(dh-BY=? z{!Cl{V(@m`p0w-5IU__X2cEg*$R0TPne)_5@4JU`uD*<`Z}H{ZuH9+psW}>=yLI*j z>%28(T->`{w`-;FMlWt!uG?|f)}FI%$k;aAwz=hFi|aCE8T9Cdlt`RyN8p;m9AbCcNRypUAvMr%=(^#s*eXh9K6+$ z89tiSru50F)L*3Ac4S(1LSe`H9J4jUY+X8hb1KU`lhox~TvyDO&7U0lhi8BK?9bt2 zhUY&#cys^Fvy1xF)Z*Et`Xx=uv*gXJhYuW{Uv4?^xt?-$<$E^fdiG~}_J88O^-{Ly zh2+yKu8!1(cSrK;deZB*^+pe`;UzhLQ z{J!Oy<$7&?&%Td`KOFx1y*YQ_gKbMgOWt42q(@&~e)V+j)rrij6Str9FLo>*zt){~ z2R=7w-R;T4U{q=AzH;>P(LCeMcMdFy?@wQwUOaQN{^pLGy`QYVWlTSR>XSFpFN~&{ zm%r3$+S*r5;Bj}+JZrvC1CI9z^H*O?tEk2i)mQ(;-v8AX4yx@X)mM`9)K~WhsP!jQ zU)^_58_%eOjj)}5+V2`^)BbFI^GK)mUk|uOHfVp@>Kf_O{+lfgBSWTNZRs4@Y5I4K zI*7q^g$}op3HLdCKnvc%k_D6rlTlHA^WpKT1Uv;{qf+<(24eWSLnAw7#r2RPUx~&I zz?&=3WlvUf2BZhU4o;q0fI!oAMF38v-2L-mtpLBpqZ?AWv{`70)!isccz}^AP~_uE zUB!wYVexHnYty9?B^59$snRp1kColrVHeb_A~k+}P2iTQe)vV)U=8R*1e|IeNXvA| zF*Q&{Is|7um5kkXbXmbx?ImoLRH;>vVT;ao4;gh8*-z<%HIPr;24Zvo{Y5JRv{yx@ zs@_XX2fUK;hUvOiZbaz;q>FO=s?qk(k6)Eu06&nstK#&r9P{#%uQH%$fBDJ2 zlhDT0t9{IP1U^PUK8oRAfGRx741|A_Y=pyD`-!|%SC&bDR0r6Nyj}<}W zCf#;$IDzk*_+@PQuMxHuI6B$InVd@e5iA+N=vj;oV1zpy4fWs?P5h;04-H(8{B4Z> zJ4Seb@t>PNUM8o_?kaC-3_U(UQ(f`Ege_@0scbS`GASF4+LJy~(gs?a=0X1GF z&Pe8l>_BeiQw`7*a+4nTtZ}C2L5`>z+eGK&b%&TUY9a0n>Ufq0$mx43qB6az{i1&>D~jkEeG%5*Mb$< z%aCoh&xs4M`B=u>l{%X>_vUSliGz3R8y6h&jDz#8pDFL5;cYje!@46}WS&Fy$9v*Rha7*BBN z$hkDLJ-CJNURwZPn4MJyc6;-s#G>w0&-@)$P4gZ(r5H z6V_H!>#6}$G}X|wie6-7zp8>Aw@C7QsD>#9y8ptb6nHuKSqscjA zY`EhKr%ejjr_e?s+?N@q_+Rw;ab(j`A{3Gy@j zs<)5`oxM*WRzc!-I6trH(rvqAZC};F6EJCPP1=&&mEh%Ix^*Lbai)dV>q zNe`cBf)!7myC~+=m%O^*kA|4h~Y znp*cyRKq_}mS0m0#Q&8V_|MeFU*rGY&-EIWBc*=Nbk%g9f+UbzZT_u#qY7pkqWc+pvrz{C literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/skip_nan_step.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/skip_nan_step.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..171f0b20d42238235150ff34a99a813a0800dbfb GIT binary patch literal 4307 zcma)9Uu+Y}8K1Si_WEB;hyw|5+=hhMn*=9!t++M>3QBq$2MsMr@2a$DyV)H(cGkP@ z>>5bwNVGy~knRLLR8XC&9um?~aSuG+<2@qv1&1I`Pun!b30cL{Fm10ZfA4QZT?irgH>QQD{b=6r1S z7Xx!VTl4EdpaaFwT!_5~bfFlY3v0qU-aDx~RGc?la&{+}1o0zXL%rn&UJC!f|tc zjn{&!D9dTSYw@{27-^v}YX8#&&3}!b<6qzLxU{eq01iO8!p((dkQ8yEGmH7sv@$(o zk4g zX<3;_M1=7nila>G#G)#7l~v*BD4^H%rW-sOnfT8WJf4!}apI zE|x847OSz;I$V3QpD>G=AWp+d2ot z@44H^Zf;%oRnW%}w+|XC9I9{?BsWiM^sNzp2x1uC8sUS`5|aGW?-I%@@eXg5OPDx< zEW=f8S$4X1P>2(!YLPW;W0l6~p6wcg_p)J2ekWjIJxgMM?Sy4n)fLNH_?=)7-ZFtr+ZD)eswCsLa_hL@)GK7|*s9S0K|g{GCf6@)6j z2aWwgBOAB&T2{9D+fah-94`I;p%8(2M^n}%>Jh%_S?Ub7Nz?_0&s$(_#}BSf>AaC8 zarlB1APJb9aM9GTE^AuJ3DB(Og!2#{Zc3-9G!jTB<=kx=@;uI(1cNQr#mfrOmJhaT z`}O@XIA@K+<0^U)L$S^q$F3i{X+Mb`s71wUR9uf9x`#hoyu0{t;K|@SwZUW6!DH)# z$2a;8uTDLU9jyCN>IB&h$JKWIgkXV1QW~EB(-x@E)D4_#3<71wfbx>#H2>#;2Mrsp z1Z|4VMnUkX)?5kNO*smz0=M=kRiKfwZG6sq=p3@C!r7<<3sj}sh8O>y_pF54pNG^Nx!+B3=$Vti@f`>VazgRO9Ok4G2wc~oom zZbIS}u5h5W?olgY!&l)}!j*90O>eC=d(uv!ixNUBkxHZ>wst-0D)-T@op%cT9#3ob zFgf(mZ~#@H*cXzmU60a2%i!^Xbaf?SOX zpvUc_VULNOF?K$)_zSQ{zdh_>Y0;feg9I@qVZ0^BZ@K?&Zz(aIb8+iGXb=$60ky0t z(4z%4d?~MDpnPVD6_e*QgDt<=>VP1nvd%}b#b$yKm(wGxUYBS$@M?(^(`|KiTztt% z%SUFt5?nScn^w&6W|4iRvl1c6h|5Fmm3bRFbPlh`5zV+&9^}ZpEA1iF3!I=$iwR-r z&I!=g)QLb*m)#!35iYP^DZxq+$}F**0By}29>7eQ(5m2sAc4|; z#I3Kf3s7?NMf^iTOd>^DZYDGuop4jHPS{cxKxQ3Ee3atJ7F0t_2#r-ophZ2aD>1ucSN%zodF|ztLOIkt?_D?v;m^Of9w5Pa-fUj*EOi=1LJfs+d zBq!*?+wfTKc8@SO$(;9oY?1HKy@B>Uoq$I-JUnym!i-0g?^4#J%c>MhnbY9>GcDiW zE`dNhU;{!AI^c*byWt3QOqPFDR`kYBNR~BIm1Wj9lKoU%+NwZ%?6$fphFl?oqISWm zMf2fF79fPS2JArsgE_N9#1b`#nGu@C(j zxgVvC%u8XyRq_dP241PItO0m{w{qM!BI5eLMzODu_%|edg@#@PPjg)V&Ga{j&J5+> DY3@1{ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/termination_signal_checkpoint.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/termination_signal_checkpoint.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f73829e2221627caaf2cf04d9ad07cbbecef771f GIT binary patch literal 8169 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 z zDNAic;>+`5LCAKG*Qc&+qOI2vEc&k)&C7cjDe#w-oP3p@<#>$QG6cE$-pv=iOgEwW zaQmgkzsFFx(;nR7DFV@Y$u>2)z9*ZLr*0b6DqX-@oAJ$aJQ}MN0oHSN4r3;S=mj0& z7A{Zpis0z3{QKKK!X2%%v6km?>$G&-75U?luGLR&zR{FAw~kK4^-Y&4e<9|wA~&-2 z6Pqt|rgeoNm^HdIw${nM;Jb112#msFM#}FcbUXsN4kLN=Vun?)y(lgqUIOnl1*7P- z-4neeH^;OjBSYm!1W~-1c`>Zwa?j{>@tBLk%NFve#b|EJZF*7x(U*ysG~2};DBDK_ zq%_r45$xe5*-PUArCM`?TFkT^C-5~Ca3Vk~g5i!sW}Z9I(2Z^nLj|D9-KTaQk0zIy-a$M4*K=i%9v6O&6PCYOh& zR?}PmQ|i7a-IW@Bg=MMuBq8kT|H+XLkAHA{rGIp(e{`k)_)`DzhuX@iD@*w8zw*h> zZ{A6-rg!`#wzBJuCH$t}czEJB-RD0|_uhN^?%S(dU;DN3ulrWsQkU@iy4rYcc1;lT z;*r%Or+%`sm+-F_URThEdly}d&_9H^J7sdMN%_R5Bgya86s~$i?*OQ)mnY)=E72dc0*7`_W+5g7U{x_DTqanZg zEGH{?R_R|UF~xhC#PV2`8A(W)KK5<4^=DG9G-`@DSqc~ z?aeM=5W9#31QUrNov%`vsAF_tV#NQpvFWyRoI=+ZJ2fNkUc(0+ zns|wl(@5&+_TFcZ(IP3q-lmzHj4_nFggr~I1MQU1n~4QBr|2gx&pXnN)lLrgD3Bt&2*Mu>I&-l1eGzg~7FM zDy4<(JNg?0?Bbx}zYcIMWSR7wfG{cGJ+ z@)x3#zYt1~C&WnnlK8)p7&k>Op`7k)51Rzlgjb;k$pc;tS{_6PXQ_0Q`s18g-d6t& zCC4wiIsPKUB^ZvLzuDw!_{VDyNbG2D%7w2kjILCC>-&OF2E7A(A)WBqb&AcPO6Jdb z4GDakDE>iyU5x#)DhlG>&xPb~h0Je-)aOF_zl7ZHggw6#2A?FZi=ruhH1J1(7x=RO E2eYq%l>h($ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/training_stats.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/training_stats.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88f0ce8c32ab99b05c27030531774e5733167afc GIT binary patch literal 15494 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 W4Yz#GzR1uQ>G|{jLXilO=l=pfBbx63 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/wandb_log.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/wandb_log.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d2c4555fb375b9a65df43391648a0a8e6357d7b GIT binary patch literal 11074 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 CYEA?I literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/__pycache__/wandb_log_eval.cpython-312.pyc b/cosmos_training/cosmos/callbacks/__pycache__/wandb_log_eval.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8d291f3d2f28afa296073bbf83db19289f8501b GIT binary patch literal 7357 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 BEJOeR literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/callbacks/compile_tokenizer.py b/cosmos_training/cosmos/callbacks/compile_tokenizer.py new file mode 100644 index 00000000..b27198fc --- /dev/null +++ b/cosmos_training/cosmos/callbacks/compile_tokenizer.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Training callback that defers AOT compilation of the VAE tokenizer. + +The actual compilation logic lives in +:meth:`~projects.cosmos3.vfm.tokenizers.wan2pt2_vae_4x16x16.Wan2pt2VAEInterface.compile_encode`. +This module provides a :class:`CompileTokenizer` callback that invokes it +at the right point during training (after ``compile_after_iterations`` +steps, to avoid NCCL timeouts during CUDA/cuDNN warm-up). + +Typical config usage +-------------------- +.. code-block:: python + + CompileTokenizer( + enabled=True, + compile_after_iterations=3, + warmup_resolutions=["256", "480", "720"], + ) +""" + +from collections.abc import Sequence + +import torch + +from cosmos.utils import log +from cosmos.utils.callback import Callback +from cosmos.model.vfm.omni_mot_model import OmniMoTModel + + +class CompileTokenizer(Callback): + """Training callback that defers AOT compilation of the VAE tokenizer. + + Hooks into ``on_training_step_start``. On the + ``compile_after_iterations``-th step it calls + ``Wan2pt2VAEInterface.compile_encode`` to compile and load all chunk + variants. Every subsequent step is a no-op. + """ + + def __init__( + self, + enabled: bool = False, + compile_after_iterations: int = 3, + warmup_resolutions: Sequence[str] | None = None, + ): + """ + Args: + enabled: Master switch. When ``False`` the callback is a + complete no-op and no compilation occurs. + compile_after_iterations: How many training steps to skip + before triggering compilation. The default (3) lets CUDA + context setup and Transformer compilation finish first. + warmup_resolutions: Resolution keys (e.g. ``["256", "480", "720"]``) + to AOT-compile. Should include every resolution used in + training. Must be a non-empty list when *enabled* is ``True``. + """ + super().__init__() + self.enabled: bool = enabled + self.compile_after_iterations: int = compile_after_iterations + self.skip_counter: int = 0 + self.warmup_resolutions: Sequence[str] | None = warmup_resolutions + + if self.enabled: + if self.warmup_resolutions is None: + raise ValueError("warmup_resolutions must be provided when enabled, got None") + if len(self.warmup_resolutions) == 0: + raise ValueError("warmup_resolutions must be a non-empty list when enabled, got an empty list") + + def on_training_step_start( + self, model: OmniMoTModel, data_batch: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + """Called at the start of every training step. + + On the ``compile_after_iterations``-th call, triggers AOT compilation + via ``tokenizer.compile_encode``. + + Args: + model: The OmniMoTModel whose ``tokenizer_vision_gen`` will be compiled. + data_batch: Current training batch (unused, required by Callback API). + iteration: Current training iteration (unused; we track our own counter + via ``skip_counter`` because this callback may be registered after + iteration 0). + """ + if not self.enabled: + return + + tokenizer = model.tokenizer_vision_gen + + if isinstance(tokenizer, torch.jit.ScriptModule): + log.critical( + f"The Tokenizer model {type(tokenizer)} is a JIT model, " + "which is not compilable. The Tokenizer will not be compiled.", + rank0_only=False, + ) + self.enabled = False + return + + if self.skip_counter == self.compile_after_iterations: + if self.warmup_resolutions is not None: + tokenizer.compile_encode( + self.warmup_resolutions, + output_dir=self.config.job.path_local, + ) + + self.skip_counter += 1 diff --git a/cosmos_training/cosmos/callbacks/data_stats.py b/cosmos_training/cosmos/callbacks/data_stats.py new file mode 100644 index 00000000..237a3876 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/data_stats.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed +from cosmos.utils.callback import Callback + + +class DataStatsCallback(Callback): + def __init__( + self, + logging_iter_multipler: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.logging_iter_multipler = logging_iter_multipler + assert self.logging_iter_multipler > 0, "logging_iter_multipler should be greater than 0" + self.save_s3 = save_s3 + self.wandb_extra_tag = f"@{logging_iter_multipler}" if logging_iter_multipler > 1 else "" + self.name = "data_stats" + self.wandb_extra_tag + self.data_freq_current = {} + self.data_freq_acc = {} + self.avg_num_assistant_tokens = [] + self.avg_num_real_tokens = [] + self.max_num_real_tokens = [] + self.min_num_real_tokens = [] + + # Per-dataset token length tracking + self.dataset_token_lengths = {} # dataset_name -> list of avg_num_real_tokens + self.dataset_assistant_tokens = {} # dataset_name -> list of avg_num_assistant_tokens + self.num_log_current = 0 + self.total_count_acc = {} + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self.num_log_current += 1 + dataset_name = data_batch.get("dataset_name", "default") + + # Handle case where dataset_name gets batched into a list + if isinstance(dataset_name, list): + + assert len(dataset_name) == 1, "dataset_name should be a list of 1" + dataset_name = dataset_name[0] + + if dataset_name in ["default"] and "__url__" in data_batch: + # try to get the name from url + dataset_name = "/".join(data_batch["__url__"][0].split("/")[:-1]) + + if dataset_name not in self.data_freq_current: + self.data_freq_current[dataset_name] = torch.tensor(0, device="cuda") # [] + self.data_freq_current[dataset_name] += 1 + + if dataset_name not in self.data_freq_acc: + self.data_freq_acc[dataset_name] = torch.tensor(0, device="cuda") # [] + self.data_freq_acc[dataset_name] += 1 + + if "avg_num_assistant_tokens" in output_batch: + self.avg_num_assistant_tokens.append(output_batch["avg_num_assistant_tokens"]) + # Track per-dataset assistant tokens + if dataset_name not in self.dataset_assistant_tokens: + self.dataset_assistant_tokens[dataset_name] = [] + self.dataset_assistant_tokens[dataset_name].append(output_batch["avg_num_assistant_tokens"]) + if "avg_num_real_tokens" in output_batch: + self.avg_num_real_tokens.append(output_batch["avg_num_real_tokens"]) + # Track per-dataset token lengths + if dataset_name not in self.dataset_token_lengths: + self.dataset_token_lengths[dataset_name] = [] + self.dataset_token_lengths[dataset_name].append(output_batch["avg_num_real_tokens"]) + if "max_num_real_tokens" in output_batch: + self.max_num_real_tokens.append(output_batch["max_num_real_tokens"]) + if "min_num_real_tokens" in output_batch: + self.min_num_real_tokens.append(output_batch["min_num_real_tokens"]) + + if iteration % (self.config.trainer.logging_iter * self.logging_iter_multipler) == 0: + # Step 1: Gather all dataset names across ranks + local_dataset_names = list(self.data_freq_current.keys()) + all_dataset_names = [None for _ in range(dist.get_world_size())] + dist.all_gather_object(all_dataset_names, local_dataset_names) + + # Step 2: Create the union of all dataset names + union_dataset_names = set() + for names in all_dataset_names: + union_dataset_names.update(names) + union_dataset_names = sorted(list(union_dataset_names)) + + # Step 3: For any missing dataset name, add dummy _LossRecord with NaN loss + for dataset_name in union_dataset_names: + if dataset_name not in self.data_freq_acc: + self.data_freq_acc[dataset_name] = torch.tensor(0, device="cuda") # [] + if dataset_name not in self.data_freq_current: + self.data_freq_current[dataset_name] = torch.tensor(0, device="cuda") # [] + + # Step 4: Calculate the total count of each dataset across all ranks + total_count_current = {} + for dataset_name in union_dataset_names: + acc_tensor = self.data_freq_acc[dataset_name].clone() + current_tensor = self.data_freq_current[dataset_name].clone() + + dist.all_reduce(acc_tensor, op=dist.ReduceOp.SUM) + dist.all_reduce(current_tensor, op=dist.ReduceOp.SUM) + + self.total_count_acc[dataset_name] = acc_tensor.item() + total_count_current[dataset_name] = current_tensor.item() + + if distributed.is_rank0() and wandb.run is not None: + info = {} + if len(self.avg_num_assistant_tokens) > 0: + info["data_stats_tokens/avg_num_assistant_tokens"] = sum(self.avg_num_assistant_tokens) / len( + self.avg_num_assistant_tokens + ) + self.avg_num_assistant_tokens = [] + if len(self.avg_num_real_tokens) > 0: + info["data_stats_tokens/avg_num_real_tokens"] = sum(self.avg_num_real_tokens) / len( + self.avg_num_real_tokens + ) + self.avg_num_real_tokens = [] + if len(self.max_num_real_tokens) > 0: + info["data_stats_tokens/max_num_real_tokens"] = max(self.max_num_real_tokens) + self.max_num_real_tokens = [] + + if len(self.min_num_real_tokens) > 0: + info["data_stats_tokens/min_num_real_tokens"] = min(self.min_num_real_tokens) + self.min_num_real_tokens = [] + + # Log per-dataset average token lengths + for dataset_name in union_dataset_names: + if dataset_name in self.dataset_token_lengths and len(self.dataset_token_lengths[dataset_name]) > 0: + avg_token_length = sum(self.dataset_token_lengths[dataset_name]) / len( + self.dataset_token_lengths[dataset_name] + ) + info[f"data_stats_avg_tokens_per_dataset/{dataset_name}"] = avg_token_length + if ( + dataset_name in self.dataset_assistant_tokens + and len(self.dataset_assistant_tokens[dataset_name]) > 0 + ): + avg_assistant_tokens = sum(self.dataset_assistant_tokens[dataset_name]) / len( + self.dataset_assistant_tokens[dataset_name] + ) + info[f"data_stats_avg_assistant_tokens_per_dataset/{dataset_name}"] = avg_assistant_tokens + + # Reset per-dataset token lengths after logging + self.dataset_token_lengths = {} + self.dataset_assistant_tokens = {} + + # Log the valid count per dataset + for dataset_name in union_dataset_names: + info[f"data_stats_count_acc/{dataset_name}"] = self.total_count_acc[dataset_name] + info[f"data_stats_count_current/{dataset_name}"] = total_count_current[dataset_name] + self.num_log_current = 0 + + wandb.log(info, step=iteration) + + # Create a table of the data stats, columns: Dataset, Accumulated frequency, Current frequency, Accumulated Count, Current Count + table_html = "" + total_count_acc_sum = sum(self.total_count_acc.values()) + total_count_current_sum = sum(total_count_current.values()) + # Sort union_dataset_names by total_count_acc, from highest to lowest + union_dataset_names = sorted(union_dataset_names, key=lambda x: self.total_count_acc[x], reverse=True) + acc_freq_list = [] + current_freq_list = [] + for name in union_dataset_names: + acc_freq = self.total_count_acc[name] / total_count_acc_sum + acc_freq_list.append(acc_freq) + current_freq = total_count_current[name] / total_count_current_sum + current_freq_list.append(current_freq) + table_html += f"" + # Sum over all dataset for each column + acc_freq_sum = sum(acc_freq_list) + current_freq_sum = sum(current_freq_list) + table_html += f"" + + table_html += "
DatasetAccumulated frequencyCurrent frequencyAccumulated CountCurrent Count
{name}{acc_freq}{current_freq}{self.total_count_acc[name]}{total_count_current[name]}
Total ({len(union_dataset_names)}){acc_freq_sum}{current_freq_sum}{total_count_acc_sum}{total_count_current_sum}
" + wandb.log({"table_data_stats/html": wandb.Html(table_html)}, step=iteration) + # Reset self.data_freq_current + self.data_freq_current = {k: v * 0 for k, v in self.data_freq_current.items()} + if ( + distributed.is_rank0() + and wandb.run is not None + and iteration in [100, 1000, 2000, 5000, 15000, 30000] + and len(self.total_count_acc) + ): + # log a table of the total_count_acc + # Sort self.total_count_acc by value, from highest to lowest + sorted_total_count_acc = sorted(self.total_count_acc.items(), key=lambda x: x[1], reverse=True) + table = wandb.Table(data=[[k, v] for k, v in sorted_total_count_acc], columns=["Dataset", "Count"]) + + wandb.log( + { + f"data_counts_bar_{iteration:09d}": wandb.plot.bar( + table, "Dataset", "Count", title=f"Count per Dataset iter {iteration:09d}" + ) + }, + step=iteration, + ) diff --git a/cosmos_training/cosmos/callbacks/dataloader_state.py b/cosmos_training/cosmos/callbacks/dataloader_state.py new file mode 100644 index 00000000..efcbb0bc --- /dev/null +++ b/cosmos_training/cosmos/callbacks/dataloader_state.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +import torch + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import log +from cosmos.utils.callback import Callback + + +@dataclass +class NoReplaceShardlistState: + epoch: int = 0 + index: int = 0 + + +class DataLoaderStateCallback(Callback): + checkpoint_component: str = "dataloader" + + def __init__( + self, + distributor_type: str | None = None, + ) -> None: + super().__init__() + self.distributor_type = distributor_type + self.config: Any = None + self.state: dict[int, NoReplaceShardlistState] = {} + self.verbose = True + + def _update_state_from_batch(self, data_batch: dict[str, torch.Tensor]) -> None: + worker_ids = data_batch["sample_worker_id"].tolist() # [B] + epochs = data_batch["sample_epoch"].tolist() # [B] + indices = data_batch["sample_index"].tolist() # [B] + for worker_id, epoch, index in zip(worker_ids, epochs, indices, strict=True): + if worker_id not in self.state: + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + + elif self.state[worker_id].epoch < epoch or ( + self.state[worker_id].index < index and self.state[worker_id].epoch == epoch + ): + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + + def on_training_step_batch_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.distributor_type == "no_replace": + self._update_state_from_batch(data_batch) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.distributor_type == "no_replace": + if self.verbose: + if iteration % self.config.trainer.logging_iter == 0: + msg = "\n" + for wid, state in self.state.items(): + msg += f"worker {wid}: epoch={state.epoch}, index={state.index}\n" + log.info(msg) + + def has_checkpoint_state(self) -> bool: + return self.distributor_type == "no_replace" + + def state_dict(self) -> dict[int, dict[str, int]]: + if self.distributor_type != "no_replace": + return {} + + state_dict: dict[int, dict[str, int]] = {} + for worker_id, per_worker_state in self.state.items(): + state_dict[worker_id] = {"epoch": per_worker_state.epoch, "index": per_worker_state.index} + log.info( + f"Saved dataloader state for worker {worker_id}: " + f"epoch={per_worker_state.epoch}, index={per_worker_state.index}" + ) + return state_dict + + def load_state_dict(self, state_dict: dict[int, dict[str, int]]) -> None: + if self.distributor_type != "no_replace": + return + + if not state_dict: + log.info("No dataloader state found in checkpoint") + return + + self.state = {} + for worker_id, per_worker_state in state_dict.items(): + epoch = per_worker_state["epoch"] + index = per_worker_state["index"] + self.state[worker_id] = NoReplaceShardlistState(epoch=epoch, index=index) + os.environ[f"NSL_STATE_WORKER_{worker_id}_EPOCH"] = str(epoch) + os.environ[f"NSL_STATE_WORKER_{worker_id}_INDEX"] = str(index) + log.info(f"Loaded no replace dataloader state for worker {worker_id}: epoch={epoch}, index={index}") diff --git a/cosmos_training/cosmos/callbacks/dataloading_monitor.py b/cosmos_training/cosmos/callbacks/dataloading_monitor.py new file mode 100644 index 00000000..23bffbd0 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/dataloading_monitor.py @@ -0,0 +1,538 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from collections import defaultdict + +import psutil +import torch +import torch.distributed as dist +import wandb + +from imaginaire.datasets.webdataset.utils.stream import ( + ENABLE_STREAM_WANDB, + WATCHDOG_ENABLED, + collect_throughput_ipc_stats, +) +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed +from cosmos.utils.callback import Callback +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.joint_dataloader import _PackingMetrics + +_AGG_COUNT, _AGG_SUM, _AGG_MIN, _AGG_MAX = 0, 1, 2, 3 +_AGG_COLS = 4 + + +class DetailedDataLoadingSpeedMonitor(Callback): + def __init__( + self, + every_n: int, + step_size: int = 1, + save_s3: bool = False, + ): + self.every_n = every_n + self.step_size = step_size + self.should_run = False + self.start_dataloading_time = None + self.dataloading_time = None + self.name = self.__class__.__name__ + self.save_s3 = save_s3 + self.time_delta_list = [] + self.memory_consumption_list = [] + self.memory_consumption_percentage_list = [] + self._pending_time_delta = None + self.dataloading_time_per_dataset = {} + self._worker_batch_times = [] + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times: dict[str, list[float]] = defaultdict(list) + self._worker_times_by_ds_wid: dict[tuple[str, int], list[float]] = defaultdict(list) + self._dataset_scalar_stats: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats: dict[str, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list)) + + def on_before_dataloading(self, iteration: int = 0) -> None: + # We want to run it one iteration before on_training_step_start should_run is set to True. + global_step = iteration // self.step_size + self.should_run = (global_step + 1) % self.every_n == 0 + self.start_dataloading_time = time.time() + + def on_after_dataloading(self, iteration: int = 0) -> None: + self._pending_time_delta = time.time() - self.start_dataloading_time + self.time_delta_list.append(self._pending_time_delta) + memory = psutil.virtual_memory() + self.memory_consumption_list.append(memory.used / (1024**3)) + self.memory_consumption_percentage_list.append(memory.percent) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + dataset_name = data_batch.get("dataset_name", ["default"])[0] + if self._pending_time_delta is not None: + if dataset_name not in self.dataloading_time_per_dataset: + self.dataloading_time_per_dataset[dataset_name] = [] + self.dataloading_time_per_dataset[dataset_name].append(self._pending_time_delta) + self._pending_time_delta = None + + for batch_key, _, agg_type in _PackingMetrics.STATS_SPEC: + if batch_key not in data_batch: + continue + val = int(data_batch[batch_key]) + if agg_type == "scalar": + self._dataset_scalar_stats[batch_key][dataset_name] += val + else: + self._dataset_list_stats[batch_key][dataset_name].append(val) + + if "_worker_batch_time" in data_batch: + bt = float(data_batch["_worker_batch_time"]) + self._worker_batch_times.append(bt) + wid = int(data_batch.get("_worker_id", 0)) + self._worker_times_by_ds_wid[(dataset_name, wid)].append(bt) + if "_worker_aug_time" in data_batch: + self._worker_aug_times.append(float(data_batch["_worker_aug_time"])) + if "_worker_io_time" in data_batch: + self._worker_io_times.append(float(data_batch["_worker_io_time"])) + if "_worker_aug_step_times" in data_batch: + for step_name, t in data_batch["_worker_aug_step_times"].items(): + self._worker_aug_step_times[step_name].append(float(t)) + + if self.should_run: + # Convert list to tensor on GPU for gathering + local_times = torch.tensor(self.time_delta_list).cuda() # [num_iters] + local_memory_consumption = torch.tensor(self.memory_consumption_list).cuda() # [num_iters] + local_memory_consumption_percentage = torch.tensor( + self.memory_consumption_percentage_list + ).cuda() # [num_iters] + iteration_count = len(self.time_delta_list) + self.time_delta_list = [] # Reset the list + self.memory_consumption_list = [] + self.memory_consumption_percentage_list = [] + + # Gather all times from all ranks + # Each tensor in the list has shape (num_iterations,) + gathered_times_list = distributed.all_gather_tensor(local_times) # list of [num_iters], len=world_size + + # Stack to get shape (world_size, num_iterations) + all_times = torch.stack(gathered_times_list) # [world_size,num_iters] + + # Calculate per-rank statistics + # dim=1 is across iterations + rank_means = torch.mean(all_times, dim=1) # [world_size] + rank_maxes = torch.max(all_times, dim=1).values # [world_size] + + wandb_info = {f"{self.name}_mean/dataloading_{k:03d}": v.item() for k, v in enumerate(rank_means)} + wandb_info.update({f"{self.name}_max/dataloading_{k:03d}": v.item() for k, v in enumerate(rank_maxes)}) + + gathered_memory_consumption = distributed.all_gather_tensor( + local_memory_consumption + ) # list of [num_iters], len=world_size + gathered_memory_consumption_percentage = distributed.all_gather_tensor( + local_memory_consumption_percentage + ) # list of [num_iters], len=world_size + + wandb_info.update( + { + f"{self.name}_mean/memory_consumption_gb_{k:03d}": v.mean().item() + for k, v in enumerate(gathered_memory_consumption) + } + ) + wandb_info.update( + { + f"{self.name}_mean/memory_consumption_percentage_{k:03d}": v.mean().item() + for k, v in enumerate(gathered_memory_consumption_percentage) + } + ) + + wandb_info[f"{self.name}_mean/memory_consumption_gb_mean"] = ( + torch.stack(gathered_memory_consumption).mean().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_mean/memory_consumption_percentage_mean"] = ( + torch.stack(gathered_memory_consumption_percentage).mean().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_max/memory_consumption_gb_max"] = ( + torch.stack(gathered_memory_consumption).max().item() # [world_size,num_iters] + ) + wandb_info[f"{self.name}_max/memory_consumption_percentage_max"] = ( + torch.stack(gathered_memory_consumption_percentage).max().item() # [world_size,num_iters] + ) + + # Identify slowest rank based on mean time + slowest_dataloading_rank_id = torch.argmax(rank_means) # [] + max_dataloading = torch.max(rank_means) # [] + + # Calculate sum of max times across all iterations (new metric) + # Max across ranks for each iteration (dim=0) + max_per_iter = torch.max(all_times, dim=0).values # [num_iters] + sum_of_max_times = torch.sum(max_per_iter).item() / iteration_count + + wandb_info.update( + { + "slowest_rank/slowest_dataloading_rank": slowest_dataloading_rank_id.item(), + "slowest_rank/slowest_dataloading_time": max_dataloading.item(), + "slowest_rank/sum_of_max_dataloading_time_per_iteration": sum_of_max_times, + } + ) + + # 1. Gather and log stream throughput and watchdog reconnect stats for `stream_throughput` metrics + self._gather_and_log_stream_throughput(wandb_info) + + # Only all_gather_object to get name indices (dataset names, aug-step names, worker-balance keys) across all ranks + # Later methods 2-4 use efficient all_gather_tensor to gather tensor data, then compute statistics and log metrics + ds_index, aug_index, dswid_index = self._discover_name_indices() + + # 2.Gather and log per-dataset dataloading wait times for `dl_wait_time_per_dataset` metrics + self._gather_and_log_per_dataset_time(wandb_info, ds_index) + + # 3. Gather and log per-dataset sampling stats for `dl_packing_stats` metrics + self._gather_and_log_packing_stats(wandb_info, ds_index) + + # 4. Gather and log worker timing metrics for `dl_worker_batch_time`, `dl_worker_balance_per_dataset`, `dl_worker_augmentation` metrics + self._gather_and_log_worker_timing(wandb_info, dswid_index, aug_index) + + if wandb.run: + wandb.log(wandb_info, step=iteration) + + if self.save_s3 and distributed.is_rank0(): + easy_io.dump( + wandb_info, + f"s3://rundir/{self.name}/iter_{iteration:09d}.yaml", + ) + + def _discover_name_indices( + self, + ) -> tuple[dict[str, int], dict[str, int], dict[str, int]]: + """Discover the global union of dataset, aug-step, and worker-balance names. + + Performs a single ``all_gather_object`` call to exchange short string + lists across all ranks and returns deterministic index mappings. + + Returns: + ds_index: ``{dataset_name: col_idx}`` for per-dataset tensors. + aug_index: ``{step_name: col_idx}`` for augmentation step tensors. + dswid_index: ``{"ds|wid": col_idx}`` for worker-balance tensors. + """ + local_ds_names: set[str] = set(self.dataloading_time_per_dataset.keys()) + for key_dict in self._dataset_scalar_stats.values(): + local_ds_names.update(key_dict.keys()) + for key_dict in self._dataset_list_stats.values(): + local_ds_names.update(key_dict.keys()) + + local_names = { + "datasets": sorted(local_ds_names), + "aug_steps": sorted(self._worker_aug_step_times.keys()), + "ds_wid": sorted(f"{ds}|{wid}" for ds, wid in self._worker_times_by_ds_wid.keys()), + } + all_names: list[dict] = [{} for _ in range(dist.get_world_size())] # len=world_size + dist.all_gather_object(all_names, local_names) + + union_ds = sorted({n for r in all_names for n in r.get("datasets", [])}) + ds_index = {name: i for i, name in enumerate(union_ds)} + + union_aug = sorted({n for r in all_names for n in r.get("aug_steps", [])}) + aug_index = {name: i for i, name in enumerate(union_aug)} + + union_dswid = sorted({n for r in all_names for n in r.get("ds_wid", [])}) + dswid_index = {name: i for i, name in enumerate(union_dswid)} + + return ds_index, aug_index, dswid_index + + def _gather_and_log_per_dataset_time(self, wandb_info: dict, ds_index: dict[str, int]) -> None: + """Gather per-dataset dataloading wait times via ``all_gather_tensor``.""" + N = len(ds_index) + if N == 0: + self.dataloading_time_per_dataset = {} + return + + local_ds_time = torch.full((N,), float("nan"), dtype=torch.float64).cuda() # [num_datasets] + for ds, times in self.dataloading_time_per_dataset.items(): + if ds in ds_index: + local_ds_time[ds_index[ds]] = sum(times) / len(times) + + all_ds_time = self._gather_list_stats(local_ds_time) # [world_size, num_datasets] + for ds, i in ds_index.items(): + col = all_ds_time[:, i] # [world_size] + valid = col[~col.isnan()] # [<=world_size] + if len(valid) > 0: + wandb_info[f"dl_wait_time_per_dataset/{ds}_mean"] = valid.mean().item() + wandb_info[f"dl_wait_time_per_dataset/{ds}_max"] = valid.max().item() + + self.dataloading_time_per_dataset = {} + + def _gather_and_log_packing_stats(self, wandb_info: dict, ds_index: dict[str, int]) -> None: + """Gather packing diagnostics via ``all_gather_tensor``, driven by ``_PackingMetrics.STATS_SPEC``.""" + _STATS = "dl_packing_stats" + N = len(ds_index) + if N == 0: + self._dataset_scalar_stats = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats = defaultdict(lambda: defaultdict(list)) + return + + for batch_key, wandb_suffix, _ in _PackingMetrics.STATS_SPEC: + if batch_key == "_num_tokens": + # Token fraction: gather per-rank token sums, compute each dataset's share of total + local_v = torch.zeros(N, dtype=torch.float64).cuda() # [num_datasets] + for ds, i in ds_index.items(): + local_v[i] = self._dataset_scalar_stats.get(batch_key, {}).get(ds, 0) + all_v = self._gather_list_stats(local_v) # [world_size, num_datasets] + global_tokens = all_v.sum(dim=0) # [num_datasets] + total = global_tokens.sum().item() + for ds, i in ds_index.items(): + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}"] = global_tokens[i].item() / total if total > 0 else 0.0 + + elif batch_key == "_dropped_count": + # Dropped samples: gather per-rank counts, report global total per dataset + local_v = torch.zeros(N, dtype=torch.float64).cuda() # [num_datasets] + for ds, i in ds_index.items(): + local_v[i] = self._dataset_scalar_stats.get(batch_key, {}).get(ds, 0) + all_v = self._gather_list_stats(local_v) # [world_size, num_datasets] + for ds, i in ds_index.items(): + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_total"] = int(all_v[:, i].sum().item()) + + else: + # Per-batch distributions (_num_samples, _from_buffer, _from_workers, _buffer_size). + # Each rank packs [count, sum, min, max]; reduce to weighted global mean/min/max. + local_t = torch.full( + (N, _AGG_COLS), float("nan"), dtype=torch.float64 + ).cuda() # [num_datasets, _AGG_COLS] + for ds, i in ds_index.items(): + vals = self._dataset_list_stats.get(batch_key, {}).get(ds, []) + if vals: + local_t[i] = torch.tensor([len(vals), sum(vals), min(vals), max(vals)], dtype=torch.float64) + all_t = self._gather_list_stats(local_t) # [world_size, num_datasets, _AGG_COLS] + for ds, i in ds_index.items(): + result = self._reduce_agg_column(all_t[:, i, :]) + if result: + mean_val, min_val, max_val = result + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_mean"] = mean_val + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_min"] = min_val + wandb_info[f"{_STATS}/{ds}_{wandb_suffix}_max"] = max_val + + self._dataset_scalar_stats = defaultdict(lambda: defaultdict(int)) + self._dataset_list_stats = defaultdict(lambda: defaultdict(list)) + + def _gather_and_log_stream_throughput(self, wandb_info: dict) -> None: + """Gather stream throughput and watchdog reconnect stats via IPC files.""" + if not ENABLE_STREAM_WANDB: + return + + tp_keys = ["MBps"] + if WATCHDOG_ENABLED: + tp_keys.append("watchdog_reconnects") + tp_stats = collect_throughput_ipc_stats() + local_tp = torch.tensor([tp_stats.get(k, 0.0) for k in tp_keys]).cuda() # [num_metrics] + gathered_tp = distributed.all_gather_tensor(local_tp) # list of [num_metrics], len=world_size + all_tp = torch.stack(gathered_tp) # [world_size, num_metrics] + + for ki, k in enumerate(tp_keys): + col = all_tp[:, ki] # [world_size] + wandb_info[f"stream_throughput/{k}_mean"] = col.mean().item() + wandb_info[f"stream_throughput/{k}_min"] = col.min().item() + wandb_info[f"stream_throughput/{k}_max"] = col.max().item() + if k == "watchdog_reconnects": + wandb_info[f"stream_throughput/{k}_sum"] = col.sum().item() + + mbps_col = all_tp[:, 0] # [world_size] + slowest_throughput_rank = mbps_col.argmin().item() + wandb_info["slowest_rank/slowest_stream_throughput_rank"] = slowest_throughput_rank + + @staticmethod + def _gather_list_stats(local: torch.Tensor) -> torch.Tensor: + """all_gather_tensor + stack, returning [world_size, *local.shape].""" + return torch.stack(distributed.all_gather_tensor(local)) + + @staticmethod + def _reduce_agg_column(col: torch.Tensor) -> tuple[float, float, float] | None: + """From a [world_size, _AGG_COLS] slice, return (mean, min, max) or None if empty. + + Each row is [count, sum, min, max] from one rank. Rows with NaN count + are ranks that had no data for this key. + + Used for metrics where each rank accumulates a variable-length list of + values (e.g. samples_per_batch, buffer_size, per-aug-step times) and we + need a correct weighted global mean rather than a simple average of + per-rank means. The sum/count columns make this possible. + + Callers: ``_gather_and_log_packing_stats`` (list-type metrics) and + ``_gather_and_log_worker_timing`` (per-aug-step breakdown). + """ + valid = col[~col[:, _AGG_COUNT].isnan()] + if len(valid) == 0: + return None + total_count = valid[:, _AGG_COUNT].sum().item() + total_sum = valid[:, _AGG_SUM].sum().item() + if total_count == 0: + return None + return ( + total_sum / total_count, + valid[:, _AGG_MIN].min().item(), + valid[:, _AGG_MAX].max().item(), + ) + + def _gather_and_log_worker_timing( + self, wandb_info: dict, dswid_index: dict[str, int], aug_index: dict[str, int] + ) -> None: + """Gather worker timing from all ranks and log percentile metrics. + + All metrics here are worker-side measurements — time spent inside + DataLoader worker processes producing batches. This is different from + DetailedDataLoadingSpeedMonitor or dl_wait_time_per_dataset/ metrics which measure main-process wall-clock time, + This can help identify if the bottleneck is in the dataloader worker processes or in the main process, + for example waiting for a packed output batch from the JointDataLoader + + Logged metrics: + Section 1 – dl_worker_batch_time/ + Every individual batch time from every worker from every rank, all + thrown into one pool. One data point = one batch produced by one + worker at one step. Computes p50/p90/p99/max/mean of that pool. + Answers: What is the tail latency to produce a batch? + + Section 2 – dl_worker_balance_per_dataset/ + First computes each worker's average batch time over the logging + window. One data point = one worker's mean over several batches. + Then gathers these per-worker averages across all ranks, grouped by + dataset. Computes mean/std/min/max of those averages. + Answers: Are some workers consistently slower than others within + each sub-dataloader? + + Section 3 – dl_worker_augmentation/ + Unified augmentation profiling. Contains: + - total_aug_mean|min|max – total augmentation time per batch + - total_io_mean|min|max – I/O time per batch (batch_time minus aug_time) + - aug_fraction_mean, io_fraction_mean – what fraction of batch time is spent in augmentation vs I/O + - aug_steps/{StepName}_mean|min|max – per-augmentor-step breakdown + (e.g. VideoParsingWithFullFrames for video decode, + TextTokenizerTransform for text tokenization). + All use mean/min/max globally across all ranks. + Answers: Is the bottleneck in augmentations or downloads, and + which augmentor step dominates? + + Note: dl_packing_stats/ is logged from on_training_step_end (not here). + It reports token_fraction, samples_per_batch, from_buffer, from_workers, buffer_size, and dropped_total per dataset — useful for tuning num_workers/batch_size/prefetch per dataloader. + """ + if not self._worker_batch_times: + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times = defaultdict(list) + self._worker_times_by_ds_wid = defaultdict(list) + return + + _PERCENTILES = [0.50, 0.90, 0.99] + _PNAMES = ["p50", "p90", "p99"] + + # Gather raw batch times across all ranks + local_bt = torch.tensor(self._worker_batch_times, dtype=torch.float32).cuda() # [num_batches_local] + gathered_bt = distributed.all_gather_tensor(local_bt) # list of [num_batches_local], len=world_size + all_bt = torch.cat(gathered_bt) # [num_batches_all_ranks] + + # Section 1: global batch time percentiles + _BATCH_PREFIX = "dl_worker_batch_time" + for pval, pname in zip(_PERCENTILES, _PNAMES): + wandb_info[f"{_BATCH_PREFIX}/{pname}"] = all_bt.quantile(pval).item() + wandb_info[f"{_BATCH_PREFIX}/max"] = all_bt.max().item() + wandb_info[f"{_BATCH_PREFIX}/mean"] = all_bt.mean().item() + + # Section 2: per-dataloader worker balance + # Each rank fills its (dataset, worker_id) slots with that worker's + # mean batch time; NaN marks absent slots. After all_gather we group + # by dataset and compute cross-rank statistics. + + _BALANCE_PREFIX = "dl_worker_balance_per_dataset" + if dswid_index: + N_dswid = len(dswid_index) + local_pw = torch.full((N_dswid,), float("nan"), dtype=torch.float64).cuda() # [num_ds_worker_pairs] + for (ds_name, wid), ts in self._worker_times_by_ds_wid.items(): + key = f"{ds_name}|{wid}" + if key in dswid_index: + local_pw[dswid_index[key]] = sum(ts) / len(ts) + + all_pw = self._gather_list_stats(local_pw) # [world_size, num_ds_worker_pairs] + + # Pass 1: collect all valid per-worker means, grouped by dataset + ds_worker_vals: dict[str, list[float]] = defaultdict(list) + for key, idx in dswid_index.items(): + ds_name = key.rsplit("|", 1)[0] + col = all_pw[:, idx] # [world_size] + valid = col[~col.isnan()] # [<=world_size] + ds_worker_vals[ds_name].extend(valid.tolist()) + + # Pass 2: log per-dataset worker balance statistics + for ds_name in sorted(ds_worker_vals): + pw_means = ds_worker_vals[ds_name] + if not pw_means: + continue + pw_t = torch.tensor(pw_means, dtype=torch.float32).cuda() # [num_workers_for_ds] + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_mean"] = pw_t.mean().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_std"] = pw_t.std().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_min"] = pw_t.min().item() + wandb_info[f"{_BALANCE_PREFIX}/{ds_name}_max"] = pw_t.max().item() + + # Section 3: augmentation profiling (total aug/io + per-step breakdown) + _AUG_PREFIX = "dl_worker_augmentation" + + if self._worker_aug_times: + local_aug = torch.tensor(self._worker_aug_times, dtype=torch.float32).cuda() # [num_batches_local] + all_aug = torch.cat(distributed.all_gather_tensor(local_aug)) # [num_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/total_aug_mean"] = all_aug.mean().item() + wandb_info[f"{_AUG_PREFIX}/total_aug_min"] = all_aug.min().item() + wandb_info[f"{_AUG_PREFIX}/total_aug_max"] = all_aug.max().item() + + if self._worker_io_times: + local_io = torch.tensor(self._worker_io_times, dtype=torch.float32).cuda() # [num_batches_local] + all_io = torch.cat(distributed.all_gather_tensor(local_io)) # [num_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/total_io_mean"] = all_io.mean().item() + wandb_info[f"{_AUG_PREFIX}/total_io_min"] = all_io.min().item() + wandb_info[f"{_AUG_PREFIX}/total_io_max"] = all_io.max().item() + + if self._worker_aug_times and self._worker_batch_times: + aug_fracs = [ + a / b for a, b in zip(self._worker_aug_times, self._worker_batch_times) if b > 0 + ] # [num_valid_batches_local] + if aug_fracs: + local_fracs = torch.tensor(aug_fracs, dtype=torch.float32).cuda() # [num_valid_batches_local] + all_fracs = torch.cat(distributed.all_gather_tensor(local_fracs)) # [num_valid_batches_all_ranks] + wandb_info[f"{_AUG_PREFIX}/aug_fraction_mean"] = all_fracs.mean().item() + wandb_info[f"{_AUG_PREFIX}/io_fraction_mean"] = 1.0 - all_fracs.mean().item() + + # Per-augmentor-step breakdown (converted to all_gather_tensor) + if aug_index: + N_aug = len(aug_index) + local_aug_steps = torch.full( + (N_aug, _AGG_COLS), float("nan"), dtype=torch.float64 + ).cuda() # [num_aug_steps, _AGG_COLS] + for step_name, ts in self._worker_aug_step_times.items(): + if step_name in aug_index and ts: + local_aug_steps[aug_index[step_name]] = torch.tensor( + [len(ts), sum(ts), min(ts), max(ts)], dtype=torch.float64 + ) + + all_aug_steps = self._gather_list_stats(local_aug_steps) # [world_size, num_aug_steps, _AGG_COLS] + for step_name, idx in aug_index.items(): + result = self._reduce_agg_column(all_aug_steps[:, idx, :]) + if result: + mean_val, min_val, max_val = result + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_mean"] = mean_val + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_min"] = min_val + wandb_info[f"{_AUG_PREFIX}/aug_steps/{step_name}_max"] = max_val + + self._worker_batch_times = [] + self._worker_aug_times = [] + self._worker_io_times = [] + self._worker_aug_step_times = defaultdict(list) + self._worker_times_by_ds_wid = defaultdict(list) diff --git a/cosmos_training/cosmos/callbacks/device_monitor.py b/cosmos_training/cosmos/callbacks/device_monitor.py new file mode 100644 index 00000000..70643afc --- /dev/null +++ b/cosmos_training/cosmos/callbacks/device_monitor.py @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Any, Dict, List, Tuple + +import pandas as pd +import psutil +import pynvml +import torch +import wandb + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed, log +from cosmos.utils.easy_io import easy_io + + +def log_prof_data( + data_list: List[Dict[str, Any]], + iteration: int, +) -> Tuple[pd.DataFrame]: + # Create a table to log data with rank information + columns = ["iteration", "rank"] + list(data_list[0].keys()) + data = [] + + # Initialize dictionaries to store min and max values for each metric + min_values = {key: float("inf") for key in columns[2:]} + max_values = {key: float("-inf") for key in columns[2:]} + sum_values = {key: 0.0 for key in columns[2:]} + + count = 0 + + for _rank, prof_data in enumerate(data_list): + row = [iteration, _rank] + [prof_data[key] for key in columns[2:]] + data.append(row) + count += 1 + + # Update min, max, and sum values + for key in columns[2:]: + min_values[key] = min(min_values[key], prof_data[key]) + max_values[key] = max(max_values[key], prof_data[key]) + sum_values[key] += prof_data[key] + + # Calculate average values + avg_values = {key: sum_values[key] / count for key in columns[2:]} + + df = pd.DataFrame(data, columns=columns) + summary_df = pd.DataFrame({"Avg": avg_values, "Max": max_values, "Min": min_values}) + + if wandb.run: + # Log the table + table = wandb.Table(dataframe=df) + wandb.log({"DeviceMonitor/prof_data": table}, step=iteration) + + # Log summary statistics + summary = {} + for key in columns[2:]: + summary[f"DeviceMonitor/min_{key}"] = min_values[key] + summary[f"DeviceMonitor/max_{key}"] = max_values[key] + summary[f"DeviceMonitor/avg_{key}"] = avg_values[key] + + wandb.log(summary, step=iteration) + return df, summary_df + + +class DeviceMonitor(EveryN): + """ + A callback to monitor device (CPU/GPU) usage and log it at regular intervals. + + Args: + every_n (int, optional): The frequency at which the callback is invoked. Defaults to 200. + step_size (int, optional): The step size for the callback. Defaults to 1. + save_s3 (bool, optional): Whether to save the monitoring data to S3. Defaults to False. + """ + + def __init__( + self, + every_n: int = 200, + step_size: int = 1, + save_s3: bool = False, + upload_every_n_mul: int = 1, + log_memory_detail: bool = True, + ): + super().__init__(every_n=every_n, step_size=step_size) + self.name = self.__class__.__name__ + self.save_s3 = save_s3 + self.s3_save_fp = f"s3://rundir/{self.name}" + self.upload_every_n = upload_every_n_mul * every_n + + self.log_memory_detail = log_memory_detail + + def on_train_start(self, model, iteration=0): + torch.cuda.reset_peak_memory_stats() + self.world_size = distributed.get_world_size() + self.rank = distributed.get_rank() + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/{self.name}" + if self.rank == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"{self.name} callback: local_dir: {self.local_dir}") + + local_rank = int(os.getenv("LOCAL_RANK", 0)) + self.handle = pynvml.nvmlDeviceGetHandleByIndex(local_rank) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + cur_process = psutil.Process(os.getpid()) + # cur_process.children(recursive=True) can crash if the dataloader is constantly creating and destroying processes (e.g. calling FFmpeg). + try: + cpu_memory_usage = sum(p.memory_info().rss for p in [cur_process] + cur_process.children(recursive=True)) + except Exception as e: # e.g. psutil.NoSuchProcess + log.warning(f"Failed to get CPU memory usage with error {e}") + cpu_memory_usage = 0 + cpu_mem_gb = cpu_memory_usage / (1024**3) + + peak_gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3) + peak_gpu_mem_reserved_gb = torch.cuda.max_memory_reserved() / (1024**3) + temp = torch.cuda.temperature() + try: + power = torch.cuda.power_draw() + except Exception as e: + log.warning(f"Failed to get power draw with error {e}") + power = 0 + util = torch.cuda.utilization() + clock = torch.cuda.clock_rate() + + memory_info = pynvml.nvmlDeviceGetMemoryInfo(self.handle) + nvml_used_gpu_mem_gb = memory_info.used / (1024**3) + nvml_free_gpu_mem_gb = memory_info.free / (1024**3) + + prof_data = { + "cpu_mem_gb": cpu_mem_gb, + "peak_gpu_mem_gb": peak_gpu_mem_gb, + "peak_gpu_mem_reserved_gb": peak_gpu_mem_reserved_gb, + "nvml_used_gpu_mem_gb": nvml_used_gpu_mem_gb, + "nvml_free_gpu_mem_gb": nvml_free_gpu_mem_gb, + "temp": temp, + "power": power, + "util": util, + "clock": clock, + } + + data_list = [prof_data] * self.world_size + # this is blocking by default + if self.world_size > 1: + torch.distributed.all_gather_object(data_list, prof_data) + torch.distributed.barrier() + + df, summary_df = log_prof_data(data_list, iteration) + if self.save_s3 and self.rank == 0: + global_step = iteration // self.step_size + should_run = global_step % self.upload_every_n == 0 + if should_run: + df.to_csv(os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), index=False) + summary_df.to_csv(os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), index=True) + easy_io.copyfile_from_local( + os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), + os.path.join(self.s3_save_fp, f"prof_data_{iteration:09d}.csv"), + ) + easy_io.copyfile_from_local( + os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), + os.path.join(self.s3_save_fp, f"summary_{iteration:09d}.csv"), + ) + if self.rank == 0: + log.info(f"{self.name} Stats:\n{summary_df.to_string()}") + if self.log_memory_detail: + memory_stats = torch.cuda.memory_stats() + if wandb.run: + wandb_memory_info = {f"mem/{key}": memory_stats[key] for key in memory_stats.keys()} + wandb.log(wandb_memory_info, step=iteration) + if self.save_s3: + global_step = iteration // self.step_size + should_run = global_step % self.upload_every_n == 0 + if should_run: + easy_io.dump( + memory_stats, + os.path.join(self.s3_save_fp, f"memory_stats_{iteration:09d}.yaml"), + ) + + torch.cuda.reset_peak_memory_stats() diff --git a/cosmos_training/cosmos/callbacks/every_n.py b/cosmos_training/cosmos/callbacks/every_n.py new file mode 100644 index 00000000..575a3466 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/every_n.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +from typing import Optional + +import torch + +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback + + +class EveryN(Callback): + def __init__( + self, + every_n: Optional[int] = None, + step_size: int = 1, + barrier_after_run: bool = True, + run_at_start: bool = False, + ) -> None: + """Constructor for `EveryN`. + + Args: + every_n (int): Frequency with which callback is run during training. + step_size (int): Size of iteration step count. Default 1. + barrier_after_run (bool): Whether to have a distributed barrier after each execution. Default True, to avoid timeouts. + run_at_start (bool): Whether to run at the beginning of training. Default False. + """ + self.every_n = every_n + if self.every_n == 0: + log.warning( + f"every_n is set to 0. Callback {self.__class__.__name__} will be invoked only once in the beginning of the training. Calls happens on_training_step_end will be skipped." + ) + + self.step_size = step_size + self.barrier_after_run = barrier_after_run + self.run_at_start = run_at_start + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # every_n = 0 is a special case which means every_n_impl will be called only once in the beginning of the training + if self.every_n != 0: + trainer = self.trainer + global_step = iteration // self.step_size + should_run = (iteration == 1 and self.run_at_start) or ( + global_step % self.every_n == 0 + ) # (self.every_n - 1) + if should_run: + log.debug(f"Callback {self.__class__.__name__} fired on train_batch_end step {global_step}") + self.every_n_impl(trainer, model, data_batch, output_batch, loss, iteration) + log.debug(f"Callback {self.__class__.__name__} finished on train_batch_end step {global_step}") + # add necessary barrier to avoid timeout + if self.barrier_after_run: + distributed.barrier() + + @abstractmethod + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: ... diff --git a/cosmos_training/cosmos/callbacks/every_n_draw_sample.py b/cosmos_training/cosmos/callbacks/every_n_draw_sample.py new file mode 100644 index 00000000..c6b5a621 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/every_n_draw_sample.py @@ -0,0 +1,438 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import os +from contextlib import nullcontext +from functools import partial +from typing import List, Optional + +import numpy as np +import torch +import torch.distributed as dist +import torch.nn.functional as F +import torchvision +import torchvision.transforms.functional as torchvision_F +import wandb +from einops import rearrange + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log, misc +from cosmos.utils.easy_io import easy_io +from cosmos.tools.visualize.video import save_img_or_video +from cosmos.utils.vfm.data_utils import slice_data_batch + + +def resize_image(image: torch.Tensor, size: int = 1024) -> torch.Tensor: + """ + Resize the image to the given size. This is done so that wandb can display the image correctly. + """ + _, h, w = image.shape + ratio = size / max(h, w) + new_h, new_w = int(ratio * h), int(ratio * w) + return torchvision_F.resize(image, (new_h, new_w)) + + +def is_primitive(value): + return isinstance(value, (int, float, str, bool, type(None))) + + +def convert_to_primitive(value): + if isinstance(value, (list, tuple)): + return [convert_to_primitive(v) for v in value if is_primitive(v) or isinstance(v, (list, dict))] + elif isinstance(value, dict): + return {k: convert_to_primitive(v) for k, v in value.items() if is_primitive(v) or isinstance(v, (list, dict))} + elif is_primitive(value): + return value + else: + return "non-primitive" # Skip non-primitive types + + +def pad_images_and_cat(images: List[torch.Tensor], max_w: int, max_h: int, t_crop: int = 1) -> torch.Tensor: + """ + Pad images to a common size and concatenate them along the batch dimension. + + This function is needed because different samples in a batch can have different resolutions. + To create a unified visualization grid, all images must be padded to the same dimensions. + Images are center-padded to preserve their visual content in the middle. + + Args: + images: List of image/video tensors with shape [B, C, T, H, W]. + max_w: Target width to pad all images to. + max_h: Target height to pad all images to. + t_crop: Number of temporal frames to keep for videos. If > 1 and the image + has more than 1 frame, only the first t_crop frames are retained. + + Returns: + Concatenated tensor of padded images with shape [total_B, C, T, max_h, max_w]. + """ + padded_images = [] + for image in images: + # Pad the image to the center + padding_h = (max_h - image.shape[-2]) // 2 + padding_w = (max_w - image.shape[-1]) // 2 + padded_image = torch.nn.functional.pad( + image, (padding_w, max_w - image.shape[-1] - padding_w, padding_h, max_h - image.shape[-2] - padding_h) + ) # [B,C,T,max_h,max_w] + # Handle video case + if image.shape[2] > 1 and t_crop > 1: + padded_image = padded_image[:, :, 0:t_crop, :, :] + + padded_images.append(padded_image) + return torch.cat(padded_images, dim=0) # [total_B,C,T,max_h,max_w] (total_B = sum of batch dims) + + +class EveryNDrawSample(EveryN): + """ + This callback sample condition inputs from training data, run inference and save the results to wandb and s3. + + Args: + every_n (int): The frequency at which the callback is invoked. + step_size (int, optional): The step size for the callback. Defaults to 1. + n_viz_sample (int, optional): for each batch, min(n_viz_sample, batch_size) samples will be saved to wandb. Defaults to 3. + n_sample_to_save (int, optional): number of samples to save. The actual number of samples to save is min(n_sample_to_save, data parallel instances). Defaults to 128. + num_sampling_step (int, optional): number of sampling steps. Defaults to 35. + guidance (List[float], optional): guidance scale. Defaults to [0.0, 3.0, 7.0]. + do_x0_prediction (bool, optional): whether to do x0 prediction. Defaults to True. + n_sigmas_for_x0_prediction (int, optional): number of sigmas to use for x0 prediction. Defaults to 4. + save_s3 (bool, optional): whether to save to s3. Defaults to False. + is_ema (bool, optional): whether the callback is run for ema model. Defaults to False. + use_negative_prompt (bool, optional): whether to use negative prompt. Defaults to False. + fps (int, optional): frames per second when saving the video. Defaults to 16. + """ + + def __init__( + self, + every_n: int, + step_size: int = 1, + n_viz_sample: int = 2, + n_sample_to_save: int = 128, + num_sampling_step: int = 35, + guidance: List[float] = [0.0, 3.0, 7.0], + do_x0_prediction: bool = True, + n_sigmas_for_x0_prediction: int = 4, + save_s3: bool = False, + save_local: bool = False, + is_ema: bool = False, + use_negative_prompt: bool = False, + prompt_type: str = "t5_xxl", + fps: int = 16, + run_at_start: bool = False, + ): + # s3: # files: min(n_sample_to_save, data instance) # per file: min(batch_size, n_viz_sample) + # wandb: 1 file, # per file: min(batch_size, n_viz_sample) + super().__init__(every_n, step_size, run_at_start=run_at_start) + + self.n_viz_sample = n_viz_sample + self.n_sample_to_save = n_sample_to_save + self.save_s3 = save_s3 + self.save_local = save_local + self.do_x0_prediction = do_x0_prediction + self.n_sigmas_for_x0_prediction = n_sigmas_for_x0_prediction + self.name = self.__class__.__name__ + self.is_ema = is_ema + self.use_negative_prompt = use_negative_prompt + self.prompt_type = prompt_type + self.guidance = guidance + self.num_sampling_step = num_sampling_step + self.rank = distributed.get_rank() + self.fps = fps + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/{self.name}" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"Callback: local_dir: {self.local_dir}") + + self.data_parallel_id = self.rank + + @misc.timer("EveryNDrawSample: x0") + @torch.no_grad() + def x0_pred(self, trainer, model, data_batch, output_batch, loss, iteration): + tag = "ema" if self.is_ema else "reg" + + log.debug("starting data and condition model", rank0_only=False) + + + data_clean = model.get_data_and_condition(data_batch) + raw_data = data_clean.raw_state_vision + x0 = data_clean.x0_tokens_vision + + # Handle model parallelism if available (legacy models) + if hasattr(model, "broadcast_split_for_model_parallelsim"): + _, condition, x0, _ = model.broadcast_split_for_model_parallelsim(None, None, x0, None) + + log.debug("done data and condition model", rank0_only=False) + batch_size = len(x0) + sigmas = np.exp( + np.linspace( + math.log(model.sde.sigma_min), math.log(model.sde.sigma_max), self.n_sigmas_for_x0_prediction + 1 + )[1:] + ) + + to_show = [] + generator = torch.Generator(device="cuda") + generator.manual_seed(0) + random_noise = torch.randn(*x0.shape, generator=generator, **model.tensor_kwargs) # same shape as x0 + _ones = torch.ones(batch_size, **model.tensor_kwargs) # [B] + mse_loss_list = [] + for _, sigma in enumerate(sigmas): + x_sigma = sigma * random_noise + x0 + log.debug(f"starting denoising {sigma}", rank0_only=False) + sample = model.denoise(x_sigma, None).x0 + log.debug(f"done denoising {sigma}", rank0_only=False) + mse_loss = distributed.dist_reduce_tensor(F.mse_loss(sample, x0)) + mse_loss_list.append(mse_loss) + + if hasattr(model, "decode"): + sample = model.decode(sample) + to_show.append(sample.float().cpu()) + to_show.append( + raw_data.float().cpu(), + ) + + base_fp_wo_ext = f"{tag}_ReplicateID{self.data_parallel_id:04d}_x0_Iter{iteration:09d}" + + local_path = self.run_save(to_show, batch_size, base_fp_wo_ext) + return local_path, torch.tensor(mse_loss_list).cuda(), sigmas # [N_sigmas] + + @torch.no_grad() + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + if self.is_ema: + if not model.config.ema.enabled: + return + context = partial(model.ema_scope, "every_n_sampling") + else: + context = nullcontext + + tag = "ema" if self.is_ema else "reg" + sample_counter = getattr(trainer, "sample_counter", iteration) + batch_info = { + "data": { + k: convert_to_primitive(v) + for k, v in data_batch.items() + if is_primitive(v) or isinstance(v, (list, dict)) + }, + "sample_counter": sample_counter, + "iteration": iteration, + } + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + easy_io.dump( + batch_info, + f"s3://rundir/{self.name}/Iter{iteration:09d}/BatchInfo_ReplicateID{self.data_parallel_id:04d}_Iter{iteration:09d}.json", + ) + + log.debug("entering, every_n_impl", rank0_only=False) + with context(): + if self.do_x0_prediction: + log.debug("entering, x0_pred", rank0_only=False) + x0_img_fp, mse_loss, sigmas = self.x0_pred( + trainer, + model, + data_batch, + output_batch, + loss, + iteration, + ) + log.debug("done, x0_pred", rank0_only=False) + if self.save_s3 and self.rank == 0: + easy_io.dump( + { + "mse_loss": mse_loss.tolist(), + "sigmas": sigmas.tolist(), + "iteration": iteration, + }, + f"s3://rundir/{self.name}/{tag}_MSE_Iter{iteration:09d}.json", + ) + + log.debug("entering, sample", rank0_only=False) + sample_img_fp = self.sample( + trainer, + model, + data_batch, + output_batch, + loss, + iteration, + ) + log.debug("done, sample", rank0_only=False) + + log.debug("waiting for all ranks to finish", rank0_only=False) + dist.barrier() + if wandb.run: + sample_counter = getattr(trainer, "sample_counter", iteration) + data_type = "image" if model.is_image_batch(data_batch) else "video" + tag += f"_{data_type}" + info = { + "trainer/global_step": iteration, + "sample_counter": sample_counter, + } + if self.do_x0_prediction: + info[f"{self.name}/{tag}_x0"] = wandb.Image(x0_img_fp, caption=f"{sample_counter}") + # convert mse_loss to a dict + mse_loss = mse_loss.tolist() + info.update({f"x0_pred_mse_{tag}/Sigma{sigmas[i]:0.5f}": mse_loss[i] for i in range(len(mse_loss))}) + + info[f"{self.name}/{tag}_sample"] = wandb.Image(sample_img_fp, caption=f"{sample_counter}") + wandb.log( + info, + step=iteration, + ) + torch.cuda.empty_cache() + + @misc.timer("EveryNDrawSample: sample") + def sample(self, trainer, model, data_batch, output_batch, loss, iteration): + data_batch = slice_data_batch(data_batch, start=0, limit=self.n_viz_sample) + + tag = "ema" if self.is_ema else "reg" + + # Obtain text embeddings online + text_encoder_config = getattr(model.config, "text_encoder_config", None) + if text_encoder_config is not None and text_encoder_config.compute_online: + text_embeddings = model.text_encoder.compute_text_embeddings_online(data_batch, model.input_caption_key) + data_batch["t5_text_embeddings"] = text_embeddings + data_batch["t5_text_mask"] = torch.ones( + text_embeddings.shape[0], text_embeddings.shape[1], device="cuda" + ) # [B,N_tokens] (all tokens valid) + + data_clean = model.get_data_and_condition(data_batch) + raw_data = data_clean.raw_state_vision + x0 = data_clean.x0_tokens_vision + + # determine the number of visualized samples + n_viz_sample = min(self.n_viz_sample, data_clean.batch_size) + + # Check if this is a multi-item vision batch (image editing) + num_items = data_clean.num_vision_items_per_sample + is_multi_item = num_items is not None + + if is_multi_item: + # Image editing: raw_data is flat [src1, tgt1, src2, tgt2, ...]. + # Split into per-sample condition (source) and GT target images. + condition_images: list[torch.Tensor] = [] + gt_target_images: list[torch.Tensor] = [] + vis_offset = 0 + for sample_idx in range(data_clean.batch_size): + n_vis = num_items[sample_idx] + # First item(s) are condition, last item is generation target + + # but we need to support multiple conditions per sample in the future. Current code + # can handle this without throwing an error. + condition_images.append(raw_data[vis_offset]) # source image (1, C, 1, H, W) + gt_target_images.append(raw_data[vis_offset + n_vis - 1]) # target image (1, C, 1, H, W) + vis_offset += n_vis + + # Use target images for max_w/max_h/t_crop (generated samples match target size) + max_w = max(img.shape[-1] for img in gt_target_images) + max_h = max(img.shape[-2] for img in gt_target_images) + t_crop = min(img.shape[-3] for img in gt_target_images) + else: + max_w = max(image.shape[-1] for image in raw_data) + max_h = max(image.shape[-2] for image in raw_data) + t_crop = min(image.shape[-3] for image in raw_data) + + to_show = [] + + # Row 0 (image editing only): condition (source) images + if is_multi_item: + to_show.append(pad_images_and_cat(condition_images[:n_viz_sample], max_w, max_h, t_crop).float().cpu()) + + for guidance in self.guidance: + sample = model.generate_samples_from_batch( + data_batch, + guidance=guidance, + n_sample=n_viz_sample, + num_steps=self.num_sampling_step, + has_negative_prompt=True if self.use_negative_prompt else False, + seed=list(range(iteration, iteration + n_viz_sample)), + ) + sample_vision = sample["vision"] + assert hasattr(model, "decode") + sample_vision_decoded = [model.decode(sample_vision_i) for sample_vision_i in sample_vision] + assert len(sample_vision_decoded) == n_viz_sample + to_show.append(pad_images_and_cat(sample_vision_decoded, max_w, max_h, t_crop).float().cpu()) + + # Last row: ground truth + if is_multi_item: + # Image editing: show GT target images (not the flat raw_data which mixes src + tgt) + assert len(gt_target_images) == n_viz_sample + to_show.append(pad_images_and_cat(gt_target_images, max_w, max_h, t_crop).float().cpu()) + else: + assert len(raw_data) == n_viz_sample + to_show.append(pad_images_and_cat(raw_data, max_w, max_h, t_crop).float().cpu()) + + base_fp_wo_ext = f"{tag}_ReplicateID{self.data_parallel_id:04d}_Sample_Iter{iteration:09d}" + base_fp_wo_ext = f"Iter{iteration:09d}/{base_fp_wo_ext}" + + batch_size = data_clean.batch_size + local_path = self.run_save(to_show, batch_size, base_fp_wo_ext) + return local_path + + def run_save(self, to_show, batch_size, base_fp_wo_ext) -> Optional[str]: + to_show = (1.0 + torch.stack(to_show, dim=0).clamp(-1, 1)) / 2.0 # [N_rows,B,C,T,H,W] range [0,1] + is_single_frame = to_show.shape[3] == 1 + n_viz_sample = min(self.n_viz_sample, batch_size) + to_show = to_show[:, :n_viz_sample] + + # ! we only save first n_sample_to_save video! + video_grid = rearrange(to_show, "n b c t h w -> c t (n h) (b w)") # [C,T,N_rows*H,B*W] + if self.save_s3 and self.data_parallel_id < self.n_sample_to_save: + save_img_or_video( + video_grid, + f"s3://rundir/{self.name}/{base_fp_wo_ext}", + fps=self.fps, + ) + if self.save_local and self.data_parallel_id < self.n_sample_to_save: + local_video_path = f"{self.local_dir}/{base_fp_wo_ext}" + os.makedirs(os.path.dirname(local_video_path), exist_ok=True) + save_img_or_video(video_grid, local_video_path, fps=self.fps) + + file_base_fp = f"{base_fp_wo_ext}_resize.jpg" + local_path = f"{self.local_dir}/{file_base_fp}" + + if self.rank == 0 and wandb.run: + if is_single_frame: # image case + to_show = rearrange( + to_show[:, :n_viz_sample], + "n b c t h w -> t c (n h) (b w)", + ) # [1,C,N_rows*H,B*W] (t=1 for images) + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + # resize so that wandb can handle it + os.makedirs(os.path.dirname(local_path), exist_ok=True) + torchvision.utils.save_image(resize_image(image_grid, 1024), local_path, nrow=1, scale_each=True) + else: + to_show = to_show[:, :n_viz_sample] # [N_rows,B,C,T,H,W] + + # resize 3 frames frames so that we can display them on wandb + _T = to_show.shape[3] + three_frames_list = [0, _T // 2, _T - 1] + to_show = to_show[:, :, :, three_frames_list] # [N_rows,B,C,3,H,W] (3 sampled frames) + log_image_size = 1024 + to_show = rearrange( + to_show, + "n b c t h w -> 1 c (n h) (b t w)", + ) # [1,C,N_rows*H,B*3*W] (t=3 sampled frames) + + os.makedirs(os.path.dirname(local_path), exist_ok=True) + # resize so that wandb can handle it + image_grid = torchvision.utils.make_grid(to_show, nrow=1, padding=0, normalize=False) + os.makedirs(os.path.dirname(local_path), exist_ok=True) + torchvision.utils.save_image( + resize_image(image_grid, log_image_size), local_path, nrow=1, scale_each=True + ) + + return local_path + return None diff --git a/cosmos_training/cosmos/callbacks/expert_heatmap.py b/cosmos_training/cosmos/callbacks/expert_heatmap.py new file mode 100644 index 00000000..adf3389e --- /dev/null +++ b/cosmos_training/cosmos/callbacks/expert_heatmap.py @@ -0,0 +1,105 @@ +import matplotlib.pyplot as plt +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed +from cosmos.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def compute_expert_heatmap(vfm: torch.nn.Module) -> dict[str, torch.Tensor]: + """ + Compute the heatmap for the MoE blocks in the language model. + + The heatmap is a dictionary with keys set to ["und", "gen"] and values set to + a tensor of shape (num_layers, num_experts). + + Each element of the tensor is the average number of tokens routed to each expert for a + given layer. The sum of the elements in each row should be equal to the average number + of experts per token for the MoE model (config.num_experts_per_tok). + + For dense models, the heatmap is an empty dictionary. + """ + with torch.no_grad(): + num_layers = len(vfm.language_model.model.layers) + + example_dtensor = vfm.language_model.model.layers[0].self_attn.q_proj.weight + if isinstance(example_dtensor, DTensor): + assert hasattr(example_dtensor, "device_mesh") + device_mesh = example_dtensor.device_mesh + else: + device_mesh = None + + expert_heatmaps = {} + for tower in ["und", "gen"]: + expert_heatmaps_per_layer = [] + + for layer_idx in range(num_layers): + layer_module = vfm.language_model.model.layers[layer_idx] + mlp_module = layer_module.mlp if tower == "und" else layer_module.mlp_moe_gen + if isinstance(mlp_module, Qwen3VLMoeTextSparseMoeBlock): + # This is accumulated across all iterations. + total_tokens_per_expert = mlp_module.get_total_tokens_per_expert() + total_tokens = mlp_module.get_total_tokens() + + # Compute the average across all ranks. + assert device_mesh is not None, "MoE models require multiple GPUs." + total_tokens_per_expert = DTensor.from_local( + total_tokens_per_expert, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + total_tokens = DTensor.from_local( + total_tokens, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + mean_tokens_per_expert = total_tokens_per_expert.float() / total_tokens.float() # [num_experts] + expert_heatmaps_per_layer.append(mean_tokens_per_expert) + + if len(expert_heatmaps_per_layer) > 0: + expert_heatmaps[tower] = torch.stack(expert_heatmaps_per_layer, dim=0) # [num_layers,num_experts] + + return expert_heatmaps + + +class ExpertHeatmap(EveryN): + """ + Plots the expert heatmap for the MoE blocks in the language model. + + Args: + every_n (int): Number of iterations to log the expert heatmap. + """ + + def __init__(self, every_n: int = 1000): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + expert_heatmaps = compute_expert_heatmap(model.net) + + if distributed.is_rank0() and wandb.run: + for tower, heatmap in expert_heatmaps.items(): + fig, ax = plt.subplots() + im = ax.imshow(heatmap.cpu().numpy()) + ax.set_xlabel("Experts") + ax.set_ylabel("Layers") + plt.colorbar(im, ax=ax) + wandb.log( + { + f"expert_heatmap/{tower}": fig, + }, + step=iteration, + ) + plt.close(fig) diff --git a/cosmos_training/cosmos/callbacks/grad_clip.py b/cosmos_training/cosmos/callbacks/grad_clip.py new file mode 100644 index 00000000..7be72dcf --- /dev/null +++ b/cosmos_training/cosmos/callbacks/grad_clip.py @@ -0,0 +1,331 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from collections import defaultdict + +import torch +import wandb +from torch.distributed.tensor import DTensor +from torch.nn.parallel import DistributedDataParallel + +from cosmos.utils.callback import Callback + + +@torch.compile +def _fused_nan_to_num(grads: list[torch.Tensor]) -> None: + """Replace NaN/Inf entries with 0.0 in every floating-point grad in-place. + + The Python-level loop over ``grads`` is wrapped in ``@torch.compile`` so + Inductor can fuse the per-tensor ``nan_to_num`` ops into a single CUDA + kernel. This is NOT the ``torch._foreach_*`` API; it is fusion-via-compile + and depends on the grad list structure (length, dtypes, shapes) staying + stable across iterations so Dynamo can reuse its specialized graph. + """ + grads = [g for g in grads if torch.is_floating_point(g)] + for g in grads: + torch.nan_to_num(g, nan=0.0, posinf=0.0, neginf=0.0, out=g) + + +class _MagnitudeRecord: + def __init__(self) -> None: + self.state: torch.Tensor | None = None + self.iter_count: int = 0 + + def reset(self) -> None: + self.state = None + self.iter_count = 0 + + def update(self, cur_state: torch.Tensor) -> None: + if self.state is None: + self.state = cur_state.detach().clone() + else: + self.state.add_(cur_state) + self.iter_count += 1 + + def get_stat(self) -> float: + if self.state is not None and self.iter_count > 0: + avg_state = (self.state / self.iter_count).item() + else: + avg_state = 0.0 + self.reset() + return avg_state + + +@torch.no_grad() +def _clip_grad( + parameters: list[torch.Tensor], + max_norm: float, + norm_type: float = 2.0, + error_if_nonfinite: bool = False, + foreach: bool | None = None, + return_norm_only: bool = False, +) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + """ + Clip the gradient norm of an iterable of parameters. + + Gradient norm clipping requires computing the gradient norm over the entire model. + `torch.nn.utils.clip_grad_norm_` only computes gradient norm along DP/FSDP/TP dimensions. + We need to manually reduce the gradient norm across PP stages. + See https://github.com/pytorch/torchtitan/issues/596 for details. + + Params are grouped by their ``device_mesh`` (by mesh-dim-names string — + plain (non-DTensor) params map to ``"default"``). A scalar L2 norm is + computed per mesh group, DTensor results are reduced to local scalars + via ``.full_tensor()``, the per-mesh scalars are combined into one + global norm, and (unless ``return_norm_only=True``) every mesh group + is rescaled with that single global scalar. + + Args: + parameters: an iterable of Tensors or a single Tensor that will have gradients normalized + max_norm (float): max norm of the gradients + norm_type (float): type of the used p-norm. Can be ``'inf'`` for + infinity norm. + error_if_nonfinite (bool): if True, an error is thrown if the total + norm of the gradients from :attr:`parameters` is ``nan``, + ``inf``, or ``-inf``. Default: False (will switch to True in the future) + foreach (bool): use the faster foreach-based implementation. + If ``None``, use the foreach implementation for CUDA and CPU native tensors and silently + fall back to the slow implementation for other device types. + Default: ``None`` + return_norm_only: if True, skip in-place rescaling of grads and only + return the computed norms. + + Returns: + ``(total_norm, per_mesh_norms)`` where ``total_norm`` is the global + scalar norm used for the rescale, and ``per_mesh_norms`` maps each + mesh-dim-names key (or ``"default"`` for plain params) to its + pre-clip per-mesh L2 norm. + + """ + param_set: set[torch.Tensor] = set() + # Group the parameters by their device meshes. + parameters_by_mesh: dict[str, list[torch.Tensor]] = defaultdict(list) + for param in parameters: + if param in param_set: + continue + param_set.add(param) + if param.grad is None: + raise ValueError( + f"_clip_grad received a parameter with no gradient " + f"(shape={tuple(param.shape)}, dtype={param.dtype}); " + "callers are expected to pre-filter." + ) + + # If one parameter belongs to multiple meshes, use a flattened mesh name + # by concatenating all the mesh-dim names together. ``mesh_dim_names`` + # is ``tuple[str, ...] | None`` on DeviceMesh — fall back to ``default`` + # when names weren't assigned. + if hasattr(param, "device_mesh"): + names = param.device_mesh.mesh_dim_names + device_mesh_str = "-".join(names) if names else "default" + else: + device_mesh_str = "default" + parameters_by_mesh[device_mesh_str].append(param) + + # Compute the norm for each mesh group + per_mesh_norms: dict[str, torch.Tensor] = {} + per_mesh_norm_list = [] + for mesh, params in parameters_by_mesh.items(): + grads = [p.grad for p in params if p.grad is not None] + assert len(grads) > 0, "No gradients to compute norm" + mesh_norm = torch.nn.utils.get_total_norm(grads, norm_type, error_if_nonfinite, foreach) + + # If mesh_norm is a DTensor, the placements must be + # `torch.distributed._tensor.ops.math_ops._NormPartial`. + # We can simply reduce the DTensor to get the total norm in this + # tensor's process group and then convert it to a local tensor. + + # 1. to make sure the total norm is computed correctly when PP is used (see below) + # 2. to return a reduced mesh_norm tensor whose .item() would return the correct value + if isinstance(mesh_norm, DTensor): + # Will reach here if any non-PP parallelism is used. + # If only using PP, mesh_norm will be a local tensor. + + # Remove FT replicate dimension if it exists. + mesh_norm = mesh_norm.full_tensor() + # Expose the (rank-replicated) per-mesh scalar for diagnostic logging. + per_mesh_norms[mesh] = mesh_norm + + # Make the norm to be a 1D tensor so we can call cat() later. + if mesh_norm.ndim == 0: + mesh_norm = mesh_norm.reshape(1) + per_mesh_norm_list.append(mesh_norm) + + # Compute the total norm among all meshes. + if len(per_mesh_norm_list) > 1: + per_mesh_norm_tensor = torch.cat(per_mesh_norm_list) + if math.isinf(norm_type): + total_norm = torch.max(per_mesh_norm_tensor) + else: + per_mesh_norm_tensor **= norm_type + total_norm = torch.sum(per_mesh_norm_tensor) + total_norm **= 1.0 / norm_type + else: + assert per_mesh_norm_list[0].numel() == 1, "total_norm should be a scalar" + total_norm = per_mesh_norm_list[0].view(-1)[0] + + if not return_norm_only: + # Perform clipping on each mesh group + for mesh, params in parameters_by_mesh.items(): + torch.nn.utils.clip_grads_with_norm_(params, max_norm, total_norm, foreach) + + return total_norm, per_mesh_norms + + +class GradClip(Callback): + """Unified gradient-clipping callback for both VFM (diffusion) and VLM training. + + The heavy lifting is delegated to ``_clip_grad``: it groups + params by their ``device_mesh`` (using mesh-dim-names as the key), + computes a scalar L2 norm per mesh group (reducing any DTensor result + via ``.full_tensor()``), combines the per-mesh scalars into ONE global + norm via ``sqrt(sum(per_mesh_norm**2))``, and applies + ``torch.nn.utils.clip_grads_with_norm_`` per mesh group with the SAME + global scalar — a SINGLE GLOBAL rescale across every parameter. + + This is necessary for correctness when parameters live on multiple device + meshes (e.g. dense FSDP-shard + EP-shard MoE experts): clipping each + mesh independently with stock ``torch.nn.utils.clip_grad_norm_`` would + assign a different rescale factor per mesh and distort the relative + magnitudes of dense vs MoE updates. Under VFM's current FSDP-only + setup the math reduces to a single mesh group and is identical to + stock ``clip_grad_norm_``; this implementation is forward-correct + once EP is enabled. + + For diagnostics, the callback ALSO records pre-clip per-mesh sub-norms + alongside the actual global norm. When ``track_per_modality=True`` (VFM), + samples are bucketed by image/video via ``model.is_image_batch(data_batch)``, + producing wandb keys ``clip_grad_norm/{image|video}/{mesh_key}`` plus a + ``.../global`` synthetic key carrying the actual rescale norm. When False + (VLM), keys are ``clip_grad_norm/{mesh_key}`` plus ``clip_grad_norm/global``. + + Param-source semantics: + * ``track_per_modality=True`` (VFM): caller passes the ``OmniMoTModel``; + only ``model_ddp.net.parameters()`` is iterated, matching legacy VFM + behavior (the optimizer is built from ``self.net``). + * ``track_per_modality=False`` (VLM): caller passes a single + ``nn.Module`` or a list of model parts; each is unwrapped from + ``DistributedDataParallel`` if needed, then ``parameters()`` is + iterated and filtered by grad-presence. + + Args: + clip_norm: max norm to clip to. + force_finite: if True, NaN/Inf in any grad is zeroed in-place before + the norm computation. + track_per_modality: if True, route stats into image/video buckets via + ``model.is_image_batch(data_batch)``. If False, accumulate into a + single un-bucketed log group. + """ + + def __init__( + self, + clip_norm: float = 1.0, + force_finite: bool = True, + track_per_modality: bool = False, + ): + self.clip_norm = clip_norm + self.force_finite = force_finite + self.track_per_modality = track_per_modality + + # Outer key: modality bucket name. For VLM we use a single bucket "" so + # wandb keys are short (`clip_grad_norm/{mesh}`); for VFM the bucket is + # "image" or "video" (`clip_grad_norm/image/{mesh}`). + # Inner key: mesh string, plus the synthetic "global" key for the + # actual rescale norm returned by _clip_grad. + self._states: dict[str, dict[str, _MagnitudeRecord]] = defaultdict(lambda: defaultdict(_MagnitudeRecord)) + self._state_key: str = "" + + def on_training_step_start( + self, + model: torch.nn.Module, + data_batch: dict[str, torch.Tensor], + iteration: int = 0, + ) -> None: + if not self.track_per_modality: + return + self._state_key = "image" if model.is_image_batch(data_batch) else "video" + + def on_before_optimizer_step( + self, + model_ddp: torch.nn.Module | list[torch.nn.Module], + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + del optimizer, scheduler, grad_scaler + + # 1. Resolve which parameters to clip. + if self.track_per_modality: + # VFM: only clip `.net` params, matching legacy semantics + the + # optimizer's actual param set. + assert not isinstance(model_ddp, list), "track_per_modality=True expects a single OmniMoTModel, not a list" + model_parts = [model_ddp.module if isinstance(model_ddp, DistributedDataParallel) else model_ddp] + else: + # VLM: list of model parts (or single module). Unwrap DDP per part. + model_parts = model_ddp if isinstance(model_ddp, list) else [model_ddp] + model_parts = [m.module if isinstance(m, DistributedDataParallel) else m for m in model_parts] + + # 2. Collect params with grads. + all_params: list[torch.Tensor] = [] + for part in model_parts: + for p in part.parameters(): + if p.grad is not None: + all_params.append(p) + + # 3. No-grad / all-frozen step → skip. _clip_grad's empty + # fallback uses torch.cuda.current_device() and would crash on CPU. + if not all_params: + return + + # 4. Optionally zero NaN/Inf in grads. + if self.force_finite: + _fused_nan_to_num([p.grad for p in all_params]) + + # 5. Compute per-mesh norms, the global rescale norm, and clip in + # one call. ``_clip_grad`` groups params by mesh, + # computes per-mesh L2 norms (reducing DTensor results to local + # scalars), combines them into a single global norm, and + # rescales every mesh group with that scalar. + # + # When ``force_finite`` is False we did NOT sanitize the grads, so + # ask ``get_total_norm`` to raise rather than silently producing a + # NaN ``total_norm`` that would taint every parameter on rescale. + global_norm, per_mesh_norms = _clip_grad( + all_params, + self.clip_norm, + error_if_nonfinite=False, + foreach=True, + ) + + # 6. Record diagnostic stats: pre-clip per-mesh sub-norms plus the + # actual global rescale norm. + cur_state = self._states[self._state_key] + for mesh_str, mesh_norm in per_mesh_norms.items(): + cur_state[mesh_str].update(mesh_norm) + cur_state["global"].update(global_norm) + + # 7. Log every logging_iter. + if iteration % self.config.trainer.logging_iter == 0 and wandb.run: + log_dict: dict[str, float | int] = {"iteration": iteration} + for modality, state in self._states.items(): + for mesh_str, record in state.items(): + avg = record.get_stat() + if self.track_per_modality: + log_dict[f"clip_grad_norm/{modality}/{mesh_str}"] = avg + else: + log_dict[f"clip_grad_norm/{mesh_str}"] = avg + wandb.log(log_dict, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/heart_beat.py b/cosmos_training/cosmos/callbacks/heart_beat.py new file mode 100644 index 00000000..f16fa925 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/heart_beat.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from datetime import datetime + +import pytz +import torch + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed +from cosmos.utils.easy_io import easy_io + + +class HeartBeat(EveryN): + """ + A callback that logs a heartbeat message at regular intervals to indicate that the training process is still running. + + Args: + every_n (int): The frequency at which the callback is invoked. + step_size (int, optional): The step size for the callback. Defaults to 1. + update_interval_in_minute (int, optional): The interval in minutes for logging the heartbeat. Defaults to 20 minutes. + save_s3 (bool, optional): Whether to save the heartbeat information to S3. Defaults to False. + """ + + def __init__(self, every_n: int, step_size: int = 1, update_interval_in_minute: int = 20, save_s3: bool = False): + super().__init__(every_n=every_n, step_size=step_size) + self.name = self.__class__.__name__ + self.update_interval_in_minute = update_interval_in_minute + self.save_s3 = save_s3 + self.pst = pytz.timezone("America/Los_Angeles") + self.is_hitted = False + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.time = time.time() + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}_start.yaml") + easy_io.dump(info, f"s3://timestamps_rundir/{self.name}_start.yaml") + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if not self.is_hitted: + self.is_hitted = True + if distributed.get_rank() == 0: + self.report(iteration) + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + @distributed.rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if time.time() - self.time > 60 * self.update_interval_in_minute: + self.report(iteration) + + def report(self, iteration: int = 0): + self.time = time.time() + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}.yaml") + + @distributed.rank0_only + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + if self.save_s3: + current_time_pst = datetime.now(self.pst).strftime("%Y_%m_%d-%H_%M_%S") + info = { + "iteration": iteration, + "time": current_time_pst, + } + easy_io.dump(info, f"s3://rundir/{self.name}_end.yaml") + easy_io.dump(info, f"s3://timestamps_rundir/{self.name}_end.yaml") diff --git a/cosmos_training/cosmos/callbacks/hf_export.py b/cosmos_training/cosmos/callbacks/hf_export.py new file mode 100644 index 00000000..fcb8c379 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/hf_export.py @@ -0,0 +1,471 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""HFExportCallback: export VLM DCP checkpoints to HuggingFace safetensors format. + +Design notes +------------ +- Hooks into ``on_save_checkpoint`` (called by DistributedCheckpointer.save() before I/O). +- All ranks participate in the weight-gather phase (DTensor.full_tensor() all-gathers). +- Rank 0 accumulates CPU tensors, writes shards, and uploads — other ranks exit early. +- File I/O and upload run in a background thread on rank 0 to avoid blocking training. +- Worker exceptions are stored in ``_worker_exception`` and re-raised on the next + checkpoint or at train end, so failures are never silently swallowed. +- Controlled entirely via ``config.checkpoint.hf_export`` (HFExportConfig). + +Phase 2+ note +------------- +Weight parameters are iterated via ``model.model._model.named_parameters()`` where +``model.model`` is the ``HFModel`` wrapper and ``model.model._model`` is the underlying +HuggingFace transformer. Parameter names are already HF-native — no weight_mapper +remapping is required. +""" + +import json +import os +import shutil +import threading +from typing import Any + +import torch + +from cosmos.utils import log +from cosmos.utils.callback import Callback +from cosmos.utils.distributed import is_rank0 + +try: + from safetensors.torch import save_file as _safetensors_save_file +except ImportError: + _safetensors_save_file = None + +try: + from transformers import AutoTokenizer, GenerationConfig +except ImportError: + AutoTokenizer = None + GenerationConfig = None + +# Map string dtype names (as stored in ParallelismConfig.precision) to torch dtypes. +_DTYPE_MAP: dict[str, torch.dtype] = { + "float32": torch.float32, + "float16": torch.float16, + "bfloat16": torch.bfloat16, + "float64": torch.float64, +} + + +def _upload_folder_to_s3(local_folder: str, bucket: str, s3_prefix: str, credential_path: str) -> None: + """Upload every file under *local_folder* to ``s3://{bucket}/{s3_prefix}/...``. + + Uses the i4 ``easy_io`` S3 backend (Boto3Backend), which reads credentials from + *credential_path*. Files are uploaded as streaming transfers via boto3's + ``upload_file()`` — the full shard is never loaded into memory. + """ + from cosmos.utils.easy_io import easy_io + + backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": credential_path, + "path_mapping": None, + } + ) + for root, _, files in os.walk(local_folder): + for fname in sorted(files): + local_path = os.path.join(root, fname) + rel = os.path.relpath(local_path, local_folder) + s3_path = f"s3://{bucket}/{s3_prefix}/{rel}" + # Pass the local path string so Boto3Backend uses upload_file() — + # a streaming transfer that avoids reading the whole shard into memory. + backend.put(local_path, s3_path) + log.info(f"[HFExportCallback] Uploaded {local_path} → {s3_path}") + + +class HFExportCallback(Callback): + """Export VLM weights to HuggingFace-compatible safetensors after each DCP checkpoint. + + Enabled / configured via ``config.checkpoint.hf_export`` (HFExportConfig). Disabled + by default — add this callback and set ``hf_export.enabled = True`` to activate. + + Exports written to:: + + {job.path_local}/hf_exports/iter_{iteration:09d}/ + 00000.safetensors + ... + model.safetensors.index.json + config.json + tokenizer.json (if tokenizer can be loaded from model_name_or_path) + + Optionally uploads to: + - S3 (``hf_export.upload_to_object_store``) + - HuggingFace Hub (``hf_export.hf_repo_id``) + + Args: + dtype: Export weight dtype (e.g. ``"bfloat16"``). Use + ``"${model.config.policy.parallelism.precision}"`` in the Hydra callback config to + inherit from the training precision. + """ + + # HuggingFace convention: max 4 GB per shard file. + _MAX_SHARD_BYTES: int = 4 * 1024**3 + + def __init__(self, dtype: str = "bfloat16") -> None: + self._export_dtype: torch.dtype | None = _DTYPE_MAP.get(dtype) + self._current_iteration: int = 0 + self._export_thread: threading.Thread | None = None + # Stores any exception raised inside the background worker so it can be + # re-raised on the main thread at the next checkpoint or train end. + self._worker_exception: BaseException | None = None + + # ------------------------------------------------------------------ + # Callback hooks + # ------------------------------------------------------------------ + + def on_save_checkpoint_start(self, model: Any, iteration: int = 0) -> None: + self._current_iteration = iteration + + def on_save_checkpoint(self, model: Any, state_dict: dict[str, Any]) -> None: + hf_cfg = self.config.checkpoint.hf_export + if not hf_cfg.enabled: + return + + iteration = self._current_iteration + if iteration % hf_cfg.export_every_n != 0: + return + + # Deferred import to avoid circular dependency at module load time. + from cosmos.model.vfm.vlm_model import VLMModel + + if not isinstance(model, VLMModel): + # The legacy vlm/train.py path passes model_parts: list[nn.Module] (raw HF + # models without the VLMModel attribute structure). HF export requires the + # VLMModel wrapper, which is only available via the unified scripts/train.py path. + if isinstance(model, list): + log.warning( + "[HFExportCallback] Received model_parts (list) instead of VLMModel. " + "HF export requires the unified training path (scripts/train.py). Skipping." + ) + else: + log.warning( + "[HFExportCallback] model is not VLMModel (got %s); skipping HF export.", + type(model).__name__, + ) + return + + if _safetensors_save_file is None: + raise ImportError("safetensors is required for HFExportCallback. Install it with: pip install safetensors") + + output_dir = os.path.join(self.config.job.path_local, "hf_exports", f"iter_{iteration:09d}") + + # ---------------------------------------------------------------- + # Phase 1 (all ranks): gather sharded parameters into CPU chunks. + # full_tensor() is a collective operation — all ranks must participate. + # ---------------------------------------------------------------- + cpu_chunks, manifest, total_size = self._gather_weights(model) + + # ---------------------------------------------------------------- + # Phase 2 (rank 0, background thread): file I/O + optional upload. + # ---------------------------------------------------------------- + if not is_rank0(): + return + + # Block on any still-running export from the previous checkpoint and + # propagate any worker exception before starting a new export. + if self._export_thread is not None and self._export_thread.is_alive(): + log.warning( + "[HFExportCallback] Previous export thread still running; waiting before starting export for iter %d.", + iteration, + ) + self._export_thread.join() + + if self._worker_exception is not None: + exc = self._worker_exception + self._worker_exception = None + raise RuntimeError(f"[HFExportCallback] Previous export failed with: {exc}") from exc + + self._export_thread = threading.Thread( + target=self._save_and_upload, + args=(cpu_chunks, manifest, total_size, model.hf_config, model.model_name_or_path, output_dir, iteration), + daemon=True, + ) + self._export_thread.start() + + def on_train_end(self, model: Any, iteration: int = 0) -> None: + """Wait for the final export thread so the process does not exit prematurely.""" + if self._export_thread is not None and self._export_thread.is_alive(): + log.info("[HFExportCallback] Waiting for export thread to finish...") + self._export_thread.join() + log.info("[HFExportCallback] Export thread done.") + + if self._worker_exception is not None: + exc = self._worker_exception + self._worker_exception = None + raise RuntimeError(f"[HFExportCallback] Export thread failed with: {exc}") from exc + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _gather_weights(self, model: Any) -> tuple[list[dict[str, torch.Tensor]], dict[str, str], int]: + """Iterate model parameters, all-gather DTensor shards, and build CPU chunks. + + Must be called on **all ranks**. Only rank 0 populates the returned + ``cpu_chunks`` and ``manifest``; other ranks return empty structures but still + participate in the distributed all-gathers. + + Returns: + cpu_chunks: List of ``{weight_name: cpu_tensor}`` dicts, one per shard file. + manifest: Mapping of ``weight_name → shard_filename``. + total_size: Total byte count of all exported tensors (for the index JSON). + """ + cpu_chunks: list[dict[str, torch.Tensor]] = [] + manifest: dict[str, str] = {} + current_chunk: dict[str, torch.Tensor] = {} + current_chunk_bytes: int = 0 + total_size: int = 0 + file_idx: int = 0 + + for name, param in model.model._model.named_parameters(): + # Phase 2+: HFModel initialises _model via AutoModelForImageTextToText / + # AutoModelForCausalLM, so parameter names are HF-native and match the + # safetensors checkpoint keys loaded by _load_vlm_weights(). + # + # MoE note: Qwen3VLMoeTextExpertsGroupedMm stores expert weights in HF-native + # grouped layout — gate_up_proj: [E, H, 2F], down_proj: [E, F, H] — matching + # the checkpoint format exactly. No transposition or per-expert fan-out is + # needed. (The legacy Phase 0 path stored tensors in a transposed internal + # format [E, 2F, H] under the name "gate_and_up_projs" and required + # weight_mapper.policy_map_local_key_for_export_tensor() to transpose back on + # export. Phase 2 uses HFModel and has no such internal reformat.) + # + # torch.compile and gradient-checkpointing wrappers inject prefixes into + # named_parameters() output. Strip them so exported keys are HF-native, + # matching what HFModel._load_vlm_weights() does for the in-memory state dict. + name = name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", "") + + # Gather across FSDP / TP / CP ranks (collective — all ranks must call). + if isinstance(param, torch.distributed.tensor.DTensor): + param = param.full_tensor() + param = param.detach() + if self._export_dtype is not None: + param = param.to(dtype=self._export_dtype) + + tensor_bytes = param.element_size() * param.numel() + + # Flush the current chunk when the shard size limit would be exceeded. + # current_chunk_bytes is tracked on ALL ranks so shard boundaries are + # consistent (the shard_name written into manifest must agree everywhere). + if current_chunk_bytes + tensor_bytes > self._MAX_SHARD_BYTES and current_chunk_bytes > 0: + if is_rank0(): + cpu_chunks.append(current_chunk) + current_chunk = {} + file_idx += 1 + current_chunk_bytes = 0 + + shard_name = f"{file_idx:05d}.safetensors" + if is_rank0(): + current_chunk[name] = param.cpu() + manifest[name] = shard_name + total_size += tensor_bytes + current_chunk_bytes += tensor_bytes + + # Flush the final (possibly partial) chunk. + if current_chunk_bytes > 0 and is_rank0() and current_chunk: + cpu_chunks.append(current_chunk) + + return cpu_chunks, manifest, total_size + + def _save_and_upload( + self, + cpu_chunks: list[dict[str, torch.Tensor]], + manifest: dict[str, str], + total_size: int, + hf_config: Any, + model_name_or_path: str, + output_dir: str, + iteration: int, + ) -> None: + """Write safetensors shards, HF config, tokenizer; upload to S3 / HF Hub. + + Runs on rank 0 inside a background thread. Any exception is stored in + ``self._worker_exception`` so the main thread can re-raise it. + """ + try: + self._do_save_and_upload( + cpu_chunks, manifest, total_size, hf_config, model_name_or_path, output_dir, iteration + ) + except Exception as exc: + log.error( + "[HFExportCallback] Export worker for iter %d raised an exception: %s", + iteration, + exc, + exc_info=True, + ) + self._worker_exception = exc + + def _do_save_and_upload( + self, + cpu_chunks: list[dict[str, torch.Tensor]], + manifest: dict[str, str], + total_size: int, + hf_config: Any, + model_name_or_path: str, + output_dir: str, + iteration: int, + ) -> None: + """Core export logic (called from the background thread via ``_save_and_upload``). + + Error handling is tiered: + - Steps 1-4 (shards, index JSON, HF config, source-model file copy): any exception + propagates to the outer ``_save_and_upload`` wrapper so the main thread is notified. + A failed file copy leaves the checkpoint unusable for trust_remote_code models, so + it is treated as a hard failure like the shard writes. + - Steps 5-7 (tokenizer, generation_config, S3 upload, HF Hub upload): failures are + treated as soft warnings. The tokenizer and generation config are best-effort; upload + failures do not invalidate the local safetensors export, so an outage must not abort + training. + """ + hf_cfg = self.config.checkpoint.hf_export + os.makedirs(output_dir, exist_ok=True) + log.info(f"[HFExportCallback] Writing iter {iteration} export to {output_dir}") + + # 1. Safetensors shards — one file per chunk (ordered by file_idx). + # Each chunk is cleared after writing so its tensors can be GC'd + # incrementally rather than being held until the whole loop completes. + for i in range(len(cpu_chunks)): + chunk = cpu_chunks[i] + shard_path = os.path.join(output_dir, f"{i:05d}.safetensors") + _safetensors_save_file(chunk, shard_path) + log.info(f"[HFExportCallback] Wrote {shard_path}") + cpu_chunks[i] = {} # release tensor references for GC + + # 2. model.safetensors.index.json + # total_size is pre-computed in _gather_weights to avoid needing chunks here. + index_json = {"metadata": {"total_size": total_size}, "weight_map": manifest} + index_path = os.path.join(output_dir, "model.safetensors.index.json") + with open(index_path, "w") as fh: + json.dump(index_json, fh, indent=4) + + # 3. HuggingFace model config. + hf_config.save_pretrained(output_dir) + + # 4. Copy missing .py/.json files for trust_remote_code models. + # Only applicable when model_name_or_path is a local directory. + # The full directory layout is preserved so nested packages referenced by + # auto_map are included (mirroring convert_checkpoint.py's copytree approach). + # Files already present in the export dir (e.g., config.json written by + # hf_config.save_pretrained) are never overwritten. + # HARD failure: a broken copy leaves the checkpoint unloadable, so any I/O error + # propagates to the background-worker wrapper (same as shard writes). + if model_name_or_path and os.path.isdir(model_name_or_path): + real_src = os.path.realpath(model_name_or_path) + real_out = os.path.realpath(output_dir) + copied = [] + for root, dirs, files in os.walk(real_src): + real_root = os.path.realpath(root) + # Prune any subtree that is, leads to, or is inside output_dir. + # This prevents recursing into previously written export dirs when + # output_dir (or a parent of it) lives inside model_name_or_path. + dirs[:] = [ + d + for d in dirs + if not ( + (p := os.path.realpath(os.path.join(real_root, d))) == real_out + or p.startswith(real_out + os.sep) + or real_out.startswith(p + os.sep) + ) + ] + if real_root == real_out or real_root.startswith(real_out + os.sep): + continue + rel_dir = os.path.relpath(real_root, real_src) + for fname in files: + if not (fname.endswith(".py") or fname.endswith(".json")): + continue + src = os.path.join(real_root, fname) + dst_dir = output_dir if rel_dir == "." else os.path.join(output_dir, rel_dir) + dst = os.path.join(dst_dir, fname) + if not os.path.exists(dst): + os.makedirs(dst_dir, exist_ok=True) + shutil.copy2(src, dst) + copied.append(os.path.join(rel_dir, fname) if rel_dir != "." else fname) + if copied: + log.info(f"[HFExportCallback] Copied missing files from source model: {copied}") + + # 5. Tokenizer (best-effort — may fail for custom / gated models). + if AutoTokenizer is not None and model_name_or_path: + try: + tok = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True) + tok.save_pretrained(output_dir) + except Exception as exc: + log.warning(f"[HFExportCallback] Tokenizer save skipped: {exc}") + + # 6. Generation config (best-effort — not all models expose one). + if GenerationConfig is not None and model_name_or_path: + try: + gen_cfg = GenerationConfig.from_pretrained(model_name_or_path, trust_remote_code=True) + gen_cfg.save_pretrained(output_dir) + except Exception as exc: + log.warning(f"[HFExportCallback] generation_config save skipped: {exc}") + + # 7. S3 upload — soft failure: local export is intact regardless of upload outcome. + obj_store = hf_cfg.upload_to_object_store + if obj_store.enabled: + s3_prefix = f"{self.config.job.path}/hf_exports/iter_{iteration:09d}" + try: + _upload_folder_to_s3(output_dir, obj_store.bucket, s3_prefix, obj_store.credentials) + log.info(f"[HFExportCallback] S3 upload done: s3://{obj_store.bucket}/{s3_prefix}") + except Exception as exc: + # Intentionally soft: an upload outage must not crash training. + log.warning(f"[HFExportCallback] S3 upload failed (local export intact): {exc}") + + # 8. HuggingFace Hub upload — soft failure: see comment above. + if hf_cfg.hf_repo_id: + self._upload_to_hf_hub(output_dir, hf_cfg.hf_repo_id) + + log.info(f"[HFExportCallback] Export complete for iter {iteration}.") + + @staticmethod + def _upload_to_hf_hub(output_dir: str, repo_id: str, max_retries: int = 3) -> None: + try: + from huggingface_hub import HfApi + except ImportError: + log.warning("[HFExportCallback] huggingface_hub not installed; skipping HF Hub upload.") + return + + api = HfApi() + for attempt in range(1, max_retries + 1): + try: + api.create_repo(repo_id=repo_id, exist_ok=True) + break + except Exception as exc: + log.warning(f"[HFExportCallback] create_repo attempt {attempt}/{max_retries} failed: {exc}") + if attempt == max_retries: + log.warning( + f"[HFExportCallback] Could not create HF Hub repo '{repo_id}' after " + f"{max_retries} attempts; skipping upload." + ) + return + + for attempt in range(1, max_retries + 1): + try: + api.upload_folder( + folder_path=output_dir, + repo_id=repo_id, + commit_message=f"Upload checkpoint from {os.path.basename(output_dir)}", + ) + log.info(f"[HFExportCallback] Uploaded to HF Hub: {repo_id}") + return + except Exception as exc: + log.warning(f"[HFExportCallback] HF Hub upload attempt {attempt}/{max_retries} failed: {exc}") + + log.warning(f"[HFExportCallback] All {max_retries} HF Hub upload attempts failed for {repo_id}.") diff --git a/cosmos_training/cosmos/callbacks/iter_speed.py b/cosmos_training/cosmos/callbacks/iter_speed.py new file mode 100644 index 00000000..73489ec2 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/iter_speed.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import torch +import wandb +from torch import Tensor + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import log +from cosmos.utils.distributed import rank0_only +from cosmos.utils.easy_io import easy_io + + +class IterSpeed(EveryN): + """ + Args: + hit_thres (int): Number of iterations to wait before logging. + save_s3 (bool): Whether to save to S3. + save_s3_every_log_n (int): Save to S3 every n log iterations, which means save_s3_every_log_n n * every_n global iterations. + """ + + def __init__(self, *args, hit_thres: int = 5, save_s3: bool = True, save_s3_every_log_n: int = 10, **kwargs): + super().__init__(*args, **kwargs) + self.time = None + self.hit_counter = 0 + self.hit_thres = hit_thres + self.save_s3 = save_s3 + self.save_s3_every_log_n = save_s3_every_log_n + self.name = self.__class__.__name__ + self.last_hit_time = time.time() + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if self.hit_counter < self.hit_thres: + log.info( + f"Iteration {iteration}: " + f"Hit counter: {self.hit_counter + 1}/{self.hit_thres} | " + f"Loss: {loss.detach().item():.4f} | " + f"Time: {time.time() - self.last_hit_time:.2f}s", + rank0_only=False, + ) + self.hit_counter += 1 + self.last_hit_time = time.time() + #! useful for large scale training and avoid oom crash in the first two iterations!!! + torch.cuda.synchronize() + return + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, Tensor], + output_batch: dict[str, Tensor], + loss: Tensor, + iteration: int, + ) -> None: + if self.time is None: + self.time = time.time() + return + cur_time = time.time() + iter_speed = (cur_time - self.time) / self.every_n / self.step_size + + log.info( + f"{iteration} : iter_speed {iter_speed:.2f} seconds per iteration | Loss: {loss.detach().item():.4f}", + rank0_only=False, + ) + + per_sample_batch_counter = dict() + if hasattr(model, "is_image_batch"): + is_image_batch = model.is_image_batch(data_batch) + if is_image_batch: + image_batch_size = len(data_batch[model.input_image_key]) + per_sample_batch_counter["image_batch_size"] = image_batch_size + else: + video_batch_size = len(data_batch[model.input_video_key]) + per_sample_batch_counter["video_batch_size"] = video_batch_size + + if wandb.run: + sample_counter = getattr(trainer, "sample_counter", iteration) + wandb.log( + { + "timer/iter_speed": iter_speed, + "sample_counter": sample_counter, + } + | per_sample_batch_counter, + step=iteration, + ) + self.time = cur_time + if self.save_s3: + if iteration % (self.save_s3_every_log_n * self.every_n) == 0: + easy_io.dump( + { + "iter_speed": iter_speed, + "iteration": iteration, + }, + f"s3://rundir/{self.name}/iter_{iteration:09d}.yaml", + ) diff --git a/cosmos_training/cosmos/callbacks/load_pretrained.py b/cosmos_training/cosmos/callbacks/load_pretrained.py new file mode 100644 index 00000000..602c5d99 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/load_pretrained.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.model._base import ImaginaireModel +from cosmos.utils.callback import Callback + + +class LoadPretrained(Callback): + def __init__(self): + r""" + This callback enables us to load pretrained model weights if needed. + Model weights are initialized from safetensors if not loaded already from DCP checkpoint. + """ + super().__init__() + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + model.load_pretrained_model_if_needed() diff --git a/cosmos_training/cosmos/callbacks/log_tensor_shape.py b/cosmos_training/cosmos/callbacks/log_tensor_shape.py new file mode 100644 index 00000000..c71e5975 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/log_tensor_shape.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch + +from cosmos.utils import log +from cosmos.utils.callback import Callback + + +class LogTensorShapeCallback(Callback): + def __init__(self, num_log: int = 10): + self.num_log = num_log + + def on_training_step_start(self, model_parts, data_batch, iteration): + if iteration > self.num_log: + return + summary_str = f"[Tensor Shape] Iteration {iteration}" + for key in data_batch.keys(): + if isinstance(data_batch[key], torch.Tensor): + summary_str += f" | {key} shape: {data_batch[key].shape}, dtype: {data_batch[key].dtype} " + summary_str += f"data_batch: {data_batch.keys()}" + if iteration < 1000: + # Only log the first 1000 iterations + for key in ["__url__", "__key__", "image_grid_thw", "video_grid_thw"]: + if key in data_batch: + summary_str += f" | {key}: {data_batch[key]}" + log.info(summary_str, rank0_only=False) diff --git a/cosmos_training/cosmos/callbacks/low_precision.py b/cosmos_training/cosmos/callbacks/low_precision.py new file mode 100644 index 00000000..a730b228 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/low_precision.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import torch + +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import log +from cosmos.utils.callback import LowPrecisionCallback as BaseLowPrecisionCallback + + +class LowPrecisionCallback(BaseLowPrecisionCallback): + """ + Unified low-precision callback for VFM and VLM. + + Two initialization modes: + - Config-driven (VLM): pass ``param_torch_dtype`` as a string (e.g. "bfloat16"). + ``precision_type`` is set immediately from the string. + - Runtime-inspection (VFM): omit ``param_torch_dtype``. + ``precision_type`` is determined from ``model.precision`` at ``on_train_start``. + + Auto-disabled (``update_iter`` set to maxsize) when fp32 is detected. + """ + + def __init__( + self, + config, + trainer: ImaginaireTrainer, + update_iter: int, + param_torch_dtype: str | None = None, + ): + self.config = config + self.trainer = trainer + self.update_iter = update_iter + if param_torch_dtype is not None: + # Config-driven path (VLM) + self.precision_type = getattr(torch, param_torch_dtype) + + def on_train_start(self, model, iteration: int = 0) -> None: + if hasattr(self, "precision_type"): + # Already set from config — nothing to do + return + # Runtime-inspection path (VFM): read precision from model + if not isinstance(model, list): + model = [model] + for model_part in model: + if getattr(model_part, "precision", None) == torch.float32: + log.critical("Using fp32, should disable master weights.") + self.update_iter = sys.maxsize + else: + precision = getattr(model_part, "precision", None) + assert precision in [ + torch.bfloat16, + torch.float16, + torch.half, + ], "LowPrecisionCallback must use a low precision dtype." + self.precision_type = precision diff --git a/cosmos_training/cosmos/callbacks/manual_gc.py b/cosmos_training/cosmos/callbacks/manual_gc.py new file mode 100644 index 00000000..0a677b29 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/manual_gc.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc + +from cosmos.callbacks.every_n import EveryN +from cosmos.utils import log + + +class ManualGarbageCollection(EveryN): + """ + Disable auto gc and manually trigger garbage collection every N iterations + It is super useful for large scale training to reduce gpu sync time! + Can reach 50% speedup. + + It is important to note that this callback only disables gc in main process and have auto gc enabled in subprocesses. + + We start disable gc after warm_up iterations to avoid disabling gc in subprocesses, such as dataloader, which can cause OOM + """ + + def __init__(self, *args, warm_up: int = 5, gc_level: int = 1, **kwargs): + kwargs["barrier_after_run"] = False + super().__init__(*args, **kwargs) + + self.counter = 0 + self.warm = warm_up + self.gc_level = gc_level + + def every_n_impl(self, trainer, model, data_batch, output_batch, loss, iteration): + del trainer, model, data_batch, output_batch, loss + self.counter += 1 + if self.counter < self.warm: + return + if self.counter == self.warm: + gc.disable() + log.critical("Garbage collection disabled") + + gc.collect(self.gc_level) diff --git a/cosmos_training/cosmos/callbacks/mfu.py b/cosmos_training/cosmos/callbacks/mfu.py new file mode 100644 index 00000000..f8ae1f1d --- /dev/null +++ b/cosmos_training/cosmos/callbacks/mfu.py @@ -0,0 +1,318 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MFU (Model FLOPs Utilization) callback for OmniMoT training. + +Computes and logs MFU metrics for specified hardware targets (e.g. H100, GB200) +by calculating the actual training FLOPs per step and comparing against +theoretical peak throughput. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from decimal import Decimal + +import torch +import wandb + +from cosmos.model.attention.utils import is_blackwell_dc +from cosmos.callbacks.every_n import EveryN +from cosmos.tools.flops import ( + OmniMoTModelDescriptor, + compute_omni_mot_flops_per_batch, + compute_wan_vae_encoder_flops, + get_omni_mot_model_descriptor, +) +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import log +from cosmos.utils.distributed import rank0_only + + +@dataclass +class HardwareTarget: + """Specification of a hardware target for MFU computation. + + Attributes: + name: Human-readable name (used as W&B tag, e.g. "H100"). + peak_tflops: Theoretical peak throughput in TFLOPS (e.g. 989 for H100 BF16). + """ + + name: str + peak_tflops: float + + +# Pre-defined hardware targets +H100 = HardwareTarget(name="H100", peak_tflops=989.0) +GB200 = HardwareTarget(name="GB200", peak_tflops=2250.0) + + +class MFUCallback(EveryN): + """Callback that computes and logs Model FLOPs Utilization (MFU) to W&B. + + MFU is defined as: + MFU = achieved_tflops_per_gpu / peak_tflops_per_gpu + + where achieved_tflops_per_gpu is computed from the model's theoretical + training FLOPs (forward + backward) divided by the measured wall-clock + time per step. + + This callback accumulates per-step FLOPs between logging intervals and + reports the average MFU over that window. + + Args: + backwardpass_ratio: Ratio of backward-to-forward FLOPs (default 2.0). + hit_thres: Number of warm-up iterations before logging begins. + include_vae_encoder: If True (default), include the Wan 2.2 VAE encoder + forward-pass FLOPs in the per-step total. The VAE is frozen during + training so only forward FLOPs are counted. + include_padding: If True, include FLOPs spent on padding tokens (the + causal split appended by sequence-packing finalize()). Gives a + ``total GPU FLOPs`` view instead of ``useful FLOPs`` only. + grad_accum_iter: Number of gradient accumulation steps per optimizer + update (default 1). When > 1, ``on_training_step_end`` is called + once per optimizer step but the wall-clock time covers all + micro-batches, so per-step FLOPs are multiplied by this count. + """ + + def __init__( + self, + *args, + backwardpass_ratio: float = 2.0, + hit_thres: int = 5, + include_vae_encoder: bool = True, + include_padding: bool = True, + grad_accum_iter: int = 1, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.hardware_target = GB200 if is_blackwell_dc() else H100 + self.backwardpass_ratio = backwardpass_ratio + self.hit_thres = hit_thres + self.include_vae_encoder = include_vae_encoder + self.include_padding = include_padding + self.grad_accum_iter = grad_accum_iter + + # Lazily initialised from model config on first call + self._model_descriptor: OmniMoTModelDescriptor | None = None + self._freeze_und: bool = False + self._vision_gen: bool = True + self._action_gen: bool = False + self._sound_gen: bool = False + self._world_size: int = 1 + self._use_activation_checkpointing: bool = False + + # Accumulation state between every_n windows + self._accumulated_flops = Decimal(0) + self._accumulated_flops_vae = Decimal(0) + self._steps_in_window: int = 0 + self._window_start_time: float | None = None + + # Warm-up counter + self._hit_counter: int = 0 + + # ------------------------------------------------------------------ # + # Lazy initialisation from model + # ------------------------------------------------------------------ # + + def _ensure_initialised(self, model: ImaginaireModel) -> None: + """Build the ``OmniMoTModelDescriptor`` from the live model config.""" + if self._model_descriptor is not None: + return + + # Access VLM config from the language model inside the network + vlm_cfg = model.net.language_model.config # type: ignore[attr-defined] + net_cfg = model.net.config # type: ignore[attr-defined] + + self._freeze_und = getattr(vlm_cfg, "freeze_und", False) + self._vision_gen = getattr(net_cfg, "vision_gen", True) + self._action_gen = getattr(net_cfg, "action_gen", False) + self._sound_gen = getattr(net_cfg, "sound_gen", False) + + # Read activation checkpointing from the model's parallelism config + model_cfg = getattr(model, "config", None) + parallelism_cfg = getattr(model_cfg, "parallelism", None) + self._use_activation_checkpointing = getattr(parallelism_cfg, "use_activation_checkpointing", False) + + # MoE fields (may not exist for dense-only configs) + use_moe = getattr(vlm_cfg, "use_moe", False) + num_experts = getattr(vlm_cfg, "num_experts", 0) + num_experts_per_tok = getattr(vlm_cfg, "num_experts_per_tok", 0) + moe_intermediate_size = getattr(vlm_cfg, "moe_intermediate_size", 0) + decoder_sparse_step = getattr(vlm_cfg, "decoder_sparse_step", 1) + mlp_only_layers = list(getattr(vlm_cfg, "mlp_only_layers", [])) + + self._model_descriptor = get_omni_mot_model_descriptor( + hidden_size=vlm_cfg.hidden_size, + num_hidden_layers=vlm_cfg.num_hidden_layers, + num_attention_heads=vlm_cfg.num_attention_heads, + num_key_value_heads=vlm_cfg.num_key_value_heads, + head_dim=getattr(vlm_cfg, "head_dim", None), + intermediate_size=vlm_cfg.intermediate_size, + vocab_size=vlm_cfg.vocab_size, + use_moe=use_moe, + num_experts=num_experts, + num_experts_per_tok=num_experts_per_tok, + moe_intermediate_size=moe_intermediate_size, + decoder_sparse_step=decoder_sparse_step, + mlp_only_layers=mlp_only_layers, + latent_patch_size=getattr(net_cfg, "latent_patch_size", 2), + latent_channel_size=getattr(net_cfg, "latent_channel_size", 48), + action_dim=getattr(net_cfg, "action_dim", 32), + sound_dim=getattr(net_cfg, "sound_dim", 64), + frequency_embedding_size=getattr(net_cfg, "frequency_embedding_size", 256), + predict_text_tokens=getattr(net_cfg, "predict_text_tokens", False), + ) + + self._world_size = torch.distributed.get_world_size() if torch.distributed.is_initialized() else 1 + + # ------------------------------------------------------------------ # + # Per-step accumulation + # ------------------------------------------------------------------ # + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # Warm-up: skip first few iterations (compilation, allocation, etc.) + if self._hit_counter < self.hit_thres: + self._hit_counter += 1 + return + + self._ensure_initialised(model) + + # Start the timing window on the first post-warmup step + if self._window_start_time is None: + self._window_start_time = time.monotonic() + + # Extract per-modality token counts from output_batch + und_token_length = output_batch.get("und_token_length") + if und_token_length is None: + return + + und_tokens = int(und_token_length) + vision_tokens = int(output_batch.get("vision_token_length", 0)) + action_tokens = int(output_batch.get("action_token_length", 0)) + sound_tokens = int(output_batch.get("sound_token_length", 0)) + + # Per-split attention metadata for packed sequences + split_lens: list[int] | None = output_batch.get("split_lens") + attn_modes_list: list[str] | None = output_batch.get("attn_modes") + + # Compute FLOPs for this per-device micro-batch. + # B = 1 because token counts are already summed across all samples in + # the packed sequence on this device. + assert self._model_descriptor is not None + step_flops = compute_omni_mot_flops_per_batch( + cfg=self._model_descriptor, + B=1, + text_tokens=und_tokens, + vision_tokens=vision_tokens, + action_tokens=action_tokens, + sound_tokens=sound_tokens, + freeze_und=self._freeze_und, + vision_gen=self._vision_gen, + action_gen=self._action_gen, + sound_gen=self._sound_gen, + backwardpass_ratio=self.backwardpass_ratio, + split_lens=split_lens, + attn_modes=attn_modes_list, + include_padding=self.include_padding, + use_activation_checkpointing=self._use_activation_checkpointing, + ) + + # VAE encoder forward-pass FLOPs (frozen, no backward). + if self.include_vae_encoder: + vae_pixel_shapes = output_batch.get("vae_pixel_shapes") + if vae_pixel_shapes: + for pT, pH, pW in vae_pixel_shapes: + vae_flops = compute_wan_vae_encoder_flops(B=1, T=pT, H=pH, W=pW) + self._accumulated_flops_vae += vae_flops + step_flops += vae_flops + + # When gradient accumulation is used, on_training_step_end is called + # once per optimizer step (not per micro-batch). Multiply by the + # accumulation count so the FLOPs cover all micro-batches in the step. + # For VAE with gradient accumulation we assume all micro-batches have the same FLOP count + if self.grad_accum_iter > 1: + step_flops *= self.grad_accum_iter + + self._accumulated_flops += step_flops + self._steps_in_window += 1 + + # Delegate to EveryN for the periodic reporting logic + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + # ------------------------------------------------------------------ # + # Periodic reporting + # ------------------------------------------------------------------ # + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if self._window_start_time is None or self._steps_in_window == 0: + return + + elapsed = time.monotonic() - self._window_start_time + if elapsed <= 0: + return + + if self._accumulated_flops <= 0: + log.warning( + f"Number of calculated FLOPs must be more than 0, got {self._accumulated_flops} at iteration {iteration} for {self._steps_in_window} steps." + ) + + # Achieved TFLOPS *per GPU* over the window + # accumulated_flops is the total per-device FLOPs over all steps in window + achieved_tflops_per_gpu = float(self._accumulated_flops) / elapsed / 1e12 + + avg_flops_per_step = float(self._accumulated_flops) / self._steps_in_window + avg_time_per_step = elapsed / self._steps_in_window + + log_info: dict[str, float] = { + "mfu/achieved_tflops_per_gpu": achieved_tflops_per_gpu, + "mfu/avg_flops_per_step": avg_flops_per_step, + "mfu/avg_time_per_step_s": avg_time_per_step, + "mfu/steps_in_window": float(self._steps_in_window), + "mfu/vae_flops_percentage": float(self._accumulated_flops_vae / self._accumulated_flops) * 100.0, + } + + mfu = ( + achieved_tflops_per_gpu / self.hardware_target.peak_tflops if self.hardware_target.peak_tflops > 0 else 0.0 + ) + log_info[f"mfu/{self.hardware_target.name}"] = mfu + + # W&B log + if wandb.run is not None: + wandb.log(log_info, step=iteration) + + # Reset accumulation window + self._accumulated_flops = Decimal(0) + self._accumulated_flops_vae = Decimal(0) + self._steps_in_window = 0 + self._window_start_time = time.monotonic() diff --git a/cosmos_training/cosmos/callbacks/moe_specialization_callback.py b/cosmos_training/cosmos/callbacks/moe_specialization_callback.py new file mode 100644 index 00000000..dec5c069 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/moe_specialization_callback.py @@ -0,0 +1,203 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +""" +MoE Specialization Callback +============================ +Monitors whether MoE experts are developing distinct, stable roles over training. +A well-trained MoE should have experts that specialize — each processing a different +kind of input — rather than a few generalist experts doing everything while the rest +idle. + + Expert Co-activation Rate + ------------------------- + If two experts frequently fire together on the same token (both in the top-K + selected), they are likely learning redundant representations. Ideally experts + specialize on non-overlapping token types, so co-activation should stay close + to the chance baseline of K/N (e.g. 8/128 ≈ 0.0625 for the 235B model). + + For each layer and each unique expert pair (i, j), we compute: + CoAct(i, j) = N_{i,j} / N_i + where N_{i,j} = number of tokens where both i and j were selected, and N_i = + total tokens routed to expert i. We then summarize across all pairs as max and + mean. A rising mean_coact, especially well above the chance baseline, signals + that the router is collapsing onto a small correlated cluster of experts. + +Buffer ownership +---------------- + coactivation_counts is reset here (in compute_moe_coactivation_metrics). + Per-expert token counts are derived from coactivation_counts itself + (row_sum + col_sum) / (K-1), so this callback is fully independent of + ExpertHeatmap's reset cycle for total_tokens_per_expert. +""" + +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed +from cosmos.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def _get_device_mesh(vfm: torch.nn.Module): + weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight + return weight.device_mesh if isinstance(weight, DTensor) else None + + +def _allreduce_dtensor(t: torch.Tensor, device_mesh) -> torch.Tensor: + """Sum-reduce a local tensor across all FSDP ranks and return the global tensor.""" + return DTensor.from_local( + t, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + +def compute_moe_coactivation_metrics(vfm: torch.nn.Module) -> dict[str, dict]: + """ + Compute per-layer Expert Co-activation metrics for both towers. + + For each unique expert pair (i < j) in the upper triangle of the N×N + coactivation matrix, computes: + CoAct(i, j) = N_{i,j} / N_i + where N_{i,j} is the count of tokens where both i and j were in the top-K, + and N_i is the total token count for expert i (the row expert, i.e. the + lower-indexed expert in the pair). + + N_i is derived directly from the co-activation matrix rather than from + the shared total_tokens_per_expert buffer, so this metric is independent + of ExpertHeatmap's reset cycle. Each token routed to expert i contributes + to (K-1) co-activation pairs, so N_i = (row_sum_i + col_sum_i) / (K-1). + + High co-activation relative to the chance baseline (K/N) indicates that + certain expert pairs are systematically selected together — a sign of + redundancy rather than specialization. + + Returns a dict: tower -> { + "layer_indices": list[int] — actual model layer positions + "max_coact": Tensor[num_moe_layers] — worst pair per layer + "mean_coact": Tensor[num_moe_layers] — average over all pairs + "chance_baseline": float — K/N, same for all layers (reference) + } + """ + with torch.no_grad(): + device_mesh = _get_device_mesh(vfm) + if device_mesh is None: + return {} + + results: dict[str, dict] = {} + for tower in ["und", "gen"]: + layer_indices, max_coacts, mean_coacts, chance_baselines = [], [], [], [] + + num_layers = len(vfm.language_model.model.layers) + for layer_idx in range(num_layers): + layer = vfm.language_model.model.layers[layer_idx] + mlp = layer.mlp if tower == "und" else getattr(layer, "mlp_moe_gen", None) + if not isinstance(mlp, Qwen3VLMoeTextSparseMoeBlock): + continue + + coact_counts = _allreduce_dtensor(mlp.get_coactivation_counts(reset=True), device_mesh) # [N, N] + + n = mlp.num_experts + k = mlp.top_k + + # Derive per-expert token counts directly from the co-activation + # matrix so we don't depend on ExpertHeatmap's reset cycle. + # Each token that routes to expert i contributes (K-1) entries + # across row i and column i of the upper-triangle matrix. + tokens_per_expert = (coact_counts.sum(dim=1) + coact_counts.sum(dim=0)).float() / (k - 1) + + mask = torch.triu(torch.ones(n, n, dtype=torch.bool, device=coact_counts.device), diagonal=1) + # CoAct(i, j) = N_{i,j} / N_i — normalise by how often expert i fires overall. + denom = tokens_per_expert.unsqueeze(1).clamp(min=1) # [N, 1] + coact_rates = (coact_counts.float() / denom)[mask] # [N*(N-1)/2] + + layer_indices.append(layer_idx) + max_coacts.append(coact_rates.max()) + mean_coacts.append(coact_rates.mean()) + # Chance baseline = probability two randomly-chosen top-K slots land on the + # same pair under uniform routing = K/N. Constant across layers and steps, + # logged once per tower as a reference line. + chance_baselines.append(k / n) + + if layer_indices: + results[tower] = { + "layer_indices": layer_indices, + "max_coact": torch.stack(max_coacts), + "mean_coact": torch.stack(mean_coacts), + "chance_baseline": chance_baselines[0], # same value for all layers + } + + return results + + +class MoESpecializationCallback(EveryN): + """ + Logs per-layer MoE specialization metrics to W&B every N training steps. + + What it captures + ---------------- + Whether MoE experts are developing distinct routing identities: + + Expert Co-activation (logged every N steps) + - mean_coact / max_coact per layer: how often expert pairs fire together + relative to the chance_baseline (K/N). Values well above the baseline + suggest the router is selecting a redundant cluster of experts rather + than a diverse set. + + W&B layout + ---------- + moe_specialization/coact_chance_baseline/ — flat reference (K/N) + moe_specialization/max_coact//layer_NNN|mean|max + moe_specialization/mean_coact//layer_NNN|mean|max + + Args: + every_n (int): Logging interval in training steps. + """ + + def __init__(self, every_n: int = 100): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + vfm = model.net + + coact_results = compute_moe_coactivation_metrics(vfm) + + if not (distributed.is_rank0() and wandb.run): + return + + log_dict: dict[str, float] = {} + + for tower, tower_metrics in coact_results.items(): + layer_indices = tower_metrics.pop("layer_indices") + chance_baseline = tower_metrics.pop("chance_baseline") + log_dict[f"moe_specialization/coact_chance_baseline/{tower}"] = chance_baseline + for metric_name, values in tower_metrics.items(): + for layer_idx, val in zip(layer_indices, values): + log_dict[f"moe_specialization/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item() + log_dict[f"moe_specialization/{metric_name}/{tower}/mean"] = values.mean().item() + log_dict[f"moe_specialization/{metric_name}/{tower}/max"] = values.max().item() + + wandb.log(log_dict, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/moe_stability_callback.py b/cosmos_training/cosmos/callbacks/moe_stability_callback.py new file mode 100644 index 00000000..cfd9c1bf --- /dev/null +++ b/cosmos_training/cosmos/callbacks/moe_stability_callback.py @@ -0,0 +1,203 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- + +""" +MoE Stability Callback +====================== +Monitors whether the MoE router is staying healthy over the course of training. +A healthy router distributes tokens reasonably evenly, keeps all experts alive, +and remains uncertain enough (high entropy) that it is still learning to route. + +Three metrics are tracked per layer, per tower (und / gen): + + Dead Expert Rate + ---------------- + Fraction of experts receiving fewer than 10% of their fair-share of tokens + (i.e. load fraction f_i < 0.1 / N). A dead expert has been effectively shut + out by the router — it gets no gradient signal and its capacity is wasted. + Ideal = 0. A rising dead-expert rate in the gen tower during early training + is a common failure mode. + + Load Imbalance Factor (LIF) + --------------------------- + N * max(f_i), where f_i is the fraction of tokens routed to expert i. + Measures how much the busiest expert is overloaded relative to uniform. + LIF = 1.0 is perfect balance; <= 1.3 is healthy; > 3.0 indicates severe + collapse onto a small set of experts. This is the same quantity watched by + the load-balancing loss, but measured empirically rather than from the loss + objective. + + Router Entropy (normalized) + --------------------------- + Mean per-token Shannon entropy of the full routing distribution, divided by + log(N) to put it on a [0, 1] scale. H = 1 means the router is maximally + uncertain (uniform over all experts); H = 0 means it always picks the same + expert. Early in training entropy is high; we want it to stay reasonably + high (> ~0.7) so the router continues to explore. A sudden drop signals + routing collapse. + +Buffer ownership +---------------- + This callback is fully self-contained: it reads and resets its own dedicated + buffers (stability_tokens_per_expert, stability_total_tokens, sum_token_entropy). + It does not depend on ExpertHeatmap's reset cycle. +""" + +import math + +# Fraction of uniform fair-share below which an expert is considered "dead" (e.g. 0.1 → < 10% of K/N). +DEAD_EXPERT_THRESHOLD_MULTIPLIER = 0.1 + +import torch +import wandb +from torch.distributed.tensor import DTensor, Partial + +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import distributed +from cosmos.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import Qwen3VLMoeTextSparseMoeBlock + + +def compute_moe_stability_metrics(vfm: torch.nn.Module) -> dict[str, dict]: + """ + Compute per-layer MoE stability metrics for both towers. + + Iterates over all model layers, skipping any that do not use + Qwen3VLMoeTextSparseMoeBlock (e.g. dense layers when decoder_sparse_step > 1). + Actual model layer indices are preserved so W&B keys (layer_000, layer_042, ...) + always refer to the correct transformer layer regardless of MoE sparsity pattern. + + Returns a dict: tower -> { + "layer_indices": list[int] — actual model layer positions + "dead_expert_rate": Tensor[num_moe_layers] + "lif": Tensor[num_moe_layers] + "router_entropy_normalized":Tensor[num_moe_layers] + } + """ + with torch.no_grad(): + num_layers = len(vfm.language_model.model.layers) + + example_weight = vfm.language_model.model.layers[0].self_attn.q_proj.weight + device_mesh = example_weight.device_mesh if isinstance(example_weight, DTensor) else None + + if device_mesh is None: + return {} + + def _allreduce(t: torch.Tensor) -> torch.Tensor: + return DTensor.from_local( + t, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + results: dict[str, dict] = {} + for tower in ["und", "gen"]: + layer_indices, dead_rates, lifs, entropies = [], [], [], [] + + for layer_idx in range(num_layers): + layer_module = vfm.language_model.model.layers[layer_idx] + # "und" tower uses layer.mlp; "gen" tower uses layer.mlp_moe_gen. + # Both attributes exist on every layer (set in unified_mot.py), but only + # layers where (layer_idx+1) % decoder_sparse_step == 0 are MoE blocks. + mlp_module = layer_module.mlp if tower == "und" else getattr(layer_module, "mlp_moe_gen", None) + if not isinstance(mlp_module, Qwen3VLMoeTextSparseMoeBlock): + continue + + total_tokens_per_expert = _allreduce(mlp_module.get_stability_tokens_per_expert(reset=True)) + total_tokens = _allreduce(mlp_module.get_stability_total_tokens(reset=True)) + sum_token_entropy = _allreduce(mlp_module.get_sum_token_entropy(reset=True)) + + n = mlp_module.num_experts + total = total_tokens.float().clamp(min=1) + f_i = total_tokens_per_expert.float() / total # [N] load fraction per expert + + k = mlp_module.top_k + + layer_indices.append(layer_idx) + # Uniform fair share per expert is K/N. "Dead" = below 10% of that. + dead_rates.append((f_i < DEAD_EXPERT_THRESHOLD_MULTIPLIER * k / n).float().mean()) + # LIF = max(f_i) * N / K. Interpretation: + # 1.0 = perfectly balanced (every expert gets its fair share) + # 2.0 = busiest expert handles 2x its fair share + # >3.0 = severe imbalance, consider tuning load-balancing loss + lifs.append(f_i.max() * n / k) + # Mean per-token entropy, normalized to [0, 1] by log(N). + # squeeze() collapses the [1] buffer shape to a 0-d scalar. + entropies.append((sum_token_entropy.float() / total / math.log(n)).squeeze()) + + if layer_indices: + results[tower] = { + "layer_indices": layer_indices, + "dead_expert_rate": torch.stack(dead_rates), + "lif": torch.stack(lifs), + "router_entropy_normalized": torch.stack(entropies), + } + + return results + + +class MoEStabilityCallback(EveryN): + """ + Logs per-layer MoE stability metrics to W&B every N training steps. + + What it captures + ---------------- + Whether the MoE router remains in a healthy, balanced state over training. + The three metrics collectively answer: are all experts still being used + (dead_expert_rate), is load spread evenly (lif), and is the router still + making uncertain, exploratory decisions (router_entropy_normalized)? + + W&B layout + ---------- + For each metric and each tower, two kinds of series are logged: + - moe_stability///layer_NNN — per model layer time series + - moe_stability///mean|max — summary across all MoE layers + + Typical healthy ranges: + dead_expert_rate → 0 (any sustained non-zero value is a concern) + lif → <= 1.3 (alarm at > 3.0) + router_entropy_normalized → > 0.7 (collapse if it drops sharply) + + Args: + every_n (int): Logging interval in training steps. + """ + + def __init__(self, every_n: int = 100): + super().__init__(every_n=every_n) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + metrics = compute_moe_stability_metrics(model.net) + + if not (distributed.is_rank0() and wandb.run): + return + + log_dict: dict[str, float] = {} + for tower, tower_metrics in metrics.items(): + layer_indices = tower_metrics.pop("layer_indices") + for metric_name, values in tower_metrics.items(): + for layer_idx, val in zip(layer_indices, values): + log_dict[f"moe_stability/{metric_name}/{tower}/layer_{layer_idx:03d}"] = val.item() + log_dict[f"moe_stability/{metric_name}/{tower}/mean"] = values.mean().item() + log_dict[f"moe_stability/{metric_name}/{tower}/max"] = values.max().item() + + wandb.log(log_dict, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/norm_monitor.py b/cosmos_training/cosmos/callbacks/norm_monitor.py new file mode 100644 index 00000000..de21b44a --- /dev/null +++ b/cosmos_training/cosmos/callbacks/norm_monitor.py @@ -0,0 +1,345 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Optional, Union + +import torch +import torch.distributed as dist +import wandb +from torch import nn +from torch.distributed.tensor import DTensor + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log, misc +from cosmos.utils.callback import Callback +from cosmos.utils.distributed import DistributedDataParallel +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.sequence_packing import get_gen_seq + +try: + from apex.contrib.layer_norm import FastLayerNorm +except ImportError: + FastLayerNorm = None + + +class NormMonitor(Callback): + def __init__( + self, + every_n: Optional[int] = None, + step_size: int = 1, + layer_norm_only: bool = False, + model_key: Optional[str] = None, + log_stat_wandb: bool = False, + save_s3: bool = False, + track_activations: bool = False, + ): + """Monitor and log parameter/gradient/activation norms during training. + + Args: + every_n: Log statistics every N global steps. If None, logging is disabled. + step_size: Number of micro-steps per global step (for gradient accumulation). + layer_norm_only: If True, only track LayerNorm and Embedding parameters. + If False, track all parameters. + model_key: Attribute name to access the model (e.g., "diffusion_model"). + If None, use the model directly. + log_stat_wandb: If True, log per-parameter statistics to wandb. + If False, only log aggregate norms. + save_s3: If True, save statistics to S3 bucket. + track_activations: If True, track activation norms + and gradients of activations at each transformer block. If set to False, only + weight norms and weight gradient norms will be tracked. + """ + self.every_n = every_n + self.step_size = step_size + self.model_key = model_key + self.layer_norm_only = layer_norm_only + self.log_stat_wandb = log_stat_wandb + self.save_s3 = save_s3 + self.track_activations = track_activations + self.name = self.__class__.__name__ + + # Storage for activation statistics (populated by hooks) + self._activation_stats: dict[str, dict[str, torch.Tensor]] = {} + self._activation_grad_stats: dict[str, dict[str, torch.Tensor]] = {} + self._hooks: list[torch.utils.hooks.RemovableHandle] = [] + self._should_record = False + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + config_job = self.config.job + self.local_dir = f"{config_job.path_local}/norm_monitor" + if distributed.get_rank() == 0: + os.makedirs(self.local_dir, exist_ok=True) + log.info(f"{self.__class__.__name__} callback: local_dir: {self.local_dir}") + + # Register activation hooks if enabled + if self.track_activations: + self._register_activation_hooks(model) + + def _register_activation_hooks(self, model: ImaginaireModel) -> None: + """Register forward and backward hooks on transformer blocks to capture activation statistics. + + Hooks are registered at the block level (on model.model.layers children) rather than + on individual modules inside blocks. This is compatible with torch.compile since + compile is applied per-block, and hooks on the outer block fire outside the compiled graph. + """ + if self.model_key is not None: + model = getattr(model, self.model_key) + + # Get the transformer layers - hooks are registered on each block + if not hasattr(model.net.language_model.model, "layers"): + log.warning( + f"{self.__class__.__name__}: Could not find model.net.language_model.model.layers. " + "Activation tracking requires model structure with model.net.language_model.model.layers." + ) + return + + layers = model.net.language_model.model.layers + + for layer_id, block in layers.named_children(): + block_name = f"blocks.{layer_id}" + + # Forward hook to capture activation norms (block output) + # Also registers a tensor hook for gradient tracking + def make_forward_hook(name: str): + def forward_hook( + mod: nn.Module, inp: tuple[torch.Tensor, ...], out: torch.Tensor | tuple[torch.Tensor, ...] + ) -> None: + if not self._should_record: + return + # We track activation norms of only generation sequences. + activation = get_gen_seq(out[0]) + + # Certain algorithms do more than one pass through the model. + # (E.g. teacher forcing). We merge stats in that case. + new_stats = self._compute_l2_stats(activation) + existing = self._activation_stats.get(name) + if existing is not None: + existing["sq_sum"] += new_stats["sq_sum"] + existing["max"] = torch.max(existing["max"], new_stats["max"]) + else: + self._activation_stats[name] = new_stats + + # Register tensor hook for gradient tracking. + # This works with activation checkpointing (unlike module backward hooks). + def make_tensor_grad_hook(hook_name: str): + def tensor_grad_hook(grad: torch.Tensor | None) -> None: + # The block may get gradients internally via attention, + # even if the output is unused. + if grad is None: + return + + # If there is more than one pass through the model + # (e.g. teacher forcing), then merge the stats. + new_stats = self._compute_l2_stats(grad) + existing = self._activation_grad_stats.get(hook_name) + if existing is not None: + existing["sq_sum"] += new_stats["sq_sum"] + existing["max"] = torch.max(existing["max"], new_stats["max"]) + else: + self._activation_grad_stats[hook_name] = new_stats + + return tensor_grad_hook + + if activation.requires_grad: + activation.register_hook(make_tensor_grad_hook(name)) + + return forward_hook + + forward_handle = block.register_forward_hook(make_forward_hook(block_name)) + self._hooks.append(forward_handle) + + if distributed.is_rank0(): + num_blocks = len(list(layers.named_children())) + log.info(f"{self.__class__.__name__}: Registered activation hooks on {num_blocks} transformer blocks") + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + """Clean up hooks when training ends.""" + for hook in self._hooks: + hook.remove() + self._hooks.clear() + + def on_before_forward( + self, + iteration: int = 0, + ) -> None: + """Enable activation recording before forward pass if this iteration should be logged.""" + if not self.track_activations: + return + global_step = iteration // self.step_size + should_run = global_step % self.every_n == 0 + self._should_record = should_run + if should_run: + # Clear previous activation stats + self._activation_stats.clear() + self._activation_grad_stats.clear() + + def on_before_optimizer_step( + self, + model_ddp: Union[DistributedDataParallel, ImaginaireModel], + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + global_step = iteration // self.step_size + should_run = global_step % self.every_n == 0 + if not should_run: + return + + if isinstance(model_ddp, DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + if self.model_key is not None: + model = getattr(model, self.model_key) + + self._compute_and_log_stats(model, iteration) + + # Disable recording after logging + self._should_record = False + + def _get_named_parameters(self, model: nn.Module) -> dict[str, nn.Parameter]: + """Get named parameters, optionally filtered to layer norm only.""" + named_parameters = {} + if self.layer_norm_only: + ln_modules = (nn.LayerNorm, nn.Embedding) + if FastLayerNorm is not None: + ln_modules += (FastLayerNorm,) + for mn, m in model.named_modules(): + if isinstance(m, ln_modules): + for pn, p in m.named_parameters(): + fpn = f"{mn}.{pn}" if mn else pn + named_parameters[fpn] = p + else: + named_parameters = dict(model.named_parameters()) + return named_parameters + + def _should_track_param(self, param_name: str) -> bool: + """Check if parameter should be tracked based on naming conventions.""" + # Track only generation tower params, exclude EMA params + return "moe_gen" in param_name and "net_ema" not in param_name + + def _compute_l2_stats(self, tensor: torch.Tensor, detach: bool = True) -> dict[str, torch.Tensor]: + """Compute statistics (squared sum and max) for a tensor. + + Args: + tensor: Input tensor to compute statistics for. + detach: If True, detach the tensor before computing stats. + + Returns: + Dictionary with "sq_sum" (squared sum for L2 norm) and "max" (absolute max). + """ + data = tensor.detach() if detach else tensor + if isinstance(data, DTensor): + data = data.to_local() + + return { + "sq_sum": (data.float() ** 2).sum(), + "max": data.abs().max(), + } + + @misc.timer("norm_monitor") + def _compute_and_log_stats(self, model: nn.Module, iteration: int = 0) -> None: + """FSDP-efficient implementation using local shards + all_reduce. + + Instead of gathering full parameters with summon_full_params (expensive), + we compute local statistics on each rank's shard and use all_reduce to + aggregate them across all ranks. + """ + named_parameters = self._get_named_parameters(model) + + # Accumulators for local shard statistics (squared sum for L2 norm) + local_param_sq_sum = torch.tensor(0.0, device="cuda", dtype=torch.float32) + local_grad_sq_sum = torch.tensor(0.0, device="cuda", dtype=torch.float32) + + # Per-parameter stats: {param_name: [local_sq_sum, local_max]} + per_param_stats: dict[str, dict[str, torch.Tensor]] = {} + per_grad_stats: dict[str, dict[str, torch.Tensor]] = {} + + for param_name, param in named_parameters.items(): + if not self._should_track_param(param_name): + continue + + # Compute local statistics on this rank's shard + per_param_stats[param_name] = self._compute_l2_stats(param) + local_param_sq_sum += per_param_stats[param_name]["sq_sum"] + + if param.grad is not None: + per_grad_stats[param_name] = self._compute_l2_stats(param.grad, detach=False) + local_grad_sq_sum += per_grad_stats[param_name]["sq_sum"] + + # All-reduce to aggregate statistics across all FSDP ranks + dist.all_reduce(local_param_sq_sum, op=dist.ReduceOp.SUM) + dist.all_reduce(local_grad_sq_sum, op=dist.ReduceOp.SUM) + + # All-reduce per-parameter stats + for param_name, stats_dict in per_param_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + for param_name, stats_dict in per_grad_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + # All-reduce activation stats (activations are replicated, so reduce across all ranks for consistency) + for module_name, stats_dict in self._activation_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + for module_name, stats_dict in self._activation_grad_stats.items(): + dist.all_reduce(stats_dict["sq_sum"], op=dist.ReduceOp.SUM) + dist.all_reduce(stats_dict["max"], op=dist.ReduceOp.MAX) + + # Only rank 0 logs the results + if distributed.is_rank0(): + important_info = { + "trainer/global_step": iteration, + "sample_counter": getattr(self.trainer, "sample_counter", iteration), + "total_param_l2_norm": local_param_sq_sum.sqrt().item(), + } + if local_grad_sq_sum > 0: + important_info["total_grad_l2_norm"] = local_grad_sq_sum.sqrt().item() + + stats = {} + for param_name, stats_dict in per_param_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/weight_norm/{param_name}"] = l2_norm.item() + stats[f"stats/weight_max/{param_name}"] = stats_dict["max"].item() + + for param_name, stats_dict in per_grad_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/grad_norm/{param_name}"] = l2_norm.item() + stats[f"stats/grad_max/{param_name}"] = stats_dict["max"].item() + + # Add activation stats + for module_name, stats_dict in self._activation_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/act_norm/{module_name}"] = l2_norm.item() + stats[f"stats/act_max/{module_name}"] = stats_dict["max"].item() + + for module_name, stats_dict in self._activation_grad_stats.items(): + l2_norm = stats_dict["sq_sum"].sqrt() + stats[f"stats/act_grad_norm/{module_name}"] = l2_norm.item() + stats[f"stats/act_grad_max/{module_name}"] = stats_dict["max"].item() + + if wandb.run is not None: + if self.log_stat_wandb: + wandb.log({**stats, **important_info}, step=iteration) + else: + wandb.log(important_info, step=iteration) + + if self.save_s3: + easy_io.dump({**stats, **important_info}, f"s3://rundir/{self.name}/stats_{iteration:09d}.pt") diff --git a/cosmos_training/cosmos/callbacks/ofu.py b/cosmos_training/cosmos/callbacks/ofu.py new file mode 100644 index 00000000..fc3ec883 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/ofu.py @@ -0,0 +1,293 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OFU (Operational FLOPs Utilization) callback for OmniMoT training. + +Computes and logs OFU metrics by launching ``nvidia-smi dmon`` as a background +subprocess and parsing the Tensor Core activity (mmaact) and processor clock +(pclk) columns. OFU is defined as:: + + OFU = mmaact * (pclk / max_pclk) + +where ``max_pclk`` is the max boost clock for the detected hardware (e.g. +1980 MHz for H100, 2062 MHz for GB200). The result is in the 0-100 range. +""" + +from __future__ import annotations + +import subprocess +import threading +from collections import defaultdict +from dataclasses import dataclass + +import torch +import wandb + +from cosmos.model.attention.utils import is_blackwell_dc +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer +from cosmos.utils import log +from cosmos.utils.distributed import is_rank0, rank0_only + + +@dataclass +class HardwareTarget: + """Hardware-specific constants for OFU normalisation. + + Attributes: + name: Human-readable name (used as W&B tag, e.g. "H100"). + max_pclk_mhz: Max boost SM clock in MHz used to normalise OFU. + """ + + name: str + max_pclk_mhz: float + + +# Pre-defined hardware targets +H100 = HardwareTarget(name="H100", max_pclk_mhz=1980.0) +GB200 = HardwareTarget(name="GB200", max_pclk_mhz=2062.0) + + +class OFUCallback(EveryN): + """Callback that computes and logs Operational FLOPs Utilization (OFU) to W&B. + + OFU = mmaact * (pclk / max_pclk), where mmaact is the MMA activity + percentage and pclk is the current processor clock from ``nvidia-smi dmon``. + ``max_pclk`` is determined from the detected hardware (H100 or GB200). + The result is in the 0-100 range. + + The callback launches ``nvidia-smi dmon`` as a background subprocess on + ``on_train_start`` and a daemon thread continuously reads its output. + At every logging interval, accumulated samples are consumed, averaged per GPU + and overall, and logged to W&B under ``ofu/{hardware_name}``. + + Args: + hit_thres: Number of warm-up training iterations to skip before logging. + """ + + def __init__( + self, + *args, + hit_thres: int = 5, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.hardware_target = GB200 if is_blackwell_dc() else H100 + self.hit_thres = hit_thres + + # Subprocess state + self._process: subprocess.Popen | None = None + self._reader_thread: threading.Thread | None = None + self._stop_event = threading.Event() + + # Buffered samples protected by a lock: list of (gpu_idx, mmaact, pclk) + self._lock = threading.Lock() + self._samples: list[tuple[int, float, float]] = [] + + # Column indices parsed from the header (set by _reader_loop) + self._col_gpu: int | None = None + self._col_mmaact: int | None = None + self._col_pclk: int | None = None + + # Warm-up counter + self._hit_counter: int = 0 + + # ------------------------------------------------------------------ # + # Background reader + # ------------------------------------------------------------------ # + + def _parse_header(self, line: str) -> bool: + """Parse a dmon header line to locate column indices. + + Called on every ``#`` line because nvidia-smi dmon reprints the header + every few seconds. Returns True if ``gpu``, ``mmaact``, and ``pclk`` + columns are all found; warns only when the column-names line (identified + by the presence of ``gpu``) lacks a required column. Silently ignores + the units line (``# Idx W C ...``) which does not contain ``gpu``. + """ + cols = line.lstrip("#").strip().split() + col_map = {name.lower(): idx for idx, name in enumerate(cols)} + gpu_idx = col_map.get("gpu") + mmaact_idx = col_map.get("mmaact") + pclk_idx = col_map.get("pclk") + if gpu_idx is not None and mmaact_idx is not None and pclk_idx is not None: + if self._col_mmaact is None: + log.info(f"OFUCallback: found mmaact at column {mmaact_idx}, pclk at column {pclk_idx}") + self._col_gpu = gpu_idx + self._col_mmaact = mmaact_idx + self._col_pclk = pclk_idx + return True + if gpu_idx is not None: + missing = [name for name, idx in [("mmaact", mmaact_idx), ("pclk", pclk_idx)] if idx is None] + log.warning( + f"OFUCallback: column(s) {missing} not found in nvidia-smi dmon header: {cols}. " + "OFU metrics will not be available." + ) + return False + + def _reader_loop(self) -> None: + """Background thread that reads nvidia-smi dmon output line-by-line.""" + assert self._process is not None and self._process.stdout is not None + + for line in self._process.stdout: + if self._stop_event.is_set(): + break + line = line.strip() + if not line: + continue + + # Header lines repeat every few seconds — always re-parse so that a + # missed or failed first parse is recovered on the next occurrence. + if line.startswith("#"): + self._parse_header(line) + continue + + # Skip data lines until we have column indices + if self._col_gpu is None or self._col_mmaact is None or self._col_pclk is None: + continue + + parts = line.split() + try: + gpu_idx = int(parts[self._col_gpu]) + mmaact = float(parts[self._col_mmaact]) + pclk = float(parts[self._col_pclk]) + except (ValueError, IndexError): + continue + + with self._lock: + self._samples.append((gpu_idx, mmaact, pclk)) + + # ------------------------------------------------------------------ # + # Lifecycle hooks + # ------------------------------------------------------------------ # + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + if not is_rank0(): + return + + try: + # --gpm-metrics 5 means that we access Tensor Activity under mmaact column. + # -d 5 means that we sample the data every 5 seconds. + cmd = ["nvidia-smi", "dmon", "--gpm-metrics", "5", "-d", "5"] + self._process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, # line-buffered + ) + self._reader_thread = threading.Thread(target=self._reader_loop, daemon=True) + self._reader_thread.start() + log.info(f"OFUCallback: launched nvidia-smi dmon --gpm-metrics 5") + except FileNotFoundError: + log.warning("OFUCallback: nvidia-smi not found, OFU metrics will not be available") + except Exception as e: + log.warning(f"OFUCallback: failed to launch nvidia-smi dmon: {e}") + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + if not is_rank0(): + return + self._stop_event.set() + if self._process is not None: + try: + self._process.terminate() + self._process.wait(timeout=5) + except ProcessLookupError: + pass # already exited + except subprocess.TimeoutExpired: + self._process.kill() + self._process = None + if self._reader_thread is not None: + self._reader_thread.join(timeout=5) + self._reader_thread = None + + # ------------------------------------------------------------------ # + # Per-step gating + # ------------------------------------------------------------------ # + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # All ranks must enter super().on_training_step_end() so they reach the + # distributed barrier inside EveryN. Only rank 0 has samples to clear. + if self._hit_counter < self.hit_thres: + self._hit_counter += 1 + if self._hit_counter == self.hit_thres: + # Discard samples collected during warm-up (compilation, allocation, etc.) + with self._lock: + self._samples.clear() + return + # Delegate to EveryN for the periodic reporting logic + super().on_training_step_end(model, data_batch, output_batch, loss, iteration) + + # ------------------------------------------------------------------ # + # Periodic reporting + # ------------------------------------------------------------------ # + + @rank0_only + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if self._process is None: + return + + # Drain buffered samples + with self._lock: + samples = list(self._samples) + self._samples.clear() + + if not samples: + log.warning( + f"OFUCallback: no nvidia-smi samples collected at iteration {iteration}. " + "Check that the dmon subprocess launched and that the mmaact column is present." + ) + return + + # Compute per-GPU OFU: mmaact * (pclk / max_pclk) + max_pclk = self.hardware_target.max_pclk_mhz + gpu_ofu: dict[int, list[float]] = defaultdict(list) + gpu_mmaact: dict[int, list[float]] = defaultdict(list) + gpu_pclk: dict[int, list[float]] = defaultdict(list) + for gpu_idx, mmaact, pclk in samples: + gpu_ofu[gpu_idx].append(mmaact * (pclk / max_pclk)) + gpu_mmaact[gpu_idx].append(mmaact) + gpu_pclk[gpu_idx].append(pclk) + + # Overall averages across all GPUs and samples + all_ofu = [v for vals in gpu_ofu.values() for v in vals] + all_mmaact = [v for vals in gpu_mmaact.values() for v in vals] + all_pclk = [v for vals in gpu_pclk.values() for v in vals] + + log_info: dict[str, float] = { + f"ofu/{self.hardware_target.name}": sum(all_ofu) / len(all_ofu), + "ofu/mmaact": sum(all_mmaact) / len(all_mmaact), + "ofu/avg_pclk_mhz": sum(all_pclk) / len(all_pclk), + "ofu/num_samples": float(len(samples)), + } + + if wandb.run is not None: + wandb.log(log_info, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/param_count.py b/cosmos_training/cosmos/callbacks/param_count.py new file mode 100644 index 00000000..9950c991 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/param_count.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +import torch.nn as nn + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback +from cosmos.utils.count_params import count_params +from cosmos.utils.distributed import rank0_only +from cosmos.utils.easy_io import easy_io + + +class ParamCount(Callback): + def __init__( + self, + save_s3: bool = False, + ): + self.save_s3 = save_s3 + self.name = self.__class__.__name__ + + @rank0_only + def on_train_start(self, model: Union[ImaginaireModel, list[nn.Module]], iteration: int = 0) -> None: + if isinstance(model, list): + num_param = sum([count_params(m) for m in model]) + else: + num_param = count_params(model) + + log.info(f"Total number of parameters on current rank: {num_param}", rank0_only=False) + info = { + "num_parameters": num_param, + } + + if self.save_s3: + rank = distributed.get_rank() + easy_io.dump(info, f"s3://rundir/{self.name}_{rank}.yaml") diff --git a/cosmos_training/cosmos/callbacks/sequence_packing_padding.py b/cosmos_training/cosmos/callbacks/sequence_packing_padding.py new file mode 100644 index 00000000..3a1000db --- /dev/null +++ b/cosmos_training/cosmos/callbacks/sequence_packing_padding.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import wandb + +import cosmos.data.vfm.sequence_packing as sequence_packing +from cosmos.callbacks.every_n import EveryN +from cosmos.model._base import ImaginaireModel +from cosmos.trainer import ImaginaireTrainer + + +class SequencePackingPadding(EveryN): + """ + Callback that saves lengths to which und and gen sequences are padded. This information will be used + to compute FLOPs done during training. + + Args: + every_n (int): Frequency with which callback is run during training. + """ + + def __init__(self, every_n: int = 500): + super().__init__(every_n=every_n, step_size=1, barrier_after_run=False, run_at_start=True) + + def every_n_impl( + self, + trainer: ImaginaireTrainer, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int, + ) -> None: + if wandb.run: + log_dict = { + "SequencePackingPadding/max_causal_len_image_batch": sequence_packing.MAX_CAUSAL_LEN_IMAGE_BATCH, + "SequencePackingPadding/max_full_len_image_batch": sequence_packing.MAX_FULL_LEN_IMAGE_BATCH, + "SequencePackingPadding/max_causal_len_video_batch": sequence_packing.MAX_CAUSAL_LEN_VIDEO_BATCH, + "SequencePackingPadding/max_full_len_video_batch": sequence_packing.MAX_FULL_LEN_VIDEO_BATCH, + } + modality = "video" + if "is_image_batch" in output_batch: + modality = "image" if output_batch["is_image_batch"] else "video" + if "und_token_length" in output_batch: + log_dict[f"SequencePackingPadding/und_token_length_{modality}"] = output_batch["und_token_length"] + if "gen_token_length" in output_batch: + log_dict[f"SequencePackingPadding/gen_token_length_{modality}"] = output_batch["gen_token_length"] + if "action_token_length" in output_batch: + log_dict[f"SequencePackingPadding/action_token_length"] = output_batch["action_token_length"] + if "sound_token_length" in output_batch: + log_dict[f"SequencePackingPadding/sound_token_length"] = output_batch["sound_token_length"] + if "vision_token_length" in output_batch: + log_dict[f"SequencePackingPadding/vision_token_length"] = output_batch["vision_token_length"] + + wandb.log( + log_dict, + step=iteration, + ) diff --git a/cosmos_training/cosmos/callbacks/sigma_loss_analysis.py b/cosmos_training/cosmos/callbacks/sigma_loss_analysis.py new file mode 100644 index 00000000..93bcd0e5 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/sigma_loss_analysis.py @@ -0,0 +1,356 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Optional + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import torch +import torch.distributed as dist +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, misc +from cosmos.utils.callback import Callback +from cosmos.utils.easy_io import easy_io + + +def _get_quantile_bins(n=10) -> np.ndarray: + """Get predefined bins based on logarithmically spaced values""" + points = torch.linspace(0, 1, n + 1) + return points.numpy() + + +@dataclass +class _SigmaLossCache: + """A fixed-size queue for caching sigma and loss tensors. + + Stores sigma/loss pairs on CPU. + When the total number of elements exceeds queue_size, the oldest entries + are automatically removed to maintain the size limit. + + Args: + queue_size: Maximum number of elements to store in the cache. + """ + + def __init__(self, queue_size: int = 2000): + self.queue_size = queue_size + self.reset() + + def reset(self): + self.sigma_list: list[torch.Tensor] = [] + self.loss_list: list[torch.Tensor] = [] + self._total_elements: int = 0 + + def add(self, sigma: torch.Tensor, loss: torch.Tensor): + # Convert to bf16 and store on CPU + sigma_cpu = sigma.detach().cpu().to(torch.bfloat16) + loss_cpu = loss.detach().cpu().to(torch.bfloat16) + + self.sigma_list.append(sigma_cpu) + self.loss_list.append(loss_cpu) + self._total_elements += sigma_cpu.numel() + + # Remove oldest elements if queue exceeds max size + while self._total_elements > self.queue_size and len(self.sigma_list) > 1: + removed_sigma = self.sigma_list.pop(0) + self.loss_list.pop(0) + self._total_elements -= removed_sigma.numel() + + def get_arrays(self) -> tuple[torch.Tensor, torch.Tensor]: + if not self.sigma_list: + return torch.tensor([], dtype=torch.bfloat16), torch.tensor([], dtype=torch.bfloat16) + + sigma_arr = torch.cat(self.sigma_list, dim=0) # [N_total] (concatenated across cached batches) + loss_arr = torch.cat(self.loss_list, dim=0) # [N_total] + + return sigma_arr, loss_arr + + +class SigmaLossAnalysis(Callback): + """Analyze the relationship between sigma (noise level) and flow matching loss. + + This callback tracks per-instance flow matching losses at different sigma values + during training. It maintains separate caches for image and video batches, + periodically aggregates statistics across all distributed ranks, and logs + the results to wandb. + + The analysis helps understand how well the model learns to denoise at different + noise levels, which is useful for diagnosing training dynamics in flow matching + models. + + Args: + every_n: Log statistics every N iterations. + every_n_viz: Create visualization plots every N iterations (must be multiple of every_n). + save_s3: If True, save raw data to S3 for offline analysis. + """ + + def __init__( + self, + every_n: int = 1, + every_n_viz: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.save_s3 = save_s3 + self.every_n = every_n + assert every_n_viz % every_n == 0, "every_n_viz must be a multiple of every_n in sigma_loss_analysis callback" + self.every_n_viz = every_n_viz + self.name = self.__class__.__name__ + + self.image_cache = _SigmaLossCache(queue_size=2000) + self.video_cache = _SigmaLossCache(queue_size=2000) + + def _create_analysis_plots( + self, + sigma_arr: torch.Tensor, + loss_arr: torch.Tensor, # [N] # [N] + ) -> Optional[wandb.Image]: + if len(sigma_arr) == 0: + return None + + # Convert to numpy for plotting + sigma_np = sigma_arr.cpu().float().numpy() + loss_np = loss_arr.cpu().float().numpy() + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) + + # Get predefined bins based on logarithmically spaced values + sigma_bins = _get_quantile_bins(10) + + # y_tick_min, y_tick_max = 0, 1.0 + y_tick_min, y_tick_max = 0, 1.0 + # 2D histogram with exponential sigma bins and fixed [0,1] loss range + loss_bins = np.linspace(y_tick_min, y_tick_max, 20) + + counts, xedges, yedges = np.histogram2d(sigma_np, loss_np, bins=(sigma_bins, loss_bins)) + if counts.max() < 0.1: + return None + + # Plot heatmap with exponential scale colormap + im = ax1.imshow( + counts.T, + origin="lower", + aspect="auto", + extent=[sigma_bins[0], sigma_bins[-1], y_tick_min, y_tick_max], + norm=matplotlib.colors.LogNorm(vmin=1, vmax=counts.max()), + ) + plt.colorbar(im, ax=ax1) + + # Set fixed loss ticks from 0 to 1 + yticks = np.linspace(y_tick_min, y_tick_max, 6) + ax1.set_yticks(yticks) + ax1.set_yticklabels([f"{y:.1f}" for y in yticks]) + + ax1.set_xlabel("Sigma") + ax1.set_ylabel("Loss") + title = "Sigma vs Loss Distribution" + ax1.set_title(title) + + # Sigma histogram with loss statistics per bin + hist_counts, _ = np.histogram(sigma_np, bins=sigma_bins) + bin_indices = np.digitize(sigma_np, sigma_bins) - 1 + + # Calculate statistics per bin + n_bins = len(sigma_bins) - 1 + means = np.zeros(n_bins) + stds = np.zeros(n_bins) + for i in range(n_bins): + bin_mask = bin_indices == i + if bin_mask.any(): + means[i] = loss_np[bin_mask].mean() + stds[i] = loss_np[bin_mask].std() + else: + means[i] = np.nan + stds[i] = np.nan + + # Plot histogram + bin_centers = (sigma_bins[:-1] + sigma_bins[1:]) / 2 + ax2.bar(bin_centers, hist_counts, width=np.diff(sigma_bins), alpha=0.3, align="center") + + # Plot loss statistics on twin axis + ax2_twin = ax2.twinx() + valid_mask = ~np.isnan(means) + ax2_twin.errorbar( + bin_centers[valid_mask], means[valid_mask], yerr=stds[valid_mask], color="red", fmt="o-", alpha=0.5 + ) + + ax2.set_xlabel("Sigma (Log Scale)") + ax2.set_ylabel("Count") + ax2_twin.set_ylabel("Loss (mean ± std)") + title = "Sigma Distribution with Loss Statistics" + ax2.set_title(title) + + # Add grid for better readability + ax1.grid(True, alpha=0.3) + ax2.grid(True, alpha=0.3) + + # Create log-scale labels + sigma_labels = [f"{val:.1e}" for val in sigma_bins] + ax1.set_xticks(sigma_bins[1:-1]) # Skip boundary bins + ax1.set_xticklabels(sigma_labels[1:-1], rotation=45) + ax1.set_xscale("linear") + ax2.set_xticks(sigma_bins[1:-1]) + ax2.set_xticklabels(sigma_labels[1:-1], rotation=45) + ax2.set_xscale("linear") + + plt.tight_layout() + fig_img = wandb.Image(fig) + plt.close(fig) + + return fig_img + + def _process_stats(self, sigma: torch.Tensor, loss: torch.Tensor) -> dict: + """Calculate summary statistics for sigma and loss distributions. + + Args: + sigma: Tensor of sigma (noise level) values. + loss: Tensor of corresponding loss values. + + Returns: + Dictionary containing: + - sigma_log_mean: Mean of log(sigma). Log-space is used since sigma spans + multiple orders of magnitude, a standard practice on flow matching / EDM models. + - sigma_log_std: Standard deviation of log(sigma). + - loss_mean: Average loss across all samples. + - loss_std: Standard deviation of loss, measuring spread. + - loss_min: Minimum loss value observed. + - loss_max: Maximum loss value observed. + - loss_median: Median (50th percentile) loss, robust to outliers. + - loss_q1: First quartile (25th percentile) of loss. + - loss_q3: Third quartile (75th percentile) of loss. + """ + return { + "sigma_log_mean": float(sigma.log().mean()), + "sigma_log_std": float(sigma.log().std()), + "loss_mean": float(loss.mean()), + "loss_std": float(loss.std()), + "loss_min": float(loss.min()), + "loss_max": float(loss.max()), + "loss_median": float(loss.median()), + "loss_q1": float(torch.quantile(loss.float(), 0.25)), + "loss_q3": float(torch.quantile(loss.float(), 0.75)), + } + + def _gather_and_save(self, cache: _SigmaLossCache, iteration: int, prefix: str, log_viz: bool = True) -> dict: + info = {} + + # Gather data from all ranks + local_sigma, local_loss = cache.get_arrays() + world_size = dist.get_world_size() + + if world_size > 1: + # Gather sizes first + local_size = torch.tensor([len(local_sigma)], dtype=torch.long, device="cuda") # [1] + sizes = [torch.zeros_like(local_size) for _ in range(world_size)] + dist.all_gather(sizes, local_size) + sizes = [s.item() for s in sizes] + + # Gather data + max_size = max(sizes) + if max_size > 0: + # Move to GPU for gathering + padded_sigma = torch.zeros(max_size, dtype=torch.bfloat16, device="cuda") # [max_size] + padded_loss = torch.zeros(max_size, dtype=torch.bfloat16, device="cuda") # [max_size] + + if len(local_sigma) > 0: + padded_sigma[: len(local_sigma)] = local_sigma.cuda() + padded_loss[: len(local_loss)] = local_loss.cuda() + + all_sigma = [torch.zeros_like(padded_sigma) for _ in range(world_size)] + all_loss = [torch.zeros_like(padded_loss) for _ in range(world_size)] + + dist.all_gather(all_sigma, padded_sigma) + dist.all_gather(all_loss, padded_loss) + + if distributed.is_rank0(): + # Combine data from all ranks + valid_sigma = [] + valid_loss = [] + for sigma, loss, size in zip(all_sigma, all_loss, sizes): + if size > 0: + valid_sigma.append(sigma[:size]) + valid_loss.append(loss[:size]) + + if valid_sigma: + sigma_arr = torch.cat(valid_sigma) # [N_total] (across all ranks) + loss_arr = torch.cat(valid_loss) # [N_total] + + # Overall statistics + info[f"{prefix}/total_samples"] = sigma_arr.shape[0] + + # Calculate statistics + stats = self._process_stats(sigma_arr, loss_arr) + info.update({f"{prefix}/{k}": v for k, v in stats.items()}) + + # Create visualization + if log_viz: + fig_img = self._create_analysis_plots(sigma_arr, loss_arr) + print(fig_img) + if fig_img is not None: + info[f"{prefix}/distribution_plot"] = fig_img + + if self.save_s3: + save_data = { + "sigma": sigma_arr.cpu(), + "loss": loss_arr.cpu(), + "stats": {k: v for k, v in info.items() if not isinstance(v, wandb.Image)}, + } + easy_io.dump( + save_data, + f"s3://rundir/{self.name}/{prefix}_Iter{iteration:09d}.pkl", + ) + + cache.reset() + return info + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ): + sigma = output_batch["sigma"] + fm_loss_vision_per_instance = output_batch["flow_matching_loss_vision_per_instance"] + + # sigma is [B] (base), [B,1] (TF), or [B,T_max] (DF); reduce to [B] for logging + assert sigma.ndim <= 2, f"Sigma should be [B] or [B,T_max], got shape {sigma.shape}" + if sigma.ndim == 2: + sigma = sigma.mean(dim=-1) # [B] (reduced from [B,T_max] or [B,1]) + + if model.is_image_batch(data_batch): + self.image_cache.add(sigma, fm_loss_vision_per_instance) + else: + self.video_cache.add(sigma, fm_loss_vision_per_instance) + + if iteration % self.every_n == 0: + info = {} + + with misc.timer("sigma_loss_analysis"): + log_viz = iteration % self.every_n_viz == 0 + # Process image data + if len(self.image_cache.sigma_list) > 0: + info.update(self._gather_and_save(self.image_cache, iteration, "sigma_loss_image", log_viz=log_viz)) + + # Process video data + if len(self.video_cache.sigma_list) > 0: + info.update(self._gather_and_save(self.video_cache, iteration, "sigma_loss_video", log_viz=log_viz)) + + if distributed.is_rank0() and info and wandb.run: + wandb.log(info, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/skip_nan_step.py b/cosmos_training/cosmos/callbacks/skip_nan_step.py new file mode 100644 index 00000000..9bf1d5b1 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/skip_nan_step.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.distributed as dist + +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback + + +class SkipNaNStep(Callback): + """Skip the optimizer step only when ALL ranks produce NaN/Inf loss. + + When only some ranks produce NaN, the existing GradClip callback's + nan_to_num handling is sufficient (NaN gradients become 0, valid + gradients from clean ranks are still used). This callback only + intervenes when every rank has NaN, meaning no useful gradient + signal exists. + + The all-reduce ensures all ranks agree on skip/no-skip, preventing + NCCL desync. + + Args: + max_consecutive_nan: Abort training after this many consecutive + all-rank-NaN optimizer steps. Set to 0 to disable the limit. + """ + + def __init__(self, max_consecutive_nan: int = 100) -> None: + super().__init__() + self.max_consecutive_nan = max_consecutive_nan + self._nan_detected = False + self._consecutive_nan_count = 0 + + def on_before_backward( + self, + model_ddp: distributed.DistributedDataParallel, + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss).any() or torch.isinf(loss).any(): + self._nan_detected = True + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + nan_flag = torch.tensor([1.0 if self._nan_detected else 0.0], device="cuda") + dist.all_reduce(nan_flag, op=dist.ReduceOp.SUM) + nan_rank_count = int(nan_flag.item()) + world_size = dist.get_world_size() + + if nan_rank_count > 0 and nan_rank_count < world_size: + self._consecutive_nan_count = 0 + + elif nan_rank_count == world_size: + if isinstance(model_ddp, distributed.DistributedDataParallel): + model = model_ddp.module + else: + model = model_ddp + for param in model.parameters(): + if param.grad is not None: + param.grad.zero_() + + self._consecutive_nan_count += 1 + log.warning( + f"ALL ranks NaN/Inf at iteration {iteration}, skipping optimizer step " + f"(consecutive: {self._consecutive_nan_count})", + ) + + if self.max_consecutive_nan > 0 and self._consecutive_nan_count >= self.max_consecutive_nan: + raise RuntimeError( + f"Training unstable: all-rank NaN/Inf loss for {self._consecutive_nan_count} " + f"consecutive optimizer steps at iteration {iteration}. Aborting.", + ) + else: + self._consecutive_nan_count = 0 + + self._nan_detected = False diff --git a/cosmos_training/cosmos/callbacks/termination_signal_checkpoint.py b/cosmos_training/cosmos/callbacks/termination_signal_checkpoint.py new file mode 100644 index 00000000..a127e62d --- /dev/null +++ b/cosmos_training/cosmos/callbacks/termination_signal_checkpoint.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Callback that saves an emergency checkpoint before a Slurm job is killed. + +Slurm signal timeline +--------------------- + +**Timeout** (job hits ``--time`` limit):: + + T SIGUSR1 → batch shell (N is 300 via ``--signal=B:SIGUSR1@300``) + T+N sec SIGTERM → all processes + T+N+KillWait SIGKILL → anything still alive (KillWait is 30s) + +**Preemption** (higher-priority job needs the nodes):: + + T SIGUSR1 → batch shell + T+Grace SIGTERM + SIGKILL (GraceTime is 300s in GCP-IAD) + +**User cancel** (``scancel ``):: + + T SIGTERM → all processes (no SIGUSR1) + T+KillWait SIGKILL → anything still alive (KillWait is 30s) + +Implementation +-------------- + +* **How the SIGUSR1 signal is handled:** + + - The Slurm batch script responds to SIGUSR1 by creating a sentinel file + (``$SLURM_LOG_DIR/SIGUSR1_RECEIVED``) on the shared filesystem. + - This callback polls for the presence of this sentinel file at the end of + each training step. + - When detected, it triggers an emergency checkpoint save. + +* **Why the Python process can't receive SIGUSR1 directly:** + + - Pyxis/Enroot containers do not receive SIGUSR1 signals from Slurm (the + signal is sent to the batch shell, not the container). + - Attempted to forward SIGUSR1 with both ``srun`` and + ``scancel --signal`` in batch scripts, proven not working. + +* **Why no SIGTERM handler is needed:** + + - The SIGUSR1 signal is the trigger of preemption and timeout, it + is already able to distinguish them from user cancel. + - Before SIGTERM arrives there are at least 300 s (``GraceTime=300`` for + preemption, ``--signal=B:SIGUSR1@N`` for timeout), which is sufficent + for the poll to detect the sentinel and save a checkpoint. + - SIGTERM and the subsequent SIGKILL are left to terminate the process + naturally after the checkpoint has been saved. + - SIGTERM and SIGUSR1 are logged so we can observe signal delivery into + Pyxis/Enroot containers for debugging purposes. + +To avoid redundant checkpoints, a save is only performed if at least +``save_iter * min_save_fraction`` iterations have elapsed since the last +checkpoint. +""" + +from __future__ import annotations + +import os +import signal +import sys + +import torch + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import log +from cosmos.utils.callback import Callback + + +class TerminationSignalCheckpoint(Callback): + """Save a checkpoint in response to SIGUSR1 (preemption or timeout). + + Args: + min_save_fraction: Fraction of the regular checkpoint interval (between 0 + and 1) that must have elapsed since the last checkpoint before an + emergency save is allowed. Defaults to 1/3. + """ + + def __init__(self, min_save_fraction: float = 1 / 3): + super().__init__() + self._min_save_fraction = min_save_fraction + self._current_iteration: int = 0 + self._last_checkpoint_iteration: int = 0 + # Captured from on_before_optimizer_step so we can call checkpointer.save(). + self._optimizer: torch.optim.Optimizer | None = None + self._scheduler: torch.optim.lr_scheduler.LRScheduler | None = None + self._grad_scaler: torch.amp.GradScaler | None = None + # Sentinel file created by the batch-shell trap when SIGUSR1 arrives. + # This is the sole detection mechanism because srun/Pyxis does not + # relay SIGUSR1 into the container. + slurm_log_dir = os.environ.get("SLURM_LOG_DIR", "") + self._sigusr1_sentinel = os.path.join(slurm_log_dir, "SIGUSR1_RECEIVED") if slurm_log_dir else "" + + # ------------------------------------------------------------------ + # Lifecycle hooks + # ------------------------------------------------------------------ + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self._current_iteration = iteration + self._last_checkpoint_iteration = iteration + self._install_termination_signal_handlers() + + def on_before_optimizer_step( + self, + model_ddp, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + self._optimizer = optimizer + self._scheduler = scheduler + self._grad_scaler = grad_scaler + + def on_save_checkpoint_success(self, iteration: int = 0, elapsed_time: float = 0) -> None: + self._last_checkpoint_iteration = iteration + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self._current_iteration = iteration + + if not self._sigusr1_sentinel or not os.path.exists(self._sigusr1_sentinel): + return + + log.info("[TerminationSignalCheckpoint] Detected SIGUSR1 sentinel file. Will save checkpoint.") + + # Check if the minimum progress has been reached since the last checkpoint. + min_progress = int(self.config.checkpoint.save_iter * self._min_save_fraction) + if (iteration - self._last_checkpoint_iteration) < min_progress: + log.info( + f"[TerminationSignalCheckpoint] Only {iteration - self._last_checkpoint_iteration} iterations " + f"since last checkpoint (threshold {min_progress}). Skipping checkpoint save." + ) + sys.exit(0) + + assert self._optimizer is not None, ( + "[TerminationSignalCheckpoint] Optimizer reference not set — on_before_optimizer_step was never called" + ) + + log.info(f"[TerminationSignalCheckpoint] Saving checkpoint at iteration {iteration}.") + self.trainer.checkpointer.save(model, self._optimizer, self._scheduler, self._grad_scaler, iteration=iteration) + # Async DCP checkpointing queues the write to a background process. + # We must wait for it to finish before exiting. + self.trainer.checkpointer.finalize() + log.info(f"[TerminationSignalCheckpoint] Checkpoint saved at iteration {iteration}.") + sys.exit(0) + + # ------------------------------------------------------------------ + # Termination signal handlers + # ------------------------------------------------------------------ + + def _install_termination_signal_handlers(self) -> None: + signal.signal(signal.SIGTERM, self._log_sigterm) + log.info("[TerminationSignalCheckpoint] Installed SIGTERM handler.") + + def _log_sigterm(self, signum: int, frame: object) -> None: + log.info(f"[TerminationSignalCheckpoint] Received SIGTERM at iteration {self._current_iteration}.") diff --git a/cosmos_training/cosmos/callbacks/training_stats.py b/cosmos_training/cosmos/callbacks/training_stats.py new file mode 100644 index 00000000..d4881716 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/training_stats.py @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch +import torch.distributed as dist +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed +from cosmos.utils.callback import Callback +from cosmos.callbacks.wandb_log import _LossRecord +from cosmos.data.vfm.action.domain_utils import EMBODIMENT_TO_DOMAIN_ID + +# Build inverse mapping: domain_id -> embodiment_type. First occurrence wins when multiple embodiment names share the +# same domain id. +DOMAIN_ID_TO_EMBODIMENT: dict[int, str] = {} +for _k, _v in EMBODIMENT_TO_DOMAIN_ID.items(): + DOMAIN_ID_TO_EMBODIMENT.setdefault(_v, _k) + + +class TrainingStatsCallback(Callback): + """Callback for tracking and logging training mode and embodiment statistics to wandb.""" + + def __init__(self, log_freq: int = 100): + super().__init__() + self.log_freq = log_freq + self._mode_counts: dict[str, int] = {} + self._mode_total_count: int = 0 + self._embodiment_counts: dict[str, int] = {} + self._embodiment_total_count: int = 0 + self._per_embodiment_loss: dict[str, _LossRecord] = {} + self._per_embodiment_sub_loss: dict[str, dict[str, _LossRecord]] = {} + + def _accumulate_mode_counts(self, data_batch: dict[str, torch.Tensor]) -> None: + modes = data_batch.get("mode", None) + if modes is None: + return + + if isinstance(modes, str): + modes_list = [modes] + elif isinstance(modes, (list, tuple)): + modes_list = [str(m) for m in modes] + elif isinstance(modes, torch.Tensor): + # Defensive: support cases where mode might be encoded numerically. + modes_list = [str(m) for m in modes.detach().cpu().tolist()] + else: + modes_list = [str(modes)] + + for mode in modes_list: + self._mode_total_count += 1 + self._mode_counts[mode] = self._mode_counts.get(mode, 0) + 1 + + def _accumulate_embodiment_counts(self, data_batch: dict[str, torch.Tensor]) -> None: + domain_ids = data_batch.get("domain_id", None) + if domain_ids is None: + return + + if isinstance(domain_ids, int): + domain_id_list = [domain_ids] + elif isinstance(domain_ids, (list, tuple)): + domain_id_list = [int(d) for d in domain_ids if d is not None] + elif isinstance(domain_ids, torch.Tensor): + # Flatten to handle any shape (scalar, 1D, or 2D with trailing dim) + domain_id_list = [int(d) for d in domain_ids.detach().cpu().flatten().tolist()] + else: + domain_id_list = [int(domain_ids)] + + for domain_id in domain_id_list: + embodiment = DOMAIN_ID_TO_EMBODIMENT.get(domain_id, f"unknown_{domain_id}") + self._embodiment_total_count += 1 + self._embodiment_counts[embodiment] = self._embodiment_counts.get(embodiment, 0) + 1 + + def _gather_global_mode_counts(self) -> tuple[int, dict[str, int]]: + """ + Returns (global_total, global_mode_counts) aggregated across all ranks. + """ + local: dict[str, int] = dict(self._mode_counts) + local["__total__"] = int(self._mode_total_count) + + if dist.is_available() and dist.is_initialized(): + world_size = int(dist.get_world_size()) + gathered: list[dict[str, int] | None] = [None for _ in range(world_size)] + dist.all_gather_object(gathered, local) + else: + gathered = [local] + + global_total = 0 + global_counts: dict[str, int] = {} + for item in gathered: + if not item: + continue + global_total += int(item.get("__total__", 0)) + for k, v in item.items(): + if k == "__total__": + continue + global_counts[k] = global_counts.get(k, 0) + int(v) + return global_total, global_counts + + def _gather_global_embodiment_counts(self) -> tuple[int, dict[str, int]]: + """ + Returns (global_total, global_embodiment_counts) aggregated across all ranks. + """ + local: dict[str, int] = dict(self._embodiment_counts) + local["__total__"] = int(self._embodiment_total_count) + + if dist.is_available() and dist.is_initialized(): + world_size = int(dist.get_world_size()) + gathered: list[dict[str, int] | None] = [None for _ in range(world_size)] + dist.all_gather_object(gathered, local) + else: + gathered = [local] + + global_total = 0 + global_counts: dict[str, int] = {} + for item in gathered: + if not item: + continue + global_total += int(item.get("__total__", 0)) + for k, v in item.items(): + if k == "__total__": + continue + global_counts[k] = global_counts.get(k, 0) + int(v) + return global_total, global_counts + + def _build_mode_log_dict( + self, *, log_prefix: str, global_total: int, global_counts: dict[str, int] + ) -> dict[str, float]: + info: dict[str, float] = {} + + denom = float(global_total) if global_total > 0 else 0.0 + for mode in sorted(global_counts.keys()): + count = float(global_counts.get(mode, 0)) + pct = (100.0 * count / denom) if denom > 0 else 0.0 + info[f"{log_prefix}_stats_mode/{mode}"] = pct + + return info + + def _build_embodiment_log_dict( + self, *, log_prefix: str, global_total: int, global_counts: dict[str, int] + ) -> dict[str, float]: + info: dict[str, float] = {} + + denom = float(global_total) if global_total > 0 else 0.0 + for embodiment in sorted(global_counts.keys()): + count = float(global_counts.get(embodiment, 0)) + pct = (100.0 * count / denom) if denom > 0 else 0.0 + info[f"{log_prefix}_stats_embodiment/{embodiment}"] = pct + + return info + + def _get_batch_embodiment(self, data_batch: dict[str, torch.Tensor]) -> str | None: + """Extract the embodiment name from the first non-None sample's domain_id.""" + domain_ids = data_batch.get("domain_id", None) + if domain_ids is None: + return None + if isinstance(domain_ids, torch.Tensor): + if domain_ids.numel() == 0: + return None + domain_id = int(domain_ids.flatten()[0].item()) + elif isinstance(domain_ids, (list, tuple)): + first = next((d for d in domain_ids if d is not None), None) + if first is None: + return None + domain_id = int(first) + else: + domain_id = int(domain_ids) + return DOMAIN_ID_TO_EMBODIMENT.get(domain_id, f"unknown_{domain_id}") + + def _accumulate_per_embodiment_loss( + self, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + ) -> None: + embodiment = self._get_batch_embodiment(data_batch) + if embodiment is None: + return + + if embodiment not in self._per_embodiment_loss: + self._per_embodiment_loss[embodiment] = _LossRecord() + self._per_embodiment_loss[embodiment].loss += loss.detach().float() + self._per_embodiment_loss[embodiment].iter_count += 1 + + if embodiment not in self._per_embodiment_sub_loss: + self._per_embodiment_sub_loss[embodiment] = {} + for key in output_batch: + if "loss" in key and "per_instance" not in key: + if key not in self._per_embodiment_sub_loss[embodiment]: + self._per_embodiment_sub_loss[embodiment][key] = _LossRecord() + self._per_embodiment_sub_loss[embodiment][key].loss += output_batch[key].detach().float() + self._per_embodiment_sub_loss[embodiment][key].iter_count += 1 + + def _compute_per_embodiment_loss_stats(self, log_prefix: str) -> dict[str, float]: + """Compute per-embodiment loss averages across all ranks. + + All ranks must call this method (contains collective operations). + Returns the log dict (only meaningful on rank 0). + """ + dist_available = dist.is_available() and dist.is_initialized() + world_size = int(dist.get_world_size()) if dist_available else 1 + + # Step 1: gather union of embodiment names across ranks + local_embodiments = sorted(self._per_embodiment_loss.keys()) + if dist_available: + all_embodiments: list[list[str] | None] = [None for _ in range(world_size)] + dist.all_gather_object(all_embodiments, local_embodiments) + else: + all_embodiments = [local_embodiments] + union_embodiments = sorted({e for el in all_embodiments for e in el}) + + # Step 2: gather union of sub-loss keys across ranks + local_sub_keys = sorted({k for d in self._per_embodiment_sub_loss.values() for k in d}) + if dist_available: + all_sub_keys: list[list[str] | None] = [None for _ in range(world_size)] + dist.all_gather_object(all_sub_keys, local_sub_keys) + else: + all_sub_keys = [local_sub_keys] + union_sub_keys = sorted({k for kl in all_sub_keys for k in kl}) + + # Step 3: insert NaN dummy _LossRecord for missing embodiment/key combos + for emb in union_embodiments: + if emb not in self._per_embodiment_loss: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") + dummy.iter_count += 1 + self._per_embodiment_loss[emb] = dummy + if emb not in self._per_embodiment_sub_loss: + self._per_embodiment_sub_loss[emb] = {} + for key in union_sub_keys: + if key not in self._per_embodiment_sub_loss[emb]: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") + dummy.iter_count += 1 + self._per_embodiment_sub_loss[emb][key] = dummy + + # Step 4: compute distributed averages (all ranks participate in all_reduce) + log_dict: dict[str, float] = {} + for emb in union_embodiments: + avg, valid = self._per_embodiment_loss[emb].get_stat(return_valid_mask_sum=True) + if valid > 0: + log_dict[f"{log_prefix}_stats_loss/{emb}"] = avg + + for emb in union_embodiments: + for key in union_sub_keys: + avg, valid = self._per_embodiment_sub_loss[emb][key].get_stat(return_valid_mask_sum=True) + if valid > 0: + log_dict[f"{log_prefix}_stats_loss_detail/{emb}_{key}"] = avg + + # Step 5: reset accumulators + self._per_embodiment_loss = {} + self._per_embodiment_sub_loss = {} + + return log_dict + + @torch.no_grad() + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self._accumulate_mode_counts(data_batch) + self._accumulate_embodiment_counts(data_batch) + self._accumulate_per_embodiment_loss(data_batch, output_batch, loss) + + if iteration % self.log_freq != 0: + return + + # All ranks must participate in collective operations below. + mode_total, mode_counts = self._gather_global_mode_counts() + embodiment_total, embodiment_counts = self._gather_global_embodiment_counts() + per_embodiment_loss_dict = self._compute_per_embodiment_loss_stats(log_prefix="train") + + if not distributed.is_rank0(): + return + + if wandb.run is None: + return + + log_dict: dict[str, float] = {} + log_dict.update( + self._build_mode_log_dict(log_prefix="train", global_total=mode_total, global_counts=mode_counts) + ) + log_dict.update( + self._build_embodiment_log_dict( + log_prefix="train", global_total=embodiment_total, global_counts=embodiment_counts + ) + ) + log_dict.update(per_embodiment_loss_dict) + + wandb.log({k: float(v) for k, v in log_dict.items()}, step=iteration) diff --git a/cosmos_training/cosmos/callbacks/wandb_log.py b/cosmos_training/cosmos/callbacks/wandb_log.py new file mode 100644 index 00000000..fb3b0dab --- /dev/null +++ b/cosmos_training/cosmos/callbacks/wandb_log.py @@ -0,0 +1,239 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback +from cosmos.utils.easy_io import easy_io + + +@dataclass +class _LossRecord: + loss: torch.Tensor | float = 0 + iter_count: int = 0 + name: str | None = None + + def reset(self) -> None: + self.loss = 0 + self.iter_count = 0 + + def get_stat(self, return_valid_mask_sum: bool = False) -> Tuple[float, float]: + if self.iter_count == 0: + self.loss = torch.tensor([float("nan")], device="cuda") + self.iter_count = 1 + self.loss = self.loss.mean() + msg_str = f"{self.name}: sum_loss={self.loss.item()}/iter_count={self.iter_count}=" + avg_loss_tensor = self.loss / self.iter_count + # Create a mask (1 if valid, 0 if NaN or Inf) + valid_mask = torch.tensor([torch.isfinite(avg_loss_tensor).float()], device="cuda") + msg_str += f"avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}, " + + # Replace NaN/Inf with 0 to avoid affecting sum + avg_loss_tensor = torch.where( + torch.isfinite(avg_loss_tensor), avg_loss_tensor, torch.tensor([0.0], device="cuda") + ) + + # Reduce across all ranks + dist.all_reduce(avg_loss_tensor, op=dist.ReduceOp.SUM) # Sum of valid losses + dist.all_reduce(valid_mask, op=dist.ReduceOp.SUM) # Count of valid losses + msg_str += f" | all_reduce: avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}" + # Compute final average, avoiding division by zero + if valid_mask.item() > 0: + final_avg_loss = (avg_loss_tensor / valid_mask).item() + valid_mask_sum = valid_mask.item() + else: + final_avg_loss = 0.0 # Default to zero if all values were invalid + valid_mask_sum = 0 + + avg_loss = final_avg_loss + msg_str += f" | final: avg_loss={final_avg_loss}" + if self.name is not None: + log.debug(msg_str, rank0_only=False) + self.reset() + if return_valid_mask_sum: + return avg_loss, valid_mask_sum + else: + return avg_loss + + +class WandbCallback(Callback): + def __init__( + self, + logging_iter_multipler: int = 1, + save_logging_iter_multipler: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.final_loss_log = _LossRecord() + self.final_loss_log_per_dataset = {} + self.final_all_loss_log = {} + self.logging_iter_multipler = logging_iter_multipler + self.save_logging_iter_multipler = save_logging_iter_multipler + assert self.logging_iter_multipler > 0, "logging_iter_multipler should be greater than 0" + self.save_s3 = save_s3 + self.wandb_extra_tag = f"@{logging_iter_multipler}" if logging_iter_multipler > 1 else "" + self.name = "wandb_loss_log" + self.wandb_extra_tag + self.unstable_count = torch.zeros(1, device="cuda") + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss) or torch.isinf(loss): + log.critical( + f"Unstable loss {loss} at iteration {iteration}", + rank0_only=False, + ) + self.unstable_count += 1 + + dataset_name = data_batch.get("dataset_name", "default") + + # Handle case where dataset_name gets batched into a list + if isinstance(dataset_name, list): + # For reasoner, dataset_name should be a list with single item + # For generator, dataset_name is a list of size larger than one, pick first item + dataset_name = dataset_name[0] + + if dataset_name == "default" and "__url__" in data_batch: + # try to get the name from url + dataset_name = "/".join(data_batch["__url__"][0].split("/")[:-1]) + + if dataset_name not in self.final_loss_log_per_dataset: + self.final_loss_log_per_dataset[dataset_name] = _LossRecord() + self.final_loss_log_per_dataset[dataset_name].name = dataset_name + self.final_loss_log_per_dataset[dataset_name].loss += loss.detach().float() + self.final_loss_log_per_dataset[dataset_name].iter_count += 1 + + # VLM: per-sequence loss normalization using token counts when available + if "avg_num_assistant_tokens" in output_batch: + per_seq_loss = ( + loss + * output_batch["avg_num_assistant_tokens"] + * output_batch["batch_size_local"] + / output_batch["current_num_assistant_tokens"] + ) + per_seq_key = f"per_seq/{dataset_name}" + if per_seq_key not in self.final_loss_log_per_dataset: + self.final_loss_log_per_dataset[per_seq_key] = _LossRecord() + self.final_loss_log_per_dataset[per_seq_key].name = per_seq_key + self.final_loss_log_per_dataset[per_seq_key].loss += per_seq_loss + self.final_loss_log_per_dataset[per_seq_key].iter_count += 1 + + self.final_loss_log.loss += loss.detach().float() + self.final_loss_log.iter_count += 1 + + for key in output_batch.keys(): + # Curve can be plotted only on aggregated loss, not per-instance loss + if "loss" in key and "per_instance" not in key: + if key not in self.final_all_loss_log: + self.final_all_loss_log[key] = _LossRecord() + self.final_all_loss_log[key].loss += output_batch[key].detach().float() + self.final_all_loss_log[key].iter_count += 1 + + if iteration % (self.config.trainer.logging_iter * self.logging_iter_multipler) == 0: + avg_final_loss = self.final_loss_log.get_stat() + + avg_final_all_loss = {} + for key in self.final_all_loss_log.keys(): + avg_final_all_loss[key] = self.final_all_loss_log[key].get_stat() + + # Step 1: Gather all dataset names across ranks + local_dataset_names = list(self.final_loss_log_per_dataset.keys()) + all_dataset_names = [None for _ in range(dist.get_world_size())] + dist.all_gather_object(all_dataset_names, local_dataset_names) + + # Step 2: Create the union of all dataset names + union_dataset_names = set() + for names in all_dataset_names: + union_dataset_names.update(names) + # Step 3: For any missing dataset name, add dummy _LossRecord with NaN loss + union_dataset_names = sorted(list(union_dataset_names)) # This is very important! + for dataset_name in union_dataset_names: + if dataset_name not in self.final_loss_log_per_dataset: + dummy = _LossRecord() + dummy.loss += torch.tensor([float("nan")], device="cuda") # Will be masked out + dummy.iter_count += 1 + self.final_loss_log_per_dataset[dataset_name] = dummy + + avg_final_loss_per_dataset = {} + for dataset_name in union_dataset_names: + avg_loss, valid_mask_sum = self.final_loss_log_per_dataset[dataset_name].get_stat( + return_valid_mask_sum=True + ) + if valid_mask_sum > 0: + avg_final_loss_per_dataset[dataset_name] = avg_loss + + dist.all_reduce(self.unstable_count, op=dist.ReduceOp.SUM) + + if distributed.is_rank0() and wandb.run is not None: + info = {} + info.update( + { + f"train{self.wandb_extra_tag}/loss": avg_final_loss, + f"train{self.wandb_extra_tag}/unstable_count": self.unstable_count.item(), + "iteration": iteration, + } + ) + for key, loss in avg_final_all_loss.items(): + info.update( + { + f"train{self.wandb_extra_tag}_detail/{key}": loss, + } + ) + for dataset_name, loss in avg_final_loss_per_dataset.items(): + tag = "" + if "per_seq" in dataset_name: + tag = "_per_seq" + dataset_name = dataset_name.replace("per_seq/", "") + info.update( + { + f"train{self.wandb_extra_tag}_per_data{tag}/{dataset_name}": loss, + } + ) + if self.save_s3: + if ( + iteration + % ( + self.config.trainer.logging_iter + * self.logging_iter_multipler + * self.save_logging_iter_multipler + ) + == 0 + ): + easy_io.dump( + info, + f"s3://rundir/{self.name}/Train_Iter{iteration:09d}.json", + ) + + if wandb: + wandb.log(info, step=iteration) + + # reset unstable count + self.unstable_count.zero_() + self.final_loss_log_per_dataset = {} diff --git a/cosmos_training/cosmos/callbacks/wandb_log_eval.py b/cosmos_training/cosmos/callbacks/wandb_log_eval.py new file mode 100644 index 00000000..e84ac46c --- /dev/null +++ b/cosmos_training/cosmos/callbacks/wandb_log_eval.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback +from cosmos.utils.easy_io import easy_io + + +@dataclass +class _LossRecord: + loss: float = 0 + iter_count: int = 0 + + def reset(self) -> None: + self.loss = 0 + self.iter_count = 0 + + def get_stat(self) -> Tuple[float, float]: + if self.iter_count > 0: + avg_loss_tensor = self.loss / self.iter_count + # Create a mask (1 if valid, 0 if NaN or Inf) + valid_mask = torch.tensor([torch.isfinite(avg_loss_tensor).float()], device="cuda") + + # Replace NaN/Inf with 0 to avoid affecting sum + avg_loss_tensor = torch.where( + torch.isfinite(avg_loss_tensor), avg_loss_tensor, torch.tensor([0.0], device="cuda") + ) + + # Reduce across all ranks + dist.all_reduce(avg_loss_tensor, op=dist.ReduceOp.SUM) # Sum of valid losses + dist.all_reduce(valid_mask, op=dist.ReduceOp.SUM) # Count of valid losses + + # Compute final average, avoiding division by zero + if valid_mask.item() > 0: + final_avg_loss = (avg_loss_tensor / valid_mask).item() + else: + final_avg_loss = 0.0 # Default to zero if all values were invalid + + avg_loss = final_avg_loss + else: + avg_loss = 0 + self.reset() + return avg_loss + + +class WandbCallback(Callback): + def __init__( + self, + save_s3: bool = False, + ) -> None: + super().__init__() + self.final_loss_log = _LossRecord() + self.final_loss_log_per_dataset = {} + + self.save_s3 = save_s3 + self.wandb_extra_tag = "" + self.name = "wandb_loss_val_log" + self.unstable_count = torch.zeros(1, device="cuda") + self.url_key_list = [] + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss) or torch.isinf(loss): + log.critical( + f"Unstable val loss {loss} at iteration {iteration}", + rank0_only=False, + ) + self.unstable_count += 1 + + dataset_name = data_batch.get("dataset_name", "default") + + # Handle case where dataset_name gets batched into a list + if isinstance(dataset_name, list): + + assert len(dataset_name) == 1, "dataset_name should be a list of 1" + dataset_name = dataset_name[0] + + if dataset_name not in self.final_loss_log_per_dataset: + self.final_loss_log_per_dataset[dataset_name] = _LossRecord() + + self.final_loss_log_per_dataset[dataset_name].loss += loss.detach().float() + self.final_loss_log_per_dataset[dataset_name].iter_count += 1 + self.final_loss_log.loss += loss.detach().float() + self.final_loss_log.iter_count += 1 + + self.url_key_list.append(f"{data_batch.get('__url__', [''])[0]}, {data_batch.get('__key__', [''])[0]}") + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + avg_final_loss = self.final_loss_log.get_stat() + + log.info(f"avg_final_loss: {avg_final_loss}") + dist.all_reduce(self.unstable_count, op=dist.ReduceOp.SUM) + # gather url and key list from all ranks + url_key_list = [None] * dist.get_world_size() + dist.all_gather_object(url_key_list, self.url_key_list) + url_key_list = [item for sublist in url_key_list for item in sublist] + + unique_url_key_list = list(set(url_key_list)) + if distributed.is_rank0(): + info = {} + log.info( + f"[val] number of unique url and key: {len(unique_url_key_list)} / {len(url_key_list)}; avg_final_loss: {avg_final_loss}" + ) + info.update( + { + f"val{self.wandb_extra_tag}/loss": avg_final_loss, + f"val{self.wandb_extra_tag}/unstable_count": self.unstable_count.item(), + "iteration": iteration, + f"val{self.wandb_extra_tag}/num_unique_url_key": len(unique_url_key_list), + f"val{self.wandb_extra_tag}/total_url_key": len(url_key_list), + } + ) + if self.save_s3: + easy_io.dump( + info, + f"s3://rundir/{self.name}/Val_Iter{iteration:09d}.json", + ) + + if wandb.run is not None: + wandb.log(info, step=iteration) + + # reset unstable count + self.unstable_count.zero_() + self.url_key_list = [] diff --git a/cosmos_training/cosmos/callbacks/wandb_log_simgple.py b/cosmos_training/cosmos/callbacks/wandb_log_simgple.py new file mode 100644 index 00000000..97e9a26b --- /dev/null +++ b/cosmos_training/cosmos/callbacks/wandb_log_simgple.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +import torch +import torch.distributed as dist +import torch.utils.data +import wandb + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import distributed, log +from cosmos.utils.callback import Callback +from cosmos.utils.easy_io import easy_io + + +@dataclass +class _LossRecord: + loss: float = 0 + iter_count: int = 0 + name: str = None + + def reset(self) -> None: + self.loss = 0 + self.iter_count = 0 + + def get_stat(self, return_valid_mask_sum: bool = False) -> Tuple[float, float]: + if self.iter_count == 0: + self.loss = torch.tensor([float("nan")], device="cuda") # [1] + self.iter_count = 1 + msg_str = f"{self.name}: sum_loss={self.loss.item()}/iter_count={self.iter_count}=" + avg_loss_tensor = self.loss / self.iter_count + # Create a mask (1 if valid, 0 if NaN or Inf) + valid_mask = torch.tensor([torch.isfinite(avg_loss_tensor).float()], device="cuda") # [1] + msg_str += f"avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}, " + + # Replace NaN/Inf with 0 to avoid affecting sum + avg_loss_tensor = torch.where( + torch.isfinite(avg_loss_tensor), + avg_loss_tensor, + torch.tensor([0.0], device="cuda"), # [1] + ) + + # Reduce across all ranks + dist.all_reduce(avg_loss_tensor, op=dist.ReduceOp.SUM) # Sum of valid losses + dist.all_reduce(valid_mask, op=dist.ReduceOp.SUM) # Count of valid losses + msg_str += f" | all_reduce: avg_loss={avg_loss_tensor.item()}, valid_mask={valid_mask.item()}" + # Compute final average, avoiding division by zero + if valid_mask.item() > 0: + final_avg_loss = (avg_loss_tensor / valid_mask).item() + valid_mask_sum = valid_mask.item() + else: + final_avg_loss = 0.0 # Default to zero if all values were invalid + valid_mask_sum = 0 + + avg_loss = final_avg_loss + msg_str += f" | final: avg_loss={final_avg_loss}" + if self.name is not None: + log.debug(msg_str, rank0_only=False) + self.reset() + if return_valid_mask_sum: + return avg_loss, valid_mask_sum + else: + return avg_loss + + +class WandbCallback(Callback): + def __init__( + self, + logging_iter_multipler: int = 1, + save_logging_iter_multipler: int = 1, + save_s3: bool = False, + ) -> None: + super().__init__() + self.final_loss_log = _LossRecord() + self.final_all_loss_log = {} + self.logging_iter_multipler = logging_iter_multipler + self.save_logging_iter_multipler = save_logging_iter_multipler + assert self.logging_iter_multipler > 0, "logging_iter_multipler should be greater than 0" + self.save_s3 = save_s3 + self.wandb_extra_tag = f"@{logging_iter_multipler}" if logging_iter_multipler > 1 else "" + self.name = "wandb_loss_log" + self.wandb_extra_tag + self.unstable_count = torch.zeros(1, device="cuda") # [1] + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if torch.isnan(loss) or torch.isinf(loss): + log.critical( + f"Unstable loss {loss} at iteration {iteration}", + rank0_only=False, + ) + self.unstable_count += 1 + + self.final_loss_log.loss += loss.detach().float() + self.final_loss_log.iter_count += 1 + + for key in output_batch.keys(): + if "loss" in key: + if key not in self.final_all_loss_log: + self.final_all_loss_log[key] = _LossRecord() + self.final_all_loss_log[key].loss += output_batch[key].detach().float() + self.final_all_loss_log[key].iter_count += 1 + + if iteration % (self.config.trainer.logging_iter * self.logging_iter_multipler) == 0: + avg_final_loss = self.final_loss_log.get_stat() + + avg_final_all_loss = {} + for key in self.final_all_loss_log.keys(): + avg_final_all_loss[key] = self.final_all_loss_log[key].get_stat() + + dist.all_reduce(self.unstable_count, op=dist.ReduceOp.SUM) + + if distributed.is_rank0() and wandb.run is not None: + info = {} + info.update( + { + f"train{self.wandb_extra_tag}/loss": avg_final_loss, + f"train{self.wandb_extra_tag}/unstable_count": self.unstable_count.item(), + "iteration": iteration, + } + ) + for key, loss in avg_final_all_loss.items(): + info.update( + { + f"train{self.wandb_extra_tag}_detail/{key}": loss, + } + ) + if self.save_s3: + if ( + iteration + % ( + self.config.trainer.logging_iter + * self.logging_iter_multipler + * self.save_logging_iter_multipler + ) + == 0 + ): + easy_io.dump( + info, + f"s3://rundir/{self.name}/Train_Iter{iteration:09d}.json", + ) + + if wandb: + wandb.log(info, step=iteration) + # reset unstable count + self.unstable_count.zero_() diff --git a/cosmos_training/cosmos/callbacks/wandb_vis.py b/cosmos_training/cosmos/callbacks/wandb_vis.py new file mode 100644 index 00000000..0bf9c647 --- /dev/null +++ b/cosmos_training/cosmos/callbacks/wandb_vis.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import html +import json +import tempfile +from typing import Literal + +import torch +import wandb +from einops import rearrange + +from cosmos.utils.config import JobConfig +from cosmos.utils import callback, distributed +from cosmos.utils.easy_io import easy_io + + +class VisualizationLoggingCallback(callback.WandBCallback): + def __init__( + self, + every_n: int = 1, + input_normalization: str | None = None, + job: JobConfig | None = None, + config: callback.Config | None = None, + trainer: callback.ImaginaireTrainer | None = None, + ): + super().__init__(config, trainer) + self.every_n = every_n + self.input_normalization = input_normalization + self.job = job + + def on_training_step_end( + self, + model, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + if iteration % self.every_n == 0 and distributed.is_rank0() and wandb.run is not None: + # log images to wandb for rank0 + self.log_videos(log_type="train", data=data_batch, output=output_batch, iteration=iteration) + + def on_validation_step_end( + self, + model, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: # Collect the validation batch and aggregate the overall loss. + super().on_validation_step_end(model, data_batch, output_batch, loss, iteration) + if iteration % self.every_n == 0 and distributed.is_rank0() and wandb.run is not None: + # log images to wandb for rank0 + self.log_videos(log_type="val", data=data_batch, output=output_batch, iteration=iteration) + + @torch.no_grad() + def log_videos( + self, + log_type: Literal["train", "val"], + data: dict[str, torch.Tensor], + output: dict[str, torch.Tensor], + iteration: int = 0, + ): + if "raw_image" in data: + video = data["raw_image"][0].cpu() # [3,T,H,W], range [0, 255], uint8 + elif "raw_video" in data: + video = data["raw_video"][0].cpu() # [3,T,H,W], range [0, 255], uint8 + video = video.permute(1, 0, 2, 3) # [T,3,H,W] + video = video.numpy() + wandb_video = rearrange(video, "t c h w -> t h w c") # [T,H,W,3] + # create temp file + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") + temp_file_path = temp_file.name + easy_io.dump(wandb_video, temp_file_path, file_format="mp4", format="mp4", fps=10, quality=5) + wandb.log({f"{log_type}/video": wandb.Video(temp_file_path)}, step=iteration) + + # Convert dialog string to JSON and format it for HTML display + try: + dialog_json = json.loads(data["dialog_str"][0], indent=4) + formatted_html = f""" +
{dialog_json}
+ """ + wandb.log({f"{log_type}/prompt": wandb.Html(formatted_html)}, step=iteration) + except Exception as e: + # Fallback to original format if JSON conversion fails + # Escape HTML tags in the dialog string to display them properly + escaped_dialog = html.escape(data["dialog_str"][0]) + wandb.log( + {f"{log_type}/prompt": wandb.Html(f"
{escaped_dialog}
")}, + step=iteration, + ) diff --git a/cosmos_training/cosmos/checkpoint/__init__.py b/cosmos_training/cosmos/checkpoint/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/checkpoint/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/checkpoint/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5685c3fd08b91e486216c14ba9ef1b93018653d8 GIT binary patch literal 236 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/checkpoint/__pycache__/dcp.cpython-312.pyc b/cosmos_training/cosmos/checkpoint/__pycache__/dcp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4ca0d0800b75f23f6cb2edb7004edada84ef5d6 GIT binary patch literal 41087 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 Pp;2tu{Bw>xjJW*&y_~Oh)YO*q@_wOHP)LT{ zCR9BpG|$xEv-5ZzZJaR+|Ds?jvG(mKba`NVZ4kB&x-t~D6Mi;e&ixU0gOCZ(SHJS@ zz8l!CVBc`cJZBzhPRk2n_LG8XGSX2ffs~X^mS$LvW|=x%u37UNZmbClmgde`yLoo) z@WNRgRl$GjtbVw2RtF_1(*o6cWvUV!= zdCL=|9tBqJ6>0HU?NrB_S&qx=tvmX)XuvoyXyp52!&&R95f1Fopqxn~;GrSyADO|0 z;c=T9b}$YHZqRQU-w#}Az!yo@i#($TD=6zmeE>t>81k+}A33lEGY+}9pL!p<;lOaX z=kczvLoQ@9O(Ir(gifq;0G#h0F%jz$GNO?OY^g77YDvdNL3aUmHo`K_3l>HqXghE@ zUKK4DB_s$yi7~JqhVk;3mKOokS*s@xTO%&Q z9`{@>TMm~#Oe4XE%n6sW)wLzF;AXf19J~e95VAG4gFgPlg_FfcERZ}B4qIwg9*eNA z8qi8R(j1K;XDIzqZM|(xRPAEM1t33@WRvKX8L-4zz6mJcSTBQOX4)bHN>K%3ssfSs zrfNuxlGZ`gfQxh3xBsPpsy#3+Q`jJzHBkqJnQgb*=4Px}R$%+gvf`p;L2IFnz?UuS zhsgF8mV#wb?pT(<4PzC+;0$6l1wgFh!!yT0*%Q$E6%@Yy?(Ry++Wsgm-Ue{oons>l zZ<4^PFYxGM8wnma%`H8Sz@nuDFNk~!oQCx2E;I zNqsLVpiW!qE>G&0r}h0weLvCBQY1U?o!onHZ@O(xw#}r3x-!{%C#fK}L5dr*Dsq>| zjhjzC|MJO=o6k!HwU+J%Gw#-S_{HNpkM2wxS10h*uO$UEz?Lh+^-1IUwDIW#zWPC; zqZ#(yeaIfWkKF0*wF!LNA0-$VHZN9AE5OZ9j)yk&I3JEj0MU7IE`cttrB!cI7vS!8 zqma>fXAZ~aD>yb0-~z8xd|_csZ)-C}e%iiySzj;pOYP%%=RXl2fG@oNM7|Fs$tsHS g!XQfH87coo-u;K{KPAo6{Ekxlv;N@=0&|-6e`Xe>R{#J2 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/checkpoint/__pycache__/s3_filesystem.cpython-312.pyc b/cosmos_training/cosmos/checkpoint/__pycache__/s3_filesystem.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..088703cfb1cd8fa8d7fb96dad0dea5d10b76d2a0 GIT binary patch literal 16837 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 None: + pass + + @abstractmethod + def load( + self, + model: ImaginaireModel, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None, + grad_scaler: Optional[torch.amp.GradScaler] = None, + ) -> int: + pass + + @property + def save_bucket(self): + """Get the bucket name for saving checkpoints.""" + return self.config_checkpoint.save_to_object_store.bucket if self.save_to_object_store else None + + @property + def load_bucket(self): + """Get the bucket name for loading checkpoints.""" + return self.config_checkpoint.load_from_object_store.bucket if self.load_from_object_store else None + + @property + def save_dirname(self): + return ( + f"s3://{self.save_bucket}/{self._object_store_dirname}" + if self.save_to_object_store + else self._local_dirname + ) + + @property + def load_dirname(self): + return ( + f"s3://{self.load_bucket}/{self._object_store_dirname}" + if self.load_from_object_store + else self._local_dirname + ) + + def finalize(self) -> None: + """Finalize the checkpointer.""" + if self.save_thread: + self.save_thread.join() + + def _read_latest_checkpoint_file(self) -> str | None: + """Get the file name of the latest saved checkpoint. If it doesn't exist, return None. + + Returns: + checkpoint_file (str | None): file name of the latest saved checkpoint. + """ + checkpoint_file = None + checkpoint_path = os.path.join(self.load_dirname, "latest_checkpoint.txt") + if easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key): + checkpoint_file = easy_io.load(f"{checkpoint_path}", backend_key=self.load_s3_backend_key).strip() + + return checkpoint_file + + def _write_latest_checkpoint_file(self, checkpoint_file: str) -> None: + """Track the file name of the latest saved checkpoint. + + Args: + checkpoint_file (str): file name of the latest saved checkpoint. + """ + content = f"{checkpoint_file}\n" + checkpoint_path = os.path.join(self.save_dirname, "latest_checkpoint.txt") + easy_io.dump( + content, + checkpoint_path, + backend_key=self.save_s3_backend_key, + ) + + def _check_checkpoint_exists(self, checkpoint_path: str) -> None: + """If the file checkpoint_path does not exist, raise an error. + + Args: + checkpoint_path (str): full path to the checkpoint. + """ + if not easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key): + raise FileNotFoundError(f"File not found (object store): {checkpoint_path}") diff --git a/cosmos_training/cosmos/checkpoint/dcp.py b/cosmos_training/cosmos/checkpoint/dcp.py new file mode 100644 index 00000000..2027591e --- /dev/null +++ b/cosmos_training/cosmos/checkpoint/dcp.py @@ -0,0 +1,855 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Distributed checkpoint (DCP) directory structure and storage backends. + +The checkpointer saves model state in a sharded format across multiple processes: + +self.save_dirname/ +├── iter_000000005/ # Checkpoint at iteration 5 +│ ├── model/ # Model state shards +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ ├── optim/ # Optimizer state shards +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ ├── scheduler/ # Learning rate scheduler state +│ │ ├── __0_0.distcp # Shard 0 from rank 0 +│ │ └── __1_0.distcp # Shard 1 from rank 1 +│ └── trainer/ # Additional training state +│ ├── __0_0.distcp # Shard 0 from rank 0 +│ └── __1_0.distcp # Shard 1 from rank 1 +│ └── dataloader/ # Optional per-rank dataloader state +│ ├── rank_0.pkl +│ └── rank_1.pkl +└── latest_checkpoint.txt # Points to most recent checkpoint folder, e.g. iter_000000005 + +Storage backends: +- Local filesystem: + self.save_dirname = "{config_job.path_local}/checkpoints" + +- S3 object store: + self.save_dirname = "s3://{bucket}/{config_job.path}/checkpoints" + where bucket = self.config_checkpoint.save_to_object_store.bucket + +The sharded format enables efficient distributed saving/loading by: +1. Parallelizing I/O across processes +2. Reducing memory usage per process +3. Supporting both local and cloud storage backends +""" + +import enum +import multiprocessing +import os +import re +import time +from multiprocessing import get_context +from typing import Any, Dict, List, Optional, Protocol, Tuple, Union, runtime_checkable + +import torch +import torch.distributed as dist +import torch.distributed.checkpoint as dcp +from torch import nn +from torch.distributed.checkpoint.filesystem import FileSystemReader, FileSystemWriter +from torch.distributed.checkpoint.metadata import ( + STATE_DICT_TYPE, + Metadata, + StorageMeta, +) +from torch.distributed.checkpoint.state_dict import ( + StateDictOptions, + get_model_state_dict, + set_model_state_dict, +) +from torch.distributed.checkpoint.stateful import Stateful + +from cosmos.checkpoint.base import AbstractCheckpointer +from cosmos.checkpoint.s3_filesystem import S3StorageReader, S3StorageWriter +from cosmos.utils.config import CheckpointConfig, JobConfig +from cosmos.model._base import ImaginaireModel +from cosmos.utils import callback, distributed, log, misc +from cosmos.utils.easy_io import easy_io +from cosmos.utils.vfm.rand_state import get_rand_state_dict, set_rand_state_dict + + +class ModelWrapper(Stateful): + """ + Wrapper for model state dict handling. Strips away the _orig_mod. prefix + among other things from the state dict keys. + """ + + def __init__(self, model: nn.Module) -> None: + self.model = model + + def state_dict(self) -> dict[str, Any]: + return get_model_state_dict(self.model) + + def load_state_dict(self, state_dict: dict[str, Any]) -> None: + set_model_state_dict( + self.model, + model_state_dict=state_dict, + options=StateDictOptions(strict=True), + ) + + +@runtime_checkable +class _DataloaderStateHandler(Protocol): + """Structural contract for callbacks that participate in dataloader-state checkpointing.""" + + checkpoint_component: str + + def has_checkpoint_state(self) -> bool: ... + def state_dict(self) -> dict[Any, Any]: ... + def load_state_dict(self, state_dict: dict[Any, Any]) -> None: ... + + +class _DataloaderWrapper: + """Adapter that surfaces a dataloader-state callback's checkpoint API. + + Walks the registered callbacks at construction time and binds to the + first callback that: + + 1. Declares ``checkpoint_component == "dataloader"``, AND + 2. Returns ``True`` from ``has_checkpoint_state()``. + + The bound callback's ``state_dict`` / ``load_state_dict`` methods are + re-exposed via :meth:`state_dict` / :meth:`load_state_dict`. Callers + must gate those on :meth:`has_state` — invoking them when nothing was + bound raises :class:`RuntimeError`. + + Note: only the first callback tagged ``checkpoint_component=="dataloader"`` + is considered; if it does not currently want its state checkpointed, + no further callbacks are searched. In practice there is at most one + such callback (see ``DataLoaderStateCallback``). + """ + + def __init__(self, callbacks: callback.CallBackGroup | None) -> None: + self._callback: _DataloaderStateHandler | None = None + if callbacks is None: + return + for current_callback in callbacks._callbacks: + if getattr(current_callback, "checkpoint_component", None) != "dataloader": + continue + if current_callback.has_checkpoint_state(): + self._callback = current_callback + return + + def has_state(self) -> bool: + return self._callback is not None + + def state_dict(self) -> dict[Any, Any]: + if self._callback is None: + raise RuntimeError("No dataloader state handler is registered, cannot save dataloader state.") + return self._callback.state_dict() + + def load_state_dict(self, state_dict: dict[Any, Any]) -> None: + if self._callback is None: + raise RuntimeError("No dataloader state handler is registered, cannot load dataloader state.") + self._callback.load_state_dict(state_dict) + + +class AsyncMode(str, enum.Enum): + DISABLED = "disabled" + ASYNC_WITH_PINNED_MEM = "async_with_pinned_mem" + + +class Terminate: + pass + + +class SaveDone: + def __init__(self, iteration: int, elapsed_time: float, succeeded: bool): + self.iteration = iteration + self.elapsed_time = elapsed_time + self.succeeded = succeeded + + def __str__(self): + return f"SaveDone(iteration={self.iteration}, elapsed_time={self.elapsed_time}, succeeded={self.succeeded})" + + +def save_checkpoint_in_background( + receiver_queue: multiprocessing.Queue, + sender_queue: multiprocessing.Queue, + config_checkpoint: CheckpointConfig, + config_job: JobConfig, +) -> None: + """ + Handles model checkpoint saving in a separate background process using PyTorch's distributed functionality. + This function runs in a dedicated process to avoid blocking the main training loop. + + Args: + receiver_queue: Queue to receive state dictionaries and commands from the main process + sender_queue: Queue to send completion signals back to the main process + config_checkpoint: Configuration settings for checkpoint saving behavior + config_job: Configuration settings for the training job + + Flow: + 1. Initializes distributed processing environment + 2. Continuously waits for state dictionaries to save + 3. Saves checkpoints asynchronously + 4. Signals completion back to main process + 5. Terminates when receiving a Terminate signal + + Raises: + AssertionError: If received object is neither Terminate signal nor valid state dict tuple + + Note: + - Uses a different port than the main process to avoid conflicts + - Disables TorchElastic agent store for checkpoint operations + - Automatically cleans up distributed process group on exit + """ + # Configure distributed environment + os.environ["MASTER_PORT"] = str(int(os.environ["MASTER_PORT"]) + 2) + os.environ["TORCHELASTIC_USE_AGENT_STORE"] = "False" + + # Set up GPU device and distributed processing + torch.cuda.set_device(int(os.environ["LOCAL_RANK"])) + if dist.is_initialized(): + dist.destroy_process_group() + dist.init_process_group(backend="gloo") + + # Initialize checkpointing mechanism + checkpoint_handler = DistributedCheckpointer( + config_checkpoint=config_checkpoint, + config_job=config_job, + callbacks=None, + disable_async=True, + ) + + while True: + log.info(f"Checkpoint background process is ready for next task, waiting for new state_dict") + received_data = receiver_queue.get() + log.info(f"Checkpoint background process received new state_dict") + + if isinstance(received_data, Terminate): + log.info(f"Checkpoint background process received termination signal, closing sender queue") + break + + assert isinstance(received_data, tuple), "Received data must be a tuple of (state_dict, checkpoint_path)" + state_dict, checkpoint_path = received_data + + # Save checkpoint and measure time taken. + start_time = time.monotonic() + iteration = state_dict["trainer"][0]["iteration"] + succeeded = False + + try: + log.info(f"Saving checkpoint to {checkpoint_path}") + checkpoint_handler.save_state_dict_worker(state_dict, checkpoint_path) + succeeded = True + except Exception as e: + log.error(f"Error saving checkpoint to {checkpoint_path}: {e}") + # continue because if the thread exits, the main thread keeps on adding to the queue + finally: + elapsed_time = time.monotonic() - start_time + log.info( + f"Checkpoint save completed in background process. " + f"Time taken: {elapsed_time:.2f} seconds, iteration: {iteration}, " + f"status: {'SUCCESS' if succeeded else 'FAILURE'}" + ) + sender_queue.put(SaveDone(iteration, elapsed_time, succeeded)) + + log.info("Cleaning up: destroying distributed process group") + dist.destroy_process_group() + + +def _replace_keys_with_ema_keys(state_dict: STATE_DICT_TYPE) -> STATE_DICT_TYPE: + """ + Renames model parameters from "net." to "net_ema.". + """ + if not all(k.startswith("net.") for k in state_dict.keys()): + raise ValueError("State dict must start with net. keys when load_ema_to_reg is True") + return {k.replace("net.", "net_ema."): v for k, v in state_dict.items()} + + +class CustomLoadPlanner(dcp.DefaultLoadPlanner): + """ + CustomLoadPlanner that supports ignoring keys during checkpoint load. + This is useful when the checkpoint is saved with a different component + architecture, e.g. different RoPE embeddings than the current model. + """ + + def __init__( + self, + flatten_state_dict: bool = True, + flatten_sharded_tensors: bool = True, + allow_partial_load: bool = False, + keys_to_skip_loading: List[str] = [], + load_ema_to_reg: bool = False, + ) -> None: + super().__init__( + flatten_state_dict=flatten_state_dict, + flatten_sharded_tensors=flatten_sharded_tensors, + allow_partial_load=allow_partial_load, + ) + self.keys_to_skip_loading = keys_to_skip_loading + self.load_ema_to_reg = load_ema_to_reg + if len(keys_to_skip_loading) > 0: + log.info(f"Skipping loading of keys that match the following patterns: {keys_to_skip_loading}") + + def set_up_planner( + self, + state_dict: STATE_DICT_TYPE, + metadata: Metadata | None = None, + is_coordinator: bool = False, + ) -> None: + state_dict = self._skip_keys_if_found(state_dict) + + if self.load_ema_to_reg: + state_dict = _replace_keys_with_ema_keys(state_dict) + + super().set_up_planner( + state_dict=state_dict, + metadata=metadata, + is_coordinator=is_coordinator, + ) + + def _skip_keys_if_found( + self, + state_dict: STATE_DICT_TYPE, + ) -> Dict[str, Any]: + """ + While loading the checkpoint, skip the weight loading for the keys + that contain any element of `self.keys_to_skip_loading` as a substring. + """ + if len(self.keys_to_skip_loading) == 0: + return state_dict + + new_state_dict = {} + for fqn, obj in state_dict.items(): + if any(skip_key in fqn for skip_key in self.keys_to_skip_loading): + log.warning(f"Skipping loading of key: {fqn}") + continue + new_state_dict[fqn] = obj + return new_state_dict + + +class CustomSavePlanner(dcp.DefaultSavePlanner): + """ + Custom save planner that enables an override for cache_plans_key when + caching of save plans is enabled. Caching of save plans reduces checkpointing + time by reusing the same save plan across checkpoints. This reduces the + checkpointing time by ~60% (benchmarked using the 235B-A22B Qwen3-VL model + on 64 GB200 nodes). + """ + + def __init__( + self, + flatten_state_dict: bool = True, + flatten_sharded_tensors: bool = True, + dedup_save_to_lowest_rank: bool = False, + save_reg_to_ema: bool = False, + enable_plan_caching: bool = False, + cache_plans_key: str | None = None, + ) -> None: + super().__init__( + flatten_state_dict=flatten_state_dict, + flatten_sharded_tensors=flatten_sharded_tensors, + dedup_save_to_lowest_rank=dedup_save_to_lowest_rank, + enable_plan_caching=enable_plan_caching, + ) + if cache_plans_key is not None: + self._cached_plans_key = cache_plans_key + + self.save_reg_to_ema = save_reg_to_ema + + def set_up_planner( + self, + state_dict: STATE_DICT_TYPE, + storage_meta: StorageMeta | None = None, + is_coordinator: bool = False, + ) -> None: + if self.save_reg_to_ema: + state_dict = _replace_keys_with_ema_keys(state_dict) + + super().set_up_planner( + state_dict=state_dict, + storage_meta=storage_meta, + is_coordinator=is_coordinator, + ) + + +class DistributedCheckpointer(AbstractCheckpointer): + CHECKPOINT_KEYS = ["model", "optim", "scheduler", "trainer", "dataloader"] + + def __init__( + self, + config_checkpoint: CheckpointConfig, + config_job: JobConfig, + callbacks: Optional[callback.CallBackGroup] = None, + disable_async: bool = False, + ): + super().__init__(config_checkpoint, config_job, callbacks) + self.config_checkpoint = config_checkpoint + if config_checkpoint.dcp_async_mode_enabled and not disable_async: + self.async_mode = AsyncMode.ASYNC_WITH_PINNED_MEM + else: + self.async_mode = AsyncMode.DISABLED + + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + ctx = get_context("spawn") + self.mp_queue_send = ctx.Queue() + self.mp_queue_recv = ctx.Queue() + self.mp = ctx.Process( + target=save_checkpoint_in_background, + args=( + self.mp_queue_send, + self.mp_queue_recv, + config_checkpoint, + config_job, + ), + daemon=True, + ) + self.mp.start() + self.cpu_offload_state_dict = None + self.staging_ckpt_file = None + self.staging_stream = torch.cuda.Stream() + self.checkpoint_in_progress = False + + def keys_to_resume_during_load(self) -> tuple[set[str], str | None, bool | None]: + """ + Determines the keys to resume from the checkpoint and the checkpoint path. + If the checkpoint is the latest checkpoint of the same model, then it is a + normal resume. If the checkpoint is a different model's checkpoint, then it is + a warm start. + + Args: + None + + Returns: + resume_keys: The keys to resume from the checkpoint. + checkpoint_path: The path to the checkpoint. If the checkpoint is a different + warm_start: Whether to warm start the training from a different model's checkpoint. + If the checkpoint is a different model's checkpoint, then this is True. + If the checkpoint is the latest checkpoint of the same model, then this is False. + """ + latest_checkpoint_file = self._read_latest_checkpoint_file() + + resume_keys = [] + warm_start = None + + if latest_checkpoint_file is not None: + # 1. Resume training from the latest checkpoint of the same model. + warm_start = False + checkpoint_path = os.path.join(self.load_dirname, latest_checkpoint_file) + resume_keys.extend(self.CHECKPOINT_KEYS) + + else: + if self.load_path and not str(self.load_path).endswith(".pt"): + # 2. Warm Start: Resume training from a different model's checkpoint + # specified by `load_path`. + warm_start = True + checkpoint_path = self.load_path + + if self.load_s3_backend_key: + checkpoint_path = f"s3://{self.config_checkpoint.load_from_object_store.bucket}/{checkpoint_path}" + + # If the path doesn't end with specific checkpoint, read the latest + # checkpoint file to determine the most recent checkpoint iteration. + if not re.search(r"/checkpoints/iter_\d{9}/?$", checkpoint_path): + old_ckpt_path = checkpoint_path + latest_ckpt_path = os.path.join(checkpoint_path, "checkpoints/latest_checkpoint.txt") + + # If the latest checkpoint file exists, use it to determine the + # checkpoint path. Otherwise, use the original path. + if easy_io.exists(latest_ckpt_path, backend_key=self.load_s3_backend_key): + checkpoint_file = easy_io.load( + latest_ckpt_path, backend_key=self.load_s3_backend_key + ).strip() + checkpoint_path = f"{checkpoint_path}/checkpoints/{checkpoint_file}" + else: + log.warning( + f"Latest checkpoint file {latest_ckpt_path} not found, load from {old_ckpt_path}" + ) + checkpoint_path = old_ckpt_path + + if self.load_training_state: + resume_keys.extend(self.CHECKPOINT_KEYS) + else: + resume_keys.append("model") + if self.only_load_scheduler_state: + resume_keys.append("scheduler") + else: + checkpoint_path = None + + if len(self.keys_not_to_resume) > 0: + for key in self.keys_not_to_resume: + assert key in self.CHECKPOINT_KEYS, f"Invalid key to resume: {key} not in {self.CHECKPOINT_KEYS}" + resume_keys = [key for key in resume_keys if key not in self.keys_not_to_resume] + + return set(resume_keys), checkpoint_path, warm_start + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + if self.callbacks is not None: + self.callbacks.on_load_checkpoint_start(model) + + resume_keys, checkpoint_path, warm_start = self.keys_to_resume_during_load() + resume_keys = sorted(resume_keys) + log.critical(f"Resuming ckpt {checkpoint_path} with keys: {resume_keys}") + + iteration = 0 + + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + + for key in resume_keys: + dist.barrier() + + cur_key_ckpt_full_path = os.path.join(checkpoint_path, key) + log.critical(f"Start loading checkpoint from {cur_key_ckpt_full_path}") + + storage_reader = self.get_storage_reader(cur_key_ckpt_full_path) + strict_resume = self.config_checkpoint.strict_resume + + # Note that we only allow skipping loading of keys during warm start. If the checkpoint is + # the latest checkpoint of the same model, then we don't need to skip any keys. + keys_to_skip_loading = self.config_checkpoint.keys_to_skip_loading if warm_start else [] + + load_planner = CustomLoadPlanner( + allow_partial_load=not strict_resume, + keys_to_skip_loading=keys_to_skip_loading, + ) + + if key == "model": + log.info("- Loading the model...") + _model_wrapper = ModelWrapper(model) + _state_dict = _model_wrapper.state_dict() + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + if self.config_checkpoint.load_ema_to_reg: + # The model has both net.* and net_ema.* submodules, so _state_dict + # contains both sets of keys after dcp.load(). Copy EMA weights into + # regular model weights so we can resume from EMA and reset EMA. + for sd_key in list(_state_dict.keys()): + if sd_key.startswith("net."): + key_ema = "net_ema." + sd_key.removeprefix("net.") + assert key_ema in _state_dict, ( + f"EMA key {key_ema} not found in state_dict. " + "Ensure the model has net_ema submodule." + ) + _state_dict[sd_key] = _state_dict[key_ema] + results = _model_wrapper.load_state_dict(_state_dict) + if results is not None: + if len(results.missing_keys) > 0: + raise ValueError(f"Missing keys (not found in checkpoint): {results.missing_keys}") + if len(results.unexpected_keys) > 0: + raise ValueError( + f"Unexpected keys (found in checkpoint but not in model): {results.unexpected_keys}" + ) + + elif key == "optim": + log.info("- Loading the optimizer...") + _state_dict = optimizer.state_dict() + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + optimizer.load_state_dict(_state_dict) + + elif key == "scheduler": + log.info("- Loading the scheduler...") + _state_dict = scheduler.state_dict() + # Older checkpoints pre-date _is_initial in PyTorch's LRScheduler. + # Strip it from the state dict so DCP doesn't look for a key that + # doesn't exist in the file; the scheduler reinitializes it to False. + ckpt_keys = set(storage_reader.read_metadata().state_dict_metadata.keys()) + if "_is_initial" not in ckpt_keys: + _state_dict.pop("_is_initial", None) + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + scheduler.load_state_dict(_state_dict) + + elif key == "trainer": + log.info("- Loading the trainer...") + + # Use rank-specific key for RNG state to support correct per-rank restoration + rng_key = f"rng_state_{dist.get_rank()}" + current_rng_state = get_rand_state_dict() + _state_dict = { + "grad_scaler": grad_scaler.state_dict(), + "iteration": iteration, + } + # Check if rng_key exists in checkpoint metadata to avoid failure with strict_resume=True + metadata = storage_reader.read_metadata() + rng_key_exists = any( + k.startswith(f"{rng_key}.") or k == rng_key for k in metadata.state_dict_metadata.keys() + ) + if rng_key_exists: + _state_dict[rng_key] = current_rng_state + + dcp.load( + _state_dict, + storage_reader=storage_reader, + planner=load_planner, + ) + grad_scaler.load_state_dict(_state_dict["grad_scaler"]) + iteration = _state_dict["iteration"] + set_rand_state_dict(_state_dict.get(rng_key, current_rng_state)) + + elif key == "dataloader": + if not easy_io.exists(cur_key_ckpt_full_path, backend_key=self.load_s3_backend_key): + log.info( + f"Checkpoint {cur_key_ckpt_full_path} does not exist, skip loading dataloader.", + rank0_only=False, + ) + continue + + rank = dist.get_rank() + dataloader_pkl_path = os.path.join(cur_key_ckpt_full_path, f"rank_{rank}.pkl") + if not easy_io.exists(dataloader_pkl_path, backend_key=self.load_s3_backend_key): + log.info(f"No dataloader checkpoint found at {dataloader_pkl_path}", rank0_only=False) + continue + + log.info(f"- Loading the dataloader {cur_key_ckpt_full_path}...", rank0_only=False) + _state_dict = easy_io.load( + dataloader_pkl_path, + file_format="pkl", + backend_key=self.load_s3_backend_key, + ) + dataloader_wrapper = _DataloaderWrapper(self.callbacks) + if dataloader_wrapper.has_state(): + dataloader_wrapper.load_state_dict(_state_dict) + + else: + raise ValueError(f"Invalid key: {key}. not support to resume.") + + if self.callbacks is not None and resume_keys: + # Note that this callback is never used in the codebase. + self.callbacks.on_load_checkpoint(model, state_dict={}) + log.info(f"Loaded checkpoint from {checkpoint_path} in iteration {iteration}") + + else: + log.info("Training from scratch.") + + torch.cuda.empty_cache() + + if self.callbacks is not None: + self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path) + return iteration + + def _checkpoint_async_with_pinned_memory( + self, checkpoint_file: str, state_dict: Dict[str, Tuple[Any, str]] + ) -> None: + assert self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM, "Async mode must be AsyncMode.ASYNC_WITH_PINNED_MEM" + + from torch.distributed._state_dict_utils import _copy_state_dict, _create_cpu_state_dict + + if self.cpu_offload_state_dict is None: + log.info(f"Preparing the CPU memory for staging") + self.cpu_offload_state_dict = _create_cpu_state_dict(state_dict, pin_memory=True, share_memory=True) + + log.info(f"Staging the state_dict in CPU memory") + with torch.cuda.stream(self.staging_stream): + self.cpu_offload_state_dict = _copy_state_dict( + state_dict, + self.cpu_offload_state_dict, + non_blocking=True, + ) + self.staging_ckpt_file = checkpoint_file + + self.staging_stream.synchronize() + log.info(f"Staging the state_dict in CPU memory completed") + + self.mp_queue_send.put_nowait((self.cpu_offload_state_dict, self.staging_ckpt_file)) + self.checkpoint_in_progress = True + log.info(f"Submitted checkpoint to background process") + + def _wait_for_previous_async_checkpoint(self) -> None: + """ + Gets the results of previously submitted checkpoints. + Pass them to callbacks if checkpoint succeeded. + """ + assert self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM, "Async mode must be AsyncMode.ASYNC_WITH_PINNED_MEM" + + if not self.checkpoint_in_progress: + return + + success = False + try: + log.info(f"Waiting for checkpoint save result") + + # Note that we set a timeout of 1 hour to avoid blocking the main process + # indefinitely. Gloo and NCCL timeouts are ~30 minutes, so this timeout + # should typically be sufficient. + save_done: SaveDone = self.mp_queue_recv.get(timeout=3600) + + log.info(f"Received checkpoint save result: {save_done}") + + if self.callbacks is not None and save_done.succeeded: + self.callbacks.on_save_checkpoint_success( + iteration=save_done.iteration, elapsed_time=save_done.elapsed_time + ) + self.checkpoint_in_progress = False + success = save_done.succeeded + + except Exception as e: + log.error(f"Error waiting for checkpoint save result: {e}") + + if not success: + # Terminate training execution upon a failed checkpoint save attempt. + # A failure at this stage typically indicates a non-recoverable system error. + # Continuing execution would result in subsequent persistent failures and + # unnecessary waste of GPU resources. + raise RuntimeError("Previous checkpoint save failed. Exiting...") + + def get_storage_writer(self, checkpoint_path: str) -> Union[S3StorageWriter, FileSystemWriter]: + if self.save_to_object_store: + return S3StorageWriter( + credential_path=self.config_checkpoint.save_to_object_store.credentials, + path=checkpoint_path, + enable_gcs_patch_in_boto3=self.config_checkpoint.enable_gcs_patch_in_boto3, + ) + return FileSystemWriter(path=checkpoint_path) + + def get_storage_reader(self, checkpoint_path: str) -> Union[S3StorageReader, FileSystemReader]: + if self.load_from_object_store: + return S3StorageReader( + credential_path=self.config_checkpoint.load_from_object_store.credentials, + path=checkpoint_path, + enable_gcs_patch_in_boto3=self.config_checkpoint.enable_gcs_patch_in_boto3, + ) + return FileSystemReader(checkpoint_path) + + def _save_as_pkl(self, obj: Any, output_dir: str) -> None: + """Save per-rank Python checkpoint state such as no-replace dataloader progress.""" + rank = dist.get_rank() + path = os.path.join(output_dir, f"rank_{rank}.pkl") + easy_io.dump( + obj, + path, + file_format="pkl", + backend_key=self.save_s3_backend_key, + ) + log.info(f"Saved state to {path}") + + def save_state_dict_worker(self, to_save_dict: Dict[str, Tuple[Any, str]], checkpoint_file: str) -> None: + for key, (v, full_checkpoint_path) in to_save_dict.items(): + if key == "dataloader": + self._save_as_pkl(v, full_checkpoint_path) + else: + storage_writer = self.get_storage_writer(full_checkpoint_path) + # Note that it is ok to create a new CustomSavePlanner object + # for each checkpoint save since the save plans are cached in a + # class dictionary. + save_planner = CustomSavePlanner( + dedup_save_to_lowest_rank=True, + enable_plan_caching=True, + cache_plans_key=f"custom_planner_{key}", + ) + dcp.save( + v, + storage_writer=storage_writer, + planner=save_planner, + ) + + if distributed.is_rank0(): + log.info(f"Saving last checkpoint file {checkpoint_file}") + self._write_latest_checkpoint_file(checkpoint_file) + + log.info(f"Saved checkpoint to {os.path.join(self.save_dirname, checkpoint_file)}") + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + self._wait_for_previous_async_checkpoint() + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_start(model, iteration) + + checkpoint_file = f"iter_{iteration:09}" + + # Use rank-specific key for RNG state to ensure each rank saves its own state + rng_key = f"rng_state_{dist.get_rank()}" + + to_save_dict = { + "model": ModelWrapper(model).state_dict(), + "optim": optimizer.state_dict(), + "scheduler": scheduler.state_dict(), + "trainer": { + "grad_scaler": grad_scaler.state_dict(), + "iteration": iteration, + rng_key: get_rand_state_dict(), + }, + } + dataloader_wrapper = _DataloaderWrapper(self.callbacks) + if dataloader_wrapper.has_state(): + to_save_dict["dataloader"] = dataloader_wrapper.state_dict() + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint(model, state_dict=to_save_dict) + + for k in to_save_dict.keys(): + output_dirname = os.path.join(self.save_dirname, f"iter_{iteration:09}/{k}") + to_save_dict[k] = (to_save_dict[k], output_dirname) + + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + dataloader_entry = to_save_dict.pop("dataloader", None) + if dataloader_entry is not None: + dataloader_state, dataloader_save_dir = dataloader_entry + self._save_as_pkl(dataloader_state, dataloader_save_dir) + self._checkpoint_async_with_pinned_memory(checkpoint_file, to_save_dict) + else: + start_time = time.monotonic() + self.save_state_dict_worker(to_save_dict, checkpoint_file) + elapsed_time = time.monotonic() - start_time + log.info(f"Checkpoint save completed: Time taken: {elapsed_time:.2f} seconds") + + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_success(iteration=iteration, elapsed_time=elapsed_time) + + # This measures exposed (synchronous) checkpoint time, on_save_checkpoint_success() + # is instead called to measure the entire duration for asynchronous checkpoint for the async case too. + if self.callbacks is not None: + self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration) + + def finalize(self) -> None: + super().finalize() + if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM: + if self.mp and self.mp.is_alive(): + # Wait for the previous checkpoint to complete. + self._wait_for_previous_async_checkpoint() + + self.mp_queue_send.put(Terminate()) + self.mp.join() diff --git a/cosmos_training/cosmos/checkpoint/dummy.py b/cosmos_training/cosmos/checkpoint/dummy.py new file mode 100644 index 00000000..e974faa1 --- /dev/null +++ b/cosmos_training/cosmos/checkpoint/dummy.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import torch +import torch.distributed + +from cosmos.checkpoint.base import AbstractCheckpointer +from cosmos.model._base import ImaginaireModel + + +class Checkpointer(AbstractCheckpointer): + """ + A dummy checkpointer that does not save or load anything. This is useful for debugging jobs or share workload with collobrators. + """ + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + pass + + def load( + self, + model: ImaginaireModel, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None, + grad_scaler: Optional[torch.amp.GradScaler] = None, + ) -> int: + return 0 diff --git a/cosmos_training/cosmos/checkpoint/s3_filesystem.py b/cosmos_training/cosmos/checkpoint/s3_filesystem.py new file mode 100644 index 00000000..5ba8ab4e --- /dev/null +++ b/cosmos_training/cosmos/checkpoint/s3_filesystem.py @@ -0,0 +1,330 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import time +from contextlib import contextmanager +from typing import Generator, Union +from urllib.parse import urlparse + +from botocore.exceptions import ClientError +from torch.distributed.checkpoint import FileSystemReader, FileSystemWriter +from torch.distributed.checkpoint.filesystem import FileSystemBase + +from cosmos.utils import log +from cosmos.utils.easy_io import easy_io + + +class S3Stream(io.BytesIO): + """ + Workaround for PyTorch manually closing the stream before we can upload it to S3. We override the close() as noop + and instead call our own _true_close() method to close the stream after we are done using it. + The commit at fault is https://github.com/pytorch/pytorch/commit/9c909bf3bb122db2cce95e2eb7459bbe50dfa15a + """ + + def close(self): + self.flush() + # No close + + def _true_close(self): + super().close() + + +class S3FileSystem(FileSystemBase): + """Implementation of FileSystemBase for AWS S3 storage.""" + + def __init__( + self, + credential_path: str, + max_attempts: int = 20, + initial_backoff: float = 1.0, + max_backoff: float = 30.0, + backoff_factor: float = 2.0, + enable_gcs_patch_in_boto3: bool = False, + ) -> None: + """ + Initialize S3FileSystem with retry configuration. + + Args: + credential_path: Path to AWS credentials JSON file + max_attempts: Maximum number of retry attempts + initial_backoff: Initial backoff time in seconds + max_backoff: Maximum backoff time in seconds + backoff_factor: Multiplicative factor for backoff time + enable_gcs_patch_in_boto3: Whether to enable GCS patch in boto3 + """ + self.easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": credential_path, + "path_mapping": None, + } + ) + self.max_attempts = max_attempts + self.initial_backoff = initial_backoff + self.max_backoff = max_backoff + self.backoff_factor = backoff_factor + self.enable_gcs_patch_in_boto3 = enable_gcs_patch_in_boto3 + if enable_gcs_patch_in_boto3: + log.info("enable_gcs_patch_in_boto3: True") + + def _retry_with_backoff(self, operation_func, *args, **kwargs): + """ + Execute an operation with exponential backoff retry logic. + + Args: + operation_func: Function to execute + *args: Positional arguments for the function + **kwargs: Keyword arguments for the function + + Returns: + Result of the operation function + + Raises: + Exception: If all retry attempts fail + """ + last_exception = None + backoff = self.initial_backoff + + for attempt in range(self.max_attempts): + try: + return operation_func(*args, **kwargs) + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "") + log.info(f"S3 Filesystem: Received ClientError: {error_code}", rank0_only=False) + + # Handle specific error cases + if error_code in ["SlowDown", "ThrottlingException", "RequestLimitExceeded", "InternalError"]: + last_exception = e + if attempt < self.max_attempts - 1: # Don't sleep on last attempt + current_backoff = min(backoff, self.max_backoff) + log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False) + time.sleep(current_backoff) + backoff *= self.backoff_factor + continue + # For other client errors, raise immediately + raise + except Exception as e: + log.info(f"S3 Filesystem: Received Exception: {str(e)}", rank0_only=False) + last_exception = e + if attempt < self.max_attempts - 1: + current_backoff = min(backoff, self.max_backoff) + log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False) + time.sleep(current_backoff) + backoff *= self.backoff_factor + continue + + # pyrefly: ignore [bad-raise] + raise last_exception + + @contextmanager + def create_stream(self, path: Union[str, os.PathLike], mode: str) -> Generator[io.IOBase, None, None]: + """Create a stream for reading from or writing to S3 with retry logic.""" + path_str = str(path) + bucket, key = self._parse_s3_uri(path_str) + log.info(f"S3 Filesystem: Creating stream for {key} in bucket {bucket}", rank0_only=False) + + if mode == "rb": + stream = io.BytesIO() + try: + + def download_operation(): + stream.write(self.easy_io_backend.get(filepath=path_str)) + stream.seek(0) + + log.info(f"S3 Filesystem: Downloading {key} from bucket {bucket}", rank0_only=False) + self._retry_with_backoff(download_operation) + log.info("S3 Filesystem: Download complete", rank0_only=False) + yield stream + finally: + stream.close() + elif mode == "wb": + stream = S3Stream() + try: + yield stream + + def upload_operation(): + stream.seek(0) + self.easy_io_backend.put(obj=stream, filepath=path_str) + + log.info(f"S3 Filesystem: Uploading {key} to bucket {bucket}", rank0_only=False) + self._retry_with_backoff(upload_operation) + log.info("S3 Filesystem: Upload complete", rank0_only=False) + finally: + stream._true_close() + else: + raise ValueError(f"Unsupported mode: {mode}") + + def concat_path(self, path: Union[str, os.PathLike], suffix: str) -> Union[str, os.PathLike]: + """Concatenate S3 path with suffix.""" + path_str = str(path) + if path_str.endswith("/"): + return f"{path_str}{suffix}" + return f"{path_str}/{suffix}" + + def init_path(self, path: Union[str, os.PathLike]) -> Union[str, os.PathLike]: + """Initialize and validate S3 path.""" + path_str = str(path) + if not path_str.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {path_str}. Must start with 's3://'") + return path_str + + def rename(self, path: Union[str, os.PathLike], new_path: Union[str, os.PathLike]) -> None: + """Rename (move) an object in S3 with retry logic.""" + src_path = str(path) + dst_path = str(new_path) + + def copy_operation(): + self.easy_io_backend.copyfile(src=src_path, dst=dst_path) + + self._retry_with_backoff(copy_operation) + + def delete_operation(): + self.easy_io_backend.remove(filepath=src_path) + + self._retry_with_backoff(delete_operation) + + def mkdir(self, path: Union[str, os.PathLike]) -> None: + """ + Create a "directory" in S3. + + Note: S3 doesn't have real directories, but we can create an empty object + with a trailing slash to simulate a directory. + """ + # Creating same buckets from different ranks can cause rate limit issues in GCP. + # In object store, we don't need to create a directory. + pass + + def ls(self, path: Union[str, os.PathLike]) -> list[str]: + """List objects under the given S3 path (prefix) and return s3:// URIs.""" + path_str = str(path) + return [ + f"{path_str.removesuffix('/')}/{obj_suffix}" + for obj_suffix in self.easy_io_backend.list_dir_or_file(dir_path=path_str, list_dir=False, list_file=True) + ] + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + """Validate if the checkpoint_id is a valid S3 URI.""" + checkpoint_id_str = str(checkpoint_id) + try: + if not checkpoint_id_str.startswith("s3://"): + return False + parsed = urlparse(checkpoint_id_str) + return bool(parsed.netloc and parsed.path) # Must have bucket and key + except Exception: + return False + + def exists(self, path: Union[str, os.PathLike]) -> bool: + """Check if an object exists in S3 with retry logic.""" + try: + + def head_operation() -> bool: + return self.easy_io_backend.exists(filepath=str(path)) + + return self._retry_with_backoff(head_operation) + except ClientError as e: + if e.response.get("Error", {}).get("Code", "") == "404": + return False + raise + + def rm_file(self, path: Union[str, os.PathLike]) -> None: + """Remove a file from S3 with retry logic.""" + + def delete_operation(): + self.easy_io_backend.remove(filepath=str(path)) + + self._retry_with_backoff(delete_operation) + + def _parse_s3_uri(self, uri: str) -> tuple[str, str]: + """ + Parse an S3 URI into bucket and key. + + Args: + uri: S3 URI in the format s3://bucket-name/key + + Returns: + Tuple of (bucket_name, key) + + Raises: + ValueError: If the URI is invalid + """ + uri = uri if isinstance(uri, str) else str(uri) + if not uri.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {uri}. Must start with 's3://'") + + parsed = urlparse(uri) + bucket = parsed.netloc + + # Remove leading slash from key + key = parsed.path.lstrip("/") + + if not bucket: + raise ValueError(f"Invalid S3 URI: {uri}. No bucket specified") + + return bucket, key + + +class S3StorageWriter(FileSystemWriter): + def __init__( + self, + credential_path: str, + path: str, + enable_gcs_patch_in_boto3: bool = False, + **kwargs, + ) -> None: + """ + Initialize an S3 writer for distributed checkpointing. + + Args: + region (str): The AWS region for S3. + path (str): The S3 URI to write checkpoints to. + kwargs (dict): Keyword arguments to pass to the parent :class:`FileSystemWriter`. + enable_gcs_patch_in_boto3 (bool): Whether to enable GCS patch in boto3 + """ + super().__init__( + path=path, + sync_files=False, + **kwargs, + ) + self.fs = S3FileSystem(credential_path, enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3) # type: ignore + self.path = self.fs.init_path(path) + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + return S3FileSystem.validate_checkpoint_id(checkpoint_id) + + +class S3StorageReader(FileSystemReader): + def __init__( + self, credential_path: str, path: Union[str, os.PathLike], enable_gcs_patch_in_boto3: bool = False + ) -> None: + """ + Initialize an S3 reader for distributed checkpointing. + + Args: + region (str): The AWS region for S3. + path (Union[str, os.PathLike]): The S3 path to read checkpoints from. + enable_gcs_patch_in_boto3 (bool): Whether to enable GCS patch in boto3 + """ + super().__init__(path) + self.fs = S3FileSystem(credential_path, enable_gcs_patch_in_boto3=enable_gcs_patch_in_boto3) # type: ignore + self.path = self.fs.init_path(path) + self.sync_files = False + + @classmethod + def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool: + return S3FileSystem.validate_checkpoint_id(checkpoint_id) diff --git a/cosmos_training/cosmos/communicator/__init__.py b/cosmos_training/cosmos/communicator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/data/__init__.py b/cosmos_training/cosmos/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/data/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/data/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d5a4bf0a421d066305ab16421c18dc25a8604dd GIT binary patch literal 230 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

=TFlUShbJqTrkK$MBX&JDnR^ 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 z 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 ZI#kDJObY%;bd;}le02MH8O%7&{taZgqYnT8 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/action/__init__.py b/cosmos_training/cosmos/data/vfm/action/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/data/vfm/action/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/action/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec18e59a09882ae4964aa02647a5516df283e3dd GIT binary patch literal 241 zcmZ8bK@NgI3}i(TW5Pqc*bf+=;1@P4ECd2u(iRQB@a7wQi_h={22Y&aO~8XqrkzeY zlRndQ8~Kt?Qiu5p?Z3vgSMJde?bPWJ(aLXQxS4Ly^ekkd;45@UQeaadLx$Y?(Gk`* zlV#Uu!&VEK%2lp}o-Hv9#+3mwn>2RTG4_<%Mfo}A*xZPLOVNVKq(TQ;`#{lp-wo5| W&|=;<_DG3;PDv7NR29#0*abkxUAMYW3yoIIJ0A@ z5tX8<2aZUo6jxCtj;Znw^nd6DL?qBEhe$d3wkn*W9{R=~fYOe%zxR6|Gw#8X7Z zx5295>0gxfVDpRIo!zgCv}W3lzFgEdNVQIwtCuO$Ek<28PiiHK?HX~oZd%-?j*d-k zx`ey4Uc{kB*Vq=X>IKa0wj@Z@GHZmHMxDB(9T1kthUwt0E(=|)H{1>07A^9ERl7tO zZL30&8#TKPokZSniP^Y949e_^QH6jKBX)p zH%je@h)I1GlgACS+KvfV^wQN}?YKx1H7hn`+@Y+B+X-PwT|3b-D$K4!ue`DUvl`?_ z?uDQdi$jK#*gd+m@@TENyt1_V)OfmP6xUW3msgGDqL;XK0B_cb7i5Gtm}7}n_Lrz% zH17OQfLCY_ZC>~GqTcr8rilD(^?g4{dlK(YCfb(ReJ*<1Sy*GcmCs9(09|b`r@`}SIttDT-)d_=((V$R_P9W9M$FUnb;(x_ri}!bBQs^yr4@N zC)iWANYl*)SVp*Ip@>=4^-sSRstwpl3T1bvP>0|$t=iNrSk$e7s581rEZ*0R65I!a zGq^*xQzG8){jy0i7B z7q1X*^o%xUIjA)YkBb3bpbuL5RLdM|nGd=RPQtvP>4@FY@G2T%NA!Ba>bIe6~h z5PSrw!v~B0sNiErooGGs(}ItSykGefg5T`&TY^t^V={v4NX;Au{3*e2BXwY(`P0CU k&}b{wvJU548^2ASqofq@?}eqihx0xH@n-%)h)#F@1;s|pW&i*H literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/action/action_normalization.py b/cosmos_training/cosmos/data/vfm/action/action_normalization.py new file mode 100644 index 00000000..e1c25148 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/action_normalization.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Action normalization helpers.""" + +import json +from pathlib import Path + +import numpy as np +import torch + +from cosmos.utils import log + + +def load_action_stats(stats_path: str, stats_key: str = "global") -> dict[str, np.ndarray]: + """Load pre-computed action normalization stats from a JSON file.""" + path = Path(stats_path) + if not path.exists(): + raise FileNotFoundError(f"Action normalization stats not found at {stats_path}.") + log.info(f"Loading action normalization stats from {stats_path}") + with path.open("r") as f: + raw = json.load(f) + if stats_key in raw: + raw = raw[stats_key] + if not isinstance(raw, dict): + raise TypeError(f"Action normalization stats block {stats_key!r} in {stats_path} must be a dict.") + elif stats_key != "global": + raise KeyError(f"Action normalization stats block {stats_key!r} not found in {stats_path}.") + stat_keys = {"mean", "std", "min", "max", "q01", "q99"} + return {k: np.array(v, dtype=np.float32) for k, v in raw.items() if k in stat_keys} + + +def normalize_action( + action: torch.Tensor, + method: str, + stats: dict[str, torch.Tensor], +) -> torch.Tensor: + """Normalize action tensor (all dimensions including gripper).""" + if method == "quantile": + q01, q99 = stats["q01"], stats["q99"] + denom = (q99 - q01).clamp(min=1e-8) + return (2.0 * (action - q01) / denom - 1.0).clamp(-1.0, 1.0) + if method == "meanstd": + return (action - stats["mean"]) / stats["std"].clamp(min=1e-8) + if method == "minmax": + lo, hi = stats["min"], stats["max"] + denom = (hi - lo).clamp(min=1e-8) + return (2.0 * (action - lo) / denom - 1.0).clamp(-1.0, 1.0) + raise ValueError(f"Unknown normalization method: {method!r}") diff --git a/cosmos_training/cosmos/data/vfm/action/action_normalization_test.py b/cosmos_training/cosmos/data/vfm/action/action_normalization_test.py new file mode 100644 index 00000000..a595f91a --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/action_normalization_test.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +import torch + +from cosmos.data.vfm.action.action_normalization import load_action_stats +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + BaseActionLeRobotDataset, +) + + +class _StatsOnlyDataset(BaseActionLeRobotDataset): + """Minimal shell for testing BaseActionLeRobotDataset normalization helpers.""" + + def __init__(self, stats_path: Path, action_normalization: ActionNormalization | None) -> None: + self._stats_path = stats_path + self._action_normalization = action_normalization + self._norm_stats: dict[str, torch.Tensor] | None = None + + def _normalizer_path(self) -> Path: + return self._stats_path + + +def _write_stats(tmp_path: Path) -> Path: + stats_path = tmp_path / "stats.json" + payload = { + "global": { + "mean": [0.0, 0.0], + "std": [1.0, 1.0], + "min": [0.0, -1.0], + "max": [10.0, 1.0], + "q01": [0.0, -1.0], + "q99": [10.0, 1.0], + }, + "global_raw": { + "mean": [0.0, 0.0], + "std": [1.0, 1.0], + "min": [0.0, 0.0], + "max": [10.0, 100.0], + "q01": [0.0, 0.0], + "q99": [10.0, 100.0], + }, + } + stats_path.write_text(json.dumps(payload)) + return stats_path + + +@pytest.mark.L0 +def test_load_action_stats_selects_global_raw_block(tmp_path: Path) -> None: + stats_path = _write_stats(tmp_path) + + stats = load_action_stats(str(stats_path), stats_key="global_raw") + + assert stats["q99"].tolist() == [10.0, 100.0] + + +@pytest.mark.L0 +def test_quantile_rot_uses_global_raw_stats(tmp_path: Path) -> None: + stats_path = _write_stats(tmp_path) + action = torch.tensor([[5.0, 50.0]], dtype=torch.float32) # [T,D] + + quantile_dataset = _StatsOnlyDataset(stats_path, "quantile") + quantile_rot_dataset = _StatsOnlyDataset(stats_path, "quantile_rot") + + quantile_action = quantile_dataset._normalize_action(action) # [T,D] + quantile_rot_action = quantile_rot_dataset._normalize_action(action) # [T,D] + expected_quantile = torch.tensor([[0.0, 1.0]], dtype=torch.float32) # [T,D] + expected_quantile_rot = torch.tensor([[0.0, 0.0]], dtype=torch.float32) # [T,D] + + torch.testing.assert_close(quantile_action, expected_quantile) + torch.testing.assert_close(quantile_rot_action, expected_quantile_rot) diff --git a/cosmos_training/cosmos/data/vfm/action/action_spec.py b/cosmos_training/cosmos/data/vfm/action/action_spec.py new file mode 100644 index 00000000..f845a54f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/action_spec.py @@ -0,0 +1,247 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Action-vector specification: per-dim type label + idle thresholds. + +Single concept: every column of an action vector has a :class:`DimType` label. +Idle detection iterates by type and applies the matching algorithm: + + POS → ‖action[pos_idx]‖ per arm < eps_t + ROT → distance(rot, identity) per group < eps_r + GRIPPER → max |Δgripper| < eps_g (frame 0 idle by convention) + JOINT → max |Δjoint| < joint_threshold (frame 0 idle) + RESERVED → ignored + +An :class:`ActionSpec` is just ``names`` + ``types`` + ``rotation_format``. +Build one declaratively via :func:`build_action_spec` from DSL components:: + + build_action_spec(Pos(), Rot("rot6d"), Gripper()) # 10D single arm + build_action_spec(Pos(), Rot("rot6d")) # 9D no gripper + build_action_spec(Joint(n=14, label="arm"), # 30D joint-space + Joint(n=14, label="end"), + Joint(n=2, label="gripper")) + build_action_spec(Pos(prefix="left"), Rot("rot6d", "left"), Gripper(prefix="left"), + Pos(prefix="right"), Rot("rot6d", "right"), Gripper(prefix="right")) + +Naming convention: + Default ``pos_x``, ``rot_0``, ``gripper``, ``arm_0`` ... + With ``prefix="left"`` (idempotent on trailing ``_``): ``left_pos_x`` ... +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import ClassVar + +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + _identity_rotation_vector, +) + + +class DimType(str, Enum): + """Per-column action-dim category (drives idle detection).""" + + POS = "pos" + ROT = "rot" + GRIPPER = "gripper" + JOINT = "joint" + RESERVED = "reserved" + + +@dataclass(frozen=True, slots=True) +class ActionSpec: + """Structural description of an action vector: names + per-dim types. + + All ROT dims share a single ``rotation_format``; mixed formats in one spec + are not supported (raise at build time). + + This struct contains no detection thresholds — those are passed at call + time to :func:`compute_idle_frames` so each dataset can tune them + independently of layout. + """ + + names: list[str] + types: list[DimType] + rotation_format: RotationConvention = "rot6d" + + @property + def dim(self) -> int: + return len(self.names) + + +# --------------------------------------------------------------------------- +# DSL components +# --------------------------------------------------------------------------- + + +def _join_prefix(prefix: str, name: str) -> str: + """Join ``prefix`` and ``name`` with a single ``_``; idempotent on trailing ``_``.""" + return name if not prefix else f"{prefix.rstrip('_')}_{name}" + + +@dataclass(frozen=True) +class Pos: + """Translation block. + + Default 3D (``pos_x``, ``pos_y``, ``pos_z``). For planar tasks (e.g. PushT) + use ``Pos(dim=2)`` → ``pos_x``, ``pos_y``. ``dim >= 4`` falls back to + indexed names ``pos_0``, ``pos_1``, ... + """ + + dim: int = 3 + prefix: str = "" + type: ClassVar[DimType] = DimType.POS + + def names(self) -> list[str]: + if self.dim <= 3: + return [_join_prefix(self.prefix, f"pos_{c}") for c in "xyz"[: self.dim]] + return [_join_prefix(self.prefix, f"pos_{i}") for i in range(self.dim)] + + +@dataclass(frozen=True) +class Rot: + """Rotation block; ``format`` selects the encoding. + + Supported formats and per-dim names: + + - ``rot6d`` → 6 dims, ``rot_0`` ... ``rot_5`` (identity ``[1,0,0,0,1,0]``) + - ``rot9d`` → 9 dims, ``rot_0`` ... ``rot_8`` (identity ``[1,0,0,0,1,0,0,0,1]``) + - ``euler_xyz`` → 3 dims, ``roll``, ``pitch``, ``yaw`` (identity ``[0,0,0]``) + - ``axisangle`` → 3 dims, ``axang_x/y/z`` (identity ``[0,0,0]``) + - ``quat_xyzw`` / ``quat_wxyz`` → 4 dims, ``quat_x/y/z/w`` in declared order + """ + + format: RotationConvention = "rot6d" + prefix: str = "" + type: ClassVar[DimType] = DimType.ROT + + @property + def rotation_format(self) -> RotationConvention: + return self.format + + @property + def dim(self) -> int: + return _identity_rotation_vector(self.format).shape[0] + + def names(self) -> list[str]: + if self.format == "euler_xyz": + return [_join_prefix(self.prefix, c) for c in ("roll", "pitch", "yaw")] + if self.format == "axisangle": + return [_join_prefix(self.prefix, f"axang_{c}") for c in "xyz"] + if self.format.startswith("quat_"): + order = self.format.split("_", 1)[1] # "xyzw" or "wxyz" + return [_join_prefix(self.prefix, f"quat_{c}") for c in order] + return [_join_prefix(self.prefix, f"rot_{i}") for i in range(self.dim)] + + +@dataclass(frozen=True) +class Gripper: + """1D gripper command (binary 0/1 or continuous). Detected by frame-diff.""" + + prefix: str = "" + type: ClassVar[DimType] = DimType.GRIPPER + + @property + def dim(self) -> int: + return 1 + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, "gripper")] + + +@dataclass(frozen=True) +class Joint: + """``n`` joint commands. Detected by frame-diff against ``joint_threshold``.""" + + n: int = 0 + label: str = "joint" + prefix: str = "" + type: ClassVar[DimType] = DimType.JOINT + + @property + def dim(self) -> int: + return self.n + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, f"{self.label}_{i}") for i in range(self.n)] + + +@dataclass(frozen=True) +class Reserved: + """``n`` dims counted in ``action_dim`` but ignored by idle detection.""" + + n: int = 0 + label: str = "reserved" + prefix: str = "" + type: ClassVar[DimType] = DimType.RESERVED + + @property + def dim(self) -> int: + return self.n + + def names(self) -> list[str]: + return [_join_prefix(self.prefix, f"{self.label}_{i}") for i in range(self.n)] + + +# --------------------------------------------------------------------------- +# Builder +# --------------------------------------------------------------------------- + + +# Type alias for any DSL component. Not a runtime check — only annotation hint. +Component = Pos | Rot | Gripper | Joint | Reserved + + +def build_action_spec(*components: Component) -> ActionSpec: + """Compose ``components`` into an :class:`ActionSpec`. + + Each component contributes its ``names()`` and replicates its ``type`` for + every column it occupies. The first ROT component's ``rotation_format`` + is captured for the whole spec; mixing formats raises ``ValueError``. + """ + names: list[str] = [] + types: list[DimType] = [] + rotation_format: RotationConvention | None = None + + for c in components: + names.extend(c.names()) + types.extend([c.type] * c.dim) + if c.type == DimType.ROT: + fmt = c.rotation_format # type: ignore[union-attr] + if rotation_format is None: + rotation_format = fmt + elif rotation_format != fmt: + raise ValueError(f"Mixed rotation_format in one ActionSpec: {rotation_format!r} vs {fmt!r}") + + return ActionSpec( + names=names, + types=types, + rotation_format=rotation_format or "rot6d", + ) + + +__all__ = [ + "ActionSpec", + "Component", + "DimType", + "Gripper", + "Joint", + "Pos", + "Reserved", + "Rot", + "build_action_spec", +] diff --git a/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset.py b/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset.py new file mode 100644 index 00000000..aa3dee03 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset.py @@ -0,0 +1,366 @@ +"""AgiBotWorld-Beta dataset with FK-pose actions and multi-view support. + +Uses the same calibrated G1/omnipicker URDF forward kinematics as Embodiment C to produce +relative SE(3) delta actions for head camera and left/right gripper-base wrists, +concatenated with gripper open-fraction values. + +Action layout (29 dims, same as GEAR gripper): + ``[head_cam_delta(9), right_wrist_delta(9), right_gripper(1), + left_wrist_delta(9), left_gripper(1)]`` + +View modes: + - **ego_view**: single ``observation.images.head`` camera. + - **concat_view**: head view on top, left/right wrist views resized and + concatenated horizontally on the bottom. +""" + +from __future__ import annotations + +from typing import Any, Literal, cast + +import numpy as np +import torch +import torch.nn.functional as F + +from cosmos.data.vfm.action.embodiment_c_fk import ( + AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST, + apply_agibot_gripper_to_opencv, + apply_robot_base_motion_to_poses, + compute_fk_transforms_batch, + compute_link_poses_batch, + convert_gripper_state_to_open_fraction, + extract_fk_transforms_from_link_poses, +) +from cosmos.data.vfm.action.embodiment_c_spec import AGIBOT_GEAR_GRIPPER_NORMALIZER_EMBODIMENT_TYPE +from cosmos.data.vfm.action.agibotworld_beta_dataset_config import LEROBOT_ROOTS +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.pose_utils import PoseConvention, pose_abs_to_rel +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +# Video keys. +_HEAD_KEY = "observation.images.head" +_HAND_LEFT_KEY = "observation.images.hand_left" +_HAND_RIGHT_KEY = "observation.images.hand_right" + +# State observation keys needed for FK. +_ROBOT_POSITION_KEY = "observation.states.robot.position" +_ROBOT_ORIENTATION_KEY = "observation.states.robot.orientation" +_STATE_KEYS = [ + "observation.states.effector.position", + "observation.states.joint.position", + "observation.states.head.position", + "observation.states.waist.position", + _ROBOT_POSITION_KEY, + _ROBOT_ORIENTATION_KEY, +] + +_BASE_ROOT = ( + "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/" + "cosmos3_action_datasets/AgiBotWorld-Beta_20260102/agibotworld" +) + + +def _resolve_root_paths(root: str | list[str] | tuple[str, ...] | None) -> list[str]: + """Normalize a root argument into concrete AgiBotWorld-Beta LeRobot roots.""" + + if root is None: + return [f"{_BASE_ROOT}/{subpath}" for subpath in LEROBOT_ROOTS] + if not isinstance(root, str): + return [str(path) for path in root] + return [f"{root}/{subpath}" for subpath in LEROBOT_ROOTS] + + +def _split_task_for_caption(task: str) -> tuple[str, str]: + """Split AgiBotWorld task text into train caption and debug-only detail.""" + + ai_caption, separator, debug_caption = task.partition("|") + if not separator: + return task.strip(), "" + return ai_caption.strip(), debug_caption.strip() + + +def _assemble_embodiment_c_state( + effector_pos: np.ndarray, + joint_pos: np.ndarray, + head_pos: np.ndarray, + waist_pos: np.ndarray, +) -> np.ndarray: + """Assemble a GEAR-compatible flat state vector from Beta's decomposed fields. + + Args: + effector_pos: ``(N, 2)`` gripper positions as ``[left_gripper, right_gripper]``. + joint_pos: ``(N, 14)`` arm joint positions (7 left + 7 right). + head_pos: ``(N, 2)`` head joints as ``[head_yaw, head_pitch]``. + waist_pos: ``(N, 2)`` waist joints as ``[body_pitch, lift_body]``. + + Returns: + State array of shape ``(N, 20)`` with GEAR gripper layout: + ``[arm_joints(14), gripper(2), head_yaw, head_pitch, waist_pitch, waist_lift]`` + + Note: + Beta ``waist_pos = [body_pitch, lift_body]`` maps directly into the + standard GEAR body/head block: + ``state[16:20] = [head_yaw, head_pitch, body_pitch, lift_body]``. + """ + # Body/head block: [head_yaw, head_pitch, waist_pitch, waist_lift] + body_head = np.stack( + [head_pos[:, 0], head_pos[:, 1], waist_pos[:, 0], waist_pos[:, 1]], + axis=-1, + ) # [N,4] + + return np.concatenate([joint_pos, effector_pos, body_head], axis=-1).astype(np.float32) # [N,20] + + +class AgiBotWorldBetaDataset(BaseActionLeRobotDataset): + """AgiBotWorld-Beta dataset with FK-pose actions matching GEAR gripper format. + + Concat view layout: + ┌──────────────────┐ + │ head │ (H, W) + ├─────────┬────────┤ + │ hand_L │ hand_R │ (H/2, W/2) each + └─────────┴────────┘ + """ + + def __init__( + self, + root: str | list[str] | tuple[str, ...] | None = None, + fps: float = 10.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + action_normalization: ActionNormalization | None = None, + mode: str = "joint", + viewpoint: Viewpoint = "concat_view", + pose_convention: PoseConvention = "backward_framewise", + rotation_format: Literal["rot6d"] = "rot6d", + tolerance_s: float = 3e-4, + max_loaded_datasets: int = 32, + skip_video_loading: bool = False, + sample_stride: int = 1, + enable_fast_init: bool = False, + fast_init_max_workers: int = 64, + return_agibot_link_poses: bool = False, + ) -> None: + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type="agibotworld", + viewpoint=viewpoint, + action_normalization=action_normalization, + pose_convention=pose_convention, + rotation_format=rotation_format, + tolerance_s=tolerance_s, + max_loaded_datasets=max_loaded_datasets, + skip_video_loading=skip_video_loading, + sample_stride=sample_stride, + enable_fast_init=enable_fast_init, + fast_init_max_workers=fast_init_max_workers, + ) + + self._is_concat_view = viewpoint == "concat_view" + self._to_opencv: dict[str, np.ndarray] = AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST + self._return_agibot_link_poses: bool = return_agibot_link_poses + + self._all_shard_roots = _resolve_root_paths(root) + + # T+1 frames for observations (states + video). Source action columns + # are intentionally not requested; emitted actions are state deltas. + frame_ts_obs = [i * self._dt for i in range(self._chunk_length + 1)] + + self._delta_timestamps = { + _HEAD_KEY: frame_ts_obs, + } + # State keys for FK (T+1 frames). + for key in _STATE_KEYS: + self._delta_timestamps[key] = frame_ts_obs + # Multi-view cameras. + if self._is_concat_view: + self._delta_timestamps[_HAND_LEFT_KEY] = frame_ts_obs + self._delta_timestamps[_HAND_RIGHT_KEY] = frame_ts_obs + + def _normalizer_filename(self) -> str: + """Use the shared Embodiment C gripper FK-pose stats for Beta data.""" + + return f"{AGIBOT_GEAR_GRIPPER_NORMALIZER_EMBODIMENT_TYPE}_{self._pose_convention}_{self._rotation_format}.json" + + # -- FK-based action construction ---------------------------------------- + + def _build_fk_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, dict[str, Any]]: + """Build relative FK-pose action plus absolute initial poses. + + Assembles GEAR-compatible flat state from Beta's decomposed observation + fields, runs calibrated G1/omnipicker URDF forward kinematics, and converts to framewise + relative SE(3) deltas with rot6d rotation blocks. + + Returns: + ``(action, extras)`` where ``action`` is ``(T, 29)`` and ``extras`` + carries absolute initial poses for reconstruction. + + Beta ``observation.states.effector.position`` stores left/right scalar + gripper positions. Before these are concatenated into the + GEAR-compatible 29D action, they are normalized to the shared + viewer/action convention: + ``0.0`` means closed and ``1.0`` means open. AgiBot actuator-close + state values use ``0=open`` and ``120=closed``; angle-valued gripper + states are scaled by ``convert_gripper_state_to_open_fraction``. + """ + + # Assemble GEAR-compatible flat state from decomposed Beta fields. + effector_pos = sample["observation.states.effector.position"].detach().cpu().numpy() # [T+1,2] + joint_pos = sample["observation.states.joint.position"].detach().cpu().numpy() # [T+1,14] + head_pos = sample["observation.states.head.position"].detach().cpu().numpy() # [T+1,2] + waist_pos = sample["observation.states.waist.position"].detach().cpu().numpy() # [T+1,2] + robot_pos = sample[_ROBOT_POSITION_KEY].detach().cpu().numpy() # [T+1,3] + robot_quat = sample[_ROBOT_ORIENTATION_KEY].detach().cpu().numpy() # [T+1,4] + num_state_steps = min( + effector_pos.shape[0], + joint_pos.shape[0], + head_pos.shape[0], + waist_pos.shape[0], + robot_pos.shape[0], + robot_quat.shape[0], + ) + if num_state_steps < 2: + raise ValueError(f"{self.__class__.__name__}: state observations must contain at least 2 frames.") + effector_pos = effector_pos[:num_state_steps].astype(np.float32, copy=False) # [T+1,2] + joint_pos = joint_pos[:num_state_steps].astype(np.float32, copy=False) # [T+1,14] + head_pos = head_pos[:num_state_steps].astype(np.float32, copy=False) # [T+1,2] + waist_pos = waist_pos[:num_state_steps].astype(np.float32, copy=False) # [T+1,2] + robot_pos = robot_pos[:num_state_steps].astype(np.float32, copy=False) # [T+1,3] + robot_quat = robot_quat[:num_state_steps].astype(np.float32, copy=False) # [T+1,4] + states_np = _assemble_embodiment_c_state(effector_pos, joint_pos, head_pos, waist_pos) # [T+1,20] + + # Forward kinematics → absolute 4×4 transforms; gripper rotations are + # first lifted by observed mobile-base motion, then converted through + # _to_opencv for action/viewer display. + link_poses = None + if self._return_agibot_link_poses: + link_poses = compute_link_poses_batch(states_np, "embodiment_c_gripper") # {name:[T+1,4,4]} + link_poses = apply_robot_base_motion_to_poses(link_poses, robot_pos, robot_quat) # {name:[T+1,4,4]} + native_fk = extract_fk_transforms_from_link_poses(link_poses) # {name:[T+1,4,4]} + else: + native_fk = compute_fk_transforms_batch(states_np, "embodiment_c_gripper") # {name:[T+1,4,4]} + native_fk = apply_robot_base_motion_to_poses(native_fk, robot_pos, robot_quat) # {name:[T+1,4,4]} + fk = apply_agibot_gripper_to_opencv(native_fk, self._to_opencv) # {name:[T+1,4,4]} + + # Relative SE(3) deltas. + pose_convention = cast(PoseConvention, self._pose_convention) + head_rel = pose_abs_to_rel(fk["head_camera"], rotation_format="rot6d", pose_convention=pose_convention) # [T,9] + right_rel = pose_abs_to_rel( + fk["right_wrist"], rotation_format="rot6d", pose_convention=pose_convention + ) # [T,9] + left_rel = pose_abs_to_rel(fk["left_wrist"], rotation_format="rot6d", pose_convention=pose_convention) # [T,9] + + # Gripper open fractions: Beta observed effector[0]=left, + # effector[1]=right. + # The converter standardizes URDF-angle and actuator-close-degree + # encodings to 0.0=closed, 1.0=open for viewer consistency. + right_gripper = convert_gripper_state_to_open_fraction(effector_pos[1:, 1:2]) # [T,1] + left_gripper = convert_gripper_state_to_open_fraction(effector_pos[1:, 0:1]) # [T,1] + + # Concatenate in GEAR action order. + action_np = np.concatenate( + [head_rel, right_rel, right_gripper, left_rel, left_gripper], + axis=-1, + ).astype(np.float32) # [T,29] + + extras = { + "initial_pose": torch.from_numpy(fk["head_camera"][0].copy()).float(), # [4,4] + "initial_pose_right": torch.from_numpy(fk["right_wrist"][0].copy()).float(), # [4,4] + "initial_pose_left": torch.from_numpy(fk["left_wrist"][0].copy()).float(), # [4,4] + } + if link_poses is not None: + extras["agibot_link_poses"] = { + link_name: torch.from_numpy(poses.copy()).float() for link_name, poses in link_poses.items() + } # {name:[T+1,4,4]} + return torch.from_numpy(action_np).float(), extras # [T,29] + + # -- Multi-view composition ---------------------------------------------- + + def _compose_multi_view(self, sample: dict[str, Any]) -> torch.Tensor: + """Compose head, left-hand, and right-hand views into a single frame. + + Returns: + Composited video tensor in raw LeRobot ``(T, C, H_out, W)`` float format. + """ + top = sample[_HEAD_KEY] # [T,C,H,W] + left = sample[_HAND_LEFT_KEY] # [T,C,H_l,W_l] + right = sample[_HAND_RIGHT_KEY] # [T,C,H_r,W_r] + + _, _, h_top, w_top = top.shape + half_h, half_w = h_top // 2, w_top // 2 + + left = F.interpolate(left, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + right = F.interpolate(right, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + bottom = torch.cat([left, right], dim=-1) # [T,C,H/2,W] + + composite = torch.cat([top, bottom], dim=-2) # [T,C,3H/2,W] + return composite # [T,C,3H/2,W] + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode, _, _, sample = self._fetch_sample(idx) + + ai_caption, debug_caption = _split_task_for_caption(sample["task"]) + + if self._skip_video_loading: + video = None + elif self._is_concat_view: + video = self._compose_multi_view(sample) + else: + video = sample[_HEAD_KEY] # [T,C,H,W] + + # Action: FK-pose based (same layout as GEAR gripper). + action, action_extras = self._build_fk_action(sample) + + extras: dict[str, Any] = {**action_extras} + if self._is_concat_view: + extras["additional_view_description"] = ( + "The top row shows the head-mounted camera view looking down at the workspace. " + "The bottom row contains two horizontally concatenated wrist-mounted camera views: " + "the left hand camera on the left and the right hand camera on the right." + ) + if debug_caption: + extras["debug_caption"] = debug_caption + + return self._build_result(mode=mode, video=video, action=action, ai_caption=ai_caption, **extras) + + def _build_action_spec(self) -> ActionSpec: + """AgiBotWorld-Beta bimanual layout (29D, same as GEAR gripper). + + ``[head_pos+rot6d (9) | right_pos+rot6d (9) | right_gripper (1) + | left_pos+rot6d (9) | left_gripper (1)]`` + + All three SE(3) blocks (head camera + both wrists) participate in + idle-frame detection: a chunk only counts as idle if the head is + steady AND both arms are at rest AND both grippers are unchanged. + Override this method (or use ``Reserved`` for head dims) if you want + head motion to be ignored by idle detection. + """ + return build_action_spec( + Pos(prefix="head"), + Rot("rot6d", prefix="head"), + Pos(prefix="right"), + Rot("rot6d", prefix="right"), + Gripper(prefix="right"), + Pos(prefix="left"), + Rot("rot6d", prefix="left"), + Gripper(prefix="left"), + ) + + @property + def action_dim(self) -> int: + return 29 diff --git a/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset_config.py new file mode 100644 index 00000000..7b736730 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/agibotworld_beta_dataset_config.py @@ -0,0 +1,200 @@ +LEROBOT_ROOTS = [ + "task_327", + "task_351", + "task_352", + "task_354", + "task_356", + "task_357", + "task_358", + "task_359", + "task_360", + "task_361", + "task_362", + "task_363", + "task_365", + "task_366", + "task_367", + "task_368", + "task_369", + "task_372", + "task_373", + "task_374", + "task_375", + "task_376", + "task_377", + "task_378", + "task_380", + "task_384", + "task_385", + "task_388", + "task_389", + "task_390", + "task_392", + "task_398", + "task_410", + "task_414", + "task_421", + "task_422", + "task_424", + "task_425", + "task_428", + "task_429", + "task_431", + "task_433", + "task_434", + "task_438", + "task_440", + "task_444", + "task_445", + "task_446", + "task_451", + "task_452", + "task_453", + "task_454", + "task_455", + "task_460", + "task_462", + "task_463", + "task_464", + "task_465", + "task_466", + "task_468", + "task_470", + "task_471", + "task_474", + "task_477", + "task_478", + "task_480", + "task_483", + "task_485", + "task_486", + "task_487", + "task_491", + "task_492", + "task_494", + "task_497", + "task_498", + "task_501", + "task_503", + "task_504", + "task_505", + "task_506", + "task_507", + "task_508", + "task_509", + "task_510", + "task_511", + "task_512", + "task_515", + "task_520", + "task_521", + "task_522", + "task_524", + "task_525", + "task_527", + "task_528", + "task_529", + "task_532", + "task_533", + "task_534", + "task_535", + "task_537", + "task_540", + "task_541", + "task_542", + "task_543", + "task_544", + "task_545", + "task_550", + "task_551", + "task_555", + "task_556", + "task_558", + "task_561", + "task_563", + "task_566", + "task_567", + "task_568", + "task_570", + "task_573", + "task_574", + "task_575", + "task_580", + "task_582", + "task_584", + "task_587", + "task_588", + "task_589", + "task_590", + "task_593", + "task_596", + "task_597", + "task_598", + "task_599", + "task_600", + "task_602", + "task_603", + "task_604", + "task_607", + "task_609", + "task_613", + "task_616", + "task_619", + "task_621", + "task_658", + "task_664", + "task_681", + "task_682", + "task_683", + "task_688", + "task_689", + "task_692", + "task_695", + "task_698", + "task_707", + "task_708", + "task_709", + "task_711", + "task_712", + "task_714", + "task_715", + "task_716", + "task_717", + "task_719", + "task_722", + "task_725", + "task_726", + "task_729", + "task_732", + "task_734", + "task_735", + "task_739", + "task_740", + "task_741", + "task_744", + "task_748", + "task_751", + "task_761", + "task_762", + "task_764", + "task_765", + "task_773", + "task_779", + "task_781", + "task_782", + "task_783", + "task_785", + "task_786", + "task_787", + "task_790", +] + +OBSERVATION_FEATURES = [ + "observation.images.head", + "observation.states.effector.position", + "observation.states.head.position", + "observation.states.joint.position", + "observation.states.robot.orientation", + "observation.states.robot.position", + "observation.states.waist.position", +] diff --git a/cosmos_training/cosmos/data/vfm/action/av_dataset.py b/cosmos_training/cosmos/data/vfm/action/av_dataset.py new file mode 100644 index 00000000..82cbdbac --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/av_dataset.py @@ -0,0 +1,1006 @@ +"""AV Dataset for Action training. + +This module provides an IterableDataset for AV data +that loads S3 storage containing tar files with video, action trajectories, +and route waypoints. + +Data format expected: + s3://bucket/path/*.tar -> pkl files containing: + - video: mp4 bytes + - action: pickled dict with: + - history_xyz: (history_length, 3) tensor - position history + - history_quat: (history_length, 4) tensor - quaternion history + - future_xyz: (future_length, 3) tensor - position future + - future_quat: (future_length, 4) tensor - quaternion future + - route: pickled numpy array of shape (num_waypoints, 3) - route waypoints in ego frame + +Action format: 7D pose per timestep [x, y, z, qw, qx, qy, qz] (3 position + 4 quaternion) +""" + +import io +import json +import math +import pickle +import random +import tarfile +from typing import Iterator, Literal + +import numpy as np +import torch +import torchvision +import torchvision.transforms.functional as F +from scipy.spatial.transform import Rotation +from torch.utils.data import IterableDataset + +# import torch.multiprocessing +# torch.multiprocessing.set_sharing_strategy("file_system") +from cosmos.utils import log +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.action.camera_dataset import get_target_size_and_crop +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + build_abs_pose_from_components, + pose_abs_to_rel, +) + + +def decode_video_bytes( + video_bytes: bytes, + resolution: str | None = None, + history_len: float | None = None, + future_len: float | None = None, + original_history_steps: int | None = None, +) -> tuple[torch.Tensor, float]: + """Decode video from mp4 bytes using torchvision.io. + + Args: + video_bytes: Raw mp4 video bytes. + resolution: Target resolution for video frames (e.g. "256", "480"). If None, keeps original resolution. + history_len: Desired history length in seconds. Used with future_len to slice video. + future_len: Desired future length in seconds. Used with history_len to slice video. + original_history_steps: Number of frames in the original history portion of the video. + + Returns: + Tuple of (video tensor in (C, T, H, W) uint8 format, original fps). + + Note: + The video structure is [history_frames | future_frames]. When slicing: + - History portion: take last (history_len * fps) frames from video[:original_history_steps] + - Future portion: take first (future_len * fps) frames from video[original_history_steps:] + This mirrors the slicing in process_action_trajectory. + """ + # Write bytes to a temporary file for torchvision.io.read_video + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=True) as tmp_file: + tmp_file.write(video_bytes) + tmp_file.flush() + + # Read video using torchvision.io + # Returns: (video_frames, audio_frames, info) + # video_frames shape: (T, H, W, C) uint8 + video_frames, _, info = torchvision.io.read_video(tmp_file.name, pts_unit="sec") + + original_fps = info.get("video_fps", 30.0) + + # Slice video to match history_len and future_len + # Video structure: [history_frames | future_frames] + if original_history_steps is None: + raise ValueError("original_history_steps is required to slice video") + + # Split video at history/future boundary + history_video = video_frames[:original_history_steps] + future_video = video_frames[original_history_steps:] + + # Slice history (take last N frames) + if history_len is not None: + history_steps = int(history_len * original_fps) + if history_steps > history_video.shape[0]: + raise ValueError( + f"Requested history_len={history_len}s ({history_steps} frames at {original_fps}Hz) " + f"exceeds available history video ({history_video.shape[0]} frames)" + ) + history_video = history_video[-history_steps:] + + # Slice future (take first N frames) + if future_len is not None: + future_steps = int(future_len * original_fps) + if future_steps > future_video.shape[0]: + raise ValueError( + f"Requested future_len={future_len}s ({future_steps} frames at {original_fps}Hz) " + f"exceeds available future video ({future_video.shape[0]} frames)" + ) + future_video = future_video[:future_steps] + + # Concatenate sliced portions + video_frames = torch.cat([history_video, future_video], dim=0) # [T,H,W,C] + + # Convert from (T, H, W, C) to (T, C, H, W) + video_tensor = video_frames.permute(0, 3, 1, 2) # [T,C,H,W] + + # Resize and Crop if resolution is provided + if resolution is not None: + T, C, H, W = video_tensor.shape + # get_target_size_and_crop expects (resolution, current_H, current_W) + new_H, new_W, target_canvas_H, target_canvas_W = get_target_size_and_crop(resolution, H, W) + + # Resize if needed + if new_H != H or new_W != W: + video_tensor = F.resize( + video_tensor, [new_H, new_W], interpolation=F.InterpolationMode.BICUBIC, antialias=True + ) + + # Center Crop + if new_H != target_canvas_H or new_W != target_canvas_W: + video_tensor = F.center_crop(video_tensor, [target_canvas_H, target_canvas_W]) + + # Convert to uint8 if not already + if video_tensor.dtype != torch.uint8: + video_tensor = video_tensor.to(torch.uint8) + + # Convert from (T, C, H, W) to (C, T, H, W) + video_tensor = video_tensor.permute(1, 0, 2, 3) # [C,T,H,W] + + return video_tensor, original_fps + + +# 3x3 rotation from car convention (x=forward, y=left, z=up) +# to OpenCV convention (x=right, y=down, z=forward). +# Mapping: new_x = -old_y, new_y = -old_z, new_z = old_x +CAR_TO_OPENCV_ROTATION = np.array( + [[0, -1, 0], [0, 0, -1], [1, 0, 0]], + dtype=np.float32, +) + + +def process_action_trajectory( + action_data: dict, + history_len: float | None = None, + future_len: float | None = None, + fps: int = 10, + rotation_format: Literal["9D", "rot6d", "quat_xyzw", "euler_xyz"] = "9D", + pose_convention: Literal["backward_anchored", "backward_framewise"] = ("backward_framewise"), + scale: float = 1.0, + rotation_scale: float = 1.0, + max_translation_norm: float | None = None, + align_opencv_pose: bool = False, +): + """Process action trajectories from action data dict. + + Args: + action_data: Dict with: + - history_xyz: (T_hist, 3) tensor - position history + - history_quat: (T_hist, 4) tensor - quaternion history + - future_xyz: (T_fut, 3) tensor - position future + - future_quat: (T_fut, 4) tensor - quaternion future + history_len: Desired history length in seconds. + future_len: Desired future length in seconds. + fps: Frames per second, used to compute number of steps from time durations. + align_opencv_pose: If True, transform poses from car convention + (x=forward, y=left, z=up) to OpenCV convention (x=right, y=down, z=forward). + NOTE: av_v2_* data is already in OpenCV convention, DO NOT apply this transformation! + + Returns: + Tuple of (history_action, future_action). + Both actions are torch.Tensor of shape (T, 7) in [x, y, z, qw, qx, qy, qz] format. + + Note: + History steps = history_len * fps, same as future steps = future_len * fps. + For example, with history_len=1.0s and fps=10, we get 10 history steps. + """ + # Extract and ensure tensors + history_xyz = action_data["history_xyz"] + history_quat = action_data["history_quat"] + future_xyz = action_data["future_xyz"] + future_quat = action_data["future_quat"] + + # Convert to tensors if needed + if not isinstance(history_xyz, torch.Tensor): + history_xyz = torch.tensor(history_xyz, dtype=torch.float32) # [T_hist,3] + if not isinstance(history_quat, torch.Tensor): + history_quat = torch.tensor(history_quat, dtype=torch.float32) # [T_hist,4] + if not isinstance(future_xyz, torch.Tensor): + future_xyz = torch.tensor(future_xyz, dtype=torch.float32) # [T_fut,3] + if not isinstance(future_quat, torch.Tensor): + future_quat = torch.tensor(future_quat, dtype=torch.float32) # [T_fut,4] + + # Slice history to desired length (take the last N steps) + if history_len is not None: + history_steps = int(history_len * fps) + available_history = history_xyz.shape[0] + if history_steps > available_history: + raise ValueError( + f"Requested history_len={history_len}s ({history_steps} steps at {fps}Hz) " + f"exceeds available history ({available_history} steps)" + ) + history_xyz = history_xyz[-history_steps:] + history_quat = history_quat[-history_steps:] + + # Slice future to desired length (take the first N steps) + if future_len is not None: + future_steps = int(future_len * fps) + available_future = future_xyz.shape[0] + if future_steps > available_future: + raise ValueError( + f"Requested future_len={future_len}s ({future_steps} steps at {fps}Hz) " + f"exceeds available future ({available_future} steps)" + ) + future_xyz = future_xyz[:future_steps] + future_quat = future_quat[:future_steps] + + # Concatenate to form full trajectory + # history_xyz: (T_hist, 3) + # history_quat: (T_hist, 4) [w, x, y, z] + all_xyz = torch.cat([history_xyz, future_xyz], dim=0) # [T,3] + all_quat = torch.cat([history_quat, future_quat], dim=0) # [T,4] + + poses_abs = build_abs_pose_from_components( + all_xyz, + all_quat, + "quat_wxyz", + ) + + if align_opencv_pose: + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ CAR_TO_OPENCV_ROTATION.T + + actions = pose_abs_to_rel( + poses_abs, + rotation_format=rotation_format, + pose_convention=pose_convention, + translation_scale=scale, + rotation_scale=rotation_scale, + ) + + if max_translation_norm is not None: + trans_norms = np.linalg.norm(actions[:, :3], axis=1) + if trans_norms.max() > max_translation_norm: + return None + + actions = torch.from_numpy(actions) # [T-1,action_dim] + + # Split back + # history_action has one less action than history_xyz because the first action is the initial pose + history_action = actions[: len(history_xyz) - 1] + future_action = actions[len(history_xyz) - 1 :] + + return history_action, future_action + + +def add_route_noise( + route: torch.Tensor, + lat_noise_range: float = 0.0, + long_noise_range: float = 0.0, + point_wise_noise: float = 0.0, +) -> torch.Tensor: + """Add noise to route waypoints for data augmentation. + + Applies two types of noise: + 1. Uniform lateral/longitudinal shift (same offset for all waypoints in a sample) + 2. Per-point Gaussian noise (independent per waypoint) + + Both noise types leave the z-axis unchanged. NaN waypoints (padding) are preserved. + + Args: + route: (T, 3) tensor of route waypoints in XYZ. + lat_noise_range: Half-range for uniform lateral (Y) noise. + long_noise_range: Half-range for uniform longitudinal (X) noise. + point_wise_noise: Standard deviation of per-point Gaussian noise. + + Returns: + Noisy route tensor of shape (T, 3). + """ + if lat_noise_range > 0 or long_noise_range > 0: + shift = torch.rand(3) * torch.tensor([2 * long_noise_range, 2 * lat_noise_range, 0.0]) - torch.tensor( # [3] + [long_noise_range, lat_noise_range, 0.0] + ) + route = route + shift[None, :] + + if point_wise_noise > 0: + noise = torch.randn(route.shape[0], 3) * point_wise_noise # [T,3] + noise[..., -1] = 0.0 + route = route + noise + + return route + + +def apply_route_dropout( + route: torch.Tensor, + dropout_rate: float = 0.5, + tail_dropout_rate: float = 0.3, +) -> torch.Tensor: + """Apply dropout masking to route waypoints for data augmentation. + + Three dropout behaviours, applied sequentially: + 1. With probability ``dropout_rate``, mask **all** waypoints. + Otherwise, randomly mask the first K waypoints (K ~ Uniform(0, T)). + 2. With probability ``tail_dropout_rate``, additionally mask waypoints + from a random index in [T//2, T) onward. + + Masked waypoints are set to NaN so downstream code can detect padding + via ``torch.isnan``. + + Args: + route: (T, 3) tensor of route waypoints. + dropout_rate: Probability of fully disabling the route. + tail_dropout_rate: Probability of additional tail dropout. + + Returns: + Route tensor with dropout applied, shape (T, 3). + """ + T = route.shape[0] + mask = torch.isnan(route[..., 0]) # [T] existing padding + + if random.uniform(0, 1) < dropout_rate: + dropout_mask = torch.ones(T, dtype=torch.bool) # [T] + else: + dropout_idx = random.randint(0, T) + dropout_mask = torch.arange(T) < dropout_idx # [T] + + if random.uniform(0, 1) < tail_dropout_rate: + tail_idx = random.randint(T // 2, T - 1) if T > 1 else 0 + tail_dropout_mask = torch.arange(T) >= tail_idx # [T] + dropout_mask = dropout_mask | tail_dropout_mask + + mask = mask | dropout_mask + route = route.clone() + route[mask] = float("nan") + return route + + +def _classify_displacement(dx: float, dy: float, move_threshold: float = 0.1) -> str: + """Classify a 2D displacement vector into a direction label. + + Uses the angle of the displacement in ego frame (X=forward, Y=left) to + determine the driving direction. + + Args: + dx: Forward displacement (positive = forward). + dy: Lateral displacement (positive = left). + move_threshold: Minimum displacement magnitude (meters) to count as movement. + + Returns: + One of: "go forward", "turn left", "turn right", "go backward", "stay". + """ + dist = math.sqrt(dx * dx + dy * dy) + if dist < move_threshold: + return "stay" + + angle_deg = math.degrees(math.atan2(dy, dx)) + + if -45 <= angle_deg <= 45: + return "go forward" + elif 45 < angle_deg <= 135: + return "turn left" + elif -135 <= angle_deg < -45: + return "turn right" + else: + return "go backward" + + +def classify_trajectory_to_text( + trajectory: torch.Tensor, + move_threshold: float = 0.1, + min_segment_steps: int = 2, +) -> str: + """Classify a trajectory in ego frame into a brief semantic text description. + + Classifies each consecutive point pair independently, groups consecutive + identical labels, filters out noisy short groups, and joins distinct + phases with "then". + + Works with any (T, 3) path in ego frame — route waypoints or pose + positions returned by :func:`compute_future_trajectory_in_ego_frame`. + + Args: + trajectory: (T, 3) tensor of positions in ego frame (X=forward, Y=left, Z=up). + The first point is treated as the starting position. + move_threshold: Minimum per-step displacement (meters) to count as movement. + min_segment_steps: Minimum consecutive steps required for a direction label to + be kept; shorter runs are treated as noise and dropped. + + Returns: + A description such as "go forward", "stay then go forward", + "turn left then go forward", or "stay" when the trajectory is empty + or all NaN. + """ + valid_mask = ~torch.isnan(trajectory[:, 0]) + valid_pts = trajectory[valid_mask] + + if len(valid_pts) < 2: + return "stay" + + # Classify every consecutive point pair + step_labels: list[str] = [] + for i in range(len(valid_pts) - 1): + dx = valid_pts[i + 1, 0].item() - valid_pts[i, 0].item() + dy = valid_pts[i + 1, 1].item() - valid_pts[i, 1].item() + step_labels.append(_classify_displacement(dx, dy, move_threshold)) + + # Group consecutive identical labels with their counts + groups: list[tuple[str, int]] = [] + for label in step_labels: + if groups and label == groups[-1][0]: + groups[-1] = (label, groups[-1][1] + 1) + else: + groups.append((label, 1)) + + # Filter out groups shorter than min_segment_steps to suppress noise + if len(groups) > 1: + filtered = [(lbl, cnt) for lbl, cnt in groups if cnt >= min_segment_steps] + if not filtered: + # All groups are short — keep the longest one + filtered = [max(groups, key=lambda g: g[1])] + groups = filtered + + # Deduplicate consecutive identical labels (may arise after filtering) + result = [groups[0][0]] + for label, _ in groups[1:]: + if label != result[-1]: + result.append(label) + + return " then ".join(result) + + +def compute_future_trajectory_in_ego_frame( + action_data: dict, + history_len: float | None = None, + future_len: float | None = None, + fps: int = 10, +) -> torch.Tensor: + """Compute future trajectory positions in the ego coordinate frame. + + Transforms absolute future xyz positions so that the origin is the last + history pose and axes align with ego frame (X=forward, Y=left, Z=up). + + Args: + action_data: Dict with keys ``history_xyz``, ``history_quat``, + ``future_xyz`` (and optionally ``future_quat``). + history_len: History length in seconds for slicing. If *None*, uses all. + future_len: Future length in seconds for slicing. If *None*, uses all. + fps: Frames per second. + + Returns: + (T, 3) float tensor of future positions in ego frame. + """ + history_xyz = action_data["history_xyz"] + history_quat = action_data["history_quat"] + future_xyz = action_data["future_xyz"] + + if not isinstance(history_xyz, torch.Tensor): + history_xyz = torch.tensor(history_xyz, dtype=torch.float32) + if not isinstance(history_quat, torch.Tensor): + history_quat = torch.tensor(history_quat, dtype=torch.float32) + if not isinstance(future_xyz, torch.Tensor): + future_xyz = torch.tensor(future_xyz, dtype=torch.float32) + + # Slice to match the requested durations + if history_len is not None: + history_steps = int(history_len * fps) + history_xyz = history_xyz[-history_steps:] + history_quat = history_quat[-history_steps:] + if future_len is not None: + future_steps = int(future_len * fps) + future_xyz = future_xyz[:future_steps] + + # Current pose = last history frame + current_pos = history_xyz[-1] # (3,) + current_quat_wxyz = history_quat[-1] # (4,) [w, x, y, z] + + # Scipy expects [x, y, z, w] + quat_xyzw = current_quat_wxyz[[1, 2, 3, 0]].numpy() + rot_world_to_ego = Rotation.from_quat(quat_xyzw).inv() + + # Translate then rotate into ego frame + future_rel = (future_xyz - current_pos[None, :]).numpy() + future_ego = rot_world_to_ego.apply(future_rel).astype(np.float32) + + return torch.from_numpy(future_ego) + + +class AVDataset(IterableDataset): + """AV dataset that reads tar files from S3 using wdinfo.json.""" + + def __init__( + self, + root: str | list[str] = "s3://nv-00-10206-robot/cosmos3_action_data/av_v2_02182026_wdinfo/", + credential_path: str = "credentials/gcp_training.secret", + resolution: str | None = None, + fps: int = 10, + mode: str = "policy", + embodiment_type: str = "av", + split: str = "train", + seed: int = 0, + shuffle: bool = True, + history_len: float | None = None, + future_len: float | None = None, + rotation_format: RotationConvention = "rot9d", + pose_convention: Literal["backward_anchored", "backward_framewise"] = ("backward_framewise"), + route_lat_noise_range: float = 0.0, + route_long_noise_range: float = 0.0, + route_point_wise_noise: float = 0.0, + route_dropout: bool = False, + route_dropout_rate: float = 0.0, + route_tail_dropout_rate: float = 0.0, + include_route_in_prompt: bool = True, + use_semantic_route_prompt: bool = False, + translation_scale: float = 1.0, + rotation_scale: float = 1.0, + max_action_translation_norm: float | None = None, + align_opencv_pose: bool = False, + # When True, use a separate domain ID for inverse dynamics / policy modes + # so that DomainAwareLinear learns different projections for anchored (conditioning) + # vs framewise (generation) action representations. + mode_aware_domain: bool = False, + inv_embodiment_type: str = "av_inv", + ): + """Initialize AVDataset. + + Args: + root: S3 path (or list of S3 paths) to wdinfo directories containing train/val subdirectories with wdinfo.json files. + credential_path: Path to JSON file containing S3 credentials. + resolution: Target resolution for video frames (e.g. "256", "480"). If None, keeps original resolution. + fps: Target frames per second for video and actions. + mode: Training mode ('policy', 'forward_dynamics', 'inverse_dynamics', 'image2video', 'joint'). + embodiment_type: Embodiment type for domain ID. + split: Dataset split ('train', 'val', or 'full'). + seed: Random seed for shuffling. + shuffle: Whether to shuffle tar files during iteration (for training). + history_len: Desired history length in seconds. If None, uses all available history. + future_len: Desired future length in seconds. If None, uses all available future. + rotation_format: Rotation convention for actions (e.g. "rot9d", "rot6d", "euler_xyz"). + pose_convention: Pose format for actions (e.g. "backward_framewise", "backward_framewise"). + route_lat_noise_range: Half-range for uniform lateral (Y) noise on route waypoints. + route_long_noise_range: Half-range for uniform longitudinal (X) noise on route waypoints. + route_point_wise_noise: Std-dev of per-waypoint Gaussian noise on route. + route_dropout: Whether to apply random waypoint dropout on route during training. + route_dropout_rate: Probability of fully masking the route (used when route_dropout=True). + route_tail_dropout_rate: Probability of additional tail dropout (used when route_dropout=True). + include_route_in_prompt: Whether to include route waypoints as text in the prompt. + use_semantic_route_prompt: When True and include_route_in_prompt is True, replace raw + coordinate waypoints with a brief semantic description (e.g. "go forward then turn left"). + translation_scale: Scale factor applied to the translation block of the encoded action. + rotation_scale: Scale factor applied to the rotation block of the encoded action + (uniform scalar, preserves rotation-block geometry). Pass the same value to + `pose_rel_to_abs` when decoding. + max_action_translation_norm: If set, discard the sample when any per-frame + scaled translation L2 norm exceeds this value. Acts as an outlier + filter to prevent loss spikes from extreme camera motion. + align_opencv_pose: If True, transform pose rotations from car body-frame + convention (x=forward, y=left, z=up) to OpenCV camera convention + (x=right, y=down, z=forward) before computing relative actions. + mode_aware_domain: When True, inverse_dynamics/policy modes use a separate domain ID. + inv_embodiment_type: Embodiment type string for the inverse domain ID. + """ + super().__init__() + + if isinstance(root, str): + root = [root] + self.roots = [r.rstrip("/") for r in root] + self.credential_path = credential_path + self.resolution = resolution + self.fps = fps + self.mode = mode + self.split = split.lower().strip() + self.seed = seed + self.shuffle = shuffle + self._epoch = 0 + self.history_len = history_len + self.future_len = future_len + self.rotation_format: RotationConvention = rotation_format + self.pose_convention: Literal["absolute", "backward_anchored", "backward_framewise"] = pose_convention + self.route_lat_noise_range = route_lat_noise_range + self.route_long_noise_range = route_long_noise_range + self.route_point_wise_noise = route_point_wise_noise + self.route_dropout = route_dropout + self.route_dropout_rate = route_dropout_rate + self.route_tail_dropout_rate = route_tail_dropout_rate + self.include_route_in_prompt = include_route_in_prompt + self.use_semantic_route_prompt = use_semantic_route_prompt + self.translation_scale = translation_scale + self.rotation_scale = rotation_scale + self.max_action_translation_norm = max_action_translation_norm + self.align_opencv_pose = align_opencv_pose + # Get domain ID for this embodiment + self.domain_id = get_domain_id(embodiment_type) + self.mode_aware_domain = mode_aware_domain + self.domain_id_inv = get_domain_id(inv_embodiment_type) if mode_aware_domain else self.domain_id + + # Validate mode + valid_modes = ["joint", "forward_dynamics", "inverse_dynamics", "policy", "image2video"] + if mode not in valid_modes: + raise ValueError(f"mode must be one of {valid_modes}, got {mode}") + + # Validate split + if self.split not in {"train", "val", "valid", "validation", "eval", "test", "full"}: + raise ValueError(f"Unsupported {split=}. Use train/val/full.") + + # Validate S3 roots + for r in self.roots: + if not r.startswith("s3://"): + raise ValueError(f"root must be an S3 path starting with 's3://', got: {r}") + + # Configure S3 backend using easy_io + self._setup_s3_backend() + + # Load tar files from wdinfo.json + self._tar_files: list[str] = [] + self._total_key_count: int = 0 + self._chunk_size: int = 10 + + self._load_wdinfo() + + log.info( + f"Initialized AVDataset: root={self.roots}, split={self.split}, " + f"resolution={resolution}, fps={fps}, mode={mode}, " + f"num_tar_files={len(self._tar_files)}, " + f"total_samples={self._total_key_count}" + ) + + def _setup_s3_backend(self) -> None: + """Configure the easy_io S3 backend. Called in __init__ and __iter__ for worker processes.""" + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": None, + "s3_credential_path": self.credential_path, + } + ) + + def _load_wdinfo(self) -> None: + """Load wdinfo.json for the current split from all roots and build tar file list. + + Supports two directory layouts per root: + - Split-based: ``{root}/train/wdinfo.json``, ``{root}/val/wdinfo.json`` + - Flat: ``{root}/wdinfo.json`` (treated as train-only) + + Split-based paths are tried first; the flat path is used as a fallback + only when no split-based wdinfo was found and the requested split + includes "train". + """ + self._tar_files = [] + self._total_key_count = 0 + + for root in self.roots: + bucket = root.replace("s3://", "").split("/")[0] + + # Determine which splits we need + if self.split in {"val", "valid", "validation", "eval", "test"}: + target_splits = ["val"] + elif self.split == "train": + target_splits = ["train"] + elif self.split == "full": + target_splits = ["train", "val"] + else: + raise ValueError(f"Unsupported split: {self.split}") + + # Try split-based layout first ({root}/train/wdinfo.json, {root}/val/wdinfo.json) + wdinfo_entries: list[tuple[str, dict]] = [] + for split_name in target_splits: + split_path = f"{root}/{split_name}/wdinfo.json" + try: + wdinfo_entries.append((split_path, json.loads(easy_io.get(split_path)))) + except Exception: + pass + + # Fall back to flat layout ({root}/wdinfo.json, treated as train-only) + if not wdinfo_entries and "train" in target_splits: + flat_path = f"{root}/wdinfo.json" + try: + wdinfo_entries.append((flat_path, json.loads(easy_io.get(flat_path)))) + except Exception: + pass + + if not wdinfo_entries: + log.warning(f"No wdinfo.json found for root={root}, split={self.split}") + + for wdinfo_path, wdinfo in wdinfo_entries: + log.info(f"Loading wdinfo from: {wdinfo_path}") + + # Extract metadata + self._chunk_size = wdinfo.get("chunk_size", 10) + data_root = wdinfo.get("root", "") + data_list = wdinfo.get("data_list", []) + + if not data_list: + log.warning(f"No tar files found in wdinfo: {wdinfo_path}") + continue + + # Reconstruct full S3 paths for tar files + tar_root = f"s3://{bucket}/{data_root}".rstrip("/") + tar_paths = [f"{tar_root}/{filename}" for filename in data_list] + self._tar_files.extend(tar_paths) + + # Accumulate total sample count + self._total_key_count += wdinfo.get("total_key_count", len(data_list) * self._chunk_size) + + log.info( + f"Loaded {len(data_list)} tar files from wdinfo, " + f"with {wdinfo.get('total_key_count', len(data_list) * self._chunk_size)} samples" + ) + + if not self._tar_files: + raise RuntimeError(f"No tar files found in wdinfo at {self.roots}") + + def __len__(self) -> int: + """Return the estimated number of samples in the current split.""" + return self._total_key_count + + def _process_sample(self, pkl_data: dict, key: str, global_idx: int) -> dict | None: + """Process a single sample from pkl data. + + Args: + pkl_data: Dictionary with 'video' (bytes) and 'action' (pickled dict). + key: Sample key (basename without .pkl). + global_idx: Global sample index for __key__. + + Returns: + Processed sample dictionary, or None if the sample should be discarded. + """ + # Extract video bytes + video_bytes = pkl_data.get("video") + if video_bytes is None: + raise RuntimeError(f"No video found for key {key}") + + # Extract action data + action_bytes = pkl_data.get("action") + if action_bytes is None: + raise RuntimeError(f"Missing action data for key {key}") + + action_data = pickle.loads(action_bytes) + + # Extract route data + route_bytes = pkl_data.get("route") + if route_bytes is not None: + route_data = pickle.loads(route_bytes) + if not isinstance(route_data, torch.Tensor): + route = torch.tensor(route_data, dtype=torch.float32) # [num_waypoints,3] + else: + route = route_data.float() # [num_waypoints,3] + else: + log.warning(f"No route found for key {key}") + route = torch.full((20, 3), float("nan")) # [20,3] + + # Apply route augmentations during training + if self.split == "train": + route = add_route_noise( + route, + lat_noise_range=self.route_lat_noise_range, + long_noise_range=self.route_long_noise_range, + point_wise_noise=self.route_point_wise_noise, + ) + if self.route_dropout: + route = apply_route_dropout( + route, + dropout_rate=self.route_dropout_rate, + tail_dropout_rate=self.route_tail_dropout_rate, + ) + + # Get original history frame count for video slicing + original_history_steps = len(action_data["history_xyz"]) + + # Decode video + video, _ = decode_video_bytes( + video_bytes, + resolution=self.resolution, + history_len=self.history_len, + future_len=self.future_len, + original_history_steps=original_history_steps, + ) + + # Determine mode for this sample + if self.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy"]) + # mode = random.choice(["policy", "image2video"]) + else: + mode = self.mode + + # Process actions + action_result = process_action_trajectory( + action_data, + history_len=self.history_len, + future_len=self.future_len, + fps=self.fps, + rotation_format=self.rotation_format, + pose_convention=self.pose_convention, + scale=self.translation_scale, + rotation_scale=self.rotation_scale, + max_translation_norm=self.max_action_translation_norm, + align_opencv_pose=self.align_opencv_pose, + ) + if action_result is None: + return None + history_action, future_action = action_result + + # Combine and pad actions + combined_action = torch.cat([history_action, future_action], dim=0) # [T_hist+T_fut,action_dim] + + # FPS as tensor + fps_tensor = torch.tensor(self.fps, dtype=torch.long) # scalar + + # Key as tensor + key_tensor = torch.tensor([global_idx], dtype=torch.long) # [1] + + # Compute actual history/future lengths from data + actual_history_length = history_action.shape[0] + actual_future_length = future_action.shape[0] + + # Generate prompt based on actual data lengths + history_duration = actual_history_length / self.fps + future_duration = actual_future_length / self.fps + + prompt = "You are an autonomous vehicle planning system. " + if self.include_route_in_prompt and mode == "policy": # only include route in prompt for policy mode + if self.use_semantic_route_prompt: + future_ego = compute_future_trajectory_in_ego_frame( + action_data, self.history_len, self.future_len, self.fps + ) + trajectory_desc = classify_trajectory_to_text(future_ego) + prompt += f"Please {trajectory_desc}. " + else: + num_waypoints = route.shape[0] + waypoints_str = ", ".join( + "nan" if torch.isnan(wp[0]) else f"({wp[0]:.2f}, {wp[1]:.2f}, {wp[2]:.2f})" for wp in route + ) + prompt += ( + f"The navigation route has {num_waypoints} waypoints " + f"(XYZ in ego frame with X=forward, Y=left, Z=up): " + f"[{waypoints_str}]. A nan waypoint means that waypoint is not available. " + ) + # prompt += f"Predict the future {future_duration:.1f}s action trajectory at {self.fps}Hz." + + # Select domain ID: use inverse domain for generation modes when mode_aware_domain is on + if self.mode_aware_domain and mode in ["inverse_dynamics", "policy"]: + domain_id = self.domain_id_inv + else: + domain_id = self.domain_id + + sample = { + "video": video, + "action": combined_action, + "action_history": history_action, + "action_future": future_action, + "route": route, + "conditioning_fps": fps_tensor, + "prompt": prompt, + "ai_caption": prompt, + "mode": mode, + "__key__": key_tensor, + "domain_id": torch.tensor(domain_id, dtype=torch.long), + "history_length": actual_history_length, + "future_length": actual_future_length, + "viewpoint": "ego_view", + } + return sample + + def __iter__(self) -> Iterator[dict[str, torch.Tensor | str | int]]: + """Iterate over the dataset, loading tar files from S3.""" + # Re-configure S3 backend in case this is running in a worker process after unpickling + self._setup_s3_backend() + + # Optionally shuffle tar files for training + tar_files = list(self._tar_files) + if self.shuffle: + rng = random.Random(self.seed + self._epoch) + rng.shuffle(tar_files) + self._epoch += 1 + + global_idx = 0 + + for tar_path in tar_files: + try: + # Read tar file bytes using easy_io + tar_bytes = easy_io.get(tar_path) + + with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar: + for member in tar.getmembers(): + if not member.name.endswith(".pkl"): + continue + + try: + # Extract and process the sample + f_member = tar.extractfile(member) + if f_member is None: + log.warning(f"Failed to extract {member.name} from {tar_path}") + continue + + try: + pkl_data = pickle.load(f_member) + finally: + f_member.close() + + key = member.name.rsplit(".", 1)[0] + + sample = self._process_sample(pkl_data, key, global_idx) + if sample is not None: + yield sample + global_idx += 1 + + except Exception as e: + log.warning(f"Failed to process sample {member.name} from {tar_path}: {e}") + continue + + except Exception as e: + log.warning(f"Failed to read tar file {tar_path}: {e}") + continue + + +# PYTHONPATH=. python cosmos/data/vfm/action/av_dataset.py +if __name__ == "__main__": + import json as _json + import os + import time + + import torchvision + + from cosmos.data.vfm.action.pose_utils import pose_rel_to_abs + + _ACTION_SCALE = 1.35 + _ROTATION_SCALE = 1.0 + _ROTATION_FORMAT = "rot6d" + _POSE_CONVENTION = "backward_framewise" + + dataset = AVDataset( + root=[ + # "s3://nv-00-10206-robot/cosmos3_action_data/av_02182026_wdinfo/", + # "s3://nv-00-10206-robot/cosmos3_action_data/av_03292026_wdinfo/", + "s3://nv-00-10206-robot/cosmos3_action_data/av_v2_02182026_wdinfo/", + "s3://nv-00-10206-robot/cosmos3_action_data/av_v2_03292026_wdinfo/", + ], + split="train", + shuffle=True, + fps=10, + mode="inverse_dynamics", + history_len=0.1, + future_len=6.0, + rotation_format=_ROTATION_FORMAT, + pose_convention=_POSE_CONVENTION, + translation_scale=_ACTION_SCALE, + rotation_scale=_ROTATION_SCALE, + resolution="480", + include_route_in_prompt=False, + use_semantic_route_prompt=False, + # align_opencv_pose=False, + ) + dataset_iter = iter(dataset) + os.makedirs("temp", exist_ok=True) + + for i in range(5): + print(f"==================== Sample {i} ====================") + _t0 = time.time() + data = next(dataset_iter) + _t1 = time.time() + print(f"{'Loading time':<25}: {_t1 - _t0:.2f}s") + + print(f"{'video shape':<25}: {data['video'].shape}") # [C,T,H,W] + print(f"{'action shape':<25}: {data['action'].shape}") # [T,action_dim] + print(f"{'action_history shape':<25}: {data['action_history'].shape}") + print(f"{'action_future shape':<25}: {data['action_future'].shape}") + print(f"{'route shape':<25}: {data['route'].shape}") + print(f"{'history_length':<25}: {data['history_length']}") + print(f"{'future_length':<25}: {data['future_length']}") + print(f"{'conditioning_fps':<25}: {data['conditioning_fps'].item()}") + print(f"{'mode':<25}: {data['mode']}") + print(f"{'domain_id':<25}: {data['domain_id'].item()}") + print(f"{'prompt':<25}: {data['prompt']}") + + # save video + video = data["video"].permute(1, 0, 2, 3) # [C,T,H,W] -> [T,C,H,W] + video_path = f"temp/av_sample_{i}.mp4" + torchvision.io.write_video( + video_path, video.permute(0, 2, 3, 1).numpy(), fps=data["conditioning_fps"].item() + ) # expects (T, H, W, C) + print(f"Saved video to {video_path}") + + # reconstruct absolute poses from relative actions and save as json + camera_poses = pose_rel_to_abs( + data["action"].float().numpy(), + rotation_format=_ROTATION_FORMAT, + pose_convention=_POSE_CONVENTION, + translation_scale=_ACTION_SCALE, + rotation_scale=_ROTATION_SCALE, + ) + pose_path = f"temp/av_sample_{i}_camera.json" + with open(pose_path, "w") as f: + _json.dump(camera_poses.tolist(), f) + print(f"Saved camera poses to {pose_path}") diff --git a/cosmos_training/cosmos/data/vfm/action/bridge_orig_lerobot_dataset.py b/cosmos_training/cosmos/data/vfm/action/bridge_orig_lerobot_dataset.py new file mode 100644 index 00000000..4fd69a6b --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/bridge_orig_lerobot_dataset.py @@ -0,0 +1,269 @@ +# * https://github.com/2toinf/X-VLA/blob/30090f81cf91b15da73af234ce2b098fe20590f8/datasets/domain_handler/simulations.py#L70-L93 +# * https://github.com/2toinf/X-VLA/issues/11 +# * https://github.com/2toinf/X-VLA/issues/33 +# * https://github.com/2toinf/X-VLA/issues/67 +# + +# uses identity stats (q01=-1, q99=1) on the 6D rotation dims 3..8, while +# ``"quantile_rot"`` uses the raw stats and normalizes those columns too. + +from typing import Any + +import numpy as np +import torch +from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata + +from cosmos.utils import log +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.pose_utils import ( + PoseConvention, + build_abs_pose_from_components, + convert_rotation, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +# Bridge rotation decomposition: +# 1) _DEFAULT_ROTATION: raw bridge state → kinematics (MJCF/URDF) frame. +# The WidowX controller records ``R_state = R_fk @ DEFAULT_ROTATION.T``, +# so ``R_fk = R_state @ DEFAULT_ROTATION``. +# 2) _TCP_TO_FLANGE: re-reference from ee_gripper_link to gripper_link +# (pure translation in kinematics frame). See block below. +# 3) _KIN_TO_OPENCV: kinematics → OpenCV convention (for training/vis). +# The viewer undoes this before IK to recover the kinematic frame. +_DEFAULT_ROTATION = np.array( + [[0, 0, 1], [0, 1, 0], [-1, 0, 0]], + dtype=np.float32, +) +_BRIDGE_TO_OPENCV = np.array( + [[0, 0, 1], [-1, 0, 0], [0, -1, 0]], + dtype=np.float32, +) + +# --------------------------------------------------------------------------- +# TCP → flange (gripper body) offset +# --------------------------------------------------------------------------- +# The bridge dataset records EE poses at ``ee_gripper_link`` — the Interbotix +# SDK's end-effector reference, 93.6 mm past the wrist rotate body +# (``gripper_link``), roughly at the grasp center between the finger pads. +# For action learning we re-reference poses to the *wrist rotate body* +# (``gripper_link``) because: +# 1. It is the last actuated link — its pose is fully determined by joint +# angles, with no dependence on finger opening. +# 2. The ~10 cm offset reduces the lever-arm effect of small rotation +# errors on position accuracy. +# 3. Consistent with Google Robot, where we also target the gripper body. +# +# The constant below is the SE(3) transform from ``ee_gripper_link`` to +# ``gripper_link``, computed from the SimplerEnv URDF via pinocchio FK at the +# neutral configuration: +# T = oMf[ee_gripper_link]⁻¹ · oMf[gripper_link] +# It is pure translation (identity rotation) — the two frames share the +# same orientation by construction (connected via fixed joints with no +# rotational offset). +# +# Source URDF: https://github.com/simpler-env/ManiSkill2_real2sim +# → mani_skill2_real2sim/assets/descriptions/widowx_description/ +# + +# so the translation is expressed in the kinematic (MJCF) frame. +# fmt: off +_TCP_TO_FLANGE = np.array([ + [+1.0000000000, +0.0000000000, +0.0000000000, -0.0935750000], + [+0.0000000000, +1.0000000000, +0.0000000000, +0.0000000000], + [+0.0000000000, +0.0000000000, +1.0000000000, +0.0000000000], + [+0.0000000000, +0.0000000000, +0.0000000000, +1.0000000000], +], dtype=np.float32) +# fmt: on + + +class BridgeOrigLeRobotDataset(BaseActionLeRobotDataset): + """ """ + + def __init__( + self, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/bridge_raw", + fps: float = 5.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + mode: str = "policy", + pose_convention: PoseConvention = "backward_framewise", + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "ego_view", + enable_fast_init: bool = False, + ) -> None: + """ """ + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type="bridge_orig_lerobot", + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format="rot6d", + action_normalization=action_normalization, + tolerance_s=1e-4, + enable_fast_init=enable_fast_init, + ) + # _to_opencv is the kinematics→OpenCV part only. + # The viewer undoes this before IK → recovers kinematic frame directly. + self._to_opencv = _BRIDGE_TO_OPENCV + + self._all_shard_roots = [root] + + self._delta_timestamps = { + "observation.images.image_0": [i * self._dt for i in range(0, self._chunk_length + 1)], + "observation.state": [i * self._dt for i in range(0, self._chunk_length + 1)], + "action": [i * self._dt for i in range(0, self._chunk_length)], + } + + # ------------------------------------------------------------------ + # Action computation + # ------------------------------------------------------------------ + + def _compute_absolute_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor]: + """Absolute action from state + gripper from action. + + EEF xyz+rotation come from observation.state[1:]; gripper from action[:, 6]. + + Returns: + (action_tensor, initial_pose) — initial_pose is the first-frame + absolute EE pose (4×4, in the corrected OpenCV frame). + """ + state = sample["observation.state"][1:] # [T, 8] + poses_abs = build_abs_pose_from_components( + state[:, 0:3], + state[:, 3:6], + "euler_xyz", + ) + + # 1. Raw → kinematics: apply DEFAULT_ROTATION + # 2. TCP → flange: shift from ee_gripper_link to gripper_link + # 3. Kinematics → OpenCV convention (rotation only) + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ _DEFAULT_ROTATION.astype(poses_abs.dtype) + poses_abs = poses_abs @ _TCP_TO_FLANGE.astype(poses_abs.dtype) + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ self._to_opencv.astype(poses_abs.dtype) + + initial_pose = torch.from_numpy(poses_abs[0].copy()).float() + + translation = torch.from_numpy(poses_abs[:, :3, 3]).float() + rotation_matrix = torch.from_numpy(poses_abs[:, :3, :3]).float() + rotation = convert_rotation(rotation_matrix, input_format="matrix", output_format="rot6d").float() + + pose = torch.cat([translation, rotation], dim=-1) # [T, 9] + return torch.cat([pose, sample["action"][:, [6]]], dim=-1), initial_pose # [T, 10] + + def _compute_backward_framewise_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor]: + """Body-frame (ego-frame) delta: ``T_curr^{-1} @ T_next``. + + Matches Camera/AV ``backward_framewise`` convention. Translation is in + the current frame's local coordinate system; rotation is + ``R_curr^{-1} @ R_next``. + + Returns: + (action_tensor, initial_pose) — initial_pose is the first-frame + absolute EE pose (4×4, in the corrected OpenCV frame). + """ + states = sample["observation.state"] # (chunk_length + 1, 8) + poses_abs = build_abs_pose_from_components( + states[:, 0:3], + states[:, 3:6], + "euler_xyz", + ) + + # 1. Raw → kinematics: apply DEFAULT_ROTATION + # 2. TCP → flange: shift from ee_gripper_link to gripper_link + # 3. Kinematics → OpenCV convention (rotation only) + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ _DEFAULT_ROTATION.astype(poses_abs.dtype) + poses_abs = poses_abs @ _TCP_TO_FLANGE.astype(poses_abs.dtype) + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ self._to_opencv.astype(poses_abs.dtype) + + initial_pose = torch.from_numpy(poses_abs[0].copy()).float() + + poses_rel = pose_abs_to_rel( + poses_abs=poses_abs, + rotation_format="rot6d", + pose_convention="backward_framewise", + ) + poses_rel_tensor = torch.from_numpy(poses_rel).float() + + return torch.cat([poses_rel_tensor, sample["action"][:, [6]]], dim=-1), initial_pose + + # ------------------------------------------------------------------ + # Normalization is handled by BaseActionLeRobotDataset. + # Stats are loaded from: + # cosmos/data/vfm/action/normalizers/ + # bridge_orig_lerobot__.json + # Regenerate via ``compute_action_stats.py`` + ``debug/stats_all.sh``. + # ------------------------------------------------------------------ + + # ------------------------------------------------------------------ + # Episode filtering + # ------------------------------------------------------------------ + def _filter_valid_episodes(self, meta: LeRobotDatasetMetadata, episode_ids: list[int]) -> list[int]: + """Drop episodes whose ``tasks`` metadata is empty/whitespace. + + Narrower than the offline + ``projects/cosmos3/vfm/datasets/action/filter_bridge_dataset.py`` + (which also flags gibberish/question/non-English/patterns via + ``classify_task``). + """ + kept: list[int] = [] + dropped = 0 + for ep_id in episode_ids: + ep = meta.episodes[ep_id] + tasks = ep.get("tasks", []) + if isinstance(tasks, str): + tasks = [tasks] + has_prompt = any(t and str(t).strip() for t in (tasks or [])) + if has_prompt: + kept.append(ep_id) + else: + dropped += 1 + if dropped: + log.info(f"BridgeOrigLeRobotDataset: dropped {dropped} / {len(episode_ids)} episodes with empty prompt") + return kept + + # ------------------------------------------------------------------ + # __getitem__ + # ------------------------------------------------------------------ + + def _build_action_spec(self) -> ActionSpec: + """Bridge: 10D = ``[Pos, Rot6d, Gripper]``.""" + return build_action_spec(Pos(), Rot("rot6d"), Gripper()) + + def __getitem__(self, idx: int) -> dict[str, Any]: + """ """ + mode, _, _, sample = self._fetch_sample(idx) + + ai_caption = sample["task"] + + video = sample["observation.images.image_0"] # [T,C,H,W] + if self._pose_convention == "absolute": + action, initial_pose = self._compute_absolute_action(sample) + elif self._pose_convention == "backward_framewise": + action, initial_pose = self._compute_backward_framewise_action(sample) + else: + raise ValueError(f"Unknown pose_convention: {self._pose_convention}") + + return self._build_result( + mode=mode, video=video, action=action, ai_caption=ai_caption, initial_pose=initial_pose + ) + + @property + def action_dim(self) -> int: + """Action dimensionality: position(3) + 6D rotation(6) + gripper(1) = 10.""" + return 10 diff --git a/cosmos_training/cosmos/data/vfm/action/camera_dataset.py b/cosmos_training/cosmos/data/vfm/action/camera_dataset.py new file mode 100644 index 00000000..6a93f84a --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/camera_dataset.py @@ -0,0 +1,608 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import random +import time +from dataclasses import dataclass +from functools import partial +from typing import Callable, Literal + +import numpy as np +import torch +import torch.distributed as dist +import torchvision.transforms.functional as F +from torch.utils.data import DataLoader, IterableDataset +from torchcodec.decoders import VideoDecoder + +from imaginaire.modules.camera import Camera +from cosmos.utils import log +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.unified_dataset import wrap_dataset +from cosmos.data.vfm.joint_dataloader import custom_collate_fn +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO + +""" This load the cosmos3 camera-depth data from s3. +File structure: + +s3://nv-00-10206-robot/cosmos3_action_data//v3/ +- videos/ + - .mp4 + ... +- captions/ + - .json + ... +- cameras/ + - .json + ... +meta.json + +Each video contains N = 150 frames at 30fps. + +For multi-view dataset, the structure is slightly different: +- videos/ + - / + - .mp4 + ... + +The meta.json is like: +{ + "scenes": [ + , + ... + ] +} + +The caption json is like: +{ + "Qwen3-VL-30B-A3B-Instruct": { + "long": "...", + "short": "...", + "medium": "..." + } +} + +The camera json is like: +{ + "camera": { + "focal_length": [fx, fy] * N, + "principal_point": [cx, cy] * N, + "pose_world2cam": [qx, qy, qz, qw, tx, ty, tz] * N + } +} + +Training datasets: +- tartanair: 2245 clips at 480x640 +- endeavor_forever: 40555 clips at 720x1364 +- synhuman_20251218: 24000 clips at 1080x1920 +- drivesim: 41639 scenes (each 7 clips) at 1080x1920 +- pretrain_camera_260131_10k: 42697 clips, various resolution (most are 720x1280), various fps (most are 30). ~half of the camera is static. +- MultiCamVideo: 13600*10 clips at 1280x1280, 30fps +- SyncCamVideo: 6800*10 clips at 1280x1280, 30fps + + +Testing datasets: +- camera_benchmark: 60 clips (various resolution) +- videos_camera_benchmark: 100 clips (1280x720, various fps) +""" + + +@dataclass +class DataConfig: + bucket: str = "nv-00-10206-robot" + credential_path: str = "credentials/gcp_training.secret" + validate: bool = False + validate_num: int = 100 # for each dataset + caption_model: str = "Qwen3-VL-30B-A3B-Instruct" + + + dataset_names: str = "tartanair" + + # Total video frames including the context frame (must be 4n+1 for VAE). + # E.g. 93 = 1 context + 92 action frames. + num_frames: int = 93 + + # resolution group + resolution: str = "256" + + # action mode + mode: str = "forward_dynamics" # camera-control is forward_dynamics + embodiment_type: str = "camera_pose" + # ablation caption + + fix_caption: bool = False + fix_caption_text: str = "The camera moves in a scene." + + # ablation camera action format + rotation_format: RotationConvention = "rot9d" + pose_convention: Literal["backward_anchored", "backward_framewise"] = "backward_framewise" + + translation_scale: float = 1.0 + rotation_scale: float = 1.0 + + # If set, downsample the video to this fps. Must be <= video fps (upsampling is forbidden). + target_fps: float | None = None + + +def get_target_size_and_crop(resolution: str, current_H: int, current_W: int) -> tuple[int, int, int, int]: + """Calculates resize dimensions and crop size for smallest-side resize + center crop.""" + target_resolutions = VIDEO_RES_SIZE_INFO[resolution] + + # Find closest supported aspect ratio to minimize cropping + current_ar = current_W / current_H + best_key = "1,1" + min_diff = float("inf") + + for key in target_resolutions: + w_r, h_r = map(int, key.split(",")) + target_ar = w_r / h_r + diff = abs(current_ar - target_ar) + if diff < min_diff: + min_diff = diff + best_key = key + + target_canvas_W, target_canvas_H = target_resolutions[best_key] + + # Resize logic (ResizeSmallestSideAspectPreserving) + # We want the image to cover the target canvas completely. + scaling_ratio = max(target_canvas_W / current_W, target_canvas_H / current_H) + + new_H = int(scaling_ratio * current_H + 0.5) + new_W = int(scaling_ratio * current_W + 0.5) + + return new_H, new_W, target_canvas_H, target_canvas_W + + +class CameraDataset(IterableDataset): + def __init__(self, conf: DataConfig): + super().__init__() + + self.conf = conf + self.domain_id = get_domain_id(conf.embodiment_type) + + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": None, + "s3_credential_path": self.conf.credential_path, + } + ) + + self.uids = [] + dataset_names = conf.dataset_names.split(",") + for dataset_name in dataset_names: + path_uids = f"s3://{conf.bucket}/cosmos3_action_data/{dataset_name}/v3/meta.json" + uids = easy_io.load(path_uids)["scenes"] # list of uids + + # for benchmark dataset, do not split + if dataset_name not in ["camera_benchmark", "videos_camera_benchmark"]: + # train/test split + assert self.conf.validate_num > 0 and len(uids) >= self.conf.validate_num + stride = len(uids) // self.conf.validate_num + val_indices = {i * stride for i in range(self.conf.validate_num)} + if self.conf.validate: + uids = [uids[i] for i in sorted(val_indices)] + else: + uids = [uids[i] for i in range(len(uids)) if i not in val_indices] + + for uid in uids: + self.uids.append((dataset_name, uid)) + + log.warning(f"Loaded {len(self.uids)} uids from {conf.dataset_names}") + + def __len__(self): + return len(self.uids) + + def __iter__(self): + if self.conf.validate: + for i in range(len(self.uids)): + sample = self.load_data(self.uids[i]) + if sample is None: + continue + else: + yield sample + else: + # infinite random loop for training + while True: + indices = np.random.permutation(len(self.uids)) + for i in indices: + sample = self.load_data(self.uids[i]) + if sample is None: + continue + else: + yield sample + + def load_data(self, uid: str) -> dict | None: + """Load and preprocess a single sample. + For multi-view dataset, we randomly load one view at each iteration. + + Args: + uid: Unique identifier for the sample (e.g., 'westerndesert_Hard_P007') + + Returns: + Dictionary with video, action, and metadata, or None if loading fails. + """ + try: + dataset_name, sample_name = uid + + if dataset_name == "drivesim": + # multi-view dataset + view_names = [ + "camera_bev_from_behind_ego", + "camera_bev_looking_back_at_ego", + "camera_crossleft", + "camera_crossright", + "camera_frontwide", + "camera_rearleft", + "camera_rearright", + ] + view_name = random.choice(view_names) if not self.conf.validate else view_names[0] + sample_name = os.path.join(sample_name, view_name) + elif dataset_name in ["MultiCamVideo", "SyncCamVideo"]: + # multi-view dataset + view_names = [ + "cam01", + "cam02", + "cam03", + "cam04", + "cam05", + "cam06", + "cam07", + "cam08", + "cam09", + "cam10", + ] + view_name = random.choice(view_names) if not self.conf.validate else view_names[0] + sample_name = os.path.join(sample_name, view_name) + + video_path = f"s3://{self.conf.bucket}/cosmos3_action_data/{dataset_name}/v3/videos/{sample_name}.mp4" + camera_path = f"s3://{self.conf.bucket}/cosmos3_action_data/{dataset_name}/v3/cameras/{sample_name}.json" + caption_path = f"s3://{self.conf.bucket}/cosmos3_action_data/{dataset_name}/v3/captions/{sample_name}.json" + + # Load video bytes from S3 + video_bytes = easy_io.get(video_path) + + decoder = VideoDecoder(video_bytes, num_ffmpeg_threads=4) + del video_bytes + total_frames = decoder.metadata.num_frames + video_fps = decoder.metadata.average_fps + + # Determine temporal stride for fps downsampling + if self.conf.target_fps is not None: + if self.conf.target_fps > video_fps: + raise ValueError( + f"target_fps ({self.conf.target_fps}) > video_fps ({video_fps}). Upsampling is not supported." + ) + stride = round(video_fps / self.conf.target_fps) + effective_fps = video_fps / stride + else: + stride = 1 + effective_fps = video_fps + + # Sample consecutive frames + if self.conf.num_frames == -1: + # Load all available frames, aligned to VAE constraint (1 + 4*N total frames) + num_strided_frames = (total_frames - 1) // stride + 1 + N = (num_strided_frames - 1) // 4 + if N < 1: + log.warning(f"Not enough frames for {uid}, total_frames: {total_frames}, stride: {stride}") + return None + num_frames_to_load = 1 + 4 * N + start_idx = 0 + else: + num_frames_to_load = self.conf.num_frames + num_raw_frames_needed = (num_frames_to_load - 1) * stride + 1 + + if total_frames < num_raw_frames_needed: + log.warning( + f"Not enough frames to load for {uid}, total_frames: {total_frames}, num_raw_frames_needed: {num_raw_frames_needed}" + ) + return None + + # Random start index for consecutive frame sampling + if self.conf.validate: + start_idx = 0 + else: + start_idx = random.randint(0, total_frames - num_raw_frames_needed) + + num_raw_frames_needed = (num_frames_to_load - 1) * stride + 1 + frame_indices = list(range(start_idx, start_idx + num_raw_frames_needed, stride)) + + # torchcodec returns [T,C,H,W] tensor + frame_batch = decoder.get_frames_at(frame_indices) + video_frames = frame_batch.data # [T,C,H,W] uint8 + del decoder + + # Get target size and crop params + T, C, H, W = video_frames.shape + + # temp: for pretrain_camera_260131_10k, we only use videos with at 720x1280 to enable batched training + if dataset_name == "pretrain_camera_260131_10k": + assert H == 720 and W == 1280, f"Expected resolution 720x1280, got {H}x{W}" + + new_H, new_W, target_canvas_H, target_canvas_W = get_target_size_and_crop(self.conf.resolution, H, W) + + # Resize if needed + if new_H != H or new_W != W: + video_frames = F.resize( + video_frames, [new_H, new_W], interpolation=F.InterpolationMode.BICUBIC, antialias=True + ) + + # Center Crop + if new_H != target_canvas_H or new_W != target_canvas_W: + video_frames = F.center_crop(video_frames, [target_canvas_H, target_canvas_W]) + + # Convert to (C, T, H, W) format expected by model + video = video_frames.permute(1, 0, 2, 3) # [C,T,H,W] + + # Load camera data + camera_data = easy_io.load(camera_path) + w2c = np.array(camera_data["camera"]["pose_world2cam"]).reshape(-1, 7) # [N,7] + assert w2c.shape[0] == total_frames, f"Expected {total_frames} poses, got {w2c.shape[0]}" + + # Get w2c for the sampled frames (same stride as video frames) + w2c = w2c[start_idx : start_idx + num_raw_frames_needed : stride] # [num_frames,7] + # Convert (qx,qy,qz,qw,tx,ty,tz) to [R|t] matrices + w2c = Camera.extrinsic_params_to_matrices(w2c) # [num_frames,3,4] + w2c_homo = np.eye(4, dtype=np.float32)[None, :, :].repeat(w2c.shape[0], axis=0) # [num_frames,4,4] + w2c_homo[:, :3, :] = w2c + c2w_homo = np.linalg.inv(w2c_homo) + + # Determine mode + if self.conf.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy"]) + else: + mode = self.conf.mode + + # Using c2w_homo because pose_abs_to_rel expects c2w poses + # The function handles w2c/c2w/anchored logic based on pose_convention + action = pose_abs_to_rel( + c2w_homo, + rotation_format=self.conf.rotation_format, + pose_convention=self.conf.pose_convention, + translation_scale=self.conf.translation_scale, + rotation_scale=self.conf.rotation_scale, + ) + action = torch.from_numpy(action) # [num_frames-1,action_dim] + + # Load caption data + if self.conf.fix_caption: + caption = self.conf.fix_caption_text + else: + caption_data = easy_io.load(caption_path) + captions = caption_data[self.conf.caption_model] + # Randomly select from long/short/medium captions + if self.conf.validate: + caption = captions["long"] + else: + selected_type = random.choice(["long", "short", "medium"]) + caption = captions[selected_type] + + # FPS (must be scalar fps, not [fps]) + fps = torch.tensor(effective_fps, dtype=torch.float32) # scalar + + # Build sample dict matching dummy_dataset format + sample = { + "video": video, # [C,num_frames,H,W] uint8 + "action": action, # [num_frames-1,max_action_dim] float32 + "conditioning_fps": fps, # scalar float32 + "ai_caption": caption, + "mode": mode, + "__key__": torch.tensor([hash(uid) % (2**31)], dtype=torch.long), + "domain_id": torch.tensor(self.domain_id, dtype=torch.long), + "viewpoint": "ego_view", + } + return sample + + except Exception as e: + log.warning(f"Error loading {uid}: {e}") + return None # skip this sample + + +# to align with other robot datasets interface. +class CameraDatasetPatched(CameraDataset): + def __init__( + self, + dataset_names: str, + split: Literal["train", "val"] = "train", + num_frames: int = 93, + resolution: str = "256", + mode: str = "forward_dynamics", + fix_caption: bool = False, + rotation_format: RotationConvention = "rot9d", + pose_convention: Literal["backward_anchored", "backward_framewise"] = ("backward_framewise"), + translation_scale: float = 1.0, + rotation_scale: float = 1.0, + shuffle: bool = False, # not used, decided by split + ): + super().__init__( + conf=DataConfig( + dataset_names=dataset_names, + validate=split == "val", + num_frames=num_frames, + resolution=resolution, + mode=mode, + fix_caption=fix_caption, + rotation_format=rotation_format, + pose_convention=pose_convention, + translation_scale=translation_scale, + rotation_scale=rotation_scale, + ) + ) + self.dataset_names = dataset_names + self.split = split + self.num_frames = num_frames + self.resolution = resolution + self.mode = mode + self.fix_caption = fix_caption + self.rotation_format = rotation_format + self.pose_convention = pose_convention + self.translation_scale = translation_scale + self.rotation_scale = rotation_scale + + +def worker_init_fn(worker_id: int, seed: int = 0): + try: + rank = dist.get_rank() + except Exception: + rank = 0 + seed = seed + rank * 100000 + worker_id + np.random.seed(seed) + torch.manual_seed(seed) + random.seed(seed) + + +class MixedDataLoader: + def __init__(self, dataloaders: list[DataLoader], ratios: list[float]): + self.dataloaders = dataloaders + self.ratios = ratios + + def __len__(self) -> int: + return sum(len(dl.dataset) for dl in self.dataloaders) + + def __iter__(self): + iterators = [iter(dl) for dl in self.dataloaders] + while True: + choice = np.random.choice(len(self.dataloaders), p=self.ratios) + yield next(iterators[choice]) + + +def get_camera_dataloader( + conf: dict[str, DataConfig], + ratios: list[float] | None = None, + batch_size: int = 1, + num_workers: int = 8, + seed: int | None = None, + # wrap_dataset parameters for Action transform pipeline + resolution: str | None = None, + tokenizer_config: dict | None = None, + cfg_dropout_rate: float = 0.0, + max_action_dim: int = 32, + collate_fn: Callable = custom_collate_fn, +): + # random seed to make sure each training has different data order + if seed is None: + seed = random.randint(0, 1000000) + + # num_workers == 0 will not call worker_init_fn, so we need to seed the main process directly + if num_workers == 0: + try: + rank = dist.get_rank() + except Exception: + rank = 0 + rank_seed = seed + rank * 100000 + np.random.seed(rank_seed) + torch.manual_seed(rank_seed) + random.seed(rank_seed) + + # filter out None configs (e.g. when overriding) + conf = {k: v for k, v in conf.items() if v is not None} + + if len(conf) > 1: + dataloaders = [] + dataset_lengths = [] + + for i, dataset_name in enumerate(conf.keys()): + # dataset wrapped with Action transform pipeline + dataset = CameraDataset(conf=conf[dataset_name]) + dataset_lengths.append(len(dataset)) + wrapped = wrap_dataset( + dataset, + resolution=resolution, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + max_action_dim=max_action_dim, + ) + + # loader + loader = DataLoader( + dataset=wrapped, + batch_size=batch_size, + num_workers=num_workers, + pin_memory=True, + worker_init_fn=partial(worker_init_fn, seed=seed), + collate_fn=collate_fn, + ) + dataloaders.append(loader) + + # calculate ratios if not provided + if ratios is None: + total_len = sum(dataset_lengths) + ratios = [l / total_len for l in dataset_lengths] + + log.info(f"MixedDataset sizes: {dataset_lengths}, ratios: {ratios}") + + result = MixedDataLoader(dataloaders, ratios) + else: + # dataset wrapped with Action transform pipeline + dataset = CameraDataset(conf=list(conf.values())[0]) + wrapped = wrap_dataset( + dataset, + resolution=resolution, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + max_action_dim=max_action_dim, + ) + + # loader + result = DataLoader( + dataset=wrapped, + batch_size=batch_size, + num_workers=num_workers, + pin_memory=True, + worker_init_fn=partial(worker_init_fn, seed=seed), + collate_fn=collate_fn, + ) + + log.info(f"Dataset size: {len(dataset)}") + + return result + + +# PYTHONPATH=. python cosmos/data/vfm/action/camera_dataset.py +if __name__ == "__main__": + import torchvision.io as io + + dataset = CameraDataset(conf=DataConfig(dataset_names="MultiCamVideo", validate=False, resolution="480")) + dataset_iter = iter(dataset) + + for i in range(3): + print(f"==================== Sample {i} ====================") + _t0 = time.time() + data = next(dataset_iter) + _t1 = time.time() + print(f"{'Loading time':<25}: {_t1 - _t0:.2f}s") + + print(f"==================== Sample {i} ====================") + print(f"{'video shape':<25}: {data['video'].shape}") # [C,T,H,W] + print(f"{'action shape':<25}: {data['action'].shape}") # [T,max_action_dim] + print(f"{'conditioning_fps':<25}: {data['conditioning_fps'].item()}") + print(f"{'mode':<25}: {data['mode']}") + print(f"{'domain_id':<25}: {data['domain_id'].item()}") + print(f"{'caption':<25}: {data['ai_caption']}...") + + # save video to local for debugging + video = data["video"] + video = video.permute(1, 0, 2, 3) # [T,C,H,W] + video_path = f"temp/camera_sample_{i}.mp4" + io.write_video( + video_path, video.permute(0, 2, 3, 1).numpy(), fps=data["conditioning_fps"].item() + ) # expects (T, H, W, C) + print(f"Saved video to {video_path}") diff --git a/cosmos_training/cosmos/data/vfm/action/camera_dataset_sharded.py b/cosmos_training/cosmos/data/vfm/action/camera_dataset_sharded.py new file mode 100644 index 00000000..89aa631a --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/camera_dataset_sharded.py @@ -0,0 +1,665 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sharded Camera Dataset for Action training. + +Example data structure: + +wdinfo: +s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/ + tartanair/v3/resolution_480/aspect_ratio_5_4/duration_150/wdinfo_02132026.json + +tarfiles: +s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset/ + tartanair/v3/resolution_480/aspect_ratio_5_4/duration_150/ + video/ + part_000000.tar + part_000001.tar + ... + camera/ + part_000000.tar + part_000001.tar + ... + metas/ + part_000000.tar + part_000001.tar + ... +""" + +import io +import json +import os +import random +import re +import tarfile +from typing import Iterator, Literal + +import numpy as np +import torch +from torch.utils.data import IterableDataset +from torchcodec.decoders import VideoDecoder + +from imaginaire.modules.camera import Camera +from cosmos.utils import log +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + pose_abs_to_rel, +) + +# all ready-to-use wdinfos +CAMERA_WDINFOS = { + # ----- 5s datasets ----- + "tartanair_256": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/tartanair/v1/resolution_256/aspect_ratio_5_4/duration_150/wdinfo_03022026.json", + "tartanair_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/tartanair/v1/resolution_544/aspect_ratio_23_17/duration_150/wdinfo_03022026.json", + "endeavor_forever_256": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/endeavor_forever/v1/resolution_192/aspect_ratio_5_3/duration_150/wdinfo_03032026.json", + "endeavor_forever_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/endeavor_forever/v1/resolution_480/aspect_ratio_26_15/duration_150/wdinfo_03032026.json", + "synhuman_20251218_256": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/synhuman_20251218/v1/resolution_192/aspect_ratio_5_3/duration_150/wdinfo_03032026.json", + "synhuman_20251218_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/synhuman_20251218/v1/resolution_480/aspect_ratio_26_15/duration_150/wdinfo_03032026.json", + "pretrained_clips_260131_10k_256": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260131_10k/v3/resolution_192/aspect_ratio_5_3/duration_150/wdinfo_02172026.json", + "pretrained_clips_260131_10k_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260131_10k/v3/resolution_480/aspect_ratio_26_15/duration_150/wdinfo_02172026.json", + "synhuman_20260223_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/synhuman_20260223/v1/resolution_480/aspect_ratio_26_15/duration_150/wdinfo_03032026.json", + # ----- 10s datasets ----- + # pretrain video 100k (mixed resolution, 193k samples) + "pretrained_clips_260307_100k": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260307_100k/v1p1/", + # pretrain video 100k filtered (mixed resolution, 72k samples) + "pretrained_clips_260307_100k_filtered": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260307_100k_filtered/v1p1/", + "pretrained_clips_260325_500k_01_filtered": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260325_500k_01_filtered/v1p1/", + "pretrained_clips_260325_500k_02_filtered": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260325_500k_02_filtered/v1p1/", + "pretrained_clips_260313_10s_100k_filtered": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260313_10s_100k_filtered/v1p1/", + # ----- 60s datasets ----- + # 60s duration + "endeavor_forever_480_60s": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/endeavor_forever/v1p2/resolution_480/aspect_ratio_26_15/duration_1800/wdinfo_03032026.json", # only 1798 frames actually! + "drivesim_480_60s": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/drivesim/v1p2/resolution_480/aspect_ratio_26_15/duration_1800/wdinfo_03042026.json", # 1749 frames + # pretrain video 100k 59s filtered (mixed resolution, 16k samples) + "pretrained_clips_260313_59s_100k_filtered": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260313_59s_100k_filtered/v1p2/", + "pretrained_clips_260313_59s_100k_filtered_480": "s3://nv-00-10206-robot/cosmos3_action_data/camera_webdataset_wdinfo/pretrained_clips_260313_59s_100k_filtered/v1p2/resolution_480/aspect_ratio_16_9/frames_1700_1800/wdinfo_03242026.json", +} + + +class CameraDatasetSharded(IterableDataset): + def __init__( + self, + wdinfo_paths: list[str] | None = None, + bucket: str = "nv-00-10206-robot", + credential_path: str = "credentials/gcp_training.secret", + mode: str = "forward_dynamics", + embodiment_type: str = "camera_pose", + split: str = "train", + seed: int = 0, + shuffle: bool = True, + rotation_format: RotationConvention = "rot6d", + pose_convention: Literal["backward_anchored", "backward_framewise"] = ("backward_framewise"), + fix_caption: bool = True, # fix caption by default + fix_caption_text: str = "The camera moves in a scene.", + translation_scale: float = 1.0, + rotation_scale: float = 1.0, + discard_varying_intrinsics: bool = False, + # wdinfo filtering + wdinfo_resolution: Literal["all", "gt480", "gt720"] = "all", + max_frames: int = -1, # only truncate if exceed max_frames + num_frames: int = -1, # always truncate to num_frames + # When True, use a separate domain ID for inverse dynamics / policy modes + # so that DomainAwareLinear learns different projections for anchored (conditioning) + # vs framewise (generation) action representations. + mode_aware_domain: bool = False, + inv_embodiment_type: str = "camera_pose_inv", + max_action_translation_norm: float | None = None, + # When True, decode the full video in one pass (used by KV-cache segment loaders). + whole_video: bool = False, + # Benchmark datasets: list of S3 dataset names for loading individual files + # instead of tarfiles. Activated automatically when split is val-like. + benchmark_datasets: list[str] | None = ("videos_camera_benchmark",), + # Caption model key used in the benchmark caption JSON files. + benchmark_caption_model: str = "Qwen3-VL-30B-A3B-Instruct", + ): + super().__init__() + + self.wdinfo_paths = wdinfo_paths or [] + self.bucket = bucket + self.credential_path = credential_path + self.mode = mode + self.split = split.lower().strip() + self.seed = seed + self.shuffle = shuffle + self.rotation_format: RotationConvention = rotation_format + self.pose_convention: Literal["absolute", "backward_anchored", "backward_framewise"] = pose_convention + + # hard-coded caption keys and weights for now + caption_keys = { + "qwen2p5_7b_caption": 0.7, + "qwen2p5_7b_caption_short": 0.1, + "qwen2p5_7b_caption_medium": 0.2, + } + self.caption_keys = list(caption_keys.keys()) + self.caption_weights = list(caption_keys.values()) + + self.fix_caption = fix_caption + self.fix_caption_text = fix_caption_text + self.translation_scale = translation_scale + self.rotation_scale = rotation_scale + self.discard_varying_intrinsics = discard_varying_intrinsics + self.max_action_translation_norm = max_action_translation_norm + self.wdinfo_resolution = wdinfo_resolution + self.max_frames = max_frames + self.num_frames = num_frames + self.whole_video = whole_video + # Get domain ID for this embodiment + self.domain_id = get_domain_id(embodiment_type) + # When mode_aware_domain is True, inverse_dynamics/policy modes use a separate domain ID + self.mode_aware_domain = mode_aware_domain + self.domain_id_inv = get_domain_id(inv_embodiment_type) if mode_aware_domain else self.domain_id + self.benchmark_caption_model = benchmark_caption_model + + # Validate mode + valid_modes = ["joint", "forward_dynamics", "inverse_dynamics", "policy", "image2video"] + if mode not in valid_modes: + raise ValueError(f"mode must be one of {valid_modes}, got {mode}") + + # Validate split + if self.split not in {"train", "val", "valid", "validation", "full"}: + raise ValueError(f"Unsupported {split=}. Use train/val/full.") + + # Configure S3 backend using easy_io + self._setup_s3_backend() + + # Validation always uses benchmark datasets + self._benchmark_uids: list[tuple[str, str]] = [] + if self.split in {"val", "valid", "validation"}: + if not benchmark_datasets: + raise ValueError("benchmark_datasets is required for validation splits.") + self._load_benchmark_uids(benchmark_datasets) + + # Load tar files from wdinfo.json (training / full splits only) + self._tar_files: list[str] = [] + self._key_count: list[int] = [] + self._total_key_count: int = 0 + + if self._benchmark_uids: + log.info( + f"Initialized CameraDatasetSharded (benchmark): " + f"benchmark_datasets={benchmark_datasets}, " + f"mode={mode}, split={self.split}, " + f"num_samples={len(self._benchmark_uids)}" + ) + else: + if not self.wdinfo_paths: + raise ValueError("Must provide wdinfo_paths for training.") + + # Resolve prefix paths into individual wdinfo JSON paths + self.wdinfo_paths = self._resolve_wdinfo_paths(self.wdinfo_paths) + + # Filter wdinfo paths by resolution + self.wdinfo_paths = self._filter_wdinfo_by_resolution(self.wdinfo_paths) + + self._load_wdinfo() + + log.info( + f"Initialized CameraDatasetSharded: wdinfo_paths={self.wdinfo_paths}, " + f"mode={mode}, split={self.split}, " + f"num_tar_files={len(self._tar_files)}, " + f"total_samples={self._total_key_count}" + ) + + def _setup_s3_backend(self) -> None: + """Configure the easy_io S3 backend. Called in __init__ and __iter__ for worker processes.""" + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": None, + "s3_credential_path": self.credential_path, + } + ) + + def _load_benchmark_uids(self, benchmark_datasets: list[str]) -> None: + """Load UIDs from benchmark datasets stored as individual S3 files. + + Each benchmark dataset lives at ``s3:///cosmos3_action_data//v3/`` + with a ``meta.json`` listing scene UIDs and per-UID ``videos/``, ``cameras/``, + ``captions/`` directories. + """ + for dataset_name in benchmark_datasets: + meta_path = f"s3://{self.bucket}/cosmos3_action_data/{dataset_name}/v3/meta.json" + try: + uids = json.loads(easy_io.get(meta_path))["scenes"] + except Exception as e: + raise RuntimeError(f"Failed to load benchmark meta from {meta_path}: {e}") from e + for uid in uids: + self._benchmark_uids.append((dataset_name, uid)) + log.info(f"Loaded {len(uids)} benchmark UIDs from {dataset_name}") + + def _resolve_wdinfo_paths(self, wdinfo_paths: list[str]) -> list[str]: + """Resolve wdinfo paths: direct .json paths are kept as-is, + prefix/directory paths are walked recursively to find all .json files.""" + resolved: list[str] = [] + for path in wdinfo_paths: + if path.endswith(".json"): + resolved.append(path) + else: + log.info(f"Walking S3 prefix to discover wdinfo JSONs: {path}") + relative_json_files = list( + easy_io.list_dir_or_file(path, list_dir=False, list_file=True, suffix=".json", recursive=True) + ) + if not relative_json_files: + raise RuntimeError(f"No .json wdinfo files found under prefix: {path}") + prefix = path.rstrip("/") + "/" + for rel_path in sorted(relative_json_files): + resolved.append(prefix + rel_path) + log.info(f"Discovered {len(relative_json_files)} wdinfo files under {path}") + return resolved + + def _filter_wdinfo_by_resolution(self, wdinfo_paths: list[str]) -> list[str]: + """Filter resolved wdinfo paths by minimum resolution. + + Resolution is extracted from the path pattern ``resolution_``. + For ``wdinfo_resolution="gtXXX"``, only paths with resolution >= XXX are kept. + """ + if self.wdinfo_resolution == "all": + return wdinfo_paths + + threshold_match = re.match(r"gt(\d+)", self.wdinfo_resolution) + if not threshold_match: + raise ValueError( + f"Invalid wdinfo_resolution format: {self.wdinfo_resolution!r}. " + "Expected 'all' or 'gtNNN' (e.g. 'gt480')." + ) + min_resolution = int(threshold_match.group(1)) + + filtered: list[str] = [] + for path in wdinfo_paths: + res_match = re.search(r"resolution_(\d+)", path) + if res_match: + resolution = int(res_match.group(1)) + if resolution >= min_resolution: + filtered.append(path) + else: + log.warning(f"No resolution found in wdinfo path, including by default: {path}") + filtered.append(path) + + log.info(f"Filtered wdinfo by resolution >= {min_resolution}: {len(filtered)}/{len(wdinfo_paths)} paths kept") + if not filtered: + raise RuntimeError(f"All wdinfo paths were filtered out by wdinfo_resolution={self.wdinfo_resolution!r}") + return filtered + + def _load_wdinfo(self) -> None: + self._tar_files = [] + self._key_count = [] + self._total_key_count = 0 + + for wdinfo_path in self.wdinfo_paths: + # log.info(f"Loading wdinfo from: {wdinfo_path}") + + try: + wdinfo_bytes = easy_io.get(wdinfo_path) + wdinfo = json.loads(wdinfo_bytes) + except Exception as e: + raise RuntimeError(f"Failed to load wdinfo from {wdinfo_path}: {e}") from e + + # Extract metadata + data_root = wdinfo["root"].rstrip( + "/" + ) # e.g. cosmos3_action_data/camera_webdataset/tartanair/v3/resolution_256/aspect_ratio_5_4/duration_150/ + data_list = wdinfo["data_list"] # ["part_000000.tar", "part_000001.tar", ...] + key_count = wdinfo["data_list_key_count"] # [20, 20, ...] + + # only for video tar files + tar_paths = [f"s3://{self.bucket}/{data_root}/video/{tar_path}" for tar_path in data_list] + + # Reconstruct full S3 paths for tar files + self._tar_files.extend(tar_paths) + self._key_count.extend(key_count) + + # Accumulate total sample count + self._total_key_count += sum(key_count) + + # log.info(f"Loaded {len(data_list)} tar files from wdinfo with {sum(key_count)} samples") + + if not self._tar_files: + raise RuntimeError(f"No tar files found in wdinfo at {self.wdinfo_paths}") + + def __len__(self) -> int: + if self._benchmark_uids: + return len(self._benchmark_uids) + return self._total_key_count + + def __iter__(self) -> Iterator[dict]: + """Iterate over the dataset.""" + # Re-configure S3 backend in case this is running in a worker process after unpickling + self._setup_s3_backend() + + if self._benchmark_uids: + yield from self._iter_benchmark() + else: + yield from self._iter_tar_files() + + def _iter_benchmark(self) -> Iterator[dict]: + """Iterate over benchmark datasets (individual S3 files).""" + uids = list(self._benchmark_uids) + if self.shuffle: + random.shuffle(uids) + + skipped: list[dict[str, str]] = [] + yielded = 0 + + for dataset_name, uid in uids: + try: + base = f"s3://{self.bucket}/cosmos3_action_data/{dataset_name}/v3" + video_bytes = easy_io.get(f"{base}/videos/{uid}.mp4") + camera_bytes = easy_io.get(f"{base}/cameras/{uid}.json") + + # Load caption from per-sample JSON (different format from tarfile metas) + caption = None + if not self.fix_caption: + caption_data = json.loads(easy_io.get(f"{base}/captions/{uid}.json")) + captions = caption_data[self.benchmark_caption_model] + caption = random.choice([captions["long"], captions["short"], captions["medium"]]) + + sample = self._process_sample(video_bytes, camera_bytes, uid=uid, caption_override=caption) + if sample: + yielded += 1 + yield sample + else: + log.warning(f"SKIPPED benchmark sample {dataset_name}/{uid}: _process_sample returned None") + skipped.append({"dataset": dataset_name, "uid": uid, "reason": "process_sample_returned_none"}) + + except Exception as e: + log.warning(f"SKIPPED benchmark sample {dataset_name}/{uid}: S3 load failed: {e}") + skipped.append({"dataset": dataset_name, "uid": uid, "reason": f"s3_load_error: {e}"}) + continue + + log.info(f"Benchmark iteration complete: {yielded}/{len(uids)} samples yielded, {len(skipped)} skipped") + if skipped: + log.warning(f"Skipped benchmark samples: {skipped}") + self._last_skipped_benchmark_samples = skipped + + def _iter_tar_files(self) -> Iterator[dict]: + """Iterate over sharded tar files from S3.""" + tar_files = list(self._tar_files) + if self.shuffle: + random.shuffle(tar_files) + + for tar_path in tar_files: + try: + # Read tar file bytes using easy_io + metas_path = tar_path.replace("/video/", "/metas/") + camera_path = tar_path.replace("/video/", "/camera/") + + video_bytes = easy_io.get(tar_path) + metas_bytes = easy_io.get(metas_path) + camera_bytes = easy_io.get(camera_path) + + with ( + tarfile.open(fileobj=io.BytesIO(video_bytes), mode="r") as video_tar, + tarfile.open(fileobj=io.BytesIO(metas_bytes), mode="r") as metas_tar, + tarfile.open(fileobj=io.BytesIO(camera_bytes), mode="r") as camera_tar, + ): + # Map filenames to members for fast lookup + camera_members = {m.name: m for m in camera_tar.getmembers() if m.isfile()} + metas_members = {m.name: m for m in metas_tar.getmembers() if m.isfile()} + + # Iterate over video files + for video_member in video_tar.getmembers(): + uuid = os.path.splitext(os.path.basename(video_member.name))[0] + json_name = f"{uuid}.json" + + if json_name not in camera_members or json_name not in metas_members: + raise ValueError(f"Missing metadata or camera for {uuid} in {tar_path}") + + # Extract data + video_file_bytes = video_tar.extractfile(video_member).read() + camera_file_bytes = camera_tar.extractfile(camera_members[json_name]).read() + metas_file_bytes = metas_tar.extractfile(metas_members[json_name]).read() + + sample = self._process_sample( + video_file_bytes, + camera_file_bytes, + uid=uuid, + metas_bytes=metas_file_bytes, + ) + if sample: + yield sample + # DEBUG: only use 1 video per tar + # break + + except Exception as e: + log.warning(f"Failed to read tar file {tar_path}: {e}") + continue + + def _process_sample( + self, + video_bytes: bytes, + camera_bytes: bytes, + uid: str = "", + *, + metas_bytes: bytes | None = None, + caption_override: str | None = None, + ) -> dict | None: + """Decode video frames, compute relative actions, and return a sample dict. + + When ``whole_video`` is True or ``num_frames == -1``, loads the full video from frame 0. + Otherwise applies ``max_frames`` / ``num_frames`` clipping and sampling as in the base + sharded loader. For the validation split, random clips use start index 0. + + Args: + caption_override: When provided, use this caption directly instead of + extracting from *metas_bytes*. Used by the benchmark data path where + captions live in a different JSON format. + """ + try: + decoder = VideoDecoder(video_bytes, num_ffmpeg_threads=4) + total_frames = decoder.metadata.num_frames or 0 + video_fps = decoder.metadata.average_fps or 0.0 + whole_video = self.whole_video + + if whole_video: + if total_frames == 0: + del decoder + log.warning(f"SKIPPED {uid}: whole_video=True but total_frames=0") + return None + frame_indices = list(range(total_frames)) + elif self.max_frames > 0: + # Use all frames at native fps (stride=1), capped by max_frames. + # VAE compresses temporal dim by 4x with 1 condition frame, + # so total video frames must be 1 + 4*N. + num_video_frames = min(total_frames, self.max_frames) + N = (num_video_frames - 1) // 4 + num_video_frames = 1 + 4 * N + if num_video_frames < 2: + del decoder + log.warning( + f"SKIPPED {uid}: too few frames after VAE alignment " + f"(total_frames={total_frames}, max_frames={self.max_frames}, aligned={num_video_frames})" + ) + return None + frame_indices = list(range(num_video_frames)) + else: + # No max_frames cap — use all frames, but still VAE-align to 1 + 4*N. + N = (total_frames - 1) // 4 + num_video_frames = 1 + 4 * N + if num_video_frames < 2: + del decoder + log.warning( + f"SKIPPED {uid}: too few frames after VAE alignment " + f"(total_frames={total_frames}, aligned={num_video_frames})" + ) + return None + frame_indices = list(range(num_video_frames)) + + # If num_frames is set, always sample exactly num_frames from the available frames. + if self.num_frames > 0 and not whole_video: + available = len(frame_indices) + N = (self.num_frames - 1) // 4 + target_frames = 1 + 4 * N + if target_frames < 2 or available < target_frames: + del decoder + log.warning( + f"SKIPPED {uid}: not enough frames for num_frames={self.num_frames} " + f"(available={available}, target={target_frames})" + ) + return None + max_start = available - target_frames + start = 0 if self.split != "train" else (random.randint(0, max_start) if max_start > 0 else 0) + frame_indices = frame_indices[start : start + target_frames] + + # torchcodec returns [T,C,H,W] tensor + frame_batch = decoder.get_frames_at(frame_indices) + video_frames = frame_batch.data # [T,C,H,W] uint8 + del decoder + + # Convert to [C,T,H,W] format expected by model + video = video_frames.permute(1, 0, 2, 3) # [T,C,H,W] -> [C,T,H,W] + + # Load camera data + camera_data = json.loads(camera_bytes) + + # Special check for varying intrinsics (e.g., in synhuman_20260223 dataset) + # If the intrinsic (fx, fy, cx, cy) changes during different frames, discard this sample. + fl = np.array(camera_data["camera"]["focal_length"]) + pp = np.array(camera_data["camera"]["principal_point"]) + if self.discard_varying_intrinsics and ( + not np.allclose(fl, fl[0], atol=1e-5) or not np.allclose(pp, pp[0], atol=1e-5) + ): + log.warning(f"SKIPPED {uid}: varying intrinsics detected (discard_varying_intrinsics=True)") + return None + + w2c = np.array(camera_data["camera"]["pose_world2cam"]).reshape(-1, 7) # [N,7] + if w2c.shape[0] <= frame_indices[-1]: + log.warning( + f"SKIPPED {uid}: not enough camera poses " + f"(num_poses={w2c.shape[0]}, last_frame_idx={frame_indices[-1]})" + ) + return None + + # Get w2c for the sampled frames + w2c = w2c[frame_indices] # [T,7] + + # Convert (qx,qy,qz,qw,tx,ty,tz) to [R|t] matrices + w2c = Camera.extrinsic_params_to_matrices(w2c) # [T,3,4] + w2c_homo = np.eye(4, dtype=np.float32)[None, :, :].repeat(w2c.shape[0], axis=0) # [T,4,4] + w2c_homo[:, :3, :] = w2c + c2w_homo = np.linalg.inv(w2c_homo) + + # Determine mode + if self.mode == "joint": + mode = random.choices( + ["forward_dynamics", "inverse_dynamics", "policy"], + weights=[0.8, 0.1, 0.1], + k=1, + )[0] + else: + mode = self.mode + + action = pose_abs_to_rel( + c2w_homo, + rotation_format=self.rotation_format, + pose_convention=self.pose_convention, + translation_scale=self.translation_scale, + rotation_scale=self.rotation_scale, + ) + + if self.max_action_translation_norm is not None and self.split == "train": + trans_norms = np.linalg.norm(action[:, :3], axis=1) + if trans_norms.max() > self.max_action_translation_norm: + log.warning( + f"SKIPPED {uid}: action translation norm too large " + f"(max={trans_norms.max():.4f}, threshold={self.max_action_translation_norm})" + ) + return None + + action = torch.from_numpy(action) # [num_frames,action_dim] + + # Load caption data + if self.fix_caption: + caption = self.fix_caption_text + elif caption_override is not None: + caption = caption_override + else: + metas_data = json.loads(metas_bytes) + + # Example: "t2w_windows": [{"end_frame": 150, "qwen2p5_7b_caption": "...", ...}] + t2w_windows = metas_data["t2w_windows"] + window = t2w_windows[0] + caption_key = random.choices(self.caption_keys, weights=self.caption_weights, k=1)[0] + caption = window[caption_key] + + # FPS + fps = torch.tensor(video_fps, dtype=torch.float32) # scalar + + # Select domain ID: use inverse domain for generation modes when mode_aware_domain is on + if self.mode_aware_domain and mode in ["inverse_dynamics", "policy"]: + domain_id = self.domain_id_inv + else: + domain_id = self.domain_id + + # Build sample dict + sample = { + "video": video, # [C,T,H,W] uint8 + "action": action, # [T-1,action_dim] float32 + "conditioning_fps": fps, # scalar float32 + "ai_caption": caption, + "mode": mode, + "__key__": torch.tensor([hash(uid) % (2**31)], dtype=torch.long), + "domain_id": torch.tensor(domain_id, dtype=torch.long), + "viewpoint": "ego_view", + } + return sample + + except Exception as e: + log.warning(f"Error processing sample {uid}: {e}") + return None + + +# PYTHONPATH=. python cosmos/data/vfm/action/camera_dataset_sharded.py +if __name__ == "__main__": + import time + + import torchvision + + dataset = CameraDatasetSharded( + wdinfo_paths=[ + CAMERA_WDINFOS["pretrained_clips_260313_10s_100k_filtered"], + ], + split="train", + shuffle=True, + fix_caption=False, + # wdinfo_resolution="gt720", + # max_frames=200, + ) + dataset_iter = iter(dataset) + + for i in range(10): + print(f"==================== Sample {i} ====================") + _t0 = time.time() + data = next(dataset_iter) + _t1 = time.time() + print(f"{'Loading time':<25}: {_t1 - _t0:.2f}s") + + print(f"==================== Sample {i} ====================") + print(f"{'video shape':<25}: {data['video'].shape}") # [C,T,H,W] + print(f"{'action shape':<25}: {data['action'].shape}") # [T,max_action_dim] + print(f"{'conditioning_fps':<25}: {data['conditioning_fps'].item()}") + print(f"{'mode':<25}: {data['mode']}") + print(f"{'domain_id':<25}: {data['domain_id'].item()}") + print(f"{'caption':<25}: {data['ai_caption']}") + + # save video to local for debugging + video = data["video"] + video = video.permute(1, 0, 2, 3) # [T,C,H,W] + video_path = f"temp/camera_sample_{i}.mp4" + torchvision.io.write_video( + video_path, video.permute(0, 2, 3, 1).numpy(), fps=data["conditioning_fps"].item() + ) # expects (T, H, W, C) + print(f"Saved video to {video_path}") diff --git a/cosmos_training/cosmos/data/vfm/action/compute_action_stats.py b/cosmos_training/cosmos/data/vfm/action/compute_action_stats.py new file mode 100644 index 00000000..86657466 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/compute_action_stats.py @@ -0,0 +1,1428 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Script to compute mean and std for action datasets. + +This script iterates through a dataset and computes action statistics +using Welford's online algorithm for numerical stability. + +Usage: + # Compute stats for LIBERO training dataset + PYTHONPATH=. python cosmos/data/vfm/action/compute_action_stats.py \ + --config configs/base/config.py \ + --split train \ + --max-samples 10000 \ + --output cosmos/data/vfm/action/libero_action_stats_10k.json \ + -- experiment=libero_exp + + # Compute stats for PushT + PYTHONPATH=. python cosmos/data/vfm/action/compute_action_stats.py \ + --config configs/base/config.py \ + --split train \ + --output cosmos/data/vfm/action/pusht_action_stats.json \ + -- experiment=pusht_exp + + # Quick test with limited samples + PYTHONPATH=. python cosmos/data/vfm/action/compute_action_stats.py \ + --config configs/base/config.py \ + --split train \ + --max-samples 1000 \ + --output test_stats.json \ + -- experiment=libero_exp + + # Override dataset parameters + PYTHONPATH=. python cosmos/data/vfm/action/compute_action_stats.py \ + --config configs/base/config.py \ + --split train \ + --output stats.json \ + -- experiment=libero_exp \ + dataloader_train.dataloaders.libero_data.dataloader.dataset.use_rotation_9d=False +""" + +import argparse +import copy +import importlib +import inspect +import json +import os as _os +import time +from pathlib import Path +from typing import Any, Callable + +import numpy as np +import torch +import torch.distributed as dist +from omegaconf import open_dict +from torch.utils.data import ConcatDataset, DataLoader, IterableDataset, Sampler +from tqdm import tqdm + +from cosmos.utils.config import load_config +from cosmos.utils.lazy_config import instantiate +from cosmos.utils.context_managers import data_loader_init + +# cosmos/data/vfm/action/ => 5 levels up to repo root +IMAGINAIRE4_ROOT = Path(__file__).resolve().parents[5] +# Bundled normalizers directory. When ``--output`` is not given, stats are +# written to ``/__.json`` so that +# loaders (e.g. ``BaseActionLeRobotDataset._load_norm_stats``) can resolve +# them from repo paths without hitting lustre. +DEFAULT_OUTPUT_DIR = IMAGINAIRE4_ROOT / "projects" / "cosmos3" / "vfm" / "datasets" / "action" / "normalizers" + + +# --------------------------------------------------------------------------- +# JSON pretty-printer +# --------------------------------------------------------------------------- +# ``json.dump(indent=2)`` puts every array element on its own line, which for +# stats payloads (action_dim vectors, 50-element quantile arrays) explodes +# what should be a ~17-line file into hundreds of lines. Keep dicts multi-line +# (2-space indent) but collapse numeric/scalar arrays to one line, and +# format floats at 6-decimal fixed precision (no scientific notation). +_FLOAT_DECIMALS = 6 + + +def _fmt_primitive(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, float): + return f"{value:.{_FLOAT_DECIMALS}f}" + return json.dumps(value, ensure_ascii=False) + + +def _fmt_aligned(value: Any) -> str: + """Format a primitive with leading-space padding for non-negative numerics. + + Used inside numeric arrays so that negative (``-0.000094``) and non-negative + (`` 0.001623``) values line up column-wise. Still emits valid JSON — the + extra leading whitespace is insignificant per RFC 8259. + """ + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, float): + return f"{value: .{_FLOAT_DECIMALS}f}" + if isinstance(value, int): + return f" {value}" if value >= 0 else f"{value}" + return json.dumps(value, ensure_ascii=False) + + +def _is_numeric(x: Any) -> bool: + """Real numeric (excludes bool, which is a subclass of int in Python).""" + return isinstance(x, (int, float)) and not isinstance(x, bool) + + +def _serialize(value: Any, indent: int = 0) -> str: + pad = " " * indent + inner = " " * (indent + 1) + if isinstance(value, dict): + if not value: + return "{}" + items = list(value.items()) + # Align value columns when any sibling value is a flat list (e.g., the + # ``global`` / ``global_raw`` blocks have mean/std/min/max... lists of + # different-length keys). Pad each key's trailing whitespace so all + # opening brackets land in the same column. + key_strs = [json.dumps(k, ensure_ascii=False) for k in value] + align = any(isinstance(v, list) and all(not isinstance(x, (dict, list)) for x in v) for v in value.values()) + max_key_len = max(len(s) for s in key_strs) if align else 0 + body = [] + for i, ((k, v), ks) in enumerate(zip(items, key_strs)): + gap = " " * (max_key_len - len(ks) + 1) if align else " " + sep = "," if i < len(items) - 1 else "" + body.append(f"{inner}{ks}:{gap}{_serialize(v, indent + 1)}{sep}") + return "{\n" + "\n".join(body) + f"\n{pad}}}" + if isinstance(value, list): + if not value: + return "[]" + if all(not isinstance(x, (dict, list)) for x in value): + # Numeric-array path: pad non-negatives with a leading space so + # ``-X.YYY`` and `` X.YYY`` columns line up vertically. + if any(isinstance(x, float) for x in value) and all(_is_numeric(x) for x in value): + return "[" + ", ".join(_fmt_aligned(x) for x in value) + "]" + return "[" + ", ".join(_fmt_primitive(x) for x in value) + "]" + body = [f"{inner}{_serialize(v, indent + 1)}{',' if i < len(value) - 1 else ''}" for i, v in enumerate(value)] + return "[\n" + "\n".join(body) + f"\n{pad}]" + return _fmt_primitive(value) + + +def _pretty_dump(obj: Any) -> str: + return _serialize(obj) + "\n" + + +class WelfordAccumulator: + """Welford's online algorithm for computing mean and variance. + + This is numerically stable for large datasets and works with streaming data. + + Reference: https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm + """ + + def __init__(self, dim: int): + self.dim = dim + self.count = 0 + self.mean = np.zeros(dim, dtype=np.float64) + self.m2 = np.zeros(dim, dtype=np.float64) # Sum of squared differences + self.min_val = np.full(dim, np.inf, dtype=np.float64) + self.max_val = np.full(dim, -np.inf, dtype=np.float64) + + def update(self, x: np.ndarray) -> None: + """Update statistics with a new sample or batch of samples. + + Uses Chan et al.'s parallel algorithm to merge a new batch in O(D) + numpy ops (no Python per-sample loop). + + Args: + x: Array of shape (D,) for single sample or (N, D) for batch. + """ + if x.ndim == 1: + x = x.reshape(1, -1) + x = x.astype(np.float64, copy=False) + + n_b = x.shape[0] + if n_b == 0: + return + + mean_b = x.mean(axis=0) + # sum of squared diffs within batch + m2_b = ((x - mean_b) ** 2).sum(axis=0) + + n_a = self.count + n_new = n_a + n_b + delta = mean_b - self.mean + + # Merge + self.mean = self.mean + delta * (n_b / n_new) + self.m2 = self.m2 + m2_b + (delta * delta) * (n_a * n_b / n_new) + self.count = n_new + + # Track per-dim min/max + np.minimum(self.min_val, x.min(axis=0), out=self.min_val) + np.maximum(self.max_val, x.max(axis=0), out=self.max_val) + + def get_mean(self) -> np.ndarray: + """Return the current mean.""" + return self.mean.copy() + + def get_variance(self) -> np.ndarray: + """Return the current sample variance.""" + if self.count < 2: + return np.zeros(self.dim, dtype=np.float64) + return self.m2 / (self.count - 1) + + def get_std(self) -> np.ndarray: + """Return the current standard deviation.""" + return np.sqrt(self.get_variance()) + + def get_min(self) -> np.ndarray: + """Return the minimum values seen.""" + return self.min_val.copy() + + def get_max(self) -> np.ndarray: + """Return the maximum values seen.""" + return self.max_val.copy() + + +class ReservoirAccumulator: + """Reservoir sampling (Algorithm R) for streaming quantile estimation. + + Maintains a fixed-size random sample of all observations seen so far. + After the stream finishes, exact quantiles are computed on the reservoir. + """ + + def __init__(self, dim: int, max_size: int = 50_000): + self.dim = dim + self.max_size = max_size + self.buffer = np.empty((max_size, dim), dtype=np.float32) + self.count = 0 + self._rng = np.random.RandomState(42) + + def update(self, x: np.ndarray) -> None: + """Update reservoir with a new sample or batch of samples.""" + if x.ndim == 1: + x = x.reshape(1, -1) + n = x.shape[0] + + if self.count < self.max_size: + space = self.max_size - self.count + fill_n = min(n, space) + self.buffer[self.count : self.count + fill_n] = x[:fill_n] + self.count += fill_n + x = x[fill_n:] + n = x.shape[0] + if n == 0: + return + + stream_indices = np.arange(self.count, self.count + n, dtype=np.int64) + j = (self._rng.random(n) * (stream_indices + 1)).astype(np.int64) + accept = j < self.max_size + if accept.any(): + self.buffer[j[accept]] = x[accept] + self.count += n + + def quantile(self, q: float) -> np.ndarray: + """Return the q-th quantile from the reservoir.""" + n = min(self.count, self.max_size) + if n == 0: + return np.zeros(self.dim, dtype=np.float32) + return np.quantile(self.buffer[:n], q, axis=0).astype(np.float32) + + +class SortedSubsetSampler(Sampler[int]): + """Yield a fixed list of indices in ascending order (no shuffling). + + Used to combine *unbiased* random subset sampling with *sequential* access + order: we draw a random subset once (seeded) and then iterate it sorted, + so that datasets with internal LRU caches (e.g. ``BaseActionLeRobotDataset`` + with 100+ inner LeRobotDatasets) don't thrash their cache on every batch. + + Unlike :class:`torch.utils.data.SubsetRandomSampler`, indices are **not** + reshuffled per iteration. + """ + + def __init__(self, indices: list[int]) -> None: + self._indices = sorted(indices) + + def __iter__(self): + return iter(self._indices) + + def __len__(self) -> int: + return len(self._indices) + + +ROTATION_FORMAT_DIM: dict[str, int] = {"rot9d": 9, "rot6d": 6, "euler_xyz": 3} + + +def get_rotation_dims(action_dim: int, rotation_format: str = "rot6d") -> list[int]: + """Infer rotation dim indices from ``action_dim`` and ``rotation_format``. + + Assumes the canonical layouts used in ``projects/cosmos3/vfm/datasets/action/``: + + - 9D (camera/AV): ``[pos(3) + rot(R)]`` + - 10D (single arm): ``[pos(3) + rot(R) + grip(1)]`` + - 2*(3+R+1) (dual arm): ``[L_pos + L_rot + L_grip | R_pos + R_rot + R_grip]`` + - 3*(3+R)+2 (AgiBot): ``[head_pos + head_rot | R_pos + R_rot + R_grip | + L_pos + L_rot + L_grip]`` + - 57D (HandPose rot6d, wrist+fingertips): + ``[cam(9) | R_wrist(9) + R_fingers(15) | L_wrist(9) + L_fingers(15)]`` + + For other configurations (e.g. rot9d HandPose or all-finger variants), + specify ``--skip-rotation-dims`` manually. + """ + if rotation_format not in ROTATION_FORMAT_DIM: + raise ValueError(f"rotation_format must be one of {list(ROTATION_FORMAT_DIM)}, got {rotation_format!r}") + rot_dim = ROTATION_FORMAT_DIM[rotation_format] + pos_dim = 3 + slot = pos_dim + rot_dim # 9 for rot6d + + # 9D: camera / AV — [pos + rot] + if action_dim == slot: + return list(range(pos_dim, slot)) + + # 10D: single arm + gripper — [pos + rot + grip] + if action_dim == slot + 1: + return list(range(pos_dim, slot)) + + # 20D: dual arm — [L_pos + L_rot + L_grip | R_pos + R_rot + R_grip] + if action_dim == 2 * (slot + 1): + right_rot_start = (slot + 1) + pos_dim + return list(range(pos_dim, slot)) + list(range(right_rot_start, right_rot_start + rot_dim)) + + # 29D: Embodiment C/Beta gripper FK-pose layout: + # [head_pos + head_rot | right_pos + right_rot + right_grip | left_pos + left_rot + left_grip] + if action_dim == 3 * slot + 2: + head_rot = list(range(pos_dim, slot)) + right_start = slot + right_rot = list(range(right_start + pos_dim, right_start + slot)) + left_start = slot + slot + 1 + left_rot = list(range(left_start + pos_dim, left_start + slot)) + return head_rot + right_rot + left_rot + + # 57D (rot6d) HandPose wrist+fingertips: + # [cam(3+R) | R_wrist(3+R) | R_fingers(5*3) | L_wrist(3+R) | L_fingers(5*3)] + if action_dim == slot + 2 * (slot + 5 * 3): + cam_rot = list(range(pos_dim, slot)) + r_wrist_rot = list(range(slot + pos_dim, slot + slot)) + l_wrist_rot = list(range(slot + slot + 15 + pos_dim, slot + slot + 15 + slot)) + return cam_rot + r_wrist_rot + l_wrist_rot + + # Unknown layout — most likely joint-space action (e.g. Embodiment_b 30D = + # arm(14) + end(14) + effector(2), or robomind joint-space). No SE(3) + # rotation dims to skip. Warn and return empty list rather than raising, + # so the stats get saved and the user can inspect the json. + print( + f" [warning] Cannot auto-detect SE(3) rotation dims for " + f"action_dim={action_dim}, rotation_format={rotation_format!r}. " + f"Assuming joint-space or custom layout — no dims will be skipped." + ) + return [] + + +def parse_dim_spec(spec: str | None) -> list[int]: + """Parse a dim index spec like ``"3-8,12-17,36-41"`` into a sorted unique list. + + Supports comma-separated tokens; each token is either a single integer or a + closed range ``a-b`` (both endpoints included). + """ + if not spec: + return [] + result: set[int] = set() + for token in spec.split(","): + token = token.strip() + if not token: + continue + if "-" in token: + a, b = token.split("-", 1) + lo, hi = int(a), int(b) + if lo > hi: + lo, hi = hi, lo + result.update(range(lo, hi + 1)) + else: + result.add(int(token)) + return sorted(result) + + +def _apply_skip_rotation(stats: dict[str, Any], skip_dims: list[int], action_dim: int) -> None: + """In-place overwrite stats at ``skip_dims`` with identity values. + + After this, downstream ``normalize_action`` (both ``meanstd`` and ``quantile``) + is the identity for those dims. Useful for rot6d/rot9d rotation dims, which + must not be per-dim normalized. + """ + if not skip_dims: + return + valid = [d for d in skip_dims if 0 <= d < action_dim] + if not valid: + return + + for key, identity in ( + ("mean", 0.0), + ("std", 1.0), + ("min", -1.0), + ("max", 1.0), + ("q01", -1.0), + ("q99", 1.0), + ): + arr = stats["global"][key] + for d in valid: + arr[d] = identity + stats["global"][key] = arr + + +def compute_action_stats( + dataloader: Any, + action_key: str = "action", + max_samples: int | None = None, + action_dim: int | None = None, + reservoir_size: int = 50_000, + return_accumulators: bool = False, +) -> dict[str, Any]: + """Compute mean and std for actions in a dataloader. + + Args: + dataloader: DataLoader that yields batches with action tensors. + action_key: Key for action tensor in batch output. + max_samples: If set, limit the number of samples to process. + action_dim: If set, only compute stats for first action_dim dimensions. + If None, auto-detect from first sample. + reservoir_size: Number of action frames to keep for quantile estimation. + + Returns: + Dictionary containing mean, std, quantiles, and metadata. + """ + print(f"Computing action statistics...") + print(f" action_key: {action_key}") + print(f" max_samples: {max_samples}") + + # Initialize accumulators (will be created after seeing first sample) + global_accumulator: WelfordAccumulator | None = None + reservoir: ReservoirAccumulator | None = None + detected_action_dim: int | None = None + chunk_length: int | None = None + + sample_count = 0 + start_time = time.time() + + data_iter = iter(dataloader) + pbar = tqdm(desc="Computing action stats", unit="samples") + + while True: + try: + batch = next(data_iter) + except StopIteration: + print("End of dataset reached.") + break + + if isinstance(batch, torch.Tensor): + batch = batch.numpy() + + if batch.ndim == 2: + batch = batch[:, None, :] + + B, chunk_len_batch, dim = batch.shape + + # Initialize accumulators on first batch + if global_accumulator is None: + chunk_length = chunk_len_batch + detected_action_dim = action_dim if action_dim is not None else dim + print(f" Detected action shape: {batch.shape[1:]}, using dim={detected_action_dim}") + global_accumulator = WelfordAccumulator(detected_action_dim) + reservoir = ReservoirAccumulator(detected_action_dim, max_size=reservoir_size) + + action = batch[:, :, :detected_action_dim].astype(np.float64) + + # Update global accumulator with all timesteps + flat_actions = action.reshape(-1, detected_action_dim) + global_accumulator.update(flat_actions) + reservoir.update(flat_actions.astype(np.float32)) + + sample_count += B + pbar.update(B) + + if sample_count % 1000 == 0: + elapsed = time.time() - start_time + rate = sample_count / elapsed + pbar.set_postfix({"rate": f"{rate:.1f} samples/s"}) + + if max_samples is not None and sample_count >= max_samples: + print(f"\nReached max_samples limit: {max_samples}") + break + pbar.close() + elapsed_time = time.time() - start_time + + if global_accumulator is None: + raise RuntimeError("No samples processed - dataset is empty or no actions found") + + print(f"\nProcessed {sample_count} samples in {elapsed_time:.1f}s ({sample_count / elapsed_time:.1f} samples/s)") + + # Compile results. Metadata is kept minimal; callers in main() attach + # dataset-descriptor fields after instantiation. + results: dict[str, Any] = { + "metadata": { + "action_dim": detected_action_dim, + "chunk_length": chunk_length, + "num_samples_stats": sample_count, + }, + "global": { + "mean": global_accumulator.get_mean().tolist(), + "std": global_accumulator.get_std().tolist(), + "min": global_accumulator.get_min().tolist(), + "max": global_accumulator.get_max().tolist(), + "q01": reservoir.quantile(0.01).tolist(), + "q99": reservoir.quantile(0.99).tolist(), + }, + } + + if return_accumulators: + # Multi-rank callers merge the raw accumulators via + # ``_merge_welford`` / ``_merge_reservoirs`` on rank 0 before + # rebuilding the final ``global`` block. Include the live + # reservoir buffer (only the populated prefix) so no samples are + # wasted in the merge. + results["_accumulators"] = { + "count": int(global_accumulator.count), + "mean": global_accumulator.mean.copy(), + "m2": global_accumulator.m2.copy(), + "min": global_accumulator.min_val.copy(), + "max": global_accumulator.max_val.copy(), + "reservoir_buffer": reservoir.buffer[: min(reservoir.count, reservoir.max_size)].copy(), + "reservoir_count": int(reservoir.count), + "reservoir_max_size": int(reservoir.max_size), + } + + return results + + +def get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Compute action statistics for a dataset") + parser.add_argument("--config", help="Path to the config file", required=True) + parser.add_argument("--split", help="Dataset split (train/val)", default="train") + parser.add_argument("--output", help="Output JSON file path", default=None) + + # Processing options + parser.add_argument( + "--action-key", + type=str, + default="action", + help="Key for action tensor in dataset output (default: 'action')", + ) + parser.add_argument( + "--action-dim", + type=int, + default=None, + help="Number of action dimensions to use (default: auto-detect)", + ) + parser.add_argument( + "--max-samples", + type=int, + default=None, + help=( + "If set and smaller than the full dataset size, draw a random " + "subset of this size (without replacement, deterministic via " + "--sampling-seed) before iterating. Gives unbiased stats on " + "datasets too large to fully iterate. Default: None (full data)." + ), + ) + parser.add_argument( + "--sampling-seed", + type=int, + default=42, + help=( + "Seed for the random subset sampler used when --max-samples " + "truncates the dataset. Default: 42 (reproducible)." + ), + ) + parser.add_argument( + "--reservoir-size", + type=int, + default=50_000, + help="Reservoir size for quantile estimation (default: 50000)", + ) + parser.add_argument( + "--skip-rotation-dims", + type=str, + default="auto", + help=( + "Force identity stats (mean=0, std=1, min=-1, max=1, q01=-1, q99=1) " + "on given dim indices so downstream normalization is a pass-through. " + 'Default "auto" infers from action_dim + --rotation-format (supports ' + "9D camera, 10D single-arm, 20D dual-arm, 29D AgiBot FK-pose, " + "57D HandPose rot6d). " + 'Pass explicit indices/ranges to override, e.g. "3-8" or "3-8,12-17,36-41". ' + 'Pass "none" (or an empty string) to disable skipping.' + ), + ) + parser.add_argument( + "--rotation-format", + type=str, + default="rot6d", + choices=list(ROTATION_FORMAT_DIM), + help="Rotation format used by the dataset (for --skip-rotation-dims auto).", + ) + + # DataLoader tuning + parser.add_argument("--batch-size", type=int, default=256, help="DataLoader batch size") + parser.add_argument("--num-workers", type=int, default=48, help="DataLoader num_workers") + parser.add_argument("--prefetch-factor", type=int, default=2, help="DataLoader prefetch_factor") + parser.add_argument( + "--enable-fast-init", + action="store_true", + help=( + "Enable BaseActionLeRobotDataset fast shard metadata initialization when " + "the dataset supports it. Useful for multi-shard AgiBot/RoboMIND/HandPose stats." + ), + ) + parser.add_argument( + "--fast-init-max-workers", + type=int, + default=64, + help="Max metadata prefetch workers used with --enable-fast-init (default: 64)", + ) + + # Sample stride override. Training uses stride=1 (overlapping chunks); + # stats converge equally well on non-overlapping chunks and run ~stride× + # faster. Applied to each ``BaseActionLeRobotDataset`` instance after + # construction but before episode indices are built. Datasets that don't + # expose ``_sample_stride`` are left untouched. + parser.add_argument( + "--sample-stride", + type=int, + default=16, + help=( + "Sample stride to use for stats computation (default: 16 = " + "non-overlapping chunks of ``chunk_length=16``). Passed to each " + "LeRobot-backed dataset's ``_sample_stride`` attribute before the " + "episode index is built. Use ``--sample-stride 1`` to match " + "training semantics exactly (slower but identical numerics)." + ), + ) + + # Dataset filter (for experiments that bundle multiple datasets, e.g. + # ``action_midtrain_exp004_onlyActionData`` — pick one entry at a time). + parser.add_argument( + "--dataset-name", + type=str, + default=None, + help=( + "If set, keep only the entry in ``list_of_datasets`` whose ``name`` " + "starts with this string (case-sensitive prefix match). Used when " + "the experiment bundles several datasets (e.g. " + "``action_midtrain_exp004_onlyActionData``) and you want stats for " + "just one. Default: None (keep all entries)." + ), + ) + + # Hydra-style config overrides + parser.add_argument( + "opts", + help="Modify config options (e.g., experiment=libero_exp)", + default=None, + nargs=argparse.REMAINDER, + ) + + return parser.parse_args() + + +# --------------------------------------------------------------------------- +# Distributed (torchrun) helpers +# --------------------------------------------------------------------------- +# The heavy cost on large multi-shard datasets (e.g. ``HandPoseDataset`` with +# ~900 shards) is *index construction* inside ``_register_sources`` — each +# shard opens ``episodes.parquet`` / ``subtasks.parquet`` and builds a +# per-episode span table. All shards are processed sequentially in a single +# process, so the 3k-second ``init_data_loader`` time dominates the ~3-minute +# actual stats compute. +# +# The shard registration API already supports slicing (``_register_sources( +# indices)``) so each rank can register only its share of shards. We exploit +# this here: when launched via ``torchrun``, each rank registers +# ``shards[rank::world_size]``, computes partial Welford + reservoir on its +# slice, then ``all_gather_object`` merges into a global set of stats on +# rank 0 (which writes the file). Other ranks exit. + + +def _init_distributed() -> tuple[int, int, bool]: + """Initialize the default process group from ``torchrun`` env vars. + + Uses the ``gloo`` backend (pure CPU — this script has no GPU work). + Returns ``(rank, world_size, is_distributed)``; when ``WORLD_SIZE`` is 1 + or env vars are missing, returns ``(0, 1, False)`` and no process group + is created. + """ + world_size = int(_os.environ.get("WORLD_SIZE", "1")) + rank = int(_os.environ.get("RANK", "0")) + if world_size > 1 and not dist.is_initialized(): + dist.init_process_group(backend="gloo") + return rank, world_size, world_size > 1 + + +def _register_rank_shards(unified_dataset: Any, rank: int, world_size: int) -> None: + """Register each inner dataset's shards using a rank-sliced index set. + + Replaces the default ``_ensure_sources_registered()`` path with round-robin + shard assignment: each rank only calls ``_register_sources(shards[rank:: + world_size])`` on datasets that expose ``_all_shard_roots``. Datasets + without sharded roots (single-shard LeRobot, plain ``IterableDataset``) + fall back to a full registration on every rank — acceptable because + those are cheap. + """ + from cosmos.data.vfm.action.unified_dataset import MapToIterableAdapter + + for entry in unified_dataset._datasets: + ds = entry["dataset"] + if isinstance(ds, MapToIterableAdapter): + ds = ds.dataset + if not hasattr(ds, "_register_sources"): + continue + shard_roots = getattr(ds, "_all_shard_roots", []) + if not shard_roots: + ds._register_sources() + continue + n = len(shard_roots) + my_indices = list(range(rank, n, world_size)) + print( + f" [rank {rank}/{world_size}] {ds.__class__.__name__}: " + f"registering {len(my_indices)}/{n} shards (round-robin)" + ) + ds._register_sources(my_indices) + unified_dataset._sources_initialized = True + + +def _merge_welford( + counts: list[int], + means: list[np.ndarray], + m2s: list[np.ndarray], + mins: list[np.ndarray], + maxs: list[np.ndarray], +) -> tuple[int, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Pairwise-merge Welford stats via Chan et al.'s parallel algorithm. + + All per-rank arrays must share the same ``dim``. ``min``/``max`` are + merged element-wise. Ranks with ``count == 0`` are skipped. + """ + total_count = 0 + mean = None + m2 = None + min_val = None + max_val = None + for c, mu, v, mn, mx in zip(counts, means, m2s, mins, maxs): + if c == 0: + continue + if total_count == 0: + total_count, mean, m2, min_val, max_val = c, mu.copy(), v.copy(), mn.copy(), mx.copy() + continue + n_a, n_b = total_count, c + n_new = n_a + n_b + delta = mu - mean + mean = mean + delta * (n_b / n_new) + m2 = m2 + v + (delta * delta) * (n_a * n_b / n_new) + total_count = n_new + np.minimum(min_val, mn, out=min_val) + np.maximum(max_val, mx, out=max_val) + return total_count, mean, m2, min_val, max_val + + +def _merge_reservoirs( + buffers: list[np.ndarray], + max_size: int, + seed: int = 42, +) -> np.ndarray: + """Merge per-rank reservoir buffers into a single reservoir. + + Each rank's buffer is assumed to be an unbiased uniform sample of its + local stream. We concatenate all buffers then uniformly sub-sample + ``max_size`` rows. This is strictly unbiased only when every rank + processed the same number of samples (the common case here, since we + split ``max_samples`` evenly across ranks) — accurate enough for + quantile estimation in all practical regimes. + """ + non_empty = [b for b in buffers if b.size > 0] + if not non_empty: + return np.empty((0, 0), dtype=np.float32) + concat = np.concatenate(non_empty, axis=0) + if concat.shape[0] <= max_size: + return concat + rng = np.random.RandomState(seed) + keep = rng.choice(concat.shape[0], size=max_size, replace=False) + keep.sort() + return concat[keep] + + +def _build_action_dataloader( + dl_config: Any, + action_key: str, + batch_size: int = 64, + num_workers: int = 16, + prefetch_factor: int = 2, + max_samples: int | None = None, + sampling_seed: int = 42, + dataset_name: str | None = None, + sample_stride: int = 1, + enable_fast_init: bool = False, + fast_init_max_workers: int = 64, + rank: int = 0, + world_size: int = 1, +) -> tuple[DataLoader, dict[str, Any]]: + """Instantiate the action dataset from config and wrap it in a DataLoader. + + Must be called inside a ``data_loader_init()`` context. + + When ``max_samples`` is set and smaller than the concatenated dataset size, + a random subset of size ``max_samples`` is drawn (without replacement, + seeded by ``sampling_seed``) and iterated in sorted order via + :class:`SortedSubsetSampler`. Random subset + sorted iteration gives + unbiased stats + cache-friendly access (important for datasets with many + inner LeRobotDatasets behind an LRU cache). Otherwise the full dataset is + iterated in natural order. + + When ``dataset_name`` is set, ``list_of_datasets`` is filtered in-place + to keep only the entry whose ``name`` field starts with that prefix. This + is useful when running against an experiment that bundles multiple + datasets (e.g. ``action_midtrain_exp004_onlyActionData``) — pick one at + a time. + + Returns: + (dataloader, dataset_params) where ``dataset_params`` describes each + inner dataset (class, pose_convention, rotation_format, mode, ...) + and is written into the stats JSON metadata. ``dataset_params`` also + carries ``total_len`` and, when sampling is enabled, + ``sampled_len`` and ``sampling_seed``. + """ + from cosmos.data.vfm.action.unified_dataset import MapToIterableAdapter + + ds_config = dl_config.dataloaders.action_data.dataloader.dataset + ds_config.tokenizer_config = None + + # Optional filter: keep only the entry whose ``name`` starts with ``dataset_name``. + if dataset_name is not None: + all_names = [entry.get("name", "") for entry in ds_config.list_of_datasets] + matched = [entry for entry in ds_config.list_of_datasets if str(entry.get("name", "")).startswith(dataset_name)] + if not matched: + raise ValueError( + f"--dataset-name={dataset_name!r} did not match any entry. " + f"Available names in list_of_datasets: {all_names}" + ) + if len(matched) > 1: + matched_names = [entry.get("name") for entry in matched] + print( + f" WARNING: --dataset-name={dataset_name!r} matched {len(matched)} entries " + f"({matched_names}); keeping only the first." + ) + matched = matched[:1] + with open_dict(ds_config): + ds_config.list_of_datasets = matched + print( + f" Filtered list_of_datasets to 1 entry (name={matched[0].get('name')!r}) " + f"from {len(all_names)} candidates." + ) + + def _cfg_get(cfg: Any, key: str, default: Any = None) -> Any: + """Safely read ``key`` and unwrap OmegaConf containers to plain Python + types, so downstream metadata never leaks ``ListConfig`` / ``DictConfig`` + into the JSON pretty-printer or ``json.dumps`` fallback. + """ + try: + if key not in cfg: + return default + val = cfg[key] + except Exception: + return default + try: + from omegaconf import DictConfig, ListConfig, OmegaConf + + if isinstance(val, (DictConfig, ListConfig)): + return OmegaConf.to_container(val, resolve=True) + except ImportError: + pass + return val + + def _resolve_target(target: Any) -> Callable | None: + """Resolve a ``_target_`` field (string or class) to a callable; None on failure.""" + if callable(target): + return target + if isinstance(target, str): + module_path, _, attr = target.rpartition(".") + if not module_path: + return None + try: + mod = importlib.import_module(module_path) + return getattr(mod, attr, None) + except Exception: + return None + return None + + def _callable_accepts_kwarg(target: Any, kwarg: str) -> bool: + """Check whether ``target.__init__`` (or factory) accepts ``kwarg``. + + Returns True only if the signature **explicitly** lists ``kwarg`` as a + named parameter. A bare ``**kwargs`` is intentionally NOT treated as + acceptance, because factories like ``get_umi_dataset(..., **kwargs)`` + forward extras into a struct-mode OmegaConf config via + ``cfg.update(kwargs)``, which raises ``ConfigKeyError`` on unknown + keys. For those we rely on the post-instantiation + ``ds._skip_video_loading = True`` fallback below. + + Returns False on any introspection failure (safer to under-inject + than blow up at instantiation time). + """ + fn = _resolve_target(target) + if fn is None: + return False + try: + sig = inspect.signature(fn) + except (TypeError, ValueError): + return False + for p in sig.parameters.values(): + if p.name == kwarg and p.kind is not inspect.Parameter.VAR_KEYWORD: + return True + return False + + dataset_params: dict[str, Any] = {"datasets": []} + for entry in ds_config.list_of_datasets: + ds_dict = entry.dataset + # Only disable normalization if the dataset supports that kwarg. Some + # datasets (e.g. EmbodimentCGripperDataset) don't take ``action_normalization``. + if "action_normalization" in ds_dict: + with open_dict(ds_dict): + ds_dict.action_normalization = None + # Inject ``skip_video_loading=True`` only when the target class/factory + # explicitly lists it as a named kwarg. Everything else — subclasses + # that don't forward ``**kwargs`` to super (BridgeOrigLeRobotDataset, + # DROIDLeRobotDataset, RoboMINDFrankaDataset, ...) AND factories that + # forward ``**kwargs`` into a struct-mode OmegaConf config + # (``get_umi_dataset`` does ``cfg.update(kwargs)``) — falls back to + # the post-instantiation ``ds._skip_video_loading = True`` below, + # which the base class honors via attribute lookup. + target_cfg = _cfg_get(ds_dict, "_target_") + if _callable_accepts_kwarg(target_cfg, "skip_video_loading"): + with open_dict(ds_dict): + ds_dict.skip_video_loading = True + if enable_fast_init and _callable_accepts_kwarg(target_cfg, "enable_fast_init"): + with open_dict(ds_dict): + ds_dict.enable_fast_init = True + if enable_fast_init and _callable_accepts_kwarg(target_cfg, "fast_init_max_workers"): + with open_dict(ds_dict): + ds_dict.fast_init_max_workers = fast_init_max_workers + # Extract descriptive parameters into metadata (for naming stats files). + # ``embodiment_type`` / ``root`` / ``sample_stride`` may be absent from + # the config when the dataset class hard-codes them in ``__init__`` + # (e.g. Bridge / DROID / Fractal); we fill those in after instantiation + # from the instance attributes below. + target = _cfg_get(ds_dict, "_target_", "unknown") + dataset_params["datasets"].append( + { + "name": _cfg_get(entry, "name"), + # ``target`` is typically a class object whose ``repr`` is + # ``".'>"``. Prefer ``__name__`` + # when available; otherwise strip the ```` wrapper. + "class": getattr(target, "__name__", None) + or (str(target).rsplit(".", 1)[-1].rstrip("'>") if target else "unknown"), + "embodiment_type": _cfg_get(ds_dict, "embodiment_type"), + "pose_convention": _cfg_get(ds_dict, "pose_convention"), + "rotation_format": _cfg_get(ds_dict, "rotation_format"), + "mode": _cfg_get(ds_dict, "mode"), + "chunk_length": _cfg_get(ds_dict, "chunk_length"), + "root": _cfg_get(ds_dict, "root"), + "sample_stride": _cfg_get(ds_dict, "sample_stride"), + } + ) + + unified_dataset = instantiate(ds_config) + unified_dataset._transform = lambda data_dict, resolution=None: data_dict + + # Override ``_sample_stride`` on each inner BaseActionLeRobotDataset *before* + # ``_ensure_sources_registered()`` builds the episode index. This lets the + # stats run use non-overlapping chunks (stride=chunk_length) for speed + # without touching training-side defaults. Datasets that don't expose the + # attribute (e.g. UMI BaseDataset, IterableDatasets) are skipped silently. + if sample_stride != 1: + for entry in unified_dataset._datasets: + ds = entry["dataset"] + if isinstance(ds, MapToIterableAdapter): + ds = ds.dataset + if hasattr(ds, "_sample_stride"): + ds._sample_stride = sample_stride + if enable_fast_init: + for entry in unified_dataset._datasets: + ds = entry["dataset"] + if isinstance(ds, MapToIterableAdapter): + ds = ds.dataset + if hasattr(ds, "_enable_fast_init"): + ds._enable_fast_init = True + if hasattr(ds, "_fast_init_max_workers"): + ds._fast_init_max_workers = fast_init_max_workers + + if world_size > 1: + _register_rank_shards(unified_dataset, rank=rank, world_size=world_size) + else: + unified_dataset._ensure_sources_registered() + + inner_datasets: list[Any] = [] + total_len = 0 + for idx, entry in enumerate(unified_dataset._datasets): + ds = entry["dataset"] + if isinstance(ds, MapToIterableAdapter): + ds = ds.dataset + ds._skip_video_loading = True + inner_datasets.append(ds) + ds_len = len(ds) + total_len += ds_len + print(f" Inner dataset: {ds.__class__.__name__}, len={ds_len}") + + # Backfill fields that the dataset class hard-codes in ``__init__`` + # (not visible in the config). Falls through silently if the instance + # doesn't expose a matching attribute — we just keep the None from + # the pre-instantiation record. + if idx < len(dataset_params["datasets"]): + dp_entry = dataset_params["datasets"][idx] + if not dp_entry.get("embodiment_type"): + dp_entry["embodiment_type"] = getattr(ds, "_embodiment_type", None) + if not dp_entry.get("pose_convention"): + dp_entry["pose_convention"] = getattr(ds, "_pose_convention", None) + if not dp_entry.get("rotation_format"): + dp_entry["rotation_format"] = getattr(ds, "_rotation_format", None) + if not dp_entry.get("sample_stride"): + dp_entry["sample_stride"] = getattr(ds, "_sample_stride", None) + if not dp_entry.get("root"): + # Base class exposes ``_all_shard_roots`` (list of shard roots); + # for single-shard datasets use it verbatim, else the common + # parent directory across all shard roots. + shard_roots = getattr(ds, "_all_shard_roots", None) + if shard_roots: + if len(shard_roots) == 1: + dp_entry["root"] = shard_roots[0] + else: + dp_entry["root"] = _os.path.commonpath(shard_roots) + if total_len == 0: + raise RuntimeError( + "All inner datasets are empty (total len=0). Most likely the data " + "files are missing or inaccessible on this machine. Check the " + "dataset root paths and warnings printed above." + ) + + combined_ds = ConcatDataset(inner_datasets) if len(inner_datasets) > 1 else inner_datasets[0] + + def collate_actions(batch: list[dict]) -> torch.Tensor | np.ndarray: + actions = [sample[action_key] for sample in batch] + is_tensor = isinstance(actions[0], torch.Tensor) + # Fast path: uniform chunk length (the common case). Preserves the + # (B, T, D) shape so ``chunk_length`` metadata reflects the true + # sampling window. + shapes = {tuple(a.shape) for a in actions} + if len(shapes) == 1: + return torch.stack(actions) if is_tensor else np.stack(actions) + # Fallback: variable-length chunks (e.g. HandPoseDataset with + # ``snap_to_subtask``). Per-frame stats are order-/shape-invariant, + # so we flatten each chunk and concatenate along the time axis. + # Downstream compute_action_stats reshapes 2D input to (N, 1, D) and + # computes stats over all rows — numerics unchanged. The reported + # ``chunk_length`` metadata will be 1 in this mode; the true + # config-side chunk_length is still captured in dataset_params. + if is_tensor: + dim = actions[0].shape[-1] + return torch.cat([a.reshape(-1, dim) for a in actions], dim=0) + dim = actions[0].shape[-1] + return np.concatenate([np.asarray(a).reshape(-1, dim) for a in actions], axis=0) + + dataset_params["total_len"] = total_len + + + # then yield them **sorted ascending** via SortedSubsetSampler. Random + # iteration order (as SubsetRandomSampler would give) causes catastrophic + # LRU cache misses on datasets like RoboMINDFrankaDataset that hold + # O(100) inner LeRobotDatasets behind a size-32 LRU — observed ~170x + # slowdown (10K samples/s sorted vs ~60 samples/s random). Stats only + # depend on the *set* of samples, not on visit order, so sorting is free. + sampler: Sampler[int] | None = None + is_iterable = isinstance(combined_ds, IterableDataset) or any( + isinstance(ds, IterableDataset) for ds in inner_datasets + ) + # With multi-rank launches each rank only holds ``shards[rank::world_size]``, + # so ``total_len`` above is already this rank's slice. Split the global + # ``max_samples`` evenly across ranks; combined across all ranks we still + # process ~``max_samples`` rows total. Use a per-rank seed offset so + # different ranks draw different (non-overlapping) subsets from their + # disjoint shard slices. + if max_samples is not None and max_samples > 0 and world_size > 1: + per_rank_max = (max_samples + world_size - 1) // world_size + per_rank_seed = sampling_seed + rank + else: + per_rank_max = max_samples + per_rank_seed = sampling_seed + + if is_iterable and per_rank_max is not None and per_rank_max > 0: + # IterableDataset doesn't support DataLoader samplers. Rely on the + # ``max_samples`` hard-break inside ``collect_stats_loop`` instead. + # Note: stats may be biased toward whatever order the underlying + # iterable yields (e.g. early shards) — acceptable for stats-only use. + dataset_params["sampled_len"] = per_rank_max + print( + f" IterableDataset detected — skipping subset sampler. " + f"Will stop after {per_rank_max:,} samples in natural iteration " + "order (may be biased toward early shards)." + ) + elif per_rank_max is not None and per_rank_max > 0 and per_rank_max < total_len: + g = torch.Generator().manual_seed(per_rank_seed) + indices = torch.randperm(total_len, generator=g)[:per_rank_max].tolist() + sampler = SortedSubsetSampler(indices) + dataset_params["sampled_len"] = per_rank_max + dataset_params["sampling_seed"] = per_rank_seed + print( + f" [rank {rank}/{world_size}] Random subset sampling: " + f"{per_rank_max:,} / {total_len:,} " + f"(seed={per_rank_seed}, iterated in sorted order for cache locality)" + ) + elif per_rank_max is not None and per_rank_max > 0: + print( + f" [rank {rank}/{world_size}] per_rank_max={per_rank_max:,} >= " + f"total_len={total_len:,}; iterating this rank's full slice in natural order." + ) + + dataloader = DataLoader( + combined_ds, + batch_size=batch_size, + sampler=sampler, + shuffle=False, + num_workers=num_workers, + collate_fn=collate_actions, + prefetch_factor=prefetch_factor, + persistent_workers=True, + ) + return dataloader, dataset_params + + +def _resolve_default_output_path( + dataset_params: dict[str, Any], + split: str, + exp_name: str, + cli_rotation_format: str, +) -> Path: + """Compute the default output path under ``DEFAULT_OUTPUT_DIR``. + + Matches ``BaseActionLeRobotDataset._normalizer_filename`` exactly so the + file the loader will look up is the one we write here: + + - SE(3) pose datasets (``embodiment`` + ``pose`` + ``rot``): + ``normalizers/__.json`` + - joint-space datasets (``pose`` / ``rot`` both ``None``): + ``normalizers/.json`` + - ``_`` suffix is appended for non-train splits. + + Falls back to ``action_stats__.json`` only when even + ``embodiment`` is unknown. + """ + datasets = dataset_params.get("datasets") or [] + first = datasets[0] if datasets else {} + embodiment = first.get("embodiment_type") + pose = first.get("pose_convention") + # Datasets that don't expose ``rotation_format`` as a kwarg (e.g. DROID, + # Fractal, RoboMIND) still produce rot6d actions internally; fall back + # to the CLI ``--rotation-format`` which defaults to rot6d. For + # joint-space datasets (e.g. Embodiment_b) have ``pose`` set to ``None`` — keep + # ``rot`` ``None`` too so the filename drops both suffixes instead of + # synthesizing a bogus ``_rot6d``. FK-pose datasets such as Embodiment C + # and AgiBotWorld-Beta expose both ``pose`` and ``rotation_format``. + rot = first.get("rotation_format") + if rot is None and pose is not None: + rot = cli_rotation_format + + if embodiment and pose and rot: + stem = f"{embodiment}_{pose}_{rot}" + elif embodiment and pose is None and rot is None: + stem = str(embodiment) + else: + return DEFAULT_OUTPUT_DIR / f"action_stats_{exp_name}_{split}.json" + + if split != "train": + stem = f"{stem}_{split}" + return DEFAULT_OUTPUT_DIR / f"{stem}.json" + + +def main() -> None: + args = get_args() + + # Initialize distributed (torchrun) early so all the ``[rank R/W]`` log + # prefixes below have the right values. ``WORLD_SIZE == 1`` (plain + # ``python`` invocation) is a no-op — the code paths below are identical + # to the single-process pre-change behavior. + rank, world_size, is_distributed = _init_distributed() + if is_distributed: + print(f"[dist] rank={rank} world_size={world_size} backend=gloo") + + # Infer experiment name for default output path + exp_name = "default" + if args.opts: + for opt in args.opts: + if "experiment=" in opt: + exp_name = opt.split("=")[1] + exp_name = exp_name.strip("'\"") + break + + # Load config + if rank == 0: + print(f"Loading config: {args.config}") + print(f"Config overrides: {args.opts}") + try: + config = load_config(args.config, args.opts, enable_one_logger=False) + except Exception as e: + print(f"Failed to load config: {e}") + raise + + # Instantiate dataloader + with data_loader_init(): + if args.split == "train": + dl_config = config.dataloader_train + else: + dl_config = config.dataloader_val + + if rank == 0: + print(f"Instantiating {args.split} dataloader...") + try: + dataloader, dataset_params = _build_action_dataloader( + dl_config, + args.action_key, + batch_size=args.batch_size, + num_workers=args.num_workers, + prefetch_factor=args.prefetch_factor, + max_samples=args.max_samples, + sampling_seed=args.sampling_seed, + dataset_name=args.dataset_name, + sample_stride=args.sample_stride, + enable_fast_init=args.enable_fast_init, + fast_init_max_workers=args.fast_init_max_workers, + rank=rank, + world_size=world_size, + ) + except Exception as e: + print(f"Failed to instantiate dataloader: {e}") + raise + + # Resolve output path now that we know embodiment / pose / rotation. + if args.output: + output_path = Path(args.output) + else: + output_path = _resolve_default_output_path( + dataset_params=dataset_params, + split=args.split, + exp_name=exp_name, + cli_rotation_format=args.rotation_format, + ) + if rank == 0: + print(f" Default output path: {output_path}") + + # Compute statistics. In multi-rank mode each rank iterates its own + # shard slice and returns raw accumulators; we merge them below before + # re-deriving the final ``global`` block. The global cap (``max_samples``) + # stays as-is for single-process mode, but is split evenly across ranks + # inside ``_build_action_dataloader`` when ``world_size > 1``. + per_rank_cap = args.max_samples + if is_distributed and per_rank_cap is not None: + per_rank_cap = (per_rank_cap + world_size - 1) // world_size + results = compute_action_stats( + dataloader=dataloader, + action_key=args.action_key, + max_samples=per_rank_cap, + action_dim=args.action_dim, + reservoir_size=args.reservoir_size, + return_accumulators=is_distributed, + ) + + if is_distributed: + # Gather every rank's raw accumulators on rank 0, merge via Chan's + # parallel Welford algorithm + reservoir union, then rebuild the + # ``global`` block. ``all_gather_object`` is symmetric (all ranks + # receive the list) so we only have one collective call; the + # non-zero ranks simply discard their copy. + local_accum = results.pop("_accumulators") + gathered: list[dict[str, Any]] = [None] * world_size # type: ignore[list-item] + dist.all_gather_object(gathered, local_accum) + + # Sum the raw (pre-cap) sample counts for metadata reporting. + total_samples = int(sum(g["count"] for g in gathered)) + + if rank == 0: + total_count, mean, m2, min_val, max_val = _merge_welford( + counts=[g["count"] for g in gathered], + means=[g["mean"] for g in gathered], + m2s=[g["m2"] for g in gathered], + mins=[g["min"] for g in gathered], + maxs=[g["max"] for g in gathered], + ) + if total_count == 0: + raise RuntimeError("Distributed merge saw zero total samples across all ranks.") + + merged_reservoir = _merge_reservoirs( + buffers=[g["reservoir_buffer"] for g in gathered], + max_size=args.reservoir_size, + seed=args.sampling_seed, + ) + # ``std`` from Chan-merged ``m2``: sample variance = m2 / (N-1). + std = np.sqrt(m2 / max(total_count - 1, 1)) + q01 = np.quantile(merged_reservoir, 0.01, axis=0) if merged_reservoir.size else np.zeros_like(mean) + q99 = np.quantile(merged_reservoir, 0.99, axis=0) if merged_reservoir.size else np.zeros_like(mean) + + results["global"] = { + "mean": mean.tolist(), + "std": std.tolist(), + "min": min_val.tolist(), + "max": max_val.tolist(), + "q01": q01.astype(np.float32).tolist(), + "q99": q99.astype(np.float32).tolist(), + } + results["metadata"]["num_samples_stats"] = total_samples + results["metadata"]["world_size"] = world_size + print( + f"\n[dist] merged across world_size={world_size}: total_samples={total_samples:,}, " + f"reservoir_size={merged_reservoir.shape[0]:,}" + ) + # Non-rank-0 processes have nothing more to do. + dist.barrier() + if rank != 0: + dist.destroy_process_group() + return + + # Zero-out rotation dims so normalization is a pass-through for them. + # Keep this *before* the metadata re-assembly below so skip_rotation_dims + # is ready when we compose the final payload. + act_dim = results["metadata"]["action_dim"] + spec = (args.skip_rotation_dims or "").strip().lower() + if spec in ("", "none"): + skip_dims: list[int] = [] + print("\nSkip rotation: disabled (stats reflect raw data on all dims).") + elif spec == "auto": + skip_dims = get_rotation_dims(act_dim, args.rotation_format) + print( + f"\nAuto-detected rotation dims (action_dim={act_dim}, " + f"rotation_format={args.rotation_format}): {skip_dims or '[] (none)'}" + ) + else: + skip_dims = parse_dim_spec(args.skip_rotation_dims) + if skip_dims: + print(f"\nForcing identity stats for rotation dims: {skip_dims} (action_dim={act_dim})") + + if skip_dims: + # Preserve the raw (pre-replacement) stats under ``global_raw`` so + # consumers can inspect the data's true distribution on rotation dims + # even after ``global`` has been overwritten with identity values. + results["global_raw"] = copy.deepcopy(results["global"]) + _apply_skip_rotation(results, skip_dims, act_dim) + + # Assemble the final flat metadata block. Identity fields first (who/what + # these stats describe), then stats-run configuration (how they were + # computed). Source priority for ``rotation_format``: dataset config > + # CLI ``--rotation-format``. Fields with no source stay ``None`` rather + # than being silently dropped, so gaps are visible. + first = (dataset_params.get("datasets") or [{}])[0] + meta = results["metadata"] + ordered: dict[str, Any] = { + # Identity + "embodiment_type": first.get("embodiment_type"), + "pose_convention": first.get("pose_convention"), + # Leave ``rotation_format`` as ``None`` for joint-space datasets; do + # NOT fall back to the CLI default, which would silently claim + # "rot6d" on a dataset with no rotation. + "rotation_format": first.get("rotation_format"), + "action_dim": meta["action_dim"], + "skip_rotation_dims": skip_dims, + "chunk_length": meta["chunk_length"], + "sample_stride": first.get("sample_stride"), + # Dataset provenance + "dataset_name": first.get("name"), + "dataset_class": first.get("class"), + "dataset_root": first.get("root"), + "split": args.split, + # Stats-run configuration (how these numbers were computed) + "num_samples_stats": meta["num_samples_stats"], + "reservoir_size": args.reservoir_size, + } + if args.max_samples is not None: + ordered["max_samples"] = args.max_samples + ordered["sampling_seed"] = args.sampling_seed + + results["metadata"] = ordered + + # Save results. Use a compact pretty-printer so numeric arrays stay on + # one line (``json.dump(indent=2)`` puts every number on its own line, + # blowing a 17-line payload into ~900 lines). + # Serialize fully *before* touching disk: opening with ``"w"`` truncates + # the target, so a formatting crash would otherwise wipe any previously + # good file on disk. Stage to a sibling ``.tmp`` and atomically rename + # only after successful formatting. + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + payload = _pretty_dump(results) + except Exception: + fallback = output_path.with_suffix(output_path.suffix + ".raw.json") + with open(fallback, "w") as f: + json.dump(results, f, indent=2, default=str) + print(f"\n[warning] pretty-print failed; raw results saved to: {fallback}") + raise + tmp_path = output_path.with_suffix(output_path.suffix + ".tmp") + with open(tmp_path, "w") as f: + f.write(payload) + _os.replace(tmp_path, output_path) + + print(f"\nResults saved to: {output_path}") + print("\nGlobal statistics:") + print(f" Mean: {results['global']['mean']}") + print(f" Std: {results['global']['std']}") + print(f" Min: {results['global']['min']}") + print(f" Max: {results['global']['max']}") + print(f" q01: {results['global']['q01']}") + print(f" q99: {results['global']['q99']}") + + if is_distributed: + dist.destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/cosmos_training/cosmos/data/vfm/action/compute_pose_stats.py b/cosmos_training/cosmos/data/vfm/action/compute_pose_stats.py new file mode 100644 index 00000000..425a36bb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/compute_pose_stats.py @@ -0,0 +1,637 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Compute raw action translation and rotation statistics for camera and AV pose datasets. + +Both camera and AV datasets produce actions via ``pose_abs_to_rel`` with +layout ``[translation(3), rotation(...)]``. + +This script iterates dataset samples with ``translation_scale=1.0`` and +``rotation_scale=1.0`` to collect **raw** translation and rotation +values, then reports their distribution (mean, std, min, max, +percentiles) both globally and per-timestep for each block. The +reported per-dim std is what directly matches MSE loss scale, so ratios +like ``std_translation / std_rotation`` are what you want to use to +pick ``translation_scale`` and ``rotation_scale`` so the two loss blocks +contribute comparably. + +Usage: + # Camera – backward_framewise (used in inverse_dynamics / policy) + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset camera --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 17 --rotation-format axisangle + + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset camera --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 61 --rotation-format axisangle + + # AV – backward_framewise + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset av --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 17 --rotation-format axisangle + + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset av --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 61 --rotation-format axisangle + + # Clip per-frame outliers automatically at P99 of each block's L2 norm. + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset camera --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 61 \ + --rotation-format axisangle --max-trans-norm 10 + + PYTHONPATH=. python cosmos/data/vfm/action/compute_pose_stats.py \ + --dataset av --split train --pose-convention backward_framewise --max-samples 1000 --max-frames 61 \ + --rotation-format axisangle --max-trans-norm 10 +""" + +from __future__ import annotations + +import argparse +import json +import time +from pathlib import Path +from typing import Literal + +import numpy as np +import torch +from tqdm import tqdm + +from cosmos.data.vfm.action.pose_utils import RotationConvention + +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_OUTPUT_DIR = SCRIPT_DIR / "stats" + +TRANSLATION_DIM = 3 +PoseConvention = Literal[ + "backward_framewise", + "backward_anchored", +] + + +# --------------------------------------------------------------------------- +# Welford accumulator +# --------------------------------------------------------------------------- +class WelfordAccumulator: + """Welford's online algorithm for numerically stable mean/variance.""" + + def __init__(self, dim: int) -> None: + self.dim = dim + self.count = 0 + self.mean = np.zeros(dim, dtype=np.float64) + self.m2 = np.zeros(dim, dtype=np.float64) + self.min_val = np.full(dim, np.inf, dtype=np.float64) + self.max_val = np.full(dim, -np.inf, dtype=np.float64) + + def update(self, x: np.ndarray) -> None: + """Update with a single sample (D,) or a batch (N, D).""" + if x.ndim == 1: + x = x.reshape(1, -1) + for sample in x: + self.count += 1 + delta = sample - self.mean + self.mean += delta / self.count + delta2 = sample - self.mean + self.m2 += delta * delta2 + self.min_val = np.minimum(self.min_val, sample) + self.max_val = np.maximum(self.max_val, sample) + + def get_std(self) -> np.ndarray: + if self.count < 2: + return np.zeros(self.dim, dtype=np.float64) + return np.sqrt(self.m2 / (self.count - 1)) + + def as_dict(self) -> dict: + return { + "mean": self.mean.tolist(), + "std": self.get_std().tolist(), + "min": self.min_val.tolist(), + "max": self.max_val.tolist(), + "count": self.count, + } + + +# --------------------------------------------------------------------------- +# Dataset creation helpers +# --------------------------------------------------------------------------- +def _create_camera_dataset( + split: str, + rotation_format: RotationConvention, + pose_convention: PoseConvention, + credential_path: str, + wdinfo_names: list[str] | None, +): + from cosmos.data.vfm.action.camera_dataset_sharded import ( + CAMERA_WDINFOS, + CameraDatasetSharded, + ) + + if wdinfo_names: + wdinfo_paths = [CAMERA_WDINFOS[n] for n in wdinfo_names] + else: + wdinfo_paths = [CAMERA_WDINFOS["pretrained_clips_260307_100k_filtered"]] + + ds = CameraDatasetSharded( + wdinfo_paths=wdinfo_paths, + split=split, + shuffle=False, + fix_caption=True, + mode="forward_dynamics", + rotation_format=rotation_format, + pose_convention=pose_convention, + credential_path=credential_path, + translation_scale=1.0, + rotation_scale=1.0, + ) + print(f"CameraDatasetSharded wdinfos={wdinfo_names or ['pretrained_clips_260307_100k_filtered']}") + return ds + + +def _create_av_dataset( + split: str, + rotation_format: RotationConvention, + pose_convention: PoseConvention, + credential_path: str, + av_root: str, + av_history_len: float, + av_future_len: float, + av_fps: int, +): + from cosmos.data.vfm.action.av_dataset import AVDataset + + ds = AVDataset( + root=av_root, + split=split, + fps=av_fps, + mode="policy", + history_len=av_history_len, + future_len=av_future_len, + rotation_format=rotation_format, + pose_convention=pose_convention, + credential_path=credential_path, + shuffle=False, + include_route_in_prompt=False, + translation_scale=1.0, + rotation_scale=1.0, + ) + print(f"AVDataset root={av_root}") + return ds + + +# --------------------------------------------------------------------------- +# Core computation +# --------------------------------------------------------------------------- +def _summarize_block( + kept_values: list[np.ndarray], + per_timestep_values: list[list[np.ndarray]], + chunk_length: int, + block_label: str, +) -> tuple[dict, dict]: + """Build the per-dim and L2-norm summary dicts for one (already-filtered) action block.""" + percentiles = [5, 10, 25, 50, 75, 90, 95] + + concat = np.concatenate(kept_values, axis=0) if kept_values else np.zeros((0, 0), dtype=np.float64) + if concat.size == 0: + raise RuntimeError(f"No {block_label} frames left after filtering") + + dim = concat.shape[1] + count = int(concat.shape[0]) + + global_mean = concat.mean(axis=0).tolist() + global_median = np.median(concat, axis=0).tolist() + global_std = (concat.std(axis=0, ddof=1) if count > 1 else np.zeros(dim)).tolist() + global_min = concat.min(axis=0).tolist() + global_max = concat.max(axis=0).tolist() + + # Single-scalar summary from the flattened pool. This is the RMS per element + # (about the pool mean) — the right quantity for picking one global scale + # factor that preserves the vector's internal geometry. + flat = concat.reshape(-1) + flat_mean = float(flat.mean()) + flat_std = float(flat.std(ddof=1)) if flat.size > 1 else 0.0 + + zero_dim = [0.0] * dim + per_timestep_means: list[list[float]] = [] + per_timestep_medians: list[list[float]] = [] + per_timestep_stds: list[list[float]] = [] + per_timestep_mins: list[list[float]] = [] + per_timestep_maxs: list[list[float]] = [] + for t in range(chunk_length): + vals = per_timestep_values[t] + if not vals: + per_timestep_means.append(zero_dim) + per_timestep_medians.append(zero_dim) + per_timestep_stds.append(zero_dim) + per_timestep_mins.append(zero_dim) + per_timestep_maxs.append(zero_dim) + continue + arr = np.stack(vals) + per_timestep_means.append(arr.mean(axis=0).tolist()) + per_timestep_medians.append(np.median(arr, axis=0).tolist()) + per_timestep_stds.append((arr.std(axis=0, ddof=1) if arr.shape[0] > 1 else np.zeros(dim)).tolist()) + per_timestep_mins.append(arr.min(axis=0).tolist()) + per_timestep_maxs.append(arr.max(axis=0).tolist()) + + l2_norms = np.linalg.norm(concat, axis=-1) # [N_kept] + + global_dict = { + "mean": global_mean, + "std": global_std, + "min": global_min, + "max": global_max, + "count": count, + "median": global_median, + "flat_mean": flat_mean, + "flat_std": flat_std, + } + + scale_name = "translation_scale" if block_label == "translation" else "rotation_scale" + raw_stats = { + "description": ( + f"Per-dim statistics on raw action {block_label} block (translation_scale=1.0, rotation_scale=1.0). " + f"Use flat_std (single scalar across all dims) to choose {scale_name} when you want a uniform " + f"scale that preserves the block's internal geometry; use per-dim std when per-dim rescaling is acceptable." + ), + "global": global_dict, + "per_timestep": { + "mean": per_timestep_means, + "median": per_timestep_medians, + "std": per_timestep_stds, + "min": per_timestep_mins, + "max": per_timestep_maxs, + }, + } + l2_stats = { + "description": f"L2 norm of raw {block_label} vectors across all frames.", + "median": float(np.median(l2_norms)), + "mean": float(np.mean(l2_norms)), + "std": float(np.std(l2_norms, ddof=1)) if len(l2_norms) > 1 else 0.0, + "min": float(np.min(l2_norms)), + "max": float(np.max(l2_norms)), + "percentiles": {str(p): float(np.percentile(l2_norms, p)) for p in percentiles}, + } + return raw_stats, l2_stats + + +def compute_action_stats( + dataset, + max_samples: int | None = None, + max_frames: int | None = None, + max_trans_norm: float | None = None, + max_rot_norm: float | None = None, + percentile_clip: float | None = None, +) -> dict: + """Iterate over *dataset* and collect raw action translation and rotation statistics. + + The dataset must be created with ``translation_scale=1.0`` and + ``rotation_scale=1.0`` so that the returned actions contain unmodified + translation and rotation values. + + If *max_frames* is given, each sample's action tensor is truncated to + the first *max_frames* frames before statistics are accumulated. + + Outlier filtering (applied per-frame, not per-sample): + * ``max_trans_norm``: drop frames whose translation L2 norm exceeds this value. + * ``max_rot_norm``: drop frames whose rotation L2 norm exceeds this value. + * ``percentile_clip``: if set (e.g. 99), the thresholds default to the P{n} + of the corresponding L2-norm distribution from the data actually seen. + Explicit ``max_*_norm`` arguments take precedence over this. + + Returns a dict ready for JSON serialisation. + """ + all_translations: list[np.ndarray] = [] + all_rotations: list[np.ndarray] = [] + chunk_length: int | None = None + rotation_dim: int | None = None + sample_count = 0 + start = time.time() + + pbar = tqdm(desc="Reading action tensors", unit="samples") + for sample in dataset: + action = sample["action"] + if isinstance(action, torch.Tensor): + action = action.detach().cpu().numpy() + if action.ndim == 1: + action = action.reshape(1, -1) + if max_frames is not None: + action = action[:max_frames] + + trans = action[:, :TRANSLATION_DIM].astype(np.float64) # [T, 3] + rot = action[:, TRANSLATION_DIM:].astype(np.float64) # [T, D_rot] + all_translations.append(trans) + all_rotations.append(rot) + + if rotation_dim is None: + rotation_dim = rot.shape[1] + if chunk_length is None: + chunk_length = trans.shape[0] + print( + f" action shape : {action.shape} (first {TRANSLATION_DIM} dims = translation, next {rotation_dim} = rotation)" + ) + print(f" chunk_length : {chunk_length}") + print(f" rotation_dim : {rotation_dim}") + + sample_count += 1 + pbar.update(1) + if sample_count % 1000 == 0: + elapsed = time.time() - start + pbar.set_postfix(rate=f"{sample_count / elapsed:.1f} s/s") + + if max_samples is not None and sample_count >= max_samples: + print(f"\nReached max_samples={max_samples}") + break + + pbar.close() + elapsed = time.time() - start + + if not all_translations: + raise RuntimeError("No samples processed – dataset is empty or no actions found") + assert chunk_length is not None and rotation_dim is not None + + print(f"\nProcessed {sample_count} samples in {elapsed:.1f}s ({sample_count / elapsed:.1f} samples/s)") + + # Per-frame L2 norms used for filtering and percentile-based threshold derivation. + trans_norms_per_sample = [np.linalg.norm(t, axis=-1) for t in all_translations] + rot_norms_per_sample = [np.linalg.norm(r, axis=-1) for r in all_rotations] + all_trans_norms = np.concatenate(trans_norms_per_sample) + all_rot_norms = np.concatenate(rot_norms_per_sample) + total_frames = int(all_trans_norms.shape[0]) + + if percentile_clip is not None: + if not (0.0 < percentile_clip <= 100.0): + raise ValueError(f"percentile_clip must be in (0, 100], got {percentile_clip}") + if max_trans_norm is None: + max_trans_norm = float(np.percentile(all_trans_norms, percentile_clip)) + print(f" Auto-derived max_trans_norm at P{percentile_clip:g} = {max_trans_norm:.6f}") + if max_rot_norm is None: + max_rot_norm = float(np.percentile(all_rot_norms, percentile_clip)) + print(f" Auto-derived max_rot_norm at P{percentile_clip:g} = {max_rot_norm:.6f}") + + kept_trans: list[np.ndarray] = [] + kept_rotations: list[np.ndarray] = [] + per_timestep_trans: list[list[np.ndarray]] = [[] for _ in range(chunk_length)] + per_timestep_rot: list[list[np.ndarray]] = [[] for _ in range(chunk_length)] + kept_frame_count = 0 + for trans, rot, tn, rn in zip(all_translations, all_rotations, trans_norms_per_sample, rot_norms_per_sample): + mask = np.ones(trans.shape[0], dtype=bool) + if max_trans_norm is not None: + mask &= tn <= max_trans_norm + if max_rot_norm is not None: + mask &= rn <= max_rot_norm + if mask.any(): + kept_trans.append(trans[mask]) + kept_rotations.append(rot[mask]) + kept_frame_count += int(mask.sum()) + upper = min(chunk_length, trans.shape[0]) + for t in range(upper): + if mask[t]: + per_timestep_trans[t].append(trans[t]) + per_timestep_rot[t].append(rot[t]) + + dropped = total_frames - kept_frame_count + filter_active = max_trans_norm is not None or max_rot_norm is not None + if filter_active: + pct = (100.0 * dropped / total_frames) if total_frames else 0.0 + print(f"\nOutlier filter: kept {kept_frame_count} / {total_frames} frames (dropped {dropped}, {pct:.2f}%)") + print(f" thresholds: max_trans_norm={max_trans_norm}, max_rot_norm={max_rot_norm}") + + raw_trans_stats, trans_l2_stats = _summarize_block(kept_trans, per_timestep_trans, chunk_length, "translation") + raw_rot_stats, rot_l2_stats = _summarize_block(kept_rotations, per_timestep_rot, chunk_length, "rotation") + + return { + "metadata": { + "translation_dim": TRANSLATION_DIM, + "rotation_dim": rotation_dim, + "chunk_length": chunk_length, + "num_samples": sample_count, + "total_frames": total_frames, + "kept_frames": kept_frame_count, + "dropped_frames": dropped, + "max_trans_norm": max_trans_norm, + "max_rot_norm": max_rot_norm, + "percentile_clip": percentile_clip, + "processing_time_s": round(elapsed, 2), + }, + "raw_translation_stats": raw_trans_stats, + "translation_l2_norm": trans_l2_stats, + "raw_rotation_stats": raw_rot_stats, + "rotation_l2_norm": rot_l2_stats, + } + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Compute raw action translation statistics for camera / AV pose datasets.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--dataset", required=True, choices=["camera", "av"]) + p.add_argument("--split", default="train", choices=["train", "val", "full"]) + p.add_argument("--output", default=None, help="Output JSON path (auto-generated if omitted)") + p.add_argument("--max-samples", type=int, default=None) + p.add_argument( + "--max-frames", + type=int, + default=None, + help="Use only the first N frames per sample (e.g. 17). All frames used if omitted.", + ) + + # Pose options + p.add_argument( + "--rotation-format", default="rot6d", choices=["rot9d", "rot6d", "quat_xyzw", "euler_xyz", "axisangle"] + ) + p.add_argument( + "--pose-convention", + default="backward_framewise", + choices=["backward_anchored", "backward_framewise"], + ) + p.add_argument("--credential-path", default="credentials/gcp_training.secret") + + # AV-specific + p.add_argument("--av-root", default="s3://nv-00-10206-robot/cosmos3_action_data/av_v2_02182026_wdinfo/") + p.add_argument("--av-history-len", type=float, default=0.1) + p.add_argument("--av-future-len", type=float, default=6.0) + p.add_argument("--av-fps", type=int, default=10) + + # Camera-specific + p.add_argument( + "--camera-wdinfos", + nargs="*", + default=None, + help="Camera wdinfo keys (default: pretrained_clips_260307_100k). " + "See CAMERA_WDINFOS in camera_dataset_sharded.py for available keys.", + ) + + # Outlier filtering (applied per-frame, not per-sample). + p.add_argument( + "--max-trans-norm", + type=float, + default=None, + help="Drop frames whose translation L2 norm exceeds this value. Takes precedence over --percentile-clip.", + ) + p.add_argument( + "--max-rot-norm", + type=float, + default=None, + help="Drop frames whose rotation L2 norm exceeds this value. Takes precedence over --percentile-clip.", + ) + p.add_argument( + "--percentile-clip", + type=float, + default=None, + help="Auto-derive max_trans_norm / max_rot_norm from this percentile " + "(e.g. 99 or 99.5) of the observed L2-norm distributions. " + "Ignored for a block if --max-{trans,rot}-norm is given explicitly.", + ) + + return p.parse_args() + + +def main() -> None: + args = parse_args() + + tag = f"{args.dataset}_{args.rotation_format}_{args.pose_convention}" + if args.output: + output_path = Path(args.output) + else: + DEFAULT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + output_path = DEFAULT_OUTPUT_DIR / f"pose_stats_{tag}_{args.split}.json" + + print(f"Dataset : {args.dataset}") + print(f"Split : {args.split}") + print(f"Rotation format: {args.rotation_format}") + print(f"Rel pose format: {args.pose_convention}") + print(f"Max samples : {args.max_samples}") + print(f"Max frames : {args.max_frames}") + print(f"Normalization : none (raw values, translation_scale=1.0, rotation_scale=1.0)") + filter_desc_parts: list[str] = [] + if args.max_trans_norm is not None: + filter_desc_parts.append(f"max_trans_norm={args.max_trans_norm}") + if args.max_rot_norm is not None: + filter_desc_parts.append(f"max_rot_norm={args.max_rot_norm}") + if args.percentile_clip is not None: + filter_desc_parts.append(f"percentile_clip=P{args.percentile_clip:g}") + print(f"Outlier filter : {', '.join(filter_desc_parts) if filter_desc_parts else 'off'}") + print() + + rotation_fmt: RotationConvention = args.rotation_format # type: ignore[assignment] + pose_conv: PoseConvention = args.pose_convention # type: ignore[assignment] + + if args.dataset == "camera": + dataset = _create_camera_dataset( + split=args.split, + rotation_format=rotation_fmt, + pose_convention=pose_conv, + credential_path=args.credential_path, + wdinfo_names=args.camera_wdinfos, + ) + else: + dataset = _create_av_dataset( + split=args.split, + rotation_format=rotation_fmt, + pose_convention=pose_conv, + credential_path=args.credential_path, + av_root=args.av_root, + av_history_len=args.av_history_len, + av_future_len=args.av_future_len, + av_fps=args.av_fps, + ) + + print(f"Dataset length : {len(dataset)}") + print() + + results = compute_action_stats( + dataset, + max_samples=args.max_samples, + max_frames=args.max_frames, + max_trans_norm=args.max_trans_norm, + max_rot_norm=args.max_rot_norm, + percentile_clip=args.percentile_clip, + ) + + results["metadata"]["dataset"] = args.dataset + results["metadata"]["split"] = args.split + results["metadata"]["max_frames"] = args.max_frames + results["metadata"]["rotation_format"] = args.rotation_format + results["metadata"]["pose_convention"] = args.pose_convention + if args.dataset == "av": + results["metadata"]["av_root"] = args.av_root + results["metadata"]["av_history_len"] = args.av_history_len + results["metadata"]["av_future_len"] = args.av_future_len + results["metadata"]["av_fps"] = args.av_fps + else: + results["metadata"]["camera_wdinfos"] = args.camera_wdinfos or ["pretrained_clips_260307_100k"] + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w") as f: + json.dump(results, f, indent=2) + + print(f"\nResults saved to: {output_path}") + + meta = results["metadata"] + if meta["dropped_frames"] > 0: + pct = 100.0 * meta["dropped_frames"] / meta["total_frames"] if meta["total_frames"] else 0.0 + print( + f"Filtered frames: kept {meta['kept_frames']} / {meta['total_frames']} " + f"(dropped {meta['dropped_frames']}, {pct:.2f}%) — " + f"thresholds max_trans_norm={meta['max_trans_norm']}, max_rot_norm={meta['max_rot_norm']}" + ) + + def _print_block(label: str, raw_key: str, l2_key: str) -> None: + g = results[raw_key]["global"] + n = results[l2_key] + print(f"\nRaw {label} per-dim statistics — translation_scale=1.0, rotation_scale=1.0:") + print(f" Mean : {g['mean']}") + print(f" Median : {g['median']}") + print(f" Std : {g['std']}") + print(f" Min : {g['min']}") + print(f" Max : {g['max']}") + print(f" Flat mean : {g['flat_mean']:.6f} (pooled across all dims)") + print(f" Flat std : {g['flat_std']:.6f} (single global scalar, preserves geometry)") + print(f" Count : {g['count']} frames from {results['metadata']['num_samples']} samples") + print(f"\n{label.capitalize()} L2 norm:") + print(f" Median : {n['median']:.6f}") + print(f" Mean : {n['mean']:.6f}") + print(f" Std : {n['std']:.6f}") + print(f" Min : {n['min']:.6f}") + print(f" Max : {n['max']:.6f}") + for pct, val in n["percentiles"].items(): + print(f" P{pct:<5}: {val:.6f}") + + _print_block("translation (tx, ty, tz)", "raw_translation_stats", "translation_l2_norm") + _print_block( + f"rotation ({args.rotation_format}, {results['metadata']['rotation_dim']} dims)", + "raw_rotation_stats", + "rotation_l2_norm", + ) + + trans_flat_std = results["raw_translation_stats"]["global"]["flat_std"] + rot_flat_std = results["raw_rotation_stats"]["global"]["flat_std"] + print("\nSuggested uniform scales (matches MSE-loss magnitude per dim, preserves block geometry):") + print( + f" translation_scale = 1 / trans_flat_std = {1.0 / trans_flat_std:.6f}" + if trans_flat_std > 0 + else " translation_scale : undefined (trans_flat_std=0)" + ) + print( + f" rotation_scale = 1 / rot_flat_std = {1.0 / rot_flat_std:.6f}" + if rot_flat_std > 0 + else " rotation_scale : undefined (rot_flat_std=0)" + ) + if trans_flat_std > 0 and rot_flat_std > 0: + print( + f" (equivalently, with translation_scale=1: rotation_scale = trans_flat_std / rot_flat_std = {trans_flat_std / rot_flat_std:.6f})" + ) + + +if __name__ == "__main__": + main() diff --git a/cosmos_training/cosmos/data/vfm/action/cosmos3_action_lerobot.py b/cosmos_training/cosmos/data/vfm/action/cosmos3_action_lerobot.py new file mode 100644 index 00000000..ceebeb9d --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/cosmos3_action_lerobot.py @@ -0,0 +1,1000 @@ +"""Shared LeRobot adapter utilities for Action datasets. + +These helpers centralize common behavior across Action wrappers: +- deterministic train/val episode splitting +- valid per-episode index range construction +- a reusable BaseActionLeRobotDataset class with lazy init, video formatting, + and common result building +""" + +from __future__ import annotations + +import importlib +import logging as _logging +import math +import os as _os +import random +from bisect import bisect_right +from collections import OrderedDict, defaultdict +from collections.abc import Callable, Sequence +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from threading import Lock +from typing import Any, ClassVar, Literal + +import huggingface_hub.constants as _hf_const +import numpy as np +import torch +from lerobot.datasets.lerobot_dataset import LeRobotDataset, LeRobotDatasetMetadata +from torch.utils.data import Dataset + +_hf_offline_applied = False + + +def _ensure_hf_hub_offline() -> None: + """Force HF Hub into offline mode for local-only datasets (repo_id="local"). + + Sets the ``HF_HUB_OFFLINE`` env var (for any future imports in worker + processes), patches the already-imported constant, and suppresses the + expected "Returning existing local_dir" fallback warning. + + Safe to call multiple times; only applies once per process. + """ + global _hf_offline_applied + if _hf_offline_applied: + return + if "HF_HUB_OFFLINE" not in _os.environ: + _os.environ["HF_HUB_OFFLINE"] = "1" + if not _hf_const.HF_HUB_OFFLINE: + _hf_const.HF_HUB_OFFLINE = True + _logging.getLogger("huggingface_hub._snapshot_download").setLevel(_logging.ERROR) + _hf_offline_applied = True + + +from functools import cached_property + +from cosmos.utils import log +from cosmos.data.vfm.action.action_normalization import ( + load_action_stats, + normalize_action, +) + +# Re-export the action_spec DSL from this module so that subclass datasets +# only need a single import block (alongside ``BaseActionLeRobotDataset``). +from cosmos.data.vfm.action.action_spec import ( # noqa: F401 (re-export) + ActionSpec, + DimType, + Gripper, + Joint, + Pos, + Reserved, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.pose_utils import compute_idle_frames +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint +from projects.cosmos3.vfm.scripts.action.memprofile import ( + deep_size as _deep_size, +) +from projects.cosmos3.vfm.scripts.action.memprofile import ( + fmt_mb as _fmt_mb, +) +from projects.cosmos3.vfm.scripts.action.memprofile import ( + log_worker_memory_breakdown, + rss_tracker, +) +from projects.cosmos3.vfm.scripts.action.memprofile import ( + memprofile_enabled as _memprofile_enabled, +) + +# --------------------------------------------------------------------------- +# LRU-capped VideoDecoderCache +# --------------------------------------------------------------------------- +_LRU_VIDEO_CACHE_MAX_SIZE: int = 64 +_LRU_DATASET_MAX_LOADED: int = 32 +ActionNormalization = Literal["quantile", "quantile_rot", "meanstd", "minmax"] +_ACTION_NORMALIZATION_CHOICES: tuple[str, ...] = ("quantile", "quantile_rot", "meanstd", "minmax") + +_decoder_cache_patched = False + + +class _LRUVideoDecoderCache: + """Drop-in replacement for ``lerobot.datasets.video_utils.VideoDecoderCache`` + with LRU eviction. When the cache exceeds *max_size* entries the + least-recently-used decoder (and its file handle) is evicted. + """ + + def __init__(self, max_size: int = _LRU_VIDEO_CACHE_MAX_SIZE) -> None: + self._max_size = max_size + self._cache: OrderedDict[str, tuple[Any, Any]] = OrderedDict() + self._lock = Lock() + self._hits = 0 + self._misses = 0 + self._evictions = 0 + + def get_decoder(self, video_path: str) -> Any: + if importlib.util.find_spec("torchcodec"): # type: ignore[attr-defined] + from torchcodec.decoders import VideoDecoder + else: + raise ImportError("torchcodec is required but not available.") + + import fsspec + + video_path = str(video_path) + + with self._lock: + if video_path in self._cache: + self._cache.move_to_end(video_path) + self._hits += 1 + return self._cache[video_path][0] + + self._misses += 1 + file_handle = fsspec.open(video_path).__enter__() + decoder = VideoDecoder(file_handle, seek_mode="approximate") # type: ignore[arg-type] + self._cache[video_path] = (decoder, file_handle) + + evicted = 0 + while len(self._cache) > self._max_size: + _, (_, old_fh) = self._cache.popitem(last=False) + try: + old_fh.close() + except Exception: + pass + evicted += 1 + self._evictions += evicted + + if evicted and self._evictions % 50 <= evicted: + log.debug( + f"[VideoDecoderCache pid={_os.getpid()}] " + f"evicted={self._evictions} total, size={len(self._cache)}/{self._max_size}, " + f"hits={self._hits}, misses={self._misses}, " + f"hit_rate={100 * self._hits / max(1, self._hits + self._misses):.1f}%" + ) + + return decoder + + def clear(self) -> None: + with self._lock: + for _, file_handle in self._cache.values(): + try: + file_handle.close() + except Exception: + pass + self._cache.clear() + + def size(self) -> int: + with self._lock: + return len(self._cache) + + +def _patch_decoder_cache(max_size: int = _LRU_VIDEO_CACHE_MAX_SIZE) -> None: + """Replace the module-level ``_default_decoder_cache`` in LeRobot with an + LRU-capped version to prevent unbounded memory growth in workers.""" + global _decoder_cache_patched + if _decoder_cache_patched: + return + + import lerobot.datasets.video_utils as _vu + + lru_cache = _LRUVideoDecoderCache(max_size=max_size) + _vu._default_decoder_cache = lru_cache + _decoder_cache_patched = True + log.debug(f"Patched LeRobot VideoDecoderCache with LRU max_size={max_size}") + + +def _parallel_map( + fn: Callable[[Any], Any], + items: list[Any], + *, + max_workers: int, + label: str, +) -> list[Any]: + """Thread-pool ``map`` — returns results in input order. + + Intended for IO-bound prefetch (``LeRobotDatasetMetadata`` loads, + parquet column reads). Preserves item-order so callers can ``zip`` + with their ``indices`` / ``roots`` list. Skips the thread pool + entirely when there is 0 or 1 task — avoids per-worker + ``ThreadPoolExecutor`` setup cost and log spam under + ``shard_across_workers=True`` where each worker typically gets + only 1-2 shards. + """ + if not items: + return [] + if len(items) == 1 or max_workers <= 1: + return [fn(items[0])] if len(items) == 1 else [fn(x) for x in items] + log.info(f"{label}: {len(items)} tasks (workers={max_workers})") + with ThreadPoolExecutor(max_workers=max_workers) as ex: + return list(ex.map(fn, items)) + + +def split_episode_ids(total_episodes: int, seed: int, val_ratio: float, split: str) -> list[int]: + """Create deterministic random episode ids for train/val/full splits.""" + num_val = int(round(total_episodes * val_ratio)) + g = torch.Generator().manual_seed(seed) + episode_ids = torch.randperm(total_episodes, generator=g).tolist() + + if split == "train": + return episode_ids[num_val:] + if split == "val": + return episode_ids[:num_val] + return episode_ids + + +def build_episode_spans( + episodes: Any, + episode_ids: Sequence[int], + chunk_length: int, + sample_stride: int = 1, +) -> tuple[list[tuple[int, int, int]], int, int]: + """Build valid episode spans for LeRobot frame queries. + + Returns: + - episode spans as ``(episode_id, sample_start, valid_len)`` + - total valid sample count across selected episodes + - total raw frame count across selected episodes + """ + assert sample_stride >= 1, f"sample_stride must be >= 1, got {sample_stride}" + + dataset_from_index = list(episodes["dataset_from_index"]) + dataset_to_index = list(episodes["dataset_to_index"]) + length = list(episodes["length"]) + + spans: list[tuple[int, int, int]] = [] + valid_count = 0 + sample_count = 0 + for episode_id in episode_ids: + start = dataset_from_index[episode_id] + stop = dataset_to_index[episode_id] + raw_valid_len = stop - start - chunk_length + if raw_valid_len > 0: + valid_len = (raw_valid_len + sample_stride - 1) // sample_stride + spans.append((episode_id, start, valid_len)) + valid_count += valid_len + sample_count += int(length[episode_id]) + + return spans, valid_count, sample_count + + +def _normalize_split(split: str) -> str: + """Normalize split name to one of ``'train'``, ``'val'``, ``'full'``.""" + s = split.lower().strip() + if s in {"val", "valid", "validation", "eval", "test"}: + return "val" + if s in {"train", "full"}: + return s + raise ValueError(f"Unsupported {split=}. Use train/val/full.") + + +class BaseActionLeRobotDataset(Dataset): + """Reusable base class for Action LeRobot-backed map-style datasets. + + Subclasses typically: + 1) call ``_register_source`` to register one or more LeRobot sources + 2) implement ``__getitem__`` for dataset-specific sample parsing + 3) call ``_build_result`` to assemble the return dict + """ + + # Applied as: R_opencv = R_native @ _to_opencv + # Subclasses override in __init__; default is identity (no correction). + + # Bundled normalization stats directory. Stats are committed at + # ``<_NORMALIZERS_DIR>/__.json`` (flat + # layout matching the existing UMI files) and produced by + # ``projects/cosmos3/vfm/datasets/action/compute_action_stats.py``. + # Subclasses that need a different filename scheme can override + # :meth:`_normalizer_filename`. + _NORMALIZERS_DIR: ClassVar[Path] = Path(__file__).parent / "normalizers" + + def __init__( + self, + *, + fps: float, + chunk_length: int, + split_seed: int, + split_val_ratio: float, + split: str, + mode: str, + embodiment_type: str, + viewpoint: Viewpoint, + pose_convention: str | None = None, + rotation_format: str | None = None, + action_normalization: ActionNormalization | None = None, + tolerance_s: float = 1e-4, + max_loaded_datasets: int = _LRU_DATASET_MAX_LOADED, + skip_video_loading: bool = False, + sample_stride: int = 1, + enable_fast_init: bool = False, + fast_init_max_workers: int = 64, + ) -> None: + super().__init__() + _ensure_hf_hub_offline() + _patch_decoder_cache() + self._memprofile = _memprofile_enabled() + + assert sample_stride >= 1, f"sample_stride must be >= 1, got {sample_stride}" + assert fast_init_max_workers >= 1, f"fast_init_max_workers must be >= 1, got {fast_init_max_workers}" + assert action_normalization is None or action_normalization in _ACTION_NORMALIZATION_CHOICES, ( + f"action_normalization must be None or one of {_ACTION_NORMALIZATION_CHOICES}, got {action_normalization!r}" + ) + + with rss_tracker(f"{self.__class__.__name__}.__init__", enabled=self._memprofile): + self._fps = fps + self._dt = 1.0 / fps + self._chunk_length = chunk_length + self._split_seed = split_seed + self._split_val_ratio = split_val_ratio + self._split = _normalize_split(split) + self._mode = mode + self._embodiment_type = embodiment_type + self._viewpoint: Viewpoint = viewpoint + self._pose_convention = pose_convention + self._rotation_format = rotation_format + self._action_normalization = action_normalization + # Lazy-loaded stats cache, populated on first call to + # :meth:`_normalize_action`. Per-process (workers get their own). + self._norm_stats: dict[str, torch.Tensor] | None = None + self._tolerance_s = tolerance_s + self._max_loaded_datasets = max_loaded_datasets + self._skip_video_loading = skip_video_loading + self._sample_stride = sample_stride + self._enable_fast_init = enable_fast_init + self._fast_init_max_workers = fast_init_max_workers + self._delta_timestamps: dict[str, list[float]] = {} + self._to_opencv: np.ndarray | dict[str, np.ndarray] = np.eye(3, dtype=np.float32) + + if pose_convention is None: + log.warning( + f"{self.__class__.__name__}: pose_convention is not set. " + "Consider specifying 'backward_framewise' or 'backward_anchored'." + ) + + self._datasets: list[LeRobotDataset | None] = [] + self._dataset_build_args: list[dict[str, Any] | None] = [] + self._loaded_lru: OrderedDict[int, None] = OrderedDict() + + # -- Flat index structures (populated by _append_index_records) -- + # Together these two lists form a searchable map from a flat + # global index to (dataset, row, episode, frame). One entry per + # episode span across *all* registered sources. + # + # _episode_records[i] = (ds_idx, sample_start, valid_len, episode_id) + # ds_idx – which source dataset (index into _datasets) + # sample_start – first row of this span in that dataset's table + # valid_len – number of usable frames in this span + # episode_id – the episode this span belongs to + # + # _episode_cum_ends[i] = running total of valid_len through span i + # Used for O(log N) lookup via bisect_right in _resolve_index. + self._episode_records: list[tuple[int, int, int, int]] = [] + self._episode_cum_ends: list[int] = [] + self._num_valid_indices = 0 + self._domain_id = get_domain_id(self._embodiment_type) + + # Deferred-init shard roots — a list of root paths. + # Subclasses populate this in __init__; _register_sources() + # reads _delta_timestamps and _tolerance_s from self (both + # initialised above, with _delta_timestamps overridden by + # each subclass). + # ActionUnifiedIterableDataset.assign_worker uses len() for + # round-robin shard distribution and _register_sources(indices) + # for deferred loading. When empty, shard distribution is + # skipped (every worker iterates the full dataset). + self._all_shard_roots: list[str] = [] + + # -- public properties --------------------------------------------------- + + @property + def fps(self) -> float: + return self._fps + + @property + def chunk_length(self) -> int: + return self._chunk_length + + @property + def split(self) -> str: + return self._split + + @property + def mode(self) -> str: + return self._mode + + @mode.setter + def mode(self, value: str) -> None: + self._mode = value + + @property + def domain_id(self) -> int: + return self._domain_id + + # -- source registration ------------------------------------------------- + + def _register_source( + self, + *, + delta_timestamps: dict[str, list[float]], + tolerance_s: float, + root: str | None = None, + repo_id: str = "local", + force_cache_sync: bool = False, + download_videos: bool = False, + video_backend: str | None = None, + revision: str | None = None, + dataset_label: str | None = None, + prefetched_meta: LeRobotDatasetMetadata | None = None, + ) -> LeRobotDatasetMetadata: + """Register a LeRobot dataset source lazily (metadata-only at init). + + ``prefetched_meta`` lets subclasses load metadata in a thread pool + (``LeRobotDatasetMetadata`` reads are pure I/O — ``info.json`` + + ``episodes.parquet`` + ``tasks.parquet``) and then hand the ready + object to the serial append-path below, which still manages the + order-sensitive shared state (``_datasets`` / ``_dataset_build_args`` + / ``_episode_records`` / ``_episode_cum_ends``). When ``None`` the + caller gets the original single-threaded behavior. + """ + label_str = f" [{dataset_label}]" if dataset_label else "" + cls = self.__class__.__name__ + # "local" is not a valid PEP 440 version, so LeRobot's + # is_valid_version() check skips the get_safe_version() HF API call. + if repo_id == "local" and revision is None: + revision = "local" + + with rss_tracker(f"{cls}{label_str} — metadata load", enabled=self._memprofile): + if prefetched_meta is not None: + meta = prefetched_meta + else: + meta = LeRobotDatasetMetadata( + repo_id=repo_id, + root=root, + revision=revision, + force_cache_sync=force_cache_sync, + ) + ds_idx = len(self._datasets) + self._datasets.append(None) + self._dataset_build_args.append( + { + "repo_id": repo_id, + "root": root, + "delta_timestamps": delta_timestamps, + "tolerance_s": tolerance_s, + "force_cache_sync": force_cache_sync, + "download_videos": download_videos, + "video_backend": video_backend, + "revision": revision, + } + ) + + with rss_tracker( + f"{cls}{label_str} — index records", + enabled=self._memprofile, + extras_fn=lambda: [ + f"episode_records so far: {len(self._episode_records)} entries, " + f"~{_fmt_mb(_deep_size(self._episode_records) / (1024 * 1024))}", + f"episode_cum_ends so far: {len(self._episode_cum_ends)} entries, " + f"~{_fmt_mb(_deep_size(self._episode_cum_ends) / (1024 * 1024))}", + ], + ): + self._append_index_records(meta=meta, ds_idx=ds_idx, dataset_label=dataset_label) + + return meta + + def _append_index_records( + self, + *, + meta: LeRobotDatasetMetadata, + ds_idx: int, + dataset_label: str | None = None, + ) -> None: + """Populate episode split / index records from dataset metadata.""" + episode_ids = split_episode_ids( + total_episodes=meta.total_episodes, + seed=self._split_seed, + val_ratio=self._split_val_ratio, + split=self._split, + ) + + if hasattr(self, "_filter_valid_episodes"): + episode_ids = self._filter_valid_episodes(meta, episode_ids) + episode_spans, valid_count, sample_count = build_episode_spans( + episodes=meta.episodes, + episode_ids=episode_ids, + chunk_length=self._chunk_length, + sample_stride=self._sample_stride, + ) + + class_name = self.__class__.__name__ + label = f" [{dataset_label}]" if dataset_label else "" + log.info(f"{class_name}{label}: split={self._split}, num episodes={len(episode_ids)}") + if sample_count > 0: + log.info( + f"{class_name}{label}: kept {valid_count} / {sample_count} " + f"({100 * valid_count / sample_count:.2f} %) samples" + ) + + for episode_id, sample_start, valid_len in episode_spans: + self._episode_records.append((ds_idx, sample_start, valid_len, episode_id)) + self._num_valid_indices += valid_len + self._episode_cum_ends.append(self._num_valid_indices) + + # -- deferred shard registration ----------------------------------------- + + def _register_sources(self, indices: list[int] | None = None) -> None: + """Register a subset (or all) of the shard roots in ``_all_shard_roots``. + + Called by ``ActionUnifiedIterableDataset.assign_worker`` during training, + or explicitly by eval/visualization scripts after construction. + + ``_all_shard_roots`` is a list of root paths. Per-shard args that are + shared across all shards (``delta_timestamps``, ``tolerance_s``) are + taken from ``self``. Subclasses may override this for extra per-shard + setup (e.g. loading instruction segments). + + When ``enable_fast_init=True``, ``LeRobotDatasetMetadata`` (a pure-IO + read of ``info.json`` + ``episodes.parquet`` + ``tasks.parquet``) is + prefetched in a thread pool and handed to the order-sensitive + serial register loop via ``prefetched_meta=``. Shard count scales + the speedup; for single-shard datasets the two paths are + equivalent. + + Args: + indices: Which entries of ``_all_shard_roots`` to register. + ``None`` means all. + """ + if indices is None: + indices = list(range(len(self._all_shard_roots))) + if not indices: + return + + roots = [self._all_shard_roots[i] for i in indices] + + if self._enable_fast_init: + # ``_ensure_hf_hub_offline`` already ran in ``__init__`` and is + # idempotent; no need to re-invoke here. + workers = max(1, min(self._fast_init_max_workers, len(roots))) + metas: list[LeRobotDatasetMetadata | None] = _parallel_map( + lambda root: LeRobotDatasetMetadata(repo_id="local", root=root, revision="local"), + roots, + max_workers=workers, + label=f"{type(self).__name__}: LeRobotDatasetMetadata prefetch", + ) + else: + metas = [None] * len(roots) + + for root, meta in zip(roots, metas): + label = root.rsplit("/", 1)[-1] if "/" in root else root + self._register_source( + root=root, + delta_timestamps=self._delta_timestamps, + tolerance_s=self._tolerance_s, + dataset_label=label, + prefetched_meta=meta, + ) + + # -- lazy dataset access ------------------------------------------------- + + def _get_dataset(self, ds_idx: int) -> LeRobotDataset: + """Get or lazily construct the LeRobot dataset for the given source index. + + Loaded datasets are tracked with LRU ordering. When the number of + loaded datasets exceeds ``_max_loaded_datasets`` the least-recently-used + dataset is evicted (set back to ``None``) so the GC can reclaim it. + """ + ds = self._datasets[ds_idx] + if ds is not None: + self._loaded_lru.move_to_end(ds_idx) + return ds + + _ensure_hf_hub_offline() + + build_args = self._dataset_build_args[ds_idx] + if build_args is None: + raise RuntimeError(f"Missing dataset build args for dataset index {ds_idx}") + + # Evict least-recently-used datasets before loading a new one. + while len(self._loaded_lru) >= self._max_loaded_datasets: + evict_idx, _ = self._loaded_lru.popitem(last=False) + self._datasets[evict_idx] = None + + with rss_tracker( + f"[WORKER {_os.getpid()}] Lazy-loaded ds[{ds_idx}]", + enabled=self._memprofile, + extras_fn=lambda: [f"total loaded={len(self._loaded_lru)}/{len(self._datasets)}"], + ): + delta_ts = build_args["delta_timestamps"] + if self._skip_video_loading: + # Covers both LeRobot v2 (``observation.images.``) and + # v3 (``observation.image.``) video-column conventions. + delta_ts = {k: v for k, v in delta_ts.items() if not k.startswith("observation.image")} + + log.info(f"Loading shard root={build_args['root']}") + ds = LeRobotDataset( + repo_id=build_args["repo_id"], + root=build_args["root"], + delta_timestamps=delta_ts, + tolerance_s=build_args["tolerance_s"], + force_cache_sync=build_args["force_cache_sync"], + download_videos=build_args["download_videos"], + video_backend=build_args["video_backend"], + revision=build_args["revision"], + episodes=None, + ) + if self._skip_video_loading: + ds.meta.info["features"] = { + k: v for k, v in ds.meta.info["features"].items() if v.get("dtype") != "video" + } + self._datasets[ds_idx] = ds + self._loaded_lru[ds_idx] = None + + return ds + + # -- index resolution ---------------------------------------------------- + + def _resolve_index(self, idx: int) -> tuple[int, int, int, int]: + """Map a flat global index to the source dataset, row, episode, and frame. + + Multiple datasets are concatenated into a single virtual sequence. + Each episode contributes a contiguous *span* of valid frames, and + ``_episode_cum_ends[i]`` stores the running total of valid frames + through the *i*-th span. For example, with two episodes of lengths + 5 and 3 the cum-ends are ``[5, 8]``, so global index 6 falls in the + second span at offset 1. + + The lookup is O(log N) via :func:`bisect_right`. + + Returns: + dataset_idx: Which source dataset this sample belongs to. + row_idx: Row index *within* that dataset's LeRobot table. + episode_id: The episode ID for this sample. + frame_offset: Frame offset from the start of the episode span + (0-based). + + Pure index math -- no I/O or dataset access. Higher-level helpers + like :meth:`_fetch_sample` build on this. + """ + # Support negative indexing (e.g. -1 → last sample). + if idx < 0: + idx += self._num_valid_indices + if idx < 0 or idx >= self._num_valid_indices: + raise IndexError(f"{self.__class__.__name__} index {idx} out of range for size {self._num_valid_indices}") + + # _episode_cum_ends is a monotonically increasing list where entry i + # holds the cumulative number of valid frames up to and including the + # i-th episode span. bisect_right finds the first span whose + # cumulative end is strictly greater than idx, i.e. the span that + # contains idx. + # + # Example: cum_ends = [5, 8, 20] + # idx=0 -> span_idx=0 (first span, frames 0..4) + # idx=4 -> span_idx=0 + # idx=5 -> span_idx=1 (second span, frames 5..7) + # idx=8 -> span_idx=2 (third span, frames 8..19) + span_idx = bisect_right(self._episode_cum_ends, idx) + + # The global index where this span begins is the previous span's + # cumulative end (or 0 for the very first span). The frame_offset + # is how far idx is into this particular episode. + span_start = 0 if span_idx == 0 else self._episode_cum_ends[span_idx - 1] + frame_offset = idx - span_start + + # _episode_records[span_idx] stores (dataset_idx, row_start, valid_len, + # episode_id). row_start is the absolute row in the LeRobot table + # where this episode begins. With sample_stride=k, consecutive + # valid indices map to rows k apart inside the episode, so the + # effective row is row_start + frame_offset * sample_stride. + dataset_idx, row_start, _, episode_id = self._episode_records[span_idx] + row_idx = row_start + frame_offset * self._sample_stride + return dataset_idx, row_idx, episode_id, frame_offset + + def _choose_mode(self) -> str: + """Resolve the active mode for one sample request.""" + if self._mode == "joint": + return random.choice(("forward_dynamics", "inverse_dynamics", "policy")) + return self._mode + + def _fetch_sample(self, idx: int) -> tuple[str, int, int, dict[str, Any]]: + """Resolve index, pick a mode, and load the sample from the dataset. + + Returns ``(mode, dataset_idx, row_idx, sample_dict)``. + """ + mode = self._choose_mode() + dataset_idx, row_idx, _, _ = self._resolve_index(idx) + + self._getitem_count = getattr(self, "_getitem_count", 0) + 1 + profile = self._memprofile and self._getitem_count % 50 == 1 + + with rss_tracker( + f"[WORKER {_os.getpid()}] __getitem__ transient (dataset_idx={dataset_idx})", + enabled=profile, + after_fn=lambda: log_worker_memory_breakdown(self), + ): + sample = self._get_dataset(dataset_idx)[row_idx] + + if self._skip_video_loading: + sample = defaultdict(lambda: None, sample) + + return mode, dataset_idx, row_idx, sample + + # -- action normalization ------------------------------------------------ + + def _normalizer_filename(self) -> str: + """Bundled stats filename for this dataset instance. + + Default convention (matches ``compute_action_stats.py`` output): + ``[_][_].json``. + + Pose/rotation suffixes are appended only when the instance actually + has them (SE(3) pose datasets like Bridge / DROID). Joint-space + datasets — where both are ``None`` — resolve to just + ``.json``. + + Subclasses may override when the bundled filename uses a different + scheme (e.g. UMI's ``uva_umi_single_task_normalizer.json``). + """ + if not self._embodiment_type: + raise RuntimeError( + f"{self.__class__.__name__}: embodiment_type is not set; cannot resolve normalizer filename." + ) + parts = [self._embodiment_type] + if self._pose_convention: + parts.append(self._pose_convention) + if self._rotation_format: + parts.append(self._rotation_format) + return "_".join(parts) + ".json" + + def _normalizer_path(self) -> Path: + """Full path to the bundled stats JSON for this dataset.""" + return self._NORMALIZERS_DIR / self._normalizer_filename() + + def _load_norm_stats(self) -> dict[str, torch.Tensor]: + """Lazy-load action normalization stats (once per worker process). + + Raises :class:`FileNotFoundError` if the stats file is missing. This + is intentional — silently falling back to identity normalization when + the user asked for ``quantile`` / ``quantile_rot`` / ``meanstd`` / + ``minmax`` would be a training bug. + """ + if self._norm_stats is not None: + return self._norm_stats + stats_key = "global_raw" if self._action_normalization == "quantile_rot" else "global" + raw = load_action_stats(str(self._normalizer_path()), stats_key=stats_key) + self._norm_stats = {} + for key, value in raw.items(): + self._norm_stats[key] = torch.from_numpy(value).float() # [D] + return self._norm_stats + + def _normalize_action(self, action: torch.Tensor) -> torch.Tensor: + """Apply the configured normalization, or return the raw action. + + - ``action_normalization=None`` → pass-through (used by viewer / debug) + - ``"quantile"`` → ``2·(x − q01) / (q99 − q01) − 1`` clamped to [-1, 1] + - ``"quantile_rot"`` → same as ``"quantile"``, but using ``global_raw`` + stats so rotation dimensions are normalized too. + - ``"meanstd"`` → ``(x − mean) / std`` + - ``"minmax"`` → ``2·(x − min) / (max − min) − 1`` clamped to [-1, 1] + """ + if self._action_normalization is None: + return action + method = "quantile" if self._action_normalization == "quantile_rot" else self._action_normalization + normalized_action = normalize_action( + action, + method, + self._load_norm_stats(), + ) # [T,D] + return normalized_action + + # -- video formatting ---------------------------------------------------- + + def _convert_video(self, video_tchw: torch.Tensor | None) -> torch.Tensor | None: + """Convert LeRobot ``(T,C,H,W)`` float video to Action ``(C,T,H,W)`` uint8. + + Args: + video_tchw: Raw floating-point video tensor in ``[0, 1]`` with + LeRobot layout, or ``None``. # [T,C,H,W] | None + + Returns: + Action-formatted video tensor, or ``None``. # [C,T,H,W] | None + """ + if self._skip_video_loading or video_tchw is None: + return None + if video_tchw.ndim != 4: + raise ValueError( + f"{self.__class__.__name__}._convert_video expected video with shape [T,C,H,W], " + f"got ndim={video_tchw.ndim}" + ) + if not torch.is_floating_point(video_tchw): + raise TypeError( + f"{self.__class__.__name__}._convert_video expected floating-point video in [0, 1], " + f"got dtype={video_tchw.dtype}" + ) + video_min = video_tchw.amin() # [] + video_max = video_tchw.amax() # [] + if video_min.item() < 0.0 or video_max.item() > 1.0: + raise ValueError( + f"{self.__class__.__name__}._convert_video expected floating-point video in [0, 1], " + f"got range=[{video_min.item():.6f}, {video_max.item():.6f}]" + ) + formatted_video = (video_tchw * 255.0).clamp(0.0, 255.0).to(torch.uint8).permute(1, 0, 2, 3) # [C,T,H,W] + return formatted_video + + # -- result building ----------------------------------------------------- + + def _build_action_spec(self) -> ActionSpec | None: + """Subclass override: declare this dataset's action layout. + + Called once per instance — the result is cached by ``self.action_spec``. + Return ``None`` to skip spec-driven idle detection; in that case + ``_compute_idle_frames`` will log a one-time warning and return + ``None`` for every sample. + """ + return None + + @cached_property + def action_spec(self) -> ActionSpec | None: + """Cached :class:`ActionSpec` from ``_build_action_spec``. + + Returns ``None`` when the subclass did not declare one; idle detection + is then skipped (with a one-time warning) until the subclass overrides + ``_build_action_spec``. + """ + return self._build_action_spec() + + @cached_property + def action_names(self) -> list[str] | None: + spec = self.action_spec + return spec.names if spec is not None else None + + # Idle-detection thresholds. Defined as **velocities** (per second) so the + # same numeric value means the same physical motion across datasets with + # different sampling rates; converted to per-frame at call time using + # ``self._fps`` via :meth:`_resolve_idle_thresholds`. + # + # Defaults: + # - ``idle_eps_t_per_sec`` = 5 mm/s (≈ 1 mm/frame at 5 Hz) + # - ``idle_eps_r_per_sec`` = 1.5°/s (geodesic, rotation-format aware) + # - ``idle_eps_g`` = 1e-2 unit gripper Δ (no fps) + # - ``idle_joint_threshold_per_sec`` = 5e-3 rad/s + # - ``idle_min_streak`` = 3 require ≥ 3 consecutive + # + # Subclasses can either override the ``*_per_sec`` attributes (preferred — + # keeps the velocity semantics) or set the corresponding ``idle_eps_*`` / + # ``idle_joint_threshold`` attribute to a non-``None`` value to bypass the + # per-fps conversion entirely (raw per-frame override). + idle_eps_t_per_sec: float = 5e-3 + idle_eps_r_per_sec: float = math.radians(1.5) + idle_eps_g: float = 1e-2 + idle_joint_threshold_per_sec: float = 5e-3 + idle_min_streak: int = 3 + + # Optional per-frame overrides. ``None`` (default) → use the ``*_per_sec`` + # attribute / fps conversion above. + idle_eps_t: float | None = None + idle_eps_r: float | None = None + idle_joint_threshold: float | None = None + + def _resolve_idle_thresholds(self) -> tuple[float, float, float, float]: + """Resolve per-frame idle thresholds for this dataset instance. + + Returns ``(eps_t, eps_r, eps_g, joint_threshold)`` in raw per-frame + units. Honours direct per-frame overrides if the subclass sets the + non-``_per_sec`` attribute; otherwise scales the ``_per_sec`` values + by ``self._fps``. + """ + fps = float(self._fps) if self._fps else 1.0 + eps_t = self.idle_eps_t if self.idle_eps_t is not None else self.idle_eps_t_per_sec / fps + eps_r = self.idle_eps_r if self.idle_eps_r is not None else self.idle_eps_r_per_sec / fps + joint_thr = ( + self.idle_joint_threshold + if self.idle_joint_threshold is not None + else self.idle_joint_threshold_per_sec / fps + ) + return float(eps_t), float(eps_r), float(self.idle_eps_g), float(joint_thr) + + def _compute_idle_frames(self, raw_action: torch.Tensor) -> torch.Tensor | None: + """Count idle frames in the *raw* (un-normalized) action chunk. + + Requires ``self.action_spec`` to be declared via ``_build_action_spec``. + Returns ``None`` when: + - ``pose_convention`` is not ``"backward_framewise"`` (TODO: extend), + - the subclass has not declared an ``ActionSpec`` (logs a one-time warning), + - the action layout does not match the declared spec. + + Detection thresholds come from the ``idle_eps_*`` class attributes + (overridable per dataset). Subclasses can also override this method + outright, or pass an explicit ``idle_frames`` integer via + ``**extras`` to :meth:`_build_result`. + """ + + # conventions (anchored / absolute) need different idle semantics. + if self._pose_convention != "backward_framewise": + if not getattr(self, "_warned_pose_convention", False): + log.warning( + f"Dataset {self.__class__.__name__}: pose_convention=" + f"{self._pose_convention!r} is not 'backward_framewise'; " + "skipping idle-frames detection. Centralize the dataset " + "to backward_framewise to enable IdleFrames captioning." + ) + self._warned_pose_convention = True + return None + + spec = self.action_spec + if spec is None: + if not getattr(self, "_warned_no_action_spec", False): + log.warning( + f"Dataset {self.__class__.__name__} has no action spec defined; " + "skipping idle-frames detection. Override _build_action_spec() to enable it." + ) + self._warned_no_action_spec = True + return None + + eps_t, eps_r, eps_g, joint_thr = self._resolve_idle_thresholds() + try: + n = compute_idle_frames( + raw_action, + spec, + eps_t=eps_t, + eps_r=eps_r, + eps_g=eps_g, + joint_threshold=joint_thr, + min_streak=self.idle_min_streak, + ) + except (ValueError, TypeError) as e: + if not getattr(self, "_warned_action_layout", False): + log.warning( + f"Dataset {self.__class__.__name__}: action layout does " + f"not match the declared ActionSpec " + f"(action_dim={int(raw_action.shape[-1])}, " + f"spec.dim={spec.dim}); skipping idle-frames detection. " + f"Underlying error: {e}" + ) + self._warned_action_layout = True + return None + return torch.tensor(n, dtype=torch.long) + + def _build_result( + self, + *, + mode: str, + video: torch.Tensor | None, + action: torch.Tensor, + ai_caption: str, + **extras: Any, + ) -> dict[str, Any]: + """Assemble the common return dict for ``__getitem__``. + + ``video`` is expected in raw LeRobot layout before final formatting. + Subclasses may pass extra keys (e.g. ``initial_pose``) via ``**extras``. + ``idle_frames`` is auto-computed from the raw (un-normalized) ``action`` + whenever the dataset's pose/rotation conventions allow it; subclasses + can override by passing ``idle_frames`` (int or scalar tensor) via + ``**extras``. + """ + # Compute idle_frames from the raw action before normalization, unless + # the subclass has provided one explicitly via ``**extras``. + if "idle_frames" not in extras: + idle_frames = self._compute_idle_frames(action) + if idle_frames is not None: + extras = {"idle_frames": idle_frames, **extras} + + normalized_action = self._normalize_action(action) # [T,D] + if self._skip_video_loading: + result: dict[str, Any] = {"action": normalized_action} + if "idle_frames" in extras: + result["idle_frames"] = extras["idle_frames"] + return result + formatted_video = self._convert_video(video) # [C,T,H,W] | None + return { + "ai_caption": ai_caption, + "video": formatted_video, + "action": normalized_action, + "conditioning_fps": torch.tensor(self._fps, dtype=torch.long), + "mode": mode, + "domain_id": torch.tensor(self._domain_id, dtype=torch.long), + "viewpoint": self._viewpoint, + **extras, + } + + def __len__(self) -> int: + return self._num_valid_indices diff --git a/cosmos_training/cosmos/data/vfm/action/dataloaders.py b/cosmos_training/cosmos/data/vfm/action/dataloaders.py new file mode 100644 index 00000000..c7e3dcf4 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/dataloaders.py @@ -0,0 +1,160 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import random +import time +from typing import Callable, Iterator + +import numpy as np +import torch +import torch.utils.data + +from cosmos.utils import distributed +from cosmos.data.vfm.action.unified_dataset import ActionUnifiedIterableDataset +from cosmos.data.vfm.joint_dataloader import custom_collate_fn + + +def _action_worker_init_fn( + worker_id: int, seed: int = 42, use_deterministic_seed: bool = True, rank: int = 0, world_size: int = 1 +) -> None: + if use_deterministic_seed: + worker_seed = seed + rank * 9999 + worker_id + else: + worker_seed = int(time.time() * 1000) % (2**32) + rank * 9999 + worker_id + random.seed(worker_seed) + np.random.seed(worker_seed % (2**32)) + torch.manual_seed(worker_seed) + + info = torch.utils.data.get_worker_info() + assert info is not None + dataset = info.dataset + if isinstance(dataset, ActionUnifiedIterableDataset): + dataset.assign_worker(worker_id, info.num_workers, rank, world_size) + + +def create_action_worker_init_fn(seed: int = 42, use_deterministic_seed: bool = True) -> Callable[[int], None]: + """Create a worker_init_fn for Action training with ``ActionUnifiedIterableDataset``. + + Seeds RNGs first, then calls ``dataset.assign_worker()`` to set up + rank-level dataset assignment and worker-level shard distribution. + + Passed to ``DataLoader`` (or ``InfiniteDataLoader``) as the + ``worker_init_fn`` parameter. Only called when ``num_workers > 0``. + + Args: + seed: Base seed for deterministic worker seeding. Ignored when + ``use_deterministic_seed=False`` (time-based seed used instead). + use_deterministic_seed: If True, use the provided seed for reproducible + RNG initialization. If False, derive a time-based seed so that + each resume sees different data. This is preferred for large-scale + runs that resume frequently, and when ``in_order=False`` already + makes iteration order non-deterministic. + + Returns: + A ``worker_init_fn`` suitable for ``torch.utils.data.DataLoader``. + """ + try: + rank = distributed.get_rank() + world_size = distributed.get_world_size() + except RuntimeError: + rank = 0 + world_size = 1 + + return functools.partial( + _action_worker_init_fn, + seed=seed, + use_deterministic_seed=use_deterministic_seed, + rank=rank, + world_size=world_size, + ) + + +class InfiniteDataLoader(torch.utils.data.DataLoader): + """A dataloader that yields forever with proper seeding for reproducibility. + + All Action datasets are ``IterableDataset`` instances (map-style datasets + are automatically wrapped by :class:`~.transforms.MapToIterableAdapter`). + The loader catches ``StopIteration`` and restarts the iterator so that + iteration never ends. + """ + + def __init__( + self, + *args, + seed: int = 42, + use_deterministic_seed: bool = True, + **kwargs, + ) -> None: + """Initialize InfiniteDataLoader. + + Args: + *args: Positional arguments passed to parent DataLoader. + seed: Random seed for reproducible worker initialization. + Default is 42 for reproducibility. + use_deterministic_seed: If True, use the provided seed for reproducible + RNG initialization. If False, derive a time-based seed so that + each resume sees different data. This is preferred for large-scale + runs that resume frequently, and when ``in_order=False`` already + makes iteration order non-deterministic. + **kwargs: Keyword arguments passed to parent DataLoader. + """ + kwargs.pop("shuffle", None) + kwargs["shuffle"] = False + + # Default to ``custom_collate_fn`` so that variable-length per-sample + # tensors (e.g. ``text_token_ids``) and multi-item keys (``video``, + # ``action``, ...) are returned as lists rather than stacked by + # PyTorch's ``default_collate``. + if kwargs.get("collate_fn") is None: + kwargs["collate_fn"] = custom_collate_fn + + if "worker_init_fn" not in kwargs or kwargs["worker_init_fn"] is None: + kwargs["worker_init_fn"] = create_action_worker_init_fn(seed, use_deterministic_seed=use_deterministic_seed) + + num_workers = kwargs.get("num_workers", 0) + if num_workers == 0: + try: + rank = distributed.get_rank() + except RuntimeError: + rank = 0 + if use_deterministic_seed: + rank_seed = seed + rank * 9999 + else: + rank_seed = int(time.time() * 1000) % (2**32) + rank * 9999 + random.seed(rank_seed) + np.random.seed(rank_seed % (2**32)) + torch.manual_seed(rank_seed) + + super().__init__(*args, **kwargs) + self._stream_iterator: Iterator | None = None + + def __len__(self) -> int: + # Delegate to DataLoader which calls len(self.dataset). + # Raises TypeError if the underlying dataset has no __len__. + return super().__len__() + + def __iter__(self) -> Iterator: + """Yield batches forever.""" + while True: + if self._stream_iterator is None: + self._stream_iterator = super().__iter__() + try: + yield next(self._stream_iterator) # type: ignore[arg-type] + except StopIteration: + self._stream_iterator = super().__iter__() + yield next(self._stream_iterator) # type: ignore[arg-type] diff --git a/cosmos_training/cosmos/data/vfm/action/domain_utils.py b/cosmos_training/cosmos/data/vfm/action/domain_utils.py new file mode 100644 index 00000000..85dda5e7 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/domain_utils.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Domain ID helpers for cross-embodiment action datasets.""" + +EMBODIMENT_TO_DOMAIN_ID: dict[str, int] = { + "no_action": 0, + "av": 1, + "camera_pose": 2, + "hand_pose": 3, + "pusht": 4, + "libero": 5, + "umi": 6, + "bridge_orig_lerobot": 7, + "droid_lerobot": 8, + "robomind-franka": 8, # Both Droid and RoboMIND-Franka are using robotiq and franka + "embodiment_b": 9, + "robomind-franka-dual": 12, + "robomind-ur": 13, + "agibotworld": 15, + "embodiment_c_gripper": 15, + "embodiment_c_gripper_ext": 15, + "fractal": 20, +} + + +def get_domain_id(embodiment_type: str) -> int: + """Get the domain ID for a given embodiment type.""" + key = embodiment_type.lower().strip() + if key not in EMBODIMENT_TO_DOMAIN_ID: + raise KeyError( + f"Unknown embodiment type: {embodiment_type!r}. " + f"Available embodiments: {sorted(EMBODIMENT_TO_DOMAIN_ID.keys())}" + ) + return EMBODIMENT_TO_DOMAIN_ID[key] diff --git a/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset.py b/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset.py new file mode 100644 index 00000000..77312a96 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset.py @@ -0,0 +1,466 @@ +import json +import os +import random +from typing import Any, cast + +import numpy as np +import torch +import torch.nn.functional as F +from scipy.spatial.transform import Rotation as R + +from cosmos.utils import log +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Joint, + Pos, + Rot, + build_action_spec, + build_episode_spans, + split_episode_ids, +) +from cosmos.data.vfm.action.droid_lerobot_dataset_config import ( + _GRIPPER_STATE_FEATURE, + _JOINT_ACTION_FEATURE, + _JOINT_STATE_FEATURE, + ACTION_FEATURES, + HAS_MULTI_LANGUAGE_ANNOTATIONS, + IMAGE_FEATURES, + IS_FLAT_ACTION, + IS_GRIPPER_ACTION_FLIPPED, + LEROBOT_ROOTS, + STATE_FEATURES, +) +from cosmos.data.vfm.action.pose_utils import ( + PoseConvention, + build_abs_pose_from_components, + convert_rotation, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +_FILTER_DICT_PATH = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/users/ychao/datasets/raw/KarlP-droid/keep_ranges_1_0_1.json" + +# 90-degree clockwise rotation about the Z axis (in local frame), converting +# DROID Franka panda_link8 orientation to the OpenCV camera convention. +_DROID_TO_OPENCV: np.ndarray = np.array( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, +) + + +class DROIDLeRobotDataset(BaseActionLeRobotDataset): + """ """ + + def __init__( + self, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/droid_plus_lerobot_640x360_20260412", + fps: float = 15.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.03, + split: str = "train", + mode: str = "policy", + pose_convention: PoseConvention = "backward_framewise", + action_normalization: ActionNormalization | None = None, + tolerance_s=2e-4, + viewpoint: Viewpoint = "concat_view", + use_success_only: bool = False, + video_mode: str | None = None, + action_space: str = "midtrain", + use_state: bool = False, + use_filter_dict: bool = False, + enable_fast_init: bool = False, + max_num_history_actions: int = 0, + ) -> None: + """ """ + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type="droid_lerobot", + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format="rot6d", + action_normalization=action_normalization, + tolerance_s=tolerance_s, + enable_fast_init=enable_fast_init, + ) + self._use_success_only = use_success_only + self._video_mode = video_mode + self._action_space = action_space + self._use_state = use_state + self._use_filter_dict = use_filter_dict + self._max_num_history_actions = max_num_history_actions + if max_num_history_actions > 0 and action_space not in ("midtrain", "joint_pos"): + raise ValueError( + f"max_num_history_actions is only supported with action_space='midtrain' or 'joint_pos', got {action_space!r}" + ) + + self._is_val_temp_seg = split == "val_temp_seg" + self._to_opencv = _DROID_TO_OPENCV + + version = os.path.basename(root) + try: + lerobot_roots = LEROBOT_ROOTS[version] + self._image_features = IMAGE_FEATURES[version] + self._state_features = STATE_FEATURES[version] + self._action_features = ACTION_FEATURES[version] + self._is_flat_action = IS_FLAT_ACTION[version] + self._has_multi_language_annotations = HAS_MULTI_LANGUAGE_ANNOTATIONS[version] + self._is_gripper_action_flipped = IS_GRIPPER_ACTION_FLIPPED[version] + except KeyError as e: + raise ValueError(f"Unknown version: {version!r}. Supported: {list(LEROBOT_ROOTS.keys())}") from e + + if self._use_success_only and lerobot_roots: + lerobot_roots = [x for x in lerobot_roots if x.split("/", 1)[0] == "success"] + + self._all_shard_roots = [os.path.join(root, x) for x in lerobot_roots] if lerobot_roots else [root] + + observation_ts = [i * self._dt for i in range(0, self._chunk_length + 1)] + action_ts = [i * self._dt for i in range(0, self._chunk_length)] + if self._max_num_history_actions > 0 and self._action_space in ("midtrain", "joint_pos"): + observation_ts_ext = [i * self._dt for i in range(-self._max_num_history_actions, self._chunk_length + 1)] + action_ts_ext = [i * self._dt for i in range(-self._max_num_history_actions, self._chunk_length)] + else: + observation_ts_ext = observation_ts + action_ts_ext = action_ts + self._delta_timestamps: dict[str, list[float]] = { + self._state_features: observation_ts_ext, + self._action_features: action_ts_ext, + } + if self._viewpoint in ("wrist_view", "concat_view"): + self._delta_timestamps[self._image_features["wrist"]] = observation_ts + if self._viewpoint in ("third_person_view", "concat_view"): + self._delta_timestamps[self._image_features["left"]] = observation_ts + self._delta_timestamps[self._image_features["right"]] = observation_ts + if self._action_space == "joint_pos": + self._delta_timestamps[_JOINT_ACTION_FEATURE] = action_ts + if self._use_state or self._max_num_history_actions > 0: + self._delta_timestamps[_JOINT_STATE_FEATURE] = observation_ts_ext + self._delta_timestamps[_GRIPPER_STATE_FEATURE] = observation_ts_ext + if self._use_state and self._action_space != "joint_pos": + self._delta_timestamps[_GRIPPER_STATE_FEATURE] = observation_ts + + if self._use_filter_dict: + with open(_FILTER_DICT_PATH) as f: + self._filter_dict = json.load(f) + + def _append_index_records(self, *, meta, ds_idx: int, dataset_label: str | None = None) -> None: + """ """ + if not self._use_filter_dict: + super()._append_index_records(meta=meta, ds_idx=ds_idx, dataset_label=dataset_label) + return + + episode_ids = split_episode_ids( + total_episodes=meta.total_episodes, + seed=self._split_seed, + val_ratio=self._split_val_ratio, + split=self._split, + ) + episode_spans, _, sample_count = build_episode_spans( + meta.episodes, episode_ids, self._chunk_length, sample_stride=self._sample_stride + ) + + class_name = self.__class__.__name__ + label = f" [{dataset_label}]" + + log.info(f"{class_name}{label}: split={self._split}, num episodes={len(episode_ids)}") + + filtered_count = 0 + for episode_id, sample_start, valid_len in episode_spans: + ep_id_str = meta.episodes[episode_id]["episode_id"] + episode_key = f"gs://xembodiment_data/r2d2/r2d2-data-full/{ep_id_str}/recordings/MP4--gs://xembodiment_data/r2d2/r2d2-data-full/{ep_id_str}/trajectory.h5" + ranges = self._filter_dict.get(episode_key) + if ranges is None: + continue + for s, e in ranges: + sub_start = max(s, 0) + sub_end = min(e - self._chunk_length, valid_len) + sub_valid_len = max(0, sub_end - sub_start) + if sub_valid_len > 0: + self._episode_records.append((ds_idx, sample_start + sub_start, sub_valid_len, episode_id)) + self._num_valid_indices += sub_valid_len + self._episode_cum_ends.append(self._num_valid_indices) + filtered_count += sub_valid_len + + if sample_count > 0: + log.info( + f"{class_name}{label}: kept {filtered_count} / {sample_count} ({100.0 * filtered_count / sample_count:.2f} %) samples" + ) + + def _register_sources(self, indices: list[int] | None = None) -> None: + """ """ + super()._register_sources(indices) + if self._is_val_temp_seg: + self._apply_temp_seg_filter() + + def _apply_temp_seg_filter(self) -> None: + """Replace index records with one high-scoring segment per episode. + + A segment is interesting if either: + - The gripper action changes significantly (open/close transition), or + - The gripper is closed and the end-effector position is moving. + Among qualifying segments the one with the highest score is kept. + """ + ds = self._get_dataset(0) + chunk_size = self._chunk_length + 1 + gripper_change_threshold = 0.5 + ee_movement_threshold = 0.01 + + new_records: list[tuple[int, int, int, int]] = [] + num_episodes = len(self._episode_records) + + for ds_idx, sample_start, valid_len, episode_id in self._episode_records: + end = sample_start + valid_len + self._chunk_length + num_candidates = valid_len + if num_candidates <= 0: + continue + + episode_data = ds.hf_dataset[sample_start:end] + actions = torch.tensor(np.array(episode_data[self._action_features])) # [N,action_dim] + states = torch.tensor(np.array(episode_data[self._state_features])) # [N,state_dim] + + gripper_action = actions[:, 6] if self._is_flat_action else actions # [N] + ee_pos = states[:, :3] # [N,3] + ee_disp = (ee_pos[1:] - ee_pos[:-1]).norm(dim=-1) # [N-1] + + ee_disp_windows = ee_disp.unfold(0, self._chunk_length, 1) # [num_candidates,chunk_length] + gripper_windows = gripper_action.unfold(0, chunk_size, 1) # [num_candidates,chunk_size] + + gripper_range = gripper_windows.max(dim=1).values - gripper_windows.min(dim=1).values # [num_candidates] + total_ee_movement = ee_disp_windows.sum(dim=1) # [num_candidates] + gripper_closed_ratio = (gripper_windows < 0.5).float().mean(dim=1) # [num_candidates] + + has_gripper_change = gripper_range > gripper_change_threshold + gripper_closed = gripper_closed_ratio > 0.5 + has_ee_movement = total_ee_movement > ee_movement_threshold + + scores = torch.zeros(num_candidates) # [num_candidates] + scores[has_gripper_change] = 0.5 + gripper_range[has_gripper_change] + total_ee_movement[has_gripper_change] + + closed_and_moving = gripper_closed & ~has_gripper_change & has_ee_movement + scores[closed_and_moving] = 1.0 + total_ee_movement[closed_and_moving] + + if scores.max().item() > 0: + best_offset = int(scores.argmax().item()) + new_records.append((ds_idx, sample_start + best_offset, 1, episode_id)) + + self._episode_records = new_records + self._num_valid_indices = len(new_records) + self._episode_cum_ends = list(range(1, len(new_records) + 1)) + + log.info(f"DROIDLeRobotDataset: val_temp_seg kept {len(new_records)} segments from {num_episodes} episodes") + + def _compose_multi_view(self, sample: dict[str, Any]) -> torch.Tensor: + """Compose wrist, left, and right views into a single frame. + + Layout (per frame): + ┌──────────────┐ + │ wrist │ (H, W) + ├───────┬──────┤ + │ left │ right│ (H/2, W/2) each + └───────┴──────┘ + + Left and right exterior cameras are downscaled by 2x so that they + tile to the same width as the wrist view. The output height is 3H/2. + + Returns: + Composited raw video tensor in ``(T,C,H_out,W)`` float format. + """ + wrist = sample[self._image_features["wrist"]] # [T,C,H,W] + left = sample[self._image_features["left"]] # [T,C,H_l,W_l] + right = sample[self._image_features["right"]] # [T,C,H_r,W_r] + + _, _, h_w, w_w = wrist.shape + half_h, half_w = h_w // 2, w_w // 2 + + left = F.interpolate(left, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + right = F.interpolate(right, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + bottom = torch.cat([left, right], dim=-1) # [T,C,H/2,W] + + composite = torch.cat([wrist, bottom], dim=-2) # [T,C,3H/2,W] + return composite # [T,C,3H/2,W] + + def _build_action_spec(self) -> ActionSpec: + """DROID: 10D ``[Pos, Rot6d, Gripper]`` for ``ee_pose``, + 8D ``[Joint(7), Gripper]`` for ``joint_pos``. + """ + if self._action_space == "joint_pos": + return build_action_spec(Joint(n=7, label="joint"), Gripper()) + return build_action_spec(Pos(), Rot("rot6d"), Gripper()) + + def __getitem__(self, idx: int) -> dict[str, Any]: + """ """ + mode, _, _, sample = self._fetch_sample(idx) + + if self._has_multi_language_annotations: + tasks = sample["task"].split(" | ") + ai_caption = random.choice(tasks) + else: + ai_caption = sample["task"] + + if self._skip_video_loading: + video = None + elif self._video_mode is None: + if self._viewpoint == "concat_view": + video = self._compose_multi_view(sample) + else: + video = sample[self._image_features["wrist"]] # [T,C,H,W] + else: + if self._video_mode == "wrist": + video = sample[self._image_features["wrist"]] + if self._video_mode in ("rand_exterior", "wrist_rand_exterior"): + exterior_key = random.choice([self._image_features["left"], self._image_features["right"]]) + if self._video_mode == "rand_exterior": + video = sample[exterior_key] + else: + video = torch.cat([sample[self._image_features["wrist"]], sample[exterior_key]], dim=2) + if self._video_mode in ("wrist_left_exterior", "wrist_both_exterior"): + wrist = sample[self._image_features["wrist"]] + half_h, half_w = wrist.shape[2] // 2, wrist.shape[3] // 2 + left = F.interpolate( + sample[self._image_features["left"]], size=(half_h, half_w), mode="bilinear", align_corners=False + ) + if self._video_mode == "wrist_left_exterior": + right = torch.zeros_like(left) + if self._video_mode == "wrist_both_exterior": + right = F.interpolate( + sample[self._image_features["right"]], + size=(half_h, half_w), + mode="bilinear", + align_corners=False, + ) + video = torch.cat([wrist, torch.cat([left, right], dim=-1)], dim=-2) + + extras: dict[str, Any] = {} + + if self._action_space == "midtrain": + pose_convention = cast(PoseConvention, self._pose_convention) + state = sample[self._state_features] # [T+1, state_dim] or [H+T+1, state_dim] + poses_abs = build_abs_pose_from_components(state[:, 0:3], state[:, 3:6], "euler_xyz") + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ self._to_opencv + initial_pose = torch.from_numpy(poses_abs[-self._chunk_length - 1].copy()).float() + poses_rel = pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention=pose_convention) + gripper = ( + sample[self._action_features][:, [6]] + if self._is_flat_action + else sample[self._action_features].unsqueeze(-1) + ) + if self._is_gripper_action_flipped: + gripper = 1.0 - gripper + action = torch.from_numpy( + np.concatenate([poses_rel[-self._chunk_length :], gripper[-self._chunk_length :]], axis=-1) + ).float() # [T,10] + extras["initial_pose"] = initial_pose + if self._max_num_history_actions > 0: + _, _, _, frame_offset = self._resolve_index(int(idx)) + num_available = min(self._max_num_history_actions, frame_offset * self._sample_stride) + actual_h = num_available + # with 0.5 probability, randomly sample the number of history frames + if random.random() < 0.5: + actual_h = random.randint(0, num_available) + if actual_h > 0: + hist_action_raw = torch.from_numpy( + np.concatenate( + [ + poses_rel[-self._chunk_length - actual_h : -self._chunk_length], + gripper[-self._chunk_length - actual_h : -self._chunk_length], + ], + axis=-1, + ) + ).float() + extras["history_action"] = self._normalize_action(hist_action_raw) + if self._use_state: + initial_gripper = sample[_GRIPPER_STATE_FEATURE][0].unsqueeze(-1) + if self._is_gripper_action_flipped: + initial_gripper = 1.0 - initial_gripper + initial_rot6d = convert_rotation( + poses_abs[-self._chunk_length - 1, :3, :3], input_format="matrix", output_format="rot6d" + ) + initial_state = torch.from_numpy( + np.concatenate((poses_abs[-self._chunk_length - 1, :3, 3], initial_rot6d, initial_gripper), axis=-1) + ).float() + action = torch.cat([initial_state.unsqueeze(0), action], dim=0) + if self._action_space == "ee_pose_delta": + state = sample[self._state_features] + pose = np.tile(np.eye(4), (state.shape[0], 1, 1)) + pose[:, :3, :3] = R.from_euler("xyz", state[:, 3:6]).as_matrix() + pose[:, :3, 3] = state[:, 0:3] + pose_delta = np.linalg.inv(pose[0]) @ pose[1:] + gripper = sample[self._action_features].unsqueeze(-1) + if self._is_gripper_action_flipped: + gripper = 1.0 - gripper + action = torch.from_numpy( + np.concatenate((pose_delta[:, :3, 3], pose_delta[:, :3, 0], pose_delta[:, :3, 1], gripper), axis=-1) + ).float() + if self._use_state: + initial_gripper = sample[_GRIPPER_STATE_FEATURE][0].unsqueeze(-1) + if self._is_gripper_action_flipped: + initial_gripper = 1.0 - initial_gripper + initial_state = torch.from_numpy( + np.concatenate((pose[0, :3, 3], pose[0, :3, 0], pose[0, :3, 1], initial_gripper), axis=-1) + ).float() + action = torch.cat([initial_state.unsqueeze(0), action], dim=0) + if self._action_space == "joint_pos": + gripper = sample[self._action_features][-self._chunk_length :].unsqueeze(-1) + if self._is_gripper_action_flipped: + gripper = 1.0 - gripper + action = torch.cat((sample[_JOINT_ACTION_FEATURE], gripper), dim=-1).float() + if self._max_num_history_actions > 0: + _, _, _, frame_offset = self._resolve_index(int(idx)) + num_available = min(self._max_num_history_actions, frame_offset * self._sample_stride) + actual_h = num_available + if random.random() < 0.5: + actual_h = random.randint(0, num_available) + if actual_h > 0: + hist_joint = sample[_JOINT_STATE_FEATURE][ + -self._chunk_length - 1 - actual_h : -self._chunk_length - 1 + ] + hist_gripper = sample[_GRIPPER_STATE_FEATURE][ + -self._chunk_length - 1 - actual_h : -self._chunk_length - 1 + ].unsqueeze(-1) + if self._is_gripper_action_flipped: + hist_gripper = 1.0 - hist_gripper + hist_action_raw = torch.cat((hist_joint, hist_gripper), dim=-1).float() + extras["history_action"] = self._normalize_action(hist_action_raw) + if self._use_state: + initial_gripper = sample[_GRIPPER_STATE_FEATURE][-self._chunk_length - 1].unsqueeze(-1) + if self._is_gripper_action_flipped: + initial_gripper = 1.0 - initial_gripper + initial_state = torch.cat( + (sample[_JOINT_STATE_FEATURE][-self._chunk_length - 1], initial_gripper), dim=-1 + ).float() + action = torch.cat([initial_state.unsqueeze(0), action], dim=0) + + if self._viewpoint == "concat_view" and self._video_mode in ( + None, + "wrist_left_exterior", + "wrist_both_exterior", + ): + extras["additional_view_description"] = ( + "The top row is from the wrist-mounted camera. " + "The bottom row contains two horizontally concatenated third-person perspective views of the scene from opposite sides, with the robot visible." + ) + + return self._build_result( + mode=mode, + video=video, + action=action, + ai_caption=ai_caption, + **extras, + ) + + @property + def action_dim(self) -> int: + """ """ + return 8 if self._action_space == "joint_pos" else 10 diff --git a/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset_config.py new file mode 100644 index 00000000..a2257cd5 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/droid_lerobot_dataset_config.py @@ -0,0 +1,97 @@ +_INSTITUTIONS = [ + "AUTOLab", + "CLVR", + "GuptaLab", + "ILIAD", + "IPRL", + "IRIS", + "PennPAL", + "RAD", + "RAIL", + "REAL", + "RPL", + "TRI", + "WEIRD", +] + +LEROBOT_ROOTS = { + "droid_lerobot_20260115_no_noops": None, + "droid_plus_lerobot_320x180_20260406_sharded": [f"success/{x}" for x in _INSTITUTIONS] + + [f"failure/{x}" for x in _INSTITUTIONS], + "droid_plus_lerobot_320x180_20260406": ["success", "failure"], + "droid_plus_lerobot_640x360_20260412_sharded": [f"success/{x}" for x in _INSTITUTIONS] + + [f"failure/{x}" for x in _INSTITUTIONS], + "droid_plus_lerobot_640x360_20260412": ["success", "failure"], +} + +IMAGE_FEATURES = { + "droid_lerobot_20260115_no_noops": { + "wrist": "observation.images.wrist_image_left", + "left": "observation.images.exterior_image_1_left", + "right": "observation.images.exterior_image_2_left", + }, + "droid_plus_lerobot_320x180_20260406_sharded": { + "wrist": "observation.image.wrist_image_left", + "left": "observation.image.exterior_image_1_left", + "right": "observation.image.exterior_image_2_left", + }, + "droid_plus_lerobot_320x180_20260406": { + "wrist": "observation.image.wrist_image_left", + "left": "observation.image.exterior_image_1_left", + "right": "observation.image.exterior_image_2_left", + }, + "droid_plus_lerobot_640x360_20260412_sharded": { + "wrist": "observation.image.wrist_image_left", + "left": "observation.image.exterior_image_1_left", + "right": "observation.image.exterior_image_2_left", + }, + "droid_plus_lerobot_640x360_20260412": { + "wrist": "observation.image.wrist_image_left", + "left": "observation.image.exterior_image_1_left", + "right": "observation.image.exterior_image_2_left", + }, +} + +STATE_FEATURES = { + "droid_lerobot_20260115_no_noops": "observation.state", + "droid_plus_lerobot_320x180_20260406_sharded": "observation.state.cartesian_position", + "droid_plus_lerobot_320x180_20260406": "observation.state.cartesian_position", + "droid_plus_lerobot_640x360_20260412_sharded": "observation.state.cartesian_position", + "droid_plus_lerobot_640x360_20260412": "observation.state.cartesian_position", +} + +ACTION_FEATURES = { + "droid_lerobot_20260115_no_noops": "action", + "droid_plus_lerobot_320x180_20260406_sharded": "action.gripper_position", + "droid_plus_lerobot_320x180_20260406": "action.gripper_position", + "droid_plus_lerobot_640x360_20260412_sharded": "action.gripper_position", + "droid_plus_lerobot_640x360_20260412": "action.gripper_position", +} + +IS_FLAT_ACTION = { + "droid_lerobot_20260115_no_noops": True, + "droid_plus_lerobot_320x180_20260406_sharded": False, + "droid_plus_lerobot_320x180_20260406": False, + "droid_plus_lerobot_640x360_20260412_sharded": False, + "droid_plus_lerobot_640x360_20260412": False, +} + +HAS_MULTI_LANGUAGE_ANNOTATIONS = { + "droid_lerobot_20260115_no_noops": False, + "droid_plus_lerobot_320x180_20260406_sharded": True, + "droid_plus_lerobot_320x180_20260406": True, + "droid_plus_lerobot_640x360_20260412_sharded": True, + "droid_plus_lerobot_640x360_20260412": True, +} + +IS_GRIPPER_ACTION_FLIPPED = { + "droid_lerobot_20260115_no_noops": False, + "droid_plus_lerobot_320x180_20260406_sharded": True, + "droid_plus_lerobot_320x180_20260406": True, + "droid_plus_lerobot_640x360_20260412_sharded": True, + "droid_plus_lerobot_640x360_20260412": True, +} + +_JOINT_ACTION_FEATURE = "action.joint_position" +_JOINT_STATE_FEATURE = "observation.state.joint_positions" +_GRIPPER_STATE_FEATURE = "observation.state.gripper_position" diff --git a/cosmos_training/cosmos/data/vfm/action/dummy_dataset.py b/cosmos_training/cosmos/data/vfm/action/dummy_dataset.py new file mode 100644 index 00000000..2438ca7e --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/dummy_dataset.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Any + +import torch +from torch.utils.data import Dataset + +from cosmos.utils import log + + +class DummyDataset(Dataset): + """ + A dummy dataset that generates random images/videos, camera poses, and actions + mimicking the structure of UMI/iPhUMI datasets. + """ + + def __init__( + self, + length: int = 100, + image_size: int = 256, + chunk_length: int = 16, # must be divisible by 4 + camera_pose_dim: int = 9, # 3 pos + 6 rot + fps: int = 16, + mode: str = "joint", + randomize_resolution: bool = True, + ): + self.length = int(length) + self.image_size = image_size + self.chunk_length = chunk_length + assert self.chunk_length % 4 == 0, "chunk_length must be divisible by 4" + self.camera_pose_dim = camera_pose_dim + self.fps = fps + self.randomize_resolution = randomize_resolution + + assert mode in ["joint", "forward_dynamics", "inverse_dynamics", "policy", "image2video"], ( + "mode must be either joint, forward_dynamics, inverse_dynamics, policy, or image2video" + ) + self.mode = mode + + def __len__(self) -> int: + return self.length + + def __getitem__(self, idx: int) -> dict[str, Any]: + if self.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy", "image2video"]) + else: + mode = self.mode + + # Video: (C, T, H, W) uint8 + video_length = self.chunk_length + 1 + + if self.randomize_resolution: + # Randomize one dimension downward so the aspect ratio varies, while + # keeping both dimensions <= image_size. This ensures auto-detected + # resolution stays in the expected tier and exercises the padding path + # (the transforms pipeline will pad back up to the predefined target). + h = self.image_size - random.randint(32, 32) + w = self.image_size - random.randint(32, 32) + log.debug(f"DummyDataset[{idx}]: before padding resolution = ({h}, {w})") + else: + h = self.image_size + w = self.image_size + + video = torch.randint(0, 256, (3, video_length, h, w), dtype=torch.uint8) # [3,T+1,H,W] + + # Camera poses: (T, 9) float32 + # 3 position + 6 rotation (first two rows of rotation matrix flattened) + camera_poses = torch.randn(self.chunk_length, self.camera_pose_dim, dtype=torch.float32) # [T,camera_pose_dim] + + # EEF commands (actions): (T, 1) float32 + eef_commands = torch.randn(self.chunk_length, 1, dtype=torch.float32) # [T,1] + + # FPS: scalar (0-D tensor so batching produces [B] not [B, 1]) + fps = torch.tensor(self.fps, dtype=torch.long) # scalar + + # Index + key = torch.tensor([idx], dtype=torch.long) # [1] + + + # chunk_length is L, given L + 1 video frames and optionally L relative action + # video: predicting L video frames given 1 frame + # forward_dynamics: predicting L video frames given 1 frame and L action + # inverse_dynamics: predicting L action given L+1 frames (TODO: do we need a state too?) + # policy: predicting L action and L frames given 1 frame + + # Combine camera poses and eef commands into raw action tensor + action_tensor = torch.cat([camera_poses, eef_commands], dim=1) # [T,camera_pose_dim+1] + + return { + "video": video, + "action": action_tensor, + "conditioning_fps": fps, + "ai_caption": "A dummy video for testing.", + "mode": mode, + "__key__": key, + "domain_id": torch.tensor(0, dtype=torch.long), + "viewpoint": "ego_view", + } diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset.py b/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset.py new file mode 100644 index 00000000..7a0f6e03 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset.py @@ -0,0 +1,74 @@ +from typing import Any + +import torch + +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + BaseActionLeRobotDataset, +) +from cosmos.data.vfm.action.embodiment_b_dataset_config import LEROBOT_ROOTS +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + + +class Embodiment_bDataset(BaseActionLeRobotDataset): + """ """ + + def __init__( + self, + fps: float = 10.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + video_key: str = "observation.images.camera_top_left", + mode: str = "policy", + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "ego_view", + ) -> None: + """ """ + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type="embodiment_b", + viewpoint=viewpoint, + action_normalization=action_normalization, + tolerance_s=1e-4, + ) + self._video_key = video_key + + self._all_shard_roots = [ + f"/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/ychao/datasets/lerobot_v30/embodiment_b/{x}" + for x in LEROBOT_ROOTS + ] + + self._delta_timestamps = { + "observation.images.camera_top_left": [i * self._dt for i in range(0, self._chunk_length + 1)], + "observation.images.camera_wrist_left": [i * self._dt for i in range(0, self._chunk_length + 1)], + "observation.images.camera_wrist_right": [i * self._dt for i in range(0, self._chunk_length + 1)], + "action.arm.position": [i * self._dt for i in range(0, self._chunk_length)], + "action.end.position": [i * self._dt for i in range(0, self._chunk_length)], + "action.effector.position": [i * self._dt for i in range(0, self._chunk_length)], + } + + def __getitem__(self, idx: int) -> dict[str, Any]: + """ """ + mode, _, _, sample = self._fetch_sample(idx) + + ai_caption = sample["task"] + + video = sample[self._video_key] # [T,C,H,W] + arm_position = sample["action.arm.position"] + end_position = sample["action.end.position"] + effector_position = sample["action.effector.position"] + action = torch.cat([arm_position, end_position, effector_position], dim=-1) # [T,30] + + return self._build_result(mode=mode, video=video, action=action, ai_caption=ai_caption) + + @property + def action_dim(self) -> int: + """ """ + return 30 diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset_config.py new file mode 100644 index 00000000..e98a591c --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_b_dataset_config.py @@ -0,0 +1,14 @@ +LEROBOT_ROOTS = [ + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_1/1222_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_2/1223_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_2/1224_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_3/1225_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_3/1226_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_4/1227_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_5/1229_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_5/1230_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_6/1231_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_6/1232_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_7/1233_v30", + "data_sample_embodiment_b_20260123/Embodiment_b_data_for_Nvidia_20260123_8/1235_v30", +] diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset.py b/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset.py new file mode 100644 index 00000000..fd4c6689 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset.py @@ -0,0 +1,655 @@ +"""Embodiment C datasets: multi-task LeRobot with instruction annotations from meta/info.json. + +Provides gripper variants. + +Action representation: + - **Relative FK-pose**: calibrated head camera pose + left/right + gripper-base wrist poses are computed via URDF forward kinematics from + ``observation.state``. Gripper wrist rotations are then converted through + the dataset ``_to_opencv`` mapping for action/viewer display. The viewer + can optionally request source-state FK link poses for direct mesh playback. + +View modes: + - **ego_view**: single ``observation.images.top_head`` camera. + - **concat_view**: top-head view on top, left/right wrist views resized and + concatenated horizontally on the bottom (like DROID). +""" + +from __future__ import annotations + +import json +import os +from collections.abc import Callable +from pathlib import Path +from typing import Any, Literal, cast + +import numpy as np +import torch +import torch.nn.functional as F +from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata + +from cosmos.utils import log +from cosmos.data.vfm.action.embodiment_c_dataset_config import ( + CUSTOM_GRIPPER_SUBPATHS, + DEFAULT_CUSTOM_GRIPPER_EXT_ROOT, + DEFAULT_CUSTOM_GRIPPER_ROOT, + DEFAULT_OFFSHELF_GRIPPER_ROOT, + OFFSHELF_GRIPPER_SUBPATHS, +) +from cosmos.data.vfm.action.embodiment_c_fk import ( + AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST, + apply_agibot_gripper_to_opencv, + compute_fk_transforms_batch, + compute_link_poses_batch, + convert_gripper_state_to_open_fraction, + extract_fk_transforms_from_link_poses, +) +from cosmos.data.vfm.action.embodiment_c_spec import ( + AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE, + AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE, + AGIBOT_GEAR_GRIPPER_NORMALIZER_EMBODIMENT_TYPE, + AGIBOT_GEAR_VIDEO_KEY, + get_embodiment_c_kind_spec, +) +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, + split_episode_ids, +) +from cosmos.data.vfm.action.pose_utils import PoseConvention, pose_abs_to_rel +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +# Camera keys for concat_view. +_TOP_HEAD_KEY = AGIBOT_GEAR_VIDEO_KEY +_HAND_LEFT_KEY = "observation.images.hand_left" +_HAND_RIGHT_KEY = "observation.images.hand_right" + + +def _resolve_root_paths( + root: str | list[str] | tuple[str, ...], + expand_root_fn: Callable[[str], list[str]], +) -> list[str]: + """Normalize a root argument into the list of concrete shard roots.""" + + if isinstance(root, str): + return expand_root_fn(root) + resolved_roots: list[str] = [] + for item in root: + resolved_roots.extend(expand_root_fn(item)) + return resolved_roots + + +def _build_delta_timestamps(video_key: str, chunk_length: int, dt: float) -> dict[str, list[float]]: + """Build the LeRobot delta-timestamp layout for AgiBot samples.""" + + return { + video_key: [index * dt for index in range(0, chunk_length + 1)], + "observation.state": [index * dt for index in range(0, chunk_length + 1)], + } + + +def _load_instruction_segments(lerobot_root: str) -> dict[str, list[dict[str, Any]]]: + """Load instruction_segments from meta/info.json for a task path.""" + info_path = Path(lerobot_root) / "meta" / "info.json" + if not info_path.is_file(): + return {} + try: + with open(info_path) as f: + info = json.load(f) + segments = info.get("instruction_segments", {}) + return segments if isinstance(segments, dict) else {} + except Exception as e: + log.warning(f"EmbodimentCGripperDataset: failed to load {info_path}: {e}") + return {} + + +def _extract_high_level_instruction(entry: Any) -> str | None: + """Extract the episode-level instruction text from one metadata entry.""" + + if isinstance(entry, str): + text = entry.strip() + return text or None + if isinstance(entry, dict): + value = entry.get("high_level_instruction") + if isinstance(value, str): + text = value.strip() + return text or None + return None + + +def _load_high_level_instructions(lerobot_root: str) -> dict[str, str]: + """Load high_level_instruction from meta/info.json for a task path.""" + + info_path = Path(lerobot_root) / "meta" / "info.json" + if not info_path.is_file(): + return {} + try: + with open(info_path) as f: + info = json.load(f) + raw_instructions = info.get("high_level_instruction", {}) + if not isinstance(raw_instructions, dict): + return {} + high_level_instructions: dict[str, str] = {} + for episode_key, entry in raw_instructions.items(): + instruction = _extract_high_level_instruction(entry) + if instruction is not None: + high_level_instructions[str(episode_key)] = instruction + return high_level_instructions + except Exception as e: + log.warning(f"EmbodimentCGripperDataset: failed to load high_level_instruction from {info_path}: {e}") + return {} + + +def _coerce_frame_index(value: Any) -> int | None: + """Convert a frame index value to ``int`` when possible.""" + + try: + return int(value) + except (TypeError, ValueError): + return None + + +def _get_instruction_frame_bounds(segment: dict[str, Any]) -> tuple[int, int] | None: + """Extract inclusive ``[start,end]`` frame bounds from one instruction segment.""" + + start = _coerce_frame_index(segment.get("start_frame_index")) + end = _coerce_frame_index(segment.get("end_frame_index", segment.get("success_frame_index"))) + if start is None or end is None or end < start: + return None + return start, end + + +def _get_gripper_state_slices(embodiment_type: str, kind_spec: Any) -> tuple[slice, slice]: + """Return ``(right,left)`` state slices for scalar AgiBot gripper positions.""" + + if embodiment_type == "embodiment_c_gripper_ext": + return AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE, AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE + state_hand_slice = kind_spec.state_hand_slice + return ( + slice(state_hand_slice.start + 1, state_hand_slice.start + 2), + slice(state_hand_slice.start, state_hand_slice.start + 1), + ) + + +def _build_instruction_episode_spans( + *, + episodes: Any, + episode_ids: list[int], + instruction_segments: dict[str, list[dict[str, Any]]], + high_level_instructions: dict[str, str], + chunk_length: int, + sample_stride: int = 1, +) -> tuple[list[tuple[int, int, int]], dict[int, int], int, int, int, int]: + """Build valid spans from instruction segments, with episode-level fallback.""" + + assert sample_stride >= 1, f"sample_stride must be >= 1, got {sample_stride}" + + dataset_from_index = list(episodes["dataset_from_index"]) + dataset_to_index = list(episodes["dataset_to_index"]) + length = list(episodes["length"]) + + spans: list[tuple[int, int, int]] = [] + episode_start_rows: dict[int, int] = {} + valid_count = 0 + sample_count = 0 + missing_episode_count = 0 + fallback_episode_count = 0 + + for episode_id in episode_ids: + episode_start = int(dataset_from_index[episode_id]) + episode_stop = int(dataset_to_index[episode_id]) + episode_length = int(length[episode_id]) + episode_start_rows[episode_id] = episode_start + sample_count += episode_length + + raw_segments = instruction_segments.get(str(episode_id), []) + if not raw_segments: + missing_episode_count += 1 + if str(episode_id) in high_level_instructions: + max_episode_frame = min(episode_length - 1, episode_stop - episode_start - 1) + valid_len = max_episode_frame - chunk_length + 1 + if valid_len > 0: + strided_valid_len = (valid_len + sample_stride - 1) // sample_stride + spans.append((episode_id, episode_start, strided_valid_len)) + valid_count += strided_valid_len + fallback_episode_count += 1 + continue + + max_episode_frame = min(episode_length - 1, episode_stop - episode_start - 1) + for segment in raw_segments: + bounds = _get_instruction_frame_bounds(segment) + if bounds is None: + continue + + segment_start, segment_end = bounds + segment_start = max(0, segment_start) + segment_end = min(max_episode_frame, segment_end) + valid_len = segment_end - segment_start - chunk_length + 1 + if valid_len <= 0: + continue + + strided_valid_len = (valid_len + sample_stride - 1) // sample_stride + spans.append((episode_id, episode_start + segment_start, strided_valid_len)) + valid_count += strided_valid_len + + return spans, episode_start_rows, valid_count, sample_count, missing_episode_count, fallback_episode_count + + +class EmbodimentCGripperDataset(BaseActionLeRobotDataset): + """Embodiment C Gripper dataset with deferred source registration. + + Sources are registered by ``_register_sources()`` which is called by + ``ActionUnifiedIterableDataset.assign_worker()`` during training, or + explicitly for standalone/eval use. Instruction segments and episode-level + high-level instructions are loaded alongside each source. + + Action layout: + ``[head_cam_delta(9), right_hand_delta(9), right_gripper(1), + left_hand_delta(9), left_gripper(1)]`` → 29 dims for gripper. + + Concat view layout: + ┌──────────────────┐ + │ top_head │ (H, W) + ├─────────┬────────┤ + │ hand_L │ hand_R │ (H/2, W/2) each + └─────────┴────────┘ + """ + + def __init__( + self, + root: str | list[str] | tuple[str, ...] = DEFAULT_OFFSHELF_GRIPPER_ROOT, + fps: float = 10.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.005, + split: str = "train", + mode: str = "joint", + pose_convention: PoseConvention = "backward_framewise", + embodiment_type: str = "embodiment_c_gripper", + tolerance_s: float = 1e-3, + video_key: str = AGIBOT_GEAR_VIDEO_KEY, + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "concat_view", + rotation_format: Literal["rot6d"] = "rot6d", + max_loaded_datasets: int = 32, + skip_video_loading: bool = False, + sample_stride: int = 1, + enable_fast_init: bool = False, + fast_init_max_workers: int = 64, + return_agibot_link_poses: bool = False, + ) -> None: + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type=embodiment_type, + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format=rotation_format, + action_normalization=action_normalization, + tolerance_s=tolerance_s, + max_loaded_datasets=max_loaded_datasets, + skip_video_loading=skip_video_loading, + sample_stride=sample_stride, + enable_fast_init=enable_fast_init, + fast_init_max_workers=fast_init_max_workers, + ) + self._video_key = video_key + self._kind_spec = get_embodiment_c_kind_spec(embodiment_type) + self._is_concat_view = viewpoint == "concat_view" + self._to_opencv: dict[str, np.ndarray] = AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST + self._return_agibot_link_poses: bool = return_agibot_link_poses + + self._all_shard_roots = _resolve_root_paths(root, self._expand_root) + if not self._all_shard_roots: + raise ValueError( + "EmbodimentCGripperDataset: no task directories found under root. " + "Point root to a dir with task_* subdirs, or to a single LeRobot path with meta/info.json." + ) + + frame_ts = [index * self._dt for index in range(0, self._chunk_length + 1)] + self._delta_timestamps = _build_delta_timestamps(self._video_key, self._chunk_length, self._dt) + if self._is_concat_view: + self._delta_timestamps[_HAND_LEFT_KEY] = frame_ts + self._delta_timestamps[_HAND_RIGHT_KEY] = frame_ts + + self._instruction_segments: list[dict[str, list[dict[str, Any]]]] = [] + self._high_level_instructions: list[dict[str, str]] = [] + self._task_root_paths: list[str] = [] + self._episode_start_rows: list[dict[int, int]] = [] + + def _normalizer_filename(self) -> str: + """Resolve Embodiment C gripper stats, sharing them with compatible ext data.""" + + if self._embodiment_type == "embodiment_c_gripper_ext": + return ( + f"{AGIBOT_GEAR_GRIPPER_NORMALIZER_EMBODIMENT_TYPE}_{self._pose_convention}_{self._rotation_format}.json" + ) + return super()._normalizer_filename() + + def _expand_root(self, root: str) -> list[str]: + """Expand a base root path to task-specific subpaths.""" + root_name = Path(root.rstrip("/")).name + if root_name == Path(DEFAULT_OFFSHELF_GRIPPER_ROOT).name: + return [os.path.join(root, sub) for sub in OFFSHELF_GRIPPER_SUBPATHS] + if root_name == Path(DEFAULT_CUSTOM_GRIPPER_ROOT).name: + return [os.path.join(root, sub) for sub in CUSTOM_GRIPPER_SUBPATHS] + return [root] + + def _register_sources(self, indices: list[int] | None = None) -> None: + if indices is None: + indices = list(range(len(self._all_shard_roots))) + for idx in indices: + path = self._all_shard_roots[idx] + instruction_segments = _load_instruction_segments(path) + high_level_instructions = _load_high_level_instructions(path) + self._task_root_paths.append(path) + self._instruction_segments.append(instruction_segments) + self._high_level_instructions.append(high_level_instructions) + try: + self._register_source( + root=path, + delta_timestamps=self._delta_timestamps, + tolerance_s=self._tolerance_s, + dataset_label=Path(path).name, + ) + except Exception as e: + self._task_root_paths.pop() + self._instruction_segments.pop() + self._high_level_instructions.pop() + log.warning(f"EmbodimentCGripperDataset: failed to load LeRobotDatasetMetadata at {path}: {e}") + continue + + def _append_index_records( + self, + *, + meta: LeRobotDatasetMetadata, + ds_idx: int, + dataset_label: str | None = None, + ) -> None: + """Populate index records using only frame starts that stay inside instruction segments.""" + + episode_ids = split_episode_ids( + total_episodes=meta.total_episodes, + seed=self._split_seed, + val_ratio=self._split_val_ratio, + split=self._split, + ) + instruction_segments = self._instruction_segments[ds_idx] + high_level_instructions = self._high_level_instructions[ds_idx] + episode_spans, episode_start_rows, valid_count, sample_count, missing_episode_count, fallback_episode_count = ( + _build_instruction_episode_spans( + episodes=meta.episodes, + episode_ids=episode_ids, + instruction_segments=instruction_segments, + high_level_instructions=high_level_instructions, + chunk_length=self._chunk_length, + sample_stride=self._sample_stride, + ) + ) + + while len(self._episode_start_rows) <= ds_idx: + self._episode_start_rows.append({}) + self._episode_start_rows[ds_idx] = episode_start_rows + + class_name = self.__class__.__name__ + label = f" [{dataset_label}]" if dataset_label else "" + log.info(f"{class_name}{label}: split={self._split}, num episodes={len(episode_ids)}") + if sample_count > 0: + log.info( + f"{class_name}{label}: kept {valid_count} / {sample_count} " + f"({100 * valid_count / sample_count:.2f} %) instruction-filtered samples" + ) + if missing_episode_count > 0: + if fallback_episode_count == missing_episode_count: + log.info( + f"{class_name}{label}: missing instruction_segments for {missing_episode_count} / " + f"{len(episode_ids)} episodes; using high_level_instruction fallback for all of them" + ) + else: + log.warning( + f"{class_name}{label}: missing instruction_segments for {missing_episode_count} / " + f"{len(episode_ids)} episodes; high_level_instruction fallback covered " + f"{fallback_episode_count} of them" + ) + + for episode_id, sample_start, valid_len in episode_spans: + self._episode_records.append((ds_idx, sample_start, valid_len, episode_id)) + self._num_valid_indices += valid_len + self._episode_cum_ends.append(self._num_valid_indices) + + def _get_ai_caption(self, ds_idx: int, episode_id: int, frame_index_in_episode: int) -> str: + segments = self._instruction_segments[ds_idx] + high_level_instructions = self._high_level_instructions[ds_idx] + ep_key = str(episode_id) + for seg in segments.get(ep_key, []): + bounds = _get_instruction_frame_bounds(seg) + if bounds is None: + continue + start, end = bounds + if start <= frame_index_in_episode <= end: + if "instruction" not in seg: + raise ValueError( + f"EmbodimentCGripperDataset: segment for episode_id={episode_id} " + f"frame_index_in_episode={frame_index_in_episode} missing 'instruction' key." + ) + return seg["instruction"] + if ep_key in high_level_instructions: + return high_level_instructions[ep_key] + raise ValueError( + f"EmbodimentCGripperDataset: no instruction annotation found for episode_id={episode_id} " + f"frame_index_in_episode={frame_index_in_episode} (ds_idx={ds_idx})." + ) + + # -- FK-based action construction ---------------------------------------- + + def _build_fk_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, dict[str, Any]]: + """Build relative FK-pose action plus absolute initial poses for reconstruction. + + Uses ``observation.state`` across the full ``T+1`` conditioning window to + compute absolute FK transforms, converts them to relative ``rot6d`` SE(3) + deltas, then concatenates hand/gripper values from the next observed + state frame for each delta. The source ``action`` column is intentionally + not requested or read: for Embodiment C, the training/viewer action is the + difference between consecutive observed states. + + Returns: + ``(action, extras)`` where ``action`` has shape ``(T, action_dim)`` + and ``extras`` carries the absolute initial poses for the head and + gripper-base wrist trajectories. When ``return_agibot_link_poses`` is + enabled, ``extras`` also carries native URDF link poses for direct + viewer mesh animation. + + Scalar hand actions are emitted in the shared viewer/action convention: + ``0.0`` means closed and ``1.0`` means open. + ``convert_gripper_state_to_open_fraction`` maps observed AgiBot gripper + state, including URDF ``[-pi/4,0]`` radians or actuator-close + ``[0,120]`` degrees, into that same open-fraction range. + """ + obs_state = sample["observation.state"] # [T+1,S] + states_np = obs_state.detach().cpu().numpy().astype(np.float32, copy=False) # [T+1,S] + action_steps = int(states_np.shape[0] - 1) + if action_steps <= 0: + raise ValueError(f"{self.__class__.__name__}: observation.state must contain at least 2 frames.") + link_poses = None + if self._return_agibot_link_poses: + link_poses = compute_link_poses_batch(states_np, self._embodiment_type) # {name:[T+1,4,4]} + native_fk = extract_fk_transforms_from_link_poses(link_poses) # {name:[T+1,4,4]} + else: + native_fk = compute_fk_transforms_batch(states_np, self._embodiment_type) # {name:[T+1,4,4]} + fk = apply_agibot_gripper_to_opencv(native_fk, self._to_opencv) # {name:[T+1,4,4]} + pose_convention = cast(PoseConvention, self._pose_convention) + head_rel = pose_abs_to_rel(fk["head_camera"], rotation_format="rot6d", pose_convention=pose_convention) # [T,9] + right_rel = pose_abs_to_rel( + fk["right_wrist"], rotation_format="rot6d", pose_convention=pose_convention + ) # [T,9] + left_rel = pose_abs_to_rel(fk["left_wrist"], rotation_format="rot6d", pose_convention=pose_convention) # [T,9] + # Normalize observed AgiBot gripper state to the viewer/action + # convention: 0.0=closed, 1.0=open. Actuator-close state values use + # 0=open and 120=closed, so small open-state jitter stays near 1.0 open. + right_state_slice, left_state_slice = _get_gripper_state_slices(self._embodiment_type, self._kind_spec) + right_hand = convert_gripper_state_to_open_fraction(states_np[1:, right_state_slice]) # [T,1] + left_hand = convert_gripper_state_to_open_fraction(states_np[1:, left_state_slice]) # [T,1] + action_np = np.concatenate( + [ + head_rel, # [T,9] + right_rel, # [T,9] + right_hand, # [T,1|6] + left_rel, # [T,9] + left_hand, # [T,1|6] + ], + axis=-1, + ).astype(np.float32, copy=False) # [T,A] + extras = { + "initial_pose": torch.from_numpy(fk["head_camera"][0].copy()).float(), # [4,4] + "initial_pose_right": torch.from_numpy(fk["right_wrist"][0].copy()).float(), # [4,4] + "initial_pose_left": torch.from_numpy(fk["left_wrist"][0].copy()).float(), # [4,4] + } + if link_poses is not None: + agibot_link_poses = { + link_name: torch.from_numpy(poses.copy()).float() for link_name, poses in link_poses.items() + } # {name:[T+1,4,4]} + extras["agibot_link_poses"] = agibot_link_poses + return torch.from_numpy(action_np).float(), extras # [T,A] + + # -- Multi-view composition ---------------------------------------------- + + def _compose_multi_view(self, sample: dict[str, Any]) -> torch.Tensor: + """Compose top-head, left-hand, and right-hand views into a single frame. + + Layout (per frame): + ┌──────────────────┐ + │ top_head │ (H, W) + ├─────────┬────────┤ + │ hand_L │ hand_R │ (H/2, W/2) each + └─────────┴────────┘ + + Left and right hand cameras are downscaled by 2× so they tile to the + same width as the top-head view. Output height is 3H/2. + + Returns: + Composited video tensor in raw LeRobot ``(T, C, H_out, W)`` float format. + """ + top = sample[_TOP_HEAD_KEY] # [T,C,H,W] + left = sample[_HAND_LEFT_KEY] # [T,C,H_l,W_l] + right = sample[_HAND_RIGHT_KEY] # [T,C,H_r,W_r] + + _, _, h_top, w_top = top.shape + half_h, half_w = h_top // 2, w_top // 2 + + left = F.interpolate(left, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + right = F.interpolate(right, size=(half_h, half_w), mode="bilinear", align_corners=False) # [T,C,H/2,W/2] + bottom = torch.cat([left, right], dim=-1) # [T,C,H/2,W] + + composite = torch.cat([top, bottom], dim=-2) # [T,C,3H/2,W] + return composite # [T,C,3H/2,W] + + # -- __getitem__ --------------------------------------------------------- + + def _resolve_sample(self, idx: int) -> tuple[str, int, int, int, dict[str, Any]]: + """Resolve a flat index to one dataset row and its associated metadata.""" + + mode = self._choose_mode() + dataset_idx, row_idx, episode_id, _ = self._resolve_index(int(idx)) + frame_index_in_episode = row_idx - self._episode_start_rows[dataset_idx][episode_id] + sample = self._get_dataset(dataset_idx)[row_idx] + return mode, dataset_idx, episode_id, frame_index_in_episode, sample + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode, dataset_idx, episode_id, frame_index_in_episode, sample = self._resolve_sample(idx) + ai_caption = self._get_ai_caption(dataset_idx, episode_id, frame_index_in_episode) + + # Respect skip_video_loading (used by action-stats / eval scripts that + # only need ``action``). Base class strips ``observation.image*`` from + # delta_timestamps in this mode, so ``sample`` does not contain + # ``self._video_key`` and direct indexing would KeyError. Mirrors the + # droid / robomind pattern. + if self._skip_video_loading: + video = None + # Video: concat_view or single ego_view. + elif self._is_concat_view: + video = self._compose_multi_view(sample) + else: + video = sample[self._video_key] # [T,C,H,W] + + # Action: relative FK-pose based. + action, action_extras = self._build_fk_action(sample) + + extras: dict[str, Any] = {**action_extras} + if self._is_concat_view: + extras["additional_view_description"] = ( + "The top row shows the head-mounted camera view looking down at the workspace. " + "The bottom row contains two horizontally concatenated wrist-mounted camera views: " + "the left hand camera on the left and the right hand camera on the right." + ) + + result = self._build_result(mode=mode, video=video, action=action, ai_caption=ai_caption, **extras) + result["__episode_id__"] = episode_id + result["__task_root__"] = self._task_root_paths[dataset_idx] + return result + + def _build_action_spec(self) -> ActionSpec: + """Embodiment C gripper bimanual layout (29D). + + ``[head_pos+rot6d (9) | right_pos+rot6d (9) | right_gripper (1) + | left_pos+rot6d (9) | left_gripper (1)]`` + + All three SE(3) blocks (head camera + both wrists) participate in + idle-frame detection: a chunk only counts as idle if the head is + steady AND both arms are at rest AND both grippers are unchanged. + Override this method (or use ``Reserved`` for head dims) if you want + head motion to be ignored by idle detection. + """ + return build_action_spec( + Pos(prefix="head"), + Rot("rot6d", prefix="head"), + Pos(prefix="right"), + Rot("rot6d", prefix="right"), + Gripper(prefix="right"), + Pos(prefix="left"), + Rot("rot6d", prefix="left"), + Gripper(prefix="left"), + ) + + @property + def action_dim(self) -> int: + return 29 + + +class EmbodimentCGripperExtDataset(EmbodimentCGripperDataset): + """Embodiment C Gripper Extended dataset for custom tasks. + + The source episodes use a different state layout (``state=[94]``) compared + to the standard gripper (``state=[32]``). Source action columns are not used; + emitted FK-pose actions are derived from consecutive observed states. + + Key layout differences handled here: + + **State (94-dim):** + - Arm joint positions at ``[54:68]`` (vs ``[0:14]`` in standard). + - Head yaw/pitch at ``[82:84]`` (vs ``[16]``/``[17]``). + - Waist pitch/lift at ``[84:86]`` (vs ``[18]``/``[19]``). + - Mobile-base position/quaternion at ``[86:93]``. + + The FK engine (``_extract_joint_values_from_state``) handles the state + remapping for the ``embodiment_c_gripper_ext`` embodiment type and folds + mobile-base motion into the head/gripper-base poses. The emitted FK-pose + action is the same 29-dim layout as standard gripper: + ``[head(9), right(9), right_grip(1), left(9), left_grip(1)]``. + """ + + def __init__( + self, + root: str | list[str] | tuple[str, ...] = DEFAULT_CUSTOM_GRIPPER_EXT_ROOT, + embodiment_type: str = "embodiment_c_gripper_ext", + **kwargs: Any, + ) -> None: + super().__init__(root=root, embodiment_type=embodiment_type, **kwargs) diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset_config.py new file mode 100644 index 00000000..85babe75 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_c_dataset_config.py @@ -0,0 +1,150 @@ +"""Default data roots for Embodiment C datasets.""" + +from __future__ import annotations + +DEFAULT_OFFSHELF_GRIPPER_ROOT = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/EmbodimentCWorld_20260208/agibot-offshelf" +DEFAULT_CUSTOM_GRIPPER_ROOT = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/EmbodimentCWorld_20260208/agibot-custom" + +OFFSHELF_GRIPPER_SUBPATHS = ( + # 20251016_500h/gripper + "20251016_500h/gripper/task_1018", + "20251016_500h/gripper/task_1173", + "20251016_500h/gripper/task_1174", + "20251016_500h/gripper/task_1183", + "20251016_500h/gripper/task_1185", + "20251016_500h/gripper/task_1187", + "20251016_500h/gripper/task_1189", + "20251016_500h/gripper/task_1237", + "20251016_500h/gripper/task_1245", + "20251016_500h/gripper/task_1251", + "20251016_500h/gripper/task_1252", + "20251016_500h/gripper/task_1259", + "20251016_500h/gripper/task_1286", + "20251016_500h/gripper/task_797", + "20251016_500h/gripper/task_798", + "20251016_500h/gripper/task_799", + "20251016_500h/gripper/task_800", + "20251016_500h/gripper/task_809", + "20251016_500h/gripper/task_812", + "20251016_500h/gripper/task_813", + "20251016_500h/gripper/task_815", + "20251016_500h/gripper/task_827", + "20251016_500h/gripper/task_828", + "20251016_500h/gripper/task_841", + "20251016_500h/gripper/task_857", + "20251016_500h/gripper/task_869", + "20251016_500h/gripper/task_876", + "20251016_500h/gripper/task_893", + "20251016_500h/gripper/task_901", + "20251016_500h/gripper/task_903", + "20251016_500h/gripper/task_954", + "20251016_500h/gripper/task_957", + "20251016_500h/gripper/task_964", + "20251016_500h/gripper/task_968", + # 20251031_500h/gripper + "20251031_500h/gripper/task_1018", + "20251031_500h/gripper/task_1174", + "20251031_500h/gripper/task_1183", + "20251031_500h/gripper/task_1185", + "20251031_500h/gripper/task_1237", + "20251031_500h/gripper/task_1251", + "20251031_500h/gripper/task_1286", + "20251031_500h/gripper/task_1288", + "20251031_500h/gripper/task_1292", + "20251031_500h/gripper/task_1293", + "20251031_500h/gripper/task_1296", + "20251031_500h/gripper/task_1298", + "20251031_500h/gripper/task_1299", + "20251031_500h/gripper/task_1307", + "20251031_500h/gripper/task_1310", + "20251031_500h/gripper/task_1313", + "20251031_500h/gripper/task_1314", + "20251031_500h/gripper/task_1326", + "20251031_500h/gripper/task_1331", + "20251031_500h/gripper/task_1366", + "20251031_500h/gripper/task_1368", + "20251031_500h/gripper/task_1370", + "20251031_500h/gripper/task_1371", + "20251031_500h/gripper/task_1372", + "20251031_500h/gripper/task_1389", + "20251031_500h/gripper/task_1390", + "20251031_500h/gripper/task_1405", + "20251031_500h/gripper/task_1425", + "20251031_500h/gripper/task_1430", + "20251031_500h/gripper/task_1432", + "20251031_500h/gripper/task_1439", + "20251031_500h/gripper/task_1448", + "20251031_500h/gripper/task_1450", + "20251031_500h/gripper/task_1452", + "20251031_500h/gripper/task_1453", + "20251031_500h/gripper/task_1479", + "20251031_500h/gripper/task_1487", + "20251031_500h/gripper/task_1491", +) + +# Custom gripper tasks with extended 94-dim state. These require +# EmbodimentCGripperExtDataset because their arm, head, waist, and gripper state +# offsets differ from the standard gripper layout. +DEFAULT_CUSTOM_GRIPPER_EXT_ROOT = ( + # 20251113_330h + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025110304/gripper/task_3578", + # 20251218_561h + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_3529", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_3545", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_3547", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120401/gripper/task_3529", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120401/gripper/task_3545", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120501/gripper/task_3529", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120501/gripper/task_3545", +) + +_CUSTOM_GRIPPER_TASK_ROOTS = ( + # 20251113_330h + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111101/gripper/task_2156", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111101/gripper/task_3578", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111102/gripper/task_2156", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111102/gripper/task_3578", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111104/gripper/task_3800", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111201/gripper/task_4102", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251113_330h/2025111202/gripper/task_4102", + # 20251218_561h + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_3719", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_3800", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120301/gripper/task_4392", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120401/gripper/task_3800", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120401/gripper/task_4392", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120505/gripper/task_3800", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120505/gripper/task_4392", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120801/gripper/task_3798", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025120901/gripper/task_3798", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025121001/gripper/task_3798", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025121101/gripper/task_3798", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20251218_561h/2025121201/gripper/task_3798", + # 20260205_260h + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_1183", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_1185", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_1187", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_797", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_798", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_799", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_809", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_812", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_813", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_815", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_827", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_828", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_841", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_857", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_869", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_876", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_893", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_901", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_954", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_957", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_964", + f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/20260205_260h/task_968", +) + +CUSTOM_GRIPPER_SUBPATHS = tuple( + root.removeprefix(f"{DEFAULT_CUSTOM_GRIPPER_ROOT}/") for root in _CUSTOM_GRIPPER_TASK_ROOTS +) diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk.py b/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk.py new file mode 100644 index 00000000..8a0d3a6d --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk.py @@ -0,0 +1,496 @@ +"""Lightweight Embodiment C forward kinematics for datasets and viewers.""" + +from __future__ import annotations + +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from functools import lru_cache + +import numpy as np + +from cosmos.data.vfm.action.embodiment_c_spec import ( + AGIBOT_GEAR_ARM_JOINT_NAMES_LEFT, + AGIBOT_GEAR_ARM_JOINT_NAMES_RIGHT, + AGIBOT_GEAR_ARM_STATE_SLICE, + AGIBOT_GEAR_EXT_ARM_STATE_SLICE, + AGIBOT_GEAR_EXT_STATE_HEAD_PITCH_IDX, + AGIBOT_GEAR_EXT_STATE_HEAD_YAW_IDX, + AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE, + AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE, + AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE, + AGIBOT_GEAR_EXT_STATE_ROBOT_POSITION_SLICE, + AGIBOT_GEAR_EXT_STATE_WAIST_LIFT_IDX, + AGIBOT_GEAR_EXT_STATE_WAIST_PITCH_IDX, + AGIBOT_GEAR_GRIPPER_OPEN_ACTUATOR_DEG, + AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD, + AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME, + AGIBOT_GEAR_HEAD_PITCH_JOINT_NAME, + AGIBOT_GEAR_HEAD_YAW_JOINT_NAME, + AGIBOT_GEAR_LEFT_EE_LINK_NAME, + AGIBOT_GEAR_LEFT_GRIPPER_JOINT_MIMICS, + AGIBOT_GEAR_RIGHT_EE_LINK_NAME, + AGIBOT_GEAR_RIGHT_GRIPPER_JOINT_MIMICS, + AGIBOT_GEAR_STATE_HEAD_PITCH_IDX, + AGIBOT_GEAR_STATE_HEAD_YAW_IDX, + AGIBOT_GEAR_STATE_WAIST_LIFT_IDX, + AGIBOT_GEAR_STATE_WAIST_PITCH_IDX, + AGIBOT_GEAR_WAIST_LIFT_JOINT_NAME, + AGIBOT_GEAR_WAIST_PITCH_JOINT_NAME, + get_embodiment_c_embodiment_spec, + get_embodiment_c_kind_spec, + get_embodiment_c_urdf_path, +) +from cosmos.data.vfm.action.pose_utils import convert_rotation + +# Offset from gripper base/flange to the center point between the finger pads. +DEFAULT_GRIPPER_LENGTH_M = 0.14308 +_GRIPPER_VALUE_EPS = 1e-4 +_QUATERNION_NORM_EPS = 1e-8 +_GRIPPER_ACTUATOR_OVERSHOOT_DEG = 5.0 +# Main-branch wrist rotations composed with one extra local-Z 180 degree rotation. +AGIBOT_GEAR_LEFT_GRIPPER_TO_OPENCV: np.ndarray = np.asarray( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, +) +AGIBOT_GEAR_RIGHT_GRIPPER_TO_OPENCV: np.ndarray = np.asarray( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, +) +AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST: dict[str, np.ndarray] = { + "right_wrist": AGIBOT_GEAR_RIGHT_GRIPPER_TO_OPENCV, + "left_wrist": AGIBOT_GEAR_LEFT_GRIPPER_TO_OPENCV, +} + + +@dataclass(frozen=True) +class AgibotGearViewerBatch: + """Absolute poses and auxiliary gripper values aligned with action steps.""" + + head_camera_poses: np.ndarray + right_wrist_poses: np.ndarray + left_wrist_poses: np.ndarray + right_gripper: np.ndarray | None + left_gripper: np.ndarray | None + + +def _scale_to_unit_interval(values: np.ndarray, scale: float) -> np.ndarray: + """Scale non-negative gripper actuator values to ``[0,1]``.""" + + return np.clip(values / scale, 0.0, 1.0).astype(np.float32, copy=False) + + +def _scale_negative_to_unit_interval(values: np.ndarray, scale: float) -> np.ndarray: + """Scale URDF-style negative gripper angles to ``[0,1]`` open fractions.""" + + return np.clip(-values / scale, 0.0, 1.0).astype(np.float32, copy=False) + + +def _to_numpy_float32(array: object) -> np.ndarray: + """Convert a tensor-like object to a float32 NumPy array.""" + + try: + import torch + + if isinstance(array, torch.Tensor): + return array.detach().cpu().numpy().astype(np.float32, copy=False) + except ImportError: + pass + return np.asarray(array, dtype=np.float32) + + +def _normalize_quaternions_xyzw(quaternions: np.ndarray) -> np.ndarray: + """Normalize ``xyzw`` quaternions, treating all-zero rows as identity.""" + + normalized = np.asarray(quaternions, dtype=np.float32).copy() # [T,4] + norms = np.linalg.norm(normalized, axis=-1, keepdims=True) # [T,1] + valid = norms[:, 0] >= _QUATERNION_NORM_EPS # [T] + normalized[valid] = normalized[valid] / norms[valid] # [T_valid,4] + normalized[~valid] = np.asarray([0.0, 0.0, 0.0, 1.0], dtype=np.float32) # [T_invalid,4] + return normalized + + +def _quat_xyzw_to_rotation_matrix(quaternions: np.ndarray) -> np.ndarray: + """Convert ``xyzw`` quaternions to rotation matrices.""" + + normalized = _normalize_quaternions_xyzw(quaternions) # [T,4] + rotations = convert_rotation( + normalized, + input_format="quat_xyzw", + output_format="matrix", + normalize_matrix=True, + ) + return np.asarray(rotations, dtype=np.float32) + + +def build_robot_base_transforms(positions: np.ndarray, quaternions: np.ndarray) -> np.ndarray: + """Build robot-base poses from position and ``xyzw`` quaternion arrays.""" + + positions = np.asarray(positions, dtype=np.float32) # [T,3] + quaternions = np.asarray(quaternions, dtype=np.float32) # [T,4] + if positions.ndim != 2 or positions.shape[1] != 3: + raise ValueError(f"robot base positions must have shape [T,3], got {positions.shape}.") + if quaternions.ndim != 2 or quaternions.shape[1] != 4: + raise ValueError(f"robot base quaternions must have shape [T,4], got {quaternions.shape}.") + if positions.shape[0] != quaternions.shape[0]: + raise ValueError( + f"robot base positions/quaternions must share T, got {positions.shape[0]} and {quaternions.shape[0]}." + ) + + transforms = np.tile(np.eye(4, dtype=np.float32), (positions.shape[0], 1, 1)) # [T,4,4] + transforms[:, :3, :3] = _quat_xyzw_to_rotation_matrix(quaternions) # [T,3,3] + transforms[:, :3, 3] = positions # [T,3] + return transforms + + +def _invert_rigid_transform(transform: np.ndarray) -> np.ndarray: + """Invert one homogeneous rigid transform.""" + + inverse = np.eye(4, dtype=np.float32) # [4,4] + rotation_t = transform[:3, :3].T.astype(np.float32, copy=False) # [3,3] + inverse[:3, :3] = rotation_t + inverse[:3, 3] = -(rotation_t @ transform[:3, 3]) # [3] + return inverse + + +def apply_robot_base_motion_to_poses( + poses_by_name: dict[str, np.ndarray], + positions: np.ndarray, + quaternions: np.ndarray, +) -> dict[str, np.ndarray]: + """Apply mobile-base motion to FK poses, normalized to the first frame.""" + + base_poses = build_robot_base_transforms(positions, quaternions) # [T,4,4] + initial_base_inv = _invert_rigid_transform(base_poses[0]) # [4,4] + base_motion = np.einsum("ij,tjk->tik", initial_base_inv, base_poses).astype(np.float32, copy=False) # [T,4,4] + return { + name: np.einsum("tij,tjk->tik", base_motion, poses).astype(np.float32, copy=False) # [T,4,4] + for name, poses in poses_by_name.items() + } + + +def _apply_ext_base_motion_to_poses( + poses_by_name: dict[str, np.ndarray], + states: np.ndarray, + embodiment_type: str, +) -> dict[str, np.ndarray]: + """Apply ext mobile-base motion to FK poses, normalized to the first frame.""" + + if embodiment_type != "embodiment_c_gripper_ext": + return poses_by_name + if states.shape[1] < AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE.stop: + raise ValueError( + f"embodiment_c_gripper_ext state must include robot pose through index " + f"{AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE.stop - 1}, got shape {states.shape}." + ) + + positions = states[:, AGIBOT_GEAR_EXT_STATE_ROBOT_POSITION_SLICE].astype(np.float32, copy=False) # [T,3] + quaternions = states[:, AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE].astype(np.float32, copy=False) # [T,4] + return apply_robot_base_motion_to_poses(poses_by_name, positions, quaternions) + + +def apply_agibot_gripper_to_opencv( + poses_by_name: dict[str, np.ndarray], + to_opencv_by_wrist: dict[str, np.ndarray], +) -> dict[str, np.ndarray]: + """Post-rotate AgiBot gripper wrist poses into OpenCV convention.""" + + aligned = {name: poses.astype(np.float32, copy=True) for name, poses in poses_by_name.items()} # {name:[...,4,4]} + for wrist_name, wrist_to_opencv in to_opencv_by_wrist.items(): + poses = aligned.get(wrist_name) + if poses is None: + continue + aligned[wrist_name][..., :3, :3] = poses[..., :3, :3] @ wrist_to_opencv.astype(poses.dtype) # [...,3,3] + return aligned + + +def _get_embodiment_c_mujoco_kinematics_xml() -> str: + """Build a MuJoCo-loadable kinematics-only XML string from the committed URDF.""" + + root = ET.parse(get_embodiment_c_urdf_path()).getroot() + mujoco_element = root.find("mujoco") + if mujoco_element is None: + mujoco_element = ET.Element("mujoco") + root.insert(0, mujoco_element) + compiler_element = mujoco_element.find("compiler") + if compiler_element is None: + compiler_element = ET.SubElement(mujoco_element, "compiler") + compiler_element.attrib["fusestatic"] = "false" + + for link_element in root.findall("link"): + for child_element in list(link_element): + if child_element.tag in {"visual", "collision"}: + link_element.remove(child_element) + + return ET.tostring(root, encoding="unicode") + + +class _MujocoFk: + """MuJoCo-backed FK engine for the committed AgiBot G1 omnipicker URDF.""" + + def __init__(self) -> None: + import mujoco + + self._mujoco = mujoco + self.model = mujoco.MjModel.from_xml_string(_get_embodiment_c_mujoco_kinematics_xml()) + self.data = mujoco.MjData(self.model) + self._joint_qpos_addresses: dict[str, int] = {} + for joint_id in range(self.model.njnt): + joint_name = mujoco.mj_id2name(self.model, mujoco.mjtObj.mjOBJ_JOINT, joint_id) + if joint_name is not None: + self._joint_qpos_addresses[joint_name] = int(self.model.jnt_qposadr[joint_id]) + + def link_poses(self, joint_values: dict[str, float]) -> dict[str, np.ndarray]: + """Return world transforms for every named body in the MuJoCo model.""" + + self.data.qpos[:] = 0.0 + for joint_name, joint_value in joint_values.items(): + qpos_address = self._joint_qpos_addresses.get(joint_name) + if qpos_address is not None: + self.data.qpos[qpos_address] = float(joint_value) + self._mujoco.mj_forward(self.model, self.data) + + poses: dict[str, np.ndarray] = {} + for body_id in range(1, self.model.nbody): + body_name = self._mujoco.mj_id2name(self.model, self._mujoco.mjtObj.mjOBJ_BODY, body_id) + if body_name is None: + continue + transform = np.eye(4, dtype=np.float32) + transform[:3, :3] = self.data.xmat[body_id].reshape(3, 3).astype(np.float32, copy=False) + transform[:3, 3] = self.data.xpos[body_id].astype(np.float32, copy=False) + poses[body_name] = transform + return poses + + +@lru_cache(maxsize=1) +def _get_fk_engine() -> _MujocoFk: + """Return a cached MuJoCo FK engine for the committed AgiBot URDF.""" + + return _MujocoFk() + + +def _extract_joint_values_from_state(state: np.ndarray, embodiment_type: str) -> dict[str, float]: + """Map one observation.state vector to the URDF joint names used for FK.""" + + if embodiment_type == "embodiment_c_gripper_ext": + # Ext layout: 94-dim state with joints at different offsets. + arm_state = state[AGIBOT_GEAR_EXT_ARM_STATE_SLICE] + head_yaw = float(state[AGIBOT_GEAR_EXT_STATE_HEAD_YAW_IDX]) + head_pitch = float(state[AGIBOT_GEAR_EXT_STATE_HEAD_PITCH_IDX]) + waist_lift = float(state[AGIBOT_GEAR_EXT_STATE_WAIST_LIFT_IDX]) + waist_pitch = float(state[AGIBOT_GEAR_EXT_STATE_WAIST_PITCH_IDX]) + else: + arm_state = state[AGIBOT_GEAR_ARM_STATE_SLICE] + head_yaw = float(state[AGIBOT_GEAR_STATE_HEAD_YAW_IDX]) + head_pitch = float(state[AGIBOT_GEAR_STATE_HEAD_PITCH_IDX]) + waist_pitch = float(state[AGIBOT_GEAR_STATE_WAIST_PITCH_IDX]) + waist_lift = float(state[AGIBOT_GEAR_STATE_WAIST_LIFT_IDX]) + + joint_values = { + AGIBOT_GEAR_WAIST_LIFT_JOINT_NAME: float(waist_lift), + AGIBOT_GEAR_WAIST_PITCH_JOINT_NAME: float(waist_pitch), + AGIBOT_GEAR_HEAD_YAW_JOINT_NAME: float(head_yaw), + AGIBOT_GEAR_HEAD_PITCH_JOINT_NAME: float(head_pitch), + } + joint_values.update({name: float(arm_state[idx]) for idx, name in enumerate(AGIBOT_GEAR_ARM_JOINT_NAMES_LEFT)}) + joint_values.update({name: float(arm_state[7 + idx]) for idx, name in enumerate(AGIBOT_GEAR_ARM_JOINT_NAMES_RIGHT)}) + _set_gripper_joint_values_from_state(joint_values, state, embodiment_type) + return joint_values + + +def _set_gripper_joint_values_from_state( + joint_values: dict[str, float], + state: np.ndarray, + embodiment_type: str, +) -> None: + """Map observed scalar gripper state into all omnipicker finger joints.""" + + embodiment_spec = get_embodiment_c_embodiment_spec(embodiment_type) + if embodiment_spec.kind != "gripper": + return + + if embodiment_type == "embodiment_c_gripper_ext": + left_raw = float(state[AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE][0]) + right_raw = float(state[AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE][0]) + else: + kind_spec = get_embodiment_c_kind_spec(embodiment_type) + state_hand_slice = kind_spec.state_hand_slice + left_raw = float(state[state_hand_slice.start]) + right_raw = float(state[state_hand_slice.start + 1]) + + left_open = float(convert_gripper_state_to_open_fraction(np.asarray([left_raw], dtype=np.float32))[0]) # [1] + right_open = float(convert_gripper_state_to_open_fraction(np.asarray([right_raw], dtype=np.float32))[0]) # [1] + for opening, joint_mimics in ( + (left_open, AGIBOT_GEAR_LEFT_GRIPPER_JOINT_MIMICS), + (right_open, AGIBOT_GEAR_RIGHT_GRIPPER_JOINT_MIMICS), + ): + primary_angle = -float(np.clip(opening, 0.0, 1.0)) * AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD + for joint_name, multiplier, offset in joint_mimics: + joint_values[joint_name] = multiplier * primary_angle + offset + + +def compute_fk_transforms( + state: np.ndarray, + embodiment_type: str, +) -> dict[str, np.ndarray]: + """Compute native-frame calibrated head-camera and gripper-base transforms for one state.""" + + fk_engine = _get_fk_engine() + link_poses = fk_engine.link_poses(_extract_joint_values_from_state(state, embodiment_type)) + + return { + "head_camera": link_poses[AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME].astype(np.float32, copy=False), + "right_wrist": link_poses[AGIBOT_GEAR_RIGHT_EE_LINK_NAME].astype(np.float32, copy=False), + "left_wrist": link_poses[AGIBOT_GEAR_LEFT_EE_LINK_NAME].astype(np.float32, copy=False), + } + + +def extract_fk_transforms_from_link_poses(link_poses: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + """Extract calibrated head-camera and gripper-base transforms from URDF link poses.""" + + return { + "head_camera": link_poses[AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME].astype(np.float32, copy=False), + "right_wrist": link_poses[AGIBOT_GEAR_RIGHT_EE_LINK_NAME].astype(np.float32, copy=False), + "left_wrist": link_poses[AGIBOT_GEAR_LEFT_EE_LINK_NAME].astype(np.float32, copy=False), + } + + +def compute_fk_transforms_batch( + states: np.ndarray, + embodiment_type: str, +) -> dict[str, np.ndarray]: + """Compute absolute transforms for a batch of AgiBot observation states.""" + + num_steps = int(states.shape[0]) + head_camera = np.empty((num_steps, 4, 4), dtype=np.float32) + right_wrist = np.empty((num_steps, 4, 4), dtype=np.float32) + left_wrist = np.empty((num_steps, 4, 4), dtype=np.float32) + + for step in range(num_steps): + transforms = compute_fk_transforms(states[step], embodiment_type) + head_camera[step] = transforms["head_camera"] + right_wrist[step] = transforms["right_wrist"] + left_wrist[step] = transforms["left_wrist"] + + transforms_by_name = { + "head_camera": head_camera, + "right_wrist": right_wrist, + "left_wrist": left_wrist, + } + return _apply_ext_base_motion_to_poses(transforms_by_name, states, embodiment_type) + + +def compute_link_poses_batch(states: np.ndarray, embodiment_type: str) -> dict[str, np.ndarray]: + """Compute absolute URDF link poses for a batch of AgiBot observation states.""" + + num_steps = int(states.shape[0]) + if num_steps == 0: + return {} + + fk_engine = _get_fk_engine() + first_link_poses = fk_engine.link_poses(_extract_joint_values_from_state(states[0], embodiment_type)) + batched_link_poses = {link_name: np.empty((num_steps, 4, 4), dtype=np.float32) for link_name in first_link_poses} + for link_name, pose in first_link_poses.items(): + batched_link_poses[link_name][0] = pose.astype(np.float32, copy=False) + + for step in range(1, num_steps): + link_poses = fk_engine.link_poses(_extract_joint_values_from_state(states[step], embodiment_type)) + for link_name, pose in link_poses.items(): + batched_link_poses[link_name][step] = pose.astype(np.float32, copy=False) + + return _apply_ext_base_motion_to_poses(batched_link_poses, states, embodiment_type) + + +def convert_gripper_state_to_open_fraction(values: np.ndarray) -> np.ndarray: + """Convert observed AgiBot gripper state to viewer/dataset open fractions. + + The shared viewer/action convention is ``0=closed`` and ``1=open``. + Observed AgiBot gripper state uses actuator-close angle units: ``0`` is + open and ``120`` is closed. Some episodes contain small closed-state + overshoot above ``120``; those values are accepted and clipped to fully + closed. Small open-state sensor jitter such as ``0.217`` must therefore + remain nearly fully open, not be interpreted as a normalized close fraction. + """ + + values = np.asarray(values, dtype=np.float32) + if values.size == 0: + return values + if not np.isfinite(values).all(): + raise ValueError("AgiBot gripper values contain NaN or Inf values.") + + min_value = float(np.min(values)) + max_value = float(np.max(values)) + if ( + min_value < -_GRIPPER_VALUE_EPS + and min_value >= -AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD - _GRIPPER_VALUE_EPS + and max_value <= _GRIPPER_VALUE_EPS + ): + return _scale_negative_to_unit_interval(values, AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD) + if ( + min_value < -_GRIPPER_VALUE_EPS + and min_value >= -np.degrees(AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD) - _GRIPPER_VALUE_EPS + and max_value <= _GRIPPER_VALUE_EPS + ): + return _scale_negative_to_unit_interval(values, np.degrees(AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD)) + max_actuator_value = AGIBOT_GEAR_GRIPPER_OPEN_ACTUATOR_DEG + _GRIPPER_ACTUATOR_OVERSHOOT_DEG + if min_value >= -_GRIPPER_VALUE_EPS and max_value <= max_actuator_value + _GRIPPER_VALUE_EPS: + close_fraction = _scale_to_unit_interval(values, AGIBOT_GEAR_GRIPPER_OPEN_ACTUATOR_DEG) # [*] + return (1.0 - close_fraction).astype(np.float32, copy=False) # [*] + + raise ValueError( + f"Unsupported AgiBot gripper value range; min={min_value:.4f}, max={max_value:.4f}. " + f"Expected URDF angle [-pi/4,0] or actuator-close degrees [0,{max_actuator_value:.1f}] " + f"(values above {AGIBOT_GEAR_GRIPPER_OPEN_ACTUATOR_DEG:.1f} are clipped closed)." + ) + + +def extract_gripper_states(states: np.ndarray, embodiment_type: str) -> tuple[np.ndarray | None, np.ndarray | None]: + """Extract left/right scalar gripper positions from observed AgiBot states.""" + + embodiment_spec = get_embodiment_c_embodiment_spec(embodiment_type) + if embodiment_spec.kind != "gripper": + return None, None + + if embodiment_type == "embodiment_c_gripper_ext": + right = states[:, AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE].reshape(states.shape[0], -1) # [T,1] + left = states[:, AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE].reshape(states.shape[0], -1) # [T,1] + else: + kind_spec = get_embodiment_c_kind_spec(embodiment_type) + state_hand_slice = kind_spec.state_hand_slice + right = states[:, state_hand_slice.start + 1 : state_hand_slice.start + 2].reshape(states.shape[0], -1) # [T,1] + left = states[:, state_hand_slice.start : state_hand_slice.start + 1].reshape(states.shape[0], -1) # [T,1] + + right_gripper = convert_gripper_state_to_open_fraction(right[:, 0]) # [T] + left_gripper = convert_gripper_state_to_open_fraction(left[:, 0]) # [T] + return right_gripper, left_gripper + + +def build_viewer_batch(sample: dict[str, object], embodiment_type: str) -> AgibotGearViewerBatch: + """Build viewer-ready absolute poses from one AgiBot dataset sample.""" + + states = _to_numpy_float32(sample["observation.state"]) # [T+1,S] + num_steps = int(states.shape[0] - 1) + if num_steps <= 0: + raise ValueError("AgiBot viewer batch requires observation.state to contain at least 2 frames.") + state_steps = states[1:] # [T,S] + + native_transforms = compute_fk_transforms_batch(state_steps, embodiment_type) # {name:[T,4,4]} + transforms = apply_agibot_gripper_to_opencv( + native_transforms, + AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST, + ) # {name:[T,4,4]} + right_gripper, left_gripper = extract_gripper_states(state_steps, embodiment_type) + return AgibotGearViewerBatch( + head_camera_poses=transforms["head_camera"], + right_wrist_poses=transforms["right_wrist"], + left_wrist_poses=transforms["left_wrist"], + right_gripper=right_gripper, + left_gripper=left_gripper, + ) diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk_test.py b/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk_test.py new file mode 100644 index 00000000..18b43ec5 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_c_fk_test.py @@ -0,0 +1,464 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest +import torch + +from cosmos.data.vfm.action.embodiment_c_dataset import EmbodimentCGripperDataset +from cosmos.data.vfm.action.embodiment_c_fk import ( + AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST, + build_viewer_batch, +) +from cosmos.data.vfm.action.embodiment_c_spec import ( + AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE, + AGIBOT_GEAR_EXT_STATE_ROBOT_POSITION_SLICE, + AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD, + AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME, + AGIBOT_GEAR_LEFT_EE_LINK_NAME, + AGIBOT_GEAR_LEFT_GRIPPER_CENTER_LINK_NAME, + AGIBOT_GEAR_RIGHT_EE_LINK_NAME, + get_embodiment_c_urdf_path, +) +from cosmos.data.vfm.action.agibotworld_beta_dataset import AgiBotWorldBetaDataset +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.urdf_visualizer.viewer import DATASETS + +_REPO_ROOT = Path(__file__).resolve().parents[5] +_LOCAL_GRIPPER_SAMPLE_ROOT = _REPO_ROOT / "dev" / "embodiment_c_samples" / "gripper" / "task_sample_gripper" +_REQUIRES_MUJOCO = pytest.mark.skipif( + importlib.util.find_spec("mujoco") is None, + reason="requires mujoco until CI docker images include it", +) + + +def _make_sample(state_dim: int) -> dict[str, torch.Tensor]: + state = torch.zeros((3, state_dim), dtype=torch.float32) # [T+1,S] + return { + "observation.state": state, + } + + +def _make_beta_robot_pose(num_steps: int) -> dict[str, torch.Tensor]: + robot_quat = torch.zeros((num_steps, 4), dtype=torch.float32) # [T,4] + robot_quat[:, 3] = 1.0 + return { + "observation.states.robot.position": torch.zeros((num_steps, 3), dtype=torch.float32), # [T,3] + "observation.states.robot.orientation": robot_quat, # [T,4] + } + + +def test_get_embodiment_c_urdf_path_exists() -> None: + assert get_embodiment_c_urdf_path().name == "G1_omnipicker_calibrated.urdf" + assert get_embodiment_c_urdf_path().parent.name == "urdf_visualizer" + assert get_embodiment_c_urdf_path().is_file() + + +@_REQUIRES_MUJOCO +def test_fk_uses_calibrated_head_camera_link() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms, compute_link_poses_batch + + state = np.zeros(32, dtype=np.float32) + fk = compute_fk_transforms(state, "embodiment_c_gripper") + link_poses = compute_link_poses_batch(state[None, :], "embodiment_c_gripper") + + np.testing.assert_allclose(fk["head_camera"], link_poses[AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME][0], atol=1e-6) + + +@_REQUIRES_MUJOCO +def test_fk_uses_gripper_base_not_center_link() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms, compute_link_poses_batch + + state = np.zeros(32, dtype=np.float32) + fk = compute_fk_transforms(state, "embodiment_c_gripper") + link_poses = compute_link_poses_batch(state[None, :], "embodiment_c_gripper") + + np.testing.assert_allclose(fk["left_wrist"], link_poses[AGIBOT_GEAR_LEFT_EE_LINK_NAME][0], atol=1e-6) + np.testing.assert_allclose(fk["right_wrist"], link_poses[AGIBOT_GEAR_RIGHT_EE_LINK_NAME][0], atol=1e-6) + assert not np.allclose( + fk["left_wrist"][:3, 3], + link_poses[AGIBOT_GEAR_LEFT_GRIPPER_CENTER_LINK_NAME][0, :3, 3], + atol=1e-6, + ) + + +@_REQUIRES_MUJOCO +@pytest.mark.L0 +def test_dataset_gripper_poses_are_aligned_to_opencv_convention() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms, compute_link_poses_batch + + states = np.zeros((3, 32), dtype=np.float32) # [T+1,S] + dataset = EmbodimentCGripperDataset( + root=["unused"], + chunk_length=2, + skip_video_loading=True, + return_agibot_link_poses=True, + ) + action, extras = dataset._build_fk_action({"observation.state": torch.from_numpy(states)}) + fk = compute_fk_transforms(states[0], "embodiment_c_gripper") + link_poses = compute_link_poses_batch(states[:1], "embodiment_c_gripper") + to_opencv = AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST + + assert action.shape == (2, 29) + assert "agibot_link_poses" in extras + assert extras["agibot_link_poses"][AGIBOT_GEAR_LEFT_EE_LINK_NAME].shape == (3, 4, 4) + np.testing.assert_allclose( + extras["initial_pose_left"].numpy()[:3, :3], + fk["left_wrist"][:3, :3] @ to_opencv["left_wrist"], + atol=1e-6, + ) + np.testing.assert_allclose( + extras["initial_pose_right"].numpy()[:3, :3], + fk["right_wrist"][:3, :3] @ to_opencv["right_wrist"], + atol=1e-6, + ) + np.testing.assert_allclose(fk["left_wrist"], link_poses[AGIBOT_GEAR_LEFT_EE_LINK_NAME][0], atol=1e-6) + np.testing.assert_allclose(fk["right_wrist"], link_poses[AGIBOT_GEAR_RIGHT_EE_LINK_NAME][0], atol=1e-6) + np.testing.assert_allclose( + extras["initial_pose_left"].numpy()[:3, 3], + link_poses[AGIBOT_GEAR_LEFT_EE_LINK_NAME][0, :3, 3], + atol=1e-6, + ) + np.testing.assert_allclose( + extras["initial_pose_right"].numpy()[:3, 3], + link_poses[AGIBOT_GEAR_RIGHT_EE_LINK_NAME][0, :3, 3], + atol=1e-6, + ) + + +@_REQUIRES_MUJOCO +@pytest.mark.L0 +def test_dataset_does_not_return_agibot_link_poses_by_default() -> None: + import numpy as np + + states = np.zeros((3, 32), dtype=np.float32) # [T+1,S] + dataset = EmbodimentCGripperDataset(root=["unused"], chunk_length=2, skip_video_loading=True) + + _, extras = dataset._build_fk_action({"observation.state": torch.from_numpy(states)}) + + assert "agibot_link_poses" not in extras + + +@pytest.mark.L0 +def test_agibot_gripper_to_opencv_composes_extra_180deg_z_rotation() -> None: + import numpy as np + + expected_left = np.asarray( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, + ) + expected_right = np.asarray( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, + ) + + np.testing.assert_allclose(AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST["left_wrist"], expected_left, atol=1e-6) + np.testing.assert_allclose(AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST["right_wrist"], expected_right, atol=1e-6) + + +def test_renderer_undoes_agibot_to_opencv_for_ik() -> None: + import numpy as np + + from cosmos.data.vfm.action.urdf_visualizer.unified_renderer import _undo_wrist_to_opencv_for_ik + + to_opencv = AGIBOT_GEAR_GRIPPER_TO_OPENCV_BY_WRIST + native_poses = np.tile(np.eye(4, dtype=np.float32), (2, 1, 1)) # [T,4,4] + native_poses[:, :3, 3] = np.asarray([[0.1, 0.0, 0.2], [0.2, 0.0, 0.2]], dtype=np.float32) # [T,3] + viewer_poses = native_poses.copy() # [T,4,4] + viewer_poses[:, :3, :3] = viewer_poses[:, :3, :3] @ to_opencv["right_wrist"] # [T,3,3] + + ik_poses = _undo_wrist_to_opencv_for_ik(viewer_poses, to_opencv, "right_wrist") + + assert ik_poses is not None + np.testing.assert_allclose(ik_poses, native_poses, atol=1e-6) + + +def test_gripper_open_fraction_conversions() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import convert_gripper_state_to_open_fraction + + open_jitter_degrees = np.array([0.0, 0.21733333], dtype=np.float32) + np.testing.assert_allclose( + convert_gripper_state_to_open_fraction(open_jitter_degrees), + [1.0, 0.9981889], + atol=1e-6, + ) + np.testing.assert_allclose( + convert_gripper_state_to_open_fraction(np.array([0.0], dtype=np.float32)), + [1.0], + atol=1e-6, + ) + + radians = np.array([-AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD, -AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD / 2.0, 0.0]) + np.testing.assert_allclose(convert_gripper_state_to_open_fraction(radians), [1.0, 0.5, 0.0], atol=1e-6) + + actuator_degrees = np.array([0.0, 60.0, 120.0], dtype=np.float32) + np.testing.assert_allclose(convert_gripper_state_to_open_fraction(actuator_degrees), [1.0, 0.5, 0.0], atol=1e-6) + + +@pytest.mark.L0 +def test_gripper_open_fraction_clips_small_actuator_overshoot() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import convert_gripper_state_to_open_fraction + + actuator_degrees = np.array([122.1857], dtype=np.float32) # [1] + np.testing.assert_allclose(convert_gripper_state_to_open_fraction(actuator_degrees), [0.0], atol=1e-6) + + +@_REQUIRES_MUJOCO +def test_link_poses_apply_observed_gripper_state() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_link_poses_batch + + states = np.zeros((2, 32), dtype=np.float32) # [T,S] + states[1, 14] = 120.0 + link_poses = compute_link_poses_batch(states, "embodiment_c_gripper") + + open_pose = link_poses["gripper_l_inner_link1"][0] # [4,4] + closed_pose = link_poses["gripper_l_inner_link1"][1] # [4,4] + assert not np.allclose(open_pose, closed_pose) + + +@_REQUIRES_MUJOCO +def test_standard_body_head_layout_uses_state19_as_waist_lift() -> None: + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms_batch + + states = np.zeros((2, 32), dtype=np.float32) # [T,S] + states[:, 17] = 0.25 # head pitch + states[:, 18] = 0.5 # waist pitch + states[:, 19] = 0.1 # waist lift + states[1, 19] = 0.2 + + fk = compute_fk_transforms_batch(states, "embodiment_c_gripper") + + for key in ("head_camera", "right_wrist", "left_wrist"): + np.testing.assert_allclose(fk[key][1, 2, 3] - fk[key][0, 2, 3], 0.1, atol=1e-6) + + +@_REQUIRES_MUJOCO +def test_build_viewer_batch_gripper_shapes() -> None: + sample = _make_sample(state_dim=32) + + batch = build_viewer_batch(sample, "embodiment_c_gripper") + + assert batch.head_camera_poses.shape == (2, 4, 4) + assert batch.right_wrist_poses.shape == (2, 4, 4) + assert batch.left_wrist_poses.shape == (2, 4, 4) + assert batch.right_gripper is not None + assert batch.left_gripper is not None + assert batch.right_gripper.shape == (2,) + assert batch.left_gripper.shape == (2,) + + +@_REQUIRES_MUJOCO +@pytest.mark.skipif(not _LOCAL_GRIPPER_SAMPLE_ROOT.is_dir(), reason="local Embodiment C sample data is unavailable") +def test_embodiment_c_dataset_loads_local_gripper_sample_without_video() -> None: + dataset = EmbodimentCGripperDataset( + root=[str(_LOCAL_GRIPPER_SAMPLE_ROOT)], + fps=30.0, + split="full", + mode="policy", + skip_video_loading=True, + ) + dataset._register_sources() + + sample = dataset[0] + + assert len(dataset) == 184 + assert sample["action"].shape == (16, 29) + assert sample["__episode_id__"] == 0 + assert sample["__task_root__"] == str(_LOCAL_GRIPPER_SAMPLE_ROOT) + + +@_REQUIRES_MUJOCO +def test_agibotworld_beta_builds_fk_action_for_viewer() -> None: + dataset = AgiBotWorldBetaDataset( + root=[], + chunk_length=2, + split="full", + mode="policy", + ) + sample = { + "observation.states.effector.position": torch.zeros((3, 2), dtype=torch.float32), + "observation.states.joint.position": torch.zeros((3, 14), dtype=torch.float32), + "observation.states.head.position": torch.zeros((3, 2), dtype=torch.float32), + "observation.states.waist.position": torch.zeros((3, 2), dtype=torch.float32), + **_make_beta_robot_pose(3), + } + + action, extras = dataset._build_fk_action(sample) + + assert action.shape == (2, 29) + assert extras["initial_pose"].shape == (4, 4) + assert extras["initial_pose_right"].shape == (4, 4) + assert extras["initial_pose_left"].shape == (4, 4) + + +@_REQUIRES_MUJOCO +def test_agibotworld_beta_link_poses_include_observed_base_motion() -> None: + import numpy as np + + dataset = AgiBotWorldBetaDataset( + root=[], + chunk_length=2, + split="full", + mode="policy", + return_agibot_link_poses=True, + ) + robot_pose = _make_beta_robot_pose(3) + robot_pose["observation.states.robot.position"] = torch.tensor( + [[0.0, 0.0, 0.0], [0.4, -0.2, 0.0], [0.7, -0.1, 0.0]], + dtype=torch.float32, + ) # [T+1,3] + sample = { + "observation.states.effector.position": torch.zeros((3, 2), dtype=torch.float32), + "observation.states.joint.position": torch.zeros((3, 14), dtype=torch.float32), + "observation.states.head.position": torch.zeros((3, 2), dtype=torch.float32), + "observation.states.waist.position": torch.zeros((3, 2), dtype=torch.float32), + **robot_pose, + } + + _, extras = dataset._build_fk_action(sample) + + assert "agibot_link_poses" in extras + head_poses = extras["agibot_link_poses"][AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME].numpy() # [T+1,4,4] + np.testing.assert_allclose(head_poses[1, :3, 3] - head_poses[0, :3, 3], [0.4, -0.2, 0.0], atol=1e-6) + np.testing.assert_allclose(head_poses[2, :3, 3] - head_poses[0, :3, 3], [0.7, -0.1, 0.0], atol=1e-6) + + +@_REQUIRES_MUJOCO +@pytest.mark.L0 +def test_agibotworld_beta_keeps_debug_detail_out_of_ai_caption(monkeypatch: pytest.MonkeyPatch) -> None: + dataset = AgiBotWorldBetaDataset( + root=[], + chunk_length=2, + split="full", + mode="policy", + ) + video = torch.zeros((3, 3, 4, 4), dtype=torch.float32) # [T+1,C,H,W] + wrist_video = torch.zeros((3, 3, 2, 2), dtype=torch.float32) # [T+1,C,H,W] + sample = { + "task": "Open the wardrobe and hang the clothes. | Debug detail for viewer only.", + "observation.images.head": video, + "observation.images.hand_left": wrist_video, + "observation.images.hand_right": wrist_video, + "observation.states.effector.position": torch.zeros((3, 2), dtype=torch.float32), # [T+1,2] + "observation.states.joint.position": torch.zeros((3, 14), dtype=torch.float32), # [T+1,14] + "observation.states.head.position": torch.zeros((3, 2), dtype=torch.float32), # [T+1,2] + "observation.states.waist.position": torch.zeros((3, 2), dtype=torch.float32), # [T+1,2] + **_make_beta_robot_pose(3), + } + + def _fake_fetch_sample(_idx: int) -> tuple[str, int, int, dict[str, object]]: + return "policy", 0, 0, sample + + monkeypatch.setattr(dataset, "_fetch_sample", _fake_fetch_sample) + + result = dataset[0] + + assert result["ai_caption"] == "Open the wardrobe and hang the clothes." + assert result["debug_caption"] == "Debug detail for viewer only." + + +def test_agibotworld_beta_viewer_entry_declares_agibot_robot_embodiment() -> None: + assert DATASETS["agibotworld_beta"].robot_embodiment_type == "embodiment_c_gripper" + assert DATASETS["agibotworld_beta"].dataset_kwargs["return_agibot_link_poses"] is True + + +def test_embodiment_c_gripper_ext_registered_for_viewer_load() -> None: + assert DATASETS["embodiment_c_gripper_ext"].robot_embodiment_type == "embodiment_c_gripper_ext" + assert get_domain_id("embodiment_c_gripper_ext") == get_domain_id("embodiment_c_gripper") + + +def test_agibot_viewer_entries_do_not_force_debug_roots() -> None: + for name in ("embodiment_c_gripper", "embodiment_c_gripper_ext", "agibotworld_beta"): + kwargs = DATASETS[name].dataset_kwargs + assert "root" not in kwargs + assert "max_loaded_datasets" not in kwargs + + +@_REQUIRES_MUJOCO +def test_build_viewer_batch_gripper_ext_shapes() -> None: + """Ext: 94-dim state → same viewer batch shape as standard gripper.""" + sample = _make_sample(state_dim=94) + + batch = build_viewer_batch(sample, "embodiment_c_gripper_ext") + + assert batch.head_camera_poses.shape == (2, 4, 4) + assert batch.right_wrist_poses.shape == (2, 4, 4) + assert batch.left_wrist_poses.shape == (2, 4, 4) + assert batch.right_gripper is not None + assert batch.left_gripper is not None + assert batch.right_gripper.shape == (2,) + assert batch.left_gripper.shape == (2,) + + +@_REQUIRES_MUJOCO +def test_ext_fk_uses_correct_state_indices() -> None: + """Verify ext FK reads arm joints from state[54:68], not state[0:14].""" + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms + + # Build a 94-dim state where arm joints at [54:68] differ from [0:14]. + state_ext = np.zeros(94, dtype=np.float32) + # Put distinctive values in the correct ext arm joint positions. + state_ext[54:61] = [0.1, -0.2, 0.3, -0.1, 0.2, 0.5, -0.3] # left arm + state_ext[61:68] = [-0.1, 0.2, -0.3, 0.1, -0.2, -0.5, 0.3] # right arm + state_ext[82] = 0.0 # head yaw + state_ext[83] = 0.3 # head pitch + state_ext[84] = 0.5 # waist pitch + state_ext[85] = 0.35 # waist lift + + # Build an equivalent 32-dim standard state with the same joint values. + state_std = np.zeros(32, dtype=np.float32) + state_std[0:7] = state_ext[54:61] # left arm + state_std[7:14] = state_ext[61:68] # right arm + state_std[16] = 0.0 # head yaw + state_std[17] = 0.3 # head pitch + state_std[18] = 0.5 # waist pitch + state_std[19] = 0.35 # waist lift + + fk_ext = compute_fk_transforms(state_ext, "embodiment_c_gripper_ext") + fk_std = compute_fk_transforms(state_std, "embodiment_c_gripper") + + # Ext and standard FK should produce identical transforms. + for key in ("head_camera", "right_wrist", "left_wrist"): + np.testing.assert_allclose(fk_ext[key], fk_std[key], atol=1e-6, err_msg=f"FK mismatch for {key}") + + +@_REQUIRES_MUJOCO +def test_ext_fk_applies_robot_base_motion_to_batch_poses() -> None: + """Ext FK folds state/robot pose into all head and wrist trajectories.""" + import numpy as np + + from cosmos.data.vfm.action.embodiment_c_fk import compute_fk_transforms_batch + + states = np.zeros((2, 94), dtype=np.float32) # [T,S] + states[:, AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE] = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32) + states[1, AGIBOT_GEAR_EXT_STATE_ROBOT_POSITION_SLICE] = np.array([0.4, -0.2, 0.0], dtype=np.float32) + + fk = compute_fk_transforms_batch(states, "embodiment_c_gripper_ext") + + for key in ("head_camera", "right_wrist", "left_wrist"): + np.testing.assert_allclose(fk[key][1, :3, 3] - fk[key][0, :3, 3], [0.4, -0.2, 0.0], atol=1e-6) diff --git a/cosmos_training/cosmos/data/vfm/action/embodiment_c_spec.py b/cosmos_training/cosmos/data/vfm/action/embodiment_c_spec.py new file mode 100644 index 00000000..5acf4ae0 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/embodiment_c_spec.py @@ -0,0 +1,142 @@ +"""Shared Embodiment C metadata used by datasets and visualizers.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +AgibotGearKind = Literal["gripper"] + +AGIBOT_GEAR_VIDEO_KEY = "observation.images.top_head" +AGIBOT_GEAR_URDF_FILENAME = "G1_omnipicker_calibrated.urdf" +AGIBOT_GEAR_ROBOT_NAME = "embodiment_c" +AGIBOT_GEAR_GRIPPER_NORMALIZER_EMBODIMENT_TYPE = "embodiment_c_gripper" +AGIBOT_GEAR_ARM_STATE_SLICE = slice(0, 14) +AGIBOT_GEAR_STATE_HEAD_YAW_IDX = 16 +AGIBOT_GEAR_STATE_HEAD_PITCH_IDX = 17 +AGIBOT_GEAR_STATE_WAIST_PITCH_IDX = 18 +AGIBOT_GEAR_STATE_WAIST_LIFT_IDX = 19 +AGIBOT_GEAR_HEAD_PITCH_JOINT_NAME = "idx04_head_pitch_joint" + +# -- Ext layout constants (94-dim state) ------------------------------------- +# The ext split stores joints at different offsets from the standard layout. +AGIBOT_GEAR_EXT_ARM_STATE_SLICE = slice(54, 68) +AGIBOT_GEAR_EXT_STATE_HEAD_YAW_IDX = 82 +AGIBOT_GEAR_EXT_STATE_HEAD_PITCH_IDX = 83 +AGIBOT_GEAR_EXT_STATE_WAIST_PITCH_IDX = 84 +AGIBOT_GEAR_EXT_STATE_WAIST_LIFT_IDX = 85 +AGIBOT_GEAR_EXT_STATE_ROBOT_POSITION_SLICE = slice(86, 89) +AGIBOT_GEAR_EXT_STATE_ROBOT_ORIENTATION_SLICE = slice(89, 93) +AGIBOT_GEAR_EXT_STATE_LEFT_HAND_SLICE = slice(0, 1) +AGIBOT_GEAR_EXT_STATE_RIGHT_HAND_SLICE = slice(1, 2) +AGIBOT_GEAR_ARM_BASE_LINK_NAME = "arm_base_link" +AGIBOT_GEAR_HEAD_LINK_NAME = "head_pitch_link" +AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME = "head_camera_link" +AGIBOT_GEAR_LEFT_EE_LINK_NAME = "gripper_l_base_link" +AGIBOT_GEAR_RIGHT_EE_LINK_NAME = "gripper_r_base_link" +AGIBOT_GEAR_LEFT_GRIPPER_CENTER_LINK_NAME = "gripper_l_center_link" +AGIBOT_GEAR_RIGHT_GRIPPER_CENTER_LINK_NAME = "gripper_r_center_link" +AGIBOT_GEAR_ARM_JOINT_NAMES_LEFT = tuple(f"idx{4 + i:02d}_left_arm_joint{i}" for i in range(1, 8)) +AGIBOT_GEAR_ARM_JOINT_NAMES_RIGHT = tuple(f"idx{11 + i:02d}_right_arm_joint{i}" for i in range(1, 8)) +AGIBOT_GEAR_WAIST_LIFT_JOINT_NAME = "idx01_waist_lift_joint" +AGIBOT_GEAR_WAIST_PITCH_JOINT_NAME = "idx02_waist_pitch_joint" +AGIBOT_GEAR_HEAD_YAW_JOINT_NAME = "idx03_head_yaw_joint" +AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD = math.pi / 4.0 +AGIBOT_GEAR_GRIPPER_OPEN_ACTUATOR_DEG = 120.0 +AGIBOT_GEAR_LEFT_GRIPPER_JOINT_MIMICS = ( + ("idx31_gripper_l_inner_joint1", 1.0, 0.0), + ("idx32_gripper_l_inner_joint3", 0.1, 0.0), + ("idx33_gripper_l_inner_joint4", 0.25, 0.0), + ("idx39_gripper_l_inner_joint0", -0.7, 0.0), + ("idx41_gripper_l_outer_joint1", -1.0, 0.0), + ("idx42_gripper_l_outer_joint3", 0.1, 0.0), + ("idx43_gripper_l_outer_joint4", -0.25, 0.0), + ("idx49_gripper_l_outer_joint0", 0.7, 0.0), +) +AGIBOT_GEAR_RIGHT_GRIPPER_JOINT_MIMICS = ( + ("idx71_gripper_r_inner_joint1", 1.0, 0.0), + ("idx72_gripper_r_inner_joint3", 0.1, 0.0), + ("idx73_gripper_r_inner_joint4", 0.25, 0.0), + ("idx79_gripper_r_inner_joint0", -0.7, 0.0), + ("idx81_gripper_r_outer_joint1", -1.0, 0.0), + ("idx82_gripper_r_outer_joint3", 0.1, 0.0), + ("idx83_gripper_r_outer_joint4", -0.25, 0.0), + ("idx89_gripper_r_outer_joint0", 0.7, 0.0), +) + + +@dataclass(frozen=True) +class AgibotGearKindSpec: + """Layout metadata shared across all embodiments of one hand kind.""" + + kind: AgibotGearKind + state_hand_slice: slice + + +@dataclass(frozen=True) +class AgibotGearEmbodimentSpec: + """Per-embodiment metadata shared by training and visualization code.""" + + embodiment_type: str + kind: AgibotGearKind + action_dim: int + + +AGIBOT_GEAR_KIND_SPECS: dict[AgibotGearKind, AgibotGearKindSpec] = { + "gripper": AgibotGearKindSpec( + kind="gripper", + state_hand_slice=slice(14, 16), + ), +} + +AGIBOT_GEAR_EMBODIMENT_SPECS: dict[str, AgibotGearEmbodimentSpec] = { + "embodiment_c_gripper": AgibotGearEmbodimentSpec( + embodiment_type="embodiment_c_gripper", + kind="gripper", + action_dim=29, # FK output: head(9)+right(9)+gripper(1)+left(9)+gripper(1) + ), + "embodiment_c_gripper_ext": AgibotGearEmbodimentSpec( + embodiment_type="embodiment_c_gripper_ext", + kind="gripper", + action_dim=29, # FK output: head(9)+right(9)+gripper(1)+left(9)+gripper(1) + ), +} + + +def get_embodiment_c_embodiment_spec(embodiment_type: str) -> AgibotGearEmbodimentSpec: + """Return the registered spec for one AgiBot embodiment.""" + + try: + return AGIBOT_GEAR_EMBODIMENT_SPECS[embodiment_type] + except KeyError as exc: + raise ValueError( + f"Unknown Embodiment C embodiment_type={embodiment_type!r}. " + f"Expected one of {sorted(AGIBOT_GEAR_EMBODIMENT_SPECS)}." + ) from exc + + +def get_embodiment_c_kind_spec(embodiment_type: str | AgibotGearKind) -> AgibotGearKindSpec: + """Resolve an embodiment type or kind to its shared layout metadata.""" + + kind = embodiment_type if embodiment_type in AGIBOT_GEAR_KIND_SPECS else get_embodiment_c_kind(embodiment_type) + return AGIBOT_GEAR_KIND_SPECS[kind] + + +def get_embodiment_c_kind(embodiment_type: str) -> AgibotGearKind: + """Return the hand kind used by one AgiBot embodiment.""" + + return get_embodiment_c_embodiment_spec(embodiment_type).kind + + +def get_embodiment_c_action_dim(embodiment_type: str) -> int: + """Return the action dimension for one AgiBot embodiment.""" + + return get_embodiment_c_embodiment_spec(embodiment_type).action_dim + + +def get_embodiment_c_urdf_path() -> Path: + """Return the committed Embodiment C G1 omnipicker URDF path.""" + + return Path(__file__).resolve().parent / "urdf_visualizer" / AGIBOT_GEAR_URDF_FILENAME diff --git a/cosmos_training/cosmos/data/vfm/action/filter_bridge_dataset.py b/cosmos_training/cosmos/data/vfm/action/filter_bridge_dataset.py new file mode 100644 index 00000000..e7c7534f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/filter_bridge_dataset.py @@ -0,0 +1,679 @@ +"""Filter and split bridge_orig_lerobot dataset. + +Step 1 (filter): + - Load task instructions from tasks.parquet, normalize text, classify each + task as clean or flagged (gibberish / single-word / question / non-English / + pattern-matched), merge duplicate texts, then write: + bridge_tasks_clean.json — clean tasks + bridge_tasks_flagged.json — flagged tasks + +Step 2 (split, unless --filter_only): + - Create {name}_clean dataset dir (symlinked data/videos, filtered meta). + - Optionally create {name}_dirty dir with --save_dirty. + - Optionally trim static head/tail frames with --static_threshold. + +Usage: + CMD="python filter_bridge_dataset.py" + + # Filter only (produce JSONs, no dataset split) + $CMD --root /path/to/dataset --filter_only + + # Filter + split (clean only, default) + $CMD --root /path/to/dataset + + # Filter + split (clean + dirty) + $CMD --root /path/to/dataset --save_dirty + + # Trim static frames (action[:6] near zero) from clean episodes + $CMD --root /path/to/dataset --static_threshold 5e-4 + + # Custom output locations + $CMD --root /path/to/dataset --output_dir /tmp/results --output_name my_bridge + +Arguments: + --root Path to the original LeRobot dataset + --output_dir Dir for JSON outputs (default: script dir) + --output_name Base name for split dirs (default: derived from --root) + --static_threshold Trim static head/tail frames; 0 = off (default), try 5e-4 + --save_dirty Also create the dirty (flagged) split + --filter_only Only run filter step, skip split +""" + +import argparse +import glob +import json +import os +import re +import shutil + +import nltk +import numpy as np +import pandas as pd + +nltk.download("words", quiet=True) +from nltk.corpus import words as nltk_words # noqa: E402 + +ENGLISH_WORDS = set(w.lower() for w in nltk_words.words()) +TASK_WORD_ALLOWLIST = { + # Common robotics task words that are valid English but missing from nltk.words. + "fridge", +} + +FLAGGED_PATTERNS = [ + r".*image.*", + r".*https?.*", + r".*\bnot\b.*", + r".*\bnothing\b.*", + r".*\banything\b.*", + r".*\bdidn.?t\b.*", + r".*\bdoesn.?t\b.*", + r".*\baren.?t\b.*", + r".*\bupload\b.*", + r".*\bpicture\b.*", + r"\(.*\)", +] + + +# --------------------------------------------------------------------------- +# Text normalization +# --------------------------------------------------------------------------- + + +def normalize_task(text: str) -> str: + t = text.strip() + t = t.strip("\"'''") + t = t.replace("\u201a\u00c4\u00f4", "'") + t = re.sub(r"\s{2,}", " ", t) + if "." in t and " " not in t: + t = t.replace(".", " ") + if "_" in t and " " not in t: + t = t.replace("_", " ") + return t.strip() + + +# --------------------------------------------------------------------------- +# Classification helpers +# --------------------------------------------------------------------------- + + +def _clean_word(word: str) -> str: + return word.lower().strip(".,!?;:'\"()-/\\") + + +def _is_english(word: str) -> bool: + w = _clean_word(word) + if not w: + return True + if w in TASK_WORD_ALLOWLIST: + return True + if not w.isalpha(): + alpha_part = "".join(c for c in w if c.isalpha()) + if len(alpha_part) >= 3 and alpha_part not in ENGLISH_WORDS: + return False + return True + if len(w) <= 2: + return True + if w in ENGLISH_WORDS: + return True + for suffix in ("s", "ed", "ing", "ly", "er", "est", "tion", "ness"): + stem = w.removesuffix(suffix) + if len(stem) >= 2 and stem in ENGLISH_WORDS: + return True + return False + + +def is_gibberish(text: str) -> tuple[bool, str]: + t = text.strip() + if not t: + return False, "" + words = t.split() + alpha_words = [w for w in words if any(c.isalpha() for c in w)] + if not alpha_words: + return True, "no_alphabetic_words" + non_english = [w for w in alpha_words if not _is_english(w)] + ratio = len(non_english) / len(alpha_words) + if len(alpha_words) <= 2 and len(non_english) >= 1: + return ( + True, + f"short_non_english({len(non_english)}/{len(alpha_words)}): {non_english[:8]}", + ) + if len(alpha_words) >= 3 and ratio >= 0.6: + return ( + True, + f"non_english_words({len(non_english)}/{len(alpha_words)}): {non_english[:8]}", + ) + if len(alpha_words) >= 6 and ratio >= 0.5: + return ( + True, + f"many_non_english({len(non_english)}/{len(alpha_words)}): {non_english[:8]}", + ) + if re.search(r"(.)\1{4,}", t.lower()): + return True, "excessive_char_repetition" + short_non_eng = [w for w in alpha_words if len(_clean_word(w)) <= 3 and not _is_english(w)] + if len(alpha_words) >= 5 and len(short_non_eng) / len(alpha_words) >= 0.6: + return ( + True, + f"many_short_nonsense({len(short_non_eng)}/{len(alpha_words)}): {short_non_eng[:8]}", + ) + return False, "" + + +def is_pattern_flagged(text: str) -> bool: + t = text.strip().lower() + if not t: + return True + return any(re.match(pat, t) for pat in FLAGGED_PATTERNS) + + +def is_single_word(text: str) -> bool: + words = text.strip().split() + return sum(1 for w in words if any(c.isalpha() for c in w)) == 1 + + +def is_question(text: str) -> bool: + t = text.strip() + if t.endswith("?"): + return True + t_lower = t.lower() + q_starts = ( + "what ", + "how ", + "why ", + "where ", + "when ", + "who ", + "which ", + "did ", + "does ", + "do ", + "can ", + "could ", + "would ", + "should ", + "will ", + ) + return any(t_lower.startswith(q) for q in q_starts) + + +def is_non_english(text: str) -> bool: + return sum(1 for c in text.strip() if not c.isascii() and c.isalpha()) >= 2 + + +def classify_task(text: str) -> list[str]: + reasons = [] + if is_pattern_flagged(text): + reasons.append("pattern") + gib, _ = is_gibberish(text) + if gib: + reasons.append("gibberish") + if is_question(text): + reasons.append("question") + if is_non_english(text): + reasons.append("non_english") + if is_single_word(text) and not reasons: + reasons.append("single_word") + return reasons + + +# --------------------------------------------------------------------------- +# I/O helpers +# --------------------------------------------------------------------------- + + +def save_compact_json(path, description, num_tasks, num_episodes, tasks): + with open(path, "w") as f: + f.write("{\n") + f.write(f' "description": {json.dumps(description)},\n') + f.write(f' "num_tasks": {num_tasks},\n') + f.write(f' "num_episodes": {num_episodes},\n') + f.write(' "tasks": [\n') + for i, t in enumerate(tasks): + line = json.dumps(t, ensure_ascii=False) + comma = "," if i < len(tasks) - 1 else "" + f.write(f" {line}{comma}\n") + f.write(" ]\n") + f.write("}\n") + + +def load_task_text_col(root): + tasks_df = pd.read_parquet(os.path.join(root, "meta", "tasks.parquet")).reset_index() + text_candidates = [c for c in tasks_df.columns if c != "task_index"] + if text_candidates: + return tasks_df, text_candidates[0] + if tasks_df["task_index"].dtype == object: + tasks_df["task_text"] = tasks_df["task_index"] + tasks_df["task_index"] = range(len(tasks_df)) + return tasks_df, "task_text" + raise RuntimeError(f"Cannot find task text column: {tasks_df.columns.tolist()}") + + +# --------------------------------------------------------------------------- +# Step 1: Filter tasks +# --------------------------------------------------------------------------- + + +def filter_tasks(root, output_dir): + print(f"nltk dictionary: {len(ENGLISH_WORDS)} words") + tasks_df, text_col = load_task_text_col(root) + print(f"Total unique tasks: {len(tasks_df)}") + + data_files = sorted(glob.glob(os.path.join(root, "data", "**", "*.parquet"), recursive=True)) + print(f"Reading {len(data_files)} data parquet files ...") + dfs = [pd.read_parquet(f, columns=["episode_index", "task_index"]) for f in data_files] + data_df = pd.concat(dfs, ignore_index=True) + + ep_tasks = data_df.drop_duplicates("episode_index")[["episode_index", "task_index"]] + task_counts = ep_tasks.groupby("task_index").size().reset_index(name="num_episodes") + total_episodes = ep_tasks["episode_index"].nunique() + print(f"Total episodes: {total_episodes}") + + merged = task_counts.merge(tasks_df, on="task_index", how="left") + merged[text_col] = merged[text_col].apply(normalize_task) + merged["flag_reasons"] = merged[text_col].apply(classify_task) + merged["flagged"] = merged["flag_reasons"].apply(lambda r: len(r) > 0) + + # Merge duplicate task texts + merged["task_normalized"] = merged[text_col].str.strip().str.lower() + + def _merge_group(group): + return { + "task": group[text_col].iloc[0].strip(), + "task_indices": sorted(group["task_index"].tolist()), + "num_episodes": int(group["num_episodes"].sum()), + "flagged": bool(group["flagged"].any()), + "flag_reasons": sorted(set(r for reasons in group["flag_reasons"] for r in reasons)), + } + + grouped = merged.groupby("task_normalized").apply(_merge_group, include_groups=False).tolist() + + clean_tasks = sorted([t for t in grouped if not t["flagged"]], key=lambda x: -x["num_episodes"]) + flagged_tasks = sorted([t for t in grouped if t["flagged"]], key=lambda x: -x["num_episodes"]) + + total_clean_eps = sum(t["num_episodes"] for t in clean_tasks) + total_flagged_eps = sum(t["num_episodes"] for t in flagged_tasks) + + os.makedirs(output_dir, exist_ok=True) + clean_path = os.path.join(output_dir, "bridge_tasks_clean.json") + save_compact_json( + clean_path, + "Clean task instructions", + len(clean_tasks), + total_clean_eps, + clean_tasks, + ) + + flagged_path = os.path.join(output_dir, "bridge_tasks_flagged.json") + save_compact_json( + flagged_path, + "Flagged task instructions", + len(flagged_tasks), + total_flagged_eps, + flagged_tasks, + ) + + print( + f"\nClean: {len(clean_tasks)} tasks / {total_clean_eps} episodes ({total_clean_eps / total_episodes * 100:.1f}%)" + ) + print( + f"Flagged: {len(flagged_tasks)} tasks / {total_flagged_eps} episodes ({total_flagged_eps / total_episodes * 100:.1f}%)" + ) + for label in ("pattern", "gibberish", "single_word", "question", "non_english"): + n = sum(1 for t in flagged_tasks if label in t["flag_reasons"]) + if n: + print(f" - {label}: {n}") + print(f"Saved: {clean_path}") + print(f"Saved: {flagged_path}") + + return clean_path, flagged_path + + +# --------------------------------------------------------------------------- +# Step 2: Split dataset (symlinked data/videos, filtered meta) +# --------------------------------------------------------------------------- + + +def build_episode_task_map(root): + data_files = sorted(glob.glob(os.path.join(root, "data", "**", "*.parquet"), recursive=True)) + dfs = [pd.read_parquet(f, columns=["episode_index", "task_index"]) for f in data_files] + data_df = pd.concat(dfs, ignore_index=True) + return data_df.drop_duplicates("episode_index").set_index("episode_index")["task_index"].to_dict() + + +def create_split(orig_root, output_root, episode_set, orig_info, orig_episodes_df, label, trims=None): + """Create a dataset split by zeroing out frame ranges for excluded episodes. + + LeRobot uses episode_index as a positional index into the episodes table, + so we must keep ALL rows. Instead of removing rows, we zero out + dataset_from_index and dataset_to_index for excluded episodes, which makes + build_episode_spans compute valid_len <= 0 and skip them. + + If *trims* is provided, head/tail static frames are trimmed by adjusting + dataset_from_index / dataset_to_index for included episodes. + """ + os.makedirs(output_root, exist_ok=True) + for subdir in ["data", "videos"]: + src = os.path.join(orig_root, subdir) + dst = os.path.join(output_root, subdir) + if os.path.islink(dst): + os.remove(dst) + elif os.path.isdir(dst): + shutil.rmtree(dst) + elif os.path.exists(dst): + os.remove(dst) + os.symlink(src, dst) + + modified_eps = orig_episodes_df.copy() + excluded_mask = ~modified_eps["episode_index"].isin(episode_set) + has_range = "dataset_from_index" in modified_eps.columns and "dataset_to_index" in modified_eps.columns + + if has_range: + modified_eps.loc[excluded_mask, "dataset_from_index"] = 0 + modified_eps.loc[excluded_mask, "dataset_to_index"] = 0 + + if trims: + for idx in modified_eps.index: + ep_id = modified_eps.at[idx, "episode_index"] + if ep_id in episode_set and ep_id in trims: + head_trim, tail_trim = trims[ep_id] + modified_eps.at[idx, "dataset_from_index"] += head_trim + modified_eps.at[idx, "dataset_to_index"] -= tail_trim + if modified_eps.at[idx, "dataset_from_index"] >= modified_eps.at[idx, "dataset_to_index"]: + modified_eps.at[idx, "dataset_from_index"] = 0 + modified_eps.at[idx, "dataset_to_index"] = 0 + + total_frames = 0 + included = modified_eps[~excluded_mask] + if has_range: + total_frames = int((included["dataset_to_index"] - included["dataset_from_index"]).sum()) + + meta_dir = os.path.join(output_root, "meta") + os.makedirs(meta_dir, exist_ok=True) + + ep_meta_dir = os.path.join(meta_dir, "episodes", "chunk-000") + os.makedirs(ep_meta_dir, exist_ok=True) + modified_eps.to_parquet(os.path.join(ep_meta_dir, "file-000.parquet"), index=False) + + shutil.copy2( + os.path.join(orig_root, "meta", "tasks.parquet"), + os.path.join(meta_dir, "tasks.parquet"), + ) + stats_src = os.path.join(orig_root, "meta", "stats.json") + if os.path.exists(stats_src): + shutil.copy2(stats_src, os.path.join(meta_dir, "stats.json")) + + new_info = orig_info.copy() + new_info["total_episodes"] = len(orig_episodes_df) + if total_frames > 0: + new_info["total_frames"] = total_frames + new_info["splits"] = {"train": f"0:{len(orig_episodes_df)}"} + with open(os.path.join(meta_dir, "info.json"), "w") as f: + json.dump(new_info, f, indent=4) + + print( + f" {label}: {len(episode_set)} valid episodes (of {len(orig_episodes_df)} total), " + f"{total_frames} frames -> {output_root}" + ) + + +def compute_static_trims(root, episode_set, threshold): + """Compute per-episode head/tail static frame counts for trimming. + + A frame is "static" when max(abs(action[:6])) < threshold, where the + first 6 action dims are translation + rotation. + + Returns (trims_dict, details_list): + - trims_dict: {episode_index: (head_trim, tail_trim)} + - details_list: list of dicts for the JSON report + """ + data_files = sorted(glob.glob(os.path.join(root, "data", "**", "*.parquet"), recursive=True)) + print(f" Reading action data from {len(data_files)} files for static frame detection (threshold={threshold}) ...") + dfs = [pd.read_parquet(f, columns=["episode_index", "action"]) for f in data_files] + data_df = pd.concat(dfs, ignore_index=True) + + trims = {} + details = [] + total_head, total_tail = 0, 0 + fully_static = 0 + total_checked = 0 + + for ep_id, ep_df in data_df.groupby("episode_index"): + if ep_id not in episode_set: + continue + total_checked += 1 + + actions = np.stack(ep_df["action"].values) + trans_rot = actions[:, :6] + static_mask = np.abs(trans_rot).max(axis=1) < threshold + n_static = int(static_mask.sum()) + ep_len = len(ep_df) + + if n_static == 0: + continue + + head_trim = 0 + for i in range(ep_len): + if static_mask[i]: + head_trim += 1 + else: + break + + tail_trim = 0 + for i in range(ep_len - 1, -1, -1): + if static_mask[i]: + tail_trim += 1 + else: + break + + if head_trim + tail_trim >= ep_len: + fully_static += 1 + trims[int(ep_id)] = (0, ep_len) + details.append( + { + "episode_index": int(ep_id), + "ep_len": ep_len, + "head": 0, + "tail": ep_len, + "fully_static": True, + } + ) + total_tail += ep_len + continue + + if head_trim > 0 or tail_trim > 0: + trims[int(ep_id)] = (head_trim, tail_trim) + details.append( + { + "episode_index": int(ep_id), + "ep_len": ep_len, + "head": head_trim, + "tail": tail_trim, + "fully_static": False, + } + ) + total_head += head_trim + total_tail += tail_trim + + print( + f" {total_checked} eps checked, {len(trims)} trimmed (fully_static={fully_static}), frames: head={total_head} tail={total_tail} sum={total_head + total_tail}" + ) + return trims, details + + +def save_static_report(path, threshold, details): + """Save a JSON report of static frame trimming.""" + details_sorted = sorted(details, key=lambda x: -(x["head"] + x["tail"])) + total_head = sum(d["head"] for d in details) + total_tail = sum(d["tail"] for d in details) + fully_static = sum(1 for d in details if d["fully_static"]) + + with open(path, "w") as f: + f.write("{\n") + f.write(' "description": "Static frame trimming report",\n') + f.write(f' "threshold": {threshold},\n') + f.write(f' "episodes_trimmed": {len(details)},\n') + f.write(f' "episodes_fully_static": {fully_static},\n') + f.write( + f' "total_frames_trimmed": {{"head": {total_head}, "tail": {total_tail}, "sum": {total_head + total_tail}}},\n' + ) + f.write(' "episodes": [\n') + for i, d in enumerate(details_sorted): + line = json.dumps(d, ensure_ascii=False) + comma = "," if i < len(details_sorted) - 1 else "" + f.write(f" {line}{comma}\n") + f.write(" ]\n") + f.write("}\n") + print(f"Saved: {path}") + + +def split_dataset( + orig_root, + flagged_json, + output_base, + output_dir, + output_name=None, + static_threshold=0, + save_dirty=False, +): + with open(os.path.join(orig_root, "meta", "info.json")) as f: + orig_info = json.load(f) + ep_meta_files = sorted( + glob.glob( + os.path.join(orig_root, "meta", "episodes", "**", "*.parquet"), + recursive=True, + ) + ) + orig_episodes_df = pd.concat([pd.read_parquet(f) for f in ep_meta_files], ignore_index=True) + + with open(flagged_json) as f: + flagged_indices = set() + for t in json.load(f)["tasks"]: + flagged_indices.update(t["task_indices"]) + + ep_task_map = build_episode_task_map(orig_root) + + if "task_index" not in orig_episodes_df.columns: + task_series = pd.Series(ep_task_map, name="task_index") + task_series.index.name = "episode_index" + orig_episodes_df = orig_episodes_df.merge(task_series.reset_index(), on="episode_index", how="left") + + all_episodes = set(orig_episodes_df["episode_index"].tolist()) + clean_eps, dirty_eps = set(), set() + for ep in all_episodes: + ti = ep_task_map.get(ep, -1) + if ti in flagged_indices: + dirty_eps.add(ep) + else: + clean_eps.add(ep) + + trims = None + if static_threshold > 0: + print("\nStep 2b: Detecting static head/tail frames ...") + trims, details = compute_static_trims(orig_root, clean_eps, static_threshold) + report_path = os.path.join(output_dir, "bridge_static_trims.json") + save_static_report(report_path, static_threshold, details) + + base_name = output_name or os.path.basename(orig_root.rstrip("/")) + print(f"\nSplitting: {len(clean_eps)} clean, {len(dirty_eps)} dirty (save_dirty={save_dirty})") + create_split( + orig_root, + os.path.join(output_base, f"{base_name}_clean"), + clean_eps, + orig_info, + orig_episodes_df, + "CLEAN", + trims, + ) + if save_dirty: + create_split( + orig_root, + os.path.join(output_base, f"{base_name}_dirty"), + dirty_eps, + orig_info, + orig_episodes_df, + "DIRTY", + ) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description="Filter and split bridge dataset") + parser.add_argument( + "--root", + default="/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/bilyang/datasets/bridge_orig_lerobot_20260225", + ) + parser.add_argument( + "--output_json_dir", + default=None, + help="Dir for JSON outputs (default: same as this script)", + ) + parser.add_argument( + "--output_name", + default=None, + help="Base name for split dirs (default: derived from --root)", + ) + parser.add_argument( + "--output_data_dir", + default=None, + help="Dir for output split dataset (default: parent of root)", + ) + parser.add_argument( + "--static_threshold", + type=float, + default=0, + help="Trim static head/tail frames where max(abs(action[:6])) < threshold (0=off, try 5e-4)", + ) + parser.add_argument( + "--save_dirty", + action="store_true", + help="Also create the dirty (flagged) split (default: off)", + ) + parser.add_argument("--filter_only", action="store_true", help="Only run filter step, skip split") + args = parser.parse_args() + + output_dir = args.output_json_dir or os.path.dirname(os.path.abspath(__file__)) + + print("=" * 80) + print("Step 1: Filter tasks") + print("=" * 80) + clean_json, flagged_json = filter_tasks(args.root, output_dir) + + if args.static_threshold > 0 and args.filter_only: + print() + print("=" * 80) + print("Step 2: Static frame detection (dry-run)") + print("=" * 80) + with open(flagged_json) as f: + flagged_indices = set() + for t in json.load(f)["tasks"]: + flagged_indices.update(t["task_indices"]) + ep_task_map = build_episode_task_map(args.root) + clean_eps = {ep for ep, ti in ep_task_map.items() if ti not in flagged_indices} + _, details = compute_static_trims(args.root, clean_eps, args.static_threshold) + report_path = os.path.join(output_dir, "bridge_static_trims.json") + save_static_report(report_path, args.static_threshold, details) + + if not args.filter_only: + print() + print("=" * 80) + print("Step 2: Split dataset") + print("=" * 80) + output_base = args.output_data_dir if args.output_data_dir else os.path.dirname(args.root.rstrip("/")) + split_dataset( + args.root, + flagged_json, + output_base, + output_dir, + args.output_name, + args.static_threshold, + args.save_dirty, + ) + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/cosmos_training/cosmos/data/vfm/action/fractal.py b/cosmos_training/cosmos/data/vfm/action/fractal.py new file mode 100644 index 00000000..4414be5f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/fractal.py @@ -0,0 +1,192 @@ +# Fractal (fractal20220817_data) — Google Robot RT-1 dataset +# LeRobot v2.0 format from IPEC-COMMUNITY/fractal20220817_data_lerobot +# +# Robot: google_robot +# 87,212 episodes, 3,786,400 frames, 599 tasks, fps=3 +# state: [x, y, z, rx, ry, rz, rw, gripper] (8D, quaternion) +# action: [x, y, z, roll, pitch, yaw, gripper] (7D, delta) +# video: observation.images.image (256×320) + +from typing import Any, cast + +import numpy as np +import torch +from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata + +from cosmos.utils import log +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.pose_utils import ( + PoseConvention, + build_abs_pose_from_components, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +_VALID_POSE_CONVENTIONS = ("backward_anchored", "backward_framewise") +# These episodes contain base motion, which breaks the fixed-base Google Robot +# action assumption used by training and the viewer. +_SKIPPED_EPISODE_IDS: frozenset[int] = frozenset({29, 189, 382}) + +# Google Robot raw EE frame has x/y axes rotated ~90° around z compared to +# OpenCV convention. Rz(-90°) as a right-multiply corrects this: +# new_x = -old_y (rightward), new_y = old_x (downward), z unchanged (approach). +_GOOGLE_ROBOT_TO_OPENCV = np.array([[0, 1, 0], [-1, 0, 0], [0, 0, 1]], dtype=np.float32) + +# --------------------------------------------------------------------------- +# TCP → flange (gripper body) offset +# --------------------------------------------------------------------------- +# The fractal dataset records EE poses at ``link_gripper_tcp`` — a calibrated +# tool-center-point 164 mm past the gripper body (``link_gripper``), roughly +# at the fingertip. For action learning we re-reference poses to the +# *gripper body* (``link_gripper``) because: +# 1. It is the last actuated link — its pose is fully determined by joint +# angles, whereas the TCP has a tiny calibration-dependent tilt (~0.25°). +# 2. Gripper body is a more natural frame for grasping tasks: the position +# is at the wrist, not at the fragile fingertip. +# 3. The ~10 cm offset reduces the lever-arm effect of small rotation +# errors on position accuracy. +# +# The constant below is the SE(3) transform from ``link_gripper_tcp`` to +# ``link_gripper``, computed from the SimplerEnv URDF via pinocchio FK at the +# neutral configuration: +# T = oMf[link_gripper_tcp]⁻¹ · oMf[link_gripper] +# +# Source URDF: https://github.com/simpler-env/ManiSkill2_real2sim +# → mani_skill2_real2sim/assets/descriptions/google_robot_description/ +# fmt: off +_TCP_TO_FLANGE = np.array([ + [+0.9999897671, -0.0008686425, +0.0044397163, -0.0050618476], + [+0.0008745501, +0.9999987346, -0.0013288658, -0.0016717725], + [-0.0044385564, +0.0013327349, +0.9999892615, -0.1635144743], + [+0.0000000000, +0.0000000000, +0.0000000000, +1.0000000000], +], dtype=np.float32) +# fmt: on + + +class FractalLeRobotDataset(BaseActionLeRobotDataset): + """Action wrapper for the Fractal (Google RT-1) dataset in LeRobot format.""" + + def __init__( + self, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/fractal20220817_data_no_noops", + fps: float = 3.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + mode: str = "policy", + pose_convention: PoseConvention = "backward_framewise", + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "ego_view", + enable_fast_init: bool = False, + ) -> None: + """Initialize FractalLeRobotDataset. + + Args: + root: Path to the local LeRobot dataset root. + fps: Frames per second of the dataset. + chunk_length: Number of action frames per sample. + split_seed: Seed for deterministic train/val splitting. + split_val_ratio: Fraction of episodes held out for validation. + split: One of "train", "val", or "full". + mode: Training mode — "policy", "forward_dynamics", + "inverse_dynamics", "image2video", or "joint". + pose_convention: Relative-pose convention used to encode SE(3) + actions. Supports ``"backward_framewise"`` and + ``"backward_anchored"``. Set to ``None`` to disable action + construction outside image-to-video mode. + action_normalization: Optional bundled-stats normalization + (``"quantile"`` / ``"quantile_rot"`` / ``"meanstd"`` / ``"minmax"``); + ``None`` returns raw actions. + viewpoint: Camera viewpoint type for this dataset. + """ + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type="fractal", + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format="rot6d", + action_normalization=action_normalization, + tolerance_s=1e-4, + enable_fast_init=enable_fast_init, + ) + + self._to_opencv = _GOOGLE_ROBOT_TO_OPENCV + self._all_shard_roots = [root] + + self._delta_timestamps = { + "observation.images.image": [i * self._dt for i in range(0, self._chunk_length + 1)], + "observation.state": [i * self._dt for i in range(0, self._chunk_length + 1)], + "action": [i * self._dt for i in range(0, self._chunk_length)], + } + + def _filter_valid_episodes(self, meta: LeRobotDatasetMetadata, episode_ids: list[int]) -> list[int]: + """Drop known-bad raw Fractal episode IDs before index spans are built.""" + kept = [ep_id for ep_id in episode_ids if ep_id not in _SKIPPED_EPISODE_IDS] + dropped = len(episode_ids) - len(kept) + if dropped: + log.info( + f"FractalLeRobotDataset: dropped {dropped} / {len(episode_ids)} " + f"episodes from skip list {sorted(_SKIPPED_EPISODE_IDS)} " + f"(total_episodes={meta.total_episodes})" + ) + return kept + + def _build_action_spec(self) -> ActionSpec: + """Fractal: 10D = ``[Pos(3), Rot6d(6), Gripper(1)]``.""" + return build_action_spec(Pos(dim=3), Rot("rot6d"), Gripper()) + + def __getitem__(self, idx: int) -> dict[str, Any]: + """Return a single training sample.""" + mode, _, _, sample = self._fetch_sample(idx) + + ai_caption = sample["task"] + + video = sample["observation.images.image"] # [T,C,H,W] + + # State layout: [x, y, z, rx, ry, rz, rw, gripper] (T+1 frames) + # Quaternion order from dataset: (rx, ry, rz, rw) matches scipy's (x, y, z, w). + state = sample["observation.state"] # [T+1, 8] + poses_abs = build_abs_pose_from_components( + state[:, 0:3], + state[:, 3:7], + "quat_xyzw", + ) + # 1. TCP → flange: shift from link_gripper_tcp to link_gripper + poses_abs = poses_abs @ _TCP_TO_FLANGE + # 2. Kinematics → OpenCV convention (rotation only) + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ self._to_opencv + initial_pose = torch.from_numpy(poses_abs[0].copy()).float() + poses_rel = pose_abs_to_rel( + poses_abs, + rotation_format="rot6d", + pose_convention=cast(PoseConvention, self._pose_convention), + ) + action = torch.cat( + [ + torch.from_numpy(poses_rel).float(), # SE3 relative pose (rot6d) + sample["action"][:, [6]], # gripper (1D) + ], + dim=-1, + ) # [T, 10] + return self._build_result( + mode=mode, video=video, action=action, ai_caption=ai_caption, initial_pose=initial_pose + ) + + @property + def action_dim(self) -> int: + """Action dimensionality: position(3) + 6D rotation(6) + gripper(1) = 10.""" + return 10 diff --git a/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset.py b/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset.py new file mode 100644 index 00000000..e80535f0 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset.py @@ -0,0 +1,1359 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Hand-pose manipulation dataset. + +A bimanual human hand dataset in LeRobot v3 format with 21-keypoint hand pose +annotations (positions + per-joint quaternion rotations) in camera space, +along with per-frame camera pose (position + quaternion rotation), task/subtask +labels and RGB video. + +Action layout: ``[camera, R_wrist, R_fingers, L_wrist, L_fingers]`` — camera +pose followed by right-hand then left-hand components. + +Action space (``pose_convention``) +------------------------------- +Both action spaces share the same three-stage computation: + + 1. Compute **absolute** SE(3) poses for camera and both wrists. + 2. Compute **finger positions** in the per-frame wrist coordinate frame. + 3. Convert absolute camera/wrist poses to **relative** representations + (anchored or frame-wise). + +Layout (both modes): ``[camera, R_wrist, R_fingers, L_wrist, L_fingers]`` + + ``backward_anchored`` + Camera and wrist poses anchored to frame 0: + ``P_{0}^{-1} @ P_{t}`` for camera and each wrist. + Fingers are positions in the current wrist frame. + + ``backward_framewise`` + Frame-wise SE(3) deltas: + ``P_{t-1}^{-1} @ P_{t}`` for camera and each wrist. + Fingers are positions in the current wrist frame. + +Action dimensions +~~~~~~~~~~~~~~~~~ +Both modes have the same dimensionality: + - Camera: ``3 + rot_dim`` + - Per hand (×2): wrist ``(3 + rot_dim)`` + fingers ``(N_finger × 3)`` + - Total: ``(3 + rot_dim) + 2 × ((3 + rot_dim) + N_finger × 3)`` + +Example with ``rot6d`` rotation, ``wrist_plus_finger_tips`` (5 fingertips): + - Camera: 3 + 6 = 9D + - Per hand: wrist (3 + 6 = 9D) + fingers (5 × 3 = 15D) = 24D + - Total: 9 + 24 + 24 = **57D** + +Rotation format (``rotation_format``) +-------------------------------------- +Applied uniformly to both hand joint rotations and camera ego-motion rotation: + - ``rot9d``: flattened 3x3 rotation matrix (default, converted from quaternions) + - ``rot6d``: first 2 columns of the rotation matrix (continuous, Zhou et al. CVPR 2019) + - ``euler_xyz``: Euler ``xyz`` angles in radians +""" + +import os +import random +from bisect import bisect_right +from pathlib import Path +from typing import Any, Literal + +import numpy as np +import pandas as pd +import torch + +from cosmos.utils import log +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + BaseActionLeRobotDataset, + _parallel_map, + build_episode_spans, + split_episode_ids, +) +from cosmos.data.vfm.action.hand_pose_dataset_config import ( + CAM_POSITION_KEY, + CAM_ROTATION_KEY, + FINGERTIP_JOINT_IDXS, + HAND_LEFT_POSITION_KEY, + HAND_LEFT_ROTATION_KEY, + HAND_POSE_DATASETS, + HAND_RIGHT_POSITION_KEY, + HAND_RIGHT_ROTATION_KEY, + NO_ACTION_SKIP_LABEL_PREFIXES, + NO_ACTION_SKIP_LABEL_SUBSTRINGS, + NO_ACTION_SKIP_LABELS, + NUM_JOINTS, + QUAT_DIM_PER_JOINT, + ROTATION_FORMAT_DIM, + WRIST_FRAME_ALIGN_EMBODIMENT_A, + WRIST_JOINT_IDX, +) +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + build_abs_pose_from_components, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + + +class HandPoseDataset(BaseActionLeRobotDataset): + """Hand-pose manipulation dataset backed by LeRobot v3. + + Each sample returns a video chunk and the corresponding hand-pose action + representation. Uses deferred source registration via + ``BaseActionLeRobotDataset``. + """ + + def __init__( + self, + root: str | list[str] = HAND_POSE_DATASETS["embodiment_a_feb08_500hr"], + fps: float = 15.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.005, + split: str = "train", + mode: str = "policy", + embodiment_type: str = "hand_pose", + video_key: str = "observation.images.main", + keypoint_option: Literal[ + "wrist_only", "wrist_plus_fingers", "wrist_plus_finger_tips" + ] = "wrist_plus_finger_tips", + rotation_format: RotationConvention = "rot6d", + pose_convention: Literal[ + "backward_anchored", + "backward_framewise", + ] = "backward_framewise", + action_normalization: ActionNormalization | None = None, + intra_episode_val_ratio: float = 0.0, + tolerance_s: float = 2e-4, + drop_unannotated_edge_frames: bool = True, + unannotated_pos_l1_threshold: float = 1e-6, + max_item_retries: int = 16, + return_overlay_data: bool = False, + max_episodes: int | None = None, + episode_ids: list[int] | None = None, + load_subtasks: bool = False, + snap_to_subtask: bool = False, + skip_no_action: bool = False, + max_subtasks_per_episode: int | None = None, + viewpoint: Viewpoint = "ego_view", + enable_fast_init: bool = False, + ) -> None: + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type=embodiment_type, + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format=rotation_format, + action_normalization=action_normalization, + tolerance_s=tolerance_s, + enable_fast_init=enable_fast_init, + ) + if max_episodes is not None and episode_ids is not None: + raise ValueError("Cannot specify both max_episodes and episode_ids.") + self._max_episodes = max_episodes + self._episode_ids = episode_ids + + self._video_key = video_key + self._load_subtasks = load_subtasks or skip_no_action or snap_to_subtask + self._snap_to_subtask = snap_to_subtask + self._skip_no_action = skip_no_action + self._max_subtasks_per_episode = max_subtasks_per_episode + self._subtask_transitions: dict[tuple[int, int], list[tuple[int, int]]] = {} + self._per_shard_orig_subtask_names: dict[int, dict[int, str]] = {} + self._ds_idx_to_shard_idx: list[int] | None = None + self._orig_subtask_index: dict[int, np.ndarray] = {} + self._cached_subtask_col: dict[int, np.ndarray] = {} + self._orig_subtask_transitions: dict[tuple[int, int], list[tuple[int, int]]] = {} + self._subtask_start_indices: dict[tuple[int, int], list[tuple[int, int]]] = {} + self._keypoint_option = keypoint_option + self._rotation_format = rotation_format + self._pose_convention = pose_convention + self._intra_episode_val_ratio = intra_episode_val_ratio + self._drop_unannotated_edge_frames = drop_unannotated_edge_frames + self._unannotated_pos_l1_threshold = unannotated_pos_l1_threshold + self._max_item_retries = max_item_retries + self._warned_tolerance_once = False + self._warned_unannotated_once = False + self._warned_nan_action_once = False + self._warned_build_action_error_once = False + self._logged_decode_failures: set[tuple[int, int, int]] = set() + self._return_overlay_data = return_overlay_data + + # Normalize root: config may pass OmegaConf ListConfig for list of shards; ensure list of str. + if isinstance(root, str): + _shard_roots = [root] + else: + _shard_roots = [str(p) for p in root] + + self._root = _shard_roots[0] + self._all_shard_roots = _shard_roots + + # Hand pose keys (camera-space positions + rotations). + self._position_keys = [HAND_LEFT_POSITION_KEY, HAND_RIGHT_POSITION_KEY] + self._rotation_keys = [HAND_LEFT_ROTATION_KEY, HAND_RIGHT_ROTATION_KEY] + + if keypoint_option not in {"wrist_only", "wrist_plus_fingers", "wrist_plus_finger_tips"}: + raise ValueError(f"Unsupported keypoint_option: {keypoint_option!r}") + if not (0.0 <= intra_episode_val_ratio < 1.0): + raise ValueError(f"intra_episode_val_ratio must be in [0, 1). Got: {intra_episode_val_ratio}") + + if keypoint_option == "wrist_plus_finger_tips": + self._position_joint_indices: tuple[int, ...] = (WRIST_JOINT_IDX, *FINGERTIP_JOINT_IDXS) + elif keypoint_option == "wrist_plus_fingers": + self._position_joint_indices = tuple(range(NUM_JOINTS)) + else: + self._position_joint_indices = (WRIST_JOINT_IDX,) + + # Compute raw action dimension. + # Layout: [camera, R_wrist, R_fingers, L_wrist, L_fingers] + # Camera: 3 (pos) + rot_dim. Per hand: 3 (wrist pos) + rot_dim + (N_finger × 3). + rot_dim_per_joint = ROTATION_FORMAT_DIM[rotation_format] + num_pos_joints = len(self._position_joint_indices) + num_finger_joints = num_pos_joints - 1 # exclude wrist + per_hand_dim = 3 + rot_dim_per_joint + num_finger_joints * 3 + self._raw_action_dim = (3 + rot_dim_per_joint) + 2 * per_hand_dim + + # Build delta_timestamps: T+1 video frames, T+1 hand states. + ts = [i * self._dt for i in range(self._chunk_length + 1)] + self._delta_timestamps = { + self._video_key: list(ts), + } + for key in ( + HAND_LEFT_POSITION_KEY, + HAND_RIGHT_POSITION_KEY, + HAND_LEFT_ROTATION_KEY, + HAND_RIGHT_ROTATION_KEY, + ): + self._delta_timestamps[key] = list(ts) + + # Always load camera pose (both action spaces include camera). + self._delta_timestamps[CAM_POSITION_KEY] = list(ts) + self._delta_timestamps[CAM_ROTATION_KEY] = list(ts) + + self._episode_intrinsics: dict[int, np.ndarray] = {} + if return_overlay_data: + self._load_episode_intrinsics(self._all_shard_roots[0]) + + # Load task/subtask label mappings for captions. + self._task_names: dict[int, str] = {} + self._per_shard_subtask_names: dict[int, dict[int, str]] = {} + self._load_task_labels() + + _POSE_CONVENTION_DESC = { + "backward_anchored": ( + "Camera + wrist poses anchored to frame 0, fingers in wrist frame. " + "Layout: [camera, R_wrist, R_fingers, L_wrist, L_fingers]" + ), + "backward_framewise": ( + "Frame-wise camera + wrist deltas, fingers in wrist frame. " + "Layout: [camera, R_wrist, R_fingers, L_wrist, L_fingers]" + ), + } + log.info( + f"HandPoseDataset configuration:\n" + f" root = {root}\n" + f" split = {split}\n" + f" mode = {mode}\n" + f" pose_convention = {pose_convention}\n" + f" -> {_POSE_CONVENTION_DESC.get(pose_convention, 'unknown')}\n" + f" intra_episode_val_ratio = {intra_episode_val_ratio}\n" + f" position_keys = {self._position_keys}\n" + f" rotation_keys = {self._rotation_keys}\n" + f" keypoint_option = {keypoint_option}\n" + f" selected_joint_count = {num_pos_joints} ({num_finger_joints} finger joints)\n" + f" rotation_format = {rotation_format} ({rot_dim_per_joint}D per joint)\n" + f" raw_action_dim = {self._raw_action_dim} " + f"(cam {3 + rot_dim_per_joint}D + 2 × hand {per_hand_dim}D)\n" + f" chunk_length = {chunk_length} (video frames: {chunk_length + 1})\n" + f" fps = {fps}\n" + f" tolerance_s = {tolerance_s}\n" + f" drop_unannotated_edge_frames = {drop_unannotated_edge_frames}\n" + f" unannotated_pos_l1_threshold = {unannotated_pos_l1_threshold}\n" + f" max_item_retries = {max_item_retries}\n" + f" max_episodes = {max_episodes}\n" + f" episode_ids = {episode_ids}\n" + f" snap_to_subtask = {snap_to_subtask}\n" + f" skip_no_action = {skip_no_action}\n" + f" max_subtasks_per_episode = {self._max_subtasks_per_episode}\n" + f" domain_id = {self._domain_id} ({embodiment_type})" + ) + + # ------------------------------------------------------------------------- + # Per-episode subtask transition cache + # ------------------------------------------------------------------------- + + def _get_subtask_transitions(self, ds_idx: int, ep_idx: int) -> list[tuple[int, int]]: + """Return sorted ``(row_start, subtask_index)`` transitions for an episode. + + Built lazily on first access by slicing the ``subtask_index`` column + from the LeRobot HF dataset (cheap Arrow column access). + """ + key = (ds_idx, ep_idx) + cached = self._subtask_transitions.get(key) + if cached is not None: + return cached + + ds = self._get_dataset(ds_idx) + ep = ds.meta.episodes[ep_idx] + ep_start: int = ep["dataset_from_index"] + ep_end: int = ep["dataset_to_index"] + + if ds._absolute_to_relative_idx is not None: + rel_indices = [ds._absolute_to_relative_idx[i] for i in range(ep_start, ep_end)] + subtask_col = ds.hf_dataset["subtask_index"][rel_indices] + else: + subtask_col = ds.hf_dataset["subtask_index"][ep_start:ep_end] + + transitions: list[tuple[int, int]] = [] + prev_si = None + for offset, si_val in enumerate(subtask_col): + si = int(si_val) if not isinstance(si_val, int) else si_val + if si != prev_si: + transitions.append((ep_start + offset, si)) + prev_si = si + + self._subtask_transitions[key] = transitions + return transitions + + # ------------------------------------------------------------------------- + # Original (non-augcap) subtask data for "No action" filtering + # ------------------------------------------------------------------------- + + def _load_orig_subtask_index(self, ds_idx: int) -> np.ndarray: + """Lazily load the original subtask_index column for a dataset shard. + + Reads parquet files from the shard's ``data/`` directory and extracts + the ``subtask_index`` column as a flat numpy array. + """ + if ds_idx in self._orig_subtask_index: + return self._orig_subtask_index[ds_idx] + + build_args = self._dataset_build_args[ds_idx] + if build_args is None: + self._orig_subtask_index[ds_idx] = np.array([], dtype=np.int64) + return self._orig_subtask_index[ds_idx] + + shard_root: str | None = build_args["root"] + if shard_root is None: + self._orig_subtask_index[ds_idx] = np.array([], dtype=np.int64) + return self._orig_subtask_index[ds_idx] + data_dir = Path(shard_root) / "data" + parquet_files = sorted(data_dir.rglob("*.parquet")) + dfs = [pd.read_parquet(str(f), columns=["subtask_index"]) for f in parquet_files] + if dfs: + arr = pd.concat(dfs, ignore_index=True)["subtask_index"].to_numpy(dtype=np.int64) + else: + arr = np.array([], dtype=np.int64) + + self._orig_subtask_index[ds_idx] = arr + log.info(f"HandPoseDataset: loaded {len(arr)} original subtask indices for ds_idx={ds_idx}") + return arr + + def _get_orig_subtask_transitions(self, ds_idx: int, ep_idx: int) -> list[tuple[int, int]]: + """Return sorted ``(row_start, subtask_index)`` transitions from the original data. + + Reads subtask indices from the shard root's ``data/`` parquets using + the same episode boundaries as the loaded dataset. + """ + key = (ds_idx, ep_idx) + cached = self._orig_subtask_transitions.get(key) + if cached is not None: + return cached + + orig_indices = self._load_orig_subtask_index(ds_idx) + if len(orig_indices) == 0: + self._orig_subtask_transitions[key] = [] + return [] + + ds = self._get_dataset(ds_idx) + ep = ds.meta.episodes[ep_idx] + ep_start: int = ep["dataset_from_index"] + ep_end: int = ep["dataset_to_index"] + + subtask_col = orig_indices[ep_start:ep_end] + + transitions: list[tuple[int, int]] = [] + prev_si = None + for offset, si_val in enumerate(subtask_col): + si = int(si_val) + if si != prev_si: + transitions.append((ep_start + offset, si)) + prev_si = si + + self._orig_subtask_transitions[key] = transitions + return transitions + + def _is_no_action(self, subtask_index: int, ds_idx: int = 0) -> bool: + """Return ``True`` if the original subtask name indicates an idle segment. + + Matches the name (normalized: ``_`` → space, stripped, lowercased) + against three rule sets from ``hand_pose_dataset_config``: + exact ``NO_ACTION_SKIP_LABELS``, ``NO_ACTION_SKIP_LABEL_PREFIXES``, or + ``NO_ACTION_SKIP_LABEL_SUBSTRINGS``. + """ + name = self._get_orig_subtask_names_for_ds(ds_idx).get(subtask_index, "") + normalized = name.replace("_", " ").strip().lower() + if normalized in NO_ACTION_SKIP_LABELS: + return True + if NO_ACTION_SKIP_LABEL_PREFIXES and normalized.startswith(NO_ACTION_SKIP_LABEL_PREFIXES): + return True + if any(sub in normalized for sub in NO_ACTION_SKIP_LABEL_SUBSTRINGS): + return True + return False + + def _skip_no_action_subtask(self, ds_idx: int, row_idx: int, ep_idx: int) -> int | None: + """Advance ``row_idx`` past any original "No action" subtask. + + Returns: + The (possibly advanced) ``row_idx``, or ``None`` if every + remaining subtask in the episode is "No action". + """ + orig_transitions = self._get_orig_subtask_transitions(ds_idx, ep_idx) + if not orig_transitions: + return row_idx + + row_starts = [t[0] for t in orig_transitions] + ti = bisect_right(row_starts, row_idx) - 1 + if ti < 0: + return row_idx + + _, orig_si = orig_transitions[ti] + if not self._is_no_action(orig_si, ds_idx): + return row_idx + + for next_ti in range(ti + 1, len(orig_transitions)): + _, next_si = orig_transitions[next_ti] + if not self._is_no_action(next_si, ds_idx): + return orig_transitions[next_ti][0] + + return None + + # ------------------------------------------------------------------------- + # Build-time subtask-level reindexing for uniform subtask sampling + # ------------------------------------------------------------------------- + + def _transitions(self, col: np.ndarray, ep_start: int) -> list[tuple[int, int]]: + """``(row_start, value)`` pairs at each change point (first row always emitted). + + Vectorized when ``enable_fast_init=True``, else the original Python + ``enumerate`` loop. + """ + if self._enable_fast_init: + if len(col) == 0: + return [] + mask = np.concatenate(([True], col[1:] != col[:-1])) + idx = np.flatnonzero(mask) + return list(zip((ep_start + idx).tolist(), col[idx].astype(np.int64).tolist())) + transitions: list[tuple[int, int]] = [] + prev: int | None = None + for offset, val in enumerate(col): + v = int(val) + if v != prev: + transitions.append((ep_start + offset, v)) + prev = v + return transitions + + def _rebuild_snap_indices(self, ds_idx: int, meta: Any, records_before: int) -> None: + """Replace frame-level indices with subtask-level indices. + + When ``snap_to_subtask`` is active, the default frame-level indexing + biases sampling toward longer subtasks. This method replaces the + episode records with subtask-level records so that each subtask gets + exactly one index, yielding uniform sampling over subtasks. + + When ``skip_no_action`` is also active, subtasks whose original label + starts with "No action" are excluded at build time. + """ + build_args = self._dataset_build_args[ds_idx] + if build_args is None: + return + shard_root: str | None = build_args["root"] + if shard_root is None: + return + + if self._enable_fast_init: + subtask_col = self._cached_subtask_col[ds_idx] + else: + data_dir = Path(shard_root) / "data" + parquet_files = sorted(data_dir.rglob("*.parquet")) + dfs = [pd.read_parquet(str(f), columns=["subtask_index"]) for f in parquet_files] + if not dfs: + return + subtask_col = pd.concat(dfs, ignore_index=True)["subtask_index"].to_numpy(dtype=np.int64) + if len(subtask_col) == 0: + return + + orig_subtask_col: np.ndarray | None = None + if self._skip_no_action: + orig_subtask_col = self._load_orig_subtask_index(ds_idx) + + fps_ratio = round(meta.fps / self._fps) + + ep_from = list(meta.episodes["dataset_from_index"]) + ep_to = list(meta.episodes["dataset_to_index"]) + + new_records: list[tuple[int, int, int, int]] = [] + total_subtasks = 0 + total_skipped_na = 0 + total_skipped_short = 0 + + for rec in self._episode_records[records_before:]: + rec_ds_idx, sample_start, _old_valid_len, episode_id = rec + assert rec_ds_idx == ds_idx + + ep_start: int = ep_from[episode_id] + ep_end: int = ep_to[episode_id] + + transitions = self._transitions(subtask_col[ep_start:ep_end], ep_start) + + orig_trans: list[tuple[int, int]] | None = None + orig_row_starts: list[int] | None = None + if orig_subtask_col is not None and len(orig_subtask_col) > 0: + orig_trans = self._transitions(orig_subtask_col[ep_start:ep_end], ep_start) + orig_row_starts = [t[0] for t in orig_trans] + + subtask_starts: list[tuple[int, int]] = [] + for i, (row_start, _si) in enumerate(transitions): + if i + 1 < len(transitions): + native_len = transitions[i + 1][0] - row_start + else: + native_len = ep_end - row_start + + if self._get_snapped_video_frame_count(native_len, fps_ratio) == 0: + total_skipped_short += 1 + continue + + if self._skip_no_action and orig_trans is not None and orig_row_starts is not None: + ti = bisect_right(orig_row_starts, row_start) - 1 + if ti >= 0: + _, orig_si = orig_trans[ti] + if self._is_no_action(orig_si, ds_idx): + total_skipped_na += 1 + continue + + subtask_starts.append((row_start, native_len)) + + if self._max_subtasks_per_episode is not None: + subtask_starts = subtask_starts[: self._max_subtasks_per_episode] + + self._subtask_start_indices[(ds_idx, episode_id)] = subtask_starts + + # Pre-populate transition caches so DataLoader workers inherit + # them read-only via COW instead of rebuilding per-worker. + self._subtask_transitions[(ds_idx, episode_id)] = transitions + if orig_trans is not None: + self._orig_subtask_transitions[(ds_idx, episode_id)] = orig_trans + + num_subtasks = len(subtask_starts) + total_subtasks += num_subtasks + if num_subtasks > 0: + new_records.append((ds_idx, sample_start, num_subtasks, episode_id)) + + self._episode_records = self._episode_records[:records_before] + new_records + self._episode_cum_ends = self._episode_cum_ends[:records_before] + self._num_valid_indices = self._episode_cum_ends[-1] if records_before > 0 else 0 + for rec in new_records: + self._num_valid_indices += rec[2] + self._episode_cum_ends.append(self._num_valid_indices) + + cap_info = "" + if self._max_subtasks_per_episode is not None: + cap_info = f", capped at first {self._max_subtasks_per_episode}/ep" + log.info( + f"HandPoseDataset: snap_to_subtask reindex — " + f"{total_subtasks} subtask starts across {len(new_records)} episodes{cap_info} " + f"(skipped {total_skipped_na} no-action, {total_skipped_short} too-short)" + ) + + # ------------------------------------------------------------------------- + # Episode filtering (override base class without modifying it) + # ------------------------------------------------------------------------- + + def _append_index_records(self, *, meta: Any, ds_idx: int, dataset_label: str | None = None) -> None: + """Override to filter episodes after deterministic split selection. + + Supports two mutually exclusive modes: + - ``max_episodes``: keep the first N episodes **globally** across all + shards (not per-shard). This method is called once per shard, so we + track how many episodes have already been kept via + ``len(self._episode_records)`` before and after the base-class call. + - ``episode_ids``: keep only episodes whose dataset-level episode + index appears in the given list. + """ + records_before = len(self._episode_records) + if self._intra_episode_val_ratio > 0.0 and self._split in {"train", "val"}: + episode_ids = split_episode_ids( + total_episodes=meta.total_episodes, + seed=self._split_seed, + val_ratio=self._split_val_ratio, + split="train", + ) + episode_spans, _, sample_count = build_episode_spans( + episodes=meta.episodes, + episode_ids=episode_ids, + chunk_length=self._chunk_length, + ) + + # Prevent train/val leakage caused by overlapping chunk windows. + # A gap of chunk_length sample-starts ensures no frame overlap between splits. + non_overlap_gap = self._chunk_length + valid_count = 0 + for episode_id, sample_start, valid_len in episode_spans: + if valid_len <= 0: + continue + num_val = int(round(valid_len * self._intra_episode_val_ratio)) + val_start_offset = max(0, valid_len - num_val) # take tail for validation + + if self._split == "train": + split_start = sample_start + split_len = max(0, val_start_offset - non_overlap_gap) + else: + split_start = sample_start + val_start_offset + split_len = max(0, valid_len - val_start_offset) + + if split_len <= 0: + continue + + self._episode_records.append((ds_idx, split_start, split_len, episode_id)) + self._num_valid_indices += split_len + self._episode_cum_ends.append(self._num_valid_indices) + valid_count += split_len + + class_name = self.__class__.__name__ + label = f" [{dataset_label}]" if dataset_label else "" + log.info( + f"{class_name}{label}: intra-episode split enabled " + f"(ratio={self._intra_episode_val_ratio:.3f}, split={self._split}, gap={non_overlap_gap})" + ) + if sample_count > 0: + log.info( + f"{class_name}{label}: kept {valid_count} / {sample_count} " + f"({100 * valid_count / sample_count:.2f} %) samples" + ) + else: + super()._append_index_records(meta=meta, ds_idx=ds_idx, dataset_label=dataset_label) + + new_records = self._episode_records[records_before:] + if not new_records: + return + + if self._episode_ids is not None: + keep_set = set(self._episode_ids) + kept: list[tuple[int, int, int, int]] = [] + removed_frames = 0 + for rec in new_records: + if rec[3] in keep_set: + kept.append(rec) + else: + removed_frames += rec[2] + self._episode_records = self._episode_records[:records_before] + kept + self._num_valid_indices -= removed_frames + self._episode_cum_ends = self._episode_cum_ends[:records_before] + running = self._episode_cum_ends[-1] if records_before > 0 else 0 + for rec in kept: + running += rec[2] + self._episode_cum_ends.append(running) + kept_ids = [rec[3] for rec in kept] + log.info( + f"HandPoseDataset: episode_ids filter — " + f"kept {len(kept)}/{len(new_records)} episodes " + f"({self._num_valid_indices} valid indices), " + f"retained episode IDs: {kept_ids}" + ) + elif self._max_episodes is not None: + global_total = len(self._episode_records) + remaining_budget = self._max_episodes - records_before + if remaining_budget <= 0: + self._episode_records = self._episode_records[:records_before] + self._episode_cum_ends = self._episode_cum_ends[:records_before] + removed_frames = sum(rec[2] for rec in new_records) + self._num_valid_indices -= removed_frames + log.info( + f"HandPoseDataset: max_episodes={self._max_episodes} — " + f"global budget exhausted, dropped all {len(new_records)} episodes " + f"from shard ds_idx={ds_idx}" + ) + elif len(new_records) > remaining_budget: + keep = records_before + remaining_budget + removed = self._episode_records[keep:] + self._episode_records = self._episode_records[:keep] + self._episode_cum_ends = self._episode_cum_ends[:keep] + removed_frames = sum(rec[2] for rec in removed) + self._num_valid_indices -= removed_frames + if keep > 0: + self._episode_cum_ends[-1] = self._num_valid_indices + + retained = self._episode_records[records_before:keep] + retained_ids = [rec[3] for rec in retained] + retained_frames = sum(rec[2] for rec in retained) + log.info( + f"HandPoseDataset: max_episodes={self._max_episodes} — " + f"shard ds_idx={ds_idx}: kept {remaining_budget}/{len(new_records)} episodes, " + f"removed {removed_frames} frames, " + f"retained {retained_frames} valid indices, " + f"episode IDs: {retained_ids}, " + f"global total: {len(self._episode_records)} episodes" + ) + else: + retained_ids = [rec[3] for rec in new_records] + log.info( + f"HandPoseDataset: max_episodes={self._max_episodes} — " + f"shard ds_idx={ds_idx}: kept all {len(new_records)} episodes " + f"(global total: {len(self._episode_records)}/{self._max_episodes}), " + f"episode IDs: {retained_ids}" + ) + else: + all_ids = [rec[3] for rec in new_records] + log.info( + f"HandPoseDataset: using all {len(new_records)} episodes ({self._num_valid_indices} valid indices)" + ) + + if self._snap_to_subtask: + self._rebuild_snap_indices(ds_idx=ds_idx, meta=meta, records_before=records_before) + + # ------------------------------------------------------------------------- + # Action building + # ------------------------------------------------------------------------- + + def _build_action(self, sample: dict[str, Any]) -> torch.Tensor: + """Build the action tensor from a LeRobot sample. + + Both modes share the same three-stage pipeline: + 1. Compute absolute SE(3) poses: camera (already world-frame), + wrists (converted from per-frame camera space to world space + via ``P_world = P_c2w @ P_cam``). + 2. Compute finger positions in per-frame wrist frame (valid in + camera space since both are in the same frame at each timestep). + 3. Convert world-frame absolute poses to relative (anchored or + frame-wise) for camera and wrists. + + Layout: ``[camera, R_wrist, R_fingers, L_wrist, L_fingers]`` + + Returns: + Action tensor of shape ``(T, raw_action_dim)``. + """ + pose_convention: Literal["backward_anchored", "backward_framewise"] = self._pose_convention # type: ignore[assignment] + + def _to_np(t: torch.Tensor) -> np.ndarray: + return t.detach().cpu().numpy() + + # -- Stage 1: absolute SE(3) poses ------------------------------------ + # Camera pose (world frame): c2w transforms + cam_pos = _to_np(sample[CAM_POSITION_KEY]) # (T+1, 3) + cam_rot_q = _to_np(sample[CAM_ROTATION_KEY]) # (T+1, 4) + cam_c2w = build_abs_pose_from_components(cam_pos, cam_rot_q, "quat_xyzw") # (T+1, 4, 4) + + # Wrist poses in camera frame (right first, then left) + right_pos_all = _to_np(sample[self._position_keys[1]]) # (T+1, 63) — hand_right_cam + right_rot_all = _to_np(sample[self._rotation_keys[1]]) # (T+1, 84) + left_pos_all = _to_np(sample[self._position_keys[0]]) # (T+1, 63) — hand_left_cam + left_rot_all = _to_np(sample[self._rotation_keys[0]]) # (T+1, 84) + + right_wrist_pos, right_wrist_quat = self._extract_wrist_pose_components(right_pos_all, right_rot_all) + left_wrist_pos, left_wrist_quat = self._extract_wrist_pose_components(left_pos_all, left_rot_all) + + # Wrist poses in camera frame, aligned to the unified cross-domain convention. + right_wrist_cam = build_abs_pose_from_components(right_wrist_pos, right_wrist_quat, "quat_xyzw") + left_wrist_cam = build_abs_pose_from_components(left_wrist_pos, left_wrist_quat, "quat_xyzw") + + if "embodiment_a" in self._root.lower(): + right_wrist_cam = right_wrist_cam @ WRIST_FRAME_ALIGN_EMBODIMENT_A + left_wrist_cam = left_wrist_cam @ WRIST_FRAME_ALIGN_EMBODIMENT_A + + # Wrist poses in camera frame → world frame: P_world = P_c2w @ P_cam + right_wrist_world = cam_c2w @ right_wrist_cam # (T+1, 4, 4) + left_wrist_world = cam_c2w @ left_wrist_cam # (T+1, 4, 4) + + # -- Stage 2: finger positions in per-frame wrist frame --------------- + # (Correct as-is: both finger positions and wrist pose are in the same + # camera frame at each timestep, so the transform to wrist-local is valid. + # The alignment rotation only changes the wrist-local axes; finger positions + # in camera space are unchanged, so their wrist-local coordinates rotate + # correspondingly to match the unified convention.) + right_fingers = self._build_fingers_in_wrist_frame(right_pos_all, right_wrist_cam) + left_fingers = self._build_fingers_in_wrist_frame(left_pos_all, left_wrist_cam) + + # -- Stage 3: convert world-frame absolute → relative poses ----------- + cam_rel = pose_abs_to_rel(cam_c2w, rotation_format=self._rotation_format, pose_convention=pose_convention) + right_wrist_rel = pose_abs_to_rel( + right_wrist_world, + rotation_format=self._rotation_format, + pose_convention=pose_convention, + ) + left_wrist_rel = pose_abs_to_rel( + left_wrist_world, + rotation_format=self._rotation_format, + pose_convention=pose_convention, + ) + + # -- Assemble: [camera, R_wrist, R_fingers, L_wrist, L_fingers] ------- + return torch.from_numpy( + np.concatenate([cam_rel, right_wrist_rel, right_fingers, left_wrist_rel, left_fingers], axis=-1) + ).float() + + # -- Shared action helpers ------------------------------------------------ + + def _extract_wrist_pose_components( + self, + pos_data: np.ndarray, + rot_quat: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + """Extract wrist translation and wrist quaternion from full-hand arrays.""" + wrist_pos = pos_data[:, :3] + wrist_quat = rot_quat.reshape(pos_data.shape[0], NUM_JOINTS, QUAT_DIM_PER_JOINT)[:, WRIST_JOINT_IDX] + return wrist_pos, wrist_quat + + def _build_fingers_in_wrist_frame( + self, + pos_data: np.ndarray, + wrist_poses_abs: np.ndarray, + ) -> np.ndarray: + """Express selected non-wrist keypoints in the per-frame wrist coordinate frame.""" + future_pos = pos_data[1:].astype(np.float32, copy=False) + T = future_pos.shape[0] + + selected_non_wrist = [j for j in self._position_joint_indices if j != WRIST_JOINT_IDX] + if not selected_non_wrist: + return np.empty((T, 0), dtype=np.float32) + + pos_3d = future_pos.reshape(T, NUM_JOINTS, 3) + finger_pos = pos_3d[:, selected_non_wrist, :] + finger_pos_h = np.concatenate( + [finger_pos, np.ones((*finger_pos.shape[:-1], 1), dtype=np.float32)], + axis=-1, + ) + wrist_inv = np.linalg.inv(wrist_poses_abs[1:]) + finger_pos_wrist = np.einsum("tij,tnj->tni", wrist_inv, finger_pos_h)[..., :3] + return finger_pos_wrist.reshape(T, -1) + + def _get_snapped_video_frame_count(self, subtask_native_len: int, fps_ratio: int) -> int: + """Return tokenizer-compatible video frame count for a snapped subtask. + + The tokenizer keeps one conditioning frame and consumes the remaining + temporal dimension in groups of four, so snapped clips must be ``1 + 4N`` + frames long. + """ + capped_video_frames = min(subtask_native_len // fps_ratio, self._chunk_length + 1) + if capped_video_frames < 5: + return 0 + return 1 + 4 * ((capped_video_frames - 1) // 4) + + # ------------------------------------------------------------------------- + # Caption helpers + # ------------------------------------------------------------------------- + + def _load_task_labels(self) -> None: + """Load task and subtask name mappings from meta parquet files. + + Subtask indices are **per-shard** (each shard's indices start at 0 and + the name lists differ across shards), so we must load and store subtask + names independently for every shard to avoid cross-shard caption + mismatch. + """ + if self._enable_fast_init: + self._load_task_labels_fast() + return + + tasks_path = os.path.join(self._all_shard_roots[0], "meta", "tasks.parquet") + if os.path.exists(tasks_path): + tasks_df = pd.read_parquet(tasks_path) + for _, row in tasks_df.iterrows(): + self._task_names[int(row["task_index"])] = str(row.name) + log.info(f"HandPoseDataset: loaded {len(self._task_names)} task labels") + + if self._load_subtasks: + for shard_idx, shard_root in enumerate(self._all_shard_roots): + shard_names: dict[int, str] = {} + plain_path = os.path.join(shard_root, "meta", "subtasks.parquet") + + if os.path.exists(plain_path): + df = pd.read_parquet(plain_path) + for _, row in df.iterrows(): + shard_names[int(row["subtask_index"])] = str(row.name) + log.info(f"HandPoseDataset: shard {shard_idx}: loaded {len(shard_names)} subtask labels") + + self._per_shard_subtask_names[shard_idx] = shard_names + + if self._skip_no_action: + for shard_idx, shard_root in enumerate(self._all_shard_roots): + orig_subtasks_path = os.path.join(shard_root, "meta", "subtasks.parquet") + if os.path.exists(orig_subtasks_path): + shard_names = {} + orig_df = pd.read_parquet(orig_subtasks_path) + for _, row in orig_df.iterrows(): + shard_names[int(row["subtask_index"])] = str(row.name) + self._per_shard_orig_subtask_names[shard_idx] = shard_names + log.info( + f"HandPoseDataset: shard {shard_idx}: loaded {len(shard_names)} " + f"original subtask labels (from {shard_root})" + ) + + def _load_task_labels_fast(self) -> None: + """Parallel version of ``_load_task_labels`` — bit-exact output.""" + tasks_path = os.path.join(self._all_shard_roots[0], "meta", "tasks.parquet") + if os.path.exists(tasks_path): + tasks_df = pd.read_parquet(tasks_path) + for _, row in tasks_df.iterrows(): + self._task_names[int(row["task_index"])] = str(row.name) + log.info(f"HandPoseDataset: loaded {len(self._task_names)} task labels") + + if not (self._load_subtasks or self._skip_no_action): + return + + def _read(shard_root: str) -> dict[int, str] | None: + path = os.path.join(shard_root, "meta", "subtasks.parquet") + if not os.path.exists(path): + return None + df = pd.read_parquet(path) + idx_arr = df["subtask_index"].to_numpy() + name_arr = df.index.to_numpy() + return dict(zip(idx_arr.astype(np.int64).tolist(), name_arr.astype(str).tolist())) + + roots = self._all_shard_roots + all_names = _parallel_map( + _read, + roots, + max_workers=max(1, min(self._fast_init_max_workers, len(roots))), + label="HandPoseDataset: _load_task_labels", + ) + # Match the serial version's set-membership: _per_shard_subtask_names + # is always set (``{}`` when missing); _per_shard_orig_subtask_names + # only when the file exists. + if self._load_subtasks: + for shard_idx, names in enumerate(all_names): + self._per_shard_subtask_names[shard_idx] = names if names is not None else {} + if self._skip_no_action: + for shard_idx, names in enumerate(all_names): + if names is not None: + self._per_shard_orig_subtask_names[shard_idx] = names + + def _register_sources(self, indices: list[int] | None = None) -> None: + """Register shard sources + HandPose-specific ``subtask_index`` prefetch. + + ``indices`` is the subset of ``_all_shard_roots`` assigned to this + caller (e.g. one slice per DataLoader worker under + ``shard_across_workers=True``). ``_ds_idx_to_shard_idx`` maps the + local ``ds_idx`` back to the global shard index used to key + ``_per_shard_subtask_names``, so caption lookups keep working. + + The base class owns the generic fast-init (parallel + ``LeRobotDatasetMetadata`` prefetch + serial ``_register_source`` + append loop). When ``enable_fast_init=True`` *and* snap/skip + flags are on, this override additionally prefetches the + ``subtask_index`` parquet column for each assigned shard in a + thread pool and caches it into ``_cached_subtask_col`` / + ``_orig_subtask_index``, so ``_rebuild_snap_indices`` and + ``_load_orig_subtask_index`` hit the cache instead of re-scanning + ``data/*.parquet``. + """ + if indices is None: + indices = list(range(len(self._all_shard_roots))) + self._ds_idx_to_shard_idx = list(indices) + if not indices: + return + + # Snapshot before ``super()._register_sources`` appends to + # ``_datasets``; each new ds_idx is ``base_ds_idx + offset``. + base_ds_idx = len(self._datasets) + roots = [self._all_shard_roots[i] for i in indices] + + super()._register_sources(indices) + + if not (self._enable_fast_init and (self._snap_to_subtask or self._skip_no_action)): + return + + import pyarrow.dataset as pa_ds + + def _read_subtask_col(root: str) -> np.ndarray: + data_dir = Path(root) / "data" + if not data_dir.exists(): + return np.array([], dtype=np.int64) + # Explicit ``*.parquet`` glob avoids picking up partial-write + # residues like ``file-000.parquet``. + parquet_files = sorted(str(f) for f in data_dir.rglob("*.parquet")) + if not parquet_files: + return np.array([], dtype=np.int64) + # ``use_threads=False`` avoids oversubscription; the outer + # pool already saturates lustre at the configured workers. + table = pa_ds.dataset(parquet_files, format="parquet").to_table( + columns=["subtask_index"], use_threads=False + ) + return table.column("subtask_index").to_numpy(zero_copy_only=False).astype(np.int64, copy=False) + + workers = max(1, min(self._fast_init_max_workers, len(roots))) + subtask_cols = _parallel_map( + _read_subtask_col, + roots, + max_workers=workers, + label="HandPoseDataset: subtask_index prefetch", + ) + for offset, col in enumerate(subtask_cols): + self._cached_subtask_col[base_ds_idx + offset] = col + self._orig_subtask_index[base_ds_idx + offset] = col + + def _resolve_shard_idx(self, ds_idx: int) -> int: + """Map a worker-local ``ds_idx`` to the global shard index.""" + if self._ds_idx_to_shard_idx is not None: + return self._ds_idx_to_shard_idx[ds_idx] + return ds_idx + + def _get_subtask_names_for_ds(self, ds_idx: int) -> dict[int, str]: + """Return the subtask name mapping for the shard that owns *ds_idx*.""" + return self._per_shard_subtask_names.get(self._resolve_shard_idx(ds_idx), {}) + + def _get_orig_subtask_names_for_ds(self, ds_idx: int) -> dict[int, str]: + """Return the original subtask name mapping for the shard that owns *ds_idx*.""" + return self._per_shard_orig_subtask_names.get(self._resolve_shard_idx(ds_idx), {}) + + def _load_episode_intrinsics(self, root: str) -> None: + """Load per-episode camera intrinsics from dataset metadata.""" + from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata + + meta = LeRobotDatasetMetadata(repo_id="local", root=root) + eps = meta.episodes + if "camera_intrinsics" not in eps.column_names: + log.warning( + "HandPoseDataset: 'camera_intrinsics' not found in metadata; skeleton overlay will be unavailable." + ) + return + for ep in eps: + self._episode_intrinsics[int(ep["episode_index"])] = np.asarray(ep["camera_intrinsics"], dtype=np.float32) + log.info(f"HandPoseDataset: loaded intrinsics for {len(self._episode_intrinsics)} episodes") + + def _get_chunk_caption( + self, + ds_idx: int, + row_idx: int, + ep_idx: int, + sample: dict[str, Any], + effective_chunk_length: int | None = None, + ) -> str: + """Build a caption covering all subtasks that overlap the chunk window. + + When ``_load_subtasks`` is enabled, finds every subtask whose frame + range intersects ``[row_idx, row_idx + fps_ratio * chunk_length]`` and + concatenates their descriptions. Falls back to anchor-only task name + otherwise. + + Args: + effective_chunk_length: If provided, overrides ``self._chunk_length`` + for computing the chunk window (used by ``snap_to_subtask`` to + restrict the caption to the snapped subtask). + """ + task_name: str | None = None + task_idx = sample.get("task_index") + if task_idx is not None: + ti = int(task_idx) if not isinstance(task_idx, int) else task_idx + task_name = self._task_names.get(ti) + + if not self._load_subtasks: + if task_name: + return task_name.replace("_", " ") + return "Human hand manipulation task" + + transitions = self._get_subtask_transitions(ds_idx, ep_idx) + if not transitions: + if task_name: + return task_name.replace("_", " ") + return "Human hand manipulation task" + + ds = self._get_dataset(ds_idx) + fps_ratio = round(ds.meta.fps / self._fps) + cl = effective_chunk_length if effective_chunk_length is not None else self._chunk_length + chunk_end_row = row_idx + fps_ratio * cl + + row_starts = [t[0] for t in transitions] + first_ti = max(0, bisect_right(row_starts, row_idx) - 1) + + subtask_parts: list[str] = [] + seen: set[int] = set() + for ti in range(first_ti, len(transitions)): + seg_start, si = transitions[ti] + if seg_start >= chunk_end_row: + break + if si in seen: + continue + seen.add(si) + name = self._get_subtask_names_for_ds(ds_idx).get(si) + if name: + subtask_parts.append(name.replace("_", " ")) + + if subtask_parts: + return " Then, ".join(subtask_parts) + if task_name: + return task_name.replace("_", " ") + return "Human hand manipulation task" + + # ------------------------------------------------------------------------- + # Data quality filters + # ------------------------------------------------------------------------- + + def _has_unannotated_frames(self, sample: dict[str, Any]) -> bool: + """Return True if any frame in the sampled chunk appears unannotated. + + A frame is considered unannotated when both hands' position vectors are + effectively all-zero under ``self._unannotated_pos_l1_threshold``. + """ + left = sample[self._position_keys[0]] # (T+1, 63) + right = sample[self._position_keys[1]] # (T+1, 63) + left_l1 = left.abs().sum(dim=-1) + right_l1 = right.abs().sum(dim=-1) + missing_mask = (left_l1 <= self._unannotated_pos_l1_threshold) & ( + right_l1 <= self._unannotated_pos_l1_threshold + ) + return bool(missing_mask.any()) + + def _has_nan_action(self, action: torch.Tensor) -> bool: + """Return True if the action tensor contains any NaN or Inf values.""" + return bool(torch.isnan(action).any() or torch.isinf(action).any()) + + # ------------------------------------------------------------------------- + # Dataset interface + # ------------------------------------------------------------------------- + + def _snap_to_subtask_bounds(self, ds_idx: int, row_idx: int, ep_idx: int) -> tuple[int, int | None]: + """Snap ``row_idx`` to the start of its subtask and return its native-fps duration. + + Returns: + (snapped_row_idx, subtask_native_len) where subtask_native_len is + the number of native-fps rows in the subtask, or ``None`` if + transitions are unavailable. + """ + transitions = self._get_subtask_transitions(ds_idx, ep_idx) + if not transitions: + return row_idx, None + row_starts = [t[0] for t in transitions] + ti = bisect_right(row_starts, row_idx) - 1 + if ti < 0: + return row_idx, None + snapped = row_starts[ti] + if ti + 1 < len(transitions): + subtask_len = row_starts[ti + 1] - snapped + else: + ds = self._get_dataset(ds_idx) + ep_end: int = ds.meta.episodes[ep_idx]["dataset_to_index"] + subtask_len = ep_end - snapped + return snapped, subtask_len + + def _choose_mode(self) -> str: + """Resolve mode with biased sampling toward forward_dynamics in joint mode. + Overrides the uniform sampling in BaseActionLeRobotDataset. + """ + if self._mode == "joint": + return random.choices( + ("forward_dynamics", "inverse_dynamics", "policy"), + weights=(0.8, 0.1, 0.1), + k=1, + )[0] + return self._mode + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode = self._choose_mode() + sample: dict[str, Any] | None = None + action: torch.Tensor | None = None + episode_idx: int = -1 + final_ds_idx: int = -1 + final_row_idx: int = -1 + effective_len: int | None = None + last_error: Exception | None = None + current_idx = idx + ds_idx = -1 + row_idx = -1 + frame_offset = -1 + + for _attempt in range(self._max_item_retries + 1): + try: + effective_len = None + ds_idx, row_idx, ep_idx, frame_offset = self._resolve_index(current_idx) + if self._snap_to_subtask: + subtask_info = self._subtask_start_indices.get((ds_idx, ep_idx)) + if subtask_info is not None and len(subtask_info) > 0: + si = frame_offset + if si < len(subtask_info): + row_idx, subtask_native_len = subtask_info[si] + ds = self._get_dataset(ds_idx) + fps_ratio = round(ds.meta.fps / self._fps) + snapped_video_frames = self._get_snapped_video_frame_count(subtask_native_len, fps_ratio) + if snapped_video_frames == 0: + last_error = RuntimeError("Snapped subtask is too short for tokenizer; resampling.") + current_idx = random.randrange(len(self)) + continue + effective_len = snapped_video_frames - 1 + else: + last_error = RuntimeError("Invalid subtask index; resampling.") + current_idx = random.randrange(len(self)) + continue + else: + last_error = RuntimeError("Invalid subtask index; resampling.") + current_idx = random.randrange(len(self)) + continue + elif self._skip_no_action: + skipped_row = self._skip_no_action_subtask(ds_idx, row_idx, ep_idx) + if skipped_row is None: + last_error = RuntimeError("All remaining original subtasks are 'No action'; resampling.") + current_idx = random.randrange(len(self)) + continue + row_idx = skipped_row + candidate = self._get_dataset(ds_idx)[row_idx] + except RuntimeError as error: + if self._is_video_decode_error(error): + last_error = error + self._log_bad_video_decode( + ds_idx=ds_idx, + row_idx=row_idx, + ep_idx=ep_idx, + frame_offset=frame_offset, + idx=idx, + attempt=_attempt, + error=error, + ) + current_idx = random.randrange(len(self)) + continue + raise + except (AssertionError, IndexError) as error: + if isinstance(error, AssertionError) and "violate the tolerance" not in str(error): + raise + last_error = error + if not self._warned_tolerance_once: + log.warning( + f"HandPoseDataset: encountered timestamp-tolerance mismatch; resampling index. Failed episode: {ep_idx}, row_idx: {row_idx}" + ) + self._warned_tolerance_once = True + current_idx = random.randrange(len(self)) + continue + + if self._drop_unannotated_edge_frames and self._has_unannotated_frames(candidate): + last_error = RuntimeError("Chunk contains all-zero hand annotation frame(s).") + if not self._warned_unannotated_once: + log.warning("HandPoseDataset: detected zero-filled annotation frames; resampling index.") + self._warned_unannotated_once = True + current_idx = random.randrange(len(self)) + continue + + try: + candidate_action = self._build_action(candidate) + except (ValueError, RuntimeError) as error: + last_error = error + if not self._warned_build_action_error_once: + log.warning(f"HandPoseDataset: _build_action failed at index {current_idx} ({error}); resampling.") + self._warned_build_action_error_once = True + current_idx = random.randrange(len(self)) + continue + if self._has_nan_action(candidate_action): + last_error = RuntimeError("Action contains NaN/Inf values.") + if not self._warned_nan_action_once: + log.warning(f"HandPoseDataset: NaN/Inf in action at index {current_idx}; resampling.") + self._warned_nan_action_once = True + current_idx = random.randrange(len(self)) + continue + + sample = candidate + action = candidate_action + episode_idx = ep_idx + final_ds_idx = ds_idx + final_row_idx = row_idx + break + + if sample is None or action is None: + raise RuntimeError( + "HandPoseDataset failed to sample a valid chunk after retries " + f"(max_item_retries={self._max_item_retries})." + ) from last_error + + ai_caption = self._get_chunk_caption( + final_ds_idx, + final_row_idx, + episode_idx, + sample, + effective_chunk_length=effective_len, + ) + if self._skip_video_loading: + raw_video = None + video = None + else: + # [T+1,C,H,W] float from LeRobot; _build_result converts to uint8 [C,T+1,H,W]. + raw_video = sample[self._video_key] + video = raw_video + + if effective_len is not None and effective_len < self._chunk_length: + if video is not None: + video = video[: effective_len + 1] # [T+1,C,H,W] + action = action[:effective_len] + + extras: dict[str, Any] = { + "__episode_id__": episode_idx, + "__row_idx__": final_row_idx, + "__dataset_root__": str(self._get_dataset(final_ds_idx).root), + "__index__": idx, + } + if self._return_overlay_data and not self._skip_video_loading: + extras["raw_cam_left_3d"] = sample[HAND_LEFT_POSITION_KEY] # (T+1, 63) + extras["raw_cam_right_3d"] = sample[HAND_RIGHT_POSITION_KEY] # (T+1, 63) + extras["raw_cam_left_rot"] = sample[HAND_LEFT_ROTATION_KEY] # (T+1, 84) + extras["raw_cam_right_rot"] = sample[HAND_RIGHT_ROTATION_KEY] # (T+1, 84) + extras["raw_cam_position"] = sample[CAM_POSITION_KEY] # (T+1, 3) + extras["raw_cam_rotation"] = sample[CAM_ROTATION_KEY] # (T+1, 4) + extras["orig_video_hw"] = torch.tensor([raw_video.shape[-2], raw_video.shape[-1]], dtype=torch.long) + intrinsics = self._episode_intrinsics.get(episode_idx) + if intrinsics is not None: + extras["camera_intrinsics"] = torch.from_numpy(intrinsics) + + return self._build_result(mode=mode, video=video, action=action, ai_caption=ai_caption, **extras) + + @staticmethod + def _is_video_decode_error(error: RuntimeError) -> bool: + msg = str(error) + common_err = "Requested next frame while there are no more frames left to decode" + return common_err in msg + + def _log_bad_video_decode( + self, + *, + ds_idx: int, + row_idx: int, + ep_idx: int, + frame_offset: int, + idx: int, + attempt: int, + error: RuntimeError, + ) -> None: + """Log decode failures with enough identifiers to locate bad videos later.""" + key = (ds_idx, ep_idx, row_idx) + if key in self._logged_decode_failures: + return + self._logged_decode_failures.add(key) + + ds = self._get_dataset(ds_idx) + dataset_root = str(getattr(ds, "root", "unknown")) + repo_id = getattr(getattr(ds, "meta", None), "repo_id", "unknown") + log.critical( + "HandPoseDataset video decode failure detected. " + f"idx={idx}, attempt={attempt}, split={self._split}, mode={self._mode}, " + f"dataset_idx={ds_idx}, dataset_root={dataset_root}, repo_id={repo_id}, " + f"episode_id={ep_idx}, row_idx={row_idx}, frame_offset={frame_offset}, " + f"video_key={self._video_key}, chunk_length={self._chunk_length}, fps={self._fps}, " + f"error={error!r}" + ) + + @property + def action_dim(self) -> int: + """Raw action dimensionality before padding.""" + return self._raw_action_dim diff --git a/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset_config.py new file mode 100644 index 00000000..97844b28 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/hand_pose_dataset_config.py @@ -0,0 +1,100 @@ +"""Hand-pose dataset constants: data roots, dataset keys, and skeleton topology.""" + +import numpy as np + +# --------------------------------------------------------------------------- +# Embodiment_a LeRobot parquet keys +# --------------------------------------------------------------------------- +HAND_RIGHT_POSITION_KEY = "observation.state.hand_right_cam" +HAND_RIGHT_ROTATION_KEY = "observation.state.hand_right_cam_rotation" +HAND_LEFT_POSITION_KEY = "observation.state.hand_left_cam" +HAND_LEFT_ROTATION_KEY = "observation.state.hand_left_cam_rotation" +CAM_POSITION_KEY = "observation.state.camera_position" +CAM_ROTATION_KEY = "observation.state.camera_rotation" + +# --------------------------------------------------------------------------- +# Skeleton topology +# --------------------------------------------------------------------------- +NUM_JOINTS = 21 +QUAT_DIM_PER_JOINT = 4 # quaternion (qx, qy, qz, qw) as stored in parquet +MAT_DIM_PER_JOINT = 9 # flattened 3x3 rotation matrix (internal working format) +WRIST_JOINT_IDX = 0 +FINGERTIP_JOINT_IDXS = (4, 8, 12, 16, 20) + +ROTATION_FORMAT_DIM: dict[str, int] = {"rot9d": 9, "rot6d": 6, "euler_xyz": 3} + +# --------------------------------------------------------------------------- +# Subtask-name filters for ``skip_no_action`` (exact / prefix / substring). +# Mirrors FilterConfig in +# pipelines/customers/dht/action_gen/run_extract_subtask_clips.py. +# Matching is done against the normalized name (underscores → spaces, +# stripped, lowercased). +# --------------------------------------------------------------------------- +NO_ACTION_SKIP_LABELS: tuple[str, ...] = ("no action", "no actions") +NO_ACTION_SKIP_LABEL_PREFIXES: tuple[str, ...] = ("hold", "adjust") +NO_ACTION_SKIP_LABEL_SUBSTRINGS: tuple[str, ...] = ("idle",) + +# --------------------------------------------------------------------------- +# Wrist-frame alignment (Embodiment_a) +# --------------------------------------------------------------------------- +# 90° CCW rotation about local Z so that the wrist-local frame becomes: +# X = thumb→pinky, Y = palm normal (outward), Z = wrist→fingertips +WRIST_FRAME_ALIGN_EMBODIMENT_A = np.array( + [[0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], + dtype=np.float32, +) + +WRIST_FRAME_ALIGN_EMBODIMENT_A_IDENTITY = np.eye(4, dtype=np.float32) +# WRIST_FRAME_ALIGN_EMBODIMENT_A = np.array( +# [[0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], +# dtype=np.float32, +# ) +# WRIST_FRAME_ALIGN_EMBODIMENT_A = WRIST_FRAME_ALIGN_EMBODIMENT_A_IDENTITY + +# --------------------------------------------------------------------------- +# Embodiment_a sharded dataset roots +# --------------------------------------------------------------------------- +_EMBODIMENT_A_BASE = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a" +EMBODIMENT_A_FEB08_500HR = [f"{_EMBODIMENT_A_BASE}/feb_08_500hr_lerobot_no_bframes/shard_{i:02d}" for i in range(10)] +EMBODIMENT_A_FEB15_1500HR = [f"{_EMBODIMENT_A_BASE}/feb_15_1500hr_lerobot_no_bframes/shard_{i:02d}" for i in range(31)] +EMBODIMENT_A_FEB23_1000HR = [f"{_EMBODIMENT_A_BASE}/feb_23_1000hr_lerobot_no_bframes/shard_{i:02d}" for i in range(21)] +EMBODIMENT_A_MAR02_1000HR = [f"{_EMBODIMENT_A_BASE}/mar_02_1000hr_lerobot/shard_{i:02d}" for i in range(21)] +EMBODIMENT_A_MAR09_4000HR = [f"{_EMBODIMENT_A_BASE}/mar_09_4000hr_lerobot/shard_{i:02d}" for i in range(81)] +EMBODIMENT_A_MAR16_7000HR = [f"{_EMBODIMENT_A_BASE}/mar_16_7000hr_lerobot/shard_{i:02d}" for i in range(140)] +EMBODIMENT_A_MAR30_9000HR = [f"{_EMBODIMENT_A_BASE}/mar_30_9000hr_lerobot/shard_{i:02d}" for i in range(180)] +EMBODIMENT_A_APR03 = [f"{_EMBODIMENT_A_BASE}/apr_03_lerobot/shard_{i:02d}" for i in range(181)] +EMBODIMENT_A_APR06_10000HR = [f"{_EMBODIMENT_A_BASE}/apr_06_10000hr_lerobot/shard_{i:02d}" for i in range(202)] +EMBODIMENT_A_ALL = ( + EMBODIMENT_A_FEB08_500HR + + EMBODIMENT_A_FEB15_1500HR + + EMBODIMENT_A_FEB23_1000HR + + EMBODIMENT_A_MAR02_1000HR + + EMBODIMENT_A_MAR09_4000HR + + EMBODIMENT_A_MAR16_7000HR + + EMBODIMENT_A_MAR30_9000HR + + EMBODIMENT_A_APR03 + + EMBODIMENT_A_APR06_10000HR +) + +# --------------------------------------------------------------------------- +# Registry of all hand-pose datasets for hand-only experiments +# --------------------------------------------------------------------------- +HAND_POSE_DATASETS: dict[str, str | list[str]] = { + "embodiment_a_feb08_500hr": EMBODIMENT_A_FEB08_500HR, + "embodiment_a_feb15_1500hr": EMBODIMENT_A_FEB15_1500HR, + "embodiment_a_feb23_1000hr": EMBODIMENT_A_FEB23_1000HR, + "embodiment_a_mar02_1000hr": EMBODIMENT_A_MAR02_1000HR, + "embodiment_a_mar09_4000hr": EMBODIMENT_A_MAR09_4000HR, + "embodiment_a_mar16_7000hr": EMBODIMENT_A_MAR16_7000HR, + "embodiment_a_mar30_9000hr": EMBODIMENT_A_MAR30_9000HR, + "embodiment_a_apr03": EMBODIMENT_A_APR03, + "embodiment_a_apr06_10000hr": EMBODIMENT_A_APR06_10000HR, + "embodiment_a_all": EMBODIMENT_A_ALL, + "embodiment_a_500hr_legacy_single": "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/qianlim/datasets/embodiment_a/feb_08_500hr_lerobot", + "vitra_ego4d": [ + f"/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/cosmos3_action_datasets/VITRA/ego4d/{res}" + for res in ("810x1080", "1440x1080", "1920x1080", "1920x1440", "2560x1440", "2560x1920") + ], + "hwb_egoverse_eval_set_v0p1": "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/cosmos3_action_datasets/egocentric_eval_hwb/HWB_egoverse_v0p1_lerobot_v30/", + "hwb_egoverse_eval_set_v0p2": "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/cosmos3_action_datasets/egocentric_eval_hwb/HWB_v0p2_lerobot_v3/", +} diff --git a/cosmos_training/cosmos/data/vfm/action/json_formatter.py b/cosmos_training/cosmos/data/vfm/action/json_formatter.py new file mode 100644 index 00000000..1e2224f8 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/json_formatter.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import math + +import torch + +from cosmos.utils import log +from cosmos.data.vfm.action.viewpoint_utils import DEFAULT_VIEWPOINT_TEMPLATES +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO + + +def _should_append_idle_frame_info(mode: object) -> bool: + """Return whether idle-frame prompt metadata should be surfaced.""" + return mode != "inverse_dynamics" + + +class ActionPromptJsonFormatter: + """Format action prompts into a structured JSON-compatible dictionary. + + JSON fields are emitted in this order: ``cinematography``, ``actions``, + ``duration``, ``fps``, ``resolution``, then ``aspect_ratio``. Like video JSON + prompts, ``cinematography`` is a dictionary, duration is truncated to an + integer-second string such as ``"2s"``, and aspect ratio is stored as a + comma-separated string such as ``"16,9"``. If ``data_dict["mode"]`` is + ``"inverse_dynamics"``, idle-frame metadata is omitted from the prompt. + """ + + def __init__( + self, + caption_key: str = "ai_caption", + viewpoint_key: str = "viewpoint", + video_key: str = "video", + fps_key: str = "conditioning_fps", + image_size_key: str = "image_size", + idle_frames_key: str = "idle_frames", + total_frames_key: str = "idle_frames_total", + action_key: str = "action", + viewpoint_templates: dict[str, str] | None = None, + ) -> None: + self.caption_key: str = caption_key + self.viewpoint_key: str = viewpoint_key + self.video_key: str = video_key + self.fps_key: str = fps_key + self.image_size_key: str = image_size_key + self.idle_frames_key: str = idle_frames_key + self.total_frames_key: str = total_frames_key + self.action_key: str = action_key + self.viewpoint_templates: dict[str, str] = ( + viewpoint_templates if viewpoint_templates is not None else DEFAULT_VIEWPOINT_TEMPLATES + ) + + def __call__(self, data_dict: dict) -> dict: + """Replace the caption with the action JSON prompt structure.""" + additional_view_description = data_dict.pop("additional_view_description", None) + caption = data_dict.get(self.caption_key) + if not isinstance(caption, str) or caption == "": + return data_dict + + height, width = self._get_resolution(data_dict) + fps = self._get_scalar_float(data_dict.get(self.fps_key), self.fps_key) + if fps <= 0: + raise ValueError(f"ActionPromptJsonFormatter: '{self.fps_key}' must be positive, got {fps}") + + video = data_dict.get(self.video_key) + if not isinstance(video, torch.Tensor) or video.ndim < 2: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.video_key}' to be a video tensor with shape " + f"(C, T, H, W), got {type(video).__name__}" + ) + duration_seconds = video.shape[1] / fps + duration = self._truncate_seconds(duration_seconds) + action_end_time = self._round_time_seconds(duration_seconds) + + prompt = { + "cinematography": { + "framing": self._get_viewpoint_caption(data_dict, additional_view_description), + }, + "actions": [ + { + "time": f"0:00-{self._format_time_mss(action_end_time)}", + "description": self._ensure_sentence(caption), + "idle_frame": self._get_idle_frame_info(data_dict), + } + ], + "duration": f"{duration}s", + "fps": float(fps), + "resolution": {"H": height, "W": width}, + "aspect_ratio": self._get_aspect_ratio(width, height), + } + cleaned_prompt = self._drop_empty_fields(prompt) + self._raise_if_empty_fields(cleaned_prompt) + data_dict[self.caption_key] = cleaned_prompt + return data_dict + + def _truncate_seconds(self, seconds: float) -> int: + """Truncate duration to integer seconds, matching video JSON-caption augmentors.""" + if seconds < 0 or not math.isfinite(seconds): + return 0 + return int(seconds) + + def _round_time_seconds(self, seconds: float) -> int: + """Round an action timestamp to integer seconds, matching video captioning.""" + if seconds < 0 or not math.isfinite(seconds): + return 0 + return round(seconds) + + def _format_time_mss(self, seconds: int) -> str: + """Format integer seconds as M:SS for JSON prompt time ranges.""" + minutes, remaining_seconds = divmod(seconds, 60) + return f"{minutes}:{remaining_seconds:02d}" + + def _get_aspect_ratio(self, width: int, height: int) -> str: + """Return the canonical width,height aspect ratio string when known.""" + for aspect_ratio_sizes in VIDEO_RES_SIZE_INFO.values(): + for aspect_ratio, (candidate_w, candidate_h) in aspect_ratio_sizes.items(): + if width == candidate_w and height == candidate_h: + return aspect_ratio + + divisor = math.gcd(width, height) + if divisor == 0: + raise ValueError( + f"ActionPromptJsonFormatter: width and height must be non-zero, got width={width}, height={height}." + ) + return f"{width // divisor},{height // divisor}" + + def _get_viewpoint_caption(self, data_dict: dict, additional_view_description: object | None) -> str | None: + """Resolve the viewpoint text used in the ``cinematography`` field.""" + viewpoint = data_dict.get(self.viewpoint_key) + template = self.viewpoint_templates.get(viewpoint) if isinstance(viewpoint, str) else None + + if template is None: + if viewpoint is not None: + log.warning( + f"ActionPromptJsonFormatter: unrecognized viewpoint {viewpoint!r}. " + f"Known viewpoints: {sorted(self.viewpoint_templates.keys())}. " + f"Using additional view description when available.", + rank0_only=False, + ) + return self._get_optional_text(additional_view_description) + + if additional_view_description: + separator = " " if template.endswith(".") else ". " + template = template + separator + str(additional_view_description).rstrip() + return template + + def _get_resolution(self, data_dict: dict) -> tuple[int, int]: + """Resolve ``(height, width)`` from the post-padding image size.""" + image_size = data_dict.get(self.image_size_key) + if image_size is None: + raise ValueError(f"ActionPromptJsonFormatter: missing '{self.image_size_key}' in data_dict.") + + if isinstance(image_size, torch.Tensor): + if image_size.numel() < 2: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.image_size_key}' to contain at least " + f"height and width, got shape {tuple(image_size.shape)}" + ) + return int(image_size[0].item()), int(image_size[1].item()) + + try: + return int(image_size[0]), int(image_size[1]) + except (TypeError, ValueError, IndexError) as e: + raise ValueError( + f"ActionPromptJsonFormatter: expected '{self.image_size_key}' to contain height and width." + ) from e + + def _get_scalar_float(self, value: object, key: str) -> float: + """Parse a required scalar float from a tensor or Python value.""" + if value is None: + raise ValueError(f"ActionPromptJsonFormatter: missing '{key}' in data_dict.") + + if isinstance(value, torch.Tensor): + if value.numel() != 1: + raise ValueError( + f"ActionPromptJsonFormatter: expected scalar tensor at '{key}', got shape {tuple(value.shape)}" + ) + return float(value.item()) + + if isinstance(value, (str, int, float)): + try: + return float(value) + except ValueError as e: + raise ValueError( + f"ActionPromptJsonFormatter: expected scalar float-compatible value at '{key}'." + ) from e + raise ValueError(f"ActionPromptJsonFormatter: expected scalar float-compatible value at '{key}'.") + + def _get_optional_scalar_int(self, value: object, key: str) -> int | None: + """Parse an optional scalar integer metadata value.""" + if value is None: + return None + + if isinstance(value, torch.Tensor): + if value.numel() != 1: + log.warning( + f"ActionPromptJsonFormatter: expected scalar tensor at '{key}', got shape " + f"{tuple(value.shape)}. Skipping.", + rank0_only=False, + ) + return None + return int(value.item()) + + if isinstance(value, (str, int, float)): + try: + return int(value) + except ValueError: + pass + log.warning( + f"ActionPromptJsonFormatter: expected integer-compatible value at " + f"'{key}', got {type(value).__name__}. Skipping.", + rank0_only=False, + ) + return None + + def _get_total_frames(self, data_dict: dict) -> int | None: + """Resolve the total action-frame count for idle-frame text.""" + total_frames = self._get_optional_scalar_int(data_dict.get(self.total_frames_key), self.total_frames_key) + if total_frames is not None: + return total_frames + + action = data_dict.get(self.action_key) + if isinstance(action, torch.Tensor): + if action.ndim == 0: + log.warning( + f"ActionPromptJsonFormatter: expected action tensor at " + f"'{self.action_key}' to have a frame dimension. Skipping total frames.", + rank0_only=False, + ) + return None + return int(action.shape[0]) + + try: + return len(action) if action is not None else None + except TypeError: + return None + + def _get_idle_frame_info(self, data_dict: dict) -> str | None: + """Build the idle-frame string for the action object.""" + if not _should_append_idle_frame_info(data_dict.get("mode")): + return None + + idle_frames = self._get_optional_scalar_int(data_dict.get(self.idle_frames_key), self.idle_frames_key) + total_frames = self._get_total_frames(data_dict) + + if idle_frames is not None and total_frames is not None: + return f"{idle_frames} out of {total_frames}." + if idle_frames is not None: + return f"{idle_frames}." + return None + + def _ensure_sentence(self, text: str) -> str: + """Return text with terminal sentence punctuation.""" + text = text.strip() + if text.endswith((".", "!", "?")): + return text + return f"{text}." + + def _get_optional_text(self, value: object) -> str | None: + """Return stripped text, leaving empty optional text for the final prune pass.""" + if value is None: + return None + text = str(value).rstrip() + return text if text else None + + def _drop_empty_fields(self, value: object) -> object: + """Recursively remove empty strings, dictionaries, lists, and ``None`` values.""" + if isinstance(value, dict): + return { + key: cleaned + for key, item in value.items() + if not self._is_empty(cleaned := self._drop_empty_fields(item)) + } + if isinstance(value, list): + return [cleaned for item in value if not self._is_empty(cleaned := self._drop_empty_fields(item))] + return value + + def _is_empty(self, value: object) -> bool: + """Return whether a JSON field should be dropped.""" + return value is None or value == "" or value == [] or value == {} + + def _raise_if_empty_fields(self, value: object, path: str = "prompt") -> None: + """Validate that no empty JSON fields remain after pruning.""" + if self._is_empty(value): + raise ValueError(f"ActionPromptJsonFormatter: empty field remains at {path}.") + + if isinstance(value, dict): + for key, item in value.items(): + self._raise_if_empty_fields(item, f"{path}.{key}") + elif isinstance(value, list): + for index, item in enumerate(value): + self._raise_if_empty_fields(item, f"{path}[{index}]") diff --git a/cosmos_training/cosmos/data/vfm/action/libero_action_stats_10k.json b/cosmos_training/cosmos/data/vfm/action/libero_action_stats_10k.json new file mode 100644 index 00000000..2da3b007 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/libero_action_stats_10k.json @@ -0,0 +1,101 @@ +{ + "metadata": { + "action_dim": 7, + "chunk_length": 1, + "num_samples": 10000, + "total_action_frames": 10000, + "processing_time_s": 11.293463945388794, + "config_path": "configs/base/config.py", + "split": "train", + "experiment": "libero_exp", + "config_overrides": [ + "--", + "experiment=libero_exp" + ] + }, + "global": { + "mean": [ + 0.010700089146313277, + 0.059342142708553126, + -0.08804116051685972, + 0.003358964275650197, + 0.0031843214033986456, + -0.0034359285722253877, + 0.45609999999999973 + ], + "std": [ + 0.29001801614779354, + 0.3642332438293914, + 0.4413559193152232, + 0.0468960901544023, + 0.0627405349745355, + 0.08982732429699941, + 0.49809396679740553 + ], + "min": [ + -0.9375, + -0.9375, + -0.9375, + -0.2978571355342865, + -0.302142858505249, + -0.375, + 0.0 + ], + "max": [ + 0.9375, + 0.9375, + 0.9375, + 0.3257142901420593, + 0.3546428680419922, + 0.3750000298023224, + 1.0 + ], + "count": 10000 + }, + "per_timestep": { + "mean": [ + [ + 0.010700089146313277, + 0.059342142708553126, + -0.08804116051685972, + 0.003358964275650197, + 0.0031843214033986456, + -0.0034359285722253877, + 0.45609999999999973 + ] + ], + "std": [ + [ + 0.29001801614779354, + 0.3642332438293914, + 0.4413559193152232, + 0.0468960901544023, + 0.0627405349745355, + 0.08982732429699941, + 0.49809396679740553 + ] + ], + "min": [ + [ + -0.9375, + -0.9375, + -0.9375, + -0.2978571355342865, + -0.302142858505249, + -0.375, + 0.0 + ] + ], + "max": [ + [ + 0.9375, + 0.9375, + 0.9375, + 0.3257142901420593, + 0.3546428680419922, + 0.3750000298023224, + 1.0 + ] + ] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/libero_dataset.py b/cosmos_training/cosmos/data/vfm/action/libero_dataset.py new file mode 100644 index 00000000..0f5a648c --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/libero_dataset.py @@ -0,0 +1,623 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LIBERO dataset for training from local storage, supporting multiple dataset roots.""" + +import random +from pathlib import Path +from typing import Literal + +import torch +import torchvision.transforms.functional as F +from lerobot.datasets.lerobot_dataset import LeRobotDataset +from torch.utils.data import Dataset + +from cosmos.utils import log +from cosmos.data.vfm.action.action_normalization import ( + load_action_stats, + normalize_action, +) +from cosmos.data.vfm.action.action_spec import ( + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.libero_pose_utils import ( + libero_action_dim, + libero_rotation_format, +) +from cosmos.data.vfm.action.pose_utils import ( + compute_idle_frames, + convert_rotation, +) + +LIBERO_ROOTS: list[str] = [ + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_10_no_noops_1.0.0_lerobot_aligned", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_90_no_noops_lerobot_shuffled", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_object_no_noops_1.0.0_lerobot_aligned", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_spatial_no_noops_1.0.0_lerobot", + "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/maxzhaoshuol/dataset/libero_goal_no_noops_1.0.0_lerobot", +] + + +class LIBERODataset(Dataset): + """ + A Dataset wrapper for LeRobot LIBERO dataset(s) designed for training from local storage. + + This dataset: + - Loads data from local storage using LeRobotDataset + - Supports multiple dataset roots that are concatenated into one dataset + - Supports configurable camera modes (image, wrist_image, or concat_view) + - Filters episodes for train/val split + - Filters frames at episode boundaries (to avoid padding issues with delta timestamps) + - Uses task descriptions from meta/tasks.parquet for ai_caption + """ + + _NORMALIZERS_DIR = Path(__file__).parent / "normalizers" + + def __init__( + self, + repo_id: str | list[str] = "lerobot/libero_90", + root: str | list[str] | None = LIBERO_ROOTS, + image_size: int = 256, + chunk_length: int = 16, # must be divisible by 4 + fps: int = 10, # IMPORTANT! LIBERO is at 20fps. If using frame_wise_relative in policy mode, we have to match the fps. + mode: str = "policy", + video_backend: str | None = "torchcodec", + download_videos: bool = False, + force_cache_sync: bool = False, + tolerance_s: float = 1e-4, + split: str = "train", + val_ratio: float = 0.01, + seed: int = 0, + # Camera configuration + camera_mode: str = "image", # 'image', 'wrist_image', or 'concat_view' + # Action configuration + action_space: str = "frame_wise_relative", # "absolute" or "relative" or "frame_wise_relative" + # rotation_space + rotation_space: Literal["9d", "6d", "3d"] = "3d", + # Native simulator frame or shared OpenCV-style EE frame used by midtraining. + pose_coordinate_frame: Literal["native", "opencv"] = "native", + # domain-aware configuration + embodiment_type: str = "libero", + action_normalization: Literal["quantile", "quantile_rot", "meanstd", "minmax"] | None = None, + action_stats_path: str | None = None, + skip_video_loading: bool = False, + ): + super().__init__() + self._embodiment_type = embodiment_type + self.domain_id = get_domain_id(embodiment_type) + self.image_size = image_size + self.chunk_length = chunk_length + assert self.chunk_length % 4 == 0, "chunk_length must be divisible by 4" + self.fps = fps + self.mode = mode + self.split = split.lower().strip() + self.val_ratio = val_ratio + self.seed = seed + self.camera_mode = camera_mode.lower().strip() + self.action_space = action_space + self.action_normalization = action_normalization + self.rotation_space = rotation_space.lower().strip() + self.pose_coordinate_frame = pose_coordinate_frame + self._pose_convention = self.action_space + self._rotation_format = libero_rotation_format(self.rotation_space) + # When True, skip video decoding entirely: drop image keys from + # delta_timestamps so LeRobot never touches the mp4, and return + # ``video=None`` in __getitem__. Must be set at construction time + # because LeRobotDataset is eagerly built in __init__. + self._skip_video_loading = bool(skip_video_loading) + + # Load action normalization stats. ``action_min`` / ``action_range`` are + # retained for older LIBERO eval code that knows how to invert a + # range-style [-1, 1] normalization. + self._norm_stats: dict[str, torch.Tensor] | None = None + self.action_min: torch.Tensor | None = None + self.action_max: torch.Tensor | None = None + self.action_range: torch.Tensor | None = None + if self.action_normalization is not None: + stats_path = self._resolve_action_stats_path(action_stats_path) + stats_key = "global_raw" if self.action_normalization == "quantile_rot" else "global" + raw_stats = load_action_stats(str(stats_path), stats_key=stats_key) + self._norm_stats = {} + for key, value in raw_stats.items(): + self._norm_stats[key] = torch.from_numpy(value).float() # [D] + self._set_range_denormalization_stats() + log.info( + f"Loaded LIBERO action stats from {stats_path} with action_normalization={self.action_normalization}" + ) + + # Validate camera mode + if self.camera_mode not in {"image", "wrist_image", "concat_view"}: + raise ValueError(f"Unsupported camera_mode={camera_mode!r}. Use 'image', 'wrist_image', or 'concat_view'.") + + # Validate split + if self.split not in {"train", "val", "valid", "validation", "eval", "test", "full"}: + raise ValueError(f"Unsupported {split=}. Use train/val/full.") + + # Build delta timestamps based on camera mode + dt = 1.0 / self.fps + + if self.fps != 20: + log.warning( + f"LIBERO is at 20fps. If using frame_wise_relative for policy mode training, we have to match the fps. fps={self.fps}" + ) + + # Determine which image keys to use + if self.camera_mode == "image": + self.image_keys = ["observation.images.image"] + elif self.camera_mode == "wrist_image": + self.image_keys = ["observation.images.wrist_image"] + else: # concat_view + self.image_keys = ["observation.images.image", "observation.images.wrist_image"] + + # Build delta_timestamps for all keys (same convention as PushT: 0 to chunk_length) + self.delta_timestamps: dict[str, list[float]] = {} + if not self._skip_video_loading: + for key in self.image_keys: + self.delta_timestamps[key] = [i * dt for i in range(0, chunk_length + 1)] + self.delta_timestamps["observation.state"] = [i * dt for i in range(0, chunk_length + 1)] + self.delta_timestamps["action"] = [i * dt for i in range(0, chunk_length + 1)] + + # Normalize repo_id and root to lists + repo_id_list: list[str] = [repo_id] if isinstance(repo_id, str) else list(repo_id) + root_list: list[str | None] + if root is None: + root_list = [None for _ in repo_id_list] + elif isinstance(root, str): + root_list = [root] + else: + root_list = [r for r in root] + + if len(repo_id_list) != len(root_list): + raise ValueError( + f"Length mismatch: repo_id has {len(repo_id_list)} items, root has {len(root_list)} items." + ) + + # Load all datasets + self.datasets: list[LeRobotDataset] = [] + self.tasks_dfs: list = [] # Store tasks DataFrames for each dataset + for rid, r in zip(repo_id_list, root_list): + dataset = LeRobotDataset( + repo_id=rid, + root=r, + delta_timestamps=self.delta_timestamps, # type: ignore + tolerance_s=tolerance_s, + force_cache_sync=force_cache_sync, + download_videos=download_videos, + video_backend=video_backend, + episodes=None, # Load full dataset, filter later + ) + self.datasets.append(dataset) + self.tasks_dfs.append(dataset.meta.tasks) + + # Build index mapping: list of (dataset_idx, local_idx) for valid frames + self.index_map: list[tuple[int, int, int]] = [] # (dataset_idx, local_idx, episode_idx) + self._episode_boundaries: list[dict[int, tuple[int, int]]] = [] + self._episode_splits: list[tuple[set[int], set[int]]] = [] + + total_episodes = 0 + total_frames = 0 + for ds_idx, dataset in enumerate(self.datasets): + # Compute episode splits for this dataset + train_eps, val_eps = self._compute_episode_splits_for_dataset(dataset) + self._episode_splits.append((train_eps, val_eps)) + + # Get episodes for current split + split_episodes = self._get_split_episodes_for_dataset(ds_idx) + + # Build episode boundaries + boundaries = self._build_episode_boundaries_for_dataset(dataset) + self._episode_boundaries.append(boundaries) + + # Filter indices + indices = self._filter_indices_for_dataset(ds_idx, dataset, split_episodes, boundaries) + self.index_map.extend(indices) + + total_episodes += dataset.num_episodes + total_frames += len(dataset) + + log.info( + f"Loaded LIBERO dataset with {len(repo_id_list)} source(s) split={self.split!r} " + f"camera_mode={self.camera_mode!r} " + f"total_episodes={total_episodes} " + f"total_frames={total_frames} " + f"valid_indices={len(self.index_map)}" + ) + + def _compute_episode_splits_for_dataset(self, dataset: LeRobotDataset) -> tuple[set[int], set[int]]: + """Compute train/val episode splits deterministically for a single dataset.""" + total_episodes = int(dataset.meta.total_episodes) + + if not (0.0 < self.val_ratio < 1.0): + raise ValueError(f"{self.val_ratio=} must be in (0, 1).") + + n_val = max(1, int(round(total_episodes * self.val_ratio))) + # val_eps = set(range(n_val)) + # train_eps = set(range(n_val, total_episodes)) + + # Yihuai: Randomly select validation episodes instead of the first n_val episodes (otherwise task will be repeated) + rng = random.Random(self.seed) # To ensure validation episodes are the same on all ranks + val_eps = set(rng.sample(range(total_episodes), n_val)) + train_eps = set(range(total_episodes)) - val_eps + + log.info(f"train_eps={train_eps}, val_eps={val_eps}") + + return train_eps, val_eps + + def _get_split_episodes_for_dataset(self, ds_idx: int) -> set[int]: + """Get the episode set for the current split for a specific dataset.""" + train_eps, val_eps = self._episode_splits[ds_idx] + if self.split in {"val", "valid", "validation", "eval", "test"}: + return val_eps + elif self.split == "train": + return train_eps + else: # full + return train_eps | val_eps + + def _build_episode_boundaries_for_dataset(self, dataset: LeRobotDataset) -> dict[int, tuple[int, int]]: + """Build a dict of episode_index -> (start_frame, end_frame) for a single dataset.""" + boundaries: dict[int, tuple[int, int]] = {} + for ep in dataset.meta.episodes: + ep_idx = int(ep["episode_index"]) # type: ignore[index] + start = int(ep["dataset_from_index"]) # type: ignore[index] + end = int(ep["dataset_to_index"]) # type: ignore[index] + boundaries[ep_idx] = (start, end) + return boundaries + + def _filter_indices_for_dataset( + self, + ds_idx: int, + dataset: LeRobotDataset, + split_episodes: set[int], + boundaries: dict[int, tuple[int, int]], + ) -> list[tuple[int, int, int]]: + """Filter valid indices for a single dataset, returning (dataset_idx, local_idx, episode_idx).""" + index_map: list[tuple[int, int, int]] = [] + all_meta = list(dataset.meta.episodes) + + for ep_idx in split_episodes: + if ep_idx >= len(all_meta): + continue + ep = all_meta[ep_idx] + + ep_start = int(ep["dataset_from_index"]) # type: ignore[index] + ep_end = int(ep["dataset_to_index"]) # type: ignore[index] + + # Valid range: [start, end - chunk_length - 1] inclusive + # We drop chunk_length frames at end to ensure we can query up to delta=chunk_length. + start = ep_start + end = ep_end - self.chunk_length - 1 + + if end >= start: + for local_idx in range(start, end + 1): + index_map.append((ds_idx, local_idx, ep_idx)) + + return index_map + + def __len__(self) -> int: + return len(self.index_map) + + def _get_task_description(self, ds_idx: int, item: dict) -> str: + """Get task description for the current item from meta/tasks.parquet. + + The tasks.parquet has task descriptions as the DataFrame index (row labels) + and task_index as an integer column. We look up by task_index and return + the corresponding index name (the actual task description string). + """ + task_idx = item.get("task_index") + if task_idx is not None: + if isinstance(task_idx, torch.Tensor): + task_idx = task_idx.item() + task_idx = int(task_idx) + tasks_df = self.tasks_dfs[ds_idx] + if task_idx in tasks_df["task_index"].values: + row = tasks_df[tasks_df["task_index"] == task_idx].iloc[0] + # The task description is the index name (row label), not a column value + return str(row.name) + raise ValueError(f"Task index {task_idx} not found in tasks.parquet for dataset {ds_idx}") + + def _compute_anchored_actions( + self, + state_raw: torch.Tensor, + action_raw: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Compute anchored relative actions (batched). + + Converts frame-wise relative actions to anchored relative actions where each + action[t] represents the target pose (after applying action[t] to state[t]) + expressed in state 0's local coordinate frame. + + Mathematical formulation: + 1. Compute target in world frame (LIBERO convention): + - p_{t+1} = p_t + delta_p[t] (position addition in world frame) + - R_{t+1} = R_delta[t] @ R_t (rotation composition, delta first) + 2. Compute anchored (left-multiply by T_0^{-1}): + - anchored_pos[t] = R_0^T @ (p_{t+1} - p_0) + - anchored_rot[t] = R_0^T @ R_{t+1} + + Args: + state_raw: State tensor of shape (T+1, 8): [x, y, z, ax, ay, az, grip1, grip2] + where (ax, ay, az) is axis-angle rotation. + action_raw: Action tensor of shape (T+1, 7): [dx, dy, dz, dax, day, daz, grip] + where (dax, day, daz) is axis-angle rotation delta. + + Returns: + anchored_translation: (T, 3) - position in state_0's local frame + anchored_rotation_9d: (T, 9) - rotation relative to state_0 as flattened 3x3 matrix + gripper: (T, 1) - original gripper commands (unchanged) + """ + # Extract positions and rotations from states + p_states = state_raw[:, :3] # [T+1,3] + rotvec_states = state_raw[:, 3:6] # [T+1,3] - axis-angle + + # Extract deltas from actions (use first T actions) + delta_p = action_raw[:-1, :3] # [T,3] + delta_rotvec = action_raw[:-1, 3:6] # [T,3] - axis-angle delta + gripper = action_raw[:-1, 6:7] # [T,1] + + # Convert all axis-angle to rotation matrices (batched) + R_states = convert_rotation(rotvec_states, input_format="axisangle", output_format="matrix") # [T+1,3,3] + R_deltas = convert_rotation(delta_rotvec, input_format="axisangle", output_format="matrix") # [T,3,3] + + # Initial pose (state 0) + p_0 = p_states[0] # [3] + R_0 = R_states[0] # [3,3] + R_0_T = R_0.T # [3,3] - transpose for inverse rotation + + # Current states for t = 0..T-1 + p_t = p_states[:-1] # [T,3] + R_t = R_states[:-1] # [T,3,3] + + # Step 1: Compute target poses in world frame (LIBERO convention) + # p_target = p_t + delta_p + p_target = p_t + delta_p # [T,3] + + # R_target = R_delta @ R_t (batched matrix multiply) + R_target = torch.bmm(R_deltas, R_t) # [T,3,3] + + # Step 2: Compute anchored (in state_0's local frame) + # anchored_p = R_0^T @ (p_target - p_0) + displacement = p_target - p_0 # [T,3] + anchored_p = (R_0_T @ displacement.T).T # [T,3] + + # anchored_R = R_0^T @ R_target (batched) + R_0_T_expanded = R_0_T.unsqueeze(0).expand(R_target.shape[0], -1, -1) # [T,3,3] + anchored_R = torch.bmm(R_0_T_expanded, R_target) # [T,3,3] + + return anchored_p, anchored_R, gripper + + def _convert_rotation_to_repr(self, rotation_matrix: torch.Tensor) -> torch.Tensor: + """Convert rotation matrix to the desired representation. + + Args: + rotation_matrix: Rotation matrices of shape (T, 3, 3). + + Returns: + Rotation in the configured ``rotation_space`` format. + """ + return convert_rotation(rotation_matrix, "matrix", libero_rotation_format(self.rotation_space)) + + def _normalizer_filename(self) -> str: + rotation_suffix = { + "3d": "3d", + "6d": "rot6d", + "9d": "rot9d", + }.get(self.rotation_space) + if rotation_suffix is None: + raise ValueError(f"Unsupported rotation_space={self.rotation_space!r}.") + action_space = self.action_space.replace("-", "_") + return f"{self._embodiment_type}_{action_space}_{rotation_suffix}.json" + + def _resolve_action_stats_path(self, action_stats_path: str | None) -> Path: + if action_stats_path is None: + stats_path = self._NORMALIZERS_DIR / self._normalizer_filename() + if stats_path.exists(): + return stats_path + raise FileNotFoundError( + f"Could not find bundled LIBERO action stats at {stats_path}. " + "Pass action_stats_path explicitly or regenerate stats with compute_action_stats.py." + ) + + stats_path = Path(action_stats_path) + if stats_path.is_absolute(): + if stats_path.exists(): + return stats_path + raise FileNotFoundError(f"Could not find action_stats_path={action_stats_path!r}.") + + module_dir = Path(__file__).resolve().parent + candidates: list[Path] = [] + for parent in module_dir.parents: + candidates.append(parent / stats_path) + candidates.append(self._NORMALIZERS_DIR / stats_path.name) + candidates.append(module_dir / stats_path.name) + for candidate in candidates: + if candidate.exists(): + return candidate + raise FileNotFoundError( + f"Could not resolve action_stats_path={action_stats_path!r}; tried: {[str(c) for c in candidates]}" + ) + + def _set_range_denormalization_stats(self) -> None: + if self._norm_stats is None: + return + + if self.action_normalization == "minmax": + lo_key, hi_key = "min", "max" + elif self.action_normalization in ("quantile", "quantile_rot"): + lo_key, hi_key = "q01", "q99" + else: + return + + if lo_key not in self._norm_stats or hi_key not in self._norm_stats: + raise ValueError( + f"Action stats for {self.action_normalization!r} normalization require " + f"{lo_key!r} and {hi_key!r} entries." + ) + self.action_min = self._norm_stats[lo_key] # [D] + self.action_max = self._norm_stats[hi_key] # [D] + action_range = self.action_max - self.action_min # [D] + self.action_range = torch.clamp(action_range, min=1e-6) # [D] + + def __getitem__(self, idx: int, _retry_count: int = 0) -> dict[str, torch.Tensor | str]: + """Get a single item from the dataset.""" + max_retries = 10 + ds_idx, local_idx, ep_idx = self.index_map[idx] + dataset = self.datasets[ds_idx] + try: + item = dataset[local_idx] + except Exception as e: + log.warning( + f"Error loading item (retry {_retry_count}/{max_retries}): idx={idx}, ds_idx={ds_idx}, " + f"local_idx={local_idx}, ep_idx={ep_idx}, repo_id={dataset.meta.repo_id}, error={e}" + ) + if _retry_count >= max_retries: + raise RuntimeError(f"Failed to load data after {max_retries} retries") from e + new_idx = random.randint(0, len(self) - 1) + return self.__getitem__(new_idx, _retry_count + 1) + + if self.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy", "image2video"]) + else: + mode = self.mode + + # Get task description for ai_caption + task_description = self._get_task_description(ds_idx, item) + + # Process video based on camera mode (skipped entirely when + # skip_video_loading=True; image keys are also absent from + # delta_timestamps so LeRobot never decoded them). + video: torch.Tensor | None + if self._skip_video_loading: + video = None + else: + if self.camera_mode == "concat_view": + # Load both cameras and concatenate horizontally + video_1: torch.Tensor = item["observation.images.image"] + video_2: torch.Tensor = item["observation.images.wrist_image"] + + # Resize each if needed + if video_1.shape[-1] != self.image_size or video_1.shape[-2] != self.image_size: + video_1 = F.resize(video_1, [self.image_size, self.image_size]) + if video_2.shape[-1] != self.image_size or video_2.shape[-2] != self.image_size: + video_2 = F.resize(video_2, [self.image_size, self.image_size]) + + # Concatenate along width dimension (last dim for TCHW) + video_tchw = torch.cat([video_1, video_2], dim=-1) # (T, C, H, W*2) + else: + # Single camera mode + image_key = self.image_keys[0] + video_tchw = item[image_key] + + # Resize if needed + if video_tchw.shape[-1] != self.image_size or video_tchw.shape[-2] != self.image_size: + video_tchw = F.resize(video_tchw, [self.image_size, self.image_size]) + + # Convert to uint8 and transpose to (C, T, H, W) + video = (video_tchw * 255).clamp(0, 255).to(torch.uint8).permute(1, 0, 2, 3) + + # Action (raw): LIBERO actions are 7D (6 DoF + gripper) + action_raw: torch.Tensor = item["action"] + # State (raw): LIBERO state is 8D (6 DoF + 2 gripper states) + state_raw: torch.Tensor = item["observation.state"] + + # Action: (T+1, D) -> (T, D) + # Take all but last action + # LIBERO action format: [x, y, z, ax, ay, az, gripper] (7D) where (ax,ay,az) is axis-angle + + if self.action_space == "relative": + # Compute anchored relative actions + # Returns: translation (T, 3), rotation_matrix (T, 3, 3), gripper (T, 1) + translation, rotation_matrix, gripper = self._compute_anchored_actions(state_raw, action_raw.clone()) + elif self.action_space == "frame_wise_relative": + action = action_raw[:-1].clone() # [T,7] + translation = action[:, :3] # [T,3] + rotation_rotvec = action[:, 3:6] # [T,3] + gripper = action[:, 6:] # [T,1] + rotation_matrix = convert_rotation( + rotation_rotvec, input_format="axisangle", output_format="matrix" + ) # [T,3,3] + else: + raise ValueError(f"Unsupported action space: {self.action_space}") + + rotation = self._convert_rotation_to_repr(rotation_matrix) # [T,rot_dim] + action = torch.cat([translation, rotation, gripper], dim=-1) # [T,action_dim] + + # Compute idle_frames from the raw (un-normalized) action, only when the + # action layout has correct per-frame idle semantics (frame_wise_relative + # ⇔ backward_framewise). The other action_spaces ("relative", + # "absolute") encode per-frame motion differently and would not give + # meaningful idle counts under the same threshold check. + idle_frames: torch.Tensor | None = None + if self.action_space == "frame_wise_relative": + try: + spec = build_action_spec(Pos(), Rot(libero_rotation_format(self.rotation_space)), Gripper()) + n = compute_idle_frames(action, spec) + idle_frames = torch.tensor(n, dtype=torch.long) + except (ValueError, TypeError): + idle_frames = None + + if self.action_normalization is not None and self._norm_stats is not None and self.action_min is not None: + if action.shape[-1] != self.action_min.shape[0]: + raise ValueError( + f"Action dimension {action.shape[-1]} does not match stats dimension " + f"{self.action_min.shape[0]}. Recompute stats for the current " + f"rotation_space={self.rotation_space!r} and action_space={self.action_space!r}." + ) + method = "quantile" if self.action_normalization == "quantile_rot" else self.action_normalization + action = normalize_action(action, method, self._norm_stats) # [T,D] + + # Index + key = torch.tensor([local_idx], dtype=torch.long) + + if self.camera_mode == "image": + viewpoint = "third_person_view" + elif self.camera_mode == "wrist_image": + viewpoint = "wrist_view" + else: + viewpoint = "concat_view" + + result: dict[str, torch.Tensor | str] = { + "source_repo_id": dataset.meta.repo_id, + "video": video, + "action": action, + "action_raw": action_raw, + "conditioning_fps": torch.tensor(self.fps, dtype=torch.long), + "prompt": task_description, + "ai_caption": task_description, + "mode": mode, + "state": state_raw, + "action_space": self.action_space, + "rotation_space": self.rotation_space, + "pose_coordinate_frame": self.pose_coordinate_frame, + "__key__": key, + "domain_id": torch.tensor(self.domain_id, dtype=torch.long), + "viewpoint": viewpoint, + } + if idle_frames is not None: + result["idle_frames"] = idle_frames + + if self.camera_mode == "concat_view" and not self._skip_video_loading: + result["additional_view_description"] = ( + "The left half shows the third-person view; the right half shows the wrist-mounted camera." + ) + + return result + + @property + def action_dim(self) -> int: + return libero_action_dim(self.rotation_space) diff --git a/cosmos_training/cosmos/data/vfm/action/libero_pose_utils.py b/cosmos_training/cosmos/data/vfm/action/libero_pose_utils.py new file mode 100644 index 00000000..edd89007 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/libero_pose_utils.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Small LIBERO pose helpers shared by training and closed-loop eval.""" + +from __future__ import annotations + +import numpy as np +import torch + +from cosmos.data.vfm.action.pose_utils import ( + RotationConvention, + build_abs_pose_from_components, +) + +# Same local-frame post-rotation pattern used by DROID/Bridge/Fractal: +# R_opencv = R_native @ *_TO_OPENCV. +LIBERO_TO_OPENCV: np.ndarray = np.array( + [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + dtype=np.float32, +) + +LIBERO_ROTATION_FORMATS: dict[str, RotationConvention] = { + "3d": "axisangle", + "6d": "rot6d", + "9d": "rot9d", +} +LIBERO_ACTION_DIMS: dict[str, int] = {"3d": 7, "6d": 10, "9d": 13} + + +def libero_rotation_format(rotation_space: str) -> RotationConvention: + """Return the shared ``pose_utils`` rotation format for a LIBERO setting.""" + rotation_format = LIBERO_ROTATION_FORMATS.get(rotation_space) + if rotation_format is None: + raise ValueError(f"Unsupported rotation_space={rotation_space!r}. Use 3d/6d/9d.") + return rotation_format + + +def libero_action_dim(rotation_space: str) -> int: + """Return ``[xyz, rotation, gripper]`` action width for LIBERO.""" + action_dim = LIBERO_ACTION_DIMS.get(rotation_space) + if action_dim is None: + raise ValueError(f"Unsupported rotation_space={rotation_space!r}. Use 3d/6d/9d.") + return action_dim + + +def libero_rotation_space_from_action_dim(action_dim: int) -> str: + """Infer LIBERO rotation space from unpadded action width.""" + for rotation_space, dim in LIBERO_ACTION_DIMS.items(): + if dim == action_dim: + return rotation_space + raise ValueError(f"Unable to infer rotation_space from action_dim={action_dim}.") + + +def build_libero_abs_pose(state_raw: torch.Tensor | np.ndarray, *, to_opencv: bool) -> np.ndarray: + """Build absolute LIBERO EE poses from state rows. + + ``state_raw`` is ``[x,y,z,axisangle(3),gripper(2)]``. When requested, the + local EE frame is post-rotated into the shared OpenCV-style action frame. + """ + if isinstance(state_raw, torch.Tensor): + state_np = state_raw.detach().cpu().numpy().astype(np.float32, copy=False) + else: + state_np = np.asarray(state_raw, dtype=np.float32) + + poses_abs = build_abs_pose_from_components(state_np[:, :3], state_np[:, 3:6], "axisangle") + if to_opencv: + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ LIBERO_TO_OPENCV + return poses_abs diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/README.md b/cosmos_training/cosmos/data/vfm/action/normalizers/README.md new file mode 100644 index 00000000..80d0ecab --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/README.md @@ -0,0 +1,79 @@ +# Action Normalizers + +This directory contains committed action-normalization statistics used by action +datasets during training and inference. + +There are two formats: + +- `BaseActionLeRobotDataset` stats JSONs, keyed by `global.mean/std/min/max/q01/q99`. + Files generated with skipped rotation dimensions may also include `global_raw`, + which preserves the raw pre-masking stats for `action_normalization="quantile_rot"`. + These are produced by `projects/cosmos3/vfm/datasets/action/compute_action_stats.py` + and are used by LeRobot-backed action datasets such as Bridge, DROID, + Fractal, RoboMIND, HandPose, Embodiment_b, and AgiBot. +- UMI field normalizers, keyed by output field name with per-field + `scale`/`offset` values. Normalization is `(x - offset) / scale`. + +## LeRobot-backed datasets + +Regenerate LeRobot-backed normalizers with `compute_action_stats.py`. By +default, output filenames match `BaseActionLeRobotDataset._normalizer_filename()`. +The script disables video loading for supported datasets; use fast init for +multi-shard datasets such as AgiBot: + +```bash +PYTHONPATH=. python cosmos/data/vfm/action/compute_action_stats.py \ + --config configs/base/config.py \ + --split train \ + --enable-fast-init \ + --reservoir-size 5000000 \ + -- experiment=embodiment_c_gripper +``` + +For Embodiment C gripper, the committed file is: + +```text +embodiment_c_gripper_backward_framewise_rot6d.json +``` + +This normalizer is shared by: + +- `embodiment_c_gripper` +- `embodiment_c_gripper_ext` +- `agibotworld_beta` + +The AgiBot FK-pose action layout is 29D: + +```text +[head(9), right_wrist(9), right_gripper(1), left_wrist(9), left_gripper(1)] +``` + +Rotation dims are left unnormalized by writing identity stats for the rot6d +blocks when using `action_normalization="quantile"`. Use +`action_normalization="quantile_rot"` to load `global_raw` and normalize +rotation dimensions as well. + +## UMI datasets + +UMI normalizers are produced by `UMISingleTrajDataset.fit_normalizer()`, which +computes min/max statistics over the dataset. To regenerate: + +```python +from cosmos.data.vfm.action.umi_dataset import get_umi_dataset + +dataset = get_umi_dataset( + dataset_name="", + dataset_type="single_task", + is_val=False, +) +dataset.fit_normalizer() +``` + +This writes `_normalizer.json` into the configured `normalizer_dir`. Copy the result into this directory. + +## Regenerate When + +- Changing action representation, pose convention, or rotation format. +- Changing gripper scaling or FK/action construction. +- Changing UMI `relative_pose_mode`, `use_relative_gripper_width`, or `eef_z_offset`. +- Adding a new task dataset or significantly expanding data with out-of-distribution actions. diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/bridge_orig_lerobot_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/bridge_orig_lerobot_backward_framewise_rot6d.json new file mode 100644 index 00000000..6a074f5d --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/bridge_orig_lerobot_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "bridge_orig_lerobot", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "bridge_20260416", + "dataset_class": "BridgeOrigLeRobotDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/bridge_raw", + "split": "train", + "num_samples_stats": 83036, + "reservoir_size": 5000000 + }, + "global": { + "mean": [-0.000094, -0.000394, 0.001623, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.582683], + "std": [ 0.013297, 0.009985, 0.012079, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.489959], + "min": [-0.309451, -0.074740, -0.082767, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.127018, 0.414660, 0.493186, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.038884, -0.028667, -0.037840, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.039722, 0.029068, 0.026702, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [-0.000094, -0.000394, 0.001623, 0.998307, -0.001371, 0.000061, 0.001414, 0.998226, -0.000154, 0.582683], + "std": [ 0.013297, 0.009985, 0.012079, 0.004630, 0.050168, 0.029018, 0.050165, 0.004328, 0.031742, 0.489959], + "min": [-0.309451, -0.074740, -0.082767, -0.845782, -0.636628, -0.401535, -0.590214, -0.217448, -0.979635, 0.000000], + "max": [ 0.127018, 0.414660, 0.493186, 1.000000, 0.362611, 0.601211, 0.619479, 1.000000, 0.365993, 1.000000], + "q01": [-0.038884, -0.028667, -0.037840, 0.976292, -0.163098, -0.081545, -0.160193, 0.976322, -0.078872, 0.000000], + "q99": [ 0.039722, 0.029068, 0.026702, 1.000000, 0.160195, 0.081655, 0.163227, 1.000000, 0.095189, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/droid_lerobot_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/droid_lerobot_backward_framewise_rot6d.json new file mode 100644 index 00000000..b2bd81ed --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/droid_lerobot_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "droid_lerobot", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "droid_20260418", + "dataset_class": "DROIDLeRobotDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/droid_plus_lerobot_640x360_20260412", + "split": "train", + "num_samples_stats": 1321153, + "reservoir_size": 5000000 + }, + "global": { + "mean": [-0.000017, -0.000612, 0.000568, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.588911], + "std": [ 0.004539, 0.004054, 0.004999, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.441186], + "min": [-0.075397, -0.057288, -0.056677, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.073107, 0.082187, 0.077080, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.014200, -0.013416, -0.015206, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.014515, 0.011517, 0.014520, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [-0.000017, -0.000612, 0.000568, 0.999830, 0.000227, -0.000152, -0.000222, 0.999818, 0.000417, 0.588911], + "std": [ 0.004539, 0.004054, 0.004999, 0.000336, 0.014924, 0.010784, 0.014927, 0.000351, 0.011903, 0.441186], + "min": [-0.075397, -0.057288, -0.056677, 0.695640, -0.220599, -0.195892, -0.697421, 0.600468, -0.154176, 0.000000], + "max": [ 0.073107, 0.082187, 0.077080, 1.000000, 0.698449, 0.168089, 0.220605, 1.000000, 0.391206, 1.000000], + "q01": [-0.014200, -0.013416, -0.015206, 0.998459, -0.047659, -0.034774, -0.047609, 0.998428, -0.035553, 0.000000], + "q99": [ 0.014515, 0.011517, 0.014520, 1.000000, 0.047596, 0.034660, 0.047654, 1.000000, 0.038888, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_b.json b/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_b.json new file mode 100644 index 00000000..cc625237 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_b.json @@ -0,0 +1,25 @@ +{ + "metadata": { + "embodiment_type": "embodiment_b", + "pose_convention": null, + "rotation_format": null, + "action_dim": 30, + "skip_rotation_dims": [], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "embodiment_b", + "dataset_class": "Embodiment_bDataset", + "dataset_root": "/lustre/fsw/portfolios/dir/projects/dir_cosmos_base_lustre/ychao/datasets/lerobot_v30/embodiment_b/data_sample_embodiment_b_20260123", + "split": "train", + "num_samples_stats": 29959, + "reservoir_size": 5000000 + }, + "global": { + "mean": [ 1.736306, -1.439579, -0.668389, -2.180986, -0.140066, -0.108423, 0.082355, -0.875710, 1.008860, 0.201179, 1.680600, 0.491681, -0.079254, -0.216104, 0.388113, 0.266642, 0.840022, 0.032036, 0.015728, -0.023622, 0.994667, 0.589821, -0.233003, 0.887161, -0.020604, 0.056817, 0.174483, 0.859159, 0.298373, 0.466469], + "std": [ 0.258256, 0.155732, 0.166345, 0.259065, 0.150113, 0.188906, 0.151725, 0.735972, 0.454291, 0.585101, 0.423016, 0.331052, 0.294947, 0.426356, 0.082411, 0.049379, 0.054863, 0.042942, 0.047117, 0.065842, 0.020142, 0.140435, 0.134750, 0.086622, 0.241199, 0.112985, 0.267914, 0.291596, 0.199299, 0.378883], + "min": [-0.210359, -1.590618, -2.304231, -2.450943, -1.152912, -0.820512, -0.360174, -3.004326, -1.608063, -1.690435, 0.127842, -1.044611, -0.735398, -1.531176, 0.247331, -0.017434, 0.461093, -0.075683, -0.261053, -0.638773, 0.162428, 0.251857, -0.709606, 0.437299, -0.793154, -0.370741, -0.701581, -0.499994, 0.166667, 0.166667], + "max": [ 2.898517, 1.205229, 1.096718, -0.532735, 0.288580, 0.516904, 0.841676, 1.171752, 1.608063, 2.833071, 2.567994, 1.932145, 0.822665, 1.028385, 0.918418, 0.457046, 1.316623, 0.793569, 0.204932, 0.131193, 0.999951, 0.985773, 0.198002, 1.260103, 0.889235, 0.491383, 0.829481, 0.999999, 1.000000, 1.000000], + "q01": [ 0.855471, -1.590571, -0.868137, -2.450919, -0.492864, -0.820464, -0.221557, -2.603151, -1.029259, -1.337718, 0.634284, -0.325527, -0.703343, -1.367646, 0.295801, 0.164838, 0.697646, -0.027419, -0.120393, -0.156538, 0.968709, 0.311899, -0.534542, 0.757548, -0.630892, -0.165377, -0.629816, -0.378646, 0.166667, 0.166667], + "q99": [ 2.018381, -1.090716, -0.124196, -1.152713, 0.110422, 0.177702, 0.344905, 0.813681, 1.608063, 1.769150, 2.396629, 1.414085, 0.645255, 0.542724, 0.589946, 0.352675, 0.947880, 0.122770, 0.125947, 0.115921, 0.999723, 0.887231, 0.080622, 1.114126, 0.784541, 0.386300, 0.704707, 0.999596, 0.583333, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_c_gripper_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_c_gripper_backward_framewise_rot6d.json new file mode 100644 index 00000000..e4abb873 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/embodiment_c_gripper_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "embodiment_c_gripper", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 29, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "embodiment_c_gripper", + "dataset_class": "EmbodimentCGripperDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/EmbodimentCWorld_20260208/agibot-offshelf", + "split": "train", + "num_samples_stats": 4602075, + "reservoir_size": 5000000 + }, + "global": { + "mean": [-0.000001, -0.000053, -0.000039, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000067, 0.000060, 0.000031, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.490037, -0.000099, 0.000038, 0.000047, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.556796], + "std": [ 0.001810, 0.004119, 0.010410, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.006703, 0.009197, 0.006997, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.396319, 0.006079, 0.008763, 0.006617, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.402953], + "min": [-0.607455, -0.465921, -1.314688, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.205045, -1.253672, -1.082791, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000, -1.235187, -1.328355, -1.081424, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.607457, 0.465930, 1.314692, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.205044, 1.253679, 1.097739, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.238890, 1.322212, 1.089373, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.000167, -0.007272, -0.014935, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.012912, -0.017163, -0.017614, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000, -0.011640, -0.015508, -0.013880, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.000164, 0.004822, 0.013706, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.013182, 0.016960, 0.016101, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.010890, 0.015347, 0.012968, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [-0.000001, -0.000053, -0.000039, 0.999995, 0.000001, -0.000002, -0.000001, 0.999975, 0.000081, 0.000067, 0.000060, 0.000031, 0.999687, 0.000133, -0.000151, -0.000152, 0.999683, -0.000060, 0.490037, -0.000099, 0.000038, 0.000047, 0.999815, -0.000118, 0.000219, 0.000132, 0.999816, -0.000041, 0.556796], + "std": [ 0.001810, 0.004119, 0.010410, 0.000093, 0.001281, 0.003053, 0.001279, 0.000182, 0.007016, 0.006703, 0.009197, 0.006997, 0.001695, 0.019522, 0.015552, 0.019496, 0.001879, 0.015844, 0.396319, 0.006079, 0.008763, 0.006617, 0.001385, 0.015094, 0.011857, 0.015060, 0.001785, 0.011771, 0.402953], + "min": [-0.607455, -0.465921, -1.314688, 0.985691, -0.066926, -0.159641, -0.068475, 0.951378, -0.308000, -1.205045, -1.253672, -1.082791, -0.501766, -0.909665, -0.743807, -0.901858, -0.763505, -0.903163, 0.000000, -1.235187, -1.328355, -1.081424, -0.193770, -0.811308, -0.944745, -0.828973, -0.818795, -0.773241, 0.000000], + "max": [ 0.607457, 0.465930, 1.314692, 1.000000, 0.069006, 0.146651, 0.065107, 1.000000, 0.268938, 1.205044, 1.253679, 1.097739, 1.000000, 0.820798, 0.962229, 0.831849, 1.000000, 0.837535, 1.000000, 1.238890, 1.322212, 1.089373, 1.000000, 0.795667, 0.686249, 0.796464, 1.000000, 0.696757, 1.000000], + "q01": [-0.000167, -0.007272, -0.014935, 0.999999, -0.000306, -0.000594, -0.000260, 0.999227, -0.025516, -0.012912, -0.017163, -0.017614, 0.994613, -0.064506, -0.053231, -0.066267, 0.994383, -0.051163, 0.000000, -0.011640, -0.015508, -0.013880, 0.996511, -0.050126, -0.040305, -0.047330, 0.996618, -0.038303, 0.000000], + "q99": [ 0.000164, 0.004822, 0.013706, 1.000000, 0.000240, 0.000703, 0.000278, 1.000000, 0.030090, 0.013182, 0.016960, 0.016101, 1.000000, 0.066268, 0.053905, 0.064357, 1.000000, 0.052547, 1.000000, 0.010890, 0.015347, 0.012968, 1.000000, 0.047482, 0.042217, 0.050173, 1.000000, 0.041428, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/fractal_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/fractal_backward_framewise_rot6d.json new file mode 100644 index 00000000..ffbbfcd6 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/fractal_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "fractal", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "fractal_20260413", + "dataset_class": "FractalLeRobotDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/fractal20220817_data_no_noops", + "split": "train", + "num_samples_stats": 166961, + "reservoir_size": 5000000 + }, + "global": { + "mean": [ 0.002259, 0.000721, 0.009372, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.526947], + "std": [ 0.014178, 0.016428, 0.022554, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.499273], + "min": [-0.151886, -0.176424, -0.194576, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.130892, 0.190835, 0.193839, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.039816, -0.049270, -0.056266, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.043860, 0.050352, 0.072505, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [ 0.002259, 0.000721, 0.009372, 0.998347, 0.001789, 0.002694, -0.001861, 0.997811, 0.016366, 0.526947], + "std": [ 0.014178, 0.016428, 0.022554, 0.003377, 0.043416, 0.037369, 0.043211, 0.004566, 0.047057, 0.499273], + "min": [-0.151886, -0.176424, -0.194576, 0.520558, -0.676280, -0.822475, -0.460521, 0.736643, -0.517041, 0.000000], + "max": [ 0.130892, 0.190835, 0.193839, 1.000000, 0.461026, 0.403940, 0.671708, 1.000000, 0.505528, 1.000000], + "q01": [-0.039816, -0.049270, -0.056266, 0.983667, -0.134543, -0.107048, -0.126518, 0.977277, -0.091363, 0.000000], + "q99": [ 0.043860, 0.050352, 0.072505, 1.000000, 0.127404, 0.107273, 0.134140, 1.000000, 0.179731, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/hand_pose_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/hand_pose_backward_framewise_rot6d.json new file mode 100644 index 00000000..c2cab11c --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/hand_pose_backward_framewise_rot6d.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "embodiment_type": "hand_pose", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 57, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 36, 37, 38, 39, 40, 41], + "chunk_length": 1, + "sample_stride": 16, + "dataset_name": "hand_pose_20260413", + "dataset_class": "HandPoseDataset", + "dataset_root": ["/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_08_500hr_lerobot_no_bframes/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_15_1500hr_lerobot_no_bframes/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/feb_23_1000hr_lerobot_no_bframes/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_02_1000hr_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_31", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_32", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_33", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_34", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_35", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_36", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_37", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_38", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_39", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_40", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_41", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_42", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_43", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_44", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_45", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_46", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_47", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_48", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_49", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_50", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_51", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_52", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_53", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_54", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_55", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_56", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_57", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_58", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_59", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_60", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_61", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_62", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_63", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_64", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_65", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_66", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_67", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_68", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_69", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_70", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_71", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_72", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_73", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_74", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_75", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_76", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_77", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_78", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_79", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_09_4000hr_lerobot/shard_80", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_31", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_32", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_33", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_34", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_35", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_36", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_37", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_38", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_39", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_40", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_41", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_42", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_43", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_44", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_45", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_46", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_47", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_48", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_49", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_50", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_51", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_52", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_53", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_54", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_55", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_56", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_57", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_58", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_59", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_60", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_61", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_62", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_63", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_64", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_65", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_66", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_67", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_68", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_69", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_70", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_71", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_72", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_73", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_74", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_75", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_76", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_77", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_78", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_79", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_80", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_81", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_82", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_83", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_84", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_85", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_86", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_87", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_88", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_89", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_90", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_91", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_92", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_93", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_94", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_95", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_96", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_97", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_98", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_99", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_100", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_101", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_102", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_103", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_104", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_105", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_106", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_107", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_108", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_109", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_110", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_111", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_112", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_113", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_114", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_115", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_116", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_117", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_118", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_119", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_120", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_121", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_122", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_123", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_124", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_125", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_126", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_127", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_128", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_129", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_130", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_131", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_132", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_133", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_134", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_135", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_136", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_137", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_138", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_16_7000hr_lerobot/shard_139", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_31", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_32", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_33", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_34", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_35", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_36", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_37", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_38", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_39", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_40", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_41", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_42", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_43", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_44", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_45", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_46", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_47", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_48", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_49", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_50", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_51", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_52", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_53", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_54", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_55", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_56", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_57", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_58", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_59", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_60", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_61", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_62", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_63", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_64", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_65", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_66", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_67", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_68", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_69", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_70", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_71", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_72", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_73", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_74", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_75", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_76", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_77", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_78", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_79", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_80", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_81", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_82", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_83", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_84", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_85", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_86", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_87", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_88", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_89", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_90", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_91", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_92", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_93", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_94", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_95", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_96", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_97", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_98", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_99", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_100", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_101", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_102", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_103", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_104", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_105", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_106", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_107", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_108", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_109", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_110", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_111", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_112", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_113", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_114", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_115", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_116", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_117", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_118", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_119", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_120", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_121", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_122", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_123", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_124", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_125", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_126", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_127", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_128", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_129", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_130", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_131", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_132", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_133", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_134", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_135", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_136", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_137", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_138", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_139", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_140", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_141", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_142", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_143", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_144", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_145", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_146", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_147", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_148", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_149", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_150", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_151", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_152", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_153", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_154", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_155", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_156", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_157", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_158", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_159", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_160", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_161", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_162", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_163", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_164", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_165", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_166", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_167", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_168", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_169", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_170", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_171", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_172", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_173", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_174", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_175", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_176", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_177", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_178", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/mar_30_9000hr_lerobot/shard_179", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_31", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_32", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_33", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_34", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_35", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_36", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_37", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_38", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_39", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_40", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_41", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_42", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_43", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_44", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_45", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_46", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_47", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_48", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_49", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_50", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_51", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_52", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_53", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_54", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_55", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_56", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_57", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_58", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_59", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_60", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_61", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_62", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_63", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_64", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_65", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_66", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_67", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_68", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_69", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_70", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_71", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_72", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_73", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_74", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_75", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_76", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_77", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_78", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_79", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_80", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_81", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_82", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_83", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_84", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_85", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_86", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_87", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_88", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_89", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_90", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_91", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_92", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_93", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_94", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_95", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_96", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_97", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_98", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_99", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_100", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_101", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_102", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_103", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_104", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_105", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_106", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_107", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_108", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_109", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_110", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_111", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_112", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_113", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_114", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_115", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_116", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_117", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_118", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_119", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_120", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_121", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_122", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_123", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_124", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_125", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_126", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_127", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_128", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_129", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_130", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_131", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_132", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_133", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_134", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_135", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_136", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_137", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_138", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_139", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_140", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_141", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_142", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_143", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_144", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_145", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_146", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_147", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_148", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_149", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_150", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_151", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_152", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_153", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_154", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_155", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_156", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_157", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_158", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_159", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_160", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_161", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_162", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_163", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_164", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_165", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_166", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_167", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_168", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_169", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_170", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_171", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_172", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_173", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_174", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_175", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_176", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_177", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_178", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_179", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_03_lerobot/shard_180", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_00", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_01", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_02", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_03", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_04", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_05", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_06", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_07", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_08", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_09", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_10", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_11", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_12", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_13", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_14", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_15", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_16", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_17", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_18", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_19", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_20", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_21", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_22", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_23", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_24", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_25", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_26", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_27", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_28", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_29", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_30", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_31", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_32", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_33", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_34", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_35", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_36", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_37", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_38", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_39", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_40", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_41", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_42", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_43", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_44", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_45", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_46", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_47", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_48", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_49", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_50", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_51", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_52", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_53", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_54", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_55", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_56", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_57", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_58", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_59", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_60", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_61", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_62", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_63", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_64", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_65", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_66", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_67", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_68", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_69", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_70", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_71", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_72", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_73", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_74", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_75", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_76", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_77", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_78", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_79", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_80", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_81", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_82", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_83", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_84", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_85", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_86", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_87", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_88", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_89", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_90", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_91", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_92", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_93", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_94", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_95", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_96", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_97", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_98", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_99", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_100", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_101", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_102", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_103", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_104", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_105", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_106", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_107", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_108", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_109", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_110", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_111", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_112", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_113", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_114", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_115", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_116", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_117", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_118", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_119", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_120", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_121", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_122", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_123", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_124", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_125", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_126", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_127", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_128", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_129", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_130", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_131", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_132", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_133", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_134", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_135", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_136", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_137", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_138", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_139", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_140", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_141", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_142", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_143", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_144", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_145", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_146", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_147", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_148", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_149", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_150", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_151", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_152", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_153", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_154", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_155", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_156", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_157", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_158", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_159", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_160", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_161", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_162", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_163", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_164", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_165", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_166", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_167", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_168", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_169", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_170", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_171", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_172", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_173", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_174", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_175", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_176", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_177", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_178", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_179", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_180", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_181", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_182", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_183", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_184", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_185", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_186", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_187", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_188", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_189", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_190", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_191", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_192", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_193", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_194", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_195", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_196", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_197", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_198", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_199", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_200", "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/embodiment_a/apr_06_10000hr_lerobot/shard_201"], + "split": "train", + "num_samples_stats": 1027904, + "reservoir_size": 5000000, + "max_samples": 1000000, + "sampling_seed": 42 + }, + "global": { + "mean": [-0.000051, -0.000536, 0.000271, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, -0.000250, 0.000039, 0.000358, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, -0.020389, 0.067672, 0.095163, -0.007768, 0.060786, 0.120434, 0.014182, 0.060810, 0.106781, 0.030128, 0.057963, 0.096013, 0.043033, 0.045557, 0.083792, 0.000213, 0.000032, 0.000431, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.025732, 0.066837, 0.093646, 0.009652, 0.056993, 0.125456, -0.013350, 0.057552, 0.117093, -0.030613, 0.054797, 0.107642, -0.045114, 0.042600, 0.092532], + "std": [ 0.008032, 0.006073, 0.004838, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.015564, 0.013365, 0.013550, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.020526, 0.015765, 0.014673, 0.010769, 0.016233, 0.027204, 0.009681, 0.015045, 0.031945, 0.012000, 0.015039, 0.031826, 0.012330, 0.013215, 0.025054, 0.012756, 0.011769, 0.011749, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.021840, 0.017342, 0.015389, 0.010482, 0.018389, 0.028098, 0.009773, 0.017744, 0.032972, 0.011621, 0.017690, 0.032064, 0.012763, 0.015870, 0.024854], + "min": [-0.135277, -0.178123, -0.132330, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.303401, -0.308792, -0.285116, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.169384, -0.057983, 0.004869, -0.080825, -0.066215, 0.031035, -0.062484, -0.074770, 0.021586, -0.033416, -0.068450, 0.013296, -0.018355, -0.051769, 0.009567, -0.286551, -0.261433, -0.304940, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.073763, -0.052037, -0.012102, -0.045693, -0.059251, 0.034208, -0.061135, -0.072159, 0.029027, -0.093798, -0.058713, 0.019525, -0.109188, -0.044517, 0.013484], + "max": [ 0.162729, 0.092829, 0.113989, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.412223, 0.342286, 0.258529, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.053027, 0.143626, 0.196154, 0.048750, 0.110958, 0.216498, 0.071694, 0.109664, 0.226691, 0.093366, 0.106893, 0.219522, 0.104191, 0.094833, 0.192419, 0.288662, 0.238981, 0.266988, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.180267, 0.152062, 0.183739, 0.098497, 0.114275, 0.216822, 0.064544, 0.107233, 0.233817, 0.021220, 0.107390, 0.233983, -0.000990, 0.096846, 0.206103], + "q01": [-0.026688, -0.024154, -0.013269, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.048362, -0.040973, -0.040898, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.080012, 0.029713, 0.059227, -0.038851, 0.010826, 0.062254, -0.007452, 0.010061, 0.047149, 0.005736, 0.006631, 0.036123, 0.018989, 0.001386, 0.033526, -0.037843, -0.037665, -0.036427, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -0.013628, 0.024588, 0.053585, -0.012045, 0.000813, 0.065182, -0.037355, -0.001436, 0.052279, -0.058561, -0.004175, 0.042882, -0.078014, -0.007496, 0.039113], + "q99": [ 0.026573, 0.015575, 0.015514, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.044917, 0.040128, 0.043628, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.016559, 0.105274, 0.129951, 0.014097, 0.089857, 0.188346, 0.037904, 0.087388, 0.192120, 0.059029, 0.083990, 0.177890, 0.074478, 0.069913, 0.145329, 0.041177, 0.036800, 0.039655, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.087733, 0.107510, 0.128318, 0.040024, 0.088640, 0.191875, 0.008532, 0.086990, 0.195138, -0.005823, 0.083213, 0.180562, -0.019596, 0.069257, 0.148124] + }, + "global_raw": { + "mean": [-0.000051, -0.000536, 0.000271, 0.999819, -0.000087, -0.000014, 0.000086, 0.999810, -0.000149, -0.000250, 0.000039, 0.000358, 0.990760, -0.000167, 0.000539, -0.000625, 0.991384, 0.000669, -0.020389, 0.067672, 0.095163, -0.007768, 0.060786, 0.120434, 0.014182, 0.060810, 0.106781, 0.030128, 0.057963, 0.096013, 0.043033, 0.045557, 0.083792, 0.000213, 0.000032, 0.000431, 0.993515, -0.000283, -0.000113, 0.000924, 0.993799, 0.000744, 0.025732, 0.066837, 0.093646, 0.009652, 0.056993, 0.125456, -0.013350, 0.057552, 0.117093, -0.030613, 0.054797, 0.107642, -0.045114, 0.042600, 0.092532], + "std": [ 0.008032, 0.006073, 0.004838, 0.000911, 0.017357, 0.007726, 0.017367, 0.000866, 0.008817, 0.015564, 0.013365, 0.013550, 0.043221, 0.102472, 0.077623, 0.102379, 0.039789, 0.071362, 0.020526, 0.015765, 0.014673, 0.010769, 0.016233, 0.027204, 0.009681, 0.015045, 0.031945, 0.012000, 0.015039, 0.031826, 0.012330, 0.013215, 0.025054, 0.012756, 0.011769, 0.011749, 0.034683, 0.087015, 0.064440, 0.086931, 0.032113, 0.061431, 0.021840, 0.017342, 0.015389, 0.010482, 0.018389, 0.028098, 0.009773, 0.017744, 0.032972, 0.011621, 0.017690, 0.032064, 0.012763, 0.015870, 0.024854], + "min": [-0.135277, -0.178123, -0.132330, 0.922451, -0.352303, -0.233175, -0.363654, 0.931059, -0.182811, -0.303401, -0.308792, -0.285116, -0.998424, -0.999706, -0.994105, -0.999480, -0.997971, -0.987817, -0.169384, -0.057983, 0.004869, -0.080825, -0.066215, 0.031035, -0.062484, -0.074770, 0.021586, -0.033416, -0.068450, 0.013296, -0.018355, -0.051769, 0.009567, -0.286551, -0.261433, -0.304940, -0.998922, -0.999765, -0.998850, -0.996613, -0.999843, -0.978575, -0.073763, -0.052037, -0.012102, -0.045693, -0.059251, 0.034208, -0.061135, -0.072159, 0.029027, -0.093798, -0.058713, 0.019525, -0.109188, -0.044517, 0.013484], + "max": [ 0.162729, 0.092829, 0.113989, 1.000000, 0.355478, 0.210337, 0.344650, 1.000000, 0.169127, 0.412223, 0.342286, 0.258529, 1.000000, 0.995610, 0.977012, 0.998188, 1.000000, 0.981797, 0.053027, 0.143626, 0.196154, 0.048750, 0.110958, 0.216498, 0.071694, 0.109664, 0.226691, 0.093366, 0.106893, 0.219522, 0.104191, 0.094833, 0.192419, 0.288662, 0.238981, 0.266988, 1.000000, 0.999539, 0.994357, 0.999825, 1.000000, 0.999127, 0.180267, 0.152062, 0.183739, 0.098497, 0.114275, 0.216822, 0.064544, 0.107233, 0.233817, 0.021220, 0.107390, 0.233983, -0.000990, 0.096846, 0.206103], + "q01": [-0.026688, -0.024154, -0.013269, 0.996827, -0.056513, -0.023720, -0.055905, 0.996923, -0.026957, -0.048362, -0.040973, -0.040898, 0.869093, -0.316019, -0.240748, -0.327180, 0.879926, -0.213020, -0.080012, 0.029713, 0.059227, -0.038851, 0.010826, 0.062254, -0.007452, 0.010061, 0.047149, 0.005736, 0.006631, 0.036123, 0.018989, 0.001386, 0.033526, -0.037843, -0.037665, -0.036427, 0.901690, -0.278892, -0.200027, -0.267251, 0.906997, -0.187048, -0.013628, 0.024588, 0.053585, -0.012045, 0.000813, 0.065182, -0.037355, -0.001436, 0.052279, -0.058561, -0.004175, 0.042882, -0.078014, -0.007496, 0.039113], + "q99": [ 0.026573, 0.015575, 0.015514, 1.000000, 0.055889, 0.023992, 0.056601, 1.000000, 0.027849, 0.044917, 0.040128, 0.043628, 0.999997, 0.322927, 0.238815, 0.311405, 0.999997, 0.222693, 0.016559, 0.105274, 0.129951, 0.014097, 0.089857, 0.188346, 0.037904, 0.087388, 0.192120, 0.059029, 0.083990, 0.177890, 0.074478, 0.069913, 0.145329, 0.041177, 0.036800, 0.039655, 0.999998, 0.271390, 0.203172, 0.282521, 0.999998, 0.193682, 0.087733, 0.107510, 0.128318, 0.040024, 0.088640, 0.191875, 0.008532, 0.086990, 0.195138, -0.005823, 0.083213, 0.180562, -0.019596, 0.069257, 0.148124] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/libero_native_frame_wise_relative_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/libero_native_frame_wise_relative_rot6d.json new file mode 100644 index 00000000..6cde6705 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/libero_native_frame_wise_relative_rot6d.json @@ -0,0 +1,37 @@ +{ + "metadata": { + "embodiment_type": "libero", + "pose_convention": "frame_wise_relative", + "pose_coordinate_frame": "native", + "rotation_format": "6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": null, + "dataset_name": "libero", + "dataset_class": "LIBERODataset", + "dataset_root": ["outputs/libero_datasets/libero_10", "outputs/libero_datasets/libero_object", "outputs/libero_datasets/libero_spatial", "outputs/libero_datasets/libero_goal"], + "_comment": "Dataset paths are placeholders; the statistics values are independent of local dataset location.", + "split": "train", + "num_samples_stats": 10000, + "reservoir_size": 50000, + "max_samples": 10000, + "sampling_seed": 42 + }, + "global": { + "mean": [ 0.050704, 0.097407, -0.094833, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.476725], + "std": [ 0.333621, 0.387175, 0.457140, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.499460], + "min": [-0.937500, -0.937500, -0.937500, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.937500, 0.937500, 0.937500, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.723214, -0.808929, -0.937500, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.937500, 0.870536, 0.937500, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [ 0.050704, 0.097407, -0.094833, 0.994873, -0.004579, -0.004288, 0.004389, 0.996104, 0.001109, 0.476725], + "std": [ 0.333621, 0.387175, 0.457140, 0.010807, 0.077802, 0.063386, 0.078571, 0.009994, 0.038504, 0.499460], + "min": [-0.937500, -0.937500, -0.937500, 0.902028, -0.356085, -0.367416, -0.370434, 0.921907, -0.255000, 0.000000], + "max": [ 0.937500, 0.937500, 0.937500, 1.000000, 0.368853, 0.341214, 0.356395, 1.000000, 0.348251, 1.000000], + "q01": [-0.723214, -0.808929, -0.937500, 0.934955, -0.223431, -0.189878, -0.334735, 0.938516, -0.107736, 0.000000], + "q99": [ 0.937500, 0.870536, 0.937500, 1.000000, 0.331000, 0.163153, 0.226216, 1.000000, 0.127158, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka-dual_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka-dual_backward_framewise_rot6d.json new file mode 100644 index 00000000..ddf3b6f4 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka-dual_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "robomind-franka-dual", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 20, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8, 13, 14, 15, 16, 17, 18], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "robomind_franka_dual_20260414", + "dataset_class": "RoboMINDFrankaDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/RoboMIND_20251228", + "split": "train", + "num_samples_stats": 21410, + "reservoir_size": 5000000 + }, + "global": { + "mean": [ 0.000231, 0.000179, -0.000319, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.638652, 0.000148, -0.000377, -0.000241, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.815273], + "std": [ 0.014881, 0.008081, 0.014371, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.464058, 0.010628, 0.005868, 0.007900, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.366049], + "min": [-0.115093, -0.096415, -0.112595, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000, -0.091252, -0.052148, -0.113650, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.114941, 0.063433, 0.098721, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.123908, 0.077951, 0.080229, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999628], + "q01": [-0.051367, -0.031964, -0.046482, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000, -0.035108, -0.021212, -0.029788, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.043729, 0.021737, 0.036738, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.047581, 0.021270, 0.025712, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.995443] + }, + "global_raw": { + "mean": [ 0.000231, 0.000179, -0.000319, 0.999350, 0.000318, -0.000135, -0.000286, 0.999586, -0.000051, 0.638652, 0.000148, -0.000377, -0.000241, 0.999294, 0.000343, 0.000431, -0.000147, 0.999580, -0.000570, 0.815273], + "std": [ 0.014881, 0.008081, 0.014371, 0.002235, 0.020196, 0.029781, 0.020185, 0.001125, 0.020484, 0.464058, 0.010628, 0.005868, 0.007900, 0.002664, 0.025404, 0.027550, 0.025193, 0.001657, 0.014210, 0.366049], + "min": [-0.115093, -0.096415, -0.112595, 0.944314, -0.271877, -0.325264, -0.254808, 0.962274, -0.227188, 0.000000, -0.091252, -0.052148, -0.113650, 0.941406, -0.265241, -0.273484, -0.290840, 0.954990, -0.264631, 0.000000], + "max": [ 0.114941, 0.063433, 0.098721, 1.000000, 0.258475, 0.270230, 0.271943, 1.000000, 0.221936, 1.000000, 0.123908, 0.077951, 0.080229, 1.000000, 0.296517, 0.333596, 0.269131, 1.000000, 0.139695, 0.999628], + "q01": [-0.051367, -0.031964, -0.046482, 0.988101, -0.053179, -0.128603, -0.075432, 0.994427, -0.059973, 0.000000, -0.035108, -0.021212, -0.029788, 0.986086, -0.098043, -0.111441, -0.093441, 0.991492, -0.058030, 0.000000], + "q99": [ 0.043729, 0.021737, 0.036738, 1.000000, 0.075612, 0.102791, 0.053223, 1.000000, 0.077057, 1.000000, 0.047581, 0.021270, 0.025712, 1.000000, 0.095525, 0.126049, 0.098778, 1.000000, 0.041914, 0.995443] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka_backward_framewise_rot6d.json new file mode 100644 index 00000000..9f5d5bfc --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-franka_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "robomind-franka", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "robomind_franka_20260414", + "dataset_class": "RoboMINDFrankaDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/RoboMIND_20251228", + "split": "train", + "num_samples_stats": 141658, + "reservoir_size": 5000000 + }, + "global": { + "mean": [ 0.000241, 0.000073, -0.000597, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.630501], + "std": [ 0.020545, 0.010725, 0.022054, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.434021], + "min": [-0.184377, -0.130924, -0.183947, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.227682, 0.134118, 0.133222, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.065029, -0.030683, -0.075321, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.068546, 0.036309, 0.051772, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [ 0.000241, 0.000073, -0.000597, 0.998782, 0.000605, 0.000003, -0.000592, 0.998245, -0.000508, 0.630501], + "std": [ 0.020545, 0.010725, 0.022054, 0.004056, 0.043101, 0.023671, 0.043102, 0.004948, 0.040306, 0.434021], + "min": [-0.184377, -0.130924, -0.183947, 0.837403, -0.525301, -0.384252, -0.543663, 0.801190, -0.490979, 0.000000], + "max": [ 0.227682, 0.134118, 0.133222, 1.000000, 0.543800, 0.389145, 0.522029, 1.000000, 0.414190, 1.000000], + "q01": [-0.065029, -0.030683, -0.075321, 0.981664, -0.137429, -0.069593, -0.140220, 0.976885, -0.140399, 0.000000], + "q99": [ 0.068546, 0.036309, 0.051772, 1.000000, 0.140290, 0.079942, 0.137529, 1.000000, 0.113651, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-ur_backward_framewise_rot6d.json b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-ur_backward_framewise_rot6d.json new file mode 100644 index 00000000..d76d80bb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/robomind-ur_backward_framewise_rot6d.json @@ -0,0 +1,33 @@ +{ + "metadata": { + "embodiment_type": "robomind-ur", + "pose_convention": "backward_framewise", + "rotation_format": "rot6d", + "action_dim": 10, + "skip_rotation_dims": [3, 4, 5, 6, 7, 8], + "chunk_length": 16, + "sample_stride": 16, + "dataset_name": "robomind_ur_20260501", + "dataset_class": "RoboMINDURDataset", + "dataset_root": "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/RoboMIND_20251228", + "split": "train", + "num_samples_stats": 211078, + "reservoir_size": 5000000 + }, + "global": { + "mean": [-0.000470, 0.000182, -0.000724, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.688744], + "std": [ 0.014480, 0.011243, 0.013583, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.371170], + "min": [-0.204206, -0.132684, -0.164591, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "max": [ 0.400660, 0.354529, 0.098898, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000], + "q01": [-0.050087, -0.032055, -0.045222, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, -1.000000, 0.000000], + "q99": [ 0.040385, 0.036577, 0.032376, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000] + }, + "global_raw": { + "mean": [-0.000470, 0.000182, -0.000724, 0.998910, 0.001998, 0.000529, -0.001703, 0.998614, -0.000210, 0.688744], + "std": [ 0.014480, 0.011243, 0.013583, 0.003004, 0.037484, 0.027562, 0.037337, 0.003758, 0.036869, 0.371170], + "min": [-0.204206, -0.132684, -0.164591, -0.202474, -0.937542, -0.284120, -0.671888, -0.110612, -0.442271, 0.000000], + "max": [ 0.400660, 0.354529, 0.098898, 1.000000, 0.707452, 0.399441, 0.944798, 1.000000, 0.398811, 1.000000], + "q01": [-0.050087, -0.032055, -0.045222, 0.986554, -0.114343, -0.074432, -0.128455, 0.982298, -0.132509, 0.000000], + "q99": [ 0.040385, 0.036577, 0.032376, 1.000000, 0.129744, 0.095558, 0.115080, 1.000000, 0.121987, 1.000000] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_anchored_normalizer.json b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_anchored_normalizer.json new file mode 100644 index 00000000..2c68ef64 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_anchored_normalizer.json @@ -0,0 +1,85 @@ +{ + "robot0_main_camera": { + "type": "identity", + "scale": 1.0, + "offset": 0.0 + }, + "camera_poses": { + "type": "clamped_range", + "scale": [ + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0 + ] + }, + "eef_poses": { + "type": "clamped_range", + "scale": [ + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0 + ] + }, + "eef_commands": { + "type": "clamped_range", + "scale": [ + 0.05000010132789612 + ], + "offset": [ + 0.05000000074505806 + ] + }, + "grasp_states": { + "type": "clamped_range", + "scale": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_no_rot_normalizer.json b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_no_rot_normalizer.json new file mode 100644 index 00000000..2cdaaa9d --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_no_rot_normalizer.json @@ -0,0 +1,85 @@ +{ + "robot0_main_camera": { + "type": "identity", + "scale": 1.0, + "offset": 0.0 + }, + "camera_poses": { + "type": "clamped_range", + "scale": [ + 0.08000008940696716, + 0.08000008940696716, + 0.08000008940696716, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "eef_poses": { + "type": "clamped_range", + "scale": [ + 0.08000008940696716, + 0.08000008940696716, + 0.08000008940696716, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "eef_commands": { + "type": "clamped_range", + "scale": [ + 0.10000010132789612 + ], + "offset": [ + 0.10000000074505806 + ] + }, + "grasp_states": { + "type": "clamped_range", + "scale": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_normalizer.json b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_normalizer.json new file mode 100644 index 00000000..3674e261 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_frame_wise_normalizer.json @@ -0,0 +1,85 @@ +{ + "robot0_main_camera": { + "type": "identity", + "scale": 1.0, + "offset": 0.0 + }, + "camera_poses": { + "type": "clamped_range", + "scale": [ + 0.08000008940696716, + 0.08000008940696716, + 0.08000008940696716, + 0.0050000849366188, + 0.10000008940696716, + 0.10000008940696716, + 0.10000008940696716, + 0.0050000849366188, + 0.10000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.995000011920929, + 0.0, + 0.0, + 0.0, + 0.995000011920929, + 0.0 + ] + }, + "eef_poses": { + "type": "clamped_range", + "scale": [ + 0.08000008940696716, + 0.08000008940696716, + 0.08000008940696716, + 0.0050000849366188, + 0.10000008940696716, + 0.10000008940696716, + 0.10000008940696716, + 0.0050000849366188, + 0.10000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.995000011920929, + 0.0, + 0.0, + 0.0, + 0.995000011920929, + 0.0 + ] + }, + "eef_commands": { + "type": "clamped_range", + "scale": [ + 0.10000010132789612 + ], + "offset": [ + 0.10000000074505806 + ] + }, + "grasp_states": { + "type": "clamped_range", + "scale": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_normalizer.json b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_normalizer.json new file mode 100644 index 00000000..2c68ef64 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/normalizers/uva_umi_single_task_normalizer.json @@ -0,0 +1,85 @@ +{ + "robot0_main_camera": { + "type": "identity", + "scale": 1.0, + "offset": 0.0 + }, + "camera_poses": { + "type": "clamped_range", + "scale": [ + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0 + ] + }, + "eef_poses": { + "type": "clamped_range", + "scale": [ + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716, + 0.25000008940696716, + 0.25000008940696716, + 0.0750000849366188, + 0.25000008940696716 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0, + 0.0, + 0.0, + 0.925000011920929, + 0.0 + ] + }, + "eef_commands": { + "type": "clamped_range", + "scale": [ + 0.05000010132789612 + ], + "offset": [ + 0.05000000074505806 + ] + }, + "grasp_states": { + "type": "clamped_range", + "scale": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ], + "offset": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/cosmos_training/cosmos/data/vfm/action/pose_utils.py b/cosmos_training/cosmos/data/vfm/action/pose_utils.py new file mode 100644 index 00000000..8bcedd67 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/pose_utils.py @@ -0,0 +1,759 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rotation and pose utilities for action datasets. + +This module centralizes three related responsibilities used across the action +dataset stack: + +1. Converting rotations between the conventions used by the datasets and the + action model (`euler_xyz`, quaternion, axis-angle, rot6d, rot9d, matrix). +2. Building absolute homogeneous poses of shape ``(T, 4, 4)`` from per-frame + translation and rotation components. +3. Converting trajectories between absolute-pose form and the relative-pose + action vectors consumed by the datasets. + + The relative-pose action vectors always follow the shared layout + ``[translation(3), rotation(...)]``. The rotation block is encoded with the + requested rotation output convention, and `convert_rotation()` is the + canonical public entrypoint for representation conversion. +""" + +import math +from typing import Literal + +import numpy as np +import torch +from scipy.spatial.transform import Rotation as R + +PoseConvention = Literal["absolute", "backward_anchored", "backward_framewise"] +RotationConvention = Literal["matrix", "euler_xyz", "quat_xyzw", "quat_wxyz", "rot6d", "axisangle", "rot9d"] + + +def _to_numpy_float32(array: torch.Tensor | np.ndarray) -> np.ndarray: + """Convert an input array to a NumPy ``float32`` array. + + Args: + array: A torch tensor or NumPy array with arbitrary leading dimensions. + + Returns: + A NumPy array with dtype ``float32``. Torch tensors are moved to CPU + before conversion. NumPy inputs are converted with ``copy=False`` + semantics when possible. + + Raises: + ValueError: If a torch tensor with ``requires_grad=True`` is passed. + These utilities are non-differentiable; callers must explicitly + detach tensors before conversion. + """ + if isinstance(array, torch.Tensor): + if array.requires_grad: + raise ValueError( + "pose_utils conversion is non-differentiable; call `.detach()` " + "explicitly before passing tensors with requires_grad=True" + ) + return array.cpu().numpy().astype(np.float32, copy=False) + return np.asarray(array, dtype=np.float32) + + +def _normalize_rotation_matrices(rot_matrices: np.ndarray) -> np.ndarray: + """Project approximate matrices onto valid rotation matrices. + + This helper uses an SVD-based projection onto ``SO(3)``. It is mainly used + when decoding rotations from network-like representations such as rot6d or rot9d + where the input may not already be perfectly orthonormal. + + Args: + rot_matrices: Array of shape ``(..., 3, 3)`` containing one or more + approximate rotation matrices. + + Returns: + Array of shape ``(..., 3, 3)`` whose trailing matrices are proper + rotation matrices with determinant ``+1``. + + Raises: + ValueError: If the input does not have trailing shape ``(3, 3)``. + """ + matrices = np.asarray(rot_matrices, dtype=np.float32) + if matrices.ndim < 2 or matrices.shape[-2:] != (3, 3): + raise ValueError(f"Rotation matrices must have shape (..., 3, 3), got {matrices.shape}") + + original_shape = matrices.shape[:-2] + matrices_flat = matrices.reshape(-1, 3, 3) + + # Batched SVD projection to SO(3). + U, _, Vt = np.linalg.svd(matrices_flat) + normalized = U @ Vt + + # Ensure determinant is +1 (proper rotations, no reflections). + det = np.linalg.det(normalized) + reflection_mask = det < 0 + if np.any(reflection_mask): + U_reflect = U.copy() + U_reflect[reflection_mask, :, -1] *= -1 + normalized[reflection_mask] = U_reflect[reflection_mask] @ Vt[reflection_mask] + + return normalized.astype(np.float32, copy=False).reshape(*original_shape, 3, 3) + + +def convert_rotation( + rotation: torch.Tensor | np.ndarray, + input_format: RotationConvention, + output_format: RotationConvention, + normalize_matrix: bool = False, +) -> torch.Tensor | np.ndarray: + """Convert rotations between the conventions used by action datasets. + + The function first maps the input representation to rotation matrices and + then emits the requested output convention. It is the single conversion seam + used by the public pose helpers so that all code paths share the same + convention handling. + + Supported input conventions: + - ``matrix``: rotation matrices with shape ``(..., 3, 3)`` + - ``euler_xyz``: Euler xyz angles in radians with shape ``(..., 3)`` + - ``quat_xyzw``: quaternions in SciPy's xyzw order with shape ``(..., 4)`` + - ``quat_wxyz``: quaternions in wxyz order with shape ``(..., 4)`` + - ``rot6d``: column-based 6D representation with shape ``(..., 6)`` + - ``rot9d``: flattened rotation matrices with shape ``(..., 9)`` + - ``axisangle``: axis-angle vectors with shape ``(..., 3)`` + + Supported output conventions: + - ``matrix`` + - ``euler_xyz`` + - ``quat_xyzw`` + - ``quat_wxyz`` + - ``rot6d`` + - ``axisangle`` + - ``rot9d`` + + Args: + rotation: Input rotations in the representation specified by + ``input_format``. + input_format: Convention used by ``rotation``. + output_format: Convention to return. + normalize_matrix: Whether to project intermediate matrices to a valid + rotation before returning. This is most useful when decoding from + approximate ``rot6d``/``rot9d`` inputs or non-unit quaternions. + + Returns: + Rotations with the same leading shape as the input, expressed in the + requested output convention. Torch inputs return torch outputs on the + same device with the same dtype; NumPy inputs return NumPy arrays. + + Raises: + ValueError: If the input shape is incompatible with ``input_format`` or + if either format is unsupported. + """ + input_is_tensor = isinstance(rotation, torch.Tensor) + input_dtype = rotation.dtype if input_is_tensor else None + input_device = rotation.device if input_is_tensor else None + rotation_np = _to_numpy_float32(rotation) + + if input_format == "matrix": + if rotation_np.ndim < 2 or rotation_np.shape[-2:] != (3, 3): + raise ValueError(f"matrix rotation must have shape (..., 3, 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-2] + matrices_flat = rotation_np.reshape(-1, 3, 3) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "euler_xyz": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 3: + raise ValueError(f"{input_format} rotation must have shape (..., 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = R.from_euler("xyz", rotation_np.reshape(-1, 3), degrees=False).as_matrix().astype(np.float32) + elif input_format in ("quat_xyzw", "quat_wxyz"): + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 4: + raise ValueError(f"{input_format} rotation must have shape (..., 4), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + quaternions = rotation_np.reshape(-1, 4) + if input_format == "quat_wxyz": + quaternions = quaternions[:, [1, 2, 3, 0]] + norms = np.linalg.norm(quaternions, axis=-1) + if np.any(norms < 1e-8): + raise ValueError(f"Found zero-norm quaternion(s) (min norm={norms.min():.2e}).") + if normalize_matrix: + quaternions = quaternions / norms[:, None] + matrices_flat = R.from_quat(quaternions).as_matrix().astype(np.float32) + elif input_format == "rot6d": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 6: + raise ValueError(f"{input_format} rotation must have shape (..., 6), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + rot6d_flat = rotation_np.reshape(-1, 6) + col0 = rot6d_flat[:, :3] + col1 = rot6d_flat[:, 3:] + col2 = np.cross(col0, col1, axis=-1) + matrices_flat = np.stack((col0, col1, col2), axis=-1).astype(np.float32) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "rot9d": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 9: + raise ValueError(f"rot9d rotation must have shape (..., 9), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = rotation_np.reshape(-1, 3, 3) + if normalize_matrix: + matrices_flat = _normalize_rotation_matrices(matrices_flat).reshape(-1, 3, 3) + elif input_format == "axisangle": + if rotation_np.ndim < 1 or rotation_np.shape[-1] != 3: + raise ValueError(f"axisangle rotation must have shape (..., 3), got {rotation_np.shape}") + original_shape = rotation_np.shape[:-1] + matrices_flat = R.from_rotvec(rotation_np.reshape(-1, 3)).as_matrix().astype(np.float32) + else: + raise ValueError(f"Unsupported input_format: {input_format!r}") + + if output_format == "matrix": + converted = matrices_flat.reshape(*original_shape, 3, 3).astype(np.float32) + elif output_format == "rot9d": + converted = matrices_flat.reshape(-1, 9) + elif output_format == "rot6d": + converted = matrices_flat[:, :, :2].transpose(0, 2, 1).reshape(-1, 6) + elif output_format == "quat_xyzw": + converted = R.from_matrix(matrices_flat).as_quat().astype(np.float32) + elif output_format == "quat_wxyz": + converted = R.from_matrix(matrices_flat).as_quat().astype(np.float32) + converted = converted[:, [3, 0, 1, 2]] + elif output_format == "euler_xyz": + converted = R.from_matrix(matrices_flat).as_euler("xyz", degrees=False).astype(np.float32) + elif output_format == "axisangle": + converted = R.from_matrix(matrices_flat).as_rotvec().astype(np.float32) + else: + raise ValueError(f"Unsupported output_format: {output_format!r}") + + if output_format != "matrix": + converted = converted.reshape(*original_shape, converted.shape[-1]) + + if input_is_tensor: + return torch.from_numpy(np.ascontiguousarray(converted)).to(dtype=input_dtype, device=input_device) + return converted + + +# ----------------------------------------------------------------------------- +# Absolute pose construction +# ----------------------------------------------------------------------------- + + +def build_abs_pose_from_components( + xyz: torch.Tensor | np.ndarray, + rotation: torch.Tensor | np.ndarray, + rotation_input_format: Literal["euler_xyz", "quat_xyzw", "quat_wxyz", "axisangle"], + translation_scale: float | None = None, +) -> np.ndarray: + """Build absolute homogeneous poses from per-frame translation and rotation. + + This is the canonical helper for turning dataset-provided pose components + into a sequence of rigid transforms. Each output pose is a homogeneous + transform whose top-left ``3 x 3`` block stores rotation and whose last + column stores translation. + + Args: + xyz: Per-frame translations with shape ``(T, 3)``. + rotation: Per-frame rotations with shape ``(T, 3)`` for ``euler_xyz`` + and ``axisangle``, or ``(T, 4)`` for quaternion conventions. + rotation_input_format: Convention used by ``rotation``. Supported values + are ``euler_xyz``, ``quat_xyzw``, ``quat_wxyz``, and ``axisangle``. + translation_scale: Optional factor used to divide translations before + inserting them into the output poses. This is useful when upstream + data stores translations in a scaled unit. + + Returns: + Absolute poses with shape ``(T, 4, 4)`` and dtype ``float32``. + + Raises: + ValueError: If the translation and rotation arrays have incompatible + lengths or unsupported shapes, or if ``translation_scale`` is zero. + """ + xyz_np = _to_numpy_float32(xyz) + rotation_np = _to_numpy_float32(rotation) + + if xyz_np.ndim != 2 or xyz_np.shape[1] != 3: + raise ValueError(f"xyz must have shape (T, 3), got {xyz_np.shape}") + if rotation_np.ndim != 2: + raise ValueError(f"rotation must be 2D, got {rotation_np.shape}") + if rotation_np.shape[0] != xyz_np.shape[0]: + raise ValueError( + f"xyz and rotation must have the same length, got {xyz_np.shape[0]} and {rotation_np.shape[0]}" + ) + + rot_mats = np.asarray( + convert_rotation(rotation_np, input_format=rotation_input_format, output_format="matrix"), + dtype=np.float32, + ) + + if translation_scale is not None: + if translation_scale == 0: + raise ValueError("translation_scale must be non-zero") + xyz_np = xyz_np / float(translation_scale) + + poses_abs = np.eye(4, dtype=np.float32)[None].repeat(xyz_np.shape[0], axis=0) + poses_abs[:, :3, :3] = rot_mats.astype(np.float32) + poses_abs[:, :3, 3] = xyz_np + return poses_abs + + +# ----------------------------------------------------------------------------- +# Relative pose conversions +# ----------------------------------------------------------------------------- + + +def _delta_transform_to_pose_vector( + delta_T: np.ndarray, + rotation_output_format: RotationConvention, + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Encode a relative transform as an action vector. + + The shared action-vector layout is always ``[translation(3), rotation(...)]``. + The translation block is multiplied by ``translation_scale`` before concatenation, + and the rotation block is multiplied by ``rotation_scale``. + + Args: + delta_T: Relative transform of shape ``(4, 4)``. + rotation_output_format: Concrete convention used for the output rotation + block. + translation_scale: Scalar multiplier applied to the translation block. + rotation_scale: Scalar multiplier applied to the rotation block. Used to + match the loss scale of the rotation block to the translation block. + The decoder must divide by the same factor before reconstructing the + rotation matrix. + + Returns: + A ``float32`` action vector whose first three values are translation and + whose remaining values are the rotation in ``rotation_output_format``. + """ + delta_np = np.asarray(delta_T, dtype=np.float32) + if delta_np.shape != (4, 4): + raise ValueError(f"delta_T must have shape (4, 4), got {delta_np.shape}") + + translation = delta_np[:3, 3] * translation_scale + rotation = np.asarray( + convert_rotation(delta_np[:3, :3], input_format="matrix", output_format=rotation_output_format), + dtype=np.float32, + ) + rotation = rotation * rotation_scale + return np.concatenate([translation, rotation]).astype(np.float32) + + +def _pose_vector_to_delta_transform( + pose_vector: np.ndarray, + rotation_input_format: RotationConvention, + translation_scale: float, + normalize_rotation: bool, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Decode an action vector back into a relative homogeneous transform. + + This is the inverse of `_delta_transform_to_pose_vector()` when the same + rotation convention and scale are used. + + Args: + pose_vector: Relative-pose action vector with layout + ``[translation(3), rotation(...)]``. + rotation_input_format: Concrete convention used by the rotation block. + translation_scale: Scalar used to undo the translation scaling applied during + encoding. + normalize_rotation: Whether to project the decoded rotation to a valid + matrix before assembling the transform. + rotation_scale: Scalar used to undo the rotation scaling applied during + encoding. Must match the value used by + `_delta_transform_to_pose_vector()`. + + Returns: + A relative homogeneous transform with shape ``(4, 4)`` and dtype + ``float32``. + """ + pose_vector_np = np.asarray(pose_vector, dtype=np.float32) + rotation_block = pose_vector_np[3:] / rotation_scale + + rotation_matrix = np.asarray( + convert_rotation( + rotation_block, + input_format=rotation_input_format, + output_format="matrix", + normalize_matrix=normalize_rotation, + ), + dtype=np.float32, + ) + + delta_T = np.eye(4, dtype=np.float32) + delta_T[:3, 3] = pose_vector_np[:3] / translation_scale + delta_T[:3, :3] = rotation_matrix + return delta_T + + +def _get_relative_delta_transform( + poses_abs: np.ndarray, + inv_poses_abs: np.ndarray, + frame_idx: int, + pose_convention: PoseConvention, +) -> np.ndarray: + """Compute one relative transform from an absolute-pose trajectory. + + Args: + poses_abs: Absolute poses of shape ``(T, 4, 4)``. + inv_poses_abs: Precomputed inverses of ``poses_abs`` with the same shape. + frame_idx: Index of the step to encode, in ``[0, T - 2]``. + pose_convention: Pose convention controlling which two poses + define the delta and whether it is framewise or anchored. + + Returns: + The relative transform ``delta_T`` with shape ``(4, 4)`` for the + requested step and convention. + """ + if pose_convention == "backward_framewise": + return inv_poses_abs[frame_idx] @ poses_abs[frame_idx + 1] + if pose_convention == "backward_anchored": + return inv_poses_abs[0] @ poses_abs[frame_idx + 1] + raise ValueError( + f"Unsupported pose_convention={pose_convention!r}. Expected one of: backward_framewise, backward_anchored." + ) + + +def _apply_relative_delta_transform( + current_pose: np.ndarray, + initial_pose: np.ndarray, + delta_T: np.ndarray, + pose_convention: PoseConvention, +) -> np.ndarray: + """Recover the next absolute pose from a decoded relative transform. + + Args: + current_pose: The current reconstructed pose for framewise modes. + initial_pose: The anchor pose used by anchored modes. + delta_T: Relative transform for the current step. + pose_convention: Pose convention controlling how ``delta_T`` + should be composed back into an absolute pose. + + Returns: + The next absolute pose with shape ``(4, 4)``. + """ + if pose_convention == "backward_framewise": + return current_pose @ delta_T + if pose_convention == "backward_anchored": + return initial_pose @ delta_T + raise ValueError( + f"Unsupported pose_convention={pose_convention!r}. Expected one of: backward_framewise, backward_anchored." + ) + + +def pose_abs_to_rel( + poses_abs: np.ndarray, + rotation_format: RotationConvention = "rot9d", + pose_convention: PoseConvention = "backward_framewise", + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Convert an absolute-pose trajectory into relative-pose action vectors. + + Args: + poses_abs: Absolute poses with shape ``(T, 4, 4)``. These are typically + object-in-world or camera-to-world transforms. + rotation_format: Rotation convention used for the output rotation block. + Supported values are ``rot9d``, ``rot6d``, ``quat_xyzw``, and + ``euler_xyz``. + pose_convention: Pose convention: + - ``backward_framewise``: ``delta_T = T_i^{-1} @ T_{i+1}`` + - ``backward_anchored``: ``delta_T = T_0^{-1} @ T_{i+1}`` + translation_scale: Scalar multiplier applied to the translation block of each + encoded action vector. + rotation_scale: Scalar multiplier applied to the rotation block of each + encoded action vector. Use this to match the loss scale of rotation + and translation. `pose_rel_to_abs()` must be called with the same + value to invert the scaling. + + Returns: + An array of shape ``(T - 1, D)`` where ``D = 3 + rotation_dim``. + + Raises: + AssertionError: If fewer than two absolute poses are provided. + """ + num_frames = len(poses_abs) + assert num_frames > 1, "At least 2 frames are required to compute relative poses" + + # Compute inverse poses + inv_poses_abs = np.linalg.inv(poses_abs) + + poses_rel = [] + # We produce num_frames - 1 relative poses + for i in range(num_frames - 1): + delta_T = _get_relative_delta_transform(poses_abs, inv_poses_abs, i, pose_convention) + poses_rel.append( + _delta_transform_to_pose_vector( + delta_T, + rotation_output_format=rotation_format, + translation_scale=translation_scale, + rotation_scale=rotation_scale, + ) + ) + + return np.stack(poses_rel).astype(np.float32) # [T-1,D] + + +def pose_rel_to_abs( + poses_rel: np.ndarray, + rotation_format: RotationConvention = "rot9d", + pose_convention: PoseConvention = "backward_framewise", + initial_pose: np.ndarray | None = None, + normalize_rotation: bool = True, + translation_scale: float = 1.0, + rotation_scale: float = 1.0, +) -> np.ndarray: + """Reconstruct an absolute-pose trajectory from relative-pose action vectors. + + Args: + poses_rel: Relative-pose action vectors with shape ``(T - 1, D)`` and + layout ``[translation(3), rotation(...)]``. + rotation_format: Convention used by the rotation block of ``poses_rel``. + pose_convention: Pose convention used when the vectors were + encoded. This must match the convention passed to `pose_abs_to_rel()`. + initial_pose: Absolute pose for the first frame. If ``None``, the + identity transform is used. + normalize_rotation: Whether to project decoded rotations onto ``SO(3)`` + before composing them back into the trajectory. + translation_scale: Scalar used to undo the translation scaling applied during + `pose_abs_to_rel()`. + rotation_scale: Scalar used to undo the rotation scaling applied during + `pose_abs_to_rel()`. Must match the value passed there. + + Returns: + Absolute poses with shape ``(T, 4, 4)`` where ``T = len(poses_rel) + 1``. + """ + if initial_pose is None: + initial_pose = np.eye(4) + + poses_abs = [initial_pose] + current_pose = initial_pose + + num_poses_rel = poses_rel.shape[0] + + for i in range(num_poses_rel): + delta_T = _pose_vector_to_delta_transform( + poses_rel[i], + rotation_input_format=rotation_format, + translation_scale=translation_scale, + normalize_rotation=normalize_rotation, + rotation_scale=rotation_scale, + ) + next_pose = _apply_relative_delta_transform(current_pose, initial_pose, delta_T, pose_convention) + + poses_abs.append(next_pose) + current_pose = next_pose + + return np.stack(poses_abs) # [T,4,4] + + +# ----------------------------------------------------------------------------- +# Idle-frame detection +# ----------------------------------------------------------------------------- + + +def _identity_rotation_vector(rotation_format: RotationConvention) -> np.ndarray: + """Return the identity-rotation vector for a given rotation convention. + + Used by :func:`compute_idle_frames` to test whether a rotation block is + close to "no rotation" in its current encoding. + """ + if rotation_format in ("matrix", "rot9d"): + return np.array([1, 0, 0, 0, 1, 0, 0, 0, 1], dtype=np.float32) + if rotation_format == "rot6d": + return np.array([1, 0, 0, 0, 1, 0], dtype=np.float32) + if rotation_format == "quat_xyzw": + return np.array([0, 0, 0, 1], dtype=np.float32) + if rotation_format == "quat_wxyz": + return np.array([1, 0, 0, 0], dtype=np.float32) + if rotation_format in ("euler_xyz", "axisangle"): + return np.array([0, 0, 0], dtype=np.float32) + raise ValueError(f"Unsupported rotation_format={rotation_format!r}") + + +def _rotation_angle_per_arm(rotations: np.ndarray, rotation_format: str) -> np.ndarray: + """Geodesic angle (rad) from identity for each arm at each frame. + + ``rotations`` has shape ``(T, n_arms, n_per_arm)``; the returned array has + shape ``(T, n_arms)``. The angle is rotation-format aware so a fixed + ``eps_r`` threshold has consistent geometric meaning across formats: + + - ``rot6d`` → reconstruct ``trace(R)`` in closed form from the two stored + columns ``a, b`` (already unit-orthogonal as they came from a valid + rotation matrix). The third column is ``a × b``, so + ``trace(R) = a[0] + b[1] + a[0]·b[1] - a[1]·b[0]``. + ``angle = arccos(clip((trace - 1) / 2, -1, 1))``. + - ``rot9d`` → reshape to ``(..., 3, 3)`` and use + ``trace(R) = R[0,0] + R[1,1] + R[2,2]``. + - ``quat_xyzw`` / ``quat_wxyz`` → ``angle = 2 · arccos(|q_w|)``; the + absolute value handles the double cover (``q`` and ``-q`` represent the + same rotation). + - ``axisangle`` → the magnitude of the axis-angle vector *is* the angle. + - ``euler_xyz`` → no closed-form angle; use ``‖euler‖`` as a conservative + upper bound (exact for single-axis rotations, an overestimate for + composed ones — fine for idle detection where small angles are the + regime of interest). + """ + if rotation_format == "rot6d": + a = rotations[..., :3] + b = rotations[..., 3:6] + trace = a[..., 0] + b[..., 1] + a[..., 0] * b[..., 1] - a[..., 1] * b[..., 0] + return np.arccos(np.clip((trace - 1.0) / 2.0, -1.0, 1.0)) + if rotation_format == "rot9d": + mat = rotations.reshape(*rotations.shape[:-1], 3, 3) + trace = mat[..., 0, 0] + mat[..., 1, 1] + mat[..., 2, 2] + return np.arccos(np.clip((trace - 1.0) / 2.0, -1.0, 1.0)) + if rotation_format in ("quat_xyzw", "quat_wxyz"): + qw = rotations[..., 3] if rotation_format == "quat_xyzw" else rotations[..., 0] + return 2.0 * np.arccos(np.clip(np.abs(qw), 0.0, 1.0)) + if rotation_format == "axisangle": + return np.linalg.norm(rotations, axis=-1) + if rotation_format == "euler_xyz": + # Exact for single-axis rotations, overestimate for composed ones — + # safe for idle thresholds since overestimation can only mark a frame + # as non-idle, never spuriously idle. + return np.linalg.norm(rotations, axis=-1) + raise ValueError(f"Unsupported rotation_format={rotation_format!r}") + + +def _consecutive_streaks(idle: np.ndarray, min_streak: int) -> np.ndarray: + """Zero out idle bits not belonging to a run of ``>= min_streak`` Trues. + + Pure-numpy two-pointer scan. ``min_streak <= 1`` is a no-op (returns the + input mask unchanged). + """ + if min_streak <= 1: + return idle + out = np.zeros_like(idle) + n = len(idle) + i = 0 + while i < n: + if not idle[i]: + i += 1 + continue + j = i + while j < n and idle[j]: + j += 1 + if j - i >= min_streak: + out[i:j] = True + i = j + return out + + +def compute_idle_frames( + action_raw: torch.Tensor | np.ndarray, + spec: "ActionSpec", # noqa: F821 — forward ref, real import is in action_spec.py + *, + eps_t: float = 1e-3, + eps_r: float = math.radians(5.0), + eps_g: float = 1e-2, + joint_threshold: float = 5e-4, + min_streak: int = 3, +) -> int: + """Count idle frames in a raw (un-normalized) action chunk. + + Idle detection runs per-DimType (driven by ``spec.types``); a frame is + *raw-idle* iff every relevant type group is idle on that frame, and + counts toward the final tally only if it belongs to a run of at least + ``min_streak`` consecutive raw-idle frames. The streak filter rejects + isolated low-motion frames (instantaneous slowdowns) which carry weak + physical meaning and add noise to the IdleFrames training signal. + + DimType branches: + + - ``POS`` → combined ``‖action[pos_idx]‖`` (L2 across all POS dims) + < ``eps_t``. For single-arm specs (3 dims) this is the standard ``‖t‖`` + check; for multi-arm specs the combined norm is slightly stricter than + a per-arm check. + - ``ROT`` → per-arm geodesic rotation angle (rad) from identity + < ``eps_r``. The angle is computed in a rotation-format aware way (see + :func:`_rotation_angle_per_arm`) so the threshold has consistent + geometric meaning regardless of the encoding. + - ``GRIPPER`` → ``max |action[t] - action[t-1]| < eps_g``. ``np.diff`` + with ``prepend=action[0]`` makes step 0 ``|0|`` (treated as "no change"); + with the streak filter this can no longer create a spurious single-frame + idle event. + - ``JOINT`` → same frame-diff scheme as gripper with + ``joint_threshold`` (rad / step). + - ``RESERVED`` → ignored. + + Defaults (in the units of the un-normalized action): + + - ``eps_t = 1e-3`` → 1 mm per-frame translation + - ``eps_r = 5°`` → 5° per-frame rotation (geodesic angle) + - ``eps_g = 1e-2`` → 1 % gripper command change + - ``joint_threshold = 5e-4`` → ~0.03° / step joint angle change + - ``min_streak = 3`` → require a run of >= 3 consecutive idle frames + + The input must be **un-normalized** so the identity transform sits at + known coordinates (translation ≈ 0, rotation ≈ identity). The action + vector is also assumed to be encoded in a per-step / framewise convention + (e.g. ``backward_framewise``); anchored conventions (``backward_anchored``) + accumulate over the chunk and would silently break the POS/ROT idle + checks. Callers (e.g. the LeRobot base class) gate on pose convention + before calling this function. + """ + if isinstance(action_raw, torch.Tensor): + action = action_raw.detach().cpu().numpy().astype(np.float32, copy=False) + else: + action = np.asarray(action_raw, dtype=np.float32) + + if action.ndim != 2: + raise ValueError(f"action_raw must be 2-D (T, D); got shape {action.shape}") + num_frames, action_dim = action.shape + if num_frames == 0: + return 0 + if action_dim != len(spec.types): + raise ValueError(f"action_dim={action_dim} does not match spec.dim={len(spec.types)}") + + # Import locally to avoid a circular import at module load time + # (action_spec.py imports RotationConvention from this file). + from cosmos.data.vfm.action.action_spec import DimType + + pos_idx = [i for i, t in enumerate(spec.types) if t == DimType.POS] + rot_idx = [i for i, t in enumerate(spec.types) if t == DimType.ROT] + grip_idx = [i for i, t in enumerate(spec.types) if t == DimType.GRIPPER] + joint_idx = [i for i, t in enumerate(spec.types) if t == DimType.JOINT] + + idle = np.ones(num_frames, dtype=bool) + + # POS: combined L2 norm across all translation dims. + if pos_idx: + idle &= np.linalg.norm(action[:, pos_idx], axis=1) < eps_t + + # ROT: per-arm geodesic angle (rad). + if rot_idx: + rot_id = _identity_rotation_vector(spec.rotation_format) + n_per_arm = rot_id.shape[0] + if len(rot_idx) % n_per_arm != 0: + raise ValueError( + f"ROT dims ({len(rot_idx)}) not a multiple of " + f"rotation_format={spec.rotation_format!r} dim ({n_per_arm})" + ) + rotations = action[:, rot_idx].reshape(num_frames, -1, n_per_arm) + angles = _rotation_angle_per_arm(rotations, spec.rotation_format) # (T, n_arms) + idle &= angles.max(axis=1) < eps_r + + # GRIPPER: max |Δgripper| across all gripper dims; step 0's diff is 0. + if grip_idx: + gripper = action[:, grip_idx] + diff = np.abs(np.diff(gripper, axis=0, prepend=gripper[:1])) + idle &= diff.max(axis=1) < eps_g + + # JOINT: same frame-diff scheme with joint_threshold. + if joint_idx: + joints = action[:, joint_idx] + diff = np.abs(np.diff(joints, axis=0, prepend=joints[:1])) + idle &= diff.max(axis=1) < joint_threshold + + if min_streak > 1: + idle = _consecutive_streaks(idle, min_streak) + + return int(idle.sum()) diff --git a/cosmos_training/cosmos/data/vfm/action/pose_utils_test.py b/cosmos_training/cosmos/data/vfm/action/pose_utils_test.py new file mode 100644 index 00000000..eaa0e8e6 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/pose_utils_test.py @@ -0,0 +1,206 @@ +import numpy as np +import pytest +import torch +from scipy.spatial.transform import Rotation as R + +from cosmos.data.vfm.action.pose_utils import ( + _normalize_rotation_matrices, + _to_numpy_float32, + build_abs_pose_from_components, + convert_rotation, + pose_abs_to_rel, + pose_rel_to_abs, +) + + +def _make_example_poses_abs() -> np.ndarray: + xyz = np.array( + [ + [1.0, -0.5, 0.25], + [1.5, 0.0, 0.75], + [2.0, 0.5, 1.5], + [2.5, 1.0, 2.0], + ], + dtype=np.float32, + ) + euler = np.array( + [ + [0.0, 0.0, 0.0], + [0.1, -0.2, 0.3], + [0.2, 0.15, -0.1], + [-0.25, 0.05, 0.4], + ], + dtype=np.float32, + ) + return build_abs_pose_from_components(xyz, euler, "euler_xyz") + + +@pytest.mark.L0 +def test_to_numpy_float32_raises_on_requires_grad_tensor() -> None: + """Tensor inputs with gradients must be explicitly detached by callers.""" + x = torch.randn(2, 3, requires_grad=True) + with pytest.raises(ValueError, match="non-differentiable"): + _to_numpy_float32(x) + + +@pytest.mark.L0 +def test_build_abs_pose_from_components_supports_quat_wxyz() -> None: + """AV-style wxyz quaternions should produce the same matrices as xyzw.""" + xyz = np.array([[0.0, 0.0, 0.0], [1.0, 2.0, 3.0]], dtype=np.float32) + quat_xyzw = np.array( + [ + [0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, np.sin(np.pi / 4), np.cos(np.pi / 4)], + ], + dtype=np.float32, + ) + quat_wxyz = quat_xyzw[:, [3, 0, 1, 2]] + + poses_xyzw = build_abs_pose_from_components(xyz, quat_xyzw, "quat_xyzw") + poses_wxyz = build_abs_pose_from_components(xyz, quat_wxyz, "quat_wxyz") + + np.testing.assert_allclose(poses_xyzw, poses_wxyz, atol=1e-6) + + +@pytest.mark.L0 +def test_build_abs_pose_from_components_matches_manual_euler_conversion() -> None: + """Euler component helper should match the previous matrix-building pattern.""" + xyz = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 2.0, 0.0], + ], + dtype=np.float32, + ) + euler = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, np.pi / 2], + [0.0, np.pi / 4, np.pi / 2], + ], + dtype=np.float32, + ) + + poses_abs = build_abs_pose_from_components(xyz, euler, "euler_xyz") + manual_poses_abs = np.tile(np.eye(4, dtype=np.float32), (xyz.shape[0], 1, 1)) + manual_poses_abs[:, :3, :3] = R.from_euler("xyz", euler).as_matrix() + manual_poses_abs[:, :3, 3] = xyz + + actual = pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention="backward_framewise") + expected = pose_abs_to_rel(manual_poses_abs, rotation_format="rot6d", pose_convention="backward_framewise") + + np.testing.assert_allclose(actual, expected, atol=1e-6) + + +@pytest.mark.L0 +def test_build_abs_pose_from_components_applies_translation_scale() -> None: + """Explicit translation scaling should be applied before building pose matrices.""" + xyz = np.array( + [ + [0.0, 0.0, 0.0], + [0.5, 0.0, 0.0], + [1.5, 0.0, 0.0], + ], + dtype=np.float32, + ) + euler = np.zeros((3, 3), dtype=np.float32) + + poses_abs = build_abs_pose_from_components(xyz, euler, "euler_xyz", translation_scale=2.0) + + np.testing.assert_allclose(poses_abs[:, :3, 3], xyz / 2.0, atol=1e-6) + + +@pytest.mark.L0 +def test_pose_abs_to_rel_rotation_formats_follow_centralized_conventions() -> None: + """Relative-pose conversion should emit the canonical rot6d and euler_xyz blocks.""" + poses_abs = np.tile(np.eye(4, dtype=np.float32), (3, 1, 1)) + euler = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, np.pi / 4, np.pi / 2], + ], + dtype=np.float32, + ) + matrices_np = R.from_euler("xyz", euler).as_matrix().astype(np.float32) + poses_abs[1:, :3, :3] = matrices_np + + rel_6d = pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention="backward_anchored") + expected_rot6d = matrices_np[:, :, :2].transpose(0, 2, 1).reshape(2, 6) + np.testing.assert_allclose(rel_6d[:, 3:], expected_rot6d, atol=1e-6) + + rel_3d = pose_abs_to_rel(poses_abs, rotation_format="euler_xyz", pose_convention="backward_anchored") + expected_rot3d = R.from_matrix(matrices_np).as_euler("xyz", degrees=False) + np.testing.assert_allclose(rel_3d[:, 3:], expected_rot3d, atol=1e-6) + + +@pytest.mark.L0 +def test_convert_rotation_rot6d_to_matrix_uses_column_based_action_convention() -> None: + """rot6d roundtrip should preserve matrices under the centralized column-based convention.""" + euler = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, np.pi / 4, np.pi / 2], + ], + dtype=np.float32, + ) + matrices_np = R.from_euler("xyz", euler).as_matrix().astype(np.float32) + rot6d = matrices_np[:, :, :2].transpose(0, 2, 1).reshape(2, 6) + + reconstructed = convert_rotation(rot6d, input_format="rot6d", output_format="matrix") + + np.testing.assert_allclose(reconstructed, matrices_np, atol=1e-6) + + +@pytest.mark.L0 +def test_normalize_rotation_matrices_batched_matches_reference_loop() -> None: + """Batched SVD normalization should match the previous per-matrix loop behavior.""" + rng = np.random.default_rng(42) + matrices = rng.normal(size=(32, 3, 3)).astype(np.float32) + + # New batched implementation. + actual = _normalize_rotation_matrices(matrices) + + # Reference: previous loop implementation. + expected_list: list[np.ndarray] = [] + for rot_mat in matrices: + U, _, Vt = np.linalg.svd(rot_mat) + normalized = U @ Vt + if np.linalg.det(normalized) < 0: + U[:, -1] *= -1 + normalized = U @ Vt + expected_list.append(normalized.astype(np.float32)) + expected = np.stack(expected_list, axis=0) + + np.testing.assert_allclose(actual, expected, atol=1e-6) + np.testing.assert_allclose(np.linalg.det(actual), np.ones(actual.shape[0], dtype=np.float32), atol=1e-5) + + +@pytest.mark.L0 +@pytest.mark.parametrize("rotation_format", ["rot9d", "rot6d", "quat_xyzw", "euler_xyz", "axisangle"]) +@pytest.mark.parametrize( + "pose_convention", + ["backward_anchored", "backward_framewise"], +) +def test_pose_abs_to_rel_roundtrips_through_pose_rel_to_abs( + rotation_format: str, + pose_convention: str, +) -> None: + """Relative pose encoding should invert back to the original absolute poses.""" + poses_abs = _make_example_poses_abs() + + poses_rel = pose_abs_to_rel( + poses_abs, + rotation_format=rotation_format, + pose_convention=pose_convention, + translation_scale=2.5, + ) + reconstructed = pose_rel_to_abs( + poses_rel, + rotation_format=rotation_format, + pose_convention=pose_convention, + initial_pose=poses_abs[0], + translation_scale=2.5, + ) + + np.testing.assert_allclose(reconstructed, poses_abs, atol=1e-5) diff --git a/cosmos_training/cosmos/data/vfm/action/pusht_dataset.py b/cosmos_training/cosmos/data/vfm/action/pusht_dataset.py new file mode 100644 index 00000000..8f44c129 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/pusht_dataset.py @@ -0,0 +1,249 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + +import torch + +from cosmos.utils import log +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + BaseActionLeRobotDataset, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + + +def _overlay_cross_from_xy_0_512( + video_tchw: torch.Tensor, + xy: torch.Tensor, + *, + color_rgb: tuple[float, float, float] = (1.0, 0.0, 0.0), + radius: int = 4, + thickness: int = 1, + action_norm_range: float = 512.0, +) -> torch.Tensor: + """ + Overlay a colored 'X' cross on video frames at (x, y) positions from `xy`. + + Assumptions: + - `video_tchw` is (T, C, H, W) float in [0, 1] (or at least 3-channel RGB). + - `xy` is (T_xy, 2+) with raw coords in [0, 512]. + - We draw `xy[t]` directly onto `video_tchw[t]`. + """ + if video_tchw.dim() != 4: + raise ValueError(f"Expected video_tchw to be 4D (T,C,H,W), got {tuple(video_tchw.shape)}") + if video_tchw.shape[1] < 3: + # Not RGB; nothing sensible to do. + return video_tchw + if xy.numel() == 0: + return video_tchw + + T, _, H, W = video_tchw.shape + # scale raw [0, 512] to pixel indices [0, W-1]/[0, H-1] + sx = (W - 1) / action_norm_range + sy = (H - 1) / action_norm_range + + # Ensure we can index in python loop + xy_f = xy[:, :2].detach().to(dtype=torch.float32) + + # Draw in-place + t_max = min(int(xy_f.shape[0]), int(T)) + if t_max <= 0: + return video_tchw + + r_col, g_col, b_col = (float(color_rgb[0]), float(color_rgb[1]), float(color_rgb[2])) + + for t in range(t_max): + frame_t = t + xy_t = xy_f[t] + if not torch.isfinite(xy_t).all(): + continue + x = int(torch.round(xy_t[0] * sx).clamp(0, W - 1).item()) + y = int(torch.round(xy_t[1] * sy).clamp(0, H - 1).item()) + + # Draw an "X" (two diagonals) centered at (x, y) + r = int(radius) + th = int(thickness) + for d in range(-r, r + 1): + # Diagonal 1: (x+d, y+d) + xx1, yy1 = x + d, y + d + # Diagonal 2: (x+d, y-d) + xx2, yy2 = x + d, y - d + + for ox in range(-th, th + 1): + for oy in range(-th, th + 1): + # (x+d, y+d) + xxx1, yyy1 = xx1 + ox, yy1 + oy + if 0 <= xxx1 < W and 0 <= yyy1 < H: + video_tchw[frame_t, 0, yyy1, xxx1] = r_col + video_tchw[frame_t, 1, yyy1, xxx1] = g_col + video_tchw[frame_t, 2, yyy1, xxx1] = b_col + # (x+d, y-d) + xxx2, yyy2 = xx2 + ox, yy2 + oy + if 0 <= xxx2 < W and 0 <= yyy2 < H: + video_tchw[frame_t, 0, yyy2, xxx2] = r_col + video_tchw[frame_t, 1, yyy2, xxx2] = g_col + video_tchw[frame_t, 2, yyy2, xxx2] = b_col + + return video_tchw + + +class PushTDataset(BaseActionLeRobotDataset): + """PushT dataset with deferred source registration. + + Sources are registered by ``_register_sources()`` which is called by + ``ActionUnifiedIterableDataset.assign_worker()`` during training, or + explicitly for standalone/eval use. + """ + + def __init__( + self, + repo_id: str = "lerobot/pusht", + root: str | None = None, + chunk_length: int = 16, + fps: int = 10, + split: str = "train", + split_seed: int = 0, + split_val_ratio: float = 0.05, + mode: str = "policy", + tolerance_s: float = 1e-4, + embodiment_type: str = "pusht", + force_cache_sync: bool = False, + action_norm_range: float = 512.0, + action_space: str = "relative", + overlay_cross: bool = False, + overlay_cross_radius: int = 2, + overlay_cross_thickness: int = 0, + augment_prompt: bool = False, + viewpoint: Viewpoint = "third_person_view", + ) -> None: + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type=embodiment_type, + viewpoint=viewpoint, + tolerance_s=tolerance_s, + ) + + self.action_norm_range = action_norm_range + self.overlay_cross = bool(overlay_cross) + self.overlay_cross_radius = int(overlay_cross_radius) + self.overlay_cross_thickness = int(overlay_cross_thickness) + self.action_space = action_space + self.augment_prompt = augment_prompt + + self._delta_timestamps = { + "observation.image": [i * self._dt for i in range(0, chunk_length + 1)], + "observation.state": [i * self._dt for i in range(0, chunk_length + 1)], + "action": [i * self._dt for i in range(0, chunk_length + 1)], + } + self._repo_id = repo_id + self._root = root + self._force_cache_sync = force_cache_sync + self._all_shard_roots = [root or repo_id] + + def _register_sources(self, indices: list[int] | None = None) -> None: + if indices is not None and 0 not in indices: + return + self._register_source( + repo_id=self._repo_id, + root=self._root, + force_cache_sync=self._force_cache_sync, + delta_timestamps=self._delta_timestamps, + tolerance_s=self._tolerance_s, + dataset_label=self._repo_id, + ) + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode, _, base_idx, item = self._fetch_sample(idx) + + if self.augment_prompt: + prefix = "You are given a task to push the green T into the yellow T region." + prompt = f"Current prediction mode is {mode}." + post_fix = f"The video is {self._chunk_length / self._fps} seconds long and is of {self._fps} FPS." + prompt = f"{prefix} {prompt} {post_fix}" + else: + prompt = "PushT task" + + # Video: LeRobot returns float32 in [0, 1] with shape (T, C, H, W) + video_tchw: torch.Tensor = item["observation.image"] + + # Action (raw): typically absolute XY in [0, 512] for PushT. + action_raw: torch.Tensor = item["action"] + # State (raw): typically contains current agent state/position, also in [0, 512] for PushT. + # We use the -1/fps state (first element) as the "current" reference. + state_raw: torch.Tensor = item["observation.state"] + + # Optionally overlay the raw action XY on the video for debugging/visualization. + # We draw state/action[t] directly on frame[t] (all are indexed by the same delta timestamps). + if self.overlay_cross: + try: + # State in red + _overlay_cross_from_xy_0_512( + video_tchw, + state_raw, # (T+1, D) + color_rgb=(1.0, 0.0, 0.0), + radius=self.overlay_cross_radius, + thickness=self.overlay_cross_thickness, + action_norm_range=self.action_norm_range, + ) + # Action in purple + _overlay_cross_from_xy_0_512( + video_tchw, + action_raw, # (T+1, D) + color_rgb=(1.0, 0.0, 1.0), + radius=self.overlay_cross_radius, + thickness=self.overlay_cross_thickness, + action_norm_range=self.action_norm_range, + ) + except Exception as e: + log.warning(f"Failed to overlay action cross for idx={base_idx}: {e}") + + # Keep raw video in LeRobot layout; the base helper does the final conversion. + video = video_tchw # [T,C,H,W] + + # Action: (T+1, D) -> (T, D) + # Compute relative action w.r.t. the "current" state (delta=0). + # LeRobot returns state/action at deltas [0, 1, ..., chunk_length] (length T+1). + # We use state[0] as the reference and take actions[0:T] (dropping the last one). + # + # This matches: rel_action[t] = action[t] - state_current, for t in [0..T-1]. + action = action_raw[:-1] # [T,D_action] + state_current = state_raw[:1] # [1,D_state] + if self.action_space == "relative": + action = action - state_current + elif self.action_space == "absolute": + action = action + else: + raise ValueError(f"Unsupported action space: {self.action_space}") + # Normalize action to [-1, 1] + action = action / self.action_norm_range + if action.max() > 1.0 or action.min() < -1.0: + log.warning(f"Action out of range: {action.max()}, {action.min()}") + + key = torch.tensor([base_idx], dtype=torch.long) + + return self._build_result( + mode=mode, + video=video, + action=action, + ai_caption=prompt, + action_space=self.action_space, + state=state_raw, + __key__=key, + ) diff --git a/cosmos_training/cosmos/data/vfm/action/robomind_dataset_config.py b/cosmos_training/cosmos/data/vfm/action/robomind_dataset_config.py new file mode 100644 index 00000000..f69bec0e --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/robomind_dataset_config.py @@ -0,0 +1,333 @@ +FRANKA_LEROBOT_ROOTS = [ + "benchmark1_0_release/franka_3rgb/241021_close_trash_bin_1", + "benchmark1_0_release/franka_3rgb/241021_insert_marker_1", + "benchmark1_0_release/franka_3rgb/241021_open_trash_bin_1", + "benchmark1_0_release/franka_3rgb/241021_remove_marker_1", + "benchmark1_0_release/franka_3rgb/241022_lamp_off_1", + "benchmark1_0_release/franka_3rgb/241022_lamp_on_1", + "benchmark1_0_release/franka_3rgb/241022_side_pull_close_drawer_1", + "benchmark1_0_release/franka_3rgb/241022_side_pull_open_drawer_1", + "benchmark1_0_release/franka_3rgb/241023_pick_pear_from_bowl_1", + "benchmark1_0_release/franka_3rgb/241023_pick_pear_from_bowl_2", + "benchmark1_0_release/franka_3rgb/241023_place_pear_in_bowl_1", + "benchmark1_0_release/franka_3rgb/241023_place_pear_in_bowl_2", + "benchmark1_0_release/franka_3rgb/blue_cub_on_pink", + "benchmark1_0_release/franka_3rgb/cap_close_dustbin", + "benchmark1_0_release/franka_3rgb/close_cap_lid", + "benchmark1_0_release/franka_3rgb/close_cap_tool_box", + "benchmark1_0_release/franka_3rgb/close_cap_trash_can", + "benchmark1_0_release/franka_3rgb/close_cap_trash_can_2", + "benchmark1_0_release/franka_3rgb/close_drawer", + "benchmark1_0_release/franka_3rgb/close_trash", + "benchmark1_0_release/franka_3rgb/open_cap_lid", + "benchmark1_0_release/franka_3rgb/open_cap_tool_box", + "benchmark1_0_release/franka_3rgb/open_cap_trash_can_1", + "benchmark1_0_release/franka_3rgb/open_cap_trash_can_2", + "benchmark1_0_release/franka_3rgb/open_drawer", + "benchmark1_0_release/franka_3rgb/open_the_drawer", + "benchmark1_0_release/franka_3rgb/open_trash", + "benchmark1_0_release/franka_3rgb/pick_apple_into_chest", + "benchmark1_0_release/franka_3rgb/pick_bread_desk", + "benchmark1_0_release/franka_3rgb/pick_bread_into_plate", + "benchmark1_0_release/franka_3rgb/pick_drawer_tool", + "benchmark1_0_release/franka_3rgb/pick_plate_from_plate_rack", + "benchmark1_0_release/franka_3rgb/pick_up_strawberry_from_bowl", + "benchmark1_0_release/franka_3rgb/pick_up_strawberry_in_bowl", + "benchmark1_0_release/franka_3rgb/piled_on_stack_blue_block_on_pink_block", + "benchmark1_0_release/franka_3rgb/piled_on_yellow_block_on_purple_block", + "benchmark1_0_release/franka_3rgb/place_in_block_1", + "benchmark1_0_release/franka_3rgb/place_in_block_in_plate_1", + "benchmark1_0_release/franka_3rgb/place_in_block_on_table", + "benchmark1_0_release/franka_3rgb/place_in_block_tennis_ball", + "benchmark1_0_release/franka_3rgb/place_in_bread_in_basket", + "benchmark1_0_release/franka_3rgb/place_in_bread_in_basket_1", + "benchmark1_0_release/franka_3rgb/place_in_bread_in_bread_machine", + "benchmark1_0_release/franka_3rgb/place_in_bread_in_plate", + "benchmark1_0_release/franka_3rgb/place_in_bread_on_plate_1", + "benchmark1_0_release/franka_3rgb/place_in_bread_on_plate_2", + "benchmark1_0_release/franka_3rgb/place_in_bread_on_table", + "benchmark1_0_release/franka_3rgb/place_in_bread_on_table_1", + "benchmark1_0_release/franka_3rgb/place_in_bread_on_table_2", + "benchmark1_0_release/franka_3rgb/place_in_cylinder", + "benchmark1_0_release/franka_3rgb/place_in_fruit", + "benchmark1_0_release/franka_3rgb/place_in_fruit_bread", + "benchmark1_0_release/franka_3rgb/place_in_fruit_in_basket", + "benchmark1_0_release/franka_3rgb/place_in_fruit_in_fruit_basket", + "benchmark1_0_release/franka_3rgb/place_in_fruit_on_table", + "benchmark1_0_release/franka_3rgb/place_in_pick_up_and_throw_away_1", + "benchmark1_0_release/franka_3rgb/place_in_purple_block_in_plate", + "benchmark1_0_release/franka_3rgb/place_in_purple_block_on_table", + "benchmark1_0_release/franka_3rgb/place_in_rectangular_prism", + "benchmark1_0_release/franka_3rgb/place_in_shape", + "benchmark1_0_release/franka_3rgb/place_in_take_bread_and_put_in_plate", + "benchmark1_0_release/franka_3rgb/place_in_toy", + "benchmark1_0_release/franka_3rgb/place_in_trash", + "benchmark1_0_release/franka_3rgb/place_in_yellow_block_on_table", + "benchmark1_0_release/franka_3rgb/place_plate_in_plate_rack", + "benchmark1_0_release/franka_3rgb/place_trash", + "benchmark1_0_release/franka_3rgb/rotate_close_cabinet", + "benchmark1_0_release/franka_3rgb/rotate_open_cabinet", + "benchmark1_0_release/franka_3rgb/slide_close_cabinet", + "benchmark1_0_release/franka_3rgb/slide_close_drawer", + "benchmark1_0_release/franka_3rgb/slide_close_drawer_1", + "benchmark1_0_release/franka_3rgb/slide_close_drawer_1_1", + "benchmark1_0_release/franka_3rgb/slide_open_cabinet", + "benchmark1_0_release/franka_3rgb/slide_open_drawer", + "benchmark1_0_release/franka_3rgb/slide_open_drawer_1", + "benchmark1_0_release/franka_3rgb/stick_target_blue_on_the_pink_obejct", + "benchmark1_0_release/franka_3rgb/twist_knob_start_bread_machine", + "benchmark1_1_release/franka_3rgb/apples_placed_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/bananas_placed_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/bread_is_placed_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/chili_peppers_are_placed_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/chili_peppers_are_placed_on_the_right_side_of_a_plastic_tray", + "benchmark1_1_release/franka_3rgb/close_garbage_bin", + "benchmark1_1_release/franka_3rgb/close_lid", + "benchmark1_1_release/franka_3rgb/cucumber_placed_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/flip_marco_cup", + "benchmark1_1_release/franka_3rgb/mobile_marco_cup", + "benchmark1_1_release/franka_3rgb/open_lid", + "benchmark1_1_release/franka_3rgb/place_marker", + "benchmark1_1_release/franka_3rgb/put_potatoes_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/put_the_bread_on_the_table", + "benchmark1_1_release/franka_3rgb/put_the_cucumber_on_the_left_side_of_the_bowl", + "benchmark1_1_release/franka_3rgb/put_the_red_apple_in_the_bowl", + "benchmark1_1_release/franka_3rgb/put_the_steamed_buns_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/red_apple_placed_in_the_center_of_the_desktop", + "benchmark1_1_release/franka_3rgb/side_opening_drawer", + "benchmark1_1_release/franka_3rgb/side_pull_drawer", + "benchmark1_1_release/franka_3rgb/strawberries_on_a_ceramic_plate", + "benchmark1_1_release/franka_3rgb/take_marker_pen", + "benchmark1_1_release/franka_3rgb/turn_off_lamp", + "benchmark1_1_release/franka_3rgb/turn_on_desk_lamp", + "benchmark1_1_release/franka_3rgb/yellow_bananas_placed_on_a_plastic_tray", + "benchmark1_1_release/franka_3rgb/yellow_square_placed_on_ceramic_plate", + "benchmark1_2_release/franka_3rgb/241223_upright_cup", +] + +FRANKA_DUAL_LEROBOT_ROOTS = [ + "benchmark1_1_release/franka_fr3_dual/both_pour_water", + "benchmark1_1_release/franka_fr3_dual/left_place_gray_plate_from_low_rack_on_left_of_table", + "benchmark1_1_release/franka_fr3_dual/left_place_gray_plate_on_lower_rack", + "benchmark1_1_release/franka_fr3_dual/left_push_plate_to_right_of_table", + "benchmark1_1_release/franka_fr3_dual/right_slide_close_drawer", + "benchmark1_1_release/franka_fr3_dual/right_slide_open_drawer", +] + +UR_LEROBOT_ROOTS = [ + "benchmark1_0_release/ur_1rgb/bread_in_basket_1", + "benchmark1_0_release/ur_1rgb/bread_in_basket_old", + "benchmark1_0_release/ur_1rgb/bread_on_table", + "benchmark1_0_release/ur_1rgb/close_top_drawer", + "benchmark1_0_release/ur_1rgb/close_top_white_drawer", + "benchmark1_0_release/ur_1rgb/close_trash_can", + "benchmark1_0_release/ur_1rgb/cover_pot_lid", + "benchmark1_0_release/ur_1rgb/green_pepper_in_basket", + "benchmark1_0_release/ur_1rgb/green_pepper_in_basket_1", + "benchmark1_0_release/ur_1rgb/green_pepper_on_table", + "benchmark1_0_release/ur_1rgb/open_pot_lid", + "benchmark1_0_release/ur_1rgb/open_top_drawer", + "benchmark1_0_release/ur_1rgb/open_top_white_drawer", + "benchmark1_0_release/ur_1rgb/open_trash_can", + "benchmark1_0_release/ur_1rgb/pick_up_banana", + "benchmark1_0_release/ur_1rgb/pick_up_bread", + "benchmark1_0_release/ur_1rgb/pick_up_bread_slice", + "benchmark1_0_release/ur_1rgb/pick_up_can", + "benchmark1_0_release/ur_1rgb/pick_up_donut", + "benchmark1_0_release/ur_1rgb/pick_up_egg", + "benchmark1_0_release/ur_1rgb/pick_up_green_onion", + "benchmark1_0_release/ur_1rgb/pick_up_green_pepper", + "benchmark1_0_release/ur_1rgb/pick_up_long_bread", + "benchmark1_0_release/ur_1rgb/pick_up_mangosteen", + "benchmark1_0_release/ur_1rgb/pick_up_paper_ball", + "benchmark1_0_release/ur_1rgb/pick_up_pear", + "benchmark1_0_release/ur_1rgb/pick_up_plastic_bottle", + "benchmark1_0_release/ur_1rgb/pick_up_pot_lid", + "benchmark1_0_release/ur_1rgb/pick_up_red_pepper", + "benchmark1_0_release/ur_1rgb/pick_up_round_bread", + "benchmark1_0_release/ur_1rgb/pick_up_round_bread_1", + "benchmark1_0_release/ur_1rgb/pick_up_square_bread", + "benchmark1_0_release/ur_1rgb/pick_up_toast", + "benchmark1_0_release/ur_1rgb/pick_up_triangle_bread", + "benchmark1_0_release/ur_1rgb/pick_up_yellow_pepper", + "benchmark1_0_release/ur_1rgb/put_banana_in_top_drawer", + "benchmark1_0_release/ur_1rgb/put_bread_in_pot", + "benchmark1_0_release/ur_1rgb/put_bread_slice_in_pot", + "benchmark1_0_release/ur_1rgb/put_bread_slice_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_can_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_donut_in_pot", + "benchmark1_0_release/ur_1rgb/put_donut_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_egg_in_pot", + "benchmark1_0_release/ur_1rgb/put_egg_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_green_onion_in_pot", + "benchmark1_0_release/ur_1rgb/put_green_pepper_in_pot", + "benchmark1_0_release/ur_1rgb/put_green_pepper_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_long_bread_in_pot", + "benchmark1_0_release/ur_1rgb/put_long_bread_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_long_bread_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_mangosteen_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_paper_ball_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_pear_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_plastic_bottle_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_pot_lid_on_table", + "benchmark1_0_release/ur_1rgb/put_red_pepper_in_pot", + "benchmark1_0_release/ur_1rgb/put_red_pepper_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_round_bread_in_pot", + "benchmark1_0_release/ur_1rgb/put_round_bread_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_round_bread_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_square_bread_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_square_bread_in_trash_can", + "benchmark1_0_release/ur_1rgb/put_toast_in_pot", + "benchmark1_0_release/ur_1rgb/put_toast_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_triangle_bread_in_pot", + "benchmark1_0_release/ur_1rgb/put_triangle_bread_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/put_yellow_pepper_in_pot", + "benchmark1_0_release/ur_1rgb/put_yellow_pepper_in_top_drawer", + "benchmark1_0_release/ur_1rgb/put_yellow_pepper_in_top_white_drawer", + "benchmark1_0_release/ur_1rgb/red_pepper_in_basket", + "benchmark1_0_release/ur_1rgb/red_pepper_on_table", + "benchmark1_0_release/ur_1rgb/triangle_bread_in_basket", + "benchmark1_0_release/ur_1rgb/triangle_bread_in_basket_1", + "benchmark1_0_release/ur_1rgb/triangle_bread_on_table", + "benchmark1_0_release/ur_1rgb/yellow_pepper_in_basket", + "benchmark1_0_release/ur_1rgb/yellow_pepper_in_basket_1", + "benchmark1_0_release/ur_1rgb/yellow_pepper_on_table", + "benchmark1_1_release/ur_1rgb/green_pepper_on_plate", + "benchmark1_1_release/ur_1rgb/green_pepper_on_the_table", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1025", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1028", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1029", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1030", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1031", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1101", + "benchmark1_1_release/ur_1rgb/insert_the_flowers_from_the_vase_1126", + "benchmark1_1_release/ur_1rgb/pick_up_green_pepper_from_plate", + "benchmark1_1_release/ur_1rgb/pick_up_green_pepper_from_table", + "benchmark1_1_release/ur_1rgb/pick_up_red_pepper_from_plate", + "benchmark1_1_release/ur_1rgb/pick_up_red_pepper_from_table", + "benchmark1_1_release/ur_1rgb/pick_up_the_cucumber_from_the_basket_1125", + "benchmark1_1_release/ur_1rgb/pick_up_the_cucumber_from_the_table_1125", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1025", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1028", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1029", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1030", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1031", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1101", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_table_1126", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1025", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1028", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1029", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1030", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1031", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1101", + "benchmark1_1_release/ur_1rgb/pick_up_the_flowers_from_the_vase_1126", + "benchmark1_1_release/ur_1rgb/pick_up_the_green_vegetable_from_the_basket_1121", + "benchmark1_1_release/ur_1rgb/pick_up_the_green_vegetable_from_the_table_1121", + "benchmark1_1_release/ur_1rgb/pick_up_the_mango_from_the_table_1104", + "benchmark1_1_release/ur_1rgb/pick_up_the_mango_from_the_table_1120", + "benchmark1_1_release/ur_1rgb/pick_up_the_mangoes_from_the_basket_1104", + "benchmark1_1_release/ur_1rgb/pick_up_the_mangoes_from_the_basket_1120", + "benchmark1_1_release/ur_1rgb/pick_up_the_oranges_from_the_basket_1119", + "benchmark1_1_release/ur_1rgb/pick_up_the_oranges_from_the_table_1119", + "benchmark1_1_release/ur_1rgb/pick_up_the_sweet_potato_from_the_basket_1122", + "benchmark1_1_release/ur_1rgb/pick_up_the_sweet_potato_from_the_table_1122", + "benchmark1_1_release/ur_1rgb/pick_up_the_yellow_peppers_from_the_basket_1101", + "benchmark1_1_release/ur_1rgb/pick_up_the_yellow_peppers_from_the_table_1101", + "benchmark1_1_release/ur_1rgb/pick_up_yellow_pepper_from_plate", + "benchmark1_1_release/ur_1rgb/pick_up_yellow_pepper_from_plate_copy_1734079773826", + "benchmark1_1_release/ur_1rgb/pick_up_yellow_pepper_from_table", + "benchmark1_1_release/ur_1rgb/pick_up_yellow_pepper_from_table_copy_1734079574938", + "benchmark1_1_release/ur_1rgb/pickupthebananafromtheplate", + "benchmark1_1_release/ur_1rgb/pickupthebananafromthetable", + "benchmark1_1_release/ur_1rgb/place_the_cucumber_on_the_table_1125", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1025", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1028", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1029", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1030", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1031", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1101", + "benchmark1_1_release/ur_1rgb/place_the_flowers_on_the_table_1126", + "benchmark1_1_release/ur_1rgb/place_the_green_vegetable_on_the_table_1121", + "benchmark1_1_release/ur_1rgb/place_the_mango_on_the_table_1104", + "benchmark1_1_release/ur_1rgb/place_the_mango_on_the_table_1120", + "benchmark1_1_release/ur_1rgb/place_the_oranges_on_the_table_1119", + "benchmark1_1_release/ur_1rgb/place_the_sweet_potato_on_the_table_1122", + "benchmark1_1_release/ur_1rgb/placebananaonaplate", + "benchmark1_1_release/ur_1rgb/put_the_cucumber_in_the_basket_1125", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1105", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1106", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1107", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1111", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1112", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1113", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1114", + "benchmark1_1_release/ur_1rgb/put_the_garbage_in_the_trash_can_1118", + "benchmark1_1_release/ur_1rgb/put_the_green_vegetable_in_the_basket_1121", + "benchmark1_1_release/ur_1rgb/put_the_mango_in_the_basket_1104", + "benchmark1_1_release/ur_1rgb/put_the_mango_in_the_basket_1120", + "benchmark1_1_release/ur_1rgb/put_the_oranges_in_the_basket_1119", + "benchmark1_1_release/ur_1rgb/put_the_sweet_potato_in_the_basket_1122", + "benchmark1_1_release/ur_1rgb/putthebananaonthetable", + "benchmark1_1_release/ur_1rgb/red_pepper_in_table", + "benchmark1_1_release/ur_1rgb/red_pepper_on_plate", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1104", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1105", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1106", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1107", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1108", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1111", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1112", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1113", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1114", + "benchmark1_1_release/ur_1rgb/uncap_open_the_trash_can_1118", + "benchmark1_1_release/ur_1rgb/yellow_pepper_on_plate", + "benchmark1_1_release/ur_1rgb/yellow_pepper_on_plate_copy_1734079761061", +] + +LEROBOT_ROOTS = { + "franka": FRANKA_LEROBOT_ROOTS, + "franka-dual": FRANKA_DUAL_LEROBOT_ROOTS, + "ur": UR_LEROBOT_ROOTS, +} + +FRANKA_OBSERVATION_FEATURES = ["observation.images.camera_top", "observation.states.end_effector"] + +FRANKA_DUAL_OBSERVATION_FEATURES = ["observation.images.camera_front", "observation.states.end_effector"] + +UR_OBSERVATION_FEATURES = ["observation.images.camera_top"] + +OBSERVATION_FEATURES = { + "franka": FRANKA_OBSERVATION_FEATURES, + "franka-dual": FRANKA_DUAL_OBSERVATION_FEATURES, + "ur": UR_OBSERVATION_FEATURES, +} + +# All available camera keys per embodiment (used by concat_view mode). +FRANKA_ALL_CAMERA_KEYS = [ + "observation.images.camera_top", + "observation.images.camera_left", + "observation.images.camera_right", +] + +FRANKA_DUAL_ALL_CAMERA_KEYS = [ + "observation.images.camera_front", + "observation.images.camera_left", + "observation.images.camera_right", +] + +ALL_CAMERA_KEYS = { + "franka": FRANKA_ALL_CAMERA_KEYS, + "franka-dual": FRANKA_DUAL_ALL_CAMERA_KEYS, +} + +FRANKA_ACTION_FEATURES = ["actions.joint_position"] + +FRANKA_DUAL_ACTION_FEATURES = ["actions.joint_position"] + +UR_ACTION_FEATURES = ["actions.joint_position"] + +ACTION_FEATURES = { + "franka": FRANKA_ACTION_FEATURES, + "franka-dual": FRANKA_DUAL_ACTION_FEATURES, + "ur": UR_ACTION_FEATURES, +} diff --git a/cosmos_training/cosmos/data/vfm/action/robomind_franka_dataset.py b/cosmos_training/cosmos/data/vfm/action/robomind_franka_dataset.py new file mode 100644 index 00000000..4ad31e9b --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/robomind_franka_dataset.py @@ -0,0 +1,290 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RoboMIND Franka datasets for single-arm and dual-arm embodiments.""" + +from __future__ import annotations + +import math +import os +from typing import Any, cast + +import numpy as np +import torch +import torch.nn.functional as F + +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.pose_utils import ( + PoseConvention, + build_abs_pose_from_components, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.robomind_dataset_config import ( + ACTION_FEATURES, + ALL_CAMERA_KEYS, + LEROBOT_ROOTS, + OBSERVATION_FEATURES, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +_ROBOMIND_FRANKA_TO_OPENCV: np.ndarray = np.array( + [ + [0.0, -1.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=np.float32, +) + +FrankaInitialPose = torch.Tensor | tuple[torch.Tensor, torch.Tensor] + + +class RoboMINDFrankaDataset(BaseActionLeRobotDataset): + """RoboMIND dataset for Franka single-arm and dual-arm embodiments.""" + + SUPPORTED_EMBODIMENTS: tuple[str, str] = ("robomind-franka", "robomind-franka-dual") + + # RoboMIND-Franka has ~3x faster motion than the typical teleoperation + # datasets (bridge / DROID / fractal). Empirically (see + # ``debug/idle_test/recommend_thresholds_norm.txt``) the slowest 1 % of + # motion sits at ~22 mm/s for single-arm and ~15 mm/s for dual-arm. + # + # **Dual-arm caveat**: the dual-arm dataset frequently has one arm's + # state recorded as a near-zero stutter throughout a chunk (data quality + # issue — only one arm is actually being teleoperated). Because the POS + # branch uses the combined L2 across both arms, the threshold then + # effectively becomes a per-arm threshold for whichever arm is active. + # We compensate by tightening dual-arm to the global default (5 mm/s, + # 1.5°/s) so a single arm doing a slow approach (~1mm/f at 10 Hz) is no + # longer classified as idle. + # + # Class defaults below match single-arm. Dual-arm overrides at instance + # construction (see ``__init__``). + _IDLE_EPS_T_SINGLE: float = 22e-3 + _IDLE_EPS_R_SINGLE: float = math.radians(3.0) + _IDLE_EPS_T_DUAL: float = 5e-3 # = base default; tight enough + _IDLE_EPS_R_DUAL: float = math.radians(1.5) # for "single-arm-slow" cases + idle_eps_t_per_sec: float = _IDLE_EPS_T_SINGLE + idle_eps_r_per_sec: float = _IDLE_EPS_R_SINGLE + + def __init__( + self, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/RoboMIND_20251228", + fps: float = 10.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + mode: str = "policy", + embodiment_type: str = "robomind-franka", + pose_convention: str = "backward_framewise", + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "concat_view", + enable_fast_init: bool = False, + ) -> None: + if embodiment_type not in self.SUPPORTED_EMBODIMENTS: + raise ValueError( + f"RoboMINDFrankaDataset only supports {self.SUPPORTED_EMBODIMENTS}, " + f"got embodiment_type={embodiment_type!r}" + ) + + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type=embodiment_type, + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format="rot6d", + action_normalization=action_normalization, + tolerance_s=1e-4, + enable_fast_init=enable_fast_init, + ) + + self._to_opencv: np.ndarray = _ROBOMIND_FRANKA_TO_OPENCV[:3, :3] + self._is_concat_view: bool = viewpoint == "concat_view" + + # Per-embodiment idle thresholds (instance-level override of the + # class default which matches single-arm). Dual-arm tightens both + # eps_t and eps_r to reflect its smaller per-frame motion tail. + if embodiment_type == "robomind-franka-dual": + self.idle_eps_t_per_sec = self._IDLE_EPS_T_DUAL + self.idle_eps_r_per_sec = self._IDLE_EPS_R_DUAL + + embodiment_key = embodiment_type.removeprefix("robomind-") + lerobot_roots = LEROBOT_ROOTS[embodiment_key] + observation_features = list(OBSERVATION_FEATURES[embodiment_key]) + action_features = ACTION_FEATURES[embodiment_key] + + if self._is_concat_view and embodiment_key in ALL_CAMERA_KEYS: + for cam_key in ALL_CAMERA_KEYS[embodiment_key]: + if cam_key not in observation_features: + observation_features.append(cam_key) + + self._all_shard_roots = [os.path.join(root, shard_root) for shard_root in lerobot_roots] + self._delta_timestamps = { + **{key: [i * self._dt for i in range(0, self._chunk_length + 1)] for key in observation_features}, + **{key: [i * self._dt for i in range(0, self._chunk_length)] for key in action_features}, + } + + def _build_relative_poses( + self, + positions: torch.Tensor | np.ndarray, + euler_xyz: torch.Tensor | np.ndarray, + ) -> tuple[np.ndarray, torch.Tensor]: + poses_abs = build_abs_pose_from_components(positions, euler_xyz, "euler_xyz") + poses_abs[:, :3, :3] = poses_abs[:, :3, :3] @ self._to_opencv + initial_pose = torch.from_numpy(poses_abs[0].copy()).float() + pose_convention = cast(PoseConvention, self._pose_convention) + poses_rel = cast( + np.ndarray, pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention=pose_convention) + ) + return poses_rel, initial_pose + + def _build_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, FrankaInitialPose]: + state = sample["observation.states.end_effector"] + gripper = sample["actions.joint_position"] + + if self._embodiment_type == "robomind-franka": + poses_rel, initial_pose = self._build_relative_poses(state[:, 0:3], state[:, 3:6]) + action = torch.cat( + [ + torch.from_numpy(poses_rel).float(), + 1.0 - gripper[:, [7]], + ], + dim=-1, + ) # [T, 10] + return action, initial_pose + + poses_rel_left, initial_pose_left = self._build_relative_poses(state[:, 0:3], state[:, 3:6]) + poses_rel_right, initial_pose_right = self._build_relative_poses(state[:, 6:9], state[:, 9:12]) + action = torch.cat( + [ + torch.from_numpy(poses_rel_left).float(), + 1.0 - gripper[:, [7]], + torch.from_numpy(poses_rel_right).float(), + 1.0 - gripper[:, [15]], + ], + dim=-1, + ) # [T, 20] + return action, (initial_pose_left, initial_pose_right) + + def _compose_multi_view_franka(self, sample: dict[str, Any]) -> torch.Tensor: # returns [T,C,H',W'] + top_or_front_key = ( + "observation.images.camera_top" + if self._embodiment_type == "robomind-franka" + else "observation.images.camera_front" + ) + top_or_front = sample[top_or_front_key] # [T,C,H,W] + left = sample["observation.images.camera_left"] # [T,C,H,W] + right = sample["observation.images.camera_right"] # [T,C,H,W] + + _, _, height_ref, width_ref = top_or_front.shape + half_height, half_width = height_ref // 2, width_ref // 2 + + left = F.interpolate( + left, size=(half_height, half_width), mode="bilinear", align_corners=False + ) # [T,C,H/2,W/2] + right = F.interpolate( + right, size=(half_height, half_width), mode="bilinear", align_corners=False + ) # [T,C,H/2,W/2] + bottom = torch.cat([left, right], dim=-1) # [T,C,H/2,W] + + composite = torch.cat([top_or_front, bottom], dim=-2) # [T,C,3H/2,W] + return composite # [T,C,3H/2,W] + + def _build_action_spec(self) -> ActionSpec: + """RoboMIND Franka: 10D single-arm or 20D dual-arm. + + Single (``robomind-franka``): + ``[Pos, Rot6d, Gripper]`` (10D) + + Dual (``robomind-franka-dual``): + ``[L_Pos, L_Rot6d, L_Gripper, R_Pos, R_Rot6d, R_Gripper]`` (20D) + """ + if self._embodiment_type == "robomind-franka": + return build_action_spec(Pos(), Rot("rot6d"), Gripper()) + # dual arm + return build_action_spec( + Pos(prefix="left"), + Rot("rot6d", prefix="left"), + Gripper(prefix="left"), + Pos(prefix="right"), + Rot("rot6d", prefix="right"), + Gripper(prefix="right"), + ) + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode, _, _, sample = self._fetch_sample(idx) + ai_caption = sample["task"] + + if self._skip_video_loading: + video = None + additional_view_description = None + elif self._is_concat_view: + video = self._compose_multi_view_franka(sample) + additional_view_description = ( + "The top row shows third-person perspective view looking towards the robot from the front. " + "The bottom-left video shows the third-person perspective view looking at the scene from the left side. " + "The bottom-right video shows the third-person perspective view looking at the scene from the right side." + ) + elif self._embodiment_type == "robomind-franka": + video = sample["observation.images.camera_top"] # [T,C,H,W] + additional_view_description = None + elif self._embodiment_type == "robomind-franka-dual": + video = sample["observation.images.camera_front"] # [T,C,H,W] + additional_view_description = None + else: + raise ValueError(f"Unknown embodiment: {self._embodiment_type}") + + action, initial_pose = self._build_action(sample) + + extras: dict[str, Any] = {} + if isinstance(initial_pose, tuple): + extras["initial_pose"] = initial_pose[0] + extras["initial_pose_right"] = initial_pose[1] + else: + extras["initial_pose"] = initial_pose + + if additional_view_description is not None: + extras["additional_view_description"] = additional_view_description + + return self._build_result( + mode=mode, + video=video, + action=action, + ai_caption=ai_caption, + **extras, + ) + + @property + def action_dim(self) -> int: + if self._embodiment_type == "robomind-franka": + return 10 + if self._embodiment_type == "robomind-franka-dual": + return 20 + raise ValueError(f"Unknown embodiment: {self._embodiment_type}") diff --git a/cosmos_training/cosmos/data/vfm/action/robomind_ur_dataset.py b/cosmos_training/cosmos/data/vfm/action/robomind_ur_dataset.py new file mode 100644 index 00000000..6072ee6f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/robomind_ur_dataset.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RoboMIND UR dataset for single-arm UR5e embodiment.""" + +from __future__ import annotations + +import os +from typing import Any, cast + +import numpy as np +import torch + +from cosmos.data.vfm.action.cosmos3_action_lerobot import ( + ActionNormalization, + ActionSpec, + BaseActionLeRobotDataset, + Gripper, + Pos, + Rot, + build_action_spec, +) +from cosmos.data.vfm.action.pose_utils import ( + PoseConvention, + pose_abs_to_rel, +) +from cosmos.data.vfm.action.robomind_dataset_config import ( + ACTION_FEATURES, + LEROBOT_ROOTS, + OBSERVATION_FEATURES, +) +from cosmos.data.vfm.action.viewpoint_utils import Viewpoint + +# UR EE frame → OpenCV convention rotation (3×3, post-multiplied). +# Identity: attachment_site (quat="-1 1 0 0" in ur5e_robotiq_2f85.xml) already +# satisfies OpenCV convention (z = approach) +_ROBOMIND_UR_TO_OPENCV: np.ndarray = np.eye(3, dtype=np.float32) + +_UR5E_ARM_JOINTS = 6 # shoulder_pan … wrist_3 +_UR5E_EE_SITE = "attachment_site" # flange site in ur5e_robotiq_2f85.xml + + +class RoboMINDURDataset(BaseActionLeRobotDataset): + """RoboMIND dataset for UR embodiment. + + Franka variants live in ``robomind_franka_dataset.py``. + + Action format: 10D ``[pos_delta(3) | rot6d_delta(6) | gripper(1)]`` + derived from MuJoCo FK of ``actions.joint_position`` (6 arm joints → + ``attachment_site`` SE(3) pose). ``observation.states.end_effector`` is + NOT used because it is recorded incorrectly (constant) in ~89 % of UR + episodes; ``actions.joint_position`` is valid for 100 % of episodes. + + The sample also contains ``joint_configs`` — absolute joint angles + ``(T, 7)`` from ``actions.joint_position[1:T+1]`` — for FK-based robot + mesh animation in the viewer. + """ + + SUPPORTED_EMBODIMENTS: tuple[str] = ("robomind-ur",) + + def __init__( + self, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/RoboMIND_20251228", + fps: float = 10.0, + chunk_length: int = 16, + split_seed: int = 42, + split_val_ratio: float = 0.05, + split: str = "train", + mode: str = "policy", + embodiment_type: str = "robomind-ur", + pose_convention: str = "backward_framewise", + action_normalization: ActionNormalization | None = None, + viewpoint: Viewpoint = "third_person_view", + enable_fast_init: bool = False, + ) -> None: + if embodiment_type not in self.SUPPORTED_EMBODIMENTS: + raise ValueError( + f"RoboMINDURDataset only supports {self.SUPPORTED_EMBODIMENTS}; " + "use RoboMINDFrankaDataset for Franka variants. " + f"Got embodiment_type={embodiment_type!r}." + ) + + super().__init__( + fps=fps, + chunk_length=chunk_length, + split_seed=split_seed, + split_val_ratio=split_val_ratio, + split=split, + mode=mode, + embodiment_type=embodiment_type, + viewpoint=viewpoint, + pose_convention=pose_convention, + rotation_format="rot6d", + action_normalization=action_normalization, + tolerance_s=1e-4, + enable_fast_init=enable_fast_init, + ) + + self._to_opencv: np.ndarray = _ROBOMIND_UR_TO_OPENCV + + embodiment_key = embodiment_type.removeprefix("robomind-") + lerobot_roots = LEROBOT_ROOTS[embodiment_key] + observation_features = list(OBSERVATION_FEATURES[embodiment_key]) + action_features = ACTION_FEATURES[embodiment_key] + + self._all_shard_roots = [os.path.join(root, x) for x in lerobot_roots] + + # T+1 joint positions: frame 0 is the initial state; frames 1..T are + # the targets after each action step (used for both FK EE poses and mesh). + _extended = frozenset({"actions.joint_position"}) + self._delta_timestamps = { + **{k: [i * self._dt for i in range(0, self._chunk_length + 1)] for k in observation_features}, + **{ + k: [i * self._dt for i in range(0, self._chunk_length + (1 if k in _extended else 0))] + for k in action_features + }, + } + + # MuJoCo model for FK — loaded once per dataset instance. + # We derive EE poses from FK on actions.joint_position rather than + # observation.states.end_effector because the latter is recorded + # incorrectly (frozen constant) in ~89 % of UR episodes across all 77 + # task shards (6,474 / 7,251 episodes). actions.joint_position is + # valid for 100 % of episodes and is the only reliable EE source. + self._mj_model, self._mj_data, self._ee_site_id = self._init_mujoco() + + @staticmethod + def _init_mujoco(): + """Load UR5e+Robotiq MuJoCo model (kinematics-only) and locate the EE site. + + Strips all geoms and mesh/texture/material assets from the MJCF via + ``MjSpec`` before compile, so the model loads without any mesh files + on disk. FK only needs the kinematic tree (bodies, joints, sites, + inertials), so ``mj_forward`` + ``site_xpos``/``site_xmat`` still + produce identical EE poses. Uses the committed XML directly to skip + the Menagerie mesh download in ``get_mjcf_path``. + """ + from pathlib import Path + + import mujoco + + mjcf_path = str(Path(__file__).parent / "urdf_visualizer" / "ur5e_robotiq_2f85.xml") + spec = mujoco.MjSpec.from_file(mjcf_path) + + # Drop all geoms recursively — FK never touches them. + def _strip_geoms(body): + for g in list(body.geoms): + spec.delete(g) + for child in body.bodies: + _strip_geoms(child) + + _strip_geoms(spec.worldbody) + + # Drop asset entries that reference external files. + for m in list(spec.meshes): + spec.delete(m) + for t in list(spec.textures): + spec.delete(t) + for mat in list(spec.materials): + spec.delete(mat) + + mj_model = spec.compile() + mj_data = mujoco.MjData(mj_model) + ee_site_id = mujoco.mj_name2id(mj_model, mujoco.mjtObj.mjOBJ_SITE, _UR5E_EE_SITE) + if ee_site_id < 0: + raise RuntimeError(f"EE site '{_UR5E_EE_SITE}' not found in {mjcf_path}") + return mj_model, mj_data, ee_site_id + + def _fk_ee_poses(self, arm_q: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Run MuJoCo FK for T+1 arm configs → EE site positions and rotations. + + Args: + arm_q: ``(T+1, 6)`` arm joint angles in radians. + + Returns: + ``(positions (T+1, 3), rotations (T+1, 3, 3))`` in MuJoCo world frame. + """ + import mujoco + + T1 = len(arm_q) + positions = np.empty((T1, 3), dtype=np.float32) + rotations = np.empty((T1, 3, 3), dtype=np.float32) + for t in range(T1): + self._mj_data.qpos[:_UR5E_ARM_JOINTS] = arm_q[t] + mujoco.mj_forward(self._mj_model, self._mj_data) + positions[t] = self._mj_data.site_xpos[self._ee_site_id] + rotations[t] = self._mj_data.site_xmat[self._ee_site_id].reshape(3, 3) + return positions, rotations + + def _build_action(self, sample: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Build 10D UR action from FK EE poses derived from joint positions. + + Returns ``(action_10d, initial_pose, joint_configs)`` where: + - ``action_10d`` is ``(T, 10)``: ``[pos_delta(3) | rot6d_delta(6) | gripper(1)]`` + - ``initial_pose`` is ``(4, 4)`` float32 tensor (FK EE pose at frame 0) + - ``joint_configs`` is ``(T, 7)`` float32 — frames 1..T of joint positions + (arm joints + raw gripper) for FK mesh animation in the viewer. + """ + q = sample["actions.joint_position"] # [T+1, 7]: 6 arm joints + 1 gripper + q_np = q.numpy().astype(np.float32) if isinstance(q, torch.Tensor) else np.asarray(q, dtype=np.float32) + T = len(q_np) - 1 + + # FK EE trajectory: T+1 absolute poses from arm joints via MuJoCo + fk_pos, fk_rot = self._fk_ee_poses(q_np[:, :_UR5E_ARM_JOINTS]) + + poses_abs = np.tile(np.eye(4, dtype=np.float32), (T + 1, 1, 1)) + poses_abs[:, :3, 3] = fk_pos + poses_abs[:, :3, :3] = fk_rot @ self._to_opencv + + initial_pose = torch.from_numpy(poses_abs[0].copy()).float() + pose_convention = cast(PoseConvention, self._pose_convention) + poses_rel = cast( + np.ndarray, pose_abs_to_rel(poses_abs, rotation_format="rot6d", pose_convention=pose_convention) + ) + + # Raw UR gripper: 0=open, 1=closed (maps directly to Robotiq ctrl: raw*255, + # where 0=open, 255=closed). Invert so action uses 0=closed, 1=open + # joint_configs keeps the raw value; FK mesh uses raw * 255 → Robotiq ctrl. + gripper = torch.from_numpy(1.0 - q_np[:T, 6:7]) + action = torch.cat([torch.from_numpy(poses_rel).float(), gripper.float()], dim=-1) # [T, 10] + + # Mesh animation: frames 1..T of joint position (post-action states) + joint_configs = q_np[1 : 1 + T].copy() # [T, 7] + + return action, initial_pose, torch.from_numpy(joint_configs) + + def _build_action_spec(self) -> ActionSpec: + """RoboMIND UR: 10D = ``[Pos, Rot6d, Gripper]``.""" + return build_action_spec(Pos(), Rot("rot6d"), Gripper()) + + def __getitem__(self, idx: int) -> dict[str, Any]: + mode, _, _, sample = self._fetch_sample(idx) + + ai_caption = sample["task"] + if self._skip_video_loading: + video = None + else: + video = sample["observation.images.camera_top"] # [T,C,H,W] + + action, initial_pose, joint_configs = self._build_action(sample) + + return self._build_result( + mode=mode, + video=video, + action=action, + ai_caption=ai_caption, + initial_pose=initial_pose, + joint_configs=joint_configs, + ) + + @property + def action_dim(self) -> int: + return 10 # 9D SE(3) EE deltas + 1 gripper diff --git a/cosmos_training/cosmos/data/vfm/action/transforms.py b/cosmos_training/cosmos/data/vfm/action/transforms.py new file mode 100644 index 00000000..eb41fce5 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/transforms.py @@ -0,0 +1,678 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dataset transform wrappers for the Action project. + +This module provides the ``ActionTransformPipeline`` and spatial padding utilities. + +The reflection padding snaps each sample to the closest predefined resolution from +``VIDEO_RES_SIZE_INFO`` (matching VFM's approach), guaranteeing a bounded set of +output shapes that are all multiples of 16. + +See :func:`~.unified_dataset.wrap_dataset` for the convenience factory that +combines datasets with transforms, and :class:`~.unified_dataset.MapToIterableAdapter` +for the map-to-iterable wrapper. +""" + +from __future__ import annotations + +import torch +import torchvision.transforms.functional as transforms_F + +from cosmos.utils import log +from cosmos.data.vfm.action.json_formatter import ActionPromptJsonFormatter +from cosmos.data.vfm.action.viewpoint_utils import ViewpointTextInfo +from cosmos.data.vfm.augmentors.duration_fps_text_timestamps import DurationFPSTextTimeStamps +from cosmos.data.vfm.augmentors.idle_frames_text_info import IdleFramesTextInfo +from cosmos.data.vfm.augmentors.resolution_text_info import ResolutionTextInfo +from cosmos.data.vfm.augmentors.text_tokenizer import TextTokenizerTransform +from cosmos.data.vfm.sequence_packing import SequencePlan +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO +from cosmos.utils.vfm.data_utils import get_vision_data_resolution + + +def _should_append_idle_frame_info(mode: object) -> bool: + """Return whether idle-frame prompt metadata should be surfaced.""" + return mode != "inverse_dynamics" + + +def pad_action_to_max_dim(action: torch.Tensor, max_action_dim: int) -> torch.Tensor: + """Pad action tensor to max_action_dim along the last dimension. + + Args: + action: Action tensor of shape (T, D) where D is the current action dimension. + max_action_dim: Target action dimension to pad to. + + Returns: + Padded action tensor of shape (T, max_action_dim). + """ + if action.shape[-1] > max_action_dim: + raise ValueError(f"Action dimension {action.shape[-1]} is greater than max_action_dim {max_action_dim}") + elif action.shape[-1] == max_action_dim: + return action + else: + padding_size = max_action_dim - action.shape[-1] + zero_padding = torch.zeros( + *action.shape[:-1], padding_size, dtype=action.dtype, device=action.device + ) # [T,padding_size] + return torch.cat([action, zero_padding], dim=-1) # [T,max_action_dim] + + +def find_closest_target_size(h: int, w: int, resolution: str | int) -> tuple[int, int]: + """Find the closest predefined target size for a given input resolution. + + Looks up ``VIDEO_RES_SIZE_INFO[resolution]`` and selects the aspect ratio + whose ``H/W`` ratio is closest to the input ``h/w``. + + Args: + h: Input height in pixels. + w: Input width in pixels. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``). + + Returns: + ``(target_w, target_h)`` from the predefined table. + + Raises: + ValueError: If *resolution* is not a key in ``VIDEO_RES_SIZE_INFO``. + """ + if isinstance(resolution, int): + resolution = str(resolution) + if resolution not in VIDEO_RES_SIZE_INFO: + raise ValueError( + f"Resolution '{resolution}' not found in VIDEO_RES_SIZE_INFO. Available: {list(VIDEO_RES_SIZE_INFO.keys())}" + ) + + candidates = VIDEO_RES_SIZE_INFO[resolution] + input_ratio = h / w + + best_key: str | None = None + best_diff = float("inf") + for aspect_key, (cand_w, cand_h) in candidates.items(): + cand_ratio = cand_h / cand_w + diff = abs(input_ratio - cand_ratio) + if diff < best_diff: + best_diff = diff + best_key = aspect_key + + assert best_key is not None + target_w, target_h = candidates[best_key] + return target_w, target_h + + +def reflection_pad_to_target( + data_dict: dict, + keys: list[str], + keep_aspect_ratio: bool, + target_w: int, + target_h: int, +) -> dict: + """Resize (aspect-preserving) and reflection-pad tensors to exact target size. + + For each key in *keys*, the tensor is: + + 1. Resized so its spatial dimensions fit within ``(target_h, target_w)`` + while preserving the aspect ratio (matching VFM's + ``ResizeLargestSideAspectPreserving``). + 2. Reflection-padded (or edge-padded when the padding exceeds the spatial + dimension) to reach exactly ``(target_h, target_w)`` (matching VFM's + ``ReflectionPadding``). + + After processing, the following entries are added to *data_dict*: + + - ``"image_size"``: ``torch.Tensor`` of shape ``(4,)`` containing + ``[target_h, target_w, orig_h_resized, orig_w_resized]`` where + ``target_h/w`` is the padded canvas size and ``orig_h/w_resized`` + is the original spatial size after aspect-preserving resize (i.e. + the content region before padding). After ``default_collate`` + this becomes ``(B, 4)``; the ``IterativeJointDataLoader`` then + splits it into per-sample ``(1, 4)`` tensors so the model can + index as ``data_batch["image_size"][i][0][0]``. + + Args: + data_dict: The sample dictionary (mutated in-place). + keys: Data-dict keys whose tensors should be resized and padded. + Tensors must have shape ``(C, H, W)`` or ``(C, T, H, W)``. + keep_aspect_ratio: Whether to keep the aspect ratio of the input tensor. + target_w: Target width in pixels. + target_h: Target height in pixels. + + Returns: + The mutated *data_dict*. + """ + orig_h_resized: int = 0 + orig_w_resized: int = 0 + + for key in keys: + if key not in data_dict: + continue + tensor = data_dict[key] + if not isinstance(tensor, torch.Tensor): + continue + + # Extract spatial dims + if tensor.ndim == 3: + orig_h, orig_w = tensor.shape[-2:] + elif tensor.ndim == 4: + orig_h, orig_w = tensor.shape[-2:] + else: + raise ValueError(f"Unexpected tensor ndim={tensor.ndim} for key '{key}', expected 3 or 4") + + # Step 1: aspect-preserving resize to fit within (target_h, target_w) + if keep_aspect_ratio: + # Prevent upscaling the video by setting the upper bound of scaling_ratio to 1.0. + scaling_ratio = min(target_w / orig_w, target_h / orig_h, 1.0) + orig_h_resized = int(scaling_ratio * orig_h + 0.5) + orig_w_resized = int(scaling_ratio * orig_w + 0.5) + assert orig_h_resized <= target_h and orig_w_resized <= target_w, ( + f"Resize error: orig ({orig_h}, {orig_w}) target ({target_h}, {target_w}) " + f"computed ({orig_h_resized}, {orig_w_resized})" + ) + else: + orig_h_resized = target_h + orig_w_resized = target_w + + if orig_h_resized != orig_h or orig_w_resized != orig_w: + tensor = transforms_F.resize( + tensor, + size=[orig_h_resized, orig_w_resized], + interpolation=transforms_F.InterpolationMode.BICUBIC, + antialias=True, + ) + + # Step 2: padding to exact target size (bottom and right only) + if orig_w_resized != target_w or orig_h_resized != target_h: + padding_right = target_w - orig_w_resized + padding_bottom = target_h - orig_h_resized + padding = [0, 0, padding_right, padding_bottom] + + if padding_right >= orig_w_resized or padding_bottom >= orig_h_resized: + tensor = transforms_F.pad(tensor, padding, padding_mode="edge") + else: + tensor = transforms_F.pad(tensor, padding, padding_mode="reflect") + + data_dict[key] = tensor + + # image_size: shape (4,) — [target_h, target_w, orig_h_resized, orig_w_resized]. + # Matches VFM's item_dataset convention. default_collate stacks to (B, 4); + # IterativeJointDataLoader._get_next_sample slices to (1, 4) per sample so + # the model can index [i][0][0]. + data_dict["image_size"] = torch.tensor( + [target_h, target_w, orig_h_resized, orig_w_resized], dtype=torch.float + ) # [4] + + return data_dict + + +def remove_reflection_padding( + tensor: torch.Tensor, + image_size: torch.Tensor, +) -> torch.Tensor: + """Remove reflection padding added by :func:`reflection_pad_to_target`. + + Content is at top-left; crops to ``(orig_h_resized, orig_w_resized)``. + + Args: + tensor: Tensor whose last two dimensions are the padded spatial dims. + Supports any leading dimensions, e.g. ``(C, T, H, W)`` or + ``(C, H, W)``. + image_size: 1-D tensor of shape ``(4,)`` containing + ``[target_h, target_w, orig_h_resized, orig_w_resized]`` where + ``orig_h/w_resized`` is the original spatial size after + aspect-preserving resize (i.e. the content region before + padding) — the same convention stored by + :func:`reflection_pad_to_target` and VFM's + ``ReflectionPadding``. + + Returns: + Cropped tensor of shape ``(..., orig_h_resized, orig_w_resized)``. + """ + target_h = int(image_size[0].item()) + target_w = int(image_size[1].item()) + orig_h_resized = int(image_size[2].item()) + orig_w_resized = int(image_size[3].item()) + + if orig_h_resized == target_h and orig_w_resized == target_w: + return tensor + + return tensor[..., :orig_h_resized, :orig_w_resized].contiguous() + + +def build_sequence_plan_from_mode( + mode: str, + video_length: int, + action_length: int, + has_text: bool = True, + video_temporal_downsample: int = 4, + num_history_actions: int = 0, +) -> SequencePlan: + """Build a SequencePlan based on the training mode. + + This function determines whether action should be included and computes the + appropriate condition frame indexes for vision and action based on the mode. + + Args: + mode: Training mode. One of: + - "image2video": Image-to-video generation (no action) + - "forward_dynamics": Predict video given first frame and all actions + - "inverse_dynamics": Predict actions given all video frames + - "policy": Predict both actions and video given first frame + video_length: Number of video frames (including the conditioning frame). + action_length: Number of action steps (typically video_length - 1). + has_text: Whether text conditioning is available. Defaults to True. + video_temporal_downsample: Temporal downsampling factor of the video + tokenizer. Used to compute condition frame indexes for inverse + dynamics mode. Defaults to 4. + + Returns: + SequencePlan instance with appropriate settings. + Use ``sequence_plan.has_action`` to check if action should be included. + + Raises: + ValueError: If mode is not one of the supported modes. + + Example: + >>> sequence_plan = build_sequence_plan_from_mode( + ... mode="policy", + ... video_length=5, + ... action_length=4, + ... ) + >>> sequence_plan.has_action + True + >>> sequence_plan.as_dict() + {'has_text': True, 'has_vision': True, 'has_action': True, + 'condition_frame_indexes_vision': [0], 'condition_frame_indexes_action': []} + """ + valid_modes = ["image2video", "forward_dynamics", "inverse_dynamics", "policy"] + if mode not in valid_modes: + raise ValueError(f"Invalid mode: {mode!r}. Must be one of {valid_modes}") + + # Determine if action should be included based on mode + # image2video mode: no action (pure image-to-video generation) + # forward_dynamics, inverse_dynamics, policy: action is needed + has_action = mode != "image2video" + + # Determine condition frame indexes based on mode + # image2video/forward_dynamics/policy: first frame is clean (conditioning) + # inverse_dynamics: all frames are provided as context + if mode in ["image2video", "forward_dynamics", "policy"]: + condition_frame_indexes_vision = [0] + elif mode == "inverse_dynamics": + # All frames are observed for inverse dynamics + condition_frame_indexes_vision = list(range(0, (video_length - 1) // video_temporal_downsample + 1)) + else: + condition_frame_indexes_vision = [] + + # For action conditioning indexes: + # forward_dynamics: all action steps are clean (conditioning) + # inverse_dynamics/policy: action is supervised (predicted) + # History frames (prepended) are always conditioning. + base_action_length = action_length - num_history_actions + if mode == "forward_dynamics": + condition_frame_indexes_action = list(range(action_length)) + + # This currently assumes that the action length is the same as the video length - 1 + # and if action length is the same as the video length, then the first action is the conditioning action + elif base_action_length == video_length - 1: + condition_frame_indexes_action = list(range(num_history_actions)) + elif base_action_length == video_length: + condition_frame_indexes_action = list(range(num_history_actions + 1)) + + if base_action_length == video_length - 1: + action_start_frame_offset = 1 - num_history_actions + if base_action_length == video_length: + action_start_frame_offset = -num_history_actions + + return SequencePlan( + has_text=has_text, + has_vision=True, + has_action=has_action, + condition_frame_indexes_vision=condition_frame_indexes_vision, + condition_frame_indexes_action=condition_frame_indexes_action, + action_start_frame_offset=action_start_frame_offset, + ) + + +class VideoResize: + """Resize and reflection-pad video-aligned tensors for a single sample. + + Resolution is supplied at call time. When ``resolution`` is ``None``, the + tier is auto-detected from the sample's ``"video"`` spatial dimensions. + + Args: + pad_keys: Data-dict keys whose values should be resized and padded. + Pass an empty list to disable padding entirely. Defaults to + ``["video"]``. + keep_aspect_ratio: Whether to resize aspect-preservingly to the closest + predefined target size before padding. Defaults to ``True``. + log_prefix: Prefix used in debug logging. + """ + + def __init__( + self, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + log_prefix: str = "VideoResize", + ) -> None: + self.pad_keys = pad_keys if pad_keys is not None else ["video"] + self.keep_aspect_ratio = keep_aspect_ratio + self.log_prefix = log_prefix + + def __call__(self, data_dict: dict, resolution: str | int | None) -> dict: + """Resize and pad a sample in-place. + + Args: + data_dict: Sample dictionary containing a ``"video"`` entry. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, + ``"720"``). When ``None``, auto-detected from video dimensions. + + Returns: + The same dictionary, mutated in-place with padded tensors and an + ``"image_size"`` entry. + """ + video = data_dict.get("video") + assert isinstance(video, torch.Tensor), "video is required for reflection padding" + h, w = video.shape[-2:] + + if resolution is None: + resolution = get_vision_data_resolution((h, w)) + + if self.keep_aspect_ratio: + target_w, target_h = find_closest_target_size(h, w, resolution) + else: + target_w = int(resolution) + target_h = int(resolution) + reflection_pad_to_target(data_dict, self.pad_keys, self.keep_aspect_ratio, target_w, target_h) + + return data_dict + + def _log_shapes(self, data_dict: dict, when: str) -> None: + """Log tensor shapes for the configured pad keys.""" + for key in self.pad_keys: + val = data_dict.get(key) + if isinstance(val, torch.Tensor): + log.debug(f"{self.log_prefix}: {when} padding '{key}' shape = {tuple(val.shape)}") + + +class ActionTransformPipeline: + """A composable transform pipeline that chains ``VideoResize``, text + tokenization, and automatic sequence plan construction. + + Reflection padding snaps each sample to the closest predefined aspect + ratio from ``VIDEO_RES_SIZE_INFO[resolution]``, resizes + (aspect-preserving) to fit within the target, then reflection-pads to + the exact target size. This guarantees a bounded set of output shapes + (5 per resolution tier), all multiples of 16. Resolution is supplied + at call time via the required ``resolution`` argument to ``__call__``; + when ``resolution`` is ``None``, the tier is auto-detected from the + video's spatial dimensions via ``get_vision_data_resolution``. + + Text tokenization is enabled when ``tokenizer_config`` is provided. + + When the data dictionary contains a ``"mode"`` key, the pipeline automatically + builds a ``SequencePlan`` via :func:`build_sequence_plan_from_mode` and attaches + it as ``data_dict["sequence_plan"]``. For modes where action is not needed + (e.g. ``"image2video"``), the ``"action"`` and ``"domain_id"`` keys are set to + ``None``. + + Args: + pad_keys: Data-dict keys whose values should be resized and padded. Pass + an empty list to disable padding entirely. Defaults to ``["video"]``. + tokenizer_config: A lazy-instantiable config dict for the VLM tokenizer. When + ``None``, text tokenization is skipped. Defaults to ``None``. + cfg_dropout_rate: Probability of replacing the caption with an empty string for + classifier-free guidance. Only used when text tokenization is enabled. + Defaults to ``0.0``. + caption_key: The data-dict key that contains the input caption string. + Defaults to ``"ai_caption"``. + text_token_key: The data-dict key where tokenized text IDs will be stored. + Defaults to ``"text_token_ids"``. + video_temporal_downsample: Temporal downsampling factor of the video tokenizer. + Used when building a ``SequencePlan`` for ``"inverse_dynamics"`` mode. + Defaults to 4. + max_action_dim: Target action dimension to pad to. The ``"action"`` tensor + in every sample is padded along its last dimension via + :func:`pad_action_to_max_dim`. Defaults to 32. + action_channel_masking: When ``True`` (default), the original action + dimension is stored in ``"raw_action_dim"`` so that the model masks + loss/noise/velocity on zero-padded action channels. When ``False``, + ``"raw_action_dim"`` is set to ``None`` and the model treats all + ``max_action_dim`` channels equally (original main-branch behavior). + append_viewpoint_info: Whether to append viewpoint type metadata to the + caption (via ``ViewpointTextInfo`` augmentor). Requires that + samples contain a ``"viewpoint"`` key. Defaults to ``True``. + append_duration_fps_timestamps: Whether to append duration and FPS metadata to the + caption (matching VFM's ``DurationFPSTextTimeStamps`` augmentor). + Defaults to ``True``. + append_resolution_info: Whether to append resolution metadata to the + caption (matching VFM's ``ResolutionTextInfo`` augmentor). + Defaults to ``True``. + append_idle_frames: Whether to append the idle-frame count out of the + total action frames to the caption (Pi0.7-style metadata, via + ``IdleFramesTextInfo`` augmentor). The dataset is responsible for + populating ``data_dict["idle_frames"]``; samples without it are + silently skipped. Idle-frame text is skipped only for + ``"inverse_dynamics"`` mode. Defaults to ``False`` so existing + experiments are unaffected. + idle_frames_dropout: Per-field dropout rate for the idle-frame segment. + With this probability the augmentor leaves the caption unchanged + (matching Pi0.7's ~5% per-component dropout). Independent of the + global ``cfg_dropout_rate``, which empties the whole caption. + Defaults to 0.05. + format_prompt_as_json: Whether to replace the plain text prompt with a + structured JSON-compatible dictionary before tokenization. When + enabled, legacy string metadata appenders are skipped and the JSON + formatter owns viewpoint, action, resolution, duration, FPS, and + idle-frame fields. Defaults to ``False``. + """ + + def __init__( + self, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + tokenizer_config: dict | None = None, + cfg_dropout_rate: float = 0.0, + caption_key: str = "ai_caption", + text_token_key: str = "text_token_ids", + video_temporal_downsample: int = 4, + max_action_dim: int = 32, + action_channel_masking: bool = True, + append_viewpoint_info: bool = True, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + append_idle_frames: bool = False, + idle_frames_dropout: float = 0.05, + format_prompt_as_json: bool = False, + ) -> None: + self.caption_key: str = caption_key + self.video_temporal_downsample: int = video_temporal_downsample + self.max_action_dim: int = max_action_dim + self.action_channel_masking: bool = action_channel_masking + + # --- Spatial resize/padding stage (resolution supplied at call time) --- + self.video_resize: VideoResize = VideoResize( + pad_keys=pad_keys, + keep_aspect_ratio=keep_aspect_ratio, + log_prefix="ActionTransformPipeline", + ) + self.pad_keys: list[str] = self.video_resize.pad_keys + self.keep_aspect_ratio: bool = self.video_resize.keep_aspect_ratio + + self.prompt_json_formatter: ActionPromptJsonFormatter | None = None + if format_prompt_as_json: + self.prompt_json_formatter = ActionPromptJsonFormatter(caption_key=caption_key) + + # --- Viewpoint text augmentor (runs after ai_caption, before duration/FPS) --- + self.viewpoint_augmentor: ViewpointTextInfo | None = None + if append_viewpoint_info and self.prompt_json_formatter is None: + self.viewpoint_augmentor = ViewpointTextInfo( + input_keys=[caption_key, "viewpoint"], + output_keys=[caption_key], + args={"caption_key": caption_key, "viewpoint_key": "viewpoint", "enabled": True}, + ) + + # --- Duration/FPS text augmentor (runs before tokenization) --- + self.duration_fps_augmentor: DurationFPSTextTimeStamps | None = None + if append_duration_fps_timestamps and self.prompt_json_formatter is None: + self.duration_fps_augmentor = DurationFPSTextTimeStamps( + input_keys=[caption_key, "video", "conditioning_fps"], + output_keys=[caption_key], + args={"caption_key": caption_key, "video_key": "video", "fps_key": "conditioning_fps"}, + ) + + # --- Resolution text augmentor (runs before tokenization) --- + self.resolution_info_augmentor: ResolutionTextInfo | None = None + if append_resolution_info and self.prompt_json_formatter is None: + self.resolution_info_augmentor = ResolutionTextInfo( + input_keys=[caption_key, "video", "image_size"], + output_keys=[caption_key], + args={"caption_key": caption_key, "video_key": "video", "enabled": True}, + ) + + # --- IdleFrames text augmentor (Pi0.7-style episode metadata) --- + # Runs after resolution info, before tokenization. Per-field dropout is + # independent from the tokenizer's global cfg_dropout_rate. + self.idle_frames_augmentor: IdleFramesTextInfo | None = None + if append_idle_frames and self.prompt_json_formatter is None: + self.idle_frames_augmentor = IdleFramesTextInfo( + input_keys=[caption_key, "idle_frames", "action"], + output_keys=[caption_key], + args={ + "caption_key": caption_key, + "idle_frames_key": "idle_frames", + "action_key": "action", + "dropout_rate": idle_frames_dropout, + "enabled": True, + }, + ) + + # --- Text tokenizer augmentor --- + self.text_tokenizer: TextTokenizerTransform | None = None + if tokenizer_config is not None: + self.text_tokenizer = TextTokenizerTransform( + input_keys=[caption_key], + output_keys=[text_token_key], + args={ + "tokenizer_config": tokenizer_config, + "cfg_dropout_rate": cfg_dropout_rate, + }, + ) + + def __call__(self, data_dict: dict, resolution: str | None) -> dict: + """Apply the transform pipeline to a single data dictionary. + + Resolution is required at call time and is the only source of truth + for this sample. When ``resolution`` is ``None``, the tier is + auto-detected from the video's spatial dimensions. + + The pipeline runs in order: + + 1. Resize + reflection-pad spatial dimensions to the closest + predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``. + 2. Format the caption as a structured JSON prompt (if enabled). + 3. Otherwise, append viewpoint type metadata to caption (if enabled). + 4. Append duration/FPS metadata to caption (if enabled). + 5. Append resolution metadata to caption (if enabled). + 6. Append idle-frame metadata (Pi0.7-style) to caption unless the + sample is in inverse dynamics mode (if enabled). + 7. Tokenize caption text (if enabled). + 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present). + 9. If action is needed by the plan, pad ``"action"`` to ``max_action_dim``. + 10. Otherwise, nullify ``"action"`` and ``"domain_id"`` (e.g. in + ``"image2video"`` mode). + + Args: + data_dict: A sample dictionary as returned by a Action dataset. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``) + for this sample. When ``None``, auto-detected from video dimensions. + + Returns: + The same dictionary, mutated in-place with padded tensors, + ``image_size``, tokenized text IDs, and a + ``"sequence_plan"`` entry added. + """ + mode = data_dict.get("mode") + assert mode is not None, "mode is required" + + # 1. Resize + reflection-pad spatial dimensions to the closest predefined target from ``VIDEO_RES_SIZE_INFO[resolution]``. + data_dict = self.video_resize(data_dict, resolution) + + # 2. Format the caption as structured JSON when requested; otherwise run the legacy string appenders. + if self.prompt_json_formatter is not None: + data_dict = self.prompt_json_formatter(data_dict) + else: + # 3. Append viewpoint type metadata to caption (if enabled). + if self.viewpoint_augmentor is not None: + result = self.viewpoint_augmentor(data_dict) + if result is not None: + data_dict = result + + # 4. Append duration/FPS metadata to caption (if enabled). + if self.duration_fps_augmentor is not None: + result = self.duration_fps_augmentor(data_dict) + if result is not None: + data_dict = result + + # 5. Append resolution metadata to caption (if enabled). + if self.resolution_info_augmentor is not None: + result = self.resolution_info_augmentor(data_dict) + if result is not None: + data_dict = result + + # 6. Append idle-frame metadata to caption (if enabled for this mode). + if self.idle_frames_augmentor is not None and _should_append_idle_frame_info(mode): + result = self.idle_frames_augmentor(data_dict) + if result is not None: + data_dict = result + + # 7. Tokenize caption text (if enabled). + if self.text_tokenizer is not None: + data_dict = self.text_tokenizer(data_dict) + + # 8. Build a ``SequencePlan`` from the ``"mode"`` key (if present). + video = data_dict.get("video") + action = data_dict.get("action") + assert video is not None, "video is required" + video_length = video.shape[1] # [C,T,H,W] -> T + action_length = action.shape[0] if isinstance(action, torch.Tensor) else max(video_length - 1, 0) + + # Prepend history action frames (ground-truth conditioning) if present. + history_action = data_dict.pop("history_action", None) + num_history_actions = 0 + if history_action is not None and isinstance(action, torch.Tensor): + num_history_actions = history_action.shape[0] + action = torch.cat([history_action, action], dim=0) + action_length += num_history_actions + + sequence_plan = build_sequence_plan_from_mode( + mode=mode, + video_length=video_length, + action_length=action_length, + video_temporal_downsample=self.video_temporal_downsample, + num_history_actions=num_history_actions, + ) + data_dict["sequence_plan"] = sequence_plan + + if sequence_plan.has_action: + assert isinstance(action, torch.Tensor), "action tensor is required when sequence plan has action" + data_dict["raw_action_dim"] = torch.tensor(action.shape[1]) if self.action_channel_masking else None + data_dict["action"] = pad_action_to_max_dim(action, self.max_action_dim) + else: + # Nullify action-related fields when action is not needed so the + # collate function can simply stack all non-None actions. + data_dict["raw_action_dim"] = None + data_dict["action"] = None + data_dict["domain_id"] = None + + return data_dict diff --git a/cosmos_training/cosmos/data/vfm/action/transforms_test.py b/cosmos_training/cosmos/data/vfm/action/transforms_test.py new file mode 100644 index 00000000..2afc03fb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/transforms_test.py @@ -0,0 +1,322 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +import torch + +from cosmos.data.vfm.action.json_formatter import ActionPromptJsonFormatter +from cosmos.data.vfm.action.transforms import ActionTransformPipeline +from cosmos.data.vfm.augmentors.duration_fps_text_timestamps import DurationFPSTextTimeStamps +from cosmos.data.vfm.augmentors.resolution_text_info import ResolutionTextInfo + + +@pytest.mark.L0 +def test_action_prompt_json_formatter_builds_requested_structure() -> None: + formatter = ActionPromptJsonFormatter() + video = torch.zeros(3, 12, 480, 640) # [C,T,H,W] + action = torch.zeros(11, 7) # [T,D] + image_size = torch.tensor([480, 640, 480, 640]) # [4] + fps = torch.tensor(24) # [] + idle_frames = torch.tensor(2) # [] + data_dict = { + "ai_caption": "Pick up the cup", + "video": video, + "action": action, + "conditioning_fps": fps, + "image_size": image_size, + "viewpoint": "concat_view", + "additional_view_description": "The top row is the wrist camera and the bottom row is the scene camera.", + "idle_frames": idle_frames, + } + + result = formatter(data_dict) + + prompt = result["ai_caption"] + assert list(prompt.keys()) == ["cinematography", "actions", "duration", "fps", "resolution", "aspect_ratio"] + assert list(prompt["actions"][0].keys()) == ["time", "description", "idle_frame"] + assert prompt == { + "cinematography": { + "framing": ( + "This video contains concatenated views from multiple camera perspectives. " + "The top row is the wrist camera and the bottom row is the scene camera." + ) + }, + "actions": [ + { + "time": "0:00-0:00", + "description": "Pick up the cup.", + "idle_frame": "2 out of 11.", + } + ], + "duration": "0s", + "fps": 24.0, + "resolution": {"H": 480, "W": 640}, + "aspect_ratio": "4,3", + } + assert "additional_view_description" not in result + + +@pytest.mark.L0 +def test_action_prompt_json_formatter_drops_empty_fields() -> None: + formatter = ActionPromptJsonFormatter() + video = torch.zeros(3, 12, 480, 640) # [C,T,H,W] + action = torch.zeros(11, 7) # [T,D] + image_size = torch.tensor([480, 640, 480, 640]) # [4] + fps = torch.tensor(24) # [] + data_dict = { + "ai_caption": "Pick up the cup.", + "video": video, + "action": action, + "conditioning_fps": fps, + "image_size": image_size, + "viewpoint": "third_person_view", + } + + result = formatter(data_dict) + + assert result["ai_caption"]["actions"] == [ + { + "time": "0:00-0:00", + "description": "Pick up the cup.", + } + ] + + +@pytest.mark.L0 +def test_action_prompt_json_formatter_drops_empty_viewpoint() -> None: + formatter = ActionPromptJsonFormatter() + video = torch.zeros(3, 12, 480, 640) # [C,T,H,W] + action = torch.zeros(11, 7) # [T,D] + image_size = torch.tensor([480, 640, 480, 640]) # [4] + fps = torch.tensor(24) # [] + data_dict = { + "ai_caption": "Pick up the cup.", + "video": video, + "action": action, + "conditioning_fps": fps, + "image_size": image_size, + } + + result = formatter(data_dict) + + assert "cinematography" not in result["ai_caption"] + + +@pytest.mark.L0 +def test_action_transform_pipeline_json_prompt_toggle() -> None: + pipeline = ActionTransformPipeline( + tokenizer_config=None, + max_action_dim=4, + format_prompt_as_json=True, + ) + video = torch.zeros(3, 17, 192, 320) # [C,T,H,W] + action = torch.zeros(16, 2) # [T,D] + data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": action, + "conditioning_fps": torch.tensor(8), # [] + "mode": "policy", + "domain_id": torch.tensor(0), # [] + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + result = pipeline(data_dict, resolution="256") + + prompt = result["ai_caption"] + assert isinstance(prompt, dict) + assert list(prompt.keys()) == ["cinematography", "actions", "duration", "fps", "resolution", "aspect_ratio"] + assert list(prompt["actions"][0].keys()) == ["time", "description", "idle_frame"] + assert prompt["cinematography"] == { + "framing": "This video is captured from a third-person perspective looking towards the agent from the front." + } + assert prompt["actions"] == [ + { + "time": "0:00-0:02", + "description": "Open the drawer.", + "idle_frame": "3 out of 16.", + } + ] + assert prompt["duration"] == "2s" + assert prompt["fps"] == 8.0 + assert prompt["resolution"] == {"H": 192, "W": 320} + assert prompt["aspect_ratio"] == "16,9" + assert result["action"].shape == (16, 4) + + +@pytest.mark.L0 +def test_action_transform_pipeline_keeps_ai_caption_string_path() -> None: + pipeline = ActionTransformPipeline( + tokenizer_config=None, + max_action_dim=4, + append_idle_frames=True, + idle_frames_dropout=0.0, + ) + video = torch.zeros(3, 17, 256, 256) # [C,T,H,W] + action = torch.zeros(16, 2) # [T,D] + data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": action, + "conditioning_fps": torch.tensor(8), # [] + "mode": "policy", + "domain_id": torch.tensor(0), # [] + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + result = pipeline(data_dict, resolution="256") + + assert result["ai_caption"] == ( + "Open the drawer. " + "This video is captured from a third-person perspective looking towards the agent from the front. " + "The video is 2.0 seconds long and is of 8 FPS. " + "This video is of 256x256 resolution. " + "IdleFrames: 3 out of 16." + ) + assert result["action"].shape == (16, 4) + + +@pytest.mark.L0 +def test_action_transform_pipeline_keeps_idle_frames_for_forward_dynamics() -> None: + pipeline = ActionTransformPipeline( + tokenizer_config=None, + max_action_dim=4, + append_idle_frames=True, + idle_frames_dropout=0.0, + ) + video = torch.zeros(3, 17, 256, 256) # [C,T,H,W] + action = torch.zeros(16, 2) # [T,D] + data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": action, + "conditioning_fps": torch.tensor(8), # [] + "mode": "forward_dynamics", + "domain_id": torch.tensor(0), # [] + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + result = pipeline(data_dict, resolution="256") + + assert "IdleFrames: 3 out of 16." in result["ai_caption"] + assert result["action"].shape == (16, 4) + + +@pytest.mark.L0 +def test_action_transform_pipeline_skips_idle_frames_for_inverse_dynamics_string_path() -> None: + pipeline = ActionTransformPipeline( + tokenizer_config=None, + max_action_dim=4, + append_idle_frames=True, + idle_frames_dropout=0.0, + ) + video = torch.zeros(3, 17, 256, 256) # [C,T,H,W] + action = torch.zeros(16, 2) # [T,D] + data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": action, + "conditioning_fps": torch.tensor(8), # [] + "mode": "inverse_dynamics", + "domain_id": torch.tensor(0), # [] + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + result = pipeline(data_dict, resolution="256") + + assert "IdleFrames" not in result["ai_caption"] + assert result["action"].shape == (16, 4) + + +@pytest.mark.L0 +def test_action_transform_pipeline_skips_idle_frames_for_inverse_dynamics_json_prompt() -> None: + pipeline = ActionTransformPipeline( + tokenizer_config=None, + max_action_dim=4, + format_prompt_as_json=True, + ) + video = torch.zeros(3, 17, 256, 256) # [C,T,H,W] + action = torch.zeros(16, 2) # [T,D] + data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": action, + "conditioning_fps": torch.tensor(8), # [] + "mode": "inverse_dynamics", + "domain_id": torch.tensor(0), # [] + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + result = pipeline(data_dict, resolution="256") + + prompt = result["ai_caption"] + assert isinstance(prompt, dict) + assert prompt["actions"] == [ + { + "time": "0:00-0:02", + "description": "Open the drawer.", + } + ] + assert result["action"].shape == (16, 4) + + +@pytest.mark.L0 +def test_action_prompt_json_formatter_matches_video_json_common_metadata() -> None: + formatter = ActionPromptJsonFormatter() + video = torch.zeros(3, 23, 192, 320) # [C,T,H,W] + image_size = torch.tensor([192, 320, 192, 320]) # [4] + fps = torch.tensor(8.0) # [] + action_data_dict = { + "ai_caption": "Open the drawer.", + "video": video, + "action": torch.zeros(22, 2), # [T,D] + "conditioning_fps": fps, + "image_size": image_size, + "viewpoint": "third_person_view", + "idle_frames": torch.tensor(3), # [] + } + + action_prompt = formatter(action_data_dict)["ai_caption"] + + video_data_dict = { + "ai_caption": { + "cinematography": { + "framing": "This video is captured from a third-person perspective looking towards the agent from the front." + }, + "actions": [ + { + "time": "0:00-0:03", + "description": "Open the drawer.", + } + ], + }, + "video": video, + "conditioning_fps": fps, + "image_size": image_size, + "__url__": SimpleNamespace(meta=SimpleNamespace(opts={"aspect_ratio": "16,9"})), + } + duration_augmentor = DurationFPSTextTimeStamps( + input_keys=["ai_caption", "video", "conditioning_fps"], + args={"caption_key": "ai_caption", "video_key": "video", "fps_key": "conditioning_fps"}, + ) + resolution_augmentor = ResolutionTextInfo( + input_keys=["ai_caption", "video", "image_size"], + args={"caption_key": "ai_caption", "video_key": "video"}, + ) + duration_augmentor(video_data_dict) + resolution_augmentor(video_data_dict) + video_prompt = video_data_dict["ai_caption"] + + common_top_level_keys = ["cinematography", "duration", "fps", "resolution", "aspect_ratio"] + assert {key: action_prompt[key] for key in common_top_level_keys} == { + key: video_prompt[key] for key in common_top_level_keys + } + assert action_prompt["actions"][0]["time"] == video_prompt["actions"][0]["time"] + assert action_prompt["actions"][0]["description"] == video_prompt["actions"][0]["description"] + assert action_prompt["actions"][0]["idle_frame"] == "3 out of 22." diff --git a/cosmos_training/cosmos/data/vfm/action/umi/data_classes.py b/cosmos_training/cosmos/data/vfm/action/umi/data_classes.py new file mode 100644 index 00000000..4f4edda8 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/data_classes.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from typing import Any, Union, cast + +from omegaconf import DictConfig, OmegaConf + + +@dataclass +class SourceDataMeta: + name: str + """The data name from the source dataset.""" + shape: tuple[int, ...] + """The shape of a single time step of the data.""" + include_indices: list[int] + """Indices of the data to include in the dataset relative to the current step (0). Negative indices means the data is from the past.""" + + def __post_init__(self): + if isinstance(self.shape, list): + self.shape = tuple(self.shape) + if len(self.include_indices) == 0: + raise ValueError(f"include_indices must be a non-empty list in {self.name}.") + for i, index in enumerate(self.include_indices): + if i < len(self.include_indices) - 1: + if index > self.include_indices[i + 1]: + raise ValueError( + f"include_indices must be monotonically increasing, but got {self.include_indices} in {self.name}." + ) + if len(self.shape) == 0: + raise ValueError(f"shape must be a non-empty list in {self.name}.") + + +@dataclass +class DataMeta: + name: str + """The output name to be used for training.""" + shape: tuple[int, ...] + """The shape of a single time step of the data.""" + data_type: str + """low_dim or image""" + length: int + """The length of the data.""" + normalizer: str + """identity, range, normal. range: normalize to [-1, 1]; normal: normalize to mean=0, std=1""" + augmentation: list[dict[str, Any]] + """The augmentation to apply to the data.""" + source_entry_names: list[str] + """The source entry names to use for the data.""" + + def __post_init__(self): + if isinstance(self.shape, list): + self.shape = tuple(self.shape) + + if self.data_type not in ["low_dim", "image"]: + raise ValueError(f"data_type must be one of ['low_dim', 'image'] in {self.name}.") + + if len(self.source_entry_names) == 0: + raise ValueError(f"source_entry_names must be a non-empty list in {self.name}.") + + if self.length <= 0: + raise ValueError(f"length must be greater than 0 in {self.name}.") + + if len(self.shape) == 0: + raise ValueError(f"shape must be a non-empty list in {self.name}.") + + if self.normalizer not in ["identity", "range", "normal", "clamped_range"]: + raise ValueError( + f"normalizer must be one of ['identity', 'range', 'normal', 'clamped_range'] in {self.name}." + ) + + +def construct_data_meta( + data_meta: Union[dict[str, dict[str, Any]], DictConfig], +) -> dict[str, DataMeta]: + if isinstance(data_meta, DictConfig): + data_meta = cast(dict[str, dict[str, Any]], OmegaConf.to_container(data_meta, resolve=True)) + data_meta_dict = {} + for name, entry_meta_dict in data_meta.items(): + entry_meta_dict.update({"name": name}) + data_meta_dict[name] = DataMeta(**entry_meta_dict) + return data_meta_dict + + +def construct_source_data_meta( + source_data_meta: Union[dict[str, dict[str, Any]], DictConfig], +) -> dict[str, SourceDataMeta]: + if isinstance(source_data_meta, DictConfig): + source_data_meta = cast( + dict[str, dict[str, Any]], + OmegaConf.to_container(source_data_meta, resolve=True), + ) + source_data_meta_dict = {} + for name, entry_meta_dict in source_data_meta.items(): + entry_meta_dict.update({"name": name}) + source_data_meta_dict[name] = SourceDataMeta(**entry_meta_dict) + return source_data_meta_dict diff --git a/cosmos_training/cosmos/data/vfm/action/umi/data_utils.py b/cosmos_training/cosmos/data/vfm/action/umi/data_utils.py new file mode 100644 index 00000000..8afe5f49 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/data_utils.py @@ -0,0 +1,35 @@ +from typing import Any, Callable + +import numpy as np +import torch + + +def aggregate_batch(batch: list[Any], aggregate_fn: Callable[[list[Any]], Any], merge_none: bool = True) -> Any: + """ + Custom collate function to concatenate nested tensors/ndarray/float along a specified axis. + If merge_none is True, the field that has None values will be merged into a single None value. Otherwise will return a list of None values. + Popular choices of aggregate_fn: + - partial(torch.cat, dim=existing_dim), if you want to concatenate along an existing dimension + - partial(torch.stack, dim=new_dim), if you want to stack to a new dimension + + Args: + batch (List[Any]): A list of samples from the dataset. + aggregate_fn (Callable[[list[Any]], Any]): The function to aggregate the tensors/ndarray/float. + + Returns: + Any: The concatenated batch. + """ + if len(batch) == 0: + return batch + elem = batch[0] + if isinstance(elem, torch.Tensor) or isinstance(elem, np.ndarray) or isinstance(elem, float): + return aggregate_fn(batch) + elif isinstance(elem, dict): + return {key: aggregate_batch([d[key] for d in batch], aggregate_fn) for key in elem.keys()} + elif isinstance(elem, list): + return [aggregate_batch(samples, aggregate_fn) for samples in zip(*batch)] + elif elem is None: + if merge_none: + return None + else: + return batch diff --git a/cosmos_training/cosmos/data/vfm/action/umi/imagecodecs_numcodecs.py b/cosmos_training/cosmos/data/vfm/action/umi/imagecodecs_numcodecs.py new file mode 100644 index 00000000..5fb2f81c --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/imagecodecs_numcodecs.py @@ -0,0 +1,1344 @@ +# imagecodecs/numcodecs.py + +# Copyright (c) 2021-2022, Christoph Gohlke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Additional numcodecs implemented using imagecodecs.""" + +__version__ = "2022.9.26" + +__all__ = ("register_codecs",) + +import imagecodecs +import numpy +from numcodecs.abc import Codec +from numcodecs.registry import get_codec, register_codec + + +def protective_squeeze(x: numpy.ndarray): + """ + Squeeze dim only if it's not the last dim. + Image dim expected to be *, H, W, C + """ + img_shape = x.shape[-3:] + if len(x.shape) > 3: + n_imgs = numpy.prod(x.shape[:-3]) + if n_imgs > 1: + img_shape = (-1,) + img_shape + return x.reshape(img_shape) + + +def get_default_image_compressor(**kwargs): + if imagecodecs.JPEGXL: + # has JPEGXL + this_kwargs = { + "effort": 3, + "distance": 0.3, + # bug in libjxl, invalid codestream for non-lossless + # when decoding speed > 1 + "decodingspeed": 1, + } + this_kwargs.update(kwargs) + return JpegXl(**this_kwargs) + else: + this_kwargs = {"level": 50} + this_kwargs.update(kwargs) + return Jpeg2k(**this_kwargs) + + +class Aec(Codec): + """AEC codec for numcodecs.""" + + codec_id = "imagecodecs_aec" + + def __init__(self, bitspersample=None, flags=None, blocksize=None, rsi=None): + self.bitspersample = bitspersample + self.flags = flags + self.blocksize = blocksize + self.rsi = rsi + + def encode(self, buf): + return imagecodecs.aec_encode( + buf, + bitspersample=self.bitspersample, + flags=self.flags, + blocksize=self.blocksize, + rsi=self.rsi, + ) + + def decode(self, buf, out=None): + return imagecodecs.aec_decode( + buf, + bitspersample=self.bitspersample, + flags=self.flags, + blocksize=self.blocksize, + rsi=self.rsi, + out=_flat(out), + ) + + +class Apng(Codec): + """APNG codec for numcodecs.""" + + codec_id = "imagecodecs_apng" + + def __init__(self, level=None, photometric=None, delay=None): + self.level = level + self.photometric = photometric + self.delay = delay + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.apng_encode( + buf, + level=self.level, + photometric=self.photometric, + delay=self.delay, + ) + + def decode(self, buf, out=None): + return imagecodecs.apng_decode(buf, out=out) + + +class Avif(Codec): + """AVIF codec for numcodecs.""" + + codec_id = "imagecodecs_avif" + + def __init__( + self, + level=None, + speed=None, + tilelog2=None, + bitspersample=None, + pixelformat=None, + numthreads=None, + index=None, + ): + self.level = level + self.speed = speed + self.tilelog2 = tilelog2 + self.bitspersample = bitspersample + self.pixelformat = pixelformat + self.numthreads = numthreads + self.index = index + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.avif_encode( + buf, + level=self.level, + speed=self.speed, + tilelog2=self.tilelog2, + bitspersample=self.bitspersample, + pixelformat=self.pixelformat, + numthreads=self.numthreads, + ) + + def decode(self, buf, out=None): + return imagecodecs.avif_decode(buf, index=self.index, numthreads=self.numthreads, out=out) + + +class Bitorder(Codec): + """Bitorder codec for numcodecs.""" + + codec_id = "imagecodecs_bitorder" + + def encode(self, buf): + return imagecodecs.bitorder_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.bitorder_decode(buf, out=_flat(out)) + + +class Bitshuffle(Codec): + """Bitshuffle codec for numcodecs.""" + + codec_id = "imagecodecs_bitshuffle" + + def __init__(self, itemsize=1, blocksize=0): + self.itemsize = itemsize + self.blocksize = blocksize + + def encode(self, buf): + return imagecodecs.bitshuffle_encode(buf, itemsize=self.itemsize, blocksize=self.blocksize).tobytes() + + def decode(self, buf, out=None): + return imagecodecs.bitshuffle_decode( + buf, + itemsize=self.itemsize, + blocksize=self.blocksize, + out=_flat(out), + ) + + +class Blosc(Codec): + """Blosc codec for numcodecs.""" + + codec_id = "imagecodecs_blosc" + + def __init__( + self, + level=None, + compressor=None, + typesize=None, + blocksize=None, + shuffle=None, + numthreads=None, + ): + self.level = level + self.compressor = compressor + self.typesize = typesize + self.blocksize = blocksize + self.shuffle = shuffle + self.numthreads = numthreads + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.blosc_encode( + buf, + level=self.level, + compressor=self.compressor, + typesize=self.typesize, + blocksize=self.blocksize, + shuffle=self.shuffle, + numthreads=self.numthreads, + ) + + def decode(self, buf, out=None): + return imagecodecs.blosc_decode(buf, numthreads=self.numthreads, out=_flat(out)) + + +class Blosc2(Codec): + """Blosc2 codec for numcodecs.""" + + codec_id = "imagecodecs_blosc2" + + def __init__( + self, + level=None, + compressor=None, + typesize=None, + blocksize=None, + shuffle=None, + numthreads=None, + ): + self.level = level + self.compressor = compressor + self.typesize = typesize + self.blocksize = blocksize + self.shuffle = shuffle + self.numthreads = numthreads + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.blosc2_encode( + buf, + level=self.level, + compressor=self.compressor, + typesize=self.typesize, + blocksize=self.blocksize, + shuffle=self.shuffle, + numthreads=self.numthreads, + ) + + def decode(self, buf, out=None): + return imagecodecs.blosc2_decode(buf, numthreads=self.numthreads, out=_flat(out)) + + +class Brotli(Codec): + """Brotli codec for numcodecs.""" + + codec_id = "imagecodecs_brotli" + + def __init__(self, level=None, mode=None, lgwin=None): + self.level = level + self.mode = mode + self.lgwin = lgwin + + def encode(self, buf): + return imagecodecs.brotli_encode(buf, level=self.level, mode=self.mode, lgwin=self.lgwin) + + def decode(self, buf, out=None): + return imagecodecs.brotli_decode(buf, out=_flat(out)) + + +class ByteShuffle(Codec): + """ByteShuffle codec for numcodecs.""" + + codec_id = "imagecodecs_byteshuffle" + + def __init__(self, shape, dtype, axis=-1, dist=1, delta=False, reorder=False): + self.shape = tuple(shape) + self.dtype = numpy.dtype(dtype).str + self.axis = axis + self.dist = dist + self.delta = bool(delta) + self.reorder = bool(reorder) + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + assert buf.shape == self.shape + assert buf.dtype == self.dtype + return imagecodecs.byteshuffle_encode( + buf, + axis=self.axis, + dist=self.dist, + delta=self.delta, + reorder=self.reorder, + ).tobytes() + + def decode(self, buf, out=None): + if not isinstance(buf, numpy.ndarray): + buf = numpy.frombuffer(buf, dtype=self.dtype).reshape(*self.shape) + return imagecodecs.byteshuffle_decode( + buf, + axis=self.axis, + dist=self.dist, + delta=self.delta, + reorder=self.reorder, + out=out, + ) + + +class Bz2(Codec): + """Bz2 codec for numcodecs.""" + + codec_id = "imagecodecs_bz2" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + return imagecodecs.bz2_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.bz2_decode(buf, out=_flat(out)) + + +class Cms(Codec): + """CMS codec for numcodecs.""" + + codec_id = "imagecodecs_cms" + + def __init__(self, *args, **kwargs): + pass + + def encode(self, buf, out=None): + # return imagecodecs.cms_transform(buf) + raise NotImplementedError + + def decode(self, buf, out=None): + # return imagecodecs.cms_transform(buf) + raise NotImplementedError + + +class Deflate(Codec): + """Deflate codec for numcodecs.""" + + codec_id = "imagecodecs_deflate" + + def __init__(self, level=None, raw=False): + self.level = level + self.raw = bool(raw) + + def encode(self, buf): + return imagecodecs.deflate_encode(buf, level=self.level, raw=self.raw) + + def decode(self, buf, out=None): + return imagecodecs.deflate_decode(buf, out=_flat(out), raw=self.raw) + + +class Delta(Codec): + """Delta codec for numcodecs.""" + + codec_id = "imagecodecs_delta" + + def __init__(self, shape=None, dtype=None, axis=-1, dist=1): + self.shape = None if shape is None else tuple(shape) + self.dtype = None if dtype is None else numpy.dtype(dtype).str + self.axis = axis + self.dist = dist + + def encode(self, buf): + if self.shape is not None or self.dtype is not None: + buf = protective_squeeze(numpy.asarray(buf)) + assert buf.shape == self.shape + assert buf.dtype == self.dtype + return imagecodecs.delta_encode(buf, axis=self.axis, dist=self.dist).tobytes() + + def decode(self, buf, out=None): + if self.shape is not None or self.dtype is not None: + buf = numpy.frombuffer(buf, dtype=self.dtype).reshape(*self.shape) + return imagecodecs.delta_decode(buf, axis=self.axis, dist=self.dist, out=out) + + +class Float24(Codec): + """Float24 codec for numcodecs.""" + + codec_id = "imagecodecs_float24" + + def __init__(self, byteorder=None, rounding=None): + self.byteorder = byteorder + self.rounding = rounding + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.float24_encode(buf, byteorder=self.byteorder, rounding=self.rounding) + + def decode(self, buf, out=None): + return imagecodecs.float24_decode(buf, byteorder=self.byteorder, out=out) + + +class FloatPred(Codec): + """Floating Point Predictor codec for numcodecs.""" + + codec_id = "imagecodecs_floatpred" + + def __init__(self, shape, dtype, axis=-1, dist=1): + self.shape = tuple(shape) + self.dtype = numpy.dtype(dtype).str + self.axis = axis + self.dist = dist + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + assert buf.shape == self.shape + assert buf.dtype == self.dtype + return imagecodecs.floatpred_encode(buf, axis=self.axis, dist=self.dist).tobytes() + + def decode(self, buf, out=None): + if not isinstance(buf, numpy.ndarray): + buf = numpy.frombuffer(buf, dtype=self.dtype).reshape(*self.shape) + return imagecodecs.floatpred_decode(buf, axis=self.axis, dist=self.dist, out=out) + + +class Gif(Codec): + """GIF codec for numcodecs.""" + + codec_id = "imagecodecs_gif" + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.gif_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.gif_decode(buf, asrgb=False, out=out) + + +class Heif(Codec): + """HEIF codec for numcodecs.""" + + codec_id = "imagecodecs_heif" + + def __init__( + self, + level=None, + bitspersample=None, + photometric=None, + compression=None, + numthreads=None, + index=None, + ): + self.level = level + self.bitspersample = bitspersample + self.photometric = photometric + self.compression = compression + self.numthreads = numthreads + self.index = index + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.heif_encode( + buf, + level=self.level, + bitspersample=self.bitspersample, + photometric=self.photometric, + compression=self.compression, + numthreads=self.numthreads, + ) + + def decode(self, buf, out=None): + return imagecodecs.heif_decode( + buf, + index=self.index, + photometric=self.photometric, + numthreads=self.numthreads, + out=out, + ) + + +class Jetraw(Codec): + """Jetraw codec for numcodecs.""" + + codec_id = "imagecodecs_jetraw" + + def __init__( + self, + shape, + identifier, + parameters=None, + verbosity=None, + errorbound=None, + ): + self.shape = shape + self.identifier = identifier + self.errorbound = errorbound + imagecodecs.jetraw_init(parameters, verbosity) + + def encode(self, buf): + return imagecodecs.jetraw_encode(buf, identifier=self.identifier, errorbound=self.errorbound) + + def decode(self, buf, out=None): + if out is None: + out = numpy.empty(self.shape, numpy.uint16) + return imagecodecs.jetraw_decode(buf, out=out) + + +class Jpeg(Codec): + """JPEG codec for numcodecs.""" + + codec_id = "imagecodecs_jpeg" + + def __init__( + self, + bitspersample=None, + tables=None, + header=None, + colorspace_data=None, + colorspace_jpeg=None, + level=None, + subsampling=None, + optimize=None, + smoothing=None, + ): + self.tables = tables + self.header = header + self.bitspersample = bitspersample + self.colorspace_data = colorspace_data + self.colorspace_jpeg = colorspace_jpeg + self.level = level + self.subsampling = subsampling + self.optimize = optimize + self.smoothing = smoothing + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.jpeg_encode( + buf, + level=self.level, + colorspace=self.colorspace_data, + outcolorspace=self.colorspace_jpeg, + subsampling=self.subsampling, + optimize=self.optimize, + smoothing=self.smoothing, + ) + + def decode(self, buf, out=None): + out_shape = None + if out is not None: + out_shape = out.shape + out = protective_squeeze(out) + img = imagecodecs.jpeg_decode( + buf, + bitspersample=self.bitspersample, + tables=self.tables, + header=self.header, + colorspace=self.colorspace_jpeg, + outcolorspace=self.colorspace_data, + out=out, + ) + if out_shape is not None: + img = img.reshape(out_shape) + return img + + def get_config(self): + """Return dictionary holding configuration parameters.""" + config = dict(id=self.codec_id) + for key in self.__dict__: + if not key.startswith("_"): + value = getattr(self, key) + if value is not None and key in ("header", "tables"): + import base64 + + value = base64.b64encode(value).decode() + config[key] = value + return config + + @classmethod + def from_config(cls, config): + """Instantiate codec from configuration object.""" + for key in ("header", "tables"): + value = config.get(key, None) + if value is not None and isinstance(value, str): + import base64 + + config[key] = base64.b64decode(value.encode()) + return cls(**config) + + +class Jpeg2k(Codec): + """JPEG 2000 codec for numcodecs.""" + + codec_id = "imagecodecs_jpeg2k" + + def __init__( + self, + level=None, + codecformat=None, + colorspace=None, + tile=None, + reversible=None, + bitspersample=None, + resolutions=None, + numthreads=None, + verbose=0, + ): + self.level = level + self.codecformat = codecformat + self.colorspace = colorspace + self.tile = None if tile is None else tuple(tile) + self.reversible = reversible + self.bitspersample = bitspersample + self.resolutions = resolutions + self.numthreads = numthreads + self.verbose = verbose + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.jpeg2k_encode( + buf, + level=self.level, + codecformat=self.codecformat, + colorspace=self.colorspace, + tile=self.tile, + reversible=self.reversible, + bitspersample=self.bitspersample, + resolutions=self.resolutions, + numthreads=self.numthreads, + verbose=self.verbose, + ) + + def decode(self, buf, out=None): + return imagecodecs.jpeg2k_decode(buf, verbose=self.verbose, numthreads=self.numthreads, out=out) + + +class JpegLs(Codec): + """JPEG LS codec for numcodecs.""" + + codec_id = "imagecodecs_jpegls" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.jpegls_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.jpegls_decode(buf, out=out) + + +class JpegXl(Codec): + """JPEG XL codec for numcodecs.""" + + codec_id = "imagecodecs_jpegxl" + + def __init__( + self, + # encode + level=None, + effort=None, + distance=None, + lossless=None, + decodingspeed=None, + photometric=None, + planar=None, + usecontainer=None, + # decode + index=None, + keeporientation=None, + # both + numthreads=None, + ): + """ + Return JPEG XL image from numpy array. + Float must be in nominal range 0..1. + + Currently L, LA, RGB, RGBA images are supported in contig mode. + Extra channels are only supported for grayscale images in planar mode. + + Parameters + ---------- + level : Default to None, i.e. not overwriting lossess and decodingspeed options. + When < 0: Use lossless compression + When in [0,1,2,3,4]: Sets the decoding speed tier for the provided options. + Minimum is 0 (slowest to decode, best quality/density), and maximum + is 4 (fastest to decode, at the cost of some quality/density). + effort : Default to 3. + Sets encoder effort/speed level without affecting decoding speed. + Valid values are, from faster to slower speed: 1:lightning 2:thunder + 3:falcon 4:cheetah 5:hare 6:wombat 7:squirrel 8:kitten 9:tortoise. + Speed: lightning, thunder, falcon, cheetah, hare, wombat, squirrel, kitten, tortoise + control the encoder effort in ascending order. + This also affects memory usage: using lower effort will typically reduce memory + consumption during encoding. + lightning and thunder are fast modes useful for lossless mode (modular). + falcon disables all of the following tools. + cheetah enables coefficient reordering, context clustering, and heuristics for selecting DCT sizes and quantization steps. + hare enables Gaborish filtering, chroma from luma, and an initial estimate of quantization steps. + wombat enables error diffusion quantization and full DCT size selection heuristics. + squirrel (default) enables dots, patches, and spline detection, and full context clustering. + kitten optimizes the adaptive quantization for a psychovisual metric. + tortoise enables a more thorough adaptive quantization search. + distance : Default to 1.0 + Sets the distance level for lossy compression: target max butteraugli distance, + lower = higher quality. Range: 0 .. 15. 0.0 = mathematically lossless + (however, use JxlEncoderSetFrameLossless instead to use true lossless, + as setting distance to 0 alone is not the only requirement). + 1.0 = visually lossless. Recommended range: 0.5 .. 3.0. + lossess : Default to False. + Use lossess encoding. + decodingspeed : Default to 0. + Duplicate to level. [0,4] + photometric : Return JxlColorSpace value. + Default logic is quite complicated but works most of the time. + Accepted value: + int: [-1,3] + str: ['RGB', + 'WHITEISZERO', 'MINISWHITE', + 'BLACKISZERO', 'MINISBLACK', 'GRAY', + 'XYB', 'KNOWN'] + planar : Enable multi-channel mode. + Default to false. + usecontainer : + Forces the encoder to use the box-based container format (BMFF) + even when not necessary. + When using JxlEncoderUseBoxes, JxlEncoderStoreJPEGMetadata or + JxlEncoderSetCodestreamLevel with level 10, the encoder will + automatically also use the container format, it is not necessary + to use JxlEncoderUseContainer for those use cases. + By default this setting is disabled. + index : Selectively decode frames for animation. + Default to 0, decode all frames. + When set to > 0, decode that frame index only. + keeporientation : + Enables or disables preserving of as-in-bitstream pixeldata orientation. + Some images are encoded with an Orientation tag indicating that the + decoder must perform a rotation and/or mirroring to the encoded image data. + + If skip_reorientation is JXL_FALSE (the default): the decoder will apply + the transformation from the orientation setting, hence rendering the image + according to its specified intent. When producing a JxlBasicInfo, the decoder + will always set the orientation field to JXL_ORIENT_IDENTITY (matching the + returned pixel data) and also align xsize and ysize so that they correspond + to the width and the height of the returned pixel data. + + If skip_reorientation is JXL_TRUE: the decoder will skip applying the + transformation from the orientation setting, returning the image in + the as-in-bitstream pixeldata orientation. This may be faster to decode + since the decoder doesnt have to apply the transformation, but can + cause wrong display of the image if the orientation tag is not correctly + taken into account by the user. + + By default, this option is disabled, and the returned pixel data is + re-oriented according to the images Orientation setting. + threads : Default to 1. + If <= 0, use all cores. + If > 32, clipped to 32. + """ + + self.level = level + self.effort = effort + self.distance = distance + self.lossless = bool(lossless) + self.decodingspeed = decodingspeed + self.photometric = photometric + self.planar = planar + self.usecontainer = usecontainer + self.index = index + self.keeporientation = keeporientation + self.numthreads = numthreads + + def encode(self, buf): + + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.jpegxl_encode( + buf, + level=self.level, + effort=self.effort, + distance=self.distance, + lossless=self.lossless, + decodingspeed=self.decodingspeed, + photometric=self.photometric, + planar=self.planar, + usecontainer=self.usecontainer, + numthreads=self.numthreads, + ) + + def decode(self, buf, out=None): + return imagecodecs.jpegxl_decode( + buf, + index=self.index, + keeporientation=self.keeporientation, + numthreads=self.numthreads, + out=out, + ) + + +class JpegXr(Codec): + """JPEG XR codec for numcodecs.""" + + codec_id = "imagecodecs_jpegxr" + + def __init__( + self, + level=None, + photometric=None, + hasalpha=None, + resolution=None, + fp2int=None, + ): + self.level = level + self.photometric = photometric + self.hasalpha = hasalpha + self.resolution = resolution + self.fp2int = fp2int + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.jpegxr_encode( + buf, + level=self.level, + photometric=self.photometric, + hasalpha=self.hasalpha, + resolution=self.resolution, + ) + + def decode(self, buf, out=None): + return imagecodecs.jpegxr_decode(buf, fp2int=self.fp2int, out=out) + + +class Lerc(Codec): + """LERC codec for numcodecs.""" + + codec_id = "imagecodecs_lerc" + + def __init__(self, level=None, version=None, planar=None): + self.level = level + self.version = version + self.planar = bool(planar) + + # self.mask = None + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.lerc_encode( + buf, + level=self.level, + version=self.version, + planar=self.planar, + ) + + def decode(self, buf, out=None): + return imagecodecs.lerc_decode(buf, out=out) + + +class Ljpeg(Codec): + """LJPEG codec for numcodecs.""" + + codec_id = "imagecodecs_ljpeg" + + def __init__(self, bitspersample=None): + self.bitspersample = bitspersample + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.ljpeg_encode(buf, bitspersample=self.bitspersample) + + def decode(self, buf, out=None): + return imagecodecs.ljpeg_decode(buf, out=out) + + +class Lz4(Codec): + """LZ4 codec for numcodecs.""" + + codec_id = "imagecodecs_lz4" + + def __init__(self, level=None, hc=False, header=True): + self.level = level + self.hc = hc + self.header = bool(header) + + def encode(self, buf): + return imagecodecs.lz4_encode(buf, level=self.level, hc=self.hc, header=self.header) + + def decode(self, buf, out=None): + return imagecodecs.lz4_decode(buf, header=self.header, out=_flat(out)) + + +class Lz4f(Codec): + """LZ4F codec for numcodecs.""" + + codec_id = "imagecodecs_lz4f" + + def __init__( + self, + level=None, + blocksizeid=False, + contentchecksum=None, + blockchecksum=None, + ): + self.level = level + self.blocksizeid = blocksizeid + self.contentchecksum = contentchecksum + self.blockchecksum = blockchecksum + + def encode(self, buf): + return imagecodecs.lz4f_encode( + buf, + level=self.level, + blocksizeid=self.blocksizeid, + contentchecksum=self.contentchecksum, + blockchecksum=self.blockchecksum, + ) + + def decode(self, buf, out=None): + return imagecodecs.lz4f_decode(buf, out=_flat(out)) + + +class Lzf(Codec): + """LZF codec for numcodecs.""" + + codec_id = "imagecodecs_lzf" + + def __init__(self, header=True): + self.header = bool(header) + + def encode(self, buf): + return imagecodecs.lzf_encode(buf, header=self.header) + + def decode(self, buf, out=None): + return imagecodecs.lzf_decode(buf, header=self.header, out=_flat(out)) + + +class Lzma(Codec): + """LZMA codec for numcodecs.""" + + codec_id = "imagecodecs_lzma" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + return imagecodecs.lzma_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.lzma_decode(buf, out=_flat(out)) + + +class Lzw(Codec): + """LZW codec for numcodecs.""" + + codec_id = "imagecodecs_lzw" + + def encode(self, buf): + return imagecodecs.lzw_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.lzw_decode(buf, out=_flat(out)) + + +class PackBits(Codec): + """PackBits codec for numcodecs.""" + + codec_id = "imagecodecs_packbits" + + def __init__(self, axis=None): + self.axis = axis + + def encode(self, buf): + if not isinstance(buf, (bytes, bytearray)): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.packbits_encode(buf, axis=self.axis) + + def decode(self, buf, out=None): + return imagecodecs.packbits_decode(buf, out=_flat(out)) + + +class Pglz(Codec): + """PGLZ codec for numcodecs.""" + + codec_id = "imagecodecs_pglz" + + def __init__(self, header=True, strategy=None): + self.header = bool(header) + self.strategy = strategy + + def encode(self, buf): + return imagecodecs.pglz_encode(buf, strategy=self.strategy, header=self.header) + + def decode(self, buf, out=None): + return imagecodecs.pglz_decode(buf, header=self.header, out=_flat(out)) + + +class Png(Codec): + """PNG codec for numcodecs.""" + + codec_id = "imagecodecs_png" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.png_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.png_decode(buf, out=out) + + +class Qoi(Codec): + """QOI codec for numcodecs.""" + + codec_id = "imagecodecs_qoi" + + def __init__(self): + pass + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.qoi_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.qoi_decode(buf, out=out) + + +class Rgbe(Codec): + """RGBE codec for numcodecs.""" + + codec_id = "imagecodecs_rgbe" + + def __init__(self, header=False, shape=None, rle=None): + if not header and shape is None: + raise ValueError("must specify data shape if no header") + if shape and shape[-1] != 3: + raise ValueError("invalid shape") + self.shape = shape + self.header = bool(header) + self.rle = None if rle is None else bool(rle) + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.rgbe_encode(buf, header=self.header, rle=self.rle) + + def decode(self, buf, out=None): + if out is None and not self.header: + out = numpy.empty(self.shape, numpy.float32) + return imagecodecs.rgbe_decode(buf, header=self.header, rle=self.rle, out=out) + + +class Rcomp(Codec): + """Rcomp codec for numcodecs.""" + + codec_id = "imagecodecs_rcomp" + + def __init__(self, shape, dtype, nblock=None): + self.shape = tuple(shape) + self.dtype = numpy.dtype(dtype).str + self.nblock = nblock + + def encode(self, buf): + return imagecodecs.rcomp_encode(buf, nblock=self.nblock) + + def decode(self, buf, out=None): + return imagecodecs.rcomp_decode( + buf, + shape=self.shape, + dtype=self.dtype, + nblock=self.nblock, + out=out, + ) + + +class Snappy(Codec): + """Snappy codec for numcodecs.""" + + codec_id = "imagecodecs_snappy" + + def encode(self, buf): + return imagecodecs.snappy_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.snappy_decode(buf, out=_flat(out)) + + +class Spng(Codec): + """SPNG codec for numcodecs.""" + + codec_id = "imagecodecs_spng" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.spng_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.spng_decode(buf, out=out) + + +class Tiff(Codec): + """TIFF codec for numcodecs.""" + + codec_id = "imagecodecs_tiff" + + def __init__(self, index=None, asrgb=None, verbose=0): + self.index = index + self.asrgb = bool(asrgb) + self.verbose = verbose + + def encode(self, buf): + + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.tiff_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.tiff_decode( + buf, + index=self.index, + asrgb=self.asrgb, + verbose=self.verbose, + out=out, + ) + + +class Webp(Codec): + """WebP codec for numcodecs.""" + + codec_id = "imagecodecs_webp" + + def __init__(self, level=None, lossless=None, method=None, hasalpha=None): + self.level = level + self.hasalpha = bool(hasalpha) + self.method = method + self.lossless = lossless + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + return imagecodecs.webp_encode(buf, level=self.level, lossless=self.lossless, method=self.method) + + def decode(self, buf, out=None): + return imagecodecs.webp_decode(buf, hasalpha=self.hasalpha, out=out) + + +class Xor(Codec): + """XOR codec for numcodecs.""" + + codec_id = "imagecodecs_xor" + + def __init__(self, shape=None, dtype=None, axis=-1): + self.shape = None if shape is None else tuple(shape) + self.dtype = None if dtype is None else numpy.dtype(dtype).str + self.axis = axis + + def encode(self, buf): + if self.shape is not None or self.dtype is not None: + buf = protective_squeeze(numpy.asarray(buf)) + assert buf.shape == self.shape + assert buf.dtype == self.dtype + return imagecodecs.xor_encode(buf, axis=self.axis).tobytes() + + def decode(self, buf, out=None): + if self.shape is not None or self.dtype is not None: + buf = numpy.frombuffer(buf, dtype=self.dtype).reshape(*self.shape) + return imagecodecs.xor_decode(buf, axis=self.axis, out=_flat(out)) + + +class Zfp(Codec): + """ZFP codec for numcodecs.""" + + codec_id = "imagecodecs_zfp" + + def __init__( + self, + shape=None, + dtype=None, + strides=None, + level=None, + mode=None, + execution=None, + numthreads=None, + chunksize=None, + header=True, + ): + if header: + self.shape = None + self.dtype = None + self.strides = None + elif shape is None or dtype is None: + raise ValueError("invalid shape or dtype") + else: + self.shape = tuple(shape) + self.dtype = numpy.dtype(dtype).str + self.strides = None if strides is None else tuple(strides) + self.level = level + self.mode = mode + self.execution = execution + self.numthreads = numthreads + self.chunksize = chunksize + self.header = bool(header) + + def encode(self, buf): + buf = protective_squeeze(numpy.asarray(buf)) + if not self.header: + assert buf.shape == self.shape + assert buf.dtype == self.dtype + return imagecodecs.zfp_encode( + buf, + level=self.level, + mode=self.mode, + execution=self.execution, + header=self.header, + numthreads=self.numthreads, + chunksize=self.chunksize, + ) + + def decode(self, buf, out=None): + if self.header: + return imagecodecs.zfp_decode(buf, out=out) + return imagecodecs.zfp_decode( + buf, + shape=self.shape, + dtype=numpy.dtype(self.dtype), + strides=self.strides, + numthreads=self.numthreads, + out=out, + ) + + +class Zlib(Codec): + """Zlib codec for numcodecs.""" + + codec_id = "imagecodecs_zlib" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + return imagecodecs.zlib_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.zlib_decode(buf, out=_flat(out)) + + +class Zlibng(Codec): + """Zlibng codec for numcodecs.""" + + codec_id = "imagecodecs_zlibng" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + return imagecodecs.zlibng_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.zlibng_decode(buf, out=_flat(out)) + + +class Zopfli(Codec): + """Zopfli codec for numcodecs.""" + + codec_id = "imagecodecs_zopfli" + + def encode(self, buf): + return imagecodecs.zopfli_encode(buf) + + def decode(self, buf, out=None): + return imagecodecs.zopfli_decode(buf, out=_flat(out)) + + +class Zstd(Codec): + """ZStandard codec for numcodecs.""" + + codec_id = "imagecodecs_zstd" + + def __init__(self, level=None): + self.level = level + + def encode(self, buf): + return imagecodecs.zstd_encode(buf, level=self.level) + + def decode(self, buf, out=None): + return imagecodecs.zstd_decode(buf, out=_flat(out)) + + +def _flat(out): + """Return numpy array as contiguous view of bytes if possible.""" + if out is None: + return None + view = memoryview(out) + if view.readonly or not view.contiguous: + return None + return view.cast("B") + + +def register_codecs(codecs=None, force=False, verbose=True): + """Register codecs in this module with numcodecs.""" + for name, cls in globals().items(): + if not hasattr(cls, "codec_id") or name == "Codec": + continue + if codecs is not None and cls.codec_id not in codecs: + continue + try: + try: + get_codec({"id": cls.codec_id}) + except TypeError: + # registered, but failed + pass + except ValueError: + # not registered yet + pass + else: + if not force: + if verbose: + log_warning(f"numcodec {cls.codec_id!r} already registered") + continue + if verbose: + log_warning(f"replacing registered numcodec {cls.codec_id!r}") + register_codec(cls) + + +def log_warning(msg, *args, **kwargs): + """Log message with level WARNING.""" + import logging + + logging.getLogger(__name__).warning(msg, *args, **kwargs) diff --git a/cosmos_training/cosmos/data/vfm/action/umi/normalizer.py b/cosmos_training/cosmos/data/vfm/action/umi/normalizer.py new file mode 100644 index 00000000..86082043 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/normalizer.py @@ -0,0 +1,295 @@ +from typing import Any + +import numpy as np +import torch +from torch import nn + +from cosmos.data.vfm.action.umi.data_classes import DataMeta + + +class SingleFieldLinearNormalizer(nn.Module): + def __init__(self, meta: DataMeta): + super().__init__() + self.meta: DataMeta = meta + self.scale: nn.Parameter + self.offset: nn.Parameter + self.normalizer_type: str = meta.normalizer + + def fit(self, x: torch.Tensor): + raise NotImplementedError() + + def from_dict( + self, + state_dict: dict[str, torch.Tensor], + ): + if self.normalizer_type == "identity": + return + + state_dict_tensor: dict[str, torch.Tensor] = {} + for key, val in state_dict.items(): + if key not in ["scale", "offset", "min", "max"]: + continue + + if isinstance(val, torch.Tensor): + state_dict_tensor[key] = val + elif isinstance(val, np.ndarray): + state_dict_tensor[key] = torch.from_numpy(val) + elif isinstance(val, list): + state_dict_tensor[key] = torch.Tensor(val) + else: + raise ValueError(f"Unknown type {type(val)} for {key}") + + if "min" in state_dict_tensor and "max" in state_dict_tensor: + # Map [min, max] to [-1, 1] + # normalize: (x-offset) / scale + state_dict_tensor["scale"] = (state_dict_tensor["max"] - state_dict_tensor["min"]) / 2 + 1e-7 + state_dict_tensor["offset"] = (state_dict_tensor["max"] + state_dict_tensor["min"]) / 2 + del state_dict_tensor["min"] + del state_dict_tensor["max"] + + # Set scale to 1 if the original scale is too small + if hasattr(self, "skip_threshold"): + scale_too_small_mask = state_dict_tensor["scale"] < self.skip_threshold + state_dict_tensor["scale"][scale_too_small_mask] = 1.0 + + keys = ["scale", "offset"] + + for key in keys: + val = state_dict_tensor[key] + assert key in state_dict_tensor, f"State dict must contain '{key}' key for {self.meta.name}" + + if self.normalizer_type in ["range", "normal", "clamped_range"]: + assert val.shape == self.meta.shape, ( + f"{key} must have the same shape as the data {self.meta.shape} for range normalizer {self.meta.name}" + ) + else: + raise ValueError( + f"Unknown normalizer {self.normalizer_type} for {self.meta.name}. Valid normalizers are 'identity', 'range', 'normal', 'clamped_range'." + ) + + setattr(self, key, nn.Parameter(val)) + + def normalize(self, x: torch.Tensor) -> torch.Tensor: + raise NotImplementedError() + + def unnormalize(self, x: torch.Tensor) -> torch.Tensor: + raise NotImplementedError() + + def as_dict(self, data_class: str) -> dict[str, Any]: + assert self.scale is not None and self.offset is not None, ( + f"Normalizer for {self.meta.name} is not initialized." + ) + if data_class == "numpy": + return { + "type": self.normalizer_type, + "scale": self.scale.detach().cpu().numpy(), + "offset": self.offset.detach().cpu().numpy(), + } + elif data_class == "torch": + return { + "type": self.normalizer_type, + "scale": self.scale.detach().cpu(), + "offset": self.offset.detach().cpu(), + } + elif data_class == "list": + return { + "type": self.normalizer_type, + "scale": self.scale.detach().cpu().tolist(), + "offset": self.offset.detach().cpu().tolist(), + } + else: + raise ValueError( + f"Unknown data type {data_class} for normalizer {self.meta.name}. Valid types are 'numpy', 'torch', and 'list'." + ) + + def _check_input_shape(self, x: torch.Tensor): + data_dim = len(self.meta.shape) + assert x.shape[-data_dim:] == self.meta.shape, ( + f"The last {data_dim} dimensions of {self.meta.name} (shape {x.shape}) must match {self.meta.shape} from meta data" + ) + + +class IdentityNormalizer(SingleFieldLinearNormalizer): + def __init__(self, meta: DataMeta): + super().__init__(meta) + self.scale = nn.Parameter(torch.tensor(1.0)) + self.offset = nn.Parameter(torch.tensor(0.0)) + + def fit(self, x: torch.Tensor): + pass + + def load(self, state_dict: dict[str, torch.Tensor]): + pass + + def normalize(self, x: torch.Tensor) -> torch.Tensor: + return x + + def unnormalize(self, x: torch.Tensor) -> torch.Tensor: + return x + + +class RangeNormalizer(SingleFieldLinearNormalizer): + """ + Normalize data to be between -1 and 1. + """ + + def __init__(self, meta: DataMeta, skip_threshold: float = 1e-2): + super().__init__(meta) + self.scale = nn.Parameter(torch.nan * torch.ones(meta.shape)) + self.offset = nn.Parameter(torch.nan * torch.ones(meta.shape)) + self.skip_threshold = skip_threshold + + def fit(self, x: torch.Tensor): + """ + x: (traj_num, *shape) + """ + self._check_input_shape(x) + x = x.clone().detach().reshape(-1, *self.meta.shape) + min_val = x.min(dim=0).values + max_val = x.max(dim=0).values + scale = nn.Parameter((max_val - min_val) / 2 + 1e-7) + self.scale[:] = 1.0 + max_abs_val = torch.max(torch.abs(x), dim=0).values + ratio = scale / max_abs_val + normalize_mask = (max_abs_val > self.skip_threshold) & (ratio > self.skip_threshold) + print(f"{self.meta.name}: Normalize mask {normalize_mask}") + self.scale[normalize_mask] = scale[normalize_mask] + self.offset = nn.Parameter((max_val + min_val) / 2) + assert tuple(self.scale.shape) == tuple(self.offset.shape) == self.meta.shape + + def normalize(self, x: torch.Tensor) -> torch.Tensor: + assert not self.scale.isnan().any() and not self.offset.isnan().any(), ( + f"Normalizer for {self.meta.name} is not initialized" + ) + self._check_input_shape(x) + return (x - self.offset) / self.scale + + def unnormalize(self, x: torch.Tensor) -> torch.Tensor: + assert not self.scale.isnan().any() and not self.offset.isnan().any(), ( + f"Normalizer for {self.meta.name} is not initialized" + ) + self._check_input_shape(x) + return x * self.scale + self.offset + + +class ClampedRangeNormalizer(SingleFieldLinearNormalizer): + """ + Normalize data to be between -1 and 1, but clip the data if the normalized value is out of range. + """ + + def __init__(self, meta: DataMeta): + super().__init__(meta) + self.scale = nn.Parameter(torch.nan * torch.ones(meta.shape)) + self.offset = nn.Parameter(torch.nan * torch.ones(meta.shape)) + + def fit(self, x: torch.Tensor): + raise NotImplementedError("Please manually assign the scale and offset parameters after calculating the stats") + + def normalize(self, x: torch.Tensor) -> torch.Tensor: + assert not self.scale.isnan().any() and not self.offset.isnan().any(), ( + f"Normalizer for {self.meta.name} is not initialized" + ) + self._check_input_shape(x) + return torch.clamp((x - self.offset) / self.scale, -1, 1) + + def unnormalize(self, x: torch.Tensor) -> torch.Tensor: + assert not self.scale.isnan().any() and not self.offset.isnan().any(), ( + f"Normalizer for {self.meta.name} is not initialized" + ) + self._check_input_shape(x) + return x * self.scale + self.offset + + +class NormalNormalizer(RangeNormalizer): + """ + Normalize data distribution to be N(0, 1). + """ + + def fit(self, x: torch.Tensor): + print(f"Fitting normalizer for {self.meta.name}") + self._check_input_shape(x) + x = x.clone().detach().reshape(-1, *self.meta.shape) + mean = x.mean(dim=0) + std = x.std(dim=0) + self.scale = nn.Parameter(std) + self.offset = nn.Parameter(mean) + + +class FixedNormalizer(nn.Module): + """ + Normalizer that is fixed after fitting. Not trainable. + """ + + @torch.no_grad() + def __init__( + self, + data_meta: dict[str, DataMeta], + ): + super().__init__() + self.data_meta: dict[str, DataMeta] = data_meta + self.normalizers: nn.ModuleDict = nn.ModuleDict() + + for meta in self.data_meta.values(): + if meta.normalizer == "identity": + self.normalizers[meta.name] = IdentityNormalizer(meta) + elif meta.normalizer == "range": + self.normalizers[meta.name] = RangeNormalizer(meta) + elif meta.normalizer == "normal": + self.normalizers[meta.name] = NormalNormalizer(meta) + elif meta.normalizer == "clamped_range": + self.normalizers[meta.name] = ClampedRangeNormalizer(meta) + else: + raise ValueError(f"Unknown normalizer {meta.normalizer} for {meta.name}") + + @torch.no_grad() + def fit_normalizer(self, data_dict: dict[str, torch.Tensor]): + for meta in self.data_meta.values(): + if meta.normalizer not in ["range", "normal", "clamped_range"]: + continue + normalizer = self.normalizers[meta.name] + assert isinstance(normalizer, SingleFieldLinearNormalizer) + normalizer.fit(data_dict[meta.name]) + + @torch.no_grad() + def from_dict( + self, + state_dict: dict[str, dict[str, torch.Tensor]], + ): + for meta in self.data_meta.values(): + if meta.normalizer == "identity": + continue + assert meta.name in state_dict, f"State dict for {meta.name} not found when loading normalizer" + normalizer = self.normalizers[meta.name] + assert isinstance(normalizer, SingleFieldLinearNormalizer) + normalizer.from_dict(state_dict[meta.name]) + + @torch.no_grad() + def normalize(self, data_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + for name, data in data_dict.items(): + if name not in self.normalizers: + continue + normalizer = self.normalizers[name] + assert isinstance(normalizer, SingleFieldLinearNormalizer) + data_dict[name] = normalizer.normalize(data) + + return data_dict + + @torch.no_grad() + def unnormalize(self, data_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + for name, data in data_dict.items(): + if name not in self.normalizers: + continue + normalizer = self.normalizers[name] + assert isinstance(normalizer, SingleFieldLinearNormalizer) + data_dict[name] = normalizer.unnormalize(data) + return data_dict + + @torch.no_grad() + def as_dict(self, data_class: str) -> dict[str, dict[str, Any]]: + state_dict = {} + for name, normalizer in self.normalizers.items(): + if name not in state_dict: + state_dict[name] = {} + assert isinstance(normalizer, SingleFieldLinearNormalizer) + state_dict[name] = normalizer.as_dict(data_class) + return state_dict diff --git a/cosmos_training/cosmos/data/vfm/action/umi/pose_utils.py b/cosmos_training/cosmos/data/vfm/action/umi/pose_utils.py new file mode 100644 index 00000000..3a05a041 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/pose_utils.py @@ -0,0 +1,283 @@ +from typing import Any + +import numpy as np +import numpy.typing as npt +from numba import njit + + +@njit(cache=True) +def qmult(q1: npt.NDArray[Any], q2: npt.NDArray[Any]) -> npt.NDArray[Any]: + q = np.array( + [ + q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2] - q1[3] * q2[3], + q1[0] * q2[1] + q1[1] * q2[0] + q1[2] * q2[3] - q1[3] * q2[2], + q1[0] * q2[2] - q1[1] * q2[3] + q1[2] * q2[0] + q1[3] * q2[1], + q1[0] * q2[3] + q1[1] * q2[2] - q1[2] * q2[1] + q1[3] * q2[0], + ] + ) + + return q + + +@njit(cache=True) +def qconjugate(q: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.array([q[0], -q[1], -q[2], -q[3]]) + + +@njit(cache=True) +def get_absolute_pose( + init_pose_xyz_wxyz: npt.NDArray[Any], + relative_pose_xyz_wxyz: npt.NDArray[Any], +): + """The new pose is in the same frame of reference as the initial pose""" + new_pose_xyz_wxyz = np.zeros(7, init_pose_xyz_wxyz.dtype) + relative_pos_in_init_frame_as_quat_wxyz = np.zeros(4, init_pose_xyz_wxyz.dtype) + relative_pos_in_init_frame_as_quat_wxyz[1:] = relative_pose_xyz_wxyz[:3] + init_rot_qinv = qconjugate(init_pose_xyz_wxyz[3:]) + relative_pos_in_world_frame_as_quat_wxyz = qmult( + qmult(init_pose_xyz_wxyz[3:], relative_pos_in_init_frame_as_quat_wxyz), + init_rot_qinv, + ) + new_pose_xyz_wxyz[:3] = init_pose_xyz_wxyz[:3] + relative_pos_in_world_frame_as_quat_wxyz[1:] + quat = qmult(init_pose_xyz_wxyz[3:], relative_pose_xyz_wxyz[3:]) + if quat[0] < 0: + quat = -quat + new_pose_xyz_wxyz[3:] = quat + return new_pose_xyz_wxyz + + +@njit(cache=True) +def get_relative_pose( + new_pose_xyz_wxyz: npt.NDArray[Any], + init_pose_xyz_wxyz: npt.NDArray[Any], +): + """The two poses are in the same frame of reference""" + relative_pose_xyz_wxyz = np.zeros(7, new_pose_xyz_wxyz.dtype) + relative_pos_in_world_frame_as_quat_wxyz = np.zeros(4, new_pose_xyz_wxyz.dtype) + relative_pos_in_world_frame_as_quat_wxyz[1:] = new_pose_xyz_wxyz[:3] - init_pose_xyz_wxyz[:3] + init_rot_qinv = qconjugate(init_pose_xyz_wxyz[3:]) + relative_pose_xyz_wxyz[:3] = qmult( + qmult(init_rot_qinv, relative_pos_in_world_frame_as_quat_wxyz), + init_pose_xyz_wxyz[3:], + )[1:] + quat = qmult(init_rot_qinv, new_pose_xyz_wxyz[3:]) + if quat[0] < 0: + quat = -quat + relative_pose_xyz_wxyz[3:] = quat + return relative_pose_xyz_wxyz + + +@njit(cache=True) +def invert_pose(pose_xyz_wxyz: npt.NDArray[Any]) -> npt.NDArray[Any]: + qinv = qconjugate(pose_xyz_wxyz[3:]) + pos_quat_wxyz = np.zeros(4, pose_xyz_wxyz.dtype) + pos_quat_wxyz[1:] = pose_xyz_wxyz[:3] + rotated_pos = qmult( + qmult(qinv, pos_quat_wxyz), + pose_xyz_wxyz[3:], + ) + inverted_pose = np.zeros(7, pose_xyz_wxyz.dtype) + inverted_pose[:3] = -rotated_pos[1:] + if qinv[0] < 0: + qinv = -qinv + inverted_pose[3:] = qinv + return inverted_pose + + +@njit(cache=True) +def quat_wxyz_to_rot_6d(quat_wxyz: npt.NDArray[Any]) -> npt.NDArray[Any]: + """ + Convert a quaternion to a 6D representation: the first two rows of the corresponding rotation matrix. + https://arxiv.org/pdf/1812.07035 + quat_wxyz: (4, ) + return: (6, ) + """ + assert quat_wxyz.shape == (4,) + w, x, y, z = quat_wxyz[0], quat_wxyz[1], quat_wxyz[2], quat_wxyz[3] + + R = np.array( + [ + [1 - 2 * y * y - 2 * z * z, 2 * x * y - 2 * w * z, 2 * x * z + 2 * w * y], + [2 * x * y + 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z - 2 * w * x], + [2 * x * z - 2 * w * y, 2 * y * z + 2 * w * x, 1 - 2 * x * x - 2 * y * y], + ] + ) + + rot_6d = np.zeros(6) + rot_6d[:3] = R[0, :] + rot_6d[3:] = R[1, :] + + return rot_6d + + +@njit(cache=True) +def rot_6d_to_quat_wxyz(rot_6d: npt.NDArray[Any]) -> npt.NDArray[Any]: + """ + Convert a 6D representation to a quaternion. + https://arxiv.org/pdf/1812.07035 + rot_6d: (6, ) + return: (4, ) + """ + + assert rot_6d.shape == (6,) + a1, a2 = rot_6d[:3], rot_6d[3:] + b1 = a1 / np.linalg.norm(a1) + b2 = a2 - np.dot(b1, a2) * b1 + b2 = b2 / np.linalg.norm(b2) + b3 = np.cross(b1, b2) + + m = np.zeros((3, 3)) + m[0, :] = b1 + m[1, :] = b2 + m[2, :] = b3 + + trace = np.trace(m) + + if trace > 0: + s = 0.5 / np.sqrt(trace + 1.0) + w = 0.25 / s + x = (m[2, 1] - m[1, 2]) * s + y = (m[0, 2] - m[2, 0]) * s + z = (m[1, 0] - m[0, 1]) * s + elif m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]: + s = 2.0 * np.sqrt(1.0 + m[0, 0] - m[1, 1] - m[2, 2]) + w = (m[2, 1] - m[1, 2]) / s + x = 0.25 * s + y = (m[0, 1] + m[1, 0]) / s + z = (m[0, 2] + m[2, 0]) / s + elif m[1, 1] > m[2, 2]: + s = 2.0 * np.sqrt(1.0 + m[1, 1] - m[0, 0] - m[2, 2]) + w = (m[0, 2] - m[2, 0]) / s + x = (m[0, 1] + m[1, 0]) / s + y = 0.25 * s + z = (m[1, 2] + m[2, 1]) / s + else: + s = 2.0 * np.sqrt(1.0 + m[2, 2] - m[0, 0] - m[1, 1]) + w = (m[1, 0] - m[0, 1]) / s + x = (m[0, 2] + m[2, 0]) / s + y = (m[1, 2] + m[2, 1]) / s + z = 0.25 * s + + return np.array([w, x, y, z]) + + +# @njit(cache=True) +def quat_wxyz_to_rot_6d_batch(quat_wxyz: npt.NDArray[Any]) -> npt.NDArray[Any]: + """ + input (..., 4) + output (..., 6) + """ + assert quat_wxyz.shape[-1] == 4 + input_shape = quat_wxyz.shape[:-1] + quat_wxyz = quat_wxyz.copy().reshape(-1, 4) + rot_6d = np.zeros((quat_wxyz.shape[0], 6)) + for i in range(quat_wxyz.shape[0]): + rot_6d[i] = quat_wxyz_to_rot_6d(quat_wxyz[i]) + return rot_6d.reshape(*input_shape, 6) + + +# @njit(cache=True) +def rot_6d_to_quat_wxyz_batch(rot_6d: npt.NDArray[Any]) -> npt.NDArray[Any]: + """ + input (..., 6) + output (..., 4) + """ + assert rot_6d.shape[-1] == 6, f"rot_6d.shape: {rot_6d.shape}" + input_shape = rot_6d.shape[:-1] + rot_6d = rot_6d.copy().reshape(-1, 6) + quat_wxyz = np.zeros((rot_6d.shape[0], 4)) + for i in range(rot_6d.shape[0]): + quat_wxyz[i] = rot_6d_to_quat_wxyz(rot_6d[i]) + return quat_wxyz.reshape(*input_shape, 4) + + +def convert_batch_to_10d(eef_xyz_wxyz: npt.NDArray[np.float32], gripper_width: npt.NDArray[np.float32]): + """ + eef_xyz_wxyz: (batch_size, obs_history_len, 7) + gripper_width: (batch_size, obs_history_len, 1) + + return: + robot0_10d: (batch_size, obs_history_len, 10) + """ + + assert eef_xyz_wxyz.shape[0:2] == gripper_width.shape[0:2] + + batch_size, obs_history_len = eef_xyz_wxyz.shape[0:2] + + pose10d = np.zeros((batch_size, obs_history_len, 10)) + pose10d[:, :, :3] = eef_xyz_wxyz[:, :, :3] + pose10d[:, :, 3:9] = quat_wxyz_to_rot_6d_batch(eef_xyz_wxyz[:, :, 3:]) + pose10d[:, :, 9] = gripper_width[:, :, 0] + + return pose10d + + +def convert_10d_to_batch(pose10d: npt.NDArray[np.float32]): + """ + pose10d: (batch_size, obs_history_len, 10) + + return: + eef_xyz_wxyz: (batch_size, obs_history_len, 7) + gripper_width: (batch_size, obs_history_len, 1) + """ + batch_size, obs_history_len = pose10d.shape[0:2] + eef_xyz_wxyz = np.zeros((batch_size, obs_history_len, 7), dtype=np.float32) + eef_xyz_wxyz[:, :, :3] = pose10d[:, :, :3] + eef_xyz_wxyz[:, :, 3:] = rot_6d_to_quat_wxyz_batch(pose10d[:, :, 3:9]) + gripper_width = pose10d[:, :, 9:] + + return eef_xyz_wxyz, gripper_width + + +# def pos_axang_to_mat(position: npt.NDArray[Any], axis_angle: npt.NDArray[Any]) -> npt.NDArray[Any]: +# """ +# Convert a position and axis-angle to a 4x4 matrix. +# position: (3, ) or (N, 3) +# axis_angle: (3, ) or (N, 3) +# return: (4, 4) or (N, 4, 4) +# """ +# pass + +# def mat_to_pose10d(mat: npt.NDArray[Any]) -> npt.NDArray[Any]: +# """ +# Convert a 4x4 matrix to a 10D pose. +# mat: (4, 4) or (N, 4, 4) +# return: (10, ) or (N, 10) +# """ +# if len(mat.shape) == 3: +# pose10d = np.zeros((mat.shape[0], 10)) +# pose10d[:, :3] = mat[:, :3, 3] +# pose10d[:, 3:] = quat_wxyz_to_rot_6d(mat[:, :3, :3]) +# else: +# pose10d = np.zeros(10) +# pose10d[:3] = mat[:3, 3] +# pose10d[3:] = quat_wxyz_to_rot_6d(mat[:3, :3]) + + +if __name__ == "__main__": + pose_1 = np.random.rand(7) + pose_1[3:] /= np.linalg.norm(pose_1[3:]) + pose_2 = np.random.rand(7) + pose_2[3:] /= np.linalg.norm(pose_2[3:]) + + pose_2_relative_to_pose_1 = get_relative_pose(pose_2, pose_1) + absolute_pose_2 = get_absolute_pose(pose_1, pose_2_relative_to_pose_1) + assert np.allclose(pose_2, absolute_pose_2) + pose_1_relative_to_pose_2 = get_relative_pose(pose_1, pose_2) + absolute_pose_1 = get_absolute_pose(pose_2, pose_1_relative_to_pose_2) + assert np.allclose(pose_1, absolute_pose_1) + inverted_pose_2_relative_to_pose_1 = invert_pose(pose_2_relative_to_pose_1) + assert np.allclose(inverted_pose_2_relative_to_pose_1, pose_1_relative_to_pose_2) + + assert np.allclose(pose_1, invert_pose(invert_pose(pose_1))) + assert np.allclose(pose_2, invert_pose(invert_pose(pose_2))) + + rot_6d = quat_wxyz_to_rot_6d(pose_1[3:]) + quat_wxyz = rot_6d_to_quat_wxyz(rot_6d) + assert np.allclose(quat_wxyz, pose_1[3:]) + + rot_6d = quat_wxyz_to_rot_6d(pose_2[3:]) + quat_wxyz = rot_6d_to_quat_wxyz(rot_6d) + assert np.allclose(quat_wxyz, pose_2[3:]) + + print("Test passed") diff --git a/cosmos_training/cosmos/data/vfm/action/umi/transforms.py b/cosmos_training/cosmos/data/vfm/action/umi/transforms.py new file mode 100644 index 00000000..ea760694 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi/transforms.py @@ -0,0 +1,77 @@ +from typing import Any, Union + +import kornia.augmentation as K +import torch +import torch.nn as nn +from einops import rearrange + +from cosmos.data.vfm.action.umi.data_classes import DataMeta + + +class BaseTransforms: + def __init__(self, data_meta: dict[str, DataMeta], apply_image_augmentation_in_cpu: bool): + self.transforms: dict[str, Union[K.VideoSequential, nn.Sequential]] = {} + self.data_meta: dict[str, DataMeta] = data_meta + self.apply_image_augmentation_in_cpu: bool = apply_image_augmentation_in_cpu + for entry_meta in data_meta.values(): + transforms_list = [] + for aug_cfg in entry_meta.augmentation: + aug_name = aug_cfg["name"] + aug_cfg.pop("name") + if entry_meta.data_type == "image": + if aug_name in K.__dict__: + transform_cls = K.__dict__[aug_name] + else: + raise ValueError(f"Augmentation {aug_name} not found in kornia.augmentation.") + elif entry_meta.data_type == "low_dim": + raise ValueError( + f"Augmentation {aug_name} not found in low dim transforms. Please implement your own augmentation method." + ) + + transforms_list.append(transform_cls(**aug_cfg)) + if len(transforms_list) > 0: + if entry_meta.data_type == "image": + self.transforms[entry_meta.name] = K.VideoSequential(*transforms_list) + else: + self.transforms[entry_meta.name] = nn.Sequential(*transforms_list) + + def to(self, device: Union[torch.device, str]): + for transform in self.transforms.values(): + transform.to(device) + + def apply(self, data_dict: dict[str, Any], consistent_on_batch: bool = False) -> dict[str, torch.Tensor]: + for name, data in data_dict.items(): + if not self.apply_image_augmentation_in_cpu and self.data_meta[name].data_type == "image": + continue + if isinstance(data, dict): + data_dict[name] = self.apply(data, consistent_on_batch) + elif isinstance(data, torch.Tensor): + if name in self.transforms: + batch_size, traj_len, *shape = data.shape + if consistent_on_batch: + data = data.reshape(1, batch_size * traj_len, *shape) + + data_dim_num = len(self.data_meta[name].shape) + new_data_dim_num = len(data.shape) + squeeze_data = False + if new_data_dim_num - data_dim_num == 1: + data = data.unsqueeze(0) + squeeze_data = True + elif new_data_dim_num - data_dim_num != 2: + raise ValueError( + f"Data {name} has more than 2 additional dimensions: {data.shape}. Currently only support (traj_len, *shape) or (batch_size, traj_len, *shape)." + ) + try: + data = self.transforms[name](data) + except Exception as e: + print(f"Error applying transform {name} to data {data.shape}: {e}") + raise e + if squeeze_data: + data = data.squeeze(0) + if consistent_on_batch: + data = rearrange(data, "1 (b t) ... -> b t ...", b=batch_size, t=traj_len) # [B,T,...] + data_dict[name] = data + + else: + raise ValueError(f"Unknown data type {type(data)} for {name}") + return data_dict diff --git a/cosmos_training/cosmos/data/vfm/action/umi_dataset.py b/cosmos_training/cosmos/data/vfm/action/umi_dataset.py new file mode 100644 index 00000000..81c110a2 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi_dataset.py @@ -0,0 +1,1223 @@ +import copy +import json +import multiprocessing +import os +import re +import sys +from typing import Any, Literal, Optional, Union, cast + +import numpy as np +import numpy.typing as npt +import torch +import tqdm +import zarr +from omegaconf import DictConfig, OmegaConf +from torch.utils.data import Dataset + +from cosmos.utils import log +from cosmos.data.vfm.action.umi.data_classes import ( + DataMeta, + SourceDataMeta, + construct_data_meta, + construct_source_data_meta, +) +from cosmos.data.vfm.action.umi.data_utils import aggregate_batch +from cosmos.data.vfm.action.umi.imagecodecs_numcodecs import register_codecs +from cosmos.data.vfm.action.umi.normalizer import FixedNormalizer +from cosmos.data.vfm.action.umi.transforms import BaseTransforms + +_ACTION_DIR = os.path.dirname(os.path.abspath(__file__)) +_BUNDLED_NORMALIZERS_DIR = os.path.join(_ACTION_DIR, "normalizers") + + +def _resolve_normalizer_path(normalizer_dir: str, dataset_name: str) -> str | None: + """Resolve normalizer JSON path from bundled files shipped with the repo.""" + if normalizer_dir.endswith(".json"): + filename = os.path.basename(normalizer_dir) + else: + filename = f"{dataset_name}_normalizer.json" + path = os.path.join(_BUNDLED_NORMALIZERS_DIR, filename) + return path if os.path.exists(path) else None + + +register_codecs() +import random + +import hydra + +from cosmos.data.vfm.action.domain_utils import get_domain_id +from cosmos.data.vfm.action.pose_utils import build_abs_pose_from_components, pose_abs_to_rel + + +class BaseDataset(Dataset[dict[str, torch.Tensor]]): + """ + Base class for all datasets. + """ + + def __init__( + self, + root: str, + # The folder that contains all the zarr stores + # data path should be: root/name/episode_data.zarr or root/name.zarr + name: str, + robot_num: int, + # compressed_dir: str, # The folder that contains the lz4 compressed data (compressed_dir/name.lz4) + # If dataset is not found in root, the program will extract the data from compressed_dir + include_episode_num: int, + include_episode_indices: list[int], + used_episode_ratio: float, + index_pool_size_per_episode: int, + history_padding_length: int, + future_padding_length: int, + seed: int, + source_data_meta: Union[dict[str, dict[str, Any]], DictConfig], + output_data_meta: Union[dict[str, dict[str, Any]], DictConfig], + starting_percentile_max: float, + starting_percentile_min: float, + apply_image_augmentation_in_cpu: bool, + use_relative_pose: bool, + relative_pose_mode: Literal["frame_wise", "anchored"], + use_relative_gripper_width: bool, + normalizer_sample_num: int, + normalizer_dir: str, + repeat_dataset_num: int, + mode: str, + image_size: int, + use_grasp_state_repr: bool, + eef_z_offset: float, + use_eef_pose: bool, + **unused_kwargs, + ): + log.info(f"BaseDataset unused_kwargs: {unused_kwargs}") + + if not use_grasp_state_repr: + output_data_meta.pop("grasp_states") + + if "eef_poses" not in output_data_meta: + raise ValueError("eef_poses is not in output_data_meta") + + self.use_eef_pose: bool = use_eef_pose + + if name.endswith(".zarr"): + name = name.replace(".zarr", "") + + if "/" in name: + # HACK: For re-organized UMI datasets + assert len(name.split("/")) == 2, f"name {name} should be in the format of 'project_name/dataset_name'" + data_project_name = name.split("/")[0] + name = name.split("/")[1] + root = os.path.join(root, data_project_name) + # compressed_dir = os.path.join(compressed_dir, data_project_name) + assert normalizer_dir == "" or normalizer_dir.endswith(".json"), ( + f"normalizer_dir {normalizer_dir} should be a json file or empty" + ) + + # os.makedirs(root, exist_ok=True) + + assert isinstance(robot_num, int) and robot_num >= 1, ( + f"robot_num must be an integer greater than 0, but got {robot_num}." + ) + self.robot_num: int = robot_num + + # if not torch.cuda.is_available() or torch.cuda.current_device() == 0: + # if os.path.exists(os.path.join(root, name, "episode_data.zarr")): + # zarr_path = os.path.join(root, name, "episode_data.zarr") + # elif os.path.exists(os.path.join(root, name + ".zarr")): + # zarr_path = os.path.join(root, name + ".zarr") + # elif os.path.exists(os.path.join(root, name)): + # zarr_path = os.path.join(root, name) + # else: + # print(f"Dataset {name} not found in {root}") + # print( + # f"Checked paths: {os.path.join(root, name, 'episode_data.zarr')}, {os.path.join(root, name + '.zarr')}, {os.path.join(root, name)}" + # ) + + # lz4_path = os.path.join(compressed_dir, name + ".tar.lz4") + # lz4_zarr_path = os.path.join(compressed_dir, name + ".zarr.tar.lz4") + # if os.path.exists(lz4_path): + # print(f"Extracting dataset {name} from {compressed_dir} to {root}") + # subprocess.run( + # [f"lz4 -d -c {lz4_path} | tar xf - -C {root}"], + # cwd=root, + # shell=True, + # check=True, + # ) + # elif os.path.exists(lz4_zarr_path): + # print(f"Extracting dataset {name} from {compressed_dir} to {root}") + # subprocess.run(f"mkdir -p {root}/{name}.zarr", shell=True) + # subprocess.run( + # [f"lz4 -d -c {lz4_zarr_path} | tar xf - -C {root}/{name}.zarr --strip-components=1"], + # cwd=root, + # shell=True, + # check=True, + # ) + # else: + # # For raw UMI datasets + # zip_path = os.path.join(compressed_dir, name + ".zarr.zip") + # if os.path.exists(zip_path): + # print(f"Extracting dataset {name} from {compressed_dir} to {root}") + # subprocess.run( + # [f"unzip {zip_path} -d {root}"], + # cwd=root, + # shell=True, + # check=True, + # ) + # print(f"Dataset {name} extracted to {root}") + # else: + # raise FileNotFoundError(f"Dataset {name} not found in {root} or {compressed_dir}") + # if dist.is_available() and dist.is_initialized(): + # dist.barrier() + if os.path.exists(os.path.join(root, name, "episode_data.zarr")): + zarr_path = os.path.join(root, name, "episode_data.zarr") + elif os.path.exists(os.path.join(root, name + ".zarr")): + zarr_path = os.path.join(root, name + ".zarr") + elif os.path.exists(os.path.join(root, name)): + zarr_path = os.path.join(root, name) + else: + raise FileNotFoundError(f"Dataset {name} not found in {root}") + + self.zarr_path: str = zarr_path + self.name: str = name + log.info(f"Dataset name: {self.name}") + + self.include_episode_num: int = include_episode_num + self.include_episode_indices: list[int] = include_episode_indices + self.used_episode_ratio: float = used_episode_ratio + self.index_pool_size_per_episode: int = index_pool_size_per_episode + self.history_padding_length: int = history_padding_length + self.future_padding_length: int = future_padding_length + self.seed: int = seed + self.rng: np.random.Generator = np.random.default_rng(seed) + self.starting_percentile_max: float = starting_percentile_max + self.starting_percentile_min: float = starting_percentile_min + self.apply_image_augmentation_in_cpu: bool = apply_image_augmentation_in_cpu + self.use_relative_pose: bool = use_relative_pose + assert relative_pose_mode in ["frame_wise", "anchored"], ( + f"relative_pose_mode must be one of frame_wise or anchored, but got {relative_pose_mode}" + ) + self.relative_pose_mode: Literal["frame_wise", "anchored"] = relative_pose_mode + self.use_relative_gripper_width: bool = use_relative_gripper_width + self.mode: str = mode + self.image_size: int = image_size + self.domain_id: int = get_domain_id("umi") + + if os.path.exists(f"{self.zarr_path}/../sim_config.yaml"): + log.info(f"sim_config.yaml found in {self.zarr_path}/../") + sim_config_dict = OmegaConf.load(f"{self.zarr_path}/../sim_config.yaml") + self.sim_config_str = OmegaConf.to_yaml(sim_config_dict, resolve=False) + else: + self.sim_config_str = "" + + log.info(f"{self.zarr_path=}") + zarr_store = zarr.open(self.zarr_path, mode="r") + assert isinstance(zarr_store, zarr.Group), f"zarr store {self.zarr_path} is not a group." + self.zarr_store: zarr.Group = zarr_store + + assert len(source_data_meta) > 0, "source_data_meta is empty." + self.source_data_meta: dict[str, SourceDataMeta] = construct_source_data_meta(source_data_meta) + + assert len(output_data_meta) > 0, "output_data_meta is empty." + self.output_data_meta: dict[str, DataMeta] = construct_data_meta(output_data_meta) + + self.max_history_length: int = max( + 0, + -min(entry_meta.include_indices[0] for entry_meta in self.source_data_meta.values()), + ) + self.max_future_length: int = max( + 0, + max(entry_meta.include_indices[-1] for entry_meta in self.source_data_meta.values()), + ) + if self.history_padding_length > self.max_history_length: + raise ValueError( + f"history_padding_length {self.history_padding_length} is larger than max_history_length {self.max_history_length}. This may cause ambiguity in the data." + ) + + self.normalizer: Optional[FixedNormalizer] = None + self.normalizer_dir: str = normalizer_dir + if normalizer_dir != "": + normalizer_path = _resolve_normalizer_path(normalizer_dir, self.name) + if normalizer_path is not None: + log.info(f"Loading normalizer from {normalizer_path}.") + self.normalizer = FixedNormalizer(self.output_data_meta) + with open(normalizer_path) as f: + self.normalizer.from_dict(json.load(f)) + self.normalizer.to(torch.device("cpu")) + log.info(f"Normalizer loaded from {normalizer_path}.") + else: + raise FileNotFoundError(f"No normalizer found for normalizer_dir={normalizer_dir}.") + else: + log.info(f"normalizer_dir is empty.") + self.normalizer_sample_num: int = normalizer_sample_num + + self.store_episode_num: int + self.used_episode_indices: list[int] + self.used_episode_num: int + + self.index_pool: list[tuple[int, int]] = [] + """ + index_pool has self.store_episode_num * self.used_episode_ratio * self.index_pool_size_per_episode items. + Each item contains a tuple of (episode_idx, index), where index means the 0 index of this trajectory in an episode. + """ + + self.episode_frame_nums: dict[int, int] = {} + self.episode_valid_indices_min: dict[int, int] = {} + self.episode_valid_indices_max: dict[int, int] = {} # Exclusive + + self.transforms: BaseTransforms = BaseTransforms(self.output_data_meta, self.apply_image_augmentation_in_cpu) + + self.repeat_dataset_num: float = repeat_dataset_num + self.use_grasp_state_repr: bool = use_grasp_state_repr + self.eef_z_offset: float = eef_z_offset + + def _check_data_validity(self): + raise NotImplementedError("This method should be implemented in subclasses.") + + def _get_single_traj_data(self, episode_idx: int, traj_idx: int, output_entry_names: list[str] | None = None): + raise NotImplementedError("This method should be implemented in subclasses.") + + def _create_index_pool(self): + self.index_pool = [] + for episode_idx in self.used_episode_indices: + valid_idx_min = self.episode_valid_indices_min[episode_idx] + valid_idx_max = self.episode_valid_indices_max[episode_idx] + # valid_idx_min <= sample_idx < valid_idx_max + + zero_idx_max = valid_idx_min + int( + (valid_idx_max - valid_idx_min) * self.starting_percentile_max + ) # Exclusive + zero_idx_min = valid_idx_min + int( + (valid_idx_max - valid_idx_min) * self.starting_percentile_min + ) # Inclusive + + if self.index_pool_size_per_episode == -1: + index_pool_size = zero_idx_max - zero_idx_min + else: + assert self.index_pool_size_per_episode > 0, ( + f"index_pool_size_per_episode must be positive or -1, but got {self.index_pool_size_per_episode}." + ) + index_pool_size = self.index_pool_size_per_episode + + if index_pool_size >= zero_idx_max - zero_idx_min: + indices = np.arange(zero_idx_min, zero_idx_max) + random_indices = self.rng.choice( + range(zero_idx_min, zero_idx_max), + size=index_pool_size - (zero_idx_max - zero_idx_min), + replace=True, + ) + indices = np.concatenate([indices, random_indices]) + else: + indices = self.rng.choice( + range(zero_idx_min, zero_idx_max), + size=index_pool_size, + replace=False, + ) + indices = np.sort(indices) + for index in indices: + self.index_pool.append((episode_idx, index)) + + def _update_episode_indices(self): + if len(self.include_episode_indices) > 0: + log.info(f"Dataset {self.name}: Using specified episode indices: {self.include_episode_indices}.") + self.include_episode_num = len(self.include_episode_indices) + for episode_idx in self.include_episode_indices: + assert episode_idx < self.store_episode_num, ( + f"episode_idx {episode_idx} is out of range. Max is {self.store_episode_num}." + ) + else: + if self.include_episode_num > 0: + assert self.include_episode_num <= self.store_episode_num, ( + f"include_episode_num {self.include_episode_num} is greater than the number of episodes {self.store_episode_num}." + ) + self.include_episode_indices = self.rng.choice( + self.store_episode_num, size=self.include_episode_num, replace=False + ).tolist() + log.info( + f"Dataset {self.name}: Using {self.include_episode_num} episodes from {self.store_episode_num} episodes: {self.include_episode_indices}" + ) + elif self.include_episode_num == -1: + self.include_episode_num = self.store_episode_num + self.include_episode_indices = list(range(self.include_episode_num)) + log.info( + f"Dataset {self.name}: Using all {self.include_episode_num} episodes from {self.store_episode_num}" + ) + else: + raise ValueError( + f"include_episode_num {self.include_episode_num} is invalid. Must be -1 or a positive integer." + ) + + self.include_episode_indices = sorted(self.include_episode_indices) + + self.used_episode_indices = cast( + list[int], + self.rng.choice( + self.include_episode_indices, + size=int(self.include_episode_num * self.used_episode_ratio), + replace=False, + ).tolist(), + ) + self.used_episode_indices = sorted(self.used_episode_indices) + self.used_episode_num = len(self.used_episode_indices) + + def repeat_dataset(self, repeat_num: float | None = None): + if repeat_num is None: + repeat_num = self.repeat_dataset_num + else: + self.repeat_dataset_num = repeat_num + index_pool_size = len(self.index_pool) + repeated_size = int(index_pool_size * repeat_num) + repeated_indices = self.rng.choice( + range(index_pool_size), + size=repeated_size, + replace=True, + ) + self.index_pool = [self.index_pool[i] for i in repeated_indices] + + def split_unused_episodes( + self, + remaining_ratio: float = 1.0, + other_used_episode_indices: Optional[list[int]] = None, + ): + """ + Split unused episodes from the included episodes. + """ + log.info( + f"Splitting unused data with remaining ratio {remaining_ratio} and other used episode ids {other_used_episode_indices}." + ) + unused_dataset = copy.deepcopy(self) + unused_dataset.rng = np.random.default_rng(unused_dataset.seed) + if other_used_episode_indices is None: + other_used_episode_indices = [] + unused_episode_indices = [ + episode_idx + for episode_idx in self.include_episode_indices + if episode_idx not in self.used_episode_indices and episode_idx not in other_used_episode_indices + ] + unused_dataset.used_episode_indices = cast( + list[int], + self.rng.choice( + unused_episode_indices, + size=int(len(unused_episode_indices) * remaining_ratio), + replace=False, + ).tolist(), + ) + unused_dataset.used_episode_indices = sorted(unused_dataset.used_episode_indices) + unused_dataset.used_episode_ratio = len(unused_dataset.used_episode_indices) / len( + unused_dataset.include_episode_indices + ) + unused_dataset._check_data_validity() + unused_dataset._create_index_pool() + assert len(unused_dataset) >= 1, ( + f"Splitted dataset {unused_dataset.name} has no data. Please check the used_data_ratio and the overall dataset size" + ) + return unused_dataset + + def sample_data( + self, + output_entry_names: list[str], + sample_num: int, + augment_data: bool, + normalize_data: bool, + sampled_indices: npt.NDArray[np.int64] | None = None, + ) -> dict[str, torch.Tensor]: + raise NotImplementedError("This method should be implemented in subclasses.") + + def calc_stats_worker_single_process(self, start_idx: int, end_idx: int, entry_names: list[str]): + data: dict[str, torch.Tensor] = self.sample_data( + sample_num=end_idx - start_idx, + output_entry_names=entry_names, + augment_data=False, + normalize_data=False, + sampled_indices=np.arange(start_idx, end_idx), + ) + min_vals: dict[str, torch.Tensor] = {} + max_vals: dict[str, torch.Tensor] = {} + for entry_name in entry_names: + # data[entry_name]: (batch_size, traj_length, ...) + min_vals[entry_name] = torch.min(torch.min(data[entry_name], dim=0).values, dim=0).values + max_vals[entry_name] = torch.max(torch.max(data[entry_name], dim=0).values, dim=0).values + return min_vals, max_vals + + def calc_stats(self, entry_names: list[str], process_num: int = 0): + # assert process_num > 0 + if process_num == 0: + process_num = min(20, multiprocessing.cpu_count() - 2) + log.info(f"Calculating stats with {process_num} processes.") + + index_pool_size = len(self.index_pool) + indices_per_process = index_pool_size // process_num + start_indices = np.arange(0, index_pool_size, indices_per_process)[:process_num] + end_indices = start_indices + indices_per_process + end_indices[-1] = index_pool_size + + with multiprocessing.Pool(process_num) as pool: + results = pool.starmap( + self.calc_stats_worker_single_process, zip(start_indices, end_indices, [entry_names] * process_num) + ) + + stats: dict[str, dict[str, torch.Tensor]] = {} + + for result in results: + for entry_name in entry_names: + if entry_name not in stats: + stats[entry_name] = { + "min": result[0][entry_name], + "max": result[1][entry_name], + } + else: + stats[entry_name]["min"] = torch.min(stats[entry_name]["min"], result[0][entry_name]) + stats[entry_name]["max"] = torch.max(stats[entry_name]["max"], result[1][entry_name]) + + return stats + + def fit_normalizer(self) -> FixedNormalizer: + log.info(f"Fitting normalizer for {self.name}.") + self.normalizer = FixedNormalizer(self.output_data_meta) + + normalize_entries = [ + entry_meta.name for entry_meta in self.output_data_meta.values() if entry_meta.normalizer != "identity" + ] + + stats = self.calc_stats( + entry_names=normalize_entries, + ) + log.info(f"Stats: {stats}") + + self.normalizer.from_dict(stats) + + self.normalizer.to(torch.device("cpu")) + + normalizer_state_dict = self.normalizer.as_dict("list") + + if self.normalizer_dir != "": + os.makedirs(self.normalizer_dir, exist_ok=True) + normalizer_path = os.path.join(self.normalizer_dir, f"{self.name}_normalizer.json") + with open(normalizer_path, "w") as f: + json.dump(normalizer_state_dict, f) + log.info(f"Normalizer dict saved to {normalizer_path}.") + + return self.normalizer + + def process_image_data(self, data: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + if ( + data.shape[-1] <= 4 + ): # (..., H, W, C) where the color dimension is usually a small number (1 (grayscale), 3 (RGB), or 4 (RGBD)) + dims = len(data.shape) + data = data.transpose((*range(dims - 3), -1, -3, -2)) # (..., C, H, W) + + if data.dtype == np.uint8: + return (data / 255.0).astype(np.float32) + + return data + + def __len__(self) -> int: + return len(self.index_pool) + + def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: + raise NotImplementedError("This method should be implemented in subclasses.") + + +class AggregatedDataset(BaseDataset): + """ + Dataset loader for the iPhUMI dataset. + Example structure: + / + ├── data + │ ├── camera0_main_rgb (N, image_size, image_size, 3) uint8 + │ ├── camera0_ultrawide_rgb (N/6, image_size, image_size, 3) uint8 + │ ├── robot0_demo_end_pose (N, 6) float64 + │ ├── robot0_demo_start_pose (N, 6) float64 + │ ├── robot0_eef_pos (N, 3) float32 + │ ├── robot0_eef_rot_axis_angle (N, 3) float32 + │ └── robot0_gripper_width (N, 1) float32 + └── meta + └── episode_ends (K,) int64 + + """ + + def __init__( + self, + down_sample_steps: int, + **kwargs, + ): + self.down_sample_steps: int = down_sample_steps + kwargs["history_padding_length"] = kwargs["history_padding_length"] * down_sample_steps + kwargs["future_padding_length"] = kwargs["future_padding_length"] * down_sample_steps + for meta in kwargs["source_data_meta"].values(): + meta["include_indices"] = [i * down_sample_steps for i in meta["include_indices"]] + + super().__init__(**kwargs) + + data_store = self.zarr_store["data"] + assert isinstance(data_store, zarr.Group) + + self.data_store: zarr.Group = data_store + self.data_store_keys: list[str] = list(data_store.keys()) + + self.episode_ends: npt.NDArray[np.int64] = np.array(self.zarr_store["meta"]["episode_ends"]) + self.store_episode_num: int = len(self.episode_ends) + + self._update_episode_indices() + + self.episode_starts: npt.NDArray[np.int64] = np.zeros_like(self.episode_ends) + + for i, end in enumerate(self.episode_ends): + if i == 0: + self.episode_starts[i] = 0 + else: + self.episode_starts[i] = self.episode_ends[i - 1] + + self.episode_frame_nums[i] = end - self.episode_starts[i] + self.episode_valid_indices_min[i] = self.max_history_length - self.history_padding_length + self.episode_valid_indices_max[i] = ( + self.episode_frame_nums[i] + self.future_padding_length - self.max_future_length + ) + + self._create_index_pool() + + log.info( + f"Dataset: {self.name}, store_episode_num: {self.store_episode_num}, include_episode_num: {self.include_episode_num}, used_episode_num: {self.used_episode_num}" + ) + + def _check_data_validity(self): + # Not implemented yet. Will skip checking for now. + pass + + def _process_source_data(self, data_dict: dict[str, npt.NDArray[Any]]) -> dict[str, npt.NDArray[Any]]: + # Override this function to process the source data + raise NotImplementedError("This function should be overridden by the subclass.") + + def _get_single_traj_data( + self, + episode_idx: int, + traj_idx: int, + output_entry_names: list[str] | None = None, + ): + episode_length = self.episode_frame_nums[episode_idx] + start_idx = self.episode_starts[episode_idx] + + source_data_dict: dict[str, Any] = {} + + if output_entry_names is not None and len(output_entry_names) > 0: + source_entry_names = [ + self.output_data_meta[target_entry_name].source_entry_names for target_entry_name in output_entry_names + ] + source_entry_names = [item for sublist in source_entry_names for item in sublist] + else: + source_entry_names = None + + for entry_meta in self.source_data_meta.values(): + if ( + source_entry_names is not None + and len(source_entry_names) > 0 + and entry_meta.name not in source_entry_names + ): + # Skip the entries that are not needed to make data loading faster + continue + + if entry_meta.name not in self.data_store_keys: + continue + indices = [traj_idx + i for i in entry_meta.include_indices] + # Crop the indices to the valid range. Will introduce padding if the indices are out of range. + indices = [(0 if i < 0 else episode_length - 1 if i >= episode_length else i) for i in indices] + global_indices = [start_idx + i for i in indices] + if entry_meta.name.endswith("ultrawide_rgb"): + global_indices = self.zarr_store["meta"][f"upsample_index_{entry_meta.name}"][global_indices] + + source_data_dict[entry_meta.name] = np.array(self.data_store[entry_meta.name][global_indices]) + + processed_data_dict = self._process_source_data(source_data_dict) + + torch_data_dict: dict[str, Any] = {} + torch_data_dict["episode_idx"] = torch.tensor([episode_idx]) # [1] + torch_data_dict["traj_idx"] = torch.tensor([traj_idx]) # [1] + + for entry_meta in self.output_data_meta.values(): + if ( + output_entry_names is not None + and len(output_entry_names) > 0 + and entry_meta.name not in output_entry_names + ): + # Skip the entries that are not needed to make data loading faster + continue + + assert entry_meta.name in processed_data_dict + processed_data = processed_data_dict[entry_meta.name] + if isinstance(processed_data, np.ndarray): + if entry_meta.data_type == "image": + processed_data = self.process_image_data(processed_data) # -> (..., C, H, W), float32 + processed_data = torch.from_numpy(processed_data) + + torch_data_dict[entry_meta.name] = processed_data + + # Pass through initial_pose (non-normalized route only). + if "initial_pose" in processed_data_dict: + torch_data_dict["initial_pose"] = torch.from_numpy(processed_data_dict["initial_pose"]) + + return torch_data_dict + + def __getitem__(self, idx: int): + """ + output_data_dict: + video: (C, T, H, W) uint8 + camera_poses: (T, 9) # This is actually the TCP pose: the middle point of the gripper tip + eef_poses: (T, 9) (xyz, rot6d), is defined self.eef_z_offset behind the gripper tip + eef_commands: (T, 1) + t5_text_embeddings: (512, 1024) + t5_text_mask: (512,) int64 + fps: int + __key__: int + + if self.use_grasp_state_repr: + grasp_states: (T, 6) (left-finger-xyz, right-finger-xyz) wrt eef_poses + """ + episode_idx, traj_idx = self.index_pool[idx] + + # skip data if corrupted + while True: + try: + torch_data_dict = self._get_single_traj_data(episode_idx, traj_idx) + break + except Exception: + import traceback + + traceback.print_exc() + log.warning( + f"UMI Dataset WARNING: Corrupted data found at episode_idx: {episode_idx}, traj_idx: {traj_idx}. Randomly selecting a new data.", + ) + random_idx = self.rng.choice(len(self.index_pool)) + episode_idx, traj_idx = self.index_pool[random_idx] + + # Pop initial_pose before transforms (transforms crash on unknown keys). + initial_pose = torch_data_dict.pop("initial_pose", None) + + torch_data_dict = self.transforms.apply(torch_data_dict) + + if self.normalizer is not None: + torch_data_dict = self.normalizer.normalize(torch_data_dict) + + # Re-add initial_pose (non-normalized route only, for viewer). + if initial_pose is not None: + torch_data_dict["initial_pose"] = initial_pose + + for entry_meta in self.output_data_meta.values(): + assert torch_data_dict[entry_meta.name].shape == ( + entry_meta.length, + *entry_meta.shape, + ), ( + f"entry_meta: {entry_meta.name}, torch_data_dict[entry_meta.name].shape: {torch_data_dict[entry_meta.name].shape}, entry_meta.length: {entry_meta.length}, entry_meta.shape: {entry_meta.shape}" + ) + + # resize to image_size x image_size if needed + if torch_data_dict["video"].shape[-2:] != (self.image_size, self.image_size): + torch_data_dict["video"] = torch.nn.functional.interpolate( + torch_data_dict["video"], size=(self.image_size, self.image_size), mode="bilinear" + ) + torch_data_dict["video"] = (torch_data_dict["video"] * 255.0).to(torch.uint8) + + if torch_data_dict["video"].shape[1] == 3: # [T,C,H,W] + torch_data_dict["video"] = torch_data_dict["video"].permute(1, 0, 2, 3) # [C,T,H,W] + + torch_data_dict["__key__"] = torch.tensor([idx], dtype=torch.long) # [1] + torch_data_dict["conditioning_fps"] = torch.tensor(60 / self.down_sample_steps, dtype=torch.long) # scalar + + if self.mode == "joint": + mode = random.choice(["forward_dynamics", "inverse_dynamics", "policy"]) + else: + mode = self.mode + torch_data_dict["mode"] = mode + + _name = re.sub(r"(_v?\d+)+$", "", self.name) + torch_data_dict["ai_caption"] = _name.replace("_", " ") + torch_data_dict["domain_id"] = torch.tensor([self.domain_id], dtype=torch.long) # [1] + torch_data_dict["viewpoint"] = "wrist_view" + + if not self.use_grasp_state_repr: + # Use eef_poses or camera_poses for the action based on self.use_eef_pose + action = torch.cat( + [ + torch_data_dict["eef_poses"] if self.use_eef_pose else torch_data_dict["camera_poses"], + torch_data_dict["eef_commands"], + ], + dim=1, + ).to(torch.float32) # [T,camera_pose_dim+1] + else: + action = torch.cat( + [ + torch_data_dict["eef_poses"] if self.use_eef_pose else torch_data_dict["camera_poses"], + torch_data_dict["grasp_states"], + ], + dim=1, + ).to(torch.float32) # [T,eef_pose_dim+grasp_state_dim] + + torch_data_dict["action"] = action + + return torch_data_dict + + def sample_data( + self, + output_entry_names: list[str], + sample_num: int, + augment_data: bool, + normalize_data: bool, + sampled_indices: npt.NDArray[np.int64] | None = None, + ) -> dict[str, torch.Tensor]: + if sample_num == -1: + sample_num = len(self.index_pool) + + if sampled_indices is None: + sampled_indices = self.rng.choice( + len(self.index_pool), min(sample_num, len(self.index_pool)), replace=False + ) + else: + assert len(sampled_indices) == sample_num, ( + f"sampled_indices should be of length {sample_num}, but got {len(sampled_indices)}" + ) + + samples = [] + log.info(f"Sampling {sample_num} data from {len(self.index_pool)} trajectories.") + for idx in tqdm.tqdm(sampled_indices): + episode_idx, traj_idx = self.index_pool[idx] + samples.append(self._get_single_traj_data(episode_idx, traj_idx, output_entry_names)) + + all_samples_data_dict: dict[str, torch.Tensor] = aggregate_batch(samples, aggregate_fn=torch.stack) + + return all_samples_data_dict + + +class MultiTaskDataset(Dataset[dict[str, torch.Tensor]]): + def __init__( + self, + name: str, + sub_dataset_target: str, + dataset_configs: dict[str, dict[str, Any]], + eager_load: bool = False, + **base_config: dict[str, Any], + ): + """ + name: if select certain sub-datasets, should be the name of multiple sub-datasets connected by "," + root, normalizer_dir should be the same for all datasets + dataset_configs: { + "dataset_name_1": { + "sample_ratio": 1.0, + "override_config_1": "value_1", + "override_config_2": "value_2", + ... + }, + ... + } + """ + + if isinstance(dataset_configs, DictConfig): + dataset_configs = cast(dict[str, dict[str, Any]], OmegaConf.to_container(dataset_configs)) + if ";" in name: + name = name.replace(";", ",") + if "," in name: + dataset_names = name.split(",") + dataset_configs = {name: dataset_configs[name] for name in dataset_names} + + log.info(f"dataset_configs: {dataset_configs}") + self.dataset_configs: dict[str, dict[str, Any]] = dataset_configs + assert len(self.dataset_configs) >= 1, "At least one dataset is required" + + if isinstance(base_config, DictConfig): + base_config = cast(dict[str, Any], OmegaConf.to_container(base_config)) + self.base_config: dict[str, Any] = base_config + + self.sample_ratios: dict[str, float] = { + name: config.pop("sample_ratio") for name, config in self.dataset_configs.items() + } + + + self._all_shard_roots = list(self.dataset_configs.keys()) + + self.datasets: dict[str, BaseDataset] = {} + self.index_pool: list[tuple[str, int]] = [] + + self.sub_dataset_target = sub_dataset_target + + if eager_load: + self._register_sources() + + def _register_sources(self, indices: list[int] | None = None): + if indices is None: + indices = list(range(len(self._all_shard_roots))) + log.info(f"Registering UMI Multi-Task datasets: {indices}") + + for idx in indices: + base_name = self._all_shard_roots[idx] + dataset_config = self.dataset_configs[base_name] + num_shards = dataset_config.get("num_shards", 0) + shard_config = {k: v for k, v in dataset_config.items() if k != "num_shards"} + if num_shards > 0: + for i in range(num_shards): + shard_name = f"{base_name}_v{i}" + log.info(f"Initializing dataset shard: {shard_name}") + config = copy.deepcopy(self.base_config) + config.update(copy.deepcopy(shard_config)) + config["name"] = shard_name + config["_target_"] = self.sub_dataset_target + self.datasets[shard_name] = hydra.utils.instantiate(config) + else: + log.info(f"Initializing dataset: {base_name}") + config = copy.deepcopy(self.base_config) + config.update(copy.deepcopy(shard_config)) + config["name"] = base_name + config["_target_"] = self.sub_dataset_target + self.datasets[base_name] = hydra.utils.instantiate(config) + + self._create_index_pool() + + @property + def normalizer(self) -> FixedNormalizer | None: + return next(iter(self.datasets.values())).normalizer + + @property + def action_dim(self) -> int: + """ """ + return 10 + + def _create_index_pool(self): + self.index_pool = [] + for dataset_name, dataset in self.datasets.items(): + self.index_pool.extend((dataset_name, i) for i in range(len(dataset))) + + def __len__(self): + return len(self.index_pool) + + def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: + dataset_name, data_idx = self.index_pool[idx] + data = self.datasets[dataset_name][data_idx] + dataset_index = list(self.datasets.keys()).index(dataset_name) + data["dataset_index"] = torch.tensor([dataset_index]) # [1] + data["data_index"] = torch.tensor([data_idx]) # [1] + return data + + def split_unused_episodes( + self, + remaining_ratio: float = 1.0, + other_used_episode_indices: list[int] | None = None, + ): + unused_dataset = copy.deepcopy(self) + unused_dataset.index_pool = [] + unused_dataset.datasets = {} + + for dataset_name, dataset in self.datasets.items(): + unused_dataset.datasets[dataset_name] = dataset.split_unused_episodes( + remaining_ratio, other_used_episode_indices + ) + unused_dataset._create_index_pool() + + return unused_dataset + + def repeat_dataset(self): + for dataset_name, dataset in self.datasets.items(): + dataset.repeat_dataset() + self._create_index_pool() + + +def _process_source_data( + self: AggregatedDataset, data_dict: dict[str, npt.NDArray[Any]] +) -> dict[str, npt.NDArray[Any]]: + """ + Will calculate the following data: + relative poses + poses wrt episode start + This step does not include normalization and data augmentation + Input data_dict: + camera0_main_rgb: (..., H, W, 3) uint8 + camera0_ultrawide_rgb: (..., H, W, 3) uint8 + robot0_demo_start_pose: (1, 6) float64 (optional) + robot0_eef_pos: (..., 3) float32 + robot0_eef_rot_axis_angle: (..., 3) float32 + robot0_gripper_width: (..., 1) float32 + + Output data_dict: + video: (C, T, H, W) uint8 + camera_poses: (T, 9) # 9dim: [pos_3d, rot_6d], rot_6d is the first two rows of the rotation matrix + eef_poses: (T, 9) (xyz, rot6d), is defined self.eef_z_offset behind the gripper tip + eef_commands: (T, 1) + t5_text_embeddings: (512, 1024) + t5_text_mask: (512,) int64 + fps: int + __key__: int + + if self.use_grasp_state_repr: + grasp_states: (T, 6) (left-finger-xyz, right-finger-xyz) wrt eef_poses + + """ + + processed_data_dict: dict[str, npt.NDArray[Any]] = {} + + pose_indices = self.source_data_meta["robot0_eef_pos"].include_indices + assert pose_indices == self.source_data_meta["robot0_eef_rot_axis_angle"].include_indices, ( + f"robot0_eef_pos and robot0_eef_rot_axis_angle must be aligned" + ) + + gripper_width_indices = self.source_data_meta["robot0_gripper_width"].include_indices + + ## Use absolute gripper width + if self.use_relative_gripper_width: + assert len(pose_indices) == len(gripper_width_indices), f"{len(pose_indices)=}, {len(gripper_width_indices)=}" + else: + assert len(pose_indices) == len(gripper_width_indices) + 1, ( + f"{len(pose_indices)=}, {len(gripper_width_indices)=}" + ) + + zero_idx = pose_indices.index(0) + + assert self.robot_num == 1, "Currently only support one robot" + + for i in range(self.robot_num): + assert len(pose_indices) == self.output_data_meta[f"camera_poses"].length + 1, ( + f"{len(pose_indices)=}, {self.output_data_meta[f'camera_poses'].length=}" + ) + + + camera_poses_meta = self.output_data_meta[f"camera_poses"] + camera_poses = np.zeros((camera_poses_meta.length, *camera_poses_meta.shape), dtype=np.float32) + + eef_commands_meta = self.output_data_meta[f"eef_commands"] + eef_commands = np.zeros((eef_commands_meta.length, *eef_commands_meta.shape), dtype=np.float32) + + video_meta = self.output_data_meta[f"video"] + if video_meta.source_entry_names[0] in data_dict: + video = data_dict[video_meta.source_entry_names[0]] + processed_data_dict["video"] = video + + if f"robot{i}_eef_pos" in data_dict and f"robot{i}_eef_rot_axis_angle" in data_dict: + pose_mat = build_abs_pose_from_components( + data_dict[f"robot{i}_eef_pos"], + data_dict[f"robot{i}_eef_rot_axis_angle"], + "axisangle", + ) + assert self.use_relative_pose, "Currently only support relative pose" + pose_convention = "backward_framewise" if self.relative_pose_mode == "frame_wise" else "backward_anchored" + + pose = pose_abs_to_rel(pose_mat, rotation_format="rot6d", pose_convention=pose_convention) + assert len(pose) == camera_poses_meta.length, ( + f"pose should be one step longer (zero index is excluded) than camera_poses_meta.length, but got {len(pose)} and {camera_poses_meta.length}" + ) + processed_data_dict[f"camera_poses"] = ( + pose # The camera poses here are actually TCP pose (the middle point of the gripper tip) + ) + + # Stash the absolute first-frame pose for viewer trajectory reconstruction. + # Only useful on the non-normalized route (normalizer_dir=""). + if self.normalizer is None: + processed_data_dict["initial_pose"] = pose_mat[zero_idx].astype(np.float32) + + offset_eef_pose_mat = pose_mat.copy() + offset_eef_pose_mat[:, :3, 3] += ( + offset_eef_pose_mat[:, :3, 2] * self.eef_z_offset + ) # Apply a z offset in the local coordinate frame instead of the global frame + offset_eef_pose = pose_abs_to_rel( + offset_eef_pose_mat, rotation_format="rot6d", pose_convention=pose_convention + ) + processed_data_dict[f"eef_poses"] = offset_eef_pose + + if f"robot{i}_gripper_width" in data_dict: + ## Use absolute gripper width + # eef_commands[:] = data_dict[f"robot{i}_gripper_width"] + # processed_data_dict[f"eef_commands"] = eef_commands + + assert not self.use_relative_gripper_width, "Currently only support absolute gripper width" + processed_gripper_width = data_dict[f"robot{i}_gripper_width"] + + eef_commands[:] = processed_gripper_width + processed_data_dict[f"eef_commands"] = eef_commands + + if self.use_grasp_state_repr: + processed_data_dict[f"grasp_states"] = np.zeros((eef_commands_meta.length, 6), dtype=np.float32) + processed_data_dict[f"grasp_states"][:, 2] = -self.eef_z_offset + processed_data_dict[f"grasp_states"][:, 5] = -self.eef_z_offset + processed_data_dict[f"grasp_states"][:, :1] = eef_commands / 2 + processed_data_dict[f"grasp_states"][:, 3:4] = -eef_commands / 2 + + # if f"robot{i}_demo_start_pose" in data_dict and f"robot{i}_eef_rot_axis_angle_wrt_start" in self.output_data_meta: + # # Calculate relative poses wrt episode start + # try: + # wrt_start_entry_meta = self.output_data_meta[ + # f"robot{i}_eef_rot_axis_angle_wrt_start" + # ] + # assert ( + # data_dict[f"robot{i}_demo_start_pose"].shape[0] == 1 + # ), "robot0_demo_start_pose must be (1, 6)" + # # HACK: add noise to episode start pose. Copied from the original UMI codebase. + # start_pose: npt.NDArray[np.float64] = data_dict[ + # f"robot{i}_demo_start_pose" + # ][0] + # start_pose += self.rng.normal( + # scale=[0.05, 0.05, 0.05, 0.05, 0.05, 0.05], + # size=start_pose.shape, + # ) + # start_pose_mat = pose_to_mat(start_pose) + # rel_pose_mat = convert_pose_mat_rep( + # pose_mat, + # base_pose_mat=start_pose_mat, + # pose_rep="relative", + # backward=False, + # ) + # except ValueError: + # # No wrt_start_entry_meta, so no relative poses wrt episode start + # # print(f"No wrt_start_entry_meta for robot{i}") + # pass + + return processed_data_dict + + +class iPhUMISingleTrajDataset(AggregatedDataset): + def __init__(self, align_to_ultrawide: bool, **kwargs): + """ + If align_to_ultrawide is True, the dataset will be aligned to the ultrawide camera. (will only use 1/6 of the data, but will guarantee the main camera is aligned to the ultrawide camera) + """ + if "camera0_ultrawide_rgb" in kwargs["source_data_meta"]: + # assert kwargs["down_sample_steps"] == 6, "down_sample_steps must be 6 for iPhUMI dataset if using ultrawide camera. This is because the ultrawide camera is captured at 10Hz while others are at 60Hz." + assert len(kwargs["source_data_meta"]["camera0_ultrawide_rgb"].include_indices) == 1, ( + "camera0_ultrawide_rgb should not include history, because it is captured at 10Hz" + ) + self.align_to_ultrawide: bool = align_to_ultrawide + + super().__init__(**kwargs) + if self.align_to_ultrawide: + assert self.source_data_meta["camera0_ultrawide_rgb"].include_indices == [0], ( + "camera0_ultrawide_rgb should only include the current frame" + ) + + def _create_index_pool(self): + super()._create_index_pool() + if self.align_to_ultrawide: + new_index_pool: list[tuple[int, int]] = [] + + ultrawide_indices: npt.NDArray[np.int32] = np.array( + self.zarr_store["meta"]["upsample_index_camera0_ultrawide_rgb"], dtype=np.int32 + ) + regular_indices: npt.NDArray[np.int32] = np.array( + self.zarr_store["meta"]["downsample_index_camera0_ultrawide_rgb"], dtype=np.int32 + ) + # Only use the data that is aligned to the ultrawide camera + for episode_idx, traj_idx in self.index_pool: + start_idx = self.episode_starts[episode_idx] + global_idx = start_idx + traj_idx + if regular_indices[ultrawide_indices[global_idx]] == global_idx: + new_index_pool.append((episode_idx, traj_idx)) + + log.info( + f"Aligning to ultrawide camera, index pool size changed from {len(self.index_pool)} to {len(new_index_pool)}" + ) + self.index_pool = new_index_pool + + +iPhUMISingleTrajDataset._process_source_data = _process_source_data + + +class UMISingleTrajDataset(AggregatedDataset): + pass + + +UMISingleTrajDataset._process_source_data = _process_source_data + + +def get_umi_dataset( + dataset_name: str, + dataset_type: str, + is_val: bool, + mode: str = "policy", + image_size: int = 256, + root: str = "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/umi/", + normalizer_dir: str | None = None, + **kwargs, +): + # assert len(kwargs) == 0, f"get_umi_dataset does not accept any kwargs, but got {kwargs}" + log.info(f"get_umi_dataset override kwargs: {kwargs=}") + try: + OmegaConf.register_new_resolver("eval", eval) + except Exception: + pass + if dataset_type == "umi": + cfg = OmegaConf.load("cosmos/data/vfm/action/umi_dataset.yaml") + elif dataset_type == "umi_multi_task": + if hydra.core.global_hydra.GlobalHydra().is_initialized(): + cfg = hydra.compose(config_name="umi_dataset_multi_task") + else: + with hydra.initialize("."): + cfg = hydra.compose(config_name="umi_dataset_multi_task") + else: + raise ValueError(f"Invalid dataset type: {dataset_type}. Available dataset types: umi, umi_multi_task") + + cfg.update(kwargs) + + cfg.name = dataset_name + cfg.mode = mode + if root is not None: + cfg.root = root + if normalizer_dir is not None: + cfg.normalizer_dir = normalizer_dir + assert mode in ["policy", "joint", "forward_dynamics", "inverse_dynamics", "image2video"], ( + f"Invalid mode: {mode}. Available modes: policy, joint, forward_dynamics, inverse_dynamics, video" + ) + cfg.image_size = image_size + if is_val: + cfg.output_data_meta = cfg.val_output_data_meta + cfg.eager_load = True + cfg.val_output_data_meta = None # type: ignore + OmegaConf.resolve(cfg) + dataset = hydra.utils.instantiate(cfg) + if is_val: + return dataset.split_unused_episodes() + else: + return dataset + + +if __name__ == "__main__": + # OmegaConf.register_new_resolver("eval", eval) + # cfg = OmegaConf.load("cosmos/data/vfm/action/umi_dataset.yaml") + # os.environ["HYDRA_FULL_ERROR"] = "1" + args = sys.argv[1:] + dataset_type = args[0] + dataset_name = args[1] + + dataset: BaseDataset = get_umi_dataset( + dataset_name, dataset_type, is_val=False, use_grasp_state_repr=False, use_eef_pose=False + ) + + # dataset.fit_normalizer() + + def save_np_array_as_video(rollout_images: list[np.ndarray] | torch.Tensor, video_path: str, fps: int = 10): + """ + rollout_images: (T, H, W, C) or (T, C, H, W) + Saves an MP4 replay of an episode. + """ + + if isinstance(rollout_images, torch.Tensor): + assert rollout_images.ndim == 4, f"Rollout images must be a 4D tensor, but got {rollout_images.ndim}" + if rollout_images.shape[1] == 3: + rollout_images = rollout_images.permute(0, 2, 3, 1) # [T,H,W,C] + rollout_images = rollout_images.cpu().numpy() + + import imageio + + video_writer = imageio.get_writer(video_path, fps=fps) + for img in rollout_images: + video_writer.append_data(img) + video_writer.close() + print(f"Saved rollout MP4 at path {video_path}") + + print(dataset) + ld = len(dataset) + iss = [0, ld // 4, ld // 2, ld // 4 * 3, -1] + for i in iss: + # for i in range(60, 70): + data = dataset[i] + + # print(data["video"].shape) # [C,T,H,W] + # video_thwc = data["video"].permute(1, 2, 3, 0) # [T,H,W,C] + # save_np_array_as_video(video_thwc, f"video_{i}.mp4") + # print(data["camera_poses"]) + # for k, v in data.items(): + # print(k, v.shape) + print("camera_poses", data["camera_poses"]) + print("eef_commands", data["eef_commands"]) + print("eef_poses", data["eef_poses"]) + if dataset.use_grasp_state_repr: + print("grasp_states", data["grasp_states"]) + print("action", data["action"]) diff --git a/cosmos_training/cosmos/data/vfm/action/umi_dataset.yaml b/cosmos_training/cosmos/data/vfm/action/umi_dataset.yaml new file mode 100644 index 00000000..ae59c5b6 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi_dataset.yaml @@ -0,0 +1,213 @@ +_target_: projects.cosmos3.vfm.datasets.action.umi_dataset.UMISingleTrajDataset + +root: null + +name: ??? +mode: policy +image_size: 256 + +robot_num: 1 +down_sample_steps: 3 # 60 fps -> 20 fps + +include_episode_num: -1 # To specify how many episodes to include. -1 means using all episodes. +include_episode_indices: [] # If not empty, will override used_episode_ratio +used_episode_ratio: 0.99 # How many episodes are used for training +history_padding_length: 0 +future_padding_length: 16 +starting_percentile_max: 1.0 +starting_percentile_min: 0.0 +index_pool_size_per_episode: -1 # Use all indices +seed: 0 +use_relative_pose: true # UMI uses relative pose +relative_pose_mode: frame_wise # When changing this to anchored, please also update the normalizer_dir +use_relative_gripper_width: false # UMI dataset usually does not have intential gripper recovery actions +# Using relative gripper width often leads to gripper width out of distribution which can be seen in the camera +normalizer_sample_num: -1 + + +# If relative_pose_mode == "anchored", use the following normalizer +# normalizer_dir: uva_umi_single_task_anchored_normalizer.json + +# If relative_pose_mode == "frame_wise", use the following normalizer +# normalizer_dir: uva_umi_single_task_frame_wise_normalizer.json +normalizer_dir: uva_umi_single_task_frame_wise_normalizer.json + +repeat_dataset_num: 20 # How many times to repeat the dataset. This will avoid the dataset being consumed too fast and the reload time being too long. + + +apply_image_augmentation_in_cpu: true +use_grasp_state_repr: false +eef_z_offset: -0.10 # set the eef pose to be 0.10m behind the gripper position. This is to align with the wrist settings in egocentric datasets. For different gripper designs, this offset might need to be adjusted. +use_eef_pose: false # If true, use the eef pose for the action. Please specify it in the dataset kwargs when calling `get_umi_dataset` + +source_data_meta: + camera0_rgb: + include_indices: ${eval:"ListConfig(list(range(0, 17)))"} + shape: [224, 224, 3] + + robot0_eef_pos: + # include_indices: ${eval:"ListConfig(list(range(-1, 17)))"} + # 1 frame into the past, 16 frames into the future. 0 is the current frame + # obs: relative pose of [-1] wrt to [0] + # action: relative pose of [1:17] wrt to [0] + include_indices: ${eval:"ListConfig(list(range(0, 17)))"} + shape: [3] + + robot0_eef_rot_axis_angle: + # include_indices: ${eval:"ListConfig(list(range(-1, 17)))"} + include_indices: ${eval:"ListConfig(list(range(0, 17)))"} + shape: [3] + + robot0_gripper_width: + # include_indices: ${eval:"ListConfig(list(range(0, 17)))"} + # Only use absolute width so no need to calculate relative values + # obs: absolute width of [0] + # action: absolute width of [1:17] + include_indices: ${eval:"ListConfig(list(range(1, 17)))"} # Not include the current gripper width + shape: [1] + + + +output_data_meta: + video: + name: video + data_type: image + length: 17 + normalizer: identity + augmentation: + - name: RandomCrop + size: [208, 208] + p: 0.3 + - name: Resize + size: [256, 256] + antialias: True + - name: ColorJitter + brightness: 0.3 + contrast: 0.4 + saturation: 0.5 + hue: 0.08 + p: 0.8 + - name: RandomSharpness + sharpness: 2 + p: 0.3 + - name: RandomAutoContrast + p: 0.3 + - name: RandomGrayscale + p: 0.2 + - name: RandomGaussianBlur + kernel_size: [5, 5] + sigma: [0.1, 2.0] + p: 0.3 + shape: [3, 256, 256] + source_entry_names: + - camera0_rgb + + camera_poses: # This is actually the TCP pose: the middle point of the gripper tip + name: camera_poses + data_type: low_dim + # length: 17 # 1 history relative camera pose (velocity) + length: 16 # Don't include history camera pose + # normalizer: range # Normalize to [-1, 1] + normalizer: clamped_range + augmentation: [] + shape: [9] + source_entry_names: + - robot0_eef_pos + - robot0_eef_rot_axis_angle + + eef_commands: + name: eef_commands + data_type: low_dim + # length: 17 # 1 history eef command (gripper width) + length: 16 # Don't include history eef command + normalizer: clamped_range # Normalize to [-1, 1] + augmentation: [] + shape: [1] + source_entry_names: + - robot0_gripper_width + + eef_poses: + name: eef_poses + data_type: low_dim + # length: 17 # 1 history relative camera pose (velocity) + length: 16 # Don't include history camera pose + # normalizer: range # Normalize to [-1, 1] + normalizer: clamped_range + augmentation: [] + shape: [9] + source_entry_names: + - robot0_eef_pos + - robot0_eef_rot_axis_angle + + grasp_states: + name: grasp_states + data_type: low_dim + # length: 17 # 1 history eef command (gripper width) + length: 16 # Don't include history eef command + normalizer: clamped_range # Normalize to [-1, 1] + augmentation: [] + shape: [6] + source_entry_names: + - robot0_gripper_width + +val_output_data_meta: + video: + name: video + data_type: image + length: 17 + normalizer: identity + augmentation: + - name: Resize + size: [256, 256] + antialias: True + shape: [3, 256, 256] + source_entry_names: + - camera0_rgb + + camera_poses: # This is actually the TCP pose: the middle point of the gripper tip + name: camera_poses + data_type: low_dim + # length: 17 # 1 history relative camera pose (velocity) + length: 16 # Don't include history camera pose + # normalizer: range # Normalize to [-1, 1] + normalizer: clamped_range + augmentation: [] + shape: [9] + source_entry_names: + - robot0_eef_pos + - robot0_eef_rot_axis_angle + + eef_commands: + name: eef_commands + data_type: low_dim + # length: 17 # 1 history eef command (gripper width) + length: 16 # Don't include history eef command + normalizer: clamped_range # Normalize to [-1, 1] + augmentation: [] + shape: [1] + source_entry_names: + - robot0_gripper_width + + eef_poses: + name: eef_poses + data_type: low_dim + # length: 17 # 1 history relative camera pose (velocity) + length: 16 # Don't include history camera pose + # normalizer: range # Normalize to [-1, 1] + normalizer: clamped_range + augmentation: [] + shape: [9] + source_entry_names: + - robot0_eef_pos + - robot0_eef_rot_axis_angle + + grasp_states: + name: grasp_states + data_type: low_dim + # length: 17 # 1 history eef command (gripper width) + length: 16 # Don't include history eef command + normalizer: clamped_range # Normalize to [-1, 1] + augmentation: [] + shape: [6] + source_entry_names: + - robot0_gripper_width diff --git a/cosmos_training/cosmos/data/vfm/action/umi_dataset_multi_task.yaml b/cosmos_training/cosmos/data/vfm/action/umi_dataset_multi_task.yaml new file mode 100644 index 00000000..149f46bb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/umi_dataset_multi_task.yaml @@ -0,0 +1,171 @@ +defaults: + - umi_dataset + - _self_ + +_target_: projects.cosmos3.vfm.datasets.action.umi_dataset.MultiTaskDataset + +sub_dataset_target: projects.cosmos3.vfm.datasets.action.umi_dataset.UMISingleTrajDataset + +eager_load: false + +name: umi_multi_tasks + +normalizer_dir: uva_umi_single_task_frame_wise_normalizer.json + +dataset_configs: + data_scaling_law/mouse_arrangement_0: + sample_ratio: 0.0 + data_scaling_law/mouse_arrangement_1: + sample_ratio: 0.0 + data_scaling_law/towel_folding_0: + sample_ratio: 0.0 + data_scaling_law/unplug_charger: + sample_ratio: 0.0 + data_scaling_law/water_pouring_0: + sample_ratio: 0.0 + data_scaling_law/water_pouring_1: + sample_ratio: 0.0 + umi/cup_arrangement_0: + sample_ratio: 0.0 + umi/cup_arrangement_1: + sample_ratio: 0.0 + umi/dynamic_tossing_0: + sample_ratio: 0.0 + maniwav/bagle_flipping_0: + sample_ratio: 0.0 + maniwav/bagle_flipping_1: + sample_ratio: 0.0 + maniwav/dice_pouring_0: + sample_ratio: 0.0 + maniwav/whiteboard_wiping_0: + sample_ratio: 0.0 + maniwav/wire_strapping_0: + sample_ratio: 0.0 + + ## MV-UMI uses negative gripper action - looks like relative action + # mv_umi/bottles_rack_insertion_0: + # sample_ratio: 0.0 + # mv_umi/markers_placement_0: + # sample_ratio: 0.0 + # mv_umi/markers_placement_raw_0: + # sample_ratio: 0.0 + + ## Touch in the Wild uses a different gripper scale. Will be fixed in the next UMI dataset version + # touch_in_the_wild/fluid_transfer_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 # touch_in_the_wild uses a shorter gripper design: it is 0.033m shorter than the default gripper + # touch_in_the_wild/hex_key_insertion_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/moving_cup_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/moving_tape_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/peg_insertion_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/pencil_insertion_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/test_tube_collection_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/tossing_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/whiteboard_erasing_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/whiteboard_erasing_1: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + # touch_in_the_wild/writing_0: + # sample_ratio: 0.0 + # eef_z_offset: -0.067 + + umi_on_legs/kettlebell_pushing_0: + sample_ratio: 0.0 + umi_on_legs/tennis_ball_tossing_0: + sample_ratio: 0.0 + vitamin/articulated_object_manipulation_0: + sample_ratio: 0.0 + vitamin/dynamic_peg_insertion_0: + sample_ratio: 0.0 + vitamin/orange_placement_0: + sample_ratio: 0.0 + vitamin/scissor_hanging_0: + sample_ratio: 0.0 + vitamin/sponge_insertion_0: + sample_ratio: 0.0 + vitamin/test_tube_reorientation_0: + sample_ratio: 0.0 + FastUMI/clean_table: + sample_ratio: 0.0 + num_shards: 5 + FastUMI/close_ricecooker: + sample_ratio: 0.0 + num_shards: 1 + FastUMI/cover_beef: + sample_ratio: 0.0 + num_shards: 11 + FastUMI/fold_towel: + sample_ratio: 0.0 + num_shards: 7 + FastUMI/hotdog_in_rice_cooker: + sample_ratio: 0.0 + num_shards: 2 + FastUMI/hotdog_in_roaster: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/open_container: + sample_ratio: 0.0 + num_shards: 7 + FastUMI/open_drawer: + sample_ratio: 0.0 + num_shards: 17 + FastUMI/open_ricecooker: + sample_ratio: 0.0 + num_shards: 5 + FastUMI/open_roaster: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/open_suitcase: + sample_ratio: 0.0 + num_shards: 1 + FastUMI/pick_bear: + sample_ratio: 0.0 + num_shards: 11 + FastUMI/pick_bread: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/pick_cup: + sample_ratio: 0.0 + num_shards: 18 + FastUMI/pick_lid: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/pick_pen: + sample_ratio: 0.0 + num_shards: 12 + FastUMI/place_plate: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/place_pot: + sample_ratio: 0.0 + num_shards: 6 + FastUMI/pour_coke: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/rearrange_coke: + sample_ratio: 0.0 + num_shards: 10 + FastUMI/sweep_trash: + sample_ratio: 0.0 + num_shards: 11 + FastUMI/tossing: + sample_ratio: 0.0 + FastUMI/unplug_charger: + sample_ratio: 0.0 + num_shards: 10 diff --git a/cosmos_training/cosmos/data/vfm/action/unified_dataset.py b/cosmos_training/cosmos/data/vfm/action/unified_dataset.py new file mode 100644 index 00000000..586472a1 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/unified_dataset.py @@ -0,0 +1,594 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unified iterable dataset for Action multi-embodiment robot data. + +``ActionUnifiedIterableDataset`` is the Layer 2 component of the Action data loading +pipeline. It wraps *all* Action datasets into a single ``IterableDataset`` and +handles: + +- **Rank-level dataset assignment** (Hare-Niemeyer proportional allocation) +- **Worker-level shard distribution** (round-robin within a dataset family) +- **Per-sample transforms** via :class:`~.transforms.ActionTransformPipeline` +- **Weighted random fallback** when worker assignment is not active + +See ``docs/dataloader.md`` for the full design document. +""" + +from __future__ import annotations + +import gc +import random +import warnings +from collections.abc import Iterator, Mapping, Sequence +from typing import Any + +from torch.utils.data import Dataset, IterableDataset + +from cosmos.utils import log +from cosmos.data.vfm.action.transforms import ActionTransformPipeline +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO + +_iterable_dataset_len_warning_suppressed = False + + +def _suppress_iterable_dataset_len_warning() -> None: + """Register a one-time filter for PyTorch's IterableDataset len() warning. + + The inner datasets may not implement ``__len__``, so the wrapper reports + ``len()=0``. PyTorch's iterator then warns on every ``__next__`` when + samples are fetched. This filter suppresses that warning. + """ + global _iterable_dataset_len_warning_suppressed + if _iterable_dataset_len_warning_suppressed: + return + _iterable_dataset_len_warning_suppressed = True + warnings.filterwarnings( + "ignore", + message="Length of IterableDataset.*was reported to be 0", + category=UserWarning, + module="torch.utils.data.dataloader", + ) + + +# --------------------------------------------------------------------------- +# Worker-side periodic garbage collection +# --------------------------------------------------------------------------- +# DataLoader workers running IterableDatasets are long-lived forked processes. +# Complex sample dictionaries (nested dicts, tensors, Arrow references) can +# create circular-reference chains that Python's reference counting alone +# cannot free. The generational GC *does* collect them eventually, but its +# default thresholds are too conservative for high-throughput data loading, +# causing RSS to grow monotonically until the node OOMs. +# +# Calling ``gc.collect()`` periodically inside the worker iteration loop +# eliminates the leak with negligible overhead (<1 ms per call vs ~6 s +# iteration time). +# +_GC_INTERVAL: int = 10 + + +def _maybe_gc(interval: int, count: int) -> int: + """Increment *count* and run ``gc.collect()`` every *interval* samples.""" + if interval <= 0: + return count + count += 1 + if count % interval == 0: + gc.collect() + return count + + +class ActionUnifiedIterableDataset(IterableDataset): + """Single IterableDataset wrapping all Action datasets. + + Handles worker-to-dataset assignment, shard distribution, and transforms. + + Args: + datasets: List of dicts, each with keys ``"name"`` (str identifier), + ``"dataset"`` (the dataset instance), and ``"ratio"`` (float + sampling weight). + transform: Transform pipeline applied to every yielded sample. + shard_across_workers: When ``True``, ranks are assigned to + dataset families via Hare-Niemeyer and workers get round-robin + shards. When ``False`` (default), every worker loads all + datasets and iterates with weighted random selection. + """ + + def __init__( + self, + datasets: list[dict[str, Any]], + transform: ActionTransformPipeline, + shard_across_workers: bool = False, + ) -> None: + super().__init__() + self._datasets = datasets + self._transform = transform + self._shard_across_workers = shard_across_workers + + # Set per-worker by assign_worker; None means not yet assigned. + self._dataset: Any | None = None + self._resolution: str | None = None # resolution for single-dataset path + self._sources_initialized = False + + # Backward compat: expose ``self.dataset`` pointing to the first + # inner dataset and ``self.transform`` exposing the pipeline + # (mirrors old TransformedIterableDataset interface). + self.dataset = datasets[0]["dataset"] if datasets else None + self.transform = transform + + # -- source initialization ------------------------------------------------ + + def _ensure_sources_registered(self) -> None: + if self._sources_initialized: + return + self._sources_initialized = True + for entry in self._datasets: + ds = entry["dataset"] + shard_roots = getattr(ds, "_all_shard_roots", []) + if shard_roots and hasattr(ds, "_register_sources"): + ds._register_sources() + + # -- backward-compat helpers ----------------------------------------------- + + def __len__(self) -> int: # type: ignore[override] + total = 0 + for entry in self._datasets: + ds = entry["dataset"] + try: + total += len(ds) # type: ignore[arg-type] + except TypeError: + pass + return total + + def __getattr__(self, name: str) -> Any: + """Forward attribute lookups to the first inner dataset.""" + if name.startswith("_") or not self._datasets: + raise AttributeError(name) + return getattr(self._datasets[0]["dataset"], name) + + # -- Hare-Niemeyer rank allocation ----------------------------------------- + + @staticmethod + def _compute_rank_ranges( + datasets: list[dict[str, Any]], + world_size: int, + ) -> list[tuple[int, int]]: + """Hare-Niemeyer allocation of ranks to datasets. + + Guarantees at least 1 rank per dataset, distributes the rest + proportionally. Returns a list of ``(start_rank, end_rank)`` ranges. + + Raises: + ValueError: If ``world_size < len(datasets)``. + """ + n_ds = len(datasets) + if world_size < n_ds: + raise ValueError(f"world_size ({world_size}) must be >= number of datasets ({n_ds})") + ratios = [d["ratio"] for d in datasets] + total = sum(ratios) + + # Hare-Niemeyer (largest-remainder) method: + # 1. Give every dataset a guaranteed minimum of 1 rank. + # 2. Distribute the leftover ranks proportionally to each dataset's + # ratio. Take the floor of each fractional allocation, then award + # the still-unassigned ranks one-by-one to datasets with the + # largest fractional remainders. + # Example: world_size=8, ratios=[3, 1] (2 datasets) + # remaining = 8 - 2 = 6 + # fractional = [6*3/4, 6*1/4] = [4.5, 1.5] + # floors = [4, 1], remainders = [0.5, 0.5], leftover = 1 + # award 1 extra to first dataset -> floors = [5, 1] + # counts = [1+5, 1+1] = [6, 2] + counts = [1] * n_ds + remaining = world_size - n_ds + if remaining > 0: + fractional = [remaining * r / total for r in ratios] + floors = [int(f) for f in fractional] + remainders = [f - fl for f, fl in zip(fractional, floors)] + leftover = remaining - sum(floors) + for idx in sorted(range(n_ds), key=lambda j: -remainders[j])[:leftover]: + floors[idx] += 1 + counts = [1 + f for f in floors] + + # Convert per-dataset counts into contiguous rank intervals. + # Example continued: counts=[6, 2] -> ranges=[(0,6), (6,8)] + # ranks 0..5 serve dataset 0, ranks 6..7 serve dataset 1. + ranges: list[tuple[int, int]] = [] + cursor = 0 + for c in counts: + ranges.append((cursor, cursor + c)) + cursor += c + return ranges + + # -- worker assignment ----------------------------------------------------- + + def assign_worker( + self, + worker_id: int, + num_workers: int, + rank: int, + world_size: int, + ) -> None: + """Assign this worker to a dataset family and distribute shards. + + Called by the DataLoader's ``worker_init_fn`` (via + :func:`~.dataloaders.create_action_worker_init_fn`) -- not by the + dataset itself. + + Two-level assignment: + + 1. **Rank -> dataset family** (Hare-Niemeyer over *world_size* + ranks). Every rank is fully dedicated to one family. + 2. **Workers -> shards** (round-robin within the family's worker + pool). ``family_worker_id = rank_within_family * num_workers + + worker_id``. + + When ``shard_across_workers=False``: no assignment is performed. + Every worker loads all datasets and ``__iter__`` uses weighted + random selection. + """ + self._sources_initialized = True + if not self._shard_across_workers: + for entry in self._datasets: + ds = entry["dataset"] + shard_roots = getattr(ds, "_all_shard_roots", []) + if shard_roots and hasattr(ds, "_register_sources"): + ds._register_sources() + return + + rank_ranges = self._compute_rank_ranges(self._datasets, world_size) + + # Step 1: which dataset family does this rank belong to? + # ``rank_ranges`` is a list of (start_rank, end_rank) intervals -- one + # per dataset family -- produced by ``_compute_rank_ranges()`` above + # using Hare-Niemeyer allocation. The intervals are contiguous and + # non-overlapping, covering [0, world_size), so every rank belongs to + # exactly one family. + # + # We scan through the intervals to find the one containing this rank, + # then derive two values: + # - rank_within_family: this rank's 0-based position inside its + # family (used in Step 2 to build a globally unique worker id). + # - num_family_ranks: total number of ranks assigned to this family + # (used in Step 2 to compute the family's worker pool size). + # + # ``self._dataset`` is set to the matched family's dataset object so + # that ``__iter__`` only yields samples from this one dataset. + # + # Example with world_size=8, ratios=[3,1] -> ranges=[(0,6), (6,8)]: + # rank 3 -> family 0, rank_within_family=3, num_family_ranks=6 + # rank 6 -> family 1, rank_within_family=0, num_family_ranks=2 + num_family_ranks = 1 + rank_within_family = 0 + for i, (start_rank, end_rank) in enumerate(rank_ranges): + if start_rank <= rank < end_rank: + entry = self._datasets[i] + self._dataset = entry["dataset"] + self._resolution = entry["resolution"] + rank_within_family = rank - start_rank + num_family_ranks = end_rank - start_rank + break + + # Step 2: distribute shards across workers within the family. + # Each rank spawns ``num_workers`` DataLoader workers (set by the + # DataLoader's ``num_workers`` arg). So the family's total worker + # pool is ``num_family_ranks * num_workers``. + # + # We flatten the 2D index (rank_within_family, worker_id) into a + # single linear ``family_worker_id`` so every worker in the family + # gets a globally unique id within that family: + # family_worker_id = rank_within_family * num_workers + worker_id + # + # Example: family has 3 ranks, each rank spawns 2 workers -> 6 total: + # rank_within_family=0: worker_id 0 -> fwid 0, worker_id 1 -> fwid 1 + # rank_within_family=1: worker_id 0 -> fwid 2, worker_id 1 -> fwid 3 + # rank_within_family=2: worker_id 0 -> fwid 4, worker_id 1 -> fwid 5 + # + # This linear id is then used for round-robin shard assignment below. + family_total_workers = num_family_ranks * num_workers + family_worker_id = rank_within_family * num_workers + worker_id + + # Round-robin assignment: worker k gets shards k, k+stride, k+2*stride, ... + # This ensures shards are evenly spread across the family's workers. + # + # When family_total_workers > num_shards, some workers get an empty + # list from range() (any worker with family_worker_id >= num_shards, + # since start >= stop). The ``if not my_shards`` guard catches this + # and falls back to ``family_worker_id % num_shards``, wrapping the + # worker around to an existing shard so it shares rather than idles. + # + # Example: AgiBotWorld with 190 shards and 256 family workers: + # Workers 0-189 -> each gets 1 unique shard via range() + # Workers 190-255 -> empty range, fallback to family_worker_id % 190, + # sharing a shard with an earlier worker. + # + # Multiple workers reading the same shard is fine because each worker + # has a different RNG seed (``seed + rank * 9999 + worker_id``), so + # they produce different sample orderings from the same underlying data. + shard_roots = getattr(self._dataset, "_all_shard_roots", []) + if shard_roots and hasattr(self._dataset, "_register_sources"): + num_shards = len(shard_roots) + my_shards = list(range(family_worker_id, num_shards, family_total_workers)) + if not my_shards: + my_shards = [family_worker_id % num_shards] + self._dataset._register_sources(my_shards) + + # -- iteration ------------------------------------------------------------- + + def _iter_all_datasets_weighted(self) -> Iterator[dict[str, Any]]: + """Iterate all datasets with weighted random selection. + + Used when ``shard_across_workers=False`` (every worker sees all + datasets) or as the ``num_workers=0`` fallback. + """ + iterators = [iter(d["dataset"]) for d in self._datasets] + ratios = [d["ratio"] for d in self._datasets] + total = sum(ratios) + weights = [r / total for r in ratios] + + gc_count = 0 + + while True: + chosen = random.choices(range(len(self._datasets)), weights=weights, k=1)[0] + resolution = self._datasets[chosen]["resolution"] + try: + yield self._transform(next(iterators[chosen]), resolution=resolution) + except StopIteration: + iterators[chosen] = iter(self._datasets[chosen]["dataset"]) + try: + yield self._transform(next(iterators[chosen]), resolution=resolution) + except StopIteration: + continue + gc_count = _maybe_gc(_GC_INTERVAL, gc_count) + + def __iter__(self) -> Iterator[dict[str, Any]]: + if self._dataset is not None: + gc_count = 0 + for sample in self._dataset: + yield self._transform(sample, resolution=self._resolution) + gc_count = _maybe_gc(_GC_INTERVAL, gc_count) + return + + if not self._shard_across_workers: + self._ensure_sources_registered() + yield from self._iter_all_datasets_weighted() + return + + # num_workers=0 fallback (shard_across_workers=True but no worker + # processes exist, so assign_worker was never called). + log.warning( + "ActionUnifiedIterableDataset: num_workers=0 fallback — " + "loading ALL datasets in main process. Use only for debugging." + ) + self._ensure_sources_registered() + yield from self._iter_all_datasets_weighted() + + +class MapToIterableAdapter(IterableDataset): + """Wraps a map-style ``Dataset`` as an ``IterableDataset``. + + Each iteration yields a sample from a uniformly random index, using + ``random.randint`` for O(1) time and zero extra memory. The per-worker + RNG seed (set by :func:`~.dataloaders.create_action_worker_init_fn`) ensures + different DataLoader workers produce different random sequences. + + Args: + dataset: A map-style ``Dataset`` with ``__len__`` and ``__getitem__``. + """ + + def __init__(self, dataset: Dataset) -> None: + super().__init__() + self.dataset = dataset + + def __len__(self) -> int: # type: ignore[override] + return len(self.dataset) # type: ignore[arg-type] + + def __iter__(self) -> Iterator: + n = len(self.dataset) # type: ignore[arg-type] + while True: + yield self.dataset[random.randint(0, n - 1)] + + def __getattr__(self, name: str) -> Any: + """Forward attribute lookups to the inner dataset for transparency.""" + if name == "dataset": + raise AttributeError(name) + return getattr(self.dataset, name) + + +def dataset_entry( + name: str, + dataset: Dataset | IterableDataset, + ratio: float = 1.0, + resolution: str | None = None, +) -> dict: + """Factory for a single dataset descriptor used inside ``wrap_dataset``. + + Wrapping each entry with ``LazyCall(dataset_entry)(...)`` gives it a + ``_target_`` so that ``instantiate`` recurses into the nested dataset + config automatically. + + Args: + name: Identifier for the dataset. + dataset: The dataset instance. + ratio: Sampling weight. Defaults to 1.0. + resolution: Optional resolution tier (e.g. ``"256"``, ``"480"``) for + this dataset. When ``None``, falls back to ``wrap_dataset``'s + global ``resolution`` (which may be ``None`` for auto-detect). + """ + return {"name": name, "dataset": dataset, "ratio": ratio, "resolution": resolution} + + +def wrap_dataset( + list_of_datasets: Sequence[dict] | list[dict] | Dataset | IterableDataset, + resolution: str | None = None, + pad_keys: list[str] | None = None, + keep_aspect_ratio: bool = True, + tokenizer_config: dict | None = None, + cfg_dropout_rate: float = 0.0, + caption_key: str = "ai_caption", + text_token_key: str = "text_token_ids", + video_temporal_downsample: int = 4, + max_action_dim: int = 32, + shard_across_workers: bool = False, + action_channel_masking: bool = True, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + append_idle_frames: bool = False, + idle_frames_dropout: float = 0.05, + format_prompt_as_json: bool = False, +) -> ActionUnifiedIterableDataset: + """Factory that wraps one or more datasets with the Action transform pipeline. + + ``list_of_datasets`` accepts either: + + * A **list of dicts**, where each dict has the keys: + - ``name`` (``str``): identifier for the dataset. + - ``dataset`` (``Dataset | IterableDataset``): the dataset instance. + - ``ratio`` (``float``, optional): sampling weight. Defaults to ``1``. + - ``resolution`` (``str | None``, optional): resolution tier for this + dataset. When missing, falls back to ``wrap_dataset``'s global + ``resolution`` (which may be ``None`` for auto-detect). + * A **single** ``Dataset`` or ``IterableDataset`` for backward compatibility + (auto-wrapped as ``[{"name": "default", "dataset": , "ratio": 1}]``). + + Map-style datasets are automatically wrapped with + :class:`MapToIterableAdapter` so the returned dataset is always an + ``IterableDataset``. This means callers can mix map-style and + iterable-style datasets freely. + + Args: + list_of_datasets: The dataset(s) to wrap. + resolution: Resolution tier key (e.g. ``"256"``, ``"480"``, ``"720"``). + Spatial dimensions are resized and reflection-padded to the closest + predefined target from ``VIDEO_RES_SIZE_INFO``. When ``None``, the + tier is auto-detected per sample via ``get_vision_data_resolution``. + Defaults to ``None``. + pad_keys: Data-dict keys whose values should be resized and padded. Pass + an empty list or ``None`` to disable padding. Defaults to ``["video"]``. + tokenizer_config: A lazy-instantiable config dict for the VLM tokenizer. When + ``None``, text tokenization is skipped. Defaults to ``None``. + cfg_dropout_rate: Probability of replacing the caption with an empty string for + classifier-free guidance. Defaults to ``0.0``. + caption_key: The data-dict key that contains the input caption string. + Defaults to ``"ai_caption"``. + text_token_key: The data-dict key where tokenized text IDs will be stored. + Defaults to ``"text_token_ids"``. + video_temporal_downsample: Temporal downsampling factor of the video tokenizer. + Used when building a ``SequencePlan`` for ``"inverse_dynamics"`` mode. + Defaults to 4. + max_action_dim: Target action dimension to pad to. The ``"action"`` tensor + in every sample is padded along its last dimension. Defaults to 32. + action_channel_masking: When ``True`` (default), stores the original action + dimension in ``"raw_action_dim"`` so the model masks loss/noise/velocity + on padded channels. Set to ``False`` to disable (original behavior). + shard_across_workers: When ``True``, the returned dataset + supports rank-level dataset assignment and worker-level shard + distribution via ``assign_worker()``. When ``False`` (default), + every worker iterates all datasets with weighted random selection. + append_duration_fps_timestamps: Whether to append duration and FPS metadata to the + caption before tokenization. Defaults to ``True``. + append_resolution_info: Whether to append resolution metadata to the + caption before tokenization. Defaults to ``True``. + append_idle_frames: Whether to append the idle-frame count out of the + total action frames (Pi0.7-style metadata) to the caption before + tokenization. The dataset is responsible for populating + ``data_dict["idle_frames"]``; samples without it are silently + skipped. Defaults to ``False`` so existing experiments are + unaffected. + idle_frames_dropout: Per-field dropout rate for the idle-frame segment. + Independent of ``cfg_dropout_rate`` (which empties the whole + caption). Defaults to 0.05. + format_prompt_as_json: Whether to replace the plain text prompt with a + structured JSON-compatible dictionary before tokenization. Defaults + to ``False``. + + Returns: + A :class:`ActionUnifiedIterableDataset` wrapping the dataset(s) with the + configured transforms applied. + + Raises: + TypeError: If the dataset(s) are not ``Dataset`` or ``IterableDataset``. + ValueError: If ``list_of_datasets`` is an empty list. + """ + if pad_keys is None: + pad_keys = ["video"] + + # ------------------------------------------------------------------ + # Backward compatibility: single dataset -> list-of-dicts + # ------------------------------------------------------------------ + if isinstance(list_of_datasets, (Dataset, IterableDataset)): + list_of_datasets = [{"name": "default", "dataset": list_of_datasets, "ratio": 1}] + + if ( + not isinstance(list_of_datasets, Sequence) + or isinstance(list_of_datasets, (str, bytes)) + or len(list_of_datasets) == 0 + ): + raise ValueError( + "list_of_datasets must be a non-empty list/sequence of dicts or a single Dataset/IterableDataset, " + f"got {type(list_of_datasets).__name__}" + ) + + # ------------------------------------------------------------------ + # Parse list-of-dicts, wrapping map-style datasets with + # MapToIterableAdapter so every dataset is iterable. Compute effective + # resolution per entry (per-entry overrides global). + # ------------------------------------------------------------------ + datasets: list[dict] = [] + for entry in list_of_datasets: + if not isinstance(entry, Mapping): + raise TypeError(f"Each entry in list_of_datasets must be a dict/mapping, got {type(entry).__name__}") + name: str = entry["name"] + dataset: Dataset | IterableDataset = entry["dataset"] + ratio: float = float(entry.get("ratio", 1)) + resolution: str | None = entry.get("resolution", None) + if resolution is not None: + res_key = str(resolution) if isinstance(resolution, int) else resolution + if res_key not in VIDEO_RES_SIZE_INFO: + raise ValueError( + f"Resolution '{resolution}' for dataset '{name}' not found in VIDEO_RES_SIZE_INFO. " + f"Available: {list(VIDEO_RES_SIZE_INFO.keys())}" + ) + if not isinstance(dataset, IterableDataset): + dataset = MapToIterableAdapter(dataset) + datasets.append({"name": name, "dataset": dataset, "ratio": ratio, "resolution": resolution}) + + # ------------------------------------------------------------------ + # Build the transform pipeline (resolution supplied at call time) + # ------------------------------------------------------------------ + transform = ActionTransformPipeline( + pad_keys=pad_keys, + keep_aspect_ratio=keep_aspect_ratio, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + caption_key=caption_key, + text_token_key=text_token_key, + video_temporal_downsample=video_temporal_downsample, + max_action_dim=max_action_dim, + action_channel_masking=action_channel_masking, + append_duration_fps_timestamps=append_duration_fps_timestamps, + append_resolution_info=append_resolution_info, + append_idle_frames=append_idle_frames, + idle_frames_dropout=idle_frames_dropout, + format_prompt_as_json=format_prompt_as_json, + ) + + _suppress_iterable_dataset_len_warning() + + return ActionUnifiedIterableDataset( + datasets=datasets, + transform=transform, + shard_across_workers=shard_across_workers, + ) diff --git a/cosmos_training/cosmos/data/vfm/action/unified_dataset_test.py b/cosmos_training/cosmos/data/vfm/action/unified_dataset_test.py new file mode 100644 index 00000000..7762bc2f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/unified_dataset_test.py @@ -0,0 +1,534 @@ +from __future__ import annotations + +import itertools +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +import torch +from torch.utils.data import IterableDataset + +from cosmos.data.vfm.action.unified_dataset import ActionUnifiedIterableDataset +from cosmos.model.vfm.omni_mot_model_test import _maybe_init_distributed + +""" +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_hare_niemeyer --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_assign_worker --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_iter --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_backward_compat --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_worker_init_fn --L0 +pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_register_sources --L0 +torchrun --nproc_per_node=4 --standalone -m pytest -v -s cosmos/data/vfm/action/unified_dataset_test.py::test_distributed_4rank_shard_assignment --L1 +""" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +# Dataset entries are dicts with "name", "dataset", "ratio", and optionally +# "resolution". Resolution is always in the dict (use None when not needed); +# it is not passed to the underlying dataset or to wrap_dataset. + + +def _make_sample( + video_shape: tuple[int, ...] = (3, 17, 256, 256), + action_dim: int = 10, + mode: str = "policy", +) -> dict: + """Minimal sample dict compatible with ActionTransformPipeline.""" + action_length = video_shape[1] - 1 + return { + "video": torch.randint(0, 256, video_shape, dtype=torch.uint8), + "action": torch.randn(action_length, action_dim), + "ai_caption": "A test caption.", + "mode": mode, + "domain_id": torch.tensor(0, dtype=torch.long), + } + + +class _TaggedIterableDataset(IterableDataset): + """Iterable dataset that tags samples with a ``dataset_tag``.""" + + def __init__(self, tag: str, length: int = 50) -> None: + self._tag = tag + self._length = length + + def __len__(self) -> int: + return self._length + + def __iter__(self): + while True: + sample = _make_sample() + sample["dataset_tag"] = self._tag + yield sample + + +class _FiniteIterableDataset(IterableDataset): + """Iterable dataset that yields a fixed number of samples then stops.""" + + def __init__(self, tag: str, count: int = 3) -> None: + self._tag = tag + self._count = count + + def __len__(self) -> int: + return self._count + + def __iter__(self): + for _ in range(self._count): + sample = _make_sample() + sample["dataset_tag"] = self._tag + yield sample + + +class _ShardableDataset(IterableDataset): + """Dataset with ``_all_shard_roots`` and ``_register_sources`` for testing.""" + + def __init__(self, tag: str, num_shards: int = 10) -> None: + self._tag = tag + self._all_shard_roots = [{"shard_id": i} for i in range(num_shards)] + self._registered_shards: list[int] = [] + + def _register_sources(self, indices: list[int] | None = None) -> None: + if indices is None: + indices = list(range(len(self._all_shard_roots))) + self._registered_shards = indices + + def __iter__(self): + while True: + sample = _make_sample() + sample["dataset_tag"] = self._tag + sample["registered_shards"] = list(self._registered_shards) + yield sample + + +def _identity_transform(data_dict: dict, resolution: str | None = None) -> dict: + return data_dict + + +def _make_unified( + datasets: list[dict[str, Any]], + shard_across_workers: bool = True, +) -> ActionUnifiedIterableDataset: + transform = MagicMock(side_effect=_identity_transform) + return ActionUnifiedIterableDataset( + datasets=datasets, + transform=transform, + shard_across_workers=shard_across_workers, + ) + + +# --------------------------------------------------------------------------- +# Hare-Niemeyer rank allocation +# --------------------------------------------------------------------------- + + +@pytest.mark.L0 +def test_hare_niemeyer_basic_proportional(): + """4 datasets, 8 ranks with ratios [4,2,1,1]. + + Hare-Niemeyer with 1-rank guarantee: + - Each dataset gets 1 guaranteed rank, leaving 4 to distribute. + - Remaining 4 allocated proportionally: A=2.0, B=1.0, C=0.5, D=0.5 + - Floors: [2,1,0,0], leftover=1 goes to C (highest remainder). + - Final: [3,2,2,1]. + """ + datasets = [ + {"name": "A", "ratio": 4}, + {"name": "B", "ratio": 2}, + {"name": "C", "ratio": 1}, + {"name": "D", "ratio": 1}, + ] + ranges = ActionUnifiedIterableDataset._compute_rank_ranges(datasets, 8) + counts = [end - start for start, end in ranges] + print(f" Counts: {counts}") + assert counts == [3, 2, 2, 1] + assert ranges[0] == (0, 3) + assert ranges[1] == (3, 5) + assert ranges[2] == (5, 7) + assert ranges[3] == (7, 8) + print(" Success: basic proportional allocation correct") + + +@pytest.mark.L0 +def test_hare_niemeyer_minimum_one_rank(): + """Even with very skewed ratios, each dataset gets at least 1 rank.""" + datasets = [ + {"name": "big", "ratio": 100}, + {"name": "tiny", "ratio": 1}, + ] + ranges = ActionUnifiedIterableDataset._compute_rank_ranges(datasets, 3) + counts = [end - start for start, end in ranges] + print(f" Counts: {counts}") + assert all(c >= 1 for c in counts), f"All datasets must get >= 1 rank: {counts}" + assert sum(counts) == 3 + print(" Success: minimum 1-rank guarantee holds") + + +@pytest.mark.L0 +def test_hare_niemeyer_insufficient_ranks(): + """world_size < num_datasets should raise ValueError.""" + datasets = [{"name": f"ds_{i}", "ratio": 1} for i in range(5)] + with pytest.raises(ValueError, match="must be >= number of datasets"): + ActionUnifiedIterableDataset._compute_rank_ranges(datasets, 3) + print(" Success: insufficient ranks raises ValueError") + + +@pytest.mark.L0 +def test_hare_niemeyer_total_matches_world_size(): + """Sum of allocated ranks always equals world_size.""" + for world_size in [4, 8, 16, 32, 64]: + datasets = [ + {"name": "A", "ratio": 5}, + {"name": "B", "ratio": 3}, + {"name": "C", "ratio": 1}, + ] + ranges = ActionUnifiedIterableDataset._compute_rank_ranges(datasets, world_size) + total = sum(end - start for start, end in ranges) + assert total == world_size, f"world_size={world_size}: total={total}" + print(f" world_size={world_size}: total={total} OK") + print(" Success: totals match for all world sizes") + + +@pytest.mark.L0 +def test_hare_niemeyer_contiguous_non_overlapping(): + """Ranges are contiguous and non-overlapping.""" + datasets = [ + {"name": "A", "ratio": 4}, + {"name": "B", "ratio": 2}, + {"name": "C", "ratio": 1}, + ] + ranges = ActionUnifiedIterableDataset._compute_rank_ranges(datasets, 10) + print(f" Ranges: {ranges}") + for i in range(1, len(ranges)): + assert ranges[i][0] == ranges[i - 1][1], f"Gap between range {i - 1} and {i}" + print(" Success: ranges are contiguous") + + +# --------------------------------------------------------------------------- +# assign_worker +# --------------------------------------------------------------------------- + + +@pytest.mark.L0 +def test_assign_worker_dataset_assignment(): + """Each rank is assigned to the correct dataset family.""" + ds_a = _ShardableDataset("A", num_shards=20) + ds_b = _ShardableDataset("B", num_shards=5) + datasets = [ + {"name": "A", "dataset": ds_a, "ratio": 3, "resolution": None}, + {"name": "B", "dataset": ds_b, "ratio": 1, "resolution": None}, + ] + + # 4 ranks, ratios [3,1]: guarantee 1 each, remaining 2 -> A=1.5 B=0.5 + # floors [1,0], leftover 1 -> A. Final: [2,2] -> A=[0,1], B=[2,3] + print(" Testing rank 0 -> dataset A...") + unified = _make_unified(datasets) + unified.assign_worker(worker_id=0, num_workers=2, rank=0, world_size=4) + assert unified._dataset is ds_a + + print(" Testing rank 3 -> dataset B...") + unified2 = _make_unified( + [ + {"name": "A", "dataset": _ShardableDataset("A", num_shards=20), "ratio": 3, "resolution": None}, + {"name": "B", "dataset": ds_b, "ratio": 1, "resolution": None}, + ] + ) + unified2.assign_worker(worker_id=0, num_workers=2, rank=3, world_size=4) + assert unified2._dataset is ds_b + print(" Success: rank-to-dataset assignment correct") + + +@pytest.mark.L0 +def test_assign_worker_shard_round_robin(): + """Shards are distributed round-robin across the family's workers.""" + print(" 1 rank, 4 workers, 10 shards...") + ds = _ShardableDataset("A", num_shards=10) + datasets = [{"name": "A", "dataset": ds, "ratio": 1, "resolution": None}] + unified = _make_unified(datasets) + + # Worker 0 -> shards [0, 4, 8] + unified.assign_worker(worker_id=0, num_workers=4, rank=0, world_size=1) + print(f" Worker 0 shards: {ds._registered_shards}") + assert ds._registered_shards == [0, 4, 8] + + # Worker 1 -> shards [1, 5, 9] + ds2 = _ShardableDataset("A", num_shards=10) + datasets2 = [{"name": "A", "dataset": ds2, "ratio": 1, "resolution": None}] + unified2 = _make_unified(datasets2) + unified2.assign_worker(worker_id=1, num_workers=4, rank=0, world_size=1) + print(f" Worker 1 shards: {ds2._registered_shards}") + assert ds2._registered_shards == [1, 5, 9] + print(" Success: round-robin shard distribution correct") + + +@pytest.mark.L0 +def test_assign_worker_shard_wrap_around(): + """When workers > shards, workers wrap around.""" + ds = _ShardableDataset("A", num_shards=3) + datasets = [{"name": "A", "dataset": ds, "ratio": 1, "resolution": None}] + unified = _make_unified(datasets) + + # Worker 5 of 8 -> family_worker_id = 5, range(5, 3, 8) empty -> fallback [5 % 3] = [2] + unified.assign_worker(worker_id=5, num_workers=8, rank=0, world_size=1) + print(f" Worker 5 shards: {ds._registered_shards}") + assert ds._registered_shards == [2] + print(" Success: wrap-around fallback correct") + + +@pytest.mark.L0 +def test_assign_worker_no_shard_roots(): + """Datasets without _all_shard_roots skip shard distribution (Case C). + + Most datasets (LIBERO, PushT, CameraDataset, AVDataset) don't populate + _all_shard_roots. They are still assigned to a rank family via + Hare-Niemeyer, but every worker in the family iterates the full dataset + with different RNG seeds — no round-robin shard splitting occurs. + See dataloader.md "Case C" for details. + """ + ds = _TaggedIterableDataset("plain") + datasets = [{"name": "plain", "dataset": ds, "ratio": 1, "resolution": None}] + unified = _make_unified(datasets) + + unified.assign_worker(worker_id=0, num_workers=4, rank=0, world_size=1) + assert unified._dataset is ds + assert not hasattr(ds, "_registered_shards") + print(" Success: no shard roots -> no registration") + + +@pytest.mark.L0 +def test_assign_worker_unsharded_mode(): + """shard_across_workers=False: every worker loads all datasets (default). + + When shard_across_workers=False (the default), Hare-Niemeyer rank + assignment is skipped entirely. Every worker on every rank sees all + datasets, and __iter__ uses weighted random selection based on the + ratio values. This is the safe default that works without any dataset + changes — opt in to shard_across_workers=True for large sharded + datasets like AgiBotWorld. + """ + ds_a = _ShardableDataset("A", num_shards=10) + ds_b = _ShardableDataset("B", num_shards=5) + datasets = [ + {"name": "A", "dataset": ds_a, "ratio": 1}, + {"name": "B", "dataset": ds_b, "ratio": 1}, + ] + unified = _make_unified(datasets, shard_across_workers=False) + + unified.assign_worker(worker_id=0, num_workers=4, rank=0, world_size=4) + + assert unified._dataset is None + assert ds_a._registered_shards == list(range(10)) + assert ds_b._registered_shards == list(range(5)) + print(" Success: unsharded mode registers all shards") + + +@pytest.mark.L0 +def test_assign_worker_multi_rank_family(): + """Shards distribute across workers on multiple ranks of the same family.""" + print(" 2 ranks, 4 workers each, 20 shards -> family_total_workers = 8") + + # Rank 0, worker 2 -> family_worker_id = 0*4+2 = 2 -> shards [2, 10, 18] + ds = _ShardableDataset("A", num_shards=20) + datasets = [{"name": "A", "dataset": ds, "ratio": 1, "resolution": None}] + unified = _make_unified(datasets) + unified.assign_worker(worker_id=2, num_workers=4, rank=0, world_size=2) + print(f" Rank 0, Worker 2 shards: {ds._registered_shards}") + assert ds._registered_shards == [2, 10, 18] + + # Rank 1, worker 1 -> family_worker_id = 1*4+1 = 5 -> shards [5, 13] + ds2 = _ShardableDataset("A", num_shards=20) + datasets2 = [{"name": "A", "dataset": ds2, "ratio": 1, "resolution": None}] + unified2 = _make_unified(datasets2) + unified2.assign_worker(worker_id=1, num_workers=4, rank=1, world_size=2) + print(f" Rank 1, Worker 1 shards: {ds2._registered_shards}") + assert ds2._registered_shards == [5, 13] + print(" Success: multi-rank family distribution correct") + + +# --------------------------------------------------------------------------- +# create_action_worker_init_fn +# --------------------------------------------------------------------------- + + +@pytest.mark.L0 +def test_worker_init_fn_calls_assign_worker(): + """worker_init_fn calls dataset.assign_worker with correct args.""" + from cosmos.data.vfm.action.dataloaders import create_action_worker_init_fn + + seed = 42 + + mock_dataset = MagicMock(spec=ActionUnifiedIterableDataset) + mock_info = MagicMock() + mock_info.dataset = mock_dataset + mock_info.num_workers = 8 + + with patch("cosmos.data.vfm.action.dataloaders.distributed") as mock_dist: + mock_dist.get_rank.return_value = 3 + mock_dist.get_world_size.return_value = 8 + fn = create_action_worker_init_fn(seed) + with patch("torch.utils.data.get_worker_info", return_value=mock_info): + fn(worker_id=5) + + mock_dataset.assign_worker.assert_called_once_with(5, 8, 3, 8) + print(" Success: assign_worker called with (worker_id=5, num_workers=8, rank=3, world_size=8)") + + +# --------------------------------------------------------------------------- +# BaseActionLeRobotDataset._register_sources +# --------------------------------------------------------------------------- + + +@pytest.mark.L0 +def test_register_sources_all(): + """_register_sources() with no args registers all shard roots.""" + from cosmos.data.vfm.action.cosmos3_action_lerobot import BaseActionLeRobotDataset + + ds = MagicMock(spec=BaseActionLeRobotDataset) + ds._all_shard_roots = ["/data/a", "/data/b", "/data/c"] + ds._delta_timestamps = {"obs": [0.0, 0.1]} + ds._tolerance_s = 0.01 + ds._enable_fast_init = False + BaseActionLeRobotDataset._register_sources(ds, indices=None) + print(f" _register_source called {ds._register_source.call_count} times") + assert ds._register_source.call_count == 3 + print(" Success: all 3 shard roots registered") + + +@pytest.mark.L0 +def test_register_sources_subset(): + """_register_sources(indices=[1]) registers only the specified shard.""" + from cosmos.data.vfm.action.cosmos3_action_lerobot import BaseActionLeRobotDataset + + ds = MagicMock(spec=BaseActionLeRobotDataset) + ds._all_shard_roots = ["/data/a", "/data/b"] + ds._delta_timestamps = {"obs": [0.0]} + ds._tolerance_s = 0.01 + ds._enable_fast_init = False + BaseActionLeRobotDataset._register_sources(ds, indices=[1]) + ds._register_source.assert_called_once_with( + root="/data/b", + delta_timestamps={"obs": [0.0]}, + tolerance_s=0.01, + dataset_label="b", + prefetched_meta=None, + ) + print(" Success: only shard [1] registered") + + +@pytest.mark.L0 +def test_register_sources_empty(): + """_register_sources() on empty _all_shard_roots is a no-op.""" + from cosmos.data.vfm.action.cosmos3_action_lerobot import BaseActionLeRobotDataset + + ds = MagicMock(spec=BaseActionLeRobotDataset) + ds._all_shard_roots = [] + BaseActionLeRobotDataset._register_sources(ds, indices=None) + ds._register_source.assert_not_called() + print(" Success: empty shard roots -> no calls") + + +# --------------------------------------------------------------------------- +# Distributed: 4-rank end-to-end test +# --------------------------------------------------------------------------- + + +@pytest.mark.L1 +def test_distributed_4rank_shard_assignment(): + """End-to-end test: 4 ranks, 2 datasets, 2 workers per rank via InfiniteDataLoader. + + Verifies that across 4 real distributed ranks: + - Hare-Niemeyer assigns ranks to the correct dataset family + - assign_worker is called by create_action_worker_init_fn inside DataLoader workers + - Each rank's workers only yield samples from the assigned dataset + - Shardable datasets get round-robin shard distribution + + Run with: + torchrun --nproc_per_node=4 --standalone -m pytest -v -s \\ + cosmos/data/vfm/action/unified_dataset_test.py::test_distributed_4rank_shard_assignment --L1 + """ + _maybe_init_distributed() + + world_size = torch.distributed.get_world_size() + if world_size != 4: + pytest.skip(f"This test requires exactly 4 ranks (got world_size={world_size})") + + rank = torch.distributed.get_rank() + + from cosmos.data.vfm.action.dataloaders import InfiniteDataLoader, create_action_worker_init_fn + + # Dataset A: shardable with 20 shards, ratio=3 + # Dataset B: plain iterable (no shards), ratio=1 + # Hare-Niemeyer with 4 ranks, ratios [3,1]: + # guarantee 1 each -> remaining 2 -> A=1.5 B=0.5 + # floors [1,0], leftover 1 -> A (tied remainder, lower index wins) + # Final: A=3 ranks [0,1,2], B=1 rank [3] + ds_a = _ShardableDataset("A", num_shards=20) + ds_b = _TaggedIterableDataset("B", length=100) + + datasets = [ + {"name": "A", "dataset": ds_a, "ratio": 3, "resolution": None}, + {"name": "B", "dataset": ds_b, "ratio": 1, "resolution": None}, + ] + + transform = MagicMock(side_effect=_identity_transform) + unified = ActionUnifiedIterableDataset( + datasets=datasets, + transform=transform, + shard_across_workers=True, + ) + + num_workers = 2 + loader = InfiniteDataLoader( + dataset=unified, + batch_size=1, + num_workers=num_workers, + worker_init_fn=create_action_worker_init_fn(seed=42), + ) + + # Consume samples and verify dataset assignment + n_samples = 20 + samples = list(itertools.islice(loader, n_samples)) + assert len(samples) == n_samples, f"[Rank {rank}] Expected {n_samples} batches, got {len(samples)}" + + # Each sample is a collated batch dict; extract the dataset_tag + tags = set() + for batch in samples: + if "dataset_tag" in batch: + tag = batch["dataset_tag"] + if isinstance(tag, list): + tags.update(tag) + else: + tags.add(tag) + + # Ranks 0-2 -> dataset A, rank 3 -> dataset B + if rank in (0, 1, 2): + expected_tag = "A" + else: + expected_tag = "B" + + print(f"[Rank {rank}] Tags seen: {tags}, expected: {{{expected_tag}}}") + assert tags == {expected_tag}, f"[Rank {rank}] Expected only '{expected_tag}' samples, got tags: {tags}" + + # For shardable dataset A (ranks 0-2): verify shards were distributed + if rank in (0, 1, 2): + # After workers ran, ds_a._registered_shards should have been set + # by _register_sources in the worker processes. Since workers are + # separate processes, we can't inspect ds_a directly. Instead we + # check that the "registered_shards" key in the yielded samples + # contains a non-empty subset. + for batch in samples: + if "registered_shards" in batch: + shards = batch["registered_shards"] + if isinstance(shards, torch.Tensor): + shards = shards.tolist() + elif isinstance(shards, list) and len(shards) > 0 and isinstance(shards[0], torch.Tensor): + shards = [s.item() for s in shards] + print(f"[Rank {rank}] Sample registered_shards: {shards}") + assert len(shards) > 0, f"[Rank {rank}] Expected non-empty registered_shards" + assert all(0 <= s < 20 for s in shards), f"[Rank {rank}] Shard indices out of range: {shards}" + break + + # Barrier to ensure all ranks completed + torch.distributed.barrier() + print(f"[Rank {rank}] Success: distributed shard assignment test passed!") diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/G1_omnipicker_calibrated.urdf b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/G1_omnipicker_calibrated.urdf new file mode 100644 index 00000000..bd83679e --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/G1_omnipicker_calibrated.urdf @@ -0,0 +1,1350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/__init__.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/contracts_test.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/contracts_test.py new file mode 100644 index 00000000..a0565ba5 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/contracts_test.py @@ -0,0 +1,302 @@ +import numpy as np +import pytest + +from cosmos.data.vfm.action.urdf_visualizer import ik_solver, robot_scene_model, unified_renderer +from cosmos.data.vfm.action.urdf_visualizer.robot_scene_model import RobotSceneModel +from cosmos.data.vfm.action.urdf_visualizer.unified_action import ( + ALL_FINGERS, + Action57DMask, + ActionFormat, + UnifiedAction, + build_scene_state, + to_unified, +) +from cosmos.data.vfm.action.urdf_visualizer.unified_renderer import _agibot_gripper_tip_positions + + +@pytest.mark.L0 +@pytest.mark.parametrize( + ("action_format", "action", "expected_mask"), + [ + ( + ActionFormat.EGO_9D, + np.zeros((2, 9), dtype=np.float32), + Action57DMask(ego=True), + ), + ( + ActionFormat.SINGLE_ARM_10D, + np.zeros((2, 10), dtype=np.float32), + Action57DMask(right_wrist=True), + ), + ( + ActionFormat.DUAL_ARM_20D, + np.zeros((2, 20), dtype=np.float32), + Action57DMask(right_wrist=True, left_wrist=True), + ), + ( + ActionFormat.UNIFIED_57D, + np.zeros((2, 57), dtype=np.float32), + Action57DMask( + ego=True, + right_wrist=True, + right_fingers=ALL_FINGERS, + left_wrist=True, + left_fingers=ALL_FINGERS, + ), + ), + ], +) +def test_to_unified_uses_explicit_action_format( + action_format: ActionFormat, + action: np.ndarray, + expected_mask: Action57DMask, +) -> None: + """Explicit action formats should decode to one canonical 57D layout.""" + unified = to_unified(action, action_format=action_format) + + assert unified.action.shape == (2, 57) + assert unified.mask == expected_mask + + +@pytest.mark.L0 +def test_to_unified_rejects_shape_mismatches() -> None: + """Declared raw action formats should fail fast on incompatible tensors.""" + with pytest.raises(ValueError, match="expects trailing dim 10"): + to_unified(np.zeros((2, 9), dtype=np.float32), action_format=ActionFormat.SINGLE_ARM_10D) + + +@pytest.mark.L0 +def test_build_scene_state_outputs_world_space_contract() -> None: + """SceneState should be fully canonicalized into world-space during construction.""" + action = np.zeros((1, 57), dtype=np.float32) + action[0, 9:18] = np.array([0.1, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], dtype=np.float32) + action[0, 18:21] = np.array([0.0, 0.0, 0.05], dtype=np.float32) + action[0, 33:42] = np.array([0.2, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], dtype=np.float32) + unified = UnifiedAction( + action=action, + mask=Action57DMask( + right_wrist=True, + right_fingers=ALL_FINGERS, + left_wrist=True, + ), + ) + + right_base = np.eye(4, dtype=np.float32) + right_base[1, 3] = -0.3 + left_base = np.eye(4, dtype=np.float32) + left_base[1, 3] = 0.3 + + state = build_scene_state( + unified, + right_base_pose=right_base, + left_base_pose=left_base, + ) + + np.testing.assert_allclose(state.right_poses[1, :3, 3], np.array([0.1, -0.3, 0.0], dtype=np.float32)) + np.testing.assert_allclose(state.left_poses[1, :3, 3], np.array([0.2, 0.3, 0.0], dtype=np.float32)) + np.testing.assert_allclose(state.right_fingers[0, 0], np.array([0.0, -0.3, 0.05], dtype=np.float32)) + np.testing.assert_allclose(state.right_fingers[1, 0], np.array([0.1, -0.3, 0.05], dtype=np.float32)) + + +@pytest.mark.L0 +def test_robot_scene_model_caches_home_meshes_and_applies_base_pose(monkeypatch: pytest.MonkeyPatch) -> None: + """RobotSceneModel should own home mesh loading and world-frame placement.""" + calls = {"count": 0} + mesh_transform = np.eye(4, dtype=np.float32) + mesh_transform[2, 3] = 0.2 + + monkeypatch.setattr(robot_scene_model, "get_robot_config", lambda _: {"ee_frame": "tool"}) + monkeypatch.setattr(robot_scene_model, "get_mjcf_path", lambda _: "/tmp/fake.xml") + + def _loader() -> tuple[list[tuple[str, object, np.ndarray]], np.ndarray]: + calls["count"] += 1 + return [("geom", object(), mesh_transform)], np.eye(4, dtype=np.float32) + + monkeypatch.setattr(robot_scene_model, "get_robot_loaders", lambda: {"fake_robot": _loader}) + + model = RobotSceneModel("fake_robot") + meshes_local = model.get_home_meshes() + base_pose = np.eye(4, dtype=np.float32) + base_pose[0, 3] = 1.5 + meshes_world = model.get_home_meshes(base_pose=base_pose) + + assert calls["count"] == 1 + np.testing.assert_allclose(meshes_local[0][2][:3, 3], np.array([0.0, 0.0, 0.2], dtype=np.float32)) + np.testing.assert_allclose(meshes_world[0][2][:3, 3], np.array([1.5, 0.0, 0.2], dtype=np.float32)) + + +@pytest.mark.L0 +def test_robot_scene_model_reapplies_base_pose_after_solving(monkeypatch: pytest.MonkeyPatch) -> None: + """RobotSceneModel should solve in arm-local space but return world-space geometry.""" + captured: dict[str, np.ndarray] = {} + + monkeypatch.setattr(robot_scene_model, "get_robot_config", lambda _: {"ee_frame": "tool"}) + monkeypatch.setattr(robot_scene_model, "get_mjcf_path", lambda _: "/tmp/fake.xml") + + def _fake_solve( + _mjcf_path: str, + world_ee_positions: np.ndarray, + gripper_openings: np.ndarray | None = None, + world_ee_orientations: np.ndarray | None = None, + robot_name: str | None = None, + ) -> np.ndarray: + captured["positions"] = world_ee_positions.copy() + captured["orientations"] = world_ee_orientations.copy() + captured["grippers"] = ( + gripper_openings.copy() if gripper_openings is not None else np.array([], dtype=np.float32) + ) + assert robot_name == "fake_robot" + return np.zeros((2, 1), dtype=np.float32) + + def _fake_fk( + _mjcf_path: str, + _joint_configs: np.ndarray, + robot_name: str | None = None, + ) -> tuple[np.ndarray, np.ndarray]: + assert robot_name == "fake_robot" + positions = np.array([[0.0, 0.0, 0.0], [0.2, 0.0, 0.0]], dtype=np.float32) + rotations = np.tile(np.eye(3, dtype=np.float32), (2, 1, 1)) + return positions, rotations + + def _fake_geom( + _mjcf_path: str, + _joint_configs: np.ndarray, + ) -> tuple[ + list[list[tuple[np.ndarray, np.ndarray]]], + None, + None, + dict[str, list[tuple[np.ndarray, np.ndarray]]], + ]: + transforms = [ + [(np.array([0.0, 0.0, 0.0], dtype=np.float32), np.eye(3, dtype=np.float32))], + [(np.array([0.2, 0.0, 0.0], dtype=np.float32), np.eye(3, dtype=np.float32))], + ] + frames = { + "body:tool": [ + (np.array([0.0, 0.0, 0.0], dtype=np.float32), np.eye(3, dtype=np.float32)), + (np.array([0.2, 0.0, 0.0], dtype=np.float32), np.eye(3, dtype=np.float32)), + ] + } + return transforms, None, None, frames + + monkeypatch.setattr(ik_solver, "solve_trajectory_ik", _fake_solve) + monkeypatch.setattr(ik_solver, "compute_fk_ee_poses", _fake_fk) + monkeypatch.setattr(ik_solver, "compute_mujoco_geom_transforms", _fake_geom) + + model = RobotSceneModel("fake_robot") + wrist_poses_world = np.tile(np.eye(4, dtype=np.float32), (2, 1, 1)) + wrist_poses_world[:, 0, 3] = np.array([1.0, 1.2], dtype=np.float32) + base_pose = np.eye(4, dtype=np.float32) + base_pose[0, 3] = 1.0 + grippers = np.array([0.0, 0.25], dtype=np.float32) + + result = model.solve_visual_trajectory( + wrist_poses_world, + gripper_openings=grippers, + base_pose=base_pose, + ) + + assert result is not None + np.testing.assert_allclose( + captured["positions"][:, 0], + np.array([0.0, 0.2], dtype=np.float32), + atol=1e-6, + ) + np.testing.assert_allclose( + result.mesh_transforms[1][0][0], + np.array([1.2, 0.0, 0.0], dtype=np.float32), + atol=1e-6, + ) + np.testing.assert_allclose( + result.named_frames["ik:tool"][1][0], + np.array([1.2, 0.0, 0.0], dtype=np.float32), + atol=1e-6, + ) + + +@pytest.mark.L0 +def test_agibot_gripper_tip_spheres_open_along_opencv_x_axis() -> None: + """AgiBot synthetic tip spheres should open horizontally in the corrected wrist frame.""" + + pos = np.array([0.1, 0.2, 0.3], dtype=np.float32) # [3] + rot = np.eye(3, dtype=np.float32) # [3,3] + + tip_l, tip_r = _agibot_gripper_tip_positions( + pos=pos, + rot=rot, + opening=1.0, + max_finger_width=0.12, + ) + + expected_center = pos + np.array([0.0, 0.0, 0.14308], dtype=np.float32) # [3] + np.testing.assert_allclose(tip_l, expected_center + np.array([0.06, 0.0, 0.0], dtype=np.float32), atol=1e-6) + np.testing.assert_allclose(tip_r, expected_center + np.array([-0.06, 0.0, 0.0], dtype=np.float32), atol=1e-6) + + +@pytest.mark.L0 +def test_agibot_renderer_uses_direct_link_poses_before_ik(monkeypatch: pytest.MonkeyPatch) -> None: + """Dataset FK link poses should drive AgiBot meshes directly when present.""" + + class _Scene: + def __init__(self) -> None: + self.handles: list[object] = [] + + def add_mesh_trimesh(self, *_args, **_kwargs): + class _Handle: + visible = True + + def remove(self) -> None: + self.visible = False + + handle = _Handle() + self.handles.append(handle) + return handle + + class _Server: + def __init__(self) -> None: + self.scene = _Scene() + + class _Entry: + robot_name = "embodiment_c" + robot_embodiment_type = "embodiment_c_gripper" + dataset_kwargs: dict[str, str] = {} + + geometry = type( + "Geometry", + (), + { + "name": "moving_link_geometry_0", + "link_name": "moving_link", + "mesh": object(), + "local_transform": np.eye(4, dtype=np.float32), + }, + )() + monkeypatch.setattr(unified_renderer, "get_embodiment_c_collision_geometries", lambda: [geometry]) + + def _fail_ik(*_args, **_kwargs): + raise AssertionError("IK should not run when direct AgiBot FK link poses are present") + + monkeypatch.setattr(unified_renderer, "solve_agibot_trajectory_ik", _fail_ik) + + poses = np.tile(np.eye(4, dtype=np.float32), (2, 1, 1)) # [T+1,4,4] + poses[:, 0, 3] = np.array([0.1, 0.4], dtype=np.float32) # [T+1] + state = build_scene_state( + UnifiedAction(np.zeros((1, 57), dtype=np.float32), Action57DMask()), + sample={"agibot_link_poses": {"moving_link": poses}}, + ) + + renderer = object.__new__(unified_renderer.UnifiedRenderer) + renderer.server = _Server() + renderer.robot_right = [] + renderer.robot_left = [] + renderer._robot_frame_handles_right = {} + renderer._robot_frame_handles_left = {} + renderer._robot_link_names = [] + renderer._robot_local_transforms = [] + renderer._current_robot = None + + renderer._load_robot_and_ik(state, _Entry()) + + assert renderer._ik_left is None + assert len(renderer._ik_right) == 2 + np.testing.assert_allclose(renderer._ik_right[1][0][0], np.array([0.4, 0.0, 0.0], dtype=np.float32)) diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/droid_franka_robotiq_2f85.xml b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/droid_franka_robotiq_2f85.xml new file mode 100644 index 00000000..96c81c21 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/droid_franka_robotiq_2f85.xml @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ik_solver.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ik_solver.py new file mode 100644 index 00000000..0b14bc5b --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ik_solver.py @@ -0,0 +1,849 @@ +"""Robot-agnostic IK solver using pinocchio + MuJoCo. + +Supports any robot loaded from MJCF (Google Robot, Franka Panda, WidowX, etc). +Auto-detects EE frame, arm vs finger joints, and uses multi-start random seeding. + +The solver: +1. Auto-discovers the EE frame from a list of candidate names +2. Determines which joints are "arm" joints (actuated for IK) vs "finger" joints +3. Uses multi-start random sampling to avoid local minima +4. Optionally sets finger joint angles from gripper opening fractions +""" + +from functools import lru_cache +from typing import Any + +import numpy as np + +from cosmos.utils import log +from cosmos.data.vfm.action.embodiment_c_spec import ( + AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD, + AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME, + AGIBOT_GEAR_LEFT_EE_LINK_NAME, + AGIBOT_GEAR_LEFT_GRIPPER_JOINT_MIMICS, + AGIBOT_GEAR_RIGHT_EE_LINK_NAME, + AGIBOT_GEAR_RIGHT_GRIPPER_JOINT_MIMICS, +) +from cosmos.data.vfm.action.urdf_visualizer.robot_scene_model import ( + get_ee_frame_candidates, + get_mujoco_to_pinocchio_world_transform, + get_robot_config, + get_urdf_path, + get_visual_geom_ids, + resolve_robot_name_from_mjcf, +) + +# ── IK Solver ──────────────────────────────────────────────────────────────── + + +def _find_ee_frame(model, robot_name: str | None = None) -> int | None: + """Find the end-effector frame ID by trying candidate names. + + If robot_name is given and its config specifies ``ee_frame``, that name + is tried first before falling through to the generic candidates. + """ + cfg = get_robot_config(robot_name) if robot_name else {} + override = cfg.get("ee_frame") + if override: + fid = model.getFrameId(override) + if fid < model.nframes: + return fid + log.warning(f"Configured ee_frame '{override}' not found in Pinocchio model") + + for name in get_ee_frame_candidates(): + fid = model.getFrameId(name) + if fid < model.nframes: + return fid + log.warning(f"Could not find EE frame by name for robot '{robot_name}', skipping IK") + return None + + +@lru_cache(maxsize=1) +def _get_agibot_model() -> tuple[Any, dict[str, int]]: + """Load the AgiBot URDF model and cache the key frame IDs.""" + + import pinocchio as pin # pyright: ignore[reportMissingImports] + + urdf_path = get_urdf_path("embodiment_c") + if urdf_path is None: + raise FileNotFoundError("AgiBot URDF path is unavailable") + + model = pin.buildModelFromUrdf(urdf_path) + frame_ids = { + "head": model.getFrameId(AGIBOT_GEAR_HEAD_CAMERA_LINK_NAME), + "left": model.getFrameId(AGIBOT_GEAR_LEFT_EE_LINK_NAME), + "right": model.getFrameId(AGIBOT_GEAR_RIGHT_EE_LINK_NAME), + } + for frame_name, frame_id in frame_ids.items(): + if frame_id >= model.nframes: + raise ValueError(f"AgiBot frame {frame_name!r} missing from Pinocchio model") + return model, frame_ids + + +def _set_agibot_gripper_joint_configs( + model: Any, + joint_configs: np.ndarray, + *, + left_gripper_openings: np.ndarray | None, + right_gripper_openings: np.ndarray | None, +) -> None: + """Write AgiBot omnipicker finger joint angles from open fractions.""" + + side_specs = ( + ("left", left_gripper_openings, AGIBOT_GEAR_LEFT_GRIPPER_JOINT_MIMICS), + ("right", right_gripper_openings, AGIBOT_GEAR_RIGHT_GRIPPER_JOINT_MIMICS), + ) + lower = model.lowerPositionLimit + upper = model.upperPositionLimit + num_steps = int(joint_configs.shape[0]) + + for side, openings, joint_mimics in side_specs: + if openings is None: + continue + if len(openings) != num_steps: + log.warning( + f"AgiBot {side} gripper openings length {len(openings)} does not match IK frames {num_steps}; " + "leaving finger joints at neutral." + ) + continue + + joint_indices: list[tuple[int, float, float]] = [] + for joint_name, multiplier, offset in joint_mimics: + joint_id = model.getJointId(joint_name) + if joint_id >= model.njoints: + log.warning(f"AgiBot {side} gripper joint '{joint_name}' not found in Pinocchio model") + continue + joint_indices.append((model.idx_qs[joint_id], multiplier, offset)) + + for step_idx, opening in enumerate(openings): + primary_angle = -float(np.clip(opening, 0.0, 1.0)) * AGIBOT_GEAR_GRIPPER_OPEN_ANGLE_RAD + for q_idx, multiplier, offset in joint_indices: + joint_value = multiplier * primary_angle + offset + joint_configs[step_idx, q_idx] = np.clip(joint_value, lower[q_idx], upper[q_idx]) + + +def solve_agibot_trajectory_ik( + head_camera_poses: np.ndarray | None, + left_wrist_poses: np.ndarray | None, + right_wrist_poses: np.ndarray | None, + left_gripper_openings: np.ndarray | None = None, + right_gripper_openings: np.ndarray | None = None, + max_iter: int = 200, + dt: float = 0.2, + damp: float = 1e-4, + pos_tol_m: float = 5e-3, + rot_tol_rad: float = 5e-2, +) -> np.ndarray | None: + """Solve full-body AgiBot IK from calibrated head-camera and gripper-base trajectories.""" + + import pinocchio as pin # pyright: ignore[reportMissingImports] + + targets: list[np.ndarray] = [ + poses for poses in (head_camera_poses, left_wrist_poses, right_wrist_poses) if poses is not None + ] + if not targets: + return None + + num_steps = int(targets[0].shape[0]) + if any(int(poses.shape[0]) != num_steps for poses in targets): + raise ValueError("AgiBot IK requires head/left/right trajectories to have matching lengths") + + model, frame_ids = _get_agibot_model() + data = model.createData() + lower = model.lowerPositionLimit.copy() + upper = model.upperPositionLimit.copy() + + head_targets = head_camera_poses.astype(np.float32, copy=False) if head_camera_poses is not None else None + left_targets = left_wrist_poses.astype(np.float32, copy=False) if left_wrist_poses is not None else None + right_targets = right_wrist_poses.astype(np.float32, copy=False) if right_wrist_poses is not None else None + + task_specs: list[tuple[str, np.ndarray, int, float]] = [] + if head_targets is not None: + task_specs.append(("head", head_targets, frame_ids["head"], 0.75)) + if left_targets is not None: + task_specs.append(("left", left_targets, frame_ids["left"], 1.0)) + if right_targets is not None: + task_specs.append(("right", right_targets, frame_ids["right"], 1.0)) + + joint_configs = np.empty((num_steps, model.nq), dtype=np.float32) + q = pin.neutral(model) + rot_weight = 0.35 + + for step_idx in range(num_steps): + best_q = q.copy() + best_total = float("inf") + best_pos = float("inf") + best_rot = float("inf") + + for _ in range(max_iter): + pin.forwardKinematics(model, data, q) + pin.updateFramePlacements(model, data) + + errors: list[np.ndarray] = [] + jacobians: list[np.ndarray] = [] + max_pos_error = 0.0 + max_rot_error = 0.0 + + for _, poses, frame_id, task_weight in task_specs: + target = poses[step_idx] + placement = data.oMf[frame_id] + pos_err = target[:3, 3] - placement.translation + rot_err = pin.log3(target[:3, :3] @ placement.rotation.T) + max_pos_error = max(max_pos_error, float(np.linalg.norm(pos_err))) + max_rot_error = max(max_rot_error, float(np.linalg.norm(rot_err))) + + err6 = np.concatenate([pos_err, rot_weight * rot_err]).astype(np.float64, copy=False) + errors.append(task_weight * err6) + + jacobian = pin.computeFrameJacobian(model, data, q, frame_id, pin.LOCAL_WORLD_ALIGNED).copy() + jacobian[3:, :] *= rot_weight + jacobians.append(task_weight * jacobian) + + error_vec = np.concatenate(errors, axis=0) + total_error = float(np.linalg.norm(error_vec)) + if total_error < best_total: + best_total = total_error + best_q = q.copy() + best_pos = max_pos_error + best_rot = max_rot_error + + if max_pos_error < pos_tol_m and max_rot_error < rot_tol_rad: + break + + stacked_jacobian = np.concatenate(jacobians, axis=0) + normal = stacked_jacobian @ stacked_jacobian.T + damp * np.eye(stacked_jacobian.shape[0]) + velocity = stacked_jacobian.T @ np.linalg.solve(normal, error_vec) + q = pin.integrate(model, q, velocity * dt) + q = np.clip(q, lower, upper) + + q = best_q.copy() + joint_configs[step_idx] = q.astype(np.float32, copy=False) + log.info(f"AgiBot IK frame {step_idx}: max_pos={best_pos * 1000:.2f}mm, max_rot={np.degrees(best_rot):.2f}deg") + + _set_agibot_gripper_joint_configs( + model, + joint_configs, + left_gripper_openings=left_gripper_openings, + right_gripper_openings=right_gripper_openings, + ) + return joint_configs + + +def compute_agibot_link_poses_batch_from_configs( + joint_configs: np.ndarray, + link_names: list[str], +) -> dict[str, np.ndarray]: + """Compute AgiBot URDF link poses from solved full-body joint configurations.""" + + import pinocchio as pin # pyright: ignore[reportMissingImports] + + if joint_configs.size == 0: + return {link_name: np.empty((0, 4, 4), dtype=np.float32) for link_name in link_names} + + model, _ = _get_agibot_model() + data = model.createData() + frame_ids = {link_name: model.getFrameId(link_name) for link_name in link_names} + for link_name, frame_id in frame_ids.items(): + if frame_id >= model.nframes: + raise ValueError(f"AgiBot link frame {link_name!r} missing from Pinocchio model") + + num_steps = int(joint_configs.shape[0]) + link_poses = {link_name: np.empty((num_steps, 4, 4), dtype=np.float32) for link_name in link_names} + + for step_idx in range(num_steps): + q = joint_configs[step_idx].astype(np.float64, copy=False) + pin.forwardKinematics(model, data, q) + pin.updateFramePlacements(model, data) + for link_name, frame_id in frame_ids.items(): + placement = data.oMf[frame_id] + transform = np.eye(4, dtype=np.float32) + transform[:3, :3] = placement.rotation.astype(np.float32, copy=False) + transform[:3, 3] = placement.translation.astype(np.float32, copy=False) + link_poses[link_name][step_idx] = transform + + return link_poses + + +def solve_trajectory_ik( + mjcf_path: str, + world_ee_positions: np.ndarray, + gripper_openings: np.ndarray | None = None, + world_ee_orientations: np.ndarray | None = None, + robot_name: str | None = None, + max_random_samples: int = 50_000, + seed: int = 42, +) -> np.ndarray | None: + """Solve IK for a sequence of world-space EE poses (robot-agnostic). + + Args: + mjcf_path: Path to the MJCF XML file. + world_ee_positions: (T, 3) target EE positions. + gripper_openings: (T,) gripper opening fractions [0=closed, 1=open]. + world_ee_orientations: (T, 3, 3) target EE rotation matrices. + robot_name: Robot name for config lookup (optional, inferred from path). + max_random_samples: Number of random configs to try for initial seed. + seed: Random seed. + + Returns: + (T, nq) joint configurations, or None if IK fails. + """ + import pinocchio as pin + + model, cfg = _build_pinocchio_model(mjcf_path, robot_name) + log.info(f"IK: pinocchio model nq={model.nq}") + data = model.createData() + ee_id = _find_ee_frame(model, robot_name) + if ee_id is None: + return None + ee_name = model.frames[ee_id].name + log.info(f"IK: using EE frame '{ee_name}' (id={ee_id}), nq={model.nq}") + + T = len(world_ee_positions) + use_6dof = world_ee_orientations is not None and len(world_ee_orientations) == T + + # Apply TCP offset: dataset EE poses may be at the TCP (e.g. ee_gripper_link) + # while the Pinocchio frame is at the kinematic link origin (e.g. gripper_link). + # Convert target TCP poses → IK link-frame targets: + # p_link = p_tcp - R_tcp @ tcp_offset + tcp_offset = cfg.get("tcp_offset") + if tcp_offset is not None: + tcp_offset = np.asarray(tcp_offset, dtype=np.float32) + log.info(f"IK: applying TCP offset {tcp_offset} to target positions") + world_ee_positions = world_ee_positions.copy() + for t in range(T): + if use_6dof: + world_ee_positions[t] -= world_ee_orientations[t] @ tcp_offset + else: + world_ee_positions[t] -= tcp_offset + + lower = model.lowerPositionLimit.copy() + upper = model.upperPositionLimit.copy() + + # Determine arm joints vs finger joints + # After model reduction, arm joints are first, fingers follow + n_arm = cfg.get("n_arm_joints", model.nq - 2) # default: all but last 2 are arm + n_finger = model.nq - n_arm + + log.info(f"IK: {n_arm} arm joints + {n_finger} finger joints") + + # ── 6-DoF CLIK (position + orientation) ── + def _ik_6dof(target_pos, target_rot, q_init, max_iter=800, eps_pos=5e-5, eps_rot=1e-3, dt=0.1, damp=1e-4): + q = q_init.copy() + best_q = q.copy() + best_total = float("inf") + stall_count = 0 + rot_weight = 1.0 + + for it in range(max_iter): + pin.forwardKinematics(model, data, q) + pin.updateFramePlacements(model, data) + + pos_err = target_pos - data.oMf[ee_id].translation + pos_norm = np.linalg.norm(pos_err) + R_err = target_rot @ data.oMf[ee_id].rotation.T + rot_err = pin.log3(R_err) + rot_norm = np.linalg.norm(rot_err) + + if pos_norm < eps_pos and rot_norm < eps_rot: + return q, pos_norm, rot_norm, it + 1 + + total = pos_norm + 0.05 * rot_norm + if total < best_total: + best_total = total + best_q = q.copy() + stall_count = 0 + else: + stall_count += 1 + + if stall_count > 50 and rot_weight > 0.1: + rot_weight *= 0.8 + stall_count = 0 + if stall_count > 150: + break + + err6 = np.concatenate([pos_err, rot_weight * rot_err]) + J = pin.computeFrameJacobian(model, data, q, ee_id, pin.LOCAL_WORLD_ALIGNED).copy() + J[3:, :] *= rot_weight + # Zero out finger joint columns + J[:, n_arm:] = 0 + JJt = J @ J.T + damp * np.eye(6) + v = J.T @ np.linalg.solve(JJt, err6) + v[n_arm:] = 0 # don't move finger joints + + q = pin.integrate(model, q, v * dt) + q = np.clip(q, lower, upper) + + pin.forwardKinematics(model, data, best_q) + pin.updateFramePlacements(model, data) + pos_norm = np.linalg.norm(target_pos - data.oMf[ee_id].translation) + rot_norm = np.linalg.norm(pin.log3(target_rot @ data.oMf[ee_id].rotation.T)) + return best_q, pos_norm, rot_norm, max_iter + + # ── 3-DoF CLIK (position only) ── + def _ik_3dof(target_pos, q_init, max_iter=500, eps=5e-5, dt=0.15, damp=1e-4): + q = q_init.copy() + for it in range(max_iter): + pin.forwardKinematics(model, data, q) + pin.updateFramePlacements(model, data) + err = target_pos - data.oMf[ee_id].translation + if np.linalg.norm(err) < eps: + return q, np.linalg.norm(err), it + 1 + J = pin.computeFrameJacobian(model, data, q, ee_id, pin.LOCAL_WORLD_ALIGNED)[:3] + J[:, n_arm:] = 0 + v = J.T @ np.linalg.solve(J @ J.T + damp * np.eye(3), err) + v[n_arm:] = 0 + q = pin.integrate(model, q, v * dt) + q = np.clip(q, lower, upper) + return q, np.linalg.norm(err), max_iter + + def _solve_full_trajectory(seed_q): + configs = [] + max_pe = 0.0 + max_re = 0.0 + q = seed_q.copy() + for t in range(T): + if use_6dof: + q, pe, re, _ = _ik_6dof(world_ee_positions[t], world_ee_orientations[t], q) + max_pe = max(max_pe, float(pe)) + max_re = max(max_re, float(re)) + else: + q, pe, _ = _ik_3dof(world_ee_positions[t], q) + max_pe = max(max_pe, float(pe)) + configs.append(q.copy()) + return np.array(configs), max_pe, max_re + + # ── Multi-start seed search ── + # For robots with a base rotation joint (Google Robot torso, WidowX waist), + # search multiple rotation basins. For Franka (no base rotation freedom), + # use a single wider search. + base_joint_range = upper[0] - lower[0] + if base_joint_range > 4.0: + # Wide base rotation — split into basins (Google Robot, WidowX) + n_basins = 4 + basin_size = base_joint_range / n_basins + basins = [] + for i in range(n_basins): + b_lo = lower[0] + i * basin_size + b_hi = lower[0] + (i + 1) * basin_size + basins.append((b_lo, b_hi)) + else: + # No wide base rotation — single basin (Franka) + basins = [(lower[0], upper[0])] + + samples_per_basin = max_random_samples // len(basins) + target0_pos = world_ee_positions[0] + target0_rot = world_ee_orientations[0] if use_6dof else None + + best_overall_configs = None + best_overall_score = float("inf") + best_basin_info = "" + best_max_pe = 0.0 + best_max_re = 0.0 + + for basin_idx, (b_lo, b_hi) in enumerate(basins): + rng = np.random.RandomState(seed + basin_idx) + basin_lower = lower.copy() + basin_upper = upper.copy() + basin_lower[0] = max(lower[0], b_lo) + basin_upper[0] = min(upper[0], b_hi) + + # Find best seed in this basin + basin_best_q = pin.neutral(model) + basin_best_q[0] = (b_lo + b_hi) / 2 + basin_best_score = float("inf") + + for _ in range(samples_per_basin): + q = rng.uniform(basin_lower, basin_upper) + pin.forwardKinematics(model, data, q) + pin.updateFramePlacements(model, data) + pos_err = np.linalg.norm(data.oMf[ee_id].translation - target0_pos) + + if target0_rot is not None: + rot_err = np.linalg.norm(pin.log3(target0_rot.T @ data.oMf[ee_id].rotation)) + score = pos_err + 0.3 * rot_err + else: + score = pos_err + + if score < basin_best_score: + basin_best_score = score + basin_best_q = q.copy() + if pos_err < 0.005 and (target0_rot is None or rot_err < 0.1): + break + + if basin_best_score > 0.5: + continue + + configs, max_pe, max_re = _solve_full_trajectory(basin_best_q) + traj_score = max_pe + 0.05 * max_re + log.info( + f" Basin [{b_lo:+.1f}, {b_hi:+.1f}]: seed_score={basin_best_score:.4f}, " + f"traj max_pos={max_pe * 1000:.1f}mm, max_rot={np.degrees(max_re):.1f}°, " + f"j0={basin_best_q[0]:+.2f}rad" + ) + + if traj_score < best_overall_score: + best_overall_score = traj_score + best_overall_configs = configs + best_basin_info = f"j0_basin=[{b_lo:+.1f},{b_hi:+.1f}], seed_j0={basin_best_q[0]:+.2f}rad" + best_max_pe = max_pe + best_max_re = max_re + + if best_overall_configs is None: + log.warning("IK failed: no basin converged") + return None + + configs = best_overall_configs + if use_6dof: + log.info( + f"IK solved ({T} frames, 6-DoF): max_pos={best_max_pe * 1000:.2f}mm, max_rot={np.degrees(best_max_re):.1f}° [{best_basin_info}]" + ) + else: + log.info(f"IK solved ({T} frames, 3-DoF): max_pos={best_max_pe * 1000:.2f}mm [{best_basin_info}]") + + # ── Set finger joints from gripper openings ── + if n_finger > 0: + finger_min = cfg.get("finger_min", lower[n_arm]) + finger_max = cfg.get("finger_max", upper[n_arm]) + else: + finger_min = 0.0 + finger_max = 0.0 + close_is_max = cfg.get("finger_close_is_max", True) + finger_joint_names = cfg.get("finger_joint_names") + + if gripper_openings is not None and len(gripper_openings) == T: + # Find finger joint indices by name if specified (e.g., Robotiq driver joints) + if finger_joint_names: + # Use Pinocchio joint name lookup + finger_indices = [] + for fjn in finger_joint_names: + # Pinocchio joint names include the joint name from MJCF + jid = model.getJointId(fjn) + if jid < model.njoints: + # Pinocchio joint index → qpos index + qi = model.idx_qs[jid] + finger_indices.append(qi) + else: + log.warning(f"Finger joint '{fjn}' not found in Pinocchio model") + if not finger_indices: + finger_indices = list(range(n_arm, n_arm + n_finger)) + else: + finger_indices = list(range(n_arm, n_arm + n_finger)) + + for t in range(T): + g = float(np.clip(gripper_openings[t], 0.0, 1.0)) + if close_is_max: + # Robotiq/Google Robot: high angle = closed + angle = finger_max - g * (finger_max - finger_min) + else: + # Franka/WidowX: high value = open + angle = finger_min + g * (finger_max - finger_min) + + for ji in finger_indices: + # WidowX right finger is negative range + if lower[ji] < 0 and upper[ji] < 0: + configs[t, ji] = -(finger_min + g * (finger_max - finger_min)) + else: + configs[t, ji] = angle + log.info( + f"Finger joints set from gripper openings ({gripper_openings.min():.2f} to {gripper_openings.max():.2f})" + ) + + return configs + + +def _build_pinocchio_model(mjcf_path: str, robot_name: str | None = None): + """Build a pinocchio model, reducing to arm+finger joints if configured. + + For URDF models (SimplerEnv), builds from URDF and locks non-arm joints. + For MJCF models (Menagerie), builds from MJCF and locks non-arm joints. + Shared by solve_trajectory_ik and compute_fk_ee_poses. + """ + import pinocchio as pin + + cfg = get_robot_config(robot_name) if robot_name else {} + urdf_path = get_urdf_path(robot_name) if robot_name else None + + if urdf_path: + full_model = pin.buildModelFromUrdf(urdf_path) + else: + full_model = pin.buildModelFromMJCF(mjcf_path) + + # Reduce model to arm + finger joints only (lock base, wheels, etc.) + arm_joint_names = cfg.get("arm_joints", []) + finger_jnames = cfg.get("finger_joint_names", []) + keep_names = set(arm_joint_names) | set(finger_jnames) + + if keep_names: + lock_ids = [ji for ji in range(1, full_model.njoints) if full_model.names[ji] not in keep_names] + if lock_ids: + q_ref = pin.neutral(full_model) + model = pin.buildReducedModel(full_model, lock_ids, q_ref) + else: + model = full_model + else: + model = full_model + + return model, cfg + + +def compute_fk_ee_poses( + mjcf_path: str, + joint_configs: np.ndarray, + robot_name: str | None = None, +) -> tuple[np.ndarray, np.ndarray]: + """Run FK and return EE positions and orientations.""" + import pinocchio as pin + + model, _ = _build_pinocchio_model(mjcf_path, robot_name) + data = model.createData() + ee_id = _find_ee_frame(model, robot_name) + if ee_id is None: + log.warning(f"Skipping FK — no EE frame found for robot '{robot_name}'") + T = len(joint_configs) + return np.zeros((T, 3)), np.zeros((T, 3, 3)) + + T = len(joint_configs) + fk_positions = np.zeros((T, 3)) + fk_orientations = np.zeros((T, 3, 3)) + + for t in range(T): + pin.forwardKinematics(model, data, joint_configs[t]) + pin.updateFramePlacements(model, data) + fk_positions[t] = data.oMf[ee_id].translation.copy() + fk_orientations[t] = data.oMf[ee_id].rotation.copy() + + return fk_positions, fk_orientations + + +def verify_ik_with_fk( + mjcf_path: str, + joint_configs: np.ndarray, + target_positions: np.ndarray, + target_orientations: np.ndarray | None = None, +) -> dict: + """Verify IK solution by running FK and comparing to targets.""" + import pinocchio as pin + + fk_pos, fk_rot = compute_fk_ee_poses(mjcf_path, joint_configs) + if fk_pos is None: + return None + + T = len(joint_configs) + pos_errors_mm = np.linalg.norm(fk_pos - target_positions, axis=1) * 1000 + + rot_errors_deg = None + if target_orientations is not None: + rot_errors_deg = np.zeros(T) + for t in range(T): + R_err = target_orientations[t].T @ fk_rot[t] + angle = np.linalg.norm(pin.log3(R_err)) + rot_errors_deg[t] = np.degrees(angle) + + summary = f"FK Verification ({T} frames): pos mean={pos_errors_mm.mean():.2f}mm max={pos_errors_mm.max():.2f}mm" + if rot_errors_deg is not None: + summary += f", rot mean={rot_errors_deg.mean():.1f}° max={rot_errors_deg.max():.1f}°" + + return { + "fk_positions": fk_pos, + "fk_orientations": fk_rot, + "pos_errors_mm": pos_errors_mm, + "rot_errors_deg": rot_errors_deg, + "summary": summary, + } + + +def compute_mujoco_geom_transforms( + mjcf_path: str, + joint_configs: np.ndarray, +) -> tuple[ + list[list[tuple[np.ndarray, np.ndarray]]], + list[tuple[np.ndarray, np.ndarray]] | None, + list[tuple[np.ndarray, np.ndarray]] | None, + dict[str, list[tuple[np.ndarray, np.ndarray]]] | None, +]: + """Compute MuJoCo geom/body/site transforms in the Pinocchio-aligned world. + + Also extracts camera site pose, EE body pose, and named body/site frames. + + Important: some MJCFs (notably MuJoCo Menagerie's Google Robot) include a + fixed ``worldbody -> root_body`` transform that Pinocchio's + ``buildModelFromMJCF()`` omits. Dataset poses and IK targets already live in + Pinocchio's root-free world, so we explicitly remove that MuJoCo root + transform here before returning any MuJoCo-derived poses. + + Returns: + (all_geom_transforms, camera_poses_or_None, ee_poses_or_None, robot_frames_or_None) + - all_geom_transforms: list of per-frame geom transforms [(pos, mat), ...] + - camera_poses: list of (pos, mat) per frame for 'camera_site', or None if no site. + - ee_poses: list of (pos, mat) per frame for the EE body, or None. + - robot_frames: dict mapping ``body:`` / ``site:`` to per-frame poses. + """ + import mujoco + + model = mujoco.MjModel.from_xml_path(mjcf_path) + data = mujoco.MjData(model) + + visual_geom_ids = get_visual_geom_ids(model) + + # Determine which robot config applies (by matching MJCF filename) + robot_name = resolve_robot_name_from_mjcf(mjcf_path) + cfg = get_robot_config(robot_name) if robot_name is not None else {} + + # Find camera_site if it exists + camera_site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "camera_site") + has_camera_site = camera_site_id >= 0 + if has_camera_site: + body_id = model.site_bodyid[camera_site_id] + body_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, body_id) or "?" + log.info(f"Found camera_site (id={camera_site_id}) on body '{body_name}'") + + # Also look up camera body (e.g. zed_mini for DROID) + camera_body_id = -1 + camera_body_name = cfg.get("camera_body") + if camera_body_name and not has_camera_site: + camera_body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, camera_body_name) + if camera_body_id >= 0: + log.info(f"Found camera body '{camera_body_name}' (id={camera_body_id})") + + all_transforms = [] + camera_poses = [] if (has_camera_site or camera_body_id >= 0) else None + robot_frame_specs = [] + for body_id in range(1, model.nbody): + body_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, body_id) or "" + if body_name: + robot_frame_specs.append(("body", body_name, body_id)) + for site_id in range(model.nsite): + site_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_SITE, site_id) or "" + if site_name: + robot_frame_specs.append(("site", site_name, site_id)) + robot_frames = {f"{kind}:{name}": [] for kind, name, _ in robot_frame_specs} if robot_frame_specs else None + + # Find EE body for extracting FK-derived EE pose + ee_body_id = -1 + ee_override = cfg.get("ee_frame") + ee_candidates = get_ee_frame_candidates(robot_name) + for candidate in ee_candidates: + bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, candidate) + if bid >= 0: + ee_body_id = bid + break + ee_poses = [] if ee_body_id >= 0 else None + + # Find driver joint indices for robots with finger_joint_names + finger_joint_names = cfg.get("finger_joint_names", []) + arm_jnames = cfg.get("arm_joints", []) if cfg else [] + # Indices to pin during constraint settling: arm joints + driver joints + n_arm = cfg.get("n_arm_joints", 7) if cfg else 7 + if arm_jnames: + # Use name-based mapping for arm joint indices + pin_indices = [] + for jn in arm_jnames: + jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, jn) + if jid >= 0: + pin_indices.append(model.jnt_qposadr[jid]) + else: + pin_indices = list(range(n_arm)) + if finger_joint_names: + for fjn in finger_joint_names: + for ji in range(model.njnt): + jname = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, ji) or "" + if jname == fjn: + qi = model.jnt_qposadr[ji] + pin_indices.append(qi) + + # Build mapping from IK output (pinocchio reduced model) to MuJoCo qpos indices. + # When a URDF is used with model reduction, the IK output has only arm+finger + # joints in pinocchio order. We need to map those to MuJoCo's qpos order. + arm_jnames = cfg.get("arm_joints", []) if cfg else [] + pin_to_mj_map = None # None = direct mapping (qpos[:len(q)] = q) + if arm_jnames: + # Build ordered list: arm joints first, then finger joints + ordered_jnames = list(arm_jnames) + list(finger_joint_names) + mj_indices = [] + for jn in ordered_jnames: + jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, jn) + if jid >= 0: + mj_indices.append(model.jnt_qposadr[jid]) + if mj_indices: + pin_to_mj_map = mj_indices + + # If joint_configs has more columns than n_arm, the extra column is a + # normalized gripper signal (raw UR: 0=open, 1=closed). Robotiq ctrl + # matches: 0=open, 255=closed → ctrl = raw * finger_max (no inversion). + finger_max = cfg.get("finger_max", 0.0) if cfg else 0.0 + has_gripper_ctrl = finger_max > 0.0 and model.nu > n_arm and joint_configs.shape[1] > n_arm + + def _apply_world_correction(pos: np.ndarray, mat: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Map one MuJoCo world pose into the root-free Pinocchio world.""" + corrected_pos = world_correction[:3, :3] @ pos + world_correction[:3, 3] + corrected_mat = world_correction[:3, :3] @ mat + return corrected_pos.astype(np.float32), corrected_mat.astype(np.float32) + + for q in joint_configs: + if pin_to_mj_map and len(q) == len(pin_to_mj_map): + # Full arm+finger mapping from IK (e.g. Franka): use all joints. + data.qpos[:] = 0 + for i, mi in enumerate(pin_to_mj_map): + data.qpos[mi] = q[i] + else: + # For robots with a separate gripper ctrl signal (e.g. UR5e), + # only write arm joints — the 7th column is a raw gripper value, + # not a qpos DOF. For other robots the pinocchio output already + # includes finger joints in the trailing columns; write them all. + n_set = n_arm if has_gripper_ctrl else len(q) + data.qpos[:n_set] = q[:n_set] + mujoco.mj_forward(model, data) + + # For robots with equality constraints (e.g., Robotiq 4-bar linkage), + # step physics to let constraints resolve the passive linkage joints. + if model.neq > 0: + data.qvel[:] = 0 + data.ctrl[:] = 0 + # Raw UR gripper maps directly to Robotiq ctrl: 0=open, 255=closed. + if has_gripper_ctrl: + data.ctrl[-1] = float(q[n_arm]) * finger_max + gripper_ctrl_val = float(data.ctrl[-1]) if model.nu > 0 else 0.0 + saved = data.qpos[pin_indices].copy() + for _ in range(200): + mujoco.mj_step(model, data) + data.qpos[pin_indices] = saved + if model.nu > 0: + data.ctrl[-1] = gripper_ctrl_val # keep gripper ctrl during settling + data.qvel[:] = 0 + mujoco.mj_forward(model, data) + world_correction = get_mujoco_to_pinocchio_world_transform(model, data, robot_name) + + frame_transforms = [] + for gi in visual_geom_ids: + pos = data.geom_xpos[gi].copy() + mat = data.geom_xmat[gi].reshape(3, 3).copy() + frame_transforms.append(_apply_world_correction(pos, mat)) + all_transforms.append(frame_transforms) + + # Extract camera site pose + if has_camera_site: + cam_pos = data.site_xpos[camera_site_id].copy() + cam_mat = data.site_xmat[camera_site_id].reshape(3, 3).copy() + camera_poses.append(_apply_world_correction(cam_pos, cam_mat)) + elif camera_body_id >= 0: + cam_pos = data.xpos[camera_body_id].copy() + cam_mat = data.xmat[camera_body_id].reshape(3, 3).copy() + camera_poses.append(_apply_world_correction(cam_pos, cam_mat)) + + # Extract EE body pose + if ee_body_id >= 0: + ee_pos = data.xpos[ee_body_id].copy() + ee_mat = data.xmat[ee_body_id].reshape(3, 3).copy() + ee_poses.append(_apply_world_correction(ee_pos, ee_mat)) + + if robot_frames is not None: + for kind, name, frame_id in robot_frame_specs: + if kind == "body": + pos = data.xpos[frame_id].copy() + mat = data.xmat[frame_id].reshape(3, 3).copy() + else: + pos = data.site_xpos[frame_id].copy() + mat = data.site_xmat[frame_id].reshape(3, 3).copy() + robot_frames[f"{kind}:{name}"].append(_apply_world_correction(pos, mat)) + + return all_transforms, camera_poses, ee_poses, robot_frames diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/robot_scene_model.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/robot_scene_model.py new file mode 100644 index 00000000..c53d09ee --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/robot_scene_model.py @@ -0,0 +1,213 @@ +"""Public robot-scene abstraction for the action viewer. + +`RobotSceneModel` is the viewer-facing contract for robot assets and +kinematics. It wraps the lower-level MuJoCo / Pinocchio helpers so callers do +not need to coordinate mesh loading, IK, frame extraction, or world-alignment +corrections themselves. +""" + +from __future__ import annotations + +import os +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from cosmos.data.vfm.action.urdf_visualizer import urdf_loader + +MeshSpec = tuple[str, object, np.ndarray] +FramePose = tuple[np.ndarray, np.ndarray] +FrameSeries = dict[str, list[FramePose]] + + +@dataclass(frozen=True) +class RobotTrajectory: + """Render-ready robot geometry for one solved trajectory.""" + + mesh_transforms: list[list[FramePose]] + named_frames: FrameSeries + + +def get_robot_config(robot_name: str) -> dict[str, Any]: + """Return the static config for one supported robot.""" + cfg = urdf_loader.ROBOT_CONFIGS.get(robot_name) + if cfg is None: + raise ValueError(f"Unknown robot: {robot_name}. Available: {list(urdf_loader.ROBOT_CONFIGS)}") + return cfg + + +def get_urdf_path(robot_name: str) -> str | None: + """Return the preferred URDF path for one robot, if available.""" + return urdf_loader.get_urdf_path(robot_name) + + +def get_mjcf_path(robot_name: str) -> str: + """Return the canonical MJCF path for one robot.""" + return urdf_loader.get_mjcf_path(robot_name) + + +def get_mujoco_to_pinocchio_world_transform(model: Any, data: Any, robot_name: str | None = None) -> np.ndarray: + """Map MuJoCo world poses into the root-free Pinocchio world.""" + return urdf_loader.get_mujoco_to_pinocchio_world_transform(model, data, robot_name) + + +def get_visual_geom_ids(model: Any) -> list[int]: + """Return the MuJoCo visual geom order used by the viewer.""" + return urdf_loader._get_visual_geom_ids(model) + + +def get_ee_frame_candidates(robot_name: str | None = None) -> list[str]: + """Return the ordered end-effector frame candidates for one robot.""" + if robot_name is None: + return list(urdf_loader._EE_FRAME_CANDIDATES) + cfg = urdf_loader.ROBOT_CONFIGS.get(robot_name, {}) + ee_frame = cfg.get("ee_frame") + if ee_frame is None: + return list(urdf_loader._EE_FRAME_CANDIDATES) + return [str(ee_frame), *urdf_loader._EE_FRAME_CANDIDATES] + + +def get_robot_loaders() -> dict[str, Callable[[], tuple[list[MeshSpec], np.ndarray]]]: + """Return the low-level robot mesh loader registry.""" + return urdf_loader.get_robot_loaders() + + +def resolve_robot_name_from_mjcf(mjcf_path: str) -> str | None: + """Infer a configured robot name from an MJCF filename.""" + filename = os.path.basename(mjcf_path) + for robot_name, cfg in urdf_loader.ROBOT_CONFIGS.items(): + if filename == cfg.get("mjcf"): + return robot_name + return None + + +def _copy_mesh_specs(meshes: list[MeshSpec]) -> list[MeshSpec]: + """Copy mesh transforms while reusing immutable mesh geometry.""" + return [(name, mesh, transform.copy().astype(np.float32)) for name, mesh, transform in meshes] + + +def _transform_mesh_specs(meshes: list[MeshSpec], base_pose: np.ndarray | None) -> list[MeshSpec]: + """Apply one rigid transform to a list of world-space mesh poses.""" + copied = _copy_mesh_specs(meshes) + if base_pose is None: + return copied + transformed: list[MeshSpec] = [] + for name, mesh, transform in copied: + transformed.append((name, mesh, (base_pose @ transform).astype(np.float32))) # [4,4] + return transformed + + +def _remove_pose_base(poses_world: np.ndarray, base_pose: np.ndarray | None) -> np.ndarray: + """Map world-space wrist poses back into one arm-local base frame.""" + if base_pose is None: + return poses_world.astype(np.float32) + base_inv = np.linalg.inv(base_pose).astype(np.float32) # [4,4] + return np.einsum("ij,njk->nik", base_inv, poses_world).astype(np.float32) # [T,4,4] + + +def _apply_base_to_mesh_transforms( + transforms: list[list[FramePose]], + base_pose: np.ndarray | None, +) -> list[list[FramePose]]: + """Apply one rigid base pose to per-geom world transforms.""" + if base_pose is None: + return transforms + base_rot = base_pose[:3, :3].astype(np.float32) # [3,3] + base_pos = base_pose[:3, 3].astype(np.float32) # [3] + transformed: list[list[FramePose]] = [] + for frame_transforms in transforms: + transformed_frame: list[FramePose] = [] + for pos, rot in frame_transforms: + transformed_frame.append((base_rot @ pos + base_pos, base_rot @ rot)) + transformed.append(transformed_frame) + return transformed + + +def _apply_base_to_named_frames( + frames: FrameSeries, + base_pose: np.ndarray | None, +) -> FrameSeries: + """Apply one rigid base pose to named body/site frames.""" + if base_pose is None: + return frames + base_rot = base_pose[:3, :3].astype(np.float32) # [3,3] + base_pos = base_pose[:3, 3].astype(np.float32) # [3] + transformed: FrameSeries = {} + for frame_key, poses in frames.items(): + transformed[frame_key] = [(base_rot @ pos + base_pos, base_rot @ rot) for pos, rot in poses] + return transformed + + +class RobotSceneModel: + """Single public abstraction for robot meshes, IK/FK, and debug frames.""" + + def __init__(self, robot_name: str) -> None: + self.robot_name = robot_name + self._config = get_robot_config(robot_name) + self._mjcf_path = get_mjcf_path(robot_name) + self._home_meshes: list[MeshSpec] | None = None + + @property + def mjcf_path(self) -> str: + """Return the underlying MJCF path for this robot.""" + return self._mjcf_path + + @property + def ee_frame_name(self) -> str: + """Return the canonical IK / debug frame name for this robot.""" + return str(self._config.get("ee_frame", "ee_frame")) + + def get_home_meshes(self, base_pose: np.ndarray | None = None) -> list[MeshSpec]: + """Return home-pose meshes in the requested world frame.""" + if self._home_meshes is None: + loaders = get_robot_loaders() + loader = loaders.get(self.robot_name) + if loader is None: + raise ValueError(f"No robot loader registered for {self.robot_name}") + meshes, _ = loader() + self._home_meshes = _copy_mesh_specs(meshes) + return _transform_mesh_specs(self._home_meshes, base_pose) + + def solve_visual_trajectory( + self, + wrist_poses_world: np.ndarray | None, + gripper_openings: np.ndarray | None = None, + to_opencv: np.ndarray | None = None, + base_pose: np.ndarray | None = None, + ) -> RobotTrajectory | None: + """Solve IK for world-space wrist poses and return render-ready robot state.""" + if wrist_poses_world is None or len(wrist_poses_world) < 2: + return None + + local_wrist_poses = _remove_pose_base(wrist_poses_world, base_pose) # [T,4,4] + target_positions = local_wrist_poses[:, :3, 3].astype(np.float32) # [T,3] + target_rotations = local_wrist_poses[:, :3, :3].astype(np.float32) # [T,3,3] + if to_opencv is not None and not np.allclose(to_opencv, np.eye(3, dtype=np.float32)): + target_rotations = target_rotations @ to_opencv.T[None] # [T,3,3] + + from cosmos.data.vfm.action.urdf_visualizer.ik_solver import ( + compute_fk_ee_poses, + compute_mujoco_geom_transforms, + solve_trajectory_ik, + ) + + joint_configs = solve_trajectory_ik( + self.mjcf_path, + target_positions, + gripper_openings=gripper_openings, + world_ee_orientations=target_rotations, + robot_name=self.robot_name, + ) + if joint_configs is None: + return None + + fk_pos, fk_rot = compute_fk_ee_poses(self.mjcf_path, joint_configs, robot_name=self.robot_name) + mesh_transforms, _, _, named_frames = compute_mujoco_geom_transforms(self.mjcf_path, joint_configs) + public_frames: FrameSeries = {} if named_frames is None else dict(named_frames) + public_frames[f"ik:{self.ee_frame_name}"] = list(zip(fk_pos, fk_rot, strict=True)) + + mesh_transforms = _apply_base_to_mesh_transforms(mesh_transforms, base_pose) + public_frames = _apply_base_to_named_frames(public_frames, base_pose) + return RobotTrajectory(mesh_transforms=mesh_transforms, named_frames=public_frames) diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_action.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_action.py new file mode 100644 index 00000000..025e1af7 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_action.py @@ -0,0 +1,532 @@ +"""Canonical 57D action representation with explicit input formats. + +57D layout:: + + [ego(9) | R_wrist(9) | R_fingers(15) | L_wrist(9) | L_fingers(15)] + +Each 9D SE(3) slot is ``[pos(3) + rot6d(6)]``. +Each finger slot is 3D (position in wrist-local frame), 5 fingers × 3D = 15D. + +Any supported action format is converted to ``UnifiedAction(action_57d, mask)`` +before the viewer processes it. The mask explicitly declares which slots are valid. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + +import numpy as np +import torch + +from cosmos.utils import log +from cosmos.data.vfm.action.embodiment_c_fk import build_viewer_batch +from cosmos.data.vfm.action.embodiment_c_spec import get_embodiment_c_embodiment_spec +from cosmos.data.vfm.action.pose_utils import convert_rotation + +FINGER_NAMES = ("thumb", "index", "middle", "ring", "pinky") +ALL_FINGERS = (True, True, True, True, True) +NO_FINGERS = (False, False, False, False, False) + + +class ActionFormat(str, Enum): + """Explicit raw action layouts supported by the viewer pipeline.""" + + EGO_9D = "9d" + SINGLE_ARM_10D = "10d" + DUAL_ARM_20D = "20d" + UNIFIED_57D = "57d" + + @property + def expected_dim(self) -> int: + """Return the exact trailing dimension required by this format.""" + return { + ActionFormat.EGO_9D: 9, + ActionFormat.SINGLE_ARM_10D: 10, + ActionFormat.DUAL_ARM_20D: 20, + ActionFormat.UNIFIED_57D: 57, + }[self] + + +# ─── Data Structures ───────────────────────────────────────────────────────── + + +@dataclass +class Action57DMask: + """Per-component validity for the 57D layout. + + ``right_fingers`` / ``left_fingers`` are tuples of 5 bools + (thumb, index, middle, ring, pinky) — supports any combination from + 2-finger grippers to full 5-finger hands. + """ + + ego: bool = False + right_wrist: bool = False + right_fingers: tuple[bool, ...] = NO_FINGERS + left_wrist: bool = False + left_fingers: tuple[bool, ...] = NO_FINGERS + + +@dataclass +class UnifiedAction: + """Canonical 57D action for the viewer pipeline. + + ``action`` is always shape ``(T, 57)`` with invalid slots zero-padded. + ``gripper_right`` / ``gripper_left`` carry auxiliary scalar gripper data + for embodiments that don't map to finger positions (V-shape visualisation). + """ + + action: np.ndarray # (T, 57) + mask: Action57DMask + gripper_right: np.ndarray | None = None # (T,) scalar 0-1 + gripper_left: np.ndarray | None = None # (T,) scalar 0-1 + + +@dataclass +class SceneState: + """Render-ready world-space geometry reconstructed from ``UnifiedAction``. + + Contract: + - all SE(3) trajectories live in one shared ``scene_world`` frame + - fingertip positions are world-space if present + - gripper signals are scalar open/close values sampled at ``T+1`` frames + """ + + mask: Action57DMask = field(default_factory=Action57DMask) + # Absolute SE(3) trajectories — (T+1, 4, 4) + ego_poses: np.ndarray | None = None + right_poses: np.ndarray | None = None + left_poses: np.ndarray | None = None + # World-space fingertip positions — (T+1, 5, 3) + right_fingers: np.ndarray | None = None + left_fingers: np.ndarray | None = None + # Scalar gripper — (T+1,) + gripper_right: np.ndarray | None = None + gripper_left: np.ndarray | None = None + # Metadata + video: np.ndarray | None = None # (T+1, H, W, 3) uint8 + action_raw: np.ndarray | None = None # canonical 57D action tensor for display + T: int = 0 + # FK mesh animation: raw (T, nq) joint configs populated by datasets that + # perform EE conversion internally (e.g. robomind-ur). When set, the renderer + # uses these for FK mesh animation instead of running IK on right_poses. + joint_configs: np.ndarray | None = None + # AgiBot source-state FK mesh animation. When set, the renderer uses these + # native URDF link poses directly instead of re-solving IK from pose targets. + agibot_link_poses: dict[str, np.ndarray] | None = None + + +# ─── Converters ─────────────────────────────────────────────────────────────── + + +def to_unified_from_57d(action: np.ndarray) -> UnifiedAction: + """57D hand_pose → passthrough, all 5 slots valid.""" + return UnifiedAction( + action=action.astype(np.float32), + mask=Action57DMask( + ego=True, + right_wrist=True, + right_fingers=ALL_FINGERS, + left_wrist=True, + left_fingers=ALL_FINGERS, + ), + ) + + +def to_unified_from_10d(action: np.ndarray) -> UnifiedAction: + """10D single arm ``[pos(3)+rot6d(6)+grip(1)]`` → right wrist + gripper.""" + T = action.shape[0] + a = np.zeros((T, 57), dtype=np.float32) # [T,57] + a[:, 9:18] = action[:, :9] + return UnifiedAction( + action=a, + mask=Action57DMask(right_wrist=True), + gripper_right=action[:, 9].astype(np.float32), + ) + + +def to_unified_from_20d(action: np.ndarray) -> UnifiedAction: + """20D dual arm ``[left(10) | right(10)]`` → both wrists + both grippers. + + Data layout: ``[L_pos(3) + L_rot6d(6) + L_grip(1) | R_pos(3) + R_rot6d(6) + R_grip(1)]``. + Maps left arm → left wrist slot [33:42], right arm → right wrist slot [9:18]. + """ + T = action.shape[0] + a = np.zeros((T, 57), dtype=np.float32) # [T,57] + a[:, 33:42] = action[:, :9] # left arm → left wrist slot [33:42] + a[:, 9:18] = action[:, 10:19] # right arm → right wrist slot [9:18] + return UnifiedAction( + action=a, + mask=Action57DMask(right_wrist=True, left_wrist=True), + gripper_right=action[:, 19].astype(np.float32), # right arm gripper + gripper_left=action[:, 9].astype(np.float32), # left arm gripper + ) + + +def to_unified_from_9d(action: np.ndarray) -> UnifiedAction: + """9D camera/AV ``[pos(3)+rot6d(6)]`` → ego only.""" + T = action.shape[0] + a = np.zeros((T, 57), dtype=np.float32) # [T,57] + a[:, 0:9] = action[:, :9] + return UnifiedAction( + action=a, + mask=Action57DMask(ego=True), + ) + + +def _validate_action_shape(action: np.ndarray, action_format: ActionFormat) -> None: + """Raise when a raw action tensor does not match its declared format.""" + if action.ndim != 2: + raise ValueError(f"Expected a rank-2 action array, got shape {action.shape}") + actual_dim = int(action.shape[-1]) + expected_dim = action_format.expected_dim + if actual_dim != expected_dim: + raise ValueError(f"Action format {action_format.value} expects trailing dim {expected_dim}, got {actual_dim}") + + +def to_unified(action: np.ndarray, action_format: ActionFormat) -> UnifiedAction: + """Convert one explicit raw action format into ``UnifiedAction``.""" + _validate_action_shape(action, action_format) + if action_format is ActionFormat.UNIFIED_57D: + return to_unified_from_57d(action) + if action_format is ActionFormat.DUAL_ARM_20D: + return to_unified_from_20d(action) + if action_format is ActionFormat.EGO_9D: + return to_unified_from_9d(action) + if action_format is ActionFormat.SINGLE_ARM_10D: + return to_unified_from_10d(action) + raise ValueError(f"Unsupported action format: {action_format}") + + +def _poses_to_pose9d(poses: np.ndarray) -> np.ndarray: + """Convert ``(T,4,4)`` absolute transforms to ``(T,9)`` pos+rot6d rows.""" + + positions = poses[:, :3, 3].astype(np.float32) + rotations = convert_rotation(poses[:, :3, :3], input_format="matrix", output_format="rot6d") + rotations = np.asarray(rotations, dtype=np.float32) + return np.concatenate([positions, rotations], axis=-1).astype(np.float32, copy=False) + + +def to_unified_from_embodiment_c( + sample: dict[str, Any], + *, + embodiment_type: str, +) -> UnifiedAction: + """Convert one AgiBot sample to the canonical viewer representation.""" + + viewer_batch = build_viewer_batch(sample, embodiment_type) + embodiment_spec = get_embodiment_c_embodiment_spec(embodiment_type) + T = int(viewer_batch.head_camera_poses.shape[0]) + + action = np.zeros((T, 57), dtype=np.float32) + action[:, 0:9] = _poses_to_pose9d(viewer_batch.head_camera_poses) + action[:, 9:18] = _poses_to_pose9d(viewer_batch.right_wrist_poses) + action[:, 33:42] = _poses_to_pose9d(viewer_batch.left_wrist_poses) + + return UnifiedAction( + action=action, + mask=Action57DMask( + ego=True, + right_wrist=True, + left_wrist=True, + ), + gripper_right=viewer_batch.right_gripper if embodiment_spec.kind == "gripper" else None, + gripper_left=viewer_batch.left_gripper if embodiment_spec.kind == "gripper" else None, + ) + + +def to_unified_from_embodiment_c_fk_action(action: np.ndarray, kind: str = "gripper") -> UnifiedAction: + """Embodiment C FK-pose action → unified action. + + Relative input layout (gripper, 29D): + ``[head_delta(9), right_delta(9), right_gripper(1), left_delta(9), left_gripper(1)]`` + + These inputs already store ``rot6d`` SE(3) blocks and are copied directly + into the unified slots. + """ + T = action.shape[0] + a = np.zeros((T, 57), dtype=np.float32) + + if kind == "gripper" and action.shape[1] == 29: + a[:, 0:9] = action[:, 0:9] + a[:, 9:18] = action[:, 9:18] + a[:, 33:42] = action[:, 19:28] + return UnifiedAction( + action=a, + mask=Action57DMask(ego=True, right_wrist=True, left_wrist=True), + gripper_right=action[:, 18].astype(np.float32), + gripper_left=action[:, 28].astype(np.float32), + ) + + if kind == "gripper": + raise ValueError(f"Unsupported AgiBot gripper action dim {action.shape[1]}; expected 29.") + raise ValueError(f"Unsupported AgiBot kind {kind!r}; expected 'gripper'.") + + +# ─── Trajectory Reconstruction ──────────────────────────────────────────────── + + +def _pos_rot6d_to_mat(se3: np.ndarray) -> np.ndarray: + """Convert ``(N, 9)`` pos+rot6d to ``(N, 4, 4)`` SE(3) matrices.""" + N = se3.shape[0] + pos = se3[:, :3] + r6 = se3[:, 3:9] + + col0 = r6[:, :3].copy() + col0_norm = np.linalg.norm(col0, axis=-1, keepdims=True) + 1e-8 + col0 = col0 / col0_norm + + col1 = r6[:, 3:6] - np.sum(r6[:, 3:6] * col0, axis=-1, keepdims=True) * col0 + col1_norm = np.linalg.norm(col1, axis=-1, keepdims=True) + 1e-8 + col1 = col1 / col1_norm + + col2 = np.cross(col0, col1) + + mats = np.tile(np.eye(4, dtype=np.float32), (N, 1, 1)) + mats[:, :3, 0] = col0 + mats[:, :3, 1] = col1 + mats[:, :3, 2] = col2 + mats[:, :3, 3] = pos + return mats + + +def _chain_se3( + deltas: np.ndarray, + initial_pose: np.ndarray | None = None, + pose_convention: str = "backward_framewise", +) -> np.ndarray: + """Chain ``(T, 9)`` relative deltas into ``(T+1, 4, 4)`` absolute poses. + + For ``backward_framewise``: ``P_{t+1} = P_t @ delta_t``. + For ``absolute``: each row is already an absolute pose (no chaining). + """ + T = deltas.shape[0] + delta_mats = _pos_rot6d_to_mat(deltas) + + if initial_pose is None: + initial_pose = np.eye(4, dtype=np.float32) + else: + initial_pose = initial_pose.astype(np.float32) + + poses = np.empty((T + 1, 4, 4), dtype=np.float32) + poses[0] = initial_pose + + if pose_convention == "absolute": + poses[1:] = delta_mats + else: + for t in range(T): + poses[t + 1] = poses[t] @ delta_mats[t] + + return poses + + +def _extract_fingers(raw: np.ndarray) -> np.ndarray: + """``(T, 15)`` → ``(T+1, 5, 3)`` with first frame duplicated.""" + T = raw.shape[0] + fingers = raw.reshape(T, 5, 3).astype(np.float32) # [T,5,3] + return np.concatenate([fingers[:1], fingers], axis=0) + + +def _to_numpy_float32(value: object) -> np.ndarray: + """Convert a tensor-like value to a float32 NumPy array.""" + + if isinstance(value, torch.Tensor): + return value.detach().cpu().numpy().astype(np.float32) + return np.asarray(value, dtype=np.float32) + + +def _quat_xyzw_to_rotmat(q: np.ndarray) -> np.ndarray: + """Convert ``(N, 4)`` xyzw quaternions to ``(N, 3, 3)`` rotation matrices.""" + x, y, z, w = q[:, 0], q[:, 1], q[:, 2], q[:, 3] + R = np.zeros((len(q), 3, 3), dtype=np.float32) + R[:, 0, 0] = 1 - 2 * (y * y + z * z) + R[:, 0, 1] = 2 * (x * y - z * w) + R[:, 0, 2] = 2 * (x * z + y * w) + R[:, 1, 0] = 2 * (x * y + z * w) + R[:, 1, 1] = 1 - 2 * (x * x + z * z) + R[:, 1, 2] = 2 * (y * z - x * w) + R[:, 2, 0] = 2 * (x * z - y * w) + R[:, 2, 1] = 2 * (y * z + x * w) + R[:, 2, 2] = 1 - 2 * (x * x + y * y) + return R + + +def _build_absolute_from_overlay(sample: dict) -> dict[str, np.ndarray] | None: + """Build absolute world-frame poses from HandPoseDataset overlay data. + + Returns None if overlay keys are missing. + """ + raw_cam_pos = sample.get("raw_cam_position") + if raw_cam_pos is None: + return None + + cam_pos = _to_numpy_float32(raw_cam_pos) # [T+1,3] + cam_rot_q = _to_numpy_float32(sample["raw_cam_rotation"]) # [T+1,4] + right_3d = _to_numpy_float32(sample["raw_cam_right_3d"]) # [T+1,63] + left_3d = _to_numpy_float32(sample["raw_cam_left_3d"]) # [T+1,63] + right_rot = _to_numpy_float32(sample["raw_cam_right_rot"]) # [T+1,84] + left_rot = _to_numpy_float32(sample["raw_cam_left_rot"]) # [T+1,84] + + T1 = cam_pos.shape[0] + FTIP = [4, 8, 12, 16, 20] + + # Camera c2w (world frame) + cam_c2w = np.tile(np.eye(4, dtype=np.float32), (T1, 1, 1)) # [T+1,4,4] + cam_c2w[:, :3, 3] = cam_pos + cam_c2w[:, :3, :3] = _quat_xyzw_to_rotmat(cam_rot_q) + + def _wrist_world(pos_63, rot_84): + wrist_pos = pos_63[:, :3] + wrist_q = rot_84.reshape(T1, 21, 4)[:, 0] + wrist_cam = np.tile(np.eye(4, dtype=np.float32), (T1, 1, 1)) # [T+1,4,4] + wrist_cam[:, :3, 3] = wrist_pos + wrist_cam[:, :3, :3] = _quat_xyzw_to_rotmat(wrist_q) + return cam_c2w @ wrist_cam + + def _fingers_world(pos_63): + joints = pos_63.reshape(T1, 21, 3)[:, FTIP] + R = cam_c2w[:, :3, :3] + t = cam_c2w[:, :3, 3] + return np.einsum("tij,tfj->tfi", R, joints) + t[:, None, :] # [T+1,5,3] + + return { + "ego_poses": cam_c2w, + "right_wrist_poses": _wrist_world(right_3d, right_rot), + "left_wrist_poses": _wrist_world(left_3d, left_rot), + "right_fingers": _fingers_world(right_3d), + "left_fingers": _fingers_world(left_3d), + } + + +# ─── Scene State Builder ───────────────────────────────────────────────────── + + +def build_scene_state( + unified: UnifiedAction, + initial_pose: np.ndarray | None = None, + initial_pose_right: np.ndarray | None = None, + initial_pose_left: np.ndarray | None = None, + right_base_pose: np.ndarray | None = None, + left_base_pose: np.ndarray | None = None, + pose_convention: str = "backward_framewise", + sample: dict | None = None, +) -> SceneState: + """Reconstruct a canonical world-space ``SceneState`` from ``UnifiedAction``. + + Chains SE(3) deltas for valid mask slots. If ``sample`` contains overlay + data (HandPoseDataset raw camera/joint fields), overrides with absolute + world-frame poses. + + Args: + unified: Canonical 57D action with mask. + initial_pose: Default initial pose for all slots. + initial_pose_right: Override for right wrist (dual arm). + initial_pose_left: Override for left wrist (dual arm). + right_base_pose: Right-arm base pose that maps arm-local trajectories into ``scene_world``. + left_base_pose: Left-arm base pose that maps arm-local trajectories into ``scene_world``. + pose_convention: Pose convention for SE(3) chaining. + sample: Raw dataset sample (for overlay data). + """ + + def _apply_pose_base(poses: np.ndarray | None, base_pose: np.ndarray | None) -> np.ndarray | None: + if poses is None or base_pose is None: + return poses + return np.einsum("ij,njk->nik", base_pose, poses).astype(np.float32) # [T+1,4,4] + + def _fingers_local_to_world( + fingers_local: np.ndarray | None, + wrist_poses_world: np.ndarray | None, + ) -> np.ndarray | None: + if fingers_local is None: + return None + if wrist_poses_world is None: + raise ValueError("Finger trajectories require matching wrist poses to build world-space SceneState") + wrist_rot = wrist_poses_world[:, :3, :3].astype(np.float32) # [T+1,3,3] + wrist_pos = wrist_poses_world[:, :3, 3].astype(np.float32) # [T+1,3] + return np.einsum("tij,tfj->tfi", wrist_rot, fingers_local) + wrist_pos[:, None, :] # [T+1,5,3] + + mask = unified.mask + action = unified.action + state = SceneState(mask=mask) + + ip_default = initial_pose if initial_pose is not None else np.eye(4, dtype=np.float32) + ip_right = initial_pose_right if initial_pose_right is not None else ip_default + ip_left = initial_pose_left if initial_pose_left is not None else ip_default + + if mask.ego: + state.ego_poses = _chain_se3(action[:, 0:9], ip_default, pose_convention) + if mask.right_wrist: + state.right_poses = _chain_se3(action[:, 9:18], ip_right, pose_convention) + if any(mask.right_fingers): + state.right_fingers = _extract_fingers(action[:, 18:33]) + if mask.left_wrist: + state.left_poses = _chain_se3(action[:, 33:42], ip_left, pose_convention) + if any(mask.left_fingers): + state.left_fingers = _extract_fingers(action[:, 42:57]) + + if unified.gripper_right is not None: + g = unified.gripper_right + state.gripper_right = np.concatenate([[g[0]], g]).astype(np.float32, copy=False) # [T+1] + if unified.gripper_left is not None: + g = unified.gripper_left + state.gripper_left = np.concatenate([[g[0]], g]).astype(np.float32, copy=False) # [T+1] + + abs_data = _build_absolute_from_overlay(sample) if sample is not None else None + if abs_data is not None: + state.ego_poses = abs_data["ego_poses"] + state.right_poses = abs_data["right_wrist_poses"] + state.left_poses = abs_data["left_wrist_poses"] + state.right_fingers = abs_data["right_fingers"] + state.left_fingers = abs_data["left_fingers"] + log.info( + f"Overlay absolute mode | ego range: " + f"[{abs_data['ego_poses'][:, :3, 3].min():.3f}, " + f"{abs_data['ego_poses'][:, :3, 3].max():.3f}] | " + f"R wrist[0]: {abs_data['right_wrist_poses'][0, :3, 3]}" + ) + else: + state.right_poses = _apply_pose_base(state.right_poses, right_base_pose) + state.left_poses = _apply_pose_base(state.left_poses, left_base_pose) + state.right_fingers = _fingers_local_to_world(state.right_fingers, state.right_poses) + state.left_fingers = _fingers_local_to_world(state.left_fingers, state.left_poses) + + raw_agibot_link_poses = sample.get("agibot_link_poses") if sample is not None else None + if isinstance(raw_agibot_link_poses, dict): + state.agibot_link_poses = { + str(link_name): _to_numpy_float32(link_poses) for link_name, link_poses in raw_agibot_link_poses.items() + } # {name:[T+1,4,4]} + + state.action_raw = unified.action.astype(np.float32) + state.T = action.shape[0] + return state + + +# ─── Video Extraction ───────────────────────────────────────────────────────── + + +def get_video_from_sample(sample: dict) -> np.ndarray | None: + """Extract video frames from a dataset sample. + + Returns ``(T+1, H, W, 3)`` uint8 array, or None. + """ + video = sample.get("video") + if video is None: + return None + if isinstance(video, torch.Tensor): + video = video.numpy() + + if video.ndim == 4: + C, T_dim, H, W = video.shape + if C in (1, 3) and T_dim > 3: + video = np.transpose(video, (1, 2, 3, 0)) + elif video.shape[1] in (1, 3) and T_dim <= 3: + video = np.transpose(video, (0, 2, 3, 1)) + + if video.dtype in (np.float32, np.float64): + video = np.clip(video * 255, 0, 255).astype(np.uint8) + + if video.ndim == 4 and video.shape[-1] == 1: + video = np.repeat(video, 3, axis=-1) + + return video diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_renderer.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_renderer.py new file mode 100644 index 00000000..1dd943eb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/unified_renderer.py @@ -0,0 +1,1004 @@ +"""Mask-driven renderer for ``SceneState`` — the unified 57D viewer backend. + +Owns all viser scene handles. Draws only what the mask declares valid. +No action-format branching — everything goes through ``SceneState``. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np + +from cosmos.utils import log +from cosmos.data.vfm.action.embodiment_c_fk import DEFAULT_GRIPPER_LENGTH_M +from cosmos.data.vfm.action.embodiment_c_spec import get_embodiment_c_kind +from cosmos.data.vfm.action.urdf_visualizer.ik_solver import ( + compute_agibot_link_poses_batch_from_configs, + solve_agibot_trajectory_ik, +) +from cosmos.data.vfm.action.urdf_visualizer.robot_scene_model import RobotSceneModel +from cosmos.data.vfm.action.urdf_visualizer.unified_action import FINGER_NAMES, SceneState +from cosmos.data.vfm.action.urdf_visualizer.urdf_loader import ( + get_embodiment_c_collision_geometries, +) + + +def _agibot_gripper_tip_positions( + pos: np.ndarray, + rot: np.ndarray, + opening: float, + max_finger_width: float, +) -> tuple[np.ndarray, np.ndarray]: + """Approximate AgiBot gripper tip sphere positions from an OpenCV wrist pose.""" + + half_w = float(opening) * max_finger_width / 2.0 + gripper_center = pos + DEFAULT_GRIPPER_LENGTH_M * rot[:, 2] # [3] + tip_l = gripper_center + rot @ np.array([half_w, 0.0, 0.0], dtype=np.float32) # [3] + tip_r = gripper_center + rot @ np.array([-half_w, 0.0, 0.0], dtype=np.float32) # [3] + return tip_l.astype(np.float32, copy=False), tip_r.astype(np.float32, copy=False) + + +def _get_entry_agibot_embodiment_type(entry: Any) -> str: + """Return the AgiBot robot embodiment declared by a viewer dataset entry.""" + + robot_embodiment_type = getattr(entry, "robot_embodiment_type", None) + if isinstance(robot_embodiment_type, str) and robot_embodiment_type: + return robot_embodiment_type + + dataset_kwargs = getattr(entry, "dataset_kwargs", {}) + if isinstance(dataset_kwargs, dict): + embodiment_type = dataset_kwargs.get("embodiment_type", "") + if isinstance(embodiment_type, str): + return embodiment_type + + return "" + + +def _get_wrist_to_opencv(to_opencv: object, wrist_name: str) -> np.ndarray | None: + """Resolve a native-to-OpenCV wrist rotation from renderer dataset metadata.""" + + if isinstance(to_opencv, dict): + rotation = to_opencv.get(wrist_name) + if isinstance(rotation, np.ndarray): + return rotation.astype(np.float32, copy=False) # [3,3] + return None + if isinstance(to_opencv, np.ndarray): + return to_opencv.astype(np.float32, copy=False) # [3,3] + return None + + +def _undo_wrist_to_opencv_for_ik( + poses: np.ndarray | None, + to_opencv: object, + wrist_name: str, +) -> np.ndarray | None: + """Convert viewer/OpenCV wrist rotations back to native frame for IK.""" + + if poses is None: + return None + wrist_to_opencv = _get_wrist_to_opencv(to_opencv, wrist_name) + if wrist_to_opencv is None or np.allclose(wrist_to_opencv, np.eye(3, dtype=np.float32)): + return poses + native_poses = poses.astype(np.float32, copy=True) # [T,4,4] + native_poses[:, :3, :3] = native_poses[:, :3, :3] @ wrist_to_opencv.T[None] # [T,3,3] + return native_poses + + +class UnifiedRenderer: + """Mask-driven 3D renderer for the unified 57D action viewer.""" + + # ── Palette ── + TIP_RADIUS = 0.0042 + FRUSTUM_SCALE = 0.10 + EGO_AXIS_LENGTH = 0.04 + EGO_AXIS_RADIUS = 0.002 + EGO_TRAJ_LENGTH_REFERENCE = 0.55 + EGO_TRAJ_SCALE_MIN = 0.35 + EGO_TRAJ_SCALE_MAX = 1.80 + EGO_TRAJ_LINE_WIDTH = 1.0 + EE_TRAJ_LINE_WIDTH = 1.5 + HAND_AXIS_LENGTH = 0.05 + HAND_AXIS_RADIUS = 0.002 + ROBOT_BODY_AXIS_LENGTH = 0.03 + ROBOT_BODY_AXIS_RADIUS = 0.0012 + ROBOT_SITE_AXIS_LENGTH = 0.022 + ROBOT_SITE_AXIS_RADIUS = 0.0009 + COLOR_EGO = (52, 152, 219) # blue + COLOR_EGO_TOP = (231, 76, 60) # red + COLOR_RIGHT = (243, 156, 18) # orange + COLOR_LEFT = (155, 89, 182) # purple + FINGER_COLORS = [ + (231, 76, 60), + (241, 196, 15), + (46, 204, 113), + (52, 152, 219), + (155, 89, 182), + ] + + @staticmethod + def _soften_color(color: tuple[int, int, int], mix: float = 0.35) -> tuple[int, int, int]: + """Blend a color toward white for less visually dominant trajectories.""" + base = np.asarray(color, dtype=np.float32) + soft = base * (1.0 - mix) + 255.0 * mix + rounded = soft.round() + return int(rounded[0]), int(rounded[1]), int(rounded[2]) + + def __init__(self, server): + import viser.transforms as vtf + + self.server = server + self.vtf = vtf + self.state: SceneState | None = None + self._entry: Any = None + self._axis_scale = 1.0 + self._ego_frustum_scale_base = self.FRUSTUM_SCALE + self._ego_axis_length_base = self.EGO_AXIS_LENGTH + self._ego_axis_radius_base = self.EGO_AXIS_RADIUS + self._ego_frustum_fov = np.deg2rad(60.0) + self._ego_frustum_aspect = 4 / 3 + + # ── Ego (blue) ── + self.ego_frame = server.scene.add_frame( + "/ego/frame", + axes_length=self.EGO_AXIS_LENGTH, + axes_radius=self.EGO_AXIS_RADIUS, + ) + self.ego_frustum = server.scene.add_line_segments( + "/ego/frustum", + points=self._make_ego_frustum_wireframe_points(self._ego_frustum_fov, self._ego_frustum_aspect), + colors=np.array(self.COLOR_EGO, dtype=np.uint8), + scale=self.FRUSTUM_SCALE, + line_width=2.0, + wxyz=(1.0, 0.0, 0.0, 0.0), + position=(0.0, 0.0, 0.0), + ) + self.ego_frustum_up = server.scene.add_line_segments( + "/ego/frustum_up", + points=self._make_ego_frustum_up_points(self._ego_frustum_fov, self._ego_frustum_aspect), + colors=np.array(self.COLOR_EGO_TOP, dtype=np.uint8), + line_width=3.0, + scale=self.FRUSTUM_SCALE, + wxyz=(1.0, 0.0, 0.0, 0.0), + position=(0.0, 0.0, 0.0), + ) + self.ego_traj = server.scene.add_spline_catmull_rom( + "/ego/traj", + positions=np.zeros([2, 3], dtype=np.float32), + color=self._soften_color(self.COLOR_EGO), + line_width=self.EGO_TRAJ_LINE_WIDTH, + ) + + # ── Right effector (green) ── + self.right_frame = server.scene.add_frame( + "/right/frame", + axes_length=self.HAND_AXIS_LENGTH, + axes_radius=self.HAND_AXIS_RADIUS, + ) + self.right_traj = server.scene.add_spline_catmull_rom( + "/right/traj", + positions=np.zeros([2, 3], dtype=np.float32), + color=self._soften_color(self.COLOR_RIGHT), + line_width=self.EE_TRAJ_LINE_WIDTH, + ) + self.right_ee = server.scene.add_point_cloud( + "/right/point", + points=np.zeros([1, 3], dtype=np.float32), + colors=np.array([self.COLOR_RIGHT], dtype=np.uint8), + point_size=0.015, + point_shape="circle", + ) + self.right_fingers = [ + server.scene.add_icosphere( + f"/right/finger_{FINGER_NAMES[i]}", + radius=self.TIP_RADIUS, + color=self.FINGER_COLORS[i], + position=(0.0, 0.0, 0.0), + ) + for i in range(5) + ] + self.right_gripper_tips = [ + server.scene.add_icosphere( + f"/right/gripper_tip_{side}", + radius=self.TIP_RADIUS, + color=self.FINGER_COLORS[i], + position=(0.0, 0.0, 0.0), + ) + for i, side in enumerate(("thumb", "index")) + ] + + # ── Left effector (red) ── + self.left_frame = server.scene.add_frame( + "/left/frame", + axes_length=self.HAND_AXIS_LENGTH, + axes_radius=self.HAND_AXIS_RADIUS, + ) + self.left_traj = server.scene.add_spline_catmull_rom( + "/left/traj", + positions=np.zeros([2, 3], dtype=np.float32), + color=self._soften_color(self.COLOR_LEFT), + line_width=self.EE_TRAJ_LINE_WIDTH, + ) + self.left_ee = server.scene.add_point_cloud( + "/left/point", + points=np.zeros([1, 3], dtype=np.float32), + colors=np.array([self.COLOR_LEFT], dtype=np.uint8), + point_size=0.015, + point_shape="circle", + ) + self.left_fingers = [ + server.scene.add_icosphere( + f"/left/finger_{FINGER_NAMES[i]}", + radius=self.TIP_RADIUS, + color=self.FINGER_COLORS[i], + position=(0.0, 0.0, 0.0), + ) + for i in range(5) + ] + self.left_gripper_tips = [ + server.scene.add_icosphere( + f"/left/gripper_tip_{side}", + radius=self.TIP_RADIUS, + color=self.FINGER_COLORS[i], + position=(0.0, 0.0, 0.0), + ) + for i, side in enumerate(("thumb", "index")) + ] + + # ── IK robot meshes ── + self.robot_right: list = [] + self.robot_left: list = [] + self._robot_frame_handles_right: dict[str, Any] = {} + self._robot_frame_handles_left: dict[str, Any] = {} + self._current_robot: Any | None = None + self._robot_scene_model: RobotSceneModel | None = None + self._ik_right: list | None = None + self._ik_left: list | None = None + self._robot_frames_right: dict[str, list[tuple[np.ndarray, np.ndarray]]] | None = None + self._robot_frames_left: dict[str, list[tuple[np.ndarray, np.ndarray]]] | None = None + self._robot_link_names: list[str] = [] + self._robot_local_transforms: list[np.ndarray] = [] + + # ── Video panel (set by viewer.py) ── + self._cam_handle: Any | None = None + + self.hide_all() + + # ─── Per-Episode ────────────────────────────────────────────────────────── + + def load( + self, + state: SceneState, + entry: Any, + to_opencv: np.ndarray | dict[str, np.ndarray] | None = None, + ): + """Load a new episode. Rebuild trajectories, robot meshes, and IK. + + Args: + state: Reconstructed ``SceneState`` with absolute poses. + entry: ``DatasetEntry`` with robot_name, max_finger_width, etc. + to_opencv: Optional native-to-OpenCV rotation. AgiBot can provide + side-specific ``{"left_wrist": R, "right_wrist": R}`` values. + """ + self.state = state + self._entry = entry + self._to_opencv = to_opencv if to_opencv is not None else np.eye(3, dtype=np.float32) + self._ego_frustum_fov = np.deg2rad(entry.camera_fov_deg) + self._ego_frustum_aspect = float(entry.camera_aspect) + self.ego_frustum.points = self._make_ego_frustum_wireframe_points( + self._ego_frustum_fov, + self._ego_frustum_aspect, + ) + self.ego_frustum_up.points = self._make_ego_frustum_up_points( + self._ego_frustum_fov, + self._ego_frustum_aspect, + ) + self._update_ego_visual_scale(state.ego_poses if state.mask.ego else None) + self.update_axis_scale(self._axis_scale) + self.hide_all() + + # ── Robot meshes + IK from canonical world-space SceneState ── + self._ik_right = None + self._ik_left = None + self._robot_frames_right = None + self._robot_frames_left = None + if entry.robot_name: + self._load_robot_and_ik(state, entry) + + # Rebuild trajectory splines from canonical world-space SceneState. + if state.mask.ego and state.ego_poses is not None: + self._rebuild_traj(self.ego_traj, state.ego_poses, self.COLOR_EGO) + if state.mask.right_wrist and state.right_poses is not None: + self._rebuild_traj(self.right_traj, state.right_poses, self.COLOR_RIGHT) + if state.mask.left_wrist and state.left_poses is not None: + self._rebuild_traj(self.left_traj, state.left_poses, self.COLOR_LEFT) + + def set_video_panel(self, panel_handle: Any | None) -> None: + """Attach the optional GUI image panel used for episode video.""" + self._cam_handle = panel_handle + + # ─── Per-Frame ──────────────────────────────────────────────────────────── + + def update(self, t: int, show: dict): + """Update all scene elements for time step ``t``. + + Args: + t: Frame index (0-based). + show: Visibility flags: ``frames``, ``traj``, ``fingertips``, ``ego``, ``robot``. + """ + state = self.state + if state is None: + return + mask = state.mask + + # ── Ego ── + self._update_ego(t, state.ego_poses, mask.ego and show.get("ego", False), show) + + # ── Right effector ── + self._update_effector( + t, + state.right_poses, + mask.right_wrist, + self.right_frame, + self.right_ee, + self.right_traj, + show, + ) + self._update_fingers( + t, + state.right_fingers, + mask.right_fingers, + self.right_fingers, + show, + ) + self._update_gripper( + t, + state.right_poses, + state.gripper_right, + mask.right_wrist, + mask.right_fingers, + self.right_gripper_tips, + show, + ) + + # ── Left effector ── + self._update_effector( + t, + state.left_poses, + mask.left_wrist, + self.left_frame, + self.left_ee, + self.left_traj, + show, + ) + self._update_fingers( + t, + state.left_fingers, + mask.left_fingers, + self.left_fingers, + show, + ) + self._update_gripper( + t, + state.left_poses, + state.gripper_left, + mask.left_wrist, + mask.left_fingers, + self.left_gripper_tips, + show, + ) + + # ── IK robot meshes ── + self._update_robot(t, show) + + # ── Video panel ── + if self._cam_handle is not None and state.video is not None and t < len(state.video): + self._cam_handle.image = state.video[t] + + # ─── Action Text ────────────────────────────────────────────────────────── + + def format_action_text(self, t: int) -> str: + """Return a formatted string showing 57D action values at step ``t``. + + Always shows the full 57D layout. Validity indicator (✓/·) in front of + each component based on the mask. + """ + state = self.state + if state is None or state.action_raw is None: + return "" + if t == 0: + return "*t=0: anchor pose (identity)*" + if (t - 1) >= len(state.action_raw): + return "" + + a = state.action_raw[t - 1] # always 57D (zero-padded) + mask = state.mask + + def _fmt(v): + return " ".join(f"{x:+.4f}" for x in v) + + def _v(active): + return "✓" if active else "·" + + gr = a[18:33].reshape(5, 3) + gl = a[42:57].reshape(5, 3) + + # Gripper auxiliary values (not in 57D vector) + grip_r_str = "" + grip_l_str = "" + if state.gripper_right is not None and t < len(state.gripper_right): + grip_r_str = f" ✓ gripper {state.gripper_right[t]:+.4f}" + if state.gripper_left is not None and t < len(state.gripper_left): + grip_l_str = f" ✓ gripper {state.gripper_left[t]:+.4f}" + + parts = [ + f"step {t - 1} → {t} (57D)", + "═" * 36, + f"{_v(mask.ego)} Ego pos [0:3] {_fmt(a[0:3])}", + f" {' ' * 1} rot [3:9] {_fmt(a[3:9])}", + "", + f"{_v(mask.right_wrist)} R wrist pos [9:12] {_fmt(a[9:12])}", + f" {' ' * 1} rot [12:18] {_fmt(a[12:18])}", + f" R fingers [18:33]", + ] + for i, name in enumerate(FINGER_NAMES): + parts.append(f" {_v(mask.right_fingers[i])} {name:7s} {_fmt(gr[i])}") + if grip_r_str: + parts.append(grip_r_str) + + parts += [ + "", + f"{_v(mask.left_wrist)} L wrist pos [33:36] {_fmt(a[33:36])}", + f" {' ' * 1} rot [36:42] {_fmt(a[36:42])}", + f" L fingers [42:57]", + ] + for i, name in enumerate(FINGER_NAMES): + parts.append(f" {_v(mask.left_fingers[i])} {name:7s} {_fmt(gl[i])}") + if grip_l_str: + parts.append(grip_l_str) + + return str("```\n" + "\n".join(parts) + "\n```") + + # ─── Private: Effector ──────────────────────────────────────────────────── + + @staticmethod + def _make_ego_frustum_wireframe_points(fov: float, aspect: float) -> np.ndarray: + """Build wireframe segments for the ego camera frustum.""" + half_height = float(np.tan(fov / 2.0)) + half_width = float(aspect) * half_height + top_left = np.array([-half_width, -half_height, 1.0], dtype=np.float32) + top_right = np.array([half_width, -half_height, 1.0], dtype=np.float32) + bottom_right = np.array([half_width, half_height, 1.0], dtype=np.float32) + bottom_left = np.array([-half_width, half_height, 1.0], dtype=np.float32) + origin = np.array([0.0, 0.0, 0.0], dtype=np.float32) + return np.array( + [ + [origin, top_left], + [origin, top_right], + [origin, bottom_right], + [origin, bottom_left], + [top_left, top_right], + [top_right, bottom_right], + [bottom_right, bottom_left], + [bottom_left, top_left], + ], + dtype=np.float32, + ) + + @staticmethod + def _make_ego_frustum_up_points(fov: float, aspect: float) -> np.ndarray: + """Build a red segment that marks the frustum's far-edge upright tick.""" + half_height = float(np.tan(fov / 2.0)) + _ = aspect + top_y = -half_height + return np.array( + [[[0.0, top_y, 1.0], [0.0, top_y * 1.18, 1.0]]], + dtype=np.float32, + ) + + def _update_ego(self, t: int, poses: np.ndarray | None, active: bool, show: dict) -> None: + if active and poses is not None and t < len(poses): + pos = poses[t, :3, 3] + rot = poses[t, :3, :3] + wxyz = self.vtf.SO3.from_matrix(rot).wxyz + self.ego_frame.position = pos + self.ego_frame.wxyz = wxyz + self.ego_frame.visible = show.get("frames", True) + self.ego_frustum.position = pos + self.ego_frustum.wxyz = wxyz + self.ego_frustum.visible = True + self.ego_frustum_up.position = pos + self.ego_frustum_up.wxyz = wxyz + self.ego_frustum_up.visible = True + self.ego_traj.visible = show.get("traj", True) + else: + self.ego_frame.visible = False + self.ego_frustum.visible = False + self.ego_frustum_up.visible = False + self.ego_traj.visible = False + + def _update_effector(self, t, poses, active, frame, ee, traj, show): + if active and poses is not None and t < len(poses): + pos = poses[t, :3, 3] + rot = poses[t, :3, :3] + frame.position = pos + frame.wxyz = self.vtf.SO3.from_matrix(rot).wxyz + frame.visible = show.get("frames", True) + ee.points = pos[None] + ee.visible = True + traj.visible = show.get("traj", True) + else: + frame.visible = False + ee.visible = False + traj.visible = False + + # ─── Private: Fingers ───────────────────────────────────────────────────── + + def _update_fingers(self, t, fingers, finger_mask, handles, show): + if fingers is None or t >= len(fingers): + for h in handles: + h.visible = False + return + if not show.get("fingertips", True): + for h in handles: + h.visible = False + return + + g = fingers[t] # (5, 3) + for fi, h in enumerate(handles): + if finger_mask[fi]: + h.position = g[fi].astype(np.float32) + h.visible = True + else: + h.visible = False + + # ─── Private: Gripper ───────────────────────────────────────────────────── + + def _update_gripper(self, t, poses, gripper, wrist_active, finger_mask, handle, show): + has_fingers = any(finger_mask) + if not wrist_active or has_fingers or gripper is None or poses is None or t >= len(gripper) or t >= len(poses): + for tip_handle in handle: + tip_handle.visible = False + return + if not show.get("fingertips", True): + for tip_handle in handle: + tip_handle.visible = False + return + pos = poses[t, :3, 3].astype(np.float32) + rot = poses[t, :3, :3].astype(np.float32) + g = float(gripper[t]) + mfw = getattr(self._entry, "max_finger_width", 0.05) + embodiment_type = _get_entry_agibot_embodiment_type(self._entry) + if ( + getattr(self._entry, "robot_name", "") == "embodiment_c" + and embodiment_type + and get_embodiment_c_kind(embodiment_type) == "gripper" + ): + tip_l, tip_r = _agibot_gripper_tip_positions(pos, rot, g, float(mfw)) + else: + half_w = g * mfw / 2.0 + finger_len = 0.06 + tip_l = pos + rot @ np.array([half_w, 0, finger_len], dtype=np.float32) + tip_r = pos + rot @ np.array([-half_w, 0, finger_len], dtype=np.float32) + for tip_handle, tip in zip(handle, (tip_l, tip_r), strict=True): + tip_handle.position = tip + tip_handle.visible = True + + # ─── Private: Trajectory ────────────────────────────────────────────────── + + def _rebuild_traj(self, traj_handle, poses, color): + """Rebuild a trajectory spline from absolute poses.""" + positions = poses[:, :3, 3].astype(np.float32) + if len(positions) < 2: + traj_handle.visible = False + return + line_width = self.EGO_TRAJ_LINE_WIDTH if traj_handle is self.ego_traj else self.EE_TRAJ_LINE_WIDTH + # Remove and recreate to avoid stale color issues in viser + name = traj_handle.name if hasattr(traj_handle, "name") else "/tmp/traj" + traj_handle.remove() + new_handle = self.server.scene.add_spline_catmull_rom( + name, + positions=positions, + color=self._soften_color(color), + line_width=line_width, + ) + # Update the reference — need to figure out which attribute to update + if traj_handle is self.ego_traj: + self.ego_traj = new_handle + elif traj_handle is self.right_traj: + self.right_traj = new_handle + elif traj_handle is self.left_traj: + self.left_traj = new_handle + + @staticmethod + def _trajectory_length(poses: np.ndarray | None) -> float: + """Compute the total path length of a pose trajectory.""" + if poses is None or len(poses) < 2: + return 0.0 + positions = poses[:, :3, 3].astype(np.float32) + deltas = np.diff(positions, axis=0) + return float(np.linalg.norm(deltas, axis=1).sum()) + + def _update_ego_visual_scale(self, poses: np.ndarray | None) -> None: + """Scale the ego camera frame/frustum from the episode trajectory length.""" + traj_length = self._trajectory_length(poses) + if traj_length <= 0.0: + traj_scale = 1.0 + else: + traj_scale = float( + np.clip( + traj_length / self.EGO_TRAJ_LENGTH_REFERENCE, + self.EGO_TRAJ_SCALE_MIN, + self.EGO_TRAJ_SCALE_MAX, + ) + ) + self._ego_frustum_scale_base = self.FRUSTUM_SCALE * traj_scale + self._ego_axis_length_base = self.EGO_AXIS_LENGTH * traj_scale + self._ego_axis_radius_base = self.EGO_AXIS_RADIUS * traj_scale + + # ─── Private: IK Robot ──────────────────────────────────────────────────── + + @classmethod + def _robot_frame_dims(cls, frame_key: str) -> tuple[float, float]: + """Return axis length/radius for one robot debug frame.""" + if frame_key.startswith("site:"): + return cls.ROBOT_SITE_AXIS_LENGTH, cls.ROBOT_SITE_AXIS_RADIUS + return cls.ROBOT_BODY_AXIS_LENGTH, cls.ROBOT_BODY_AXIS_RADIUS + + @staticmethod + def _clear_robot_frame_handles(handles: dict[str, Any]) -> None: + """Remove all robot debug frame handles in one arm.""" + for handle in handles.values(): + handle.remove() + handles.clear() + + def _robot_frame_selector_key(self, arm: str, frame_key: str) -> str: + """Return the GUI selector key for one robot debug frame.""" + if self._robot_frames_left is None and arm == "right": + return frame_key + return f"{arm}/{frame_key}" + + def get_robot_frame_selectors(self) -> list[tuple[str, str]]: + """Return selector keys and checkbox labels for available robot frames.""" + selectors = [] + for arm, frames in [("right", self._robot_frames_right), ("left", self._robot_frames_left)]: + if frames is None: + continue + for frame_key in sorted(frames): + selector_key = self._robot_frame_selector_key(arm, frame_key) + if self._robot_frames_left is None and arm == "right": + label = frame_key + else: + label = selector_key + selectors.append((selector_key, label)) + return selectors + + def _rebuild_robot_frame_handles( + self, + arm: str, + frames: dict[str, list[tuple[np.ndarray, np.ndarray]]] | None, + ) -> None: + """Recreate robot debug frame handles for one arm.""" + handles = self._robot_frame_handles_right if arm == "right" else self._robot_frame_handles_left + self._clear_robot_frame_handles(handles) + if frames is None: + return + for frame_key in sorted(frames): + kind, name = frame_key.split(":", 1) + axes_length, axes_radius = self._robot_frame_dims(frame_key) + handles[frame_key] = self.server.scene.add_frame( + f"/robot_{arm}_frames/{kind}/{name}", + axes_length=axes_length * self._axis_scale, + axes_radius=axes_radius * self._axis_scale, + ) + log.info(f"Loaded {len(handles)} robot debug frames for {arm} arm") + + def _update_robot_debug_frames(self, t: int, show: dict) -> None: + """Update body/site coordinate frame overlays for both arms.""" + filters = show.get("robot_frame_filters", {}) + for arm, handles, frames in [ + ("right", self._robot_frame_handles_right, self._robot_frames_right), + ("left", self._robot_frame_handles_left, self._robot_frames_left), + ]: + for frame_key, handle in handles.items(): + poses = frames.get(frame_key) if frames is not None else None + selector_key = self._robot_frame_selector_key(arm, frame_key) + frame_enabled = bool(filters.get(selector_key, False)) + if poses is not None and t < len(poses) and frame_enabled: + pos, rot = poses[t] + handle.position = pos + handle.wxyz = self.vtf.SO3.from_matrix(rot).wxyz + handle.visible = True + else: + handle.visible = False + + def _set_agibot_mesh_animation_from_link_poses(self, link_poses: dict[str, np.ndarray], num_steps: int) -> None: + """Populate AgiBot mesh transforms from absolute URDF link poses.""" + + animated_transforms: list[list[tuple[np.ndarray, np.ndarray]]] = [] + for step_idx in range(num_steps): + frame_transforms: list[tuple[np.ndarray, np.ndarray]] = [] + for link_name, local_transform in zip( + self._robot_link_names, + self._robot_local_transforms, + strict=True, + ): + world_transform = link_poses[link_name][step_idx] @ local_transform # [4,4] + frame_transforms.append( + ( + world_transform[:3, 3].astype(np.float32, copy=False), # [3] + world_transform[:3, :3].astype(np.float32, copy=False), # [3,3] + ) + ) + animated_transforms.append(frame_transforms) + + self._ik_right = animated_transforms + self._ik_left = None + self._robot_frames_right = None + self._robot_frames_left = None + + def _load_robot_and_ik(self, state: SceneState, entry: Any): + """Load robot meshes and solve IK for the episode.""" + if entry.robot_name == "embodiment_c": + embodiment_type = _get_entry_agibot_embodiment_type(entry) + if not embodiment_type: + log.warning("AgiBot entry missing embodiment_type — skipping robot animation") + return + + robot_key = (entry.robot_name, embodiment_type, "urdf_collision") + if self._current_robot != robot_key: + for h in self.robot_right + self.robot_left: + h.remove() + self.robot_right = [] + self.robot_left = [] + self._clear_robot_frame_handles(self._robot_frame_handles_right) + self._clear_robot_frame_handles(self._robot_frame_handles_left) + self._robot_link_names = [] + self._robot_local_transforms = [] + + geometries = get_embodiment_c_collision_geometries() + for geometry in geometries: + handle = self.server.scene.add_mesh_trimesh( + f"/robot_right/{geometry.name}", + mesh=geometry.mesh, + ) + handle.visible = False + self.robot_right.append(handle) + self._robot_link_names.append(geometry.link_name) + self._robot_local_transforms.append(geometry.local_transform) + + self._current_robot = robot_key + log.info(f"Loaded {len(geometries)} AgiBot URDF collision geometries") + + if state.agibot_link_poses: + missing_links = [ + link_name for link_name in self._robot_link_names if link_name not in state.agibot_link_poses + ] + if not missing_links: + num_steps = min( + int(state.agibot_link_poses[link_name].shape[0]) for link_name in self._robot_link_names + ) + if num_steps > 0: + self._set_agibot_mesh_animation_from_link_poses(state.agibot_link_poses, num_steps) + return + log.warning( + f"AgiBot direct FK link poses missing {len(missing_links)} robot links; falling back to IK." + ) + + if state.right_poses is None and state.left_poses is None and state.ego_poses is None: + log.warning("AgiBot viewer has no head/left/right pose targets — skipping robot animation") + return + + left_wrist_poses_ik = _undo_wrist_to_opencv_for_ik( + state.left_poses, + self._to_opencv, + "left_wrist", + ) # [T,4,4] | None + right_wrist_poses_ik = _undo_wrist_to_opencv_for_ik( + state.right_poses, + self._to_opencv, + "right_wrist", + ) # [T,4,4] | None + + joint_configs = solve_agibot_trajectory_ik( + head_camera_poses=state.ego_poses, + left_wrist_poses=left_wrist_poses_ik, + right_wrist_poses=right_wrist_poses_ik, + left_gripper_openings=state.gripper_left, + right_gripper_openings=state.gripper_right, + ) + if joint_configs is None: + log.warning("AgiBot IK failed — skipping robot animation") + return + + link_poses = compute_agibot_link_poses_batch_from_configs( + joint_configs, + self._robot_link_names, + ) + self._set_agibot_mesh_animation_from_link_poses(link_poses, int(joint_configs.shape[0])) + return + + # Determine robot config key (single vs dual) + is_dual = state.mask.left_wrist and entry.dual_base_left is not None + robot_key = (entry.robot_name, "dual") if is_dual else entry.robot_name + + if self._robot_scene_model is None or self._robot_scene_model.robot_name != entry.robot_name: + try: + self._robot_scene_model = RobotSceneModel(entry.robot_name) + except Exception as e: + log.warning(f"RobotSceneModel unavailable for {entry.robot_name}: {e}") + self._robot_scene_model = None + return + + # Only reload meshes if robot changed + if self._current_robot != robot_key: + for h in self.robot_right + self.robot_left: + h.remove() + self.robot_right = [] + self.robot_left = [] + self._clear_robot_frame_handles(self._robot_frame_handles_right) + self._clear_robot_frame_handles(self._robot_frame_handles_left) + + if self._robot_scene_model is None: + self._current_robot = robot_key + return + + right_meshes = self._robot_scene_model.get_home_meshes(entry.dual_base_right if is_dual else None) + for name, mesh, transform in right_meshes: + h = self.server.scene.add_mesh_trimesh( + f"/robot_right/{name}", + mesh=mesh, + ) + h.position = transform[:3, 3] + h.wxyz = self.vtf.SO3.from_matrix(transform[:3, :3]).wxyz + self.robot_right.append(h) + + if is_dual: + left_meshes = self._robot_scene_model.get_home_meshes(entry.dual_base_left) + for name, mesh, transform in left_meshes: + h = self.server.scene.add_mesh_trimesh( + f"/robot_left/{name}", + mesh=mesh, + ) + h.position = transform[:3, 3] + h.wxyz = self.vtf.SO3.from_matrix(transform[:3, :3]).wxyz + self.robot_left.append(h) + + self._current_robot = robot_key + log.info(f"Loaded {len(right_meshes)} meshes for {entry.robot_name}") + + if self._robot_scene_model is None: + return + + # Joint-position datasets (e.g. robomind-ur): bypass IK, use FK directly + if state.joint_configs is not None: + from cosmos.data.vfm.action.urdf_visualizer.ik_solver import compute_mujoco_geom_transforms + from cosmos.data.vfm.action.urdf_visualizer.robot_scene_model import get_mjcf_path + + try: + mjcf_path = get_mjcf_path(entry.robot_name) + transforms, _, _fk_ee_poses, robot_frames = compute_mujoco_geom_transforms( + mjcf_path, state.joint_configs + ) + self._ik_right = transforms + self._robot_frames_right = robot_frames + self._rebuild_robot_frame_handles("right", robot_frames) + log.info(f"FK geom transforms computed for {len(transforms)} frames ({entry.robot_name})") + except Exception as e: + log.warning(f"FK failed for {entry.robot_name}: {e}") + import traceback + + traceback.print_exc() + return + + # Right arm IK + if state.right_poses is not None: + try: + right_result = self._robot_scene_model.solve_visual_trajectory( + state.right_poses, + gripper_openings=state.gripper_right, + to_opencv=self._to_opencv, + base_pose=entry.dual_base_right if is_dual else None, + ) + if right_result is not None: + self._ik_right = right_result.mesh_transforms + self._robot_frames_right = right_result.named_frames + self._rebuild_robot_frame_handles("right", self._robot_frames_right) + else: + self._ik_right = None + self._robot_frames_right = None + self._rebuild_robot_frame_handles("right", None) + except Exception as e: + log.warning(f"IK failed (right): {e}") + self._ik_right = None + self._robot_frames_right = None + self._rebuild_robot_frame_handles("right", None) + else: + self._ik_right = None + self._robot_frames_right = None + self._rebuild_robot_frame_handles("right", None) + + # Left arm IK (dual only) + if is_dual and state.left_poses is not None: + try: + left_result = self._robot_scene_model.solve_visual_trajectory( + state.left_poses, + gripper_openings=state.gripper_left, + to_opencv=self._to_opencv, + base_pose=entry.dual_base_left, + ) + if left_result is not None: + self._ik_left = left_result.mesh_transforms + self._robot_frames_left = left_result.named_frames + self._rebuild_robot_frame_handles("left", self._robot_frames_left) + else: + self._ik_left = None + self._robot_frames_left = None + self._rebuild_robot_frame_handles("left", None) + except Exception as e: + log.warning(f"IK failed (left): {e}") + self._ik_left = None + self._robot_frames_left = None + self._rebuild_robot_frame_handles("left", None) + else: + self._ik_left = None + self._robot_frames_left = None + self._rebuild_robot_frame_handles("left", None) + + def _update_robot(self, t: int, show: dict): + vis = show.get("robot", True) + for handles, ik in [(self.robot_right, self._ik_right), (self.robot_left, self._ik_left)]: + for idx, h in enumerate(handles): + if vis and ik is not None and t < len(ik) and idx < len(ik[t]): + p, m = ik[t][idx] + h.position = p + h.wxyz = self.vtf.SO3.from_matrix(m).wxyz + h.visible = True + else: + h.visible = False + self._update_robot_debug_frames(t, show) + + # ─── Visibility ─────────────────────────────────────────────────────────── + + def hide_all(self): + """Hide every scene element.""" + for attr in [ + self.ego_frame, + self.ego_frustum, + self.ego_frustum_up, + self.ego_traj, + self.right_frame, + self.right_ee, + self.right_traj, + self.left_frame, + self.left_ee, + self.left_traj, + ]: + attr.visible = False + for h in self.right_fingers + self.left_fingers: + h.visible = False + for h in self.right_gripper_tips + self.left_gripper_tips: + h.visible = False + for h in self.robot_right + self.robot_left: + h.visible = False + for handle in list(self._robot_frame_handles_right.values()) + list(self._robot_frame_handles_left.values()): + handle.visible = False + + def update_axis_scale(self, scale: float): + """Update coordinate frame axis size and effector point size.""" + self._axis_scale = scale + s = scale + self.ego_frame.axes_length = self._ego_axis_length_base * s + self.ego_frame.axes_radius = self._ego_axis_radius_base * s + self.ego_frustum.scale = self._ego_frustum_scale_base * s + self.ego_frustum_up.scale = self._ego_frustum_scale_base * s + for frame in (self.right_frame, self.left_frame): + frame.axes_length = self.HAND_AXIS_LENGTH * s + frame.axes_radius = self.HAND_AXIS_RADIUS * s + for ee in (self.right_ee, self.left_ee): + ee.point_size = 0.015 * s + for handles in (self._robot_frame_handles_right, self._robot_frame_handles_left): + for frame_key, handle in handles.items(): + axes_length, axes_radius = self._robot_frame_dims(frame_key) + handle.axes_length = axes_length * s + handle.axes_radius = axes_radius * s diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ur5e_robotiq_2f85.xml b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ur5e_robotiq_2f85.xml new file mode 100644 index 00000000..500e6861 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/ur5e_robotiq_2f85.xml @@ -0,0 +1,321 @@ + + + + + diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/urdf_loader.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/urdf_loader.py new file mode 100644 index 00000000..d7f59959 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/urdf_loader.py @@ -0,0 +1,1106 @@ +"""Robot mesh loading from mujoco_menagerie. + +Downloads robot MJCF/URDF/mesh assets on first use, caches in ~/.cache/mujoco_menagerie/. +Each loader returns (meshes, ee_home_pose) where: + - meshes: list of (name, trimesh.Trimesh, 4x4_transform) + - ee_home_pose: (4, 4) float32 — EE pose at home configuration +""" + +from __future__ import annotations + +import os +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path +from typing import Any + +import numpy as np +from scipy.spatial.transform import Rotation as R + +from cosmos.utils import log +from cosmos.data.vfm.action.embodiment_c_spec import get_embodiment_c_urdf_path + +_MENAGERIE_REPO = "https://github.com/google-deepmind/mujoco_menagerie" +_ROBOT_CACHE_DIR = Path("/tmp") / "mujoco_menagerie" +_AGIBOT_GEAR_LUSTRE_URDF_ROOT = Path( + "/lustre/fsw/portfolios/cosmos/projects/cosmos_base_training/cosmos3_action_datasets/agibot_urdf/g1_urdf" +) +_AGIBOT_GEAR_LOCAL_SAMPLE_URDF_ROOT = Path(__file__).resolve().parents[6] / "dev/embodiment_c_samples/g1_urdf" +_FALLBACK_MESH_COLORS = np.asarray( + [ + [52, 152, 219, 255], + [243, 156, 18, 255], + [155, 89, 182, 255], + [46, 204, 113, 255], + [231, 76, 60, 255], + [26, 188, 156, 255], + [241, 196, 15, 255], + [52, 73, 94, 255], + [127, 140, 141, 255], + [211, 84, 0, 255], + [39, 174, 96, 255], + [41, 128, 185, 255], + ], + dtype=np.uint8, +) +_UNSPECIFIED_RGBA_VALUES = ( + np.asarray([0.5, 0.5, 0.5, 1.0], dtype=np.float32), + np.asarray([1.0, 1.0, 1.0, 1.0], dtype=np.float32), + # Some autogenerated URDF/MJCF assets use pink as a placeholder/default + # material. Treat it as unspecified so meshes do not all render pink. + np.asarray([1.0, 0.5, 1.0, 1.0], dtype=np.float32), +) +_UNSPECIFIED_MATERIAL_NAMES = {"", "default", "default_material"} + + +@dataclass(frozen=True) +class AgibotGeometry: + """One AgiBot collision primitive attached to a URDF link.""" + + name: str + link_name: str + mesh: Any + local_transform: np.ndarray + + +def _fallback_mesh_color(name: str, index: int = 0) -> np.ndarray: + """Return a stable fallback RGBA color for a mesh without material color.""" + + key = name or f"mesh_{index}" + palette_index = sum((char_idx + 1) * ord(char) for char_idx, char in enumerate(key)) + return _FALLBACK_MESH_COLORS[(palette_index + index) % len(_FALLBACK_MESH_COLORS)].copy() + + +def _is_unspecified_rgba(rgba: np.ndarray) -> bool: + """Return whether RGBA is an uninformative default color.""" + + return any(np.allclose(rgba, default_rgba, atol=1e-4) for default_rgba in _UNSPECIFIED_RGBA_VALUES) + + +def _apply_mesh_color(mesh: Any, rgba: np.ndarray) -> None: + """Apply one RGBA color to all mesh faces for viser/trimesh rendering.""" + + face_count = int(len(getattr(mesh, "faces", []))) + if face_count > 0: + mesh.visual.face_colors = np.tile(np.asarray(rgba, dtype=np.uint8), (face_count, 1)) + + +# ── Robot-specific configuration ────────────────────────────────────────────── + +# EE frame name candidates (tried in order by IK solver) +# --------------------------------------------------------------------------- +# End-effector frame references per dataset +# --------------------------------------------------------------------------- +# Each dataset records EE poses at a specific URDF link. The IK solver must +# target the same frame (via ``ee_frame`` or the ``_EE_FRAME_CANDIDATES`` +# fallback list). Below we document, for every robot: +# (a) which link the dataset records at, with upstream source reference, +# (b) what that link physically represents (flange, gripper body, or TCP), +# (c) any orientation transforms applied during recording. +# +# ── Google Robot (fractal / RT-1) ────────────────────────────────────────── +# Recorded link: ``link_gripper_tcp`` +# The TFDS observation field is ``base_pose_tool_reached`` (shape (7,)). +# Source: https://www.tensorflow.org/datasets/catalog/fractal20220817_data +# Meaning: TCP — a calibrated tool-center-point 164 mm past the wrist +# (``link_gripper``), roughly at the fingertip. The name contains "tool" +# and SimplerEnv's real→sim calibration targets the same link: +# ``self.ee_link_name = "link_gripper_tcp"`` +# Source: https://github.com/simpler-env/ManiSkill2_real2sim/blob/ef7a4d4/mani_skill2_real2sim/agents/configs/google_robot/defaults.py#L141 +# Orientation: recorded as-is from the URDF FK (quaternion). No extra +# rotation applied. +# Re-referenced to: ``link_gripper`` (gripper body / last actuated link) +# via ``_TCP_TO_FLANGE`` in the dataset class (−164 mm along local z). +# See ``fractal.py`` for the full transform and rationale. +# Our config: ``"ee_frame": "link_gripper"`` (explicit). +# Important MJCF note: MuJoCo Menagerie places ``base_link`` at +# ``pos="0 0 0.06205"`` under ``worldbody``. Pinocchio's +# ``buildModelFromMJCF()`` omits this global root transform, so IK / dataset +# poses live in a root-free world that is 62.05 mm lower than raw MuJoCo +# body/site poses. We therefore strip the same root transform from MuJoCo +# mesh/body/site outputs before rendering or comparing them. +# +# ── WidowX (bridge) ─────────────────────────────────────────────────────── +# Recorded link: ``ee_gripper_link`` +# The bridge data collection uses the Interbotix SDK's Modern Robotics FK +# (``FKinSpace(M, Slist, q)``). The M matrix for wx250s terminates at +# ``ee_gripper_link`` — confirmed by the Interbotix source comment and +# numerically: ``wx250s.M[0,3] = 0.458325`` matches ``ee_gripper_link`` +# position exactly (vs ``gripper_link`` at 0.3648). +# Source: https://github.com/Interbotix/interbotix_ros_toolboxes/blob/main/interbotix_xs_toolbox/interbotix_xs_modules/src/interbotix_xs_modules/mr_descriptions.py +# Line 1: "Note that the end-effector is positioned at '/ee_gripper_link'" +# SimplerEnv confirms: ``self.ee_link_name = "ee_gripper_link"`` +# Source: https://github.com/simpler-env/ManiSkill2_real2sim/blob/ef7a4d4/mani_skill2_real2sim/agents/configs/widowx/defaults.py#L87 +# Meaning: Interbotix-defined "end-effector" reference point, 93.6 mm past +# the wrist (``gripper_link``), 27.6 mm past the finger pivot +# (``fingers_link``). This is roughly the grasp center between the +# finger pads — *not* the fingertip. +# Orientation: the bridge data collection applies ``DEFAULT_ROTATION`` to +# the FK orientation before recording (in ``transform2state``): +# ``euler = rotationMatrixToEulerAngles(rot.dot(default_rotation.T))`` +# where ``DEFAULT_ROTATION = [[0,0,1],[0,1,0],[-1,0,0]]``. +# Source: https://github.com/rail-berkeley/bridge_data_robot/blob/main/widowx_envs/widowx_controller/src/widowx_controller/widowx_controller.py#L44 +# Re-referenced to: ``gripper_link`` (wrist rotate body / last actuated link) +# via ``_TCP_TO_FLANGE`` in the dataset class (−93.6 mm along local x). +# See ``bridge_orig_lerobot_dataset.py`` for the full transform and rationale. +# Our config: ``"ee_frame": "gripper_link"`` (explicit). +# +# ── Franka + Robotiq 2F-85 (DROID) ──────────────────────────────────────── +# Recorded link: ``panda_link8`` (= Franka flange) +# DROID's ``get_robot_state()`` calls Polymetis FK: +# ``pos, quat = self._robot.robot_model.forward_kinematics(joint_positions)`` +# Source: https://github.com/droid-dataset/droid/blob/main/droid/franka/robot.py +# Polymetis is configured with ``ee_link_name: panda_link8``: +# Source: https://github.com/facebookresearch/fairo/blob/main/polymetis/polymetis/conf/robot_model/franka_panda.yaml +# Meaning: flange — the bare mounting plate at the end of the Franka arm, +# *before* the Robotiq gripper. +# MJCF body: ``panda_hand`` in the composite model (pos 0 0 0.107 +# above ``link7``, identity quat). This is at the panda_link8 +# (flange) position with link7 orientation, matching the URDF. +# ``link7`` is panda_link7 (wrist), *not* the flange. +# Orientation: Polymetis reports FK orientation at ``panda_link8`` frame +# as-is. No extra rotation applied. +# Current viewer convention: all Franka-backed datasets reuse this DROID +# composite model, so ``franka_panda`` is the only supported Franka robot +# config in this module. +# Our config: ``"ee_frame": "panda_hand"`` (explicit). +# --------------------------------------------------------------------------- + +_EE_FRAME_CANDIDATES = [ + "link_gripper_tcp", # SimplerEnv Google Robot (calibrated TCP at fingertip) + "link_gripper", # Google Robot (Menagerie / fallback) + "ee_gripper_link", # SimplerEnv WidowX (end of finger chain) + "gripper_link", # WidowX (MuJoCo body name) + "wx250s/gripper_link", # WidowX Menagerie (prefixed body name) + "gripper", # Google Robot (Menagerie site) + "hand", # Franka Panda (standard gripper) + "attachment", # Franka + Robotiq + "attachment_site", # UR5e (mujoco_menagerie) + "wx250s/gripper_link", # WidowX 250S (Menagerie) + "ee_link", # generic + "tool0", # generic industrial +] + +# Robot-specific configs: menagerie name, MJCF filename, joint info, finger range. +ROBOT_CONFIGS = { + "google_robot": { + "menagerie": "google_robot", + "mjcf": "robot.xml", + # EE: link_gripper (gripper body / last actuated link). + # The Menagerie model has link_gripper directly (no TCP frame). + # The dataset applies _TCP_TO_FLANGE to shift from the recorded + # link_gripper_tcp to link_gripper, so poses already target this frame. + # MuJoCo Menagerie also adds a global worldbody -> base_link offset + # (+62.05 mm in z). Pinocchio's MJCF importer omits that root transform, + # so the viewer removes it from MuJoCo outputs to keep meshes / body + # frames aligned with dataset poses and IK outputs. + "ee_frame": "link_gripper", + "pinocchio_removed_root_body": "base_link", + "arm_joints": [ + "joint_torso", + "joint_shoulder", + "joint_bicep", + "joint_elbow", + "joint_forearm", + "joint_wrist", + "joint_gripper", + ], + "n_arm_joints": 7, + "finger_joint_names": ["joint_finger_left", "joint_finger_right"], + "finger_min": 0.01, + "finger_max": 1.30, + "finger_close_is_max": True, + "camera_body": None, + }, + "franka_panda": { + "menagerie": "droid_franka_robotiq", + "mjcf": "panda_updated_robotiq_2f85.xml", + # EE: panda_link8 (flange) — the bare mounting plate, *not* the + # gripper TCP. MJCF body: ``panda_hand`` in the composite model + # (pos 0 0 0.107 above link7, identity quat). + + "ee_frame": "panda_hand", + "arm_joints": [ + "joint1", + "joint2", + "joint3", + "joint4", + "joint5", + "joint6", + "joint7", + ], + "n_arm_joints": 7, + "finger_min": 0.0, + "finger_max": 0.8, + "finger_close_is_max": True, + # All Robotiq 2F-85 chain joints (some share names with bodies in + # pinocchio MJCF parse — must keep all to avoid buildReducedModel error) + "finger_joint_names": [ + "right_driver_joint", + "left_driver_joint", + "left_spring_link_joint", + "left_follower", + "right_spring_link_joint", + "right_follower_joint", + ], + "camera_body": None, + }, + "widowx": { + "menagerie": "trossen_wx250s", + "mjcf": "wx250s.xml", + # EE: gripper_link (wrist rotate body / last actuated link). + # The Menagerie model has gripper_link directly. + # The dataset applies _TCP_TO_FLANGE to shift from the recorded + # ee_gripper_link to gripper_link, so poses already target this frame. + "ee_frame": "gripper_link", + "arm_joints": [ + "waist", + "shoulder", + "elbow", + "forearm_roll", + "wrist_angle", + "wrist_rotate", + ], + "n_arm_joints": 6, + "finger_min": 0.015, + "finger_max": 0.037, + "finger_close_is_max": False, + "finger_joint_names": ["left_finger", "right_finger"], + "camera_body": None, + }, + "ur5e": { + "menagerie": "ur5e_robotiq", + "mjcf": "ur5e_robotiq_2f85.xml", + # EE: attachment_site — the UR5e flange site where the Robotiq mounts. + # joint_configs[:, 6] raw UR gripper maps directly to Robotiq ctrl: + # ctrl = raw * 255 (0=open, 255=closed). + "ee_frame": "attachment_site", + "n_arm_joints": 6, + "finger_min": 0.0, + "finger_max": 255.0, + "finger_close_is_max": False, + "camera_body": None, + }, + "embodiment_c": { + # No menagerie — uses local AgiBot G1/omnipicker URDF assets. + # The dataset targets the gripper base/flange, not the TCP/center link. + "menagerie": None, + "ee_frame": "gripper_r_base_link", + "arm_joints": [f"idx{11 + i:02d}_right_arm_joint{i}" for i in range(1, 8)], + "n_arm_joints": 7, + "finger_min": 0.0, + "finger_max": 1.0, + "finger_close_is_max": False, + "camera_body": "head_pitch_link", + }, +} + + +def get_urdf_path(robot_name: str) -> str | None: + """Get URDF path for a robot if available, else None (falls back to MJCF in caller).""" + cfg = ROBOT_CONFIGS.get(robot_name) + if cfg is None: + return None + + if robot_name == "embodiment_c": + return str(get_embodiment_c_urdf_path()) + + menagerie_name = cfg.get("menagerie") + if menagerie_name is None: + return None + mjcf_dir = _ensure_robot_assets(menagerie_name) + + urdf_filename = cfg.get("urdf") + if urdf_filename: + urdf_path = mjcf_dir / urdf_filename + if urdf_path.exists(): + return str(urdf_path) + + urdfs = list(mjcf_dir.glob("*.urdf")) + if urdfs: + return str(urdfs[0]) + + return None + + +def get_mjcf_path(robot_name: str) -> str: + """Get MJCF path for a robot, downloading from mujoco_menagerie if needed.""" + cfg = ROBOT_CONFIGS.get(robot_name) + if cfg is None: + raise ValueError(f"Unknown robot: {robot_name}. Available: {list(ROBOT_CONFIGS.keys())}") + + menagerie_name = cfg.get("menagerie") + if menagerie_name is None: + raise FileNotFoundError(f"Robot {robot_name!r} does not have a MuJoCo MJCF asset.") + mjcf_filename = cfg.get("mjcf", "robot.xml") + + mjcf_dir = _ensure_robot_assets(menagerie_name) + mjcf_path = mjcf_dir / mjcf_filename + if mjcf_path.exists(): + return str(mjcf_path) + + fallback = mjcf_dir / "robot.xml" + if fallback.exists(): + return str(fallback) + + raise FileNotFoundError(f"MJCF not found: {mjcf_path} (also tried robot.xml)") + + +# ── Asset download ─────────────────────────────────────────────────────────── + + +def _ensure_robot_assets(robot_name: str) -> Path: + """Ensure robot MJCF and mesh assets are available locally. + + Downloads from mujoco_menagerie GitHub repo if not found. + For composite robots (``droid_franka_robotiq``), downloads from both + MuJoCo Menagerie and MuJoCo Playground and merges assets. + + Assets are cached in ~/.cache/mujoco_menagerie//. + + Returns: + Path to the cached MJCF directory (caller picks the right .xml). + """ + cached_dir = _ROBOT_CACHE_DIR / robot_name + if cached_dir.exists(): + return cached_dir + + # Composite: Franka arm + Robotiq 2F-85 gripper (DROID setup) + if robot_name == "droid_franka_robotiq": + return _build_droid_franka_robotiq() + + # Composite: UR5e arm + Robotiq 2F-85 gripper (RoboMIND UR setup) + if robot_name == "ur5e_robotiq": + return _build_ur5e_robotiq() + + log.info(f"Downloading {robot_name} assets from mujoco_menagerie...") + _download_menagerie_model(robot_name) + return cached_dir + + +def _download_menagerie_model(model_name: str) -> Path: + """Download a single model from MuJoCo Menagerie. Returns cached path.""" + cached_dir = _ROBOT_CACHE_DIR / model_name + if cached_dir.exists(): + return cached_dir + + import shutil + import subprocess + import tempfile + + _ROBOT_CACHE_DIR.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory() as tmpdir: + subprocess.run( + ["git", "clone", "--depth=1", "--filter=blob:none", "--sparse", _MENAGERIE_REPO, tmpdir], + check=True, + capture_output=True, + ) + subprocess.run( + ["git", "sparse-checkout", "set", model_name], + cwd=tmpdir, + check=True, + capture_output=True, + ) + src = Path(tmpdir) / model_name + dst = _ROBOT_CACHE_DIR / model_name + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + + log.info(f"Cached {model_name} assets at {dst}") + return dst + + +def _build_droid_franka_robotiq() -> Path: + """Prepare composite Franka + Robotiq 2F-85 MJCF for DROID visualization. + + The hand-tuned composite XML is committed in the repo alongside this + module (``droid_franka_robotiq_2f85.xml``). This function downloads the + mesh assets from MuJoCo Menagerie (``franka_emika_panda`` + + ``robotiq_2f85_v4``) and copies them together with the XML into a cache + directory that MuJoCo can load from. + """ + import shutil + + log.info("Preparing composite Franka + Robotiq 2F-85 (committed XML + Menagerie assets)...") + + franka_dir = _download_menagerie_model("franka_emika_panda") + robotiq_dir = _download_menagerie_model("robotiq_2f85_v4") + + dst = _ROBOT_CACHE_DIR / "droid_franka_robotiq" + dst.mkdir(parents=True, exist_ok=True) + assets_dir = dst / "assets" + assets_dir.mkdir(exist_ok=True) + + for f in (franka_dir / "assets").iterdir(): + shutil.copy2(f, assets_dir / f.name) + for f in (robotiq_dir / "assets").iterdir(): + shutil.copy2(f, assets_dir / f.name) + + committed_xml = Path(__file__).parent / "droid_franka_robotiq_2f85.xml" + out_path = dst / "panda_updated_robotiq_2f85.xml" + shutil.copy2(committed_xml, out_path) + + log.info(f"Prepared composite Franka + Robotiq 2F-85 at {dst}") + return dst + + +def _build_ur5e_robotiq() -> Path: + """Prepare composite UR5e + Robotiq 2F-85 MJCF for RoboMIND UR visualization. + + The hand-tuned composite XML is committed in the repo alongside this + module (``ur5e_robotiq_2f85.xml``). This function downloads the mesh + assets from MuJoCo Menagerie (``universal_robots_ur5e`` + + ``robotiq_2f85_v4``) and copies them together with the XML into a cache + directory that MuJoCo can load from. + """ + import shutil + + log.info("Preparing composite UR5e + Robotiq 2F-85 (committed XML + Menagerie assets)...") + + ur5e_dir = _download_menagerie_model("universal_robots_ur5e") + robotiq_dir = _download_menagerie_model("robotiq_2f85_v4") + + dst = _ROBOT_CACHE_DIR / "ur5e_robotiq" + dst.mkdir(parents=True, exist_ok=True) + assets_dir = dst / "assets" + assets_dir.mkdir(exist_ok=True) + + for f in (ur5e_dir / "assets").iterdir(): + shutil.copy2(f, assets_dir / f.name) + for f in (robotiq_dir / "assets").iterdir(): + shutil.copy2(f, assets_dir / f.name) + + committed_xml = Path(__file__).parent / "ur5e_robotiq_2f85.xml" + shutil.copy2(committed_xml, dst / "ur5e_robotiq_2f85.xml") + + log.info(f"Prepared composite UR5e + Robotiq 2F-85 at {dst}") + return dst + + +# ── Robot loaders ──────────────────────────────────────────────────────────── + + +def _load_google_robot() -> tuple[list, np.ndarray]: + """Load Google Robot from MuJoCo Menagerie. + + Uses the official google_robot model from + https://github.com/google-deepmind/mujoco_menagerie/tree/main/google_robot + which has visual OBJ meshes for accurate rendering. + """ + import mujoco + + cfg = ROBOT_CONFIGS["google_robot"] + mjcf_dir = _ensure_robot_assets(cfg["menagerie"]) + mjcf_path = mjcf_dir / cfg["mjcf"] + model = mujoco.MjModel.from_xml_path(str(mjcf_path)) + data = mujoco.MjData(model) + mujoco.mj_forward(model, data) + + meshes = _extract_mujoco_meshes(model, data) + world_correction = get_mujoco_to_pinocchio_world_transform(model, data, "google_robot") + if not np.allclose(world_correction, np.eye(4, dtype=np.float32)): + meshes = _apply_world_transform_to_meshes(meshes, world_correction) + + # Find EE pose at link_gripper + ee_pose = np.eye(4, dtype=np.float32) + for i in range(model.nbody): + name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i) or "" + if name == "link_gripper": + ee_pose[:3, 3] = data.xpos[i].astype(np.float32) + ee_pose[:3, :3] = data.xmat[i].reshape(3, 3).astype(np.float32) + break + ee_pose = (world_correction @ ee_pose).astype(np.float32) + + log.info(f"Google Robot (Menagerie) loaded: {len(meshes)} meshes, EE pos={ee_pose[:3, 3]}") + return meshes, ee_pose + + +def _load_franka_panda() -> tuple[list, np.ndarray]: + """Load Franka Panda + Robotiq 2F-85 gripper (DROID variant). + + Uses the composite model built from MuJoCo Menagerie + (``franka_emika_panda`` arm + ``robotiq_2f85_v4`` gripper). + """ + import mujoco + + mjcf_dir = _ensure_robot_assets("droid_franka_robotiq") + mjcf_path = mjcf_dir / "panda_updated_robotiq_2f85.xml" + model = mujoco.MjModel.from_xml_path(str(mjcf_path)) + data = mujoco.MjData(model) + + # Franka home configuration + home_qpos = np.array([0.0, -0.78, 0.0, -2.36, 0.0, 1.57, 0.78]) + data.qpos[: len(home_qpos)] = home_qpos + mujoco.mj_forward(model, data) + + meshes = _extract_mujoco_meshes(model, data) + + # EE pose: use the "gripper" site (on the Robotiq base), or fall + # back to "base_mount" / "attachment" body if site isn't found. + ee_pose = np.eye(4, dtype=np.float32) + for si in range(model.nsite): + name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_SITE, si) or "" + if name == "gripper": + ee_pose[:3, 3] = data.site_xpos[si].astype(np.float32) + ee_pose[:3, :3] = data.site_xmat[si].reshape(3, 3).astype(np.float32) + break + else: + # Fallback to body search + for i in range(model.nbody): + name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i) or "" + if name in ("base_mount", "attachment"): + ee_pose[:3, 3] = data.xpos[i].astype(np.float32) + ee_pose[:3, :3] = data.xmat[i].reshape(3, 3).astype(np.float32) + break + + log.info(f"Franka Panda + Robotiq loaded: {len(meshes)} meshes, EE pos={ee_pose[:3, 3]}") + return meshes, ee_pose + + +def _load_widowx() -> tuple[list, np.ndarray]: + """Load WidowX 250S from MuJoCo Menagerie (trossen_wx250s). + + Uses the official Trossen Robotics WX250S model from + https://github.com/google-deepmind/mujoco_menagerie/tree/main/trossen_wx250s + which has visual OBJ meshes for accurate rendering. + """ + import mujoco + + cfg = ROBOT_CONFIGS["widowx"] + mjcf_dir = _ensure_robot_assets(cfg["menagerie"]) + mjcf_path = mjcf_dir / cfg["mjcf"] + model = mujoco.MjModel.from_xml_path(str(mjcf_path)) + data = mujoco.MjData(model) + mujoco.mj_forward(model, data) + + meshes = _extract_mujoco_meshes(model, data) + + ee_pose = np.eye(4, dtype=np.float32) + for i in range(model.nbody): + name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i) or "" + if name == "gripper_link": + ee_pose[:3, 3] = data.xpos[i].astype(np.float32) + ee_pose[:3, :3] = data.xmat[i].reshape(3, 3).astype(np.float32) + break + + log.info(f"WidowX 250S (Menagerie) loaded: {len(meshes)} meshes, EE pos={ee_pose[:3, 3]}") + return meshes, ee_pose + + +def _load_ur5e() -> tuple[list, np.ndarray]: + """Load UR5e + Robotiq 2F-85 composite from MuJoCo Menagerie.""" + import mujoco + + mjcf_dir = _ensure_robot_assets("ur5e_robotiq") + mjcf_path = mjcf_dir / "ur5e_robotiq_2f85.xml" + model = mujoco.MjModel.from_xml_path(str(mjcf_path)) + data = mujoco.MjData(model) + + # UR5e home: -90, -90, 90, -90, -90, 0 (degrees → radians); gripper open + home_qpos = np.array([-1.5708, -1.5708, 1.5708, -1.5708, -1.5708, 0.0]) + data.qpos[: len(home_qpos)] = home_qpos + mujoco.mj_forward(model, data) + + meshes = _extract_mujoco_meshes(model, data) + + # EE pose: use robotiq_base body (the flange-to-gripper attachment point) + ee_pose = np.eye(4, dtype=np.float32) + for i in range(model.nbody): + name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i) or "" + if name == "robotiq_base": + ee_pose[:3, 3] = data.xpos[i].astype(np.float32) + ee_pose[:3, :3] = data.xmat[i].reshape(3, 3).astype(np.float32) + break + + log.info(f"UR5e+Robotiq loaded: {len(meshes)} meshes, EE pos={ee_pose[:3, 3]}") + return meshes, ee_pose + + +def _load_embodiment_c() -> tuple[list, np.ndarray]: + """Load Embodiment C G1/omnipicker robot meshes from the local URDF.""" + + from cosmos.data.vfm.action.embodiment_c_fk import compute_link_poses_batch + + poses_batch = compute_link_poses_batch(np.zeros((1, 32), dtype=np.float32), "embodiment_c_gripper") + poses = {link_name: link_poses[0] for link_name, link_poses in poses_batch.items()} + geometries = get_embodiment_c_collision_geometries() + meshes = [] + for geometry in geometries: + link_pose = poses.get(geometry.link_name) + if link_pose is None: + continue + world_transform = link_pose @ geometry.local_transform + meshes.append((geometry.name, geometry.mesh.copy(), world_transform.astype(np.float32))) + + ee_pose = poses.get("gripper_r_base_link", np.eye(4, dtype=np.float32)).astype(np.float32) + log.info(f"Embodiment C (G1 omnipicker geometry) loaded: {len(meshes)} meshes, EE pos={ee_pose[:3, 3]}") + return meshes, ee_pose + + +def _parse_urdf_origin(origin_element) -> np.ndarray: + transform = np.eye(4, dtype=np.float32) + if origin_element is None: + return transform + + xyz_text = origin_element.attrib.get("xyz", "0 0 0") + rpy_text = origin_element.attrib.get("rpy", "0 0 0") + xyz = np.asarray([float(value) for value in xyz_text.split()], dtype=np.float32) + rpy = np.asarray([float(value) for value in rpy_text.split()], dtype=np.float32) + transform[:3, :3] = R.from_euler("xyz", rpy).as_matrix().astype(np.float32) + transform[:3, 3] = xyz + return transform + + +def _parse_urdf_color(color_element: ET.Element | None) -> np.ndarray | None: + if color_element is None: + return None + rgba_text = color_element.attrib.get("rgba") + if not rgba_text: + return None + rgba = np.asarray([float(value) for value in rgba_text.split()], dtype=np.float32) + if rgba.shape != (4,): + return None + return (np.clip(rgba, 0.0, 1.0) * 255.0).astype(np.uint8) + + +def _parse_urdf_material_colors(root: ET.Element) -> dict[str, np.ndarray]: + material_colors: dict[str, np.ndarray] = {} + for material_element in root.findall("material"): + material_name = material_element.attrib.get("name", "") + color = _parse_urdf_color(material_element.find("color")) + if material_name and color is not None: + material_colors[material_name] = color + return material_colors + + +def _resolve_urdf_visual_color(parent_element: ET.Element, material_colors: dict[str, np.ndarray]) -> np.ndarray | None: + material_element = parent_element.find("material") + if material_element is None: + return None + + material_name = material_element.attrib.get("name", "") + color = _parse_urdf_color(material_element.find("color")) + if color is None and material_name: + color = material_colors.get(material_name) + if color is None: + return None + + color_unit = color.astype(np.float32) / 255.0 + if material_name.lower() in _UNSPECIFIED_MATERIAL_NAMES or _is_unspecified_rgba(color_unit): + return None + return color + + +def _agibot_urdf_asset_roots() -> list[Path]: + """Return candidate roots that can satisfy package://genie_robot_description assets.""" + + roots: list[Path] = [] + env_urdf_root = os.environ.get("AGIBOT_GEAR_URDF_ROOT") + if env_urdf_root: + roots.append(Path(env_urdf_root)) + env_mesh_root = os.environ.get("AGIBOT_GEAR_MESH_ROOT") + if env_mesh_root: + roots.append(Path(env_mesh_root)) + roots.extend( + [ + get_embodiment_c_urdf_path().parent, + _AGIBOT_GEAR_LUSTRE_URDF_ROOT, + _AGIBOT_GEAR_LOCAL_SAMPLE_URDF_ROOT, + ] + ) + + unique_roots: list[Path] = [] + seen: set[Path] = set() + for root in roots: + resolved = root.expanduser() + if resolved in seen: + continue + seen.add(resolved) + unique_roots.append(resolved) + return unique_roots + + +def _resolve_agibot_mesh_path(filename: str) -> Path: + if filename.startswith("package://genie_robot_description/"): + rel = filename.removeprefix("package://genie_robot_description/") + candidates: list[Path] = [] + for root in _agibot_urdf_asset_roots(): + candidates.append(root / rel) + if rel.startswith("meshes/"): + candidates.append(root / rel.removeprefix("meshes/")) + for candidate in candidates: + if candidate.is_file(): + return candidate + return candidates[0] + path = Path(filename) + if path.is_absolute(): + return path + return get_embodiment_c_urdf_path().parent / path + + +def _build_urdf_geometry_mesh(geometry_element: ET.Element | None, warn_missing_mesh: bool = False) -> Any | None: + import trimesh + + if geometry_element is None: + return None + + mesh_element = geometry_element.find("mesh") + if mesh_element is not None: + filename = mesh_element.attrib.get("filename") + if not filename: + return None + mesh_path = _resolve_agibot_mesh_path(filename) + if not mesh_path.is_file(): + if warn_missing_mesh: + log.warning(f"AgiBot mesh file missing: {mesh_path}") + return None + try: + mesh = trimesh.load(mesh_path, force="mesh") + if hasattr(mesh, "geometry"): + mesh = trimesh.util.concatenate(tuple(mesh.geometry.values())) + except Exception as exc: + log.warning(f"Failed to load AgiBot mesh {mesh_path}: {exc}") + return None + scale_text = mesh_element.attrib.get("scale") + if scale_text: + mesh.apply_scale([float(value) for value in scale_text.split()]) + return mesh + + box = geometry_element.find("box") + if box is not None: + size = np.asarray([float(value) for value in box.attrib["size"].split()], dtype=np.float32) + return trimesh.creation.box(extents=size) + + sphere = geometry_element.find("sphere") + if sphere is not None: + radius = float(sphere.attrib["radius"]) + return trimesh.creation.icosphere(radius=radius) + + cylinder = geometry_element.find("cylinder") + if cylinder is not None: + radius = float(cylinder.attrib["radius"]) + length = float(cylinder.attrib["length"]) + return trimesh.creation.cylinder(radius=radius, height=length) + + return None + + +@lru_cache(maxsize=1) +def _load_embodiment_c_collision_geometries() -> tuple[AgibotGeometry, ...]: + root = ET.parse(get_embodiment_c_urdf_path()).getroot() + material_colors = _parse_urdf_material_colors(root) + geometries: list[AgibotGeometry] = [] + + for link_element in root.findall("link"): + link_name = link_element.attrib["name"] + visual_elements = link_element.findall("visual") + collision_elements = link_element.findall("collision") + # Prefer visual meshes when the real asset files are available. Collision + # primitives remain the fallback for environments without AgiBot mesh assets. + for elements, warn_missing_meshes in ((visual_elements, False), (collision_elements, False)): + loaded_before = len(geometries) + for geometry_idx, parent_element in enumerate(elements): + geometry_element = parent_element.find("geometry") + mesh = _build_urdf_geometry_mesh(geometry_element, warn_missing_mesh=warn_missing_meshes) + if mesh is None: + continue + geometry_name = f"{link_name}_geometry_{geometry_idx}" + face_color = _resolve_urdf_visual_color(parent_element, material_colors) + if face_color is None: + face_color = _fallback_mesh_color(geometry_name, geometry_idx) + _apply_mesh_color(mesh, face_color) + local_transform = _parse_urdf_origin(parent_element.find("origin")) + geometries.append( + AgibotGeometry( + name=geometry_name, + link_name=link_name, + mesh=mesh, + local_transform=local_transform, + ) + ) + if len(geometries) > loaded_before: + break + + return tuple(geometries) + + +def get_embodiment_c_collision_geometries() -> list[AgibotGeometry]: + """Return cached AgiBot primitive collision geometries from the committed G1 URDF.""" + + return list(_load_embodiment_c_collision_geometries()) + + +# ── Helpers ────────────────────────────────────────────────────────────────── + + +def _get_visual_geom_ids(model) -> list[int]: + """Get visual geom IDs from a MuJoCo model. + + Strategy: + 1. Google Robot uses *_v suffix for visual meshes → use those + 2. Other robots (Franka, WidowX) use geom_group=2 for visual → use those + 3. Fallback: all mesh geoms + + Includes both mesh geoms and primitive geoms (box, cylinder, sphere) in group 2. + """ + import mujoco + + VISUAL_TYPES = { + mujoco.mjtGeom.mjGEOM_MESH, + mujoco.mjtGeom.mjGEOM_BOX, + mujoco.mjtGeom.mjGEOM_CYLINDER, + mujoco.mjtGeom.mjGEOM_SPHERE, + mujoco.mjtGeom.mjGEOM_CAPSULE, + mujoco.mjtGeom.mjGEOM_ELLIPSOID, + } + + v_ids = [] + group2_ids = [] + all_ids = [] + for gi in range(model.ngeom): + gtype = model.geom_type[gi] + if gtype not in VISUAL_TYPES: + continue + all_ids.append(gi) + if gtype == mujoco.mjtGeom.mjGEOM_MESH: + mesh_id = model.geom_dataid[gi] + mesh_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_MESH, mesh_id) or "" + if mesh_name.endswith("_v"): + v_ids.append(gi) + if model.geom_group[gi] == 2: + group2_ids.append(gi) + + # Priority: _v suffix > group 2 > all + if v_ids: + return v_ids + if group2_ids: + return group2_ids + return all_ids + + +def _resolve_geom_color(model, gi: int, geom_name: str) -> np.ndarray: + """Resolve the effective RGBA color for a MuJoCo geom. + + Priority: + 1. If geom has a material with a diffuse texture → sample average texture color + 2. If geom has a material with non-default rgba → use mat_rgba + 3. If geom_rgba is non-default → use geom_rgba + 4. Fallback: deterministic per-geom color + + Returns (4,) uint8 RGBA. + """ + + rgba = model.geom_rgba[gi].copy() # (4,) float [0,1] + mat_id = model.geom_matid[gi] + + if mat_id >= 0: + # Check if material has a diffuse texture + tex_id = model.mat_texid[mat_id][1] # index 1 = diffuse texture + if tex_id >= 0: + # Sample average color from the texture + tex_adr = model.tex_adr[tex_id] + tex_w = model.tex_width[tex_id] + tex_h = model.tex_height[tex_id] + n_pixels = tex_w * tex_h + tex_data = model.tex_data[tex_adr : tex_adr + n_pixels * 3].reshape(n_pixels, 3) + avg_rgb = tex_data.mean(axis=0) / 255.0 + # Multiply by mat_rgba (which acts as a tint) + mat_rgba = model.mat_rgba[mat_id] + final = np.array( + [ + avg_rgb[0] * mat_rgba[0], + avg_rgb[1] * mat_rgba[1], + avg_rgb[2] * mat_rgba[2], + mat_rgba[3], + ] + ) + return (np.clip(final, 0, 1) * 255).astype(np.uint8) + + # No texture — use material rgba if it is informative. + mat_rgba = model.mat_rgba[mat_id].copy() + if not _is_unspecified_rgba(mat_rgba): + return (np.clip(mat_rgba, 0, 1) * 255).astype(np.uint8) + + if not _is_unspecified_rgba(rgba): + return (np.clip(rgba, 0, 1) * 255).astype(np.uint8) + + return _fallback_mesh_color(geom_name, gi) + + +def _extract_mujoco_meshes(model, data) -> list: + """Extract visual geom meshes from a MuJoCo model as trimesh objects. + + Uses the same visual geom filtering as ``ik_solver._get_visual_geom_ids`` + to ensure mesh ordering matches IK transform output. + + For geoms with primitive types (box, cylinder, etc.), generates the + corresponding trimesh primitive. + + Applies per-geom RGBA color from MuJoCo materials when available. + + Returns list of (geom_name, trimesh.Trimesh, 4x4_transform). + """ + import mujoco + import trimesh + + # Use the same visual filter as IK solver + visual_geom_ids = _get_visual_geom_ids(model) + + meshes = [] + for gi in visual_geom_ids: + gtype = model.geom_type[gi] + pos = data.geom_xpos[gi].copy() + rot = data.geom_xmat[gi].reshape(3, 3).copy() + + geom_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_GEOM, gi) or f"geom_{gi}" + face_color = _resolve_geom_color(model, gi, geom_name) + + mesh = None + if gtype == mujoco.mjtGeom.mjGEOM_MESH: + mesh_id = model.geom_dataid[gi] + vert_start = model.mesh_vertadr[mesh_id] + vert_count = model.mesh_vertnum[mesh_id] + face_start = model.mesh_faceadr[mesh_id] + face_count = model.mesh_facenum[mesh_id] + verts = model.mesh_vert[vert_start : vert_start + vert_count].copy() + faces = model.mesh_face[face_start : face_start + face_count].copy() + mesh = trimesh.Trimesh(vertices=verts, faces=faces) + _apply_mesh_color(mesh, face_color) + elif gtype == mujoco.mjtGeom.mjGEOM_BOX: + size = model.geom_size[gi].copy() + mesh = trimesh.creation.box(extents=size * 2) + _apply_mesh_color(mesh, face_color) + elif gtype == mujoco.mjtGeom.mjGEOM_SPHERE: + mesh = trimesh.creation.icosphere(radius=model.geom_size[gi][0]) + _apply_mesh_color(mesh, face_color) + elif gtype == mujoco.mjtGeom.mjGEOM_CYLINDER: + mesh = trimesh.creation.cylinder(radius=model.geom_size[gi][0], height=model.geom_size[gi][1] * 2) + _apply_mesh_color(mesh, face_color) + elif gtype == mujoco.mjtGeom.mjGEOM_CAPSULE: + mesh = trimesh.creation.capsule(radius=model.geom_size[gi][0], height=model.geom_size[gi][1] * 2) + _apply_mesh_color(mesh, face_color) + + if mesh is not None: + transform = np.eye(4) + transform[:3, :3] = rot + transform[:3, 3] = pos + meshes.append((geom_name, mesh, transform)) + return meshes + + +def get_mujoco_to_pinocchio_world_transform(model, data, robot_name: str | None = None) -> np.ndarray: + """Return a correction that maps MuJoCo world poses into Pinocchio world. + + For some MJCFs, MuJoCo includes a fixed worldbody -> root-body transform + that Pinocchio's ``buildModelFromMJCF()`` omits. When that happens, the IK + solver and dataset poses live in a root-free world, while raw MuJoCo + body/site poses are globally shifted. This helper returns the inverse root + transform so callers can strip that offset from MuJoCo-derived meshes, + body frames, and sites before comparing them against Pinocchio / dataset + poses. + """ + import mujoco + + cfg = ROBOT_CONFIGS.get(robot_name, {}) if robot_name else {} + root_body_name = cfg.get("pinocchio_removed_root_body") + if not root_body_name: + return np.eye(4, dtype=np.float32) + + root_body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, root_body_name) + if root_body_id < 0: + log.warning(f"Configured root body '{root_body_name}' not found in MuJoCo model") + return np.eye(4, dtype=np.float32) + + root_pose = np.eye(4, dtype=np.float32) + root_pose[:3, 3] = data.xpos[root_body_id].astype(np.float32) + root_pose[:3, :3] = data.xmat[root_body_id].reshape(3, 3).astype(np.float32) + return np.linalg.inv(root_pose).astype(np.float32) + + +def _apply_world_transform_to_meshes( + meshes: list[tuple[str, object, np.ndarray]], + world_transform: np.ndarray, +) -> list[tuple[str, object, np.ndarray]]: + """Left-multiply a world-space transform into each mesh pose.""" + transformed_meshes = [] + for name, mesh, transform in meshes: + transformed_meshes.append((name, mesh, (world_transform @ transform).astype(np.float32))) + return transformed_meshes + + +# ── Public API ─────────────────────────────────────────────────────────────── + + +def get_robot_loaders() -> dict[str, callable]: + """Get the robot loader registry. + + Maps robot_name → loader function that returns (meshes, ee_home_pose). + """ + return { + "google_robot": _load_google_robot, + "franka_panda": _load_franka_panda, + "widowx": _load_widowx, + "ur5e": _load_ur5e, + "embodiment_c": _load_embodiment_c, + } + + +def extract_gripper_openings(unified_57d: np.ndarray, robot_name: str = "google_robot") -> np.ndarray: + """Extract gripper opening fractions from unified action grasp state. + + Uses fingertip spread (f0-f1 distance) to invert the FK and recover + the scalar gripper opening at each timestep. + + Args: + unified_57d: (T, 57) unified action. + robot_name: Robot identifier for FK lookup table. + + Returns: + (T+1,) array of gripper openings in [0, 1]. + """ + T = unified_57d.shape[0] + grasp = unified_57d[:, 18:33].reshape(T, 5, 3) + all_grasp = np.concatenate([grasp[0:1], grasp], axis=0) + + # Build monotonic inverse lookup from FK (robot-specific) + _gs = np.linspace(0, 1, 10001).astype(np.float32) + try: + if robot_name == "franka_panda": + from cosmos.data.vfm.action.robot_descriptions.franka import franka_fingertip_fk + + _tips = franka_fingertip_fk(_gs) + elif robot_name == "widowx": + from cosmos.data.vfm.action.robot_descriptions.widowx import widowx_fingertip_fk + + _tips = widowx_fingertip_fk(_gs) + elif robot_name == "ur5e": + from cosmos.data.vfm.action.robot_descriptions.umi import _WSG50_MAX_WIDTH, umi_fingertip_fk + + _tips = umi_fingertip_fk(_gs * _WSG50_MAX_WIDTH) + else: + from cosmos.data.vfm.action.robot_descriptions.google_robot import ( + google_robot_fingertip_fk_vectorized, + ) + + _tips = google_robot_fingertip_fk_vectorized(_gs) + _spreads = np.linalg.norm(_tips[:, 0] - _tips[:, 1], axis=1) + except ImportError: + # Fallback: linear approximation + _spreads = _gs * 0.145 + _min_idx = int(np.argmin(_spreads)) + mono_gs = _gs[_min_idx:] + mono_spreads = _spreads[_min_idx:] + + openings = np.zeros(T + 1, dtype=np.float32) + for t in range(T + 1): + f0, f1 = all_grasp[t, 0], all_grasp[t, 1] + spread = np.linalg.norm(f0 - f1) + if spread <= mono_spreads[0]: + openings[t] = 0.0 + elif spread >= mono_spreads[-1]: + openings[t] = 1.0 + else: + openings[t] = float(np.interp(spread, mono_spreads, mono_gs)) + return openings diff --git a/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/viewer.py b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/viewer.py new file mode 100644 index 00000000..26e1a7f2 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/urdf_visualizer/viewer.py @@ -0,0 +1,1016 @@ +#!/usr/bin/env python +"""Interactive 3D viewer for robot action datasets. + +Uses the unified 57D action representation: every dataset declares one explicit +raw ``ActionFormat`` (9D/10D/20D/57D), which is converted to +``UnifiedAction(action_57d, mask)`` before rendering. + +**57D layout**: ``[ego(9) | R_wrist(9) | R_fingers(15) | L_wrist(9) | L_fingers(15)]`` + +Dependencies:: + + pip install viser mujoco pin + +Usage: + # Use each dataset's declared raw action format: + uv run python cosmos/data/vfm/action/urdf_visualizer/viewer.py --share + + # Override the raw action format explicitly: + uv run python cosmos/data/vfm/action/urdf_visualizer/viewer.py --action-format 57d --share +""" + +from __future__ import annotations + +import argparse +import importlib +import os +import random +import sys +import time as _time +from dataclasses import dataclass, field +from functools import lru_cache +from pathlib import Path +from typing import Any, cast + +import numpy as np +import torch + +from cosmos.utils import log +from cosmos.data.vfm.action.urdf_visualizer.unified_action import ActionFormat + +_REPO_ROOT = str(Path(__file__).resolve().parents[6]) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +# ── Dataset Registry ────────────────────────────────────────────────────────── + + +@dataclass +class DatasetEntry: + """Metadata for a dataset available in the viewer.""" + + name: str + robot_name: str + max_finger_width: float + fps: int + pose_convention: str = "backward_framewise" + camera_fov_deg: float = 60.0 + camera_aspect: float = 4 / 3 + dataset_class: str = "" + dataset_kwargs: dict[str, Any] = field(default_factory=dict) + action_format: ActionFormat = ActionFormat.SINGLE_ARM_10D + dual_base_left: np.ndarray | None = None + dual_base_right: np.ndarray | None = None + robot_embodiment_type: str | None = None + to_unified_fn: str | None = None # "module.path:function" for custom action conversion + + +def _lazycfg_to_entry( + cfg: Any, + *, + robot_name: str = "", + max_finger_width: float = 0.0, + fps: int = 10, + action_format: ActionFormat = ActionFormat.SINGLE_ARM_10D, + camera_fov_deg: float = 60.0, + camera_aspect: float = 4 / 3, + dual_base_left: np.ndarray | None = None, + dual_base_right: np.ndarray | None = None, + robot_embodiment_type: str | None = None, + to_unified_fn: str | None = None, + viewer_overrides: dict[str, Any] | None = None, +) -> DatasetEntry: + """Build a viewer dataset entry from a v1p2 ``LazyCall(dataset_entry)`` config.""" + + from omegaconf import OmegaConf + + from cosmos.utils.lazy_config.registry import convert_target_to_string + + ds_cfg = cfg.dataset + target = ds_cfg._target_ + dataset_class = target if isinstance(target, str) else convert_target_to_string(target) + dataset_kwargs = { + key: value for key, value in OmegaConf.to_container(ds_cfg, resolve=False).items() if key != "_target_" + } + dataset_kwargs["action_normalization"] = None + if viewer_overrides is not None: + dataset_kwargs.update(viewer_overrides) + + pose_convention = str(dataset_kwargs.get("pose_convention", "backward_framewise")) + cfg_dict = OmegaConf.to_container(cfg, resolve=False) + dataset_name = str(cfg_dict.get("name", "unknown")) if isinstance(cfg_dict, dict) else "unknown" + return DatasetEntry( + name=dataset_name, + robot_name=robot_name, + max_finger_width=max_finger_width, + fps=fps, + pose_convention=pose_convention, + camera_fov_deg=camera_fov_deg, + camera_aspect=camera_aspect, + dataset_class=dataset_class, + dataset_kwargs=dataset_kwargs, + action_format=action_format, + dual_base_left=dual_base_left, + dual_base_right=dual_base_right, + robot_embodiment_type=robot_embodiment_type, + to_unified_fn=to_unified_fn, + ) + + +def _build_datasets() -> dict[str, DatasetEntry]: + """Build the viewer dataset registry from ``action_datasets_v1p2``.""" + + from experiments.action.midtrain_config.action_datasets_v1p2 import ( + DATASET_EMBODIMENT_C_GRIPPER_480, + DATASET_EMBODIMENT_C_GRIPPER_EXT_480, + DATASET_AGIBOTWORLD_BETA_480, + DATASET_AV_480, + DATASET_BRIDGE_480, + DATASET_CAMERA_480, + DATASET_DROID_480, + DATASET_FRACTAL_256, + DATASET_HAND_POSE_480, + DATASET_ROBOMIND_FRANKA_480, + DATASET_ROBOMIND_FRANKA_DUAL_480, + DATASET_ROBOMIND_UR_480, + DATASET_UMI_256, + ) + + raw_action_override = {"action_normalization": None} + credential_override = {"credential_path": "credentials/gcp_training.secret"} + agibot_unifier = ( + "cosmos.data.vfm.action.urdf_visualizer.unified_action:to_unified_from_embodiment_c_fk_action" + ) + + return { + "fractal": _lazycfg_to_entry( + DATASET_FRACTAL_256, + robot_name="google_robot", + max_finger_width=0.05, + fps=3, + action_format=ActionFormat.SINGLE_ARM_10D, + camera_fov_deg=69.0, + camera_aspect=320 / 256, + viewer_overrides=raw_action_override, + ), + "droid": _lazycfg_to_entry( + DATASET_DROID_480, + robot_name="franka_panda", + max_finger_width=0.08, + fps=15, + action_format=ActionFormat.SINGLE_ARM_10D, + viewer_overrides=raw_action_override, + ), + "bridge": _lazycfg_to_entry( + DATASET_BRIDGE_480, + robot_name="widowx", + max_finger_width=0.06, + fps=5, + action_format=ActionFormat.SINGLE_ARM_10D, + viewer_overrides=raw_action_override, + ), + "robomind_franka": _lazycfg_to_entry( + DATASET_ROBOMIND_FRANKA_480, + robot_name="franka_panda", + max_finger_width=0.08, + fps=10, + action_format=ActionFormat.SINGLE_ARM_10D, + viewer_overrides=raw_action_override, + ), + "robomind_franka_dual": _lazycfg_to_entry( + DATASET_ROBOMIND_FRANKA_DUAL_480, + robot_name="franka_panda", + max_finger_width=0.08, + fps=10, + action_format=ActionFormat.DUAL_ARM_20D, + dual_base_left=np.array( + [[1, 0, 0, 0.0], [0, 1, 0, 0.3], [0, 0, 1, 0.0], [0, 0, 0, 1.0]], + dtype=np.float32, + ), + dual_base_right=np.array( + [[1, 0, 0, 0.0], [0, 1, 0, -0.3], [0, 0, 1, 0.0], [0, 0, 0, 1.0]], + dtype=np.float32, + ), + viewer_overrides=raw_action_override, + ), + "robomind_ur": _lazycfg_to_entry( + DATASET_ROBOMIND_UR_480, + robot_name="ur5e", + max_finger_width=0.085, + fps=10, + action_format=ActionFormat.SINGLE_ARM_10D, + viewer_overrides=raw_action_override, + ), + "umi": _lazycfg_to_entry( + DATASET_UMI_256, + robot_name="", + max_finger_width=0.08, + fps=10, + action_format=ActionFormat.SINGLE_ARM_10D, + viewer_overrides={**raw_action_override, "normalizer_dir": ""}, + ), + "camera": _lazycfg_to_entry( + DATASET_CAMERA_480, + robot_name="", + max_finger_width=0.0, + fps=10, + action_format=ActionFormat.EGO_9D, + viewer_overrides={**raw_action_override, **credential_override}, + ), + "av": _lazycfg_to_entry( + DATASET_AV_480, + robot_name="", + max_finger_width=0.0, + fps=10, + action_format=ActionFormat.EGO_9D, + viewer_overrides={**raw_action_override, **credential_override}, + ), + "hand_pose": _lazycfg_to_entry( + DATASET_HAND_POSE_480, + robot_name="", + max_finger_width=0.0, + fps=15, + action_format=ActionFormat.UNIFIED_57D, + viewer_overrides={**raw_action_override, "return_overlay_data": True}, + ), + "embodiment_c_gripper": _lazycfg_to_entry( + DATASET_EMBODIMENT_C_GRIPPER_480, + robot_name="embodiment_c", + max_finger_width=0.12, + fps=10, + camera_fov_deg=69.0, + camera_aspect=640 / 480, + viewer_overrides={**raw_action_override, "return_agibot_link_poses": True}, + robot_embodiment_type="embodiment_c_gripper", + to_unified_fn=agibot_unifier, + ), + "embodiment_c_gripper_ext": _lazycfg_to_entry( + DATASET_EMBODIMENT_C_GRIPPER_EXT_480, + robot_name="embodiment_c", + max_finger_width=0.12, + fps=10, + camera_fov_deg=69.0, + camera_aspect=640 / 480, + viewer_overrides={**raw_action_override, "return_agibot_link_poses": True}, + robot_embodiment_type="embodiment_c_gripper_ext", + to_unified_fn=agibot_unifier, + ), + "agibotworld_beta": _lazycfg_to_entry( + DATASET_AGIBOTWORLD_BETA_480, + robot_name="embodiment_c", + max_finger_width=0.12, + fps=10, + camera_fov_deg=69.0, + camera_aspect=640 / 480, + viewer_overrides={**raw_action_override, "return_agibot_link_poses": True}, + robot_embodiment_type="embodiment_c_gripper", + to_unified_fn=agibot_unifier, + ), + } + + +DATASETS: dict[str, DatasetEntry] = {} + + +# ── Dataset Creation ────────────────────────────────────────────────────────── + + +def _create_dataset(entry: DatasetEntry, chunk_length: int): + """Instantiate a dataset class for the given entry.""" + import importlib + import inspect + + module_path, class_name = entry.dataset_class.rsplit(".", 1) + mod = importlib.import_module(module_path) + cls = getattr(mod, class_name) + + kwargs = dict(entry.dataset_kwargs) + kwargs["chunk_length"] = chunk_length + kwargs["split"] = "full" + kwargs["mode"] = "policy" + kwargs["enable_fast_init"] = True + + # UMI: factory function + if callable(cls) and not inspect.isclass(cls): + _OMEGACONF_BLOCKLIST = {"chunk_length", "split", "action_normalization", "enable_fast_init"} + kwargs = {k: v for k, v in kwargs.items() if k not in _OMEGACONF_BLOCKLIST} + kwargs["eager_load"] = True + return cls(**kwargs) + + sig = inspect.signature(cls.__init__) + valid_params = set(sig.parameters.keys()) - {"self"} + has_var_keyword = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) + if not has_var_keyword: + kwargs = {k: v for k, v in kwargs.items() if k in valid_params} + + dataset = cls(**kwargs) + + if hasattr(dataset, "_register_sources"): + dataset._register_sources() + if hasattr(dataset, "__len__") and len(dataset) == 0: + raise RuntimeError(f"{entry.name}: registered sources but found no valid samples") + + from torch.utils.data import IterableDataset as _IterableBase + + if isinstance(dataset, _IterableBase): + dataset = _IterableToMapDataset(dataset) + + return dataset + + +def _resolve_action_format( + entry: DatasetEntry, + action_format_override: ActionFormat | None, +) -> ActionFormat: + """Return the explicit raw action format for one dataset load.""" + return action_format_override if action_format_override is not None else entry.action_format + + +@lru_cache(maxsize=None) +def _load_symbol(target: str): + """Import and cache one ``module:symbol`` reference.""" + + module_name, symbol_name = target.split(":", maxsplit=1) + module = importlib.import_module(module_name) + return getattr(module, symbol_name) + + +def _format_sample_text(value: Any, max_chars: int | None = None) -> str: + """Format optional sample text for the viewer info panel.""" + + if value is None: + return "" + text = value if isinstance(value, str) else str(value) + if not text: + return "" + if max_chars is None: + return text + return text[:max_chars] + + +def _build_viewer_idle_action_spec(action_format: ActionFormat) -> Any: + """Build a fallback idle-frame spec from the viewer-declared action format.""" + + from cosmos.data.vfm.action.action_spec import Gripper, Pos, Rot, build_action_spec + + if action_format is ActionFormat.EGO_9D: + return build_action_spec(Pos(prefix="ego"), Rot("rot6d", prefix="ego")) + if action_format is ActionFormat.SINGLE_ARM_10D: + return build_action_spec(Pos(), Rot("rot6d"), Gripper()) + if action_format is ActionFormat.DUAL_ARM_20D: + return build_action_spec( + Pos(prefix="left"), + Rot("rot6d", prefix="left"), + Gripper(prefix="left"), + Pos(prefix="right"), + Rot("rot6d", prefix="right"), + Gripper(prefix="right"), + ) + if action_format is ActionFormat.UNIFIED_57D: + return build_action_spec( + Pos(prefix="ego"), + Rot("rot6d", prefix="ego"), + Pos(prefix="right_wrist"), + Rot("rot6d", prefix="right_wrist"), + Pos(dim=15, prefix="right_fingers"), + Pos(prefix="left_wrist"), + Rot("rot6d", prefix="left_wrist"), + Pos(dim=15, prefix="left_fingers"), + ) + raise ValueError(f"Unsupported action format for idle-frame detection: {action_format}") + + +def _compute_viewer_idle_frames( + action: Any, + dataset: Any, + action_format: ActionFormat, +) -> torch.Tensor | None: + """Compute idle frames for a viewer sample when the dataset did not provide them.""" + + action_spec = getattr(dataset, "action_spec", None) + compute_idle_frames_method = getattr(dataset, "_compute_idle_frames", None) + if action_spec is not None and compute_idle_frames_method is not None: + return compute_idle_frames_method(action) + + from cosmos.data.vfm.action.pose_utils import compute_idle_frames + + spec = _build_viewer_idle_action_spec(action_format) + try: + idle_frames = compute_idle_frames(action, spec) + except (TypeError, ValueError) as error: + log.warning(f"Viewer idle-frame detection skipped for {action_format.value}: {error}") + return None + return torch.tensor(idle_frames, dtype=torch.long) # [] + + +@lru_cache(maxsize=1) +def _get_viewer_idle_frames_augmentor() -> Any: + """Return the caption augmentor used by the viewer idle-frame path.""" + + from cosmos.data.vfm.augmentors.idle_frames_text_info import IdleFramesTextInfo + + return IdleFramesTextInfo( + input_keys=["ai_caption", "idle_frames", "action"], + output_keys=["ai_caption"], + args={ + "caption_key": "ai_caption", + "idle_frames_key": "idle_frames", + "action_key": "action", + "dropout_rate": 0.0, + "enabled": True, + }, + ) + + +def _enable_viewer_idle_frames(sample: dict[str, Any], dataset: Any, action_format: ActionFormat) -> dict[str, Any]: + """Populate idle-frame metadata and append text in the direct viewer data path.""" + + updated_sample = sample + idle_frames = updated_sample.get("idle_frames") + action = updated_sample.get("action") + if idle_frames is None and action is not None: + idle_frames = _compute_viewer_idle_frames(action, dataset, action_format) + if idle_frames is not None: + updated_sample = dict(updated_sample) + updated_sample["idle_frames"] = idle_frames + + if idle_frames is None: + return updated_sample + + updated_sample = dict(updated_sample) + caption = updated_sample.get("ai_caption") + if isinstance(caption, dict): + updated_sample["ai_caption"] = dict(caption) + + augmented_sample = _get_viewer_idle_frames_augmentor()(updated_sample) + return updated_sample if augmented_sample is None else augmented_sample + + +class _IterableToMapDataset: + """Wraps an IterableDataset into a random-access dataset with lazy loading.""" + + _MAX_CACHE = 200 + + def __init__(self, iterable_dataset, max_samples: int | None = None): + self._iter = iter(iterable_dataset) + self._samples: list[dict] = [] + self._exhausted = False + self._max = max_samples or self._MAX_CACHE + self._ds_name = iterable_dataset.__class__.__name__ + log.info(f"Lazy wrapper: {self._ds_name} (max {self._max})") + + def _fetch_up_to(self, idx: int) -> bool: + while len(self._samples) <= idx and not self._exhausted and len(self._samples) < self._max: + try: + self._samples.append(next(self._iter)) + log.info(f"{self._ds_name}: fetched sample {len(self._samples) - 1}") + except StopIteration: + self._exhausted = True + log.info(f"{self._ds_name}: exhausted at {len(self._samples)}") + return idx < len(self._samples) + + def __len__(self): + return max(len(self._samples), 1) + + def __getitem__(self, idx): + if self._fetch_up_to(idx): + return self._samples[idx] + if self._samples: + return self._samples[idx % len(self._samples)] + raise IndexError(f"{self._ds_name}: no samples available") + + +# ── Viewer ──────────────────────────────────────────────────────────────────── + + +def _collect_scene_points(state) -> np.ndarray: + """Collect all visible trajectory positions for camera fitting.""" + points: list[np.ndarray] = [] + for poses in (state.ego_poses, state.right_poses, state.left_poses): + if poses is not None and len(poses) > 0: + points.append(poses[:, :3, 3].astype(np.float32)) + if not points: + return np.zeros((1, 3), dtype=np.float32) + return np.concatenate(points, axis=0) + + +def _get_observation_up_direction(state, view_forward: np.ndarray) -> np.ndarray: + """Estimate a stable viewer up-direction from the observation camera poses.""" + if state.ego_poses is None or len(state.ego_poses) == 0: + return np.array([0.0, 0.0, 1.0], dtype=np.float32) + + # Ego poses are camera-to-world transforms in OpenCV camera convention, + # where image up corresponds to the negative camera Y axis. + camera_up = -state.ego_poses[:, :3, 1].astype(np.float32) + reference = camera_up[0] + aligned_up = camera_up.copy() + for idx in range(1, len(aligned_up)): + if float(np.dot(aligned_up[idx], reference)) < 0.0: + aligned_up[idx] *= -1.0 + + up_direction = aligned_up.mean(axis=0) + up_direction -= view_forward * float(np.dot(up_direction, view_forward)) + up_norm = float(np.linalg.norm(up_direction)) + if up_norm < 1e-6: + return np.array([0.0, 0.0, 1.0], dtype=np.float32) + return up_direction / up_norm + + +def _get_observation_forward_direction(state) -> np.ndarray | None: + """Estimate a stable viewer forward direction from the observation camera poses.""" + if state.ego_poses is None or len(state.ego_poses) == 0: + return None + + # Ego poses are camera-to-world transforms in OpenCV camera convention, + # where the optical axis points along the positive camera Z axis. + camera_forward = state.ego_poses[:, :3, 2].astype(np.float32) + reference = camera_forward[0] + aligned_forward = camera_forward.copy() + for idx in range(1, len(aligned_forward)): + if float(np.dot(aligned_forward[idx], reference)) < 0.0: + aligned_forward[idx] *= -1.0 + + forward_direction = aligned_forward.mean(axis=0) + forward_norm = float(np.linalg.norm(forward_direction)) + if forward_norm < 1e-6: + return None + return forward_direction / forward_norm + + +def _reset_camera_to_trajectory(client, state, camera_fov_deg: float) -> None: + """Frame one client's viewport using the current trajectory extent.""" + points = _collect_scene_points(state) + center = points.mean(axis=0) + extent = points - center[None, :] + radius = float(np.linalg.norm(extent, axis=1).max()) if len(points) > 0 else 0.0 + radius = max(radius, 0.15) + + fov_rad = float(np.deg2rad(camera_fov_deg)) + fit_distance = radius / max(np.tan(fov_rad / 2.0), 0.35) + view_forward = _get_observation_forward_direction(state) + if view_forward is None: + view_dir = np.array([1.0, -1.0, 0.7], dtype=np.float32) + view_dir /= np.linalg.norm(view_dir) + camera_position = center + view_dir * max(fit_distance * 1.35, 0.5) + view_forward = center - camera_position + view_forward /= np.linalg.norm(view_forward) + else: + camera_position = center - view_forward * max(fit_distance * 1.35, 0.5) + view_forward = center - camera_position + view_forward /= np.linalg.norm(view_forward) + up_direction = _get_observation_up_direction(state, view_forward) + camera = client.camera + + # Camera state arrives asynchronously from the browser. Wait briefly so we can + # update only this client's viewport instead of broadcasting a global reset target. + deadline = _time.time() + 1.0 + while getattr(camera._state, "update_timestamp", 0.0) == 0.0 and _time.time() < deadline: + _time.sleep(0.01) + if getattr(camera._state, "update_timestamp", 0.0) == 0.0: + return + + camera.fov = fov_rad + camera.up_direction = tuple(up_direction.tolist()) + camera.look_at = tuple(center.tolist()) + # Setting position also translates look_at, so restore the target afterwards. + camera.position = tuple(camera_position.tolist()) + camera.look_at = tuple(center.tolist()) + client.flush() + + +def launch_viewer( + port: int = 8013, + share: bool = False, + chunk_length: int = 16, + action_format_override: ActionFormat | None = None, +) -> None: + """Launch the interactive dataset viewer.""" + global DATASETS + import threading as _threading + + import viser + + from cosmos.data.vfm.action.urdf_visualizer.unified_action import ( + build_scene_state, + get_video_from_sample, + to_unified, + ) + from cosmos.data.vfm.action.urdf_visualizer.unified_renderer import UnifiedRenderer + + server = viser.ViserServer(host="0.0.0.0", port=port) + if not DATASETS: + DATASETS = _build_datasets() + datasets = DATASETS + dataset_cache: dict[str, Any] = {} + dataset_locks: dict[str, Any] = {} + dataset_cache_lock = _threading.Lock() + sessions_lock = _threading.Lock() + + def _get_dataset_lock(cache_key: str) -> Any: + """Return the per-dataset lock for a cache key.""" + with dataset_cache_lock: + lock = dataset_locks.get(cache_key) + if lock is None: + lock = _threading.Lock() + dataset_locks[cache_key] = lock + return lock + + @dataclass + class ViewerSession: + client: Any + renderer: Any + time_slider: Any + speed_slider: Any + play_button: Any + action_text: Any + show: dict[str, bool | float] + load_lock: Any = field(default_factory=_threading.Lock) + playing: bool = False + last_frame_time: float = 0.0 + + sessions: dict[int, ViewerSession] = {} + + @server.on_client_connect + def _(client) -> None: + client.scene.reset() + client.scene.set_up_direction("+z") + client.gui.reset() + + renderer = UnifiedRenderer(client) + + with client.gui.add_folder("Dataset"): + ds_dropdown = client.gui.add_dropdown( + "Dataset", options=list(datasets.keys()), initial_value=list(datasets.keys())[0] + ) + ep_input = client.gui.add_number("Episode", initial_value=0, min=0, step=1) + random_button = client.gui.add_button("🎲 Random episode") + status_text = client.gui.add_markdown("*Ready*") + info_text = client.gui.add_markdown("") + + with client.gui.add_folder("Display", expand_by_default=False): + show_robot = client.gui.add_checkbox("Show robot mesh", initial_value=True) + show_frames = client.gui.add_checkbox("Show wrist frames", initial_value=True) + show_traj = client.gui.add_checkbox("Show trajectory", initial_value=True) + show_fingertips = client.gui.add_checkbox("Show fingertips", initial_value=True) + show_ego = client.gui.add_checkbox("Show ego camera", initial_value=True) + axis_scale = client.gui.add_slider("Axis scale", min=0.1, max=20.0, step=0.1, initial_value=1.0) + + robot_frame_toggle_handles: dict[str, Any] = {} + robot_frame_toggle_folder = client.gui.add_folder("Robot Frame Toggles", expand_by_default=False) + with robot_frame_toggle_folder: + robot_frame_toggle_status = client.gui.add_markdown("*Load an episode to choose robot frame coordinates.*") + + with client.gui.add_folder("Playback"): + time_slider = client.gui.add_slider("Time", min=0, max=1, step=1, initial_value=0) + play_button = client.gui.add_button("▶ Play") + speed_slider = client.gui.add_slider("Speed (fps)", min=1, max=30, step=1, initial_value=3) + + cam_panel = client.gui.add_image(np.zeros((64, 64, 3), dtype=np.uint8)) + renderer.set_video_panel(cam_panel) + + with client.gui.add_folder("Action (57D)"): + action_text = client.gui.add_markdown("*No episode loaded*") + + show = { + "frames": True, + "traj": True, + "fingertips": True, + "ego": True, + "robot": True, + "robot_frame_filters": {}, + "axis_scale": 1.0, + } + session = ViewerSession( + client=client, + renderer=renderer, + time_slider=time_slider, + speed_slider=speed_slider, + play_button=play_button, + action_text=action_text, + show=show, + ) + + def _update_action_text(t: int) -> None: + """Update the 57D action display for one client.""" + txt = renderer.format_action_text(t) + action_text.content = txt if txt else "*No data*" + + def _clear_robot_frame_toggles() -> None: + """Remove the dynamic per-frame toggle controls.""" + for handle in robot_frame_toggle_handles.values(): + handle.remove() + robot_frame_toggle_handles.clear() + + def _rebuild_robot_frame_toggles() -> None: + """Rebuild the GUI toggles for the currently loaded robot frames.""" + _clear_robot_frame_toggles() + selectors = renderer.get_robot_frame_selectors() + if not selectors: + show["robot_frame_filters"] = {} + robot_frame_toggle_status.content = "*No robot frame coordinates available for this episode.*" + return + + prev_filters = cast(dict[str, bool], show.get("robot_frame_filters", {})) + show["robot_frame_filters"] = { + selector_key: prev_filters.get(selector_key, False) for selector_key, _ in selectors + } + robot_frame_toggle_status.content = "*Choose which robot frame coordinates to show.*" + + with robot_frame_toggle_folder: + for selector_key, label in selectors: + checkbox = client.gui.add_checkbox( + label, + initial_value=cast(dict[str, bool], show["robot_frame_filters"])[selector_key], + ) + robot_frame_toggle_handles[selector_key] = checkbox + + @checkbox.on_update + def _(_, selector_key: str = selector_key, checkbox: Any = checkbox) -> None: + cast(dict[str, bool], show["robot_frame_filters"])[selector_key] = bool(checkbox.value) + renderer.update(time_slider.value, show) + + def do_load_episode() -> None: + t_start = _time.time() + ds_name = ds_dropdown.value + entry = datasets[ds_name] + effective_action_format = _resolve_action_format(entry, action_format_override) + ep_idx = max(int(ep_input.value), 0) + cache_key = ds_name + + status_text.content = f"⏳ Loading {ds_name} episode {ep_idx}..." + + try: + with _get_dataset_lock(cache_key): + dataset: Any + with dataset_cache_lock: + dataset = cast(Any, dataset_cache.get(cache_key)) + if dataset is None: + status_text.content = f"⏳ Creating {ds_name} dataset..." + dataset = _create_dataset(entry, chunk_length) + with dataset_cache_lock: + dataset_cache[cache_key] = dataset + to_opencv = getattr(dataset, "_to_opencv", np.eye(3, dtype=np.float32)) + + n_total = len(dataset) + if ep_idx >= n_total: + if isinstance(dataset, _IterableToMapDataset) and not dataset._exhausted: + pass + else: + ep_idx = n_total - 1 + ep_input.value = ep_idx + + sample: Any = _enable_viewer_idle_frames(dataset[ep_idx], dataset, effective_action_format) + + action_tensor = sample["action"] + action_raw = ( + action_tensor.numpy() if isinstance(action_tensor, torch.Tensor) else np.asarray(action_tensor) + ) + + uses_dual_initial_pose = effective_action_format is ActionFormat.DUAL_ARM_20D + + initial_pose_t = sample.get("initial_pose") + if initial_pose_t is None: + initial_pose = np.eye(4, dtype=np.float32) + elif isinstance(initial_pose_t, torch.Tensor): + initial_pose = initial_pose_t.numpy().astype(np.float32) + else: + initial_pose = np.asarray(initial_pose_t, dtype=np.float32) + + initial_pose_right_t = sample.get("initial_pose_right") + initial_pose_left_t = sample.get("initial_pose_left") + initial_pose_right = None + initial_pose_left = None + if initial_pose_right_t is not None: + initial_pose_right = ( + initial_pose_right_t.numpy().astype(np.float32) + if isinstance(initial_pose_right_t, torch.Tensor) + else np.asarray(initial_pose_right_t, dtype=np.float32) + ) + if initial_pose_left_t is not None: + initial_pose_left = ( + initial_pose_left_t.numpy().astype(np.float32) + if isinstance(initial_pose_left_t, torch.Tensor) + else np.asarray(initial_pose_left_t, dtype=np.float32) + ) + if uses_dual_initial_pose: + if initial_pose_left is None: + initial_pose_left = initial_pose + + if entry.to_unified_fn: + converter = _load_symbol(entry.to_unified_fn) + import inspect as _inspect + + params = _inspect.signature(converter).parameters + embodiment_type = entry.robot_embodiment_type or str( + entry.dataset_kwargs.get("embodiment_type", "") + ) + if "embodiment_type" in params: + unified = converter(sample, embodiment_type=embodiment_type) + elif "kind" in params: + unified = converter(action_raw, kind="gripper") + else: + unified = converter(action_raw) + raw_action_label = "custom" + else: + unified = to_unified(action_raw, action_format=effective_action_format) + raw_action_label = effective_action_format.value + state = build_scene_state( + unified, + initial_pose=initial_pose, + initial_pose_right=initial_pose_right, + initial_pose_left=initial_pose_left, + right_base_pose=entry.dual_base_right, + left_base_pose=entry.dual_base_left, + pose_convention=entry.pose_convention, + sample=sample, + ) + state.video = get_video_from_sample(sample) + + # Inject FK joint configs when the dataset provides them (e.g. UR). + jc = sample.get("joint_configs") + if jc is not None: + state.joint_configs = ( + jc.numpy().astype(np.float32) + if isinstance(jc, torch.Tensor) + else np.asarray(jc, dtype=np.float32) + ) + status_text.content = "⏳ Loading robot animation..." + renderer.load(state, entry, to_opencv=to_opencv) + _rebuild_robot_frame_toggles() + _reset_camera_to_trajectory(client, state, entry.camera_fov_deg) + + T = state.T + time_slider.max = max(T, 1) + time_slider.value = 0 + + ai_caption_text = _format_sample_text(sample.get("ai_caption", ""), max_chars=160) + debug_caption_text = _format_sample_text(sample.get("debug_caption", "")) + t_total = _time.time() - t_start + info_text.content = ( + f"**{ds_name.upper()}** — Episode {ep_idx}\n\n" + + (f"Task: {ai_caption_text}\n\n" if ai_caption_text else "") + + (f"Debug: {debug_caption_text}\n\n" if debug_caption_text else "") + + ( + f"Steps: {T} | Raw: {raw_action_label} ({action_raw.shape[-1]}D) → 57D | " + f"Robot: {entry.robot_name or '—'} | FPS: {entry.fps}" + ) + ) + status_text.content = f"✅ Loaded in {t_total:.1f}s" + log.info(f"Loaded {ds_name} ep {ep_idx}: {ai_caption_text[:60]}, {T} steps, {t_total:.1f}s") + + renderer.update(0, show) + renderer.update_axis_scale(axis_scale.value) + _update_action_text(0) + session.last_frame_time = _time.time() + + except Exception as e: + status_text.content = f"❌ Load failed: {e}" + log.error(f"Load failed for client {client.client_id}: {e}") + import traceback + + traceback.print_exc() + + def _do_load_threaded() -> None: + if not session.load_lock.acquire(blocking=False): + return + + def _run() -> None: + try: + do_load_episode() + finally: + session.load_lock.release() + + _threading.Thread(target=_run, daemon=True).start() + + @ds_dropdown.on_update + def _(_) -> None: + _do_load_threaded() + + @ep_input.on_update + def _(_) -> None: + _do_load_threaded() + + @random_button.on_click + def _(_) -> None: + ds_name = ds_dropdown.value + cache_key = ds_name + with _get_dataset_lock(cache_key): + with dataset_cache_lock: + ds = dataset_cache.get(cache_key) + if ds is None: + ep_input.value = 0 + elif isinstance(ds, _IterableToMapDataset): + ep_input.value = len(ds._samples) + else: + ep_input.value = random.randint(0, max(len(ds) - 1, 0)) + _do_load_threaded() + + @time_slider.on_update + def _(_) -> None: + renderer.update(time_slider.value, show) + _update_action_text(time_slider.value) + + @show_robot.on_update + def _(_) -> None: + show["robot"] = show_robot.value + renderer.update(time_slider.value, show) + + @show_frames.on_update + def _(_) -> None: + show["frames"] = show_frames.value + renderer.update(time_slider.value, show) + + @show_traj.on_update + def _(_) -> None: + show["traj"] = show_traj.value + renderer.update(time_slider.value, show) + + @show_fingertips.on_update + def _(_) -> None: + show["fingertips"] = show_fingertips.value + renderer.update(time_slider.value, show) + + @show_ego.on_update + def _(_) -> None: + show["ego"] = show_ego.value + renderer.update(time_slider.value, show) + + @axis_scale.on_update + def _(_) -> None: + show["axis_scale"] = axis_scale.value + renderer.update_axis_scale(axis_scale.value) + + @play_button.on_click + def _(_) -> None: + session.playing = not session.playing + session.last_frame_time = _time.time() + play_button.label = "⏸ Pause" if session.playing else "▶ Play" + + with sessions_lock: + sessions[client.client_id] = session + _do_load_threaded() + + @server.on_client_disconnect + def _(client) -> None: + with sessions_lock: + sessions.pop(client.client_id, None) + + # ── Share URL ── + log.info(f"✅ Viewer ready at http://0.0.0.0:{port}") + if share: + share_url = server.request_share_url() + if share_url: + log.info(f"🌐 Share URL: {share_url}") + try: + with open(os.path.expanduser("~/share_url.txt"), "w") as f: + f.write(share_url + "\n") + except Exception: + pass + + # ── Main Loop ── + try: + while True: + now = _time.time() + with sessions_lock: + active_sessions = list(sessions.values()) + for session in active_sessions: + renderer = session.renderer + if not session.playing or renderer.state is None: + continue + frame_period = 1.0 / max(float(session.speed_slider.value), 1.0) + if now - session.last_frame_time < frame_period: + continue + t = session.time_slider.value + t = (t + 1) % max(renderer.state.T, 1) + session.time_slider.value = t + renderer.update(t, session.show) + txt = renderer.format_action_text(t) + session.action_text.content = txt if txt else "*No data*" + session.last_frame_time = now + _time.sleep(0.02) + except KeyboardInterrupt: + log.info("Shutting down.") + + +def main(): + parser = argparse.ArgumentParser(description="Action dataset viewer (unified 57D)") + parser.add_argument("--port", type=int, default=8013) + parser.add_argument("--share", action="store_true") + parser.add_argument("--chunk-length", type=int, default=16) + parser.add_argument( + "--action-format", + choices=[fmt.value for fmt in ActionFormat], + default=None, + help="Optional override for the dataset-declared raw action format", + ) + args = parser.parse_args() + launch_viewer( + port=args.port, + share=args.share, + chunk_length=args.chunk_length, + action_format_override=ActionFormat(args.action_format) if args.action_format is not None else None, + ) + + +if __name__ == "__main__": + main() diff --git a/cosmos_training/cosmos/data/vfm/action/viewpoint_utils.py b/cosmos_training/cosmos/data/vfm/action/viewpoint_utils.py new file mode 100644 index 00000000..a9a9f0ca --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/action/viewpoint_utils.py @@ -0,0 +1,111 @@ +"""Viewpoint type definitions and caption augmentor for Action datasets. + +Provides a ``Viewpoint`` type alias for camera perspective labels and a +``ViewpointTextInfo`` augmentor that appends a human-readable viewpoint +description to the caption string. +""" + +from __future__ import annotations + +from typing import Literal + +from imaginaire.datasets.webdataset.augmentors.augmentor import Augmentor +from cosmos.utils import log + +Viewpoint = Literal["ego_view", "third_person_view", "wrist_view", "concat_view"] + +DEFAULT_VIEWPOINT_TEMPLATES: dict[str, str] = { + "ego_view": "This video is captured from a first-person perspective looking at the scene.", + "third_person_view": "This video is captured from a third-person perspective looking towards the agent from the front.", + "wrist_view": "This video is captured from a wrist-mounted camera.", + "concat_view": "This video contains concatenated views from multiple camera perspectives.", +} + + +class ViewpointTextInfo(Augmentor): + """Augmentor that appends viewpoint type description to captions. + + Reads a viewpoint label from ``data_dict[viewpoint_key]`` and appends + the corresponding template sentence to the caption. Designed to run + after the raw ``ai_caption`` is set but before duration/FPS metadata + is appended. + + Args: + input_keys: Input keys (kept for API compatibility). + output_keys: Output keys (kept for API compatibility). + args: Configuration arguments: + - caption_key (str): Key for caption in data_dict. Default: ``"ai_caption"`` + - viewpoint_key (str): Key for viewpoint label. Default: ``"viewpoint"`` + - templates (dict): Override mapping from viewpoint to sentence. + Default: :data:`DEFAULT_VIEWPOINT_TEMPLATES` + - separator (str): Separator between caption and metadata. Default: ``". "`` + - enabled (bool): Whether augmentation is enabled. Default: ``True`` + """ + + def __init__( + self, + input_keys: list | None = None, + output_keys: list | None = None, + args: dict | None = None, + ) -> None: + super().__init__(input_keys or [], output_keys or [], args) + + self.caption_key: str = args.get("caption_key", "ai_caption") if args else "ai_caption" + self.viewpoint_key: str = args.get("viewpoint_key", "viewpoint") if args else "viewpoint" + self.templates: dict[str, str] = ( + args.get("templates", DEFAULT_VIEWPOINT_TEMPLATES) if args else DEFAULT_VIEWPOINT_TEMPLATES + ) + self.default_separator: str = args.get("separator", ". ") if args else ". " + self.enabled: bool = args.get("enabled", True) if args else True + + def __call__(self, data_dict: dict) -> dict | None: + """Append viewpoint description to the caption. + + If the sample provides an ``"additional_view_description"`` key (a + free-form string describing the concatenated camera layout), it is + appended after the generic ``concat_view`` template. This allows each + dataset to supply its own description of which cameras are tiled and + how. + + Args: + data_dict: Sample dictionary containing caption and viewpoint. + + Returns: + The mutated *data_dict*, or the original unchanged if the + viewpoint key is missing or unrecognized. + """ + if not self.enabled: + return data_dict + + viewpoint = data_dict.get(self.viewpoint_key) + if viewpoint is None: + raise ValueError( + f"ViewpointTextInfo: missing key {self.viewpoint_key!r} in data_dict. " + f"All action datasets must provide a viewpoint label." + ) + + # Append dataset-specific concat_view details after the base template. + additional_view_description = data_dict.pop("additional_view_description", None) + template = self.templates.get(viewpoint) + + if template is None: + log.warning( + f"ViewpointTextInfo: unrecognized viewpoint {viewpoint!r}. " + f"Known viewpoints: {sorted(self.templates.keys())}. Skipping.", + rank0_only=False, + ) + return data_dict + + if additional_view_description: + separator = " " if template.endswith(".") else self.default_separator + template = template + separator + additional_view_description.rstrip() + + caption = data_dict.get(self.caption_key) + if not isinstance(caption, str) or caption == "": + return data_dict + + caption = caption.rstrip() + separator = " " if caption.endswith(".") else self.default_separator + data_dict[self.caption_key] = caption + separator + template + + return data_dict diff --git a/cosmos_training/cosmos/data/vfm/joint_dataloader.py b/cosmos_training/cosmos/data/vfm/joint_dataloader.py new file mode 100644 index 00000000..fd9508ac --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/joint_dataloader.py @@ -0,0 +1,989 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque +from dataclasses import dataclass +from typing import Any, ClassVar, Dict, Union + +import numpy as np +import torch +import webdataset +from torch.utils.data.dataloader import default_collate + +from cosmos.utils.lazy_config import instantiate +from cosmos.utils import log + +_TIMING_KEYS = {"_sample_time", "_aug_time", "_pre_aug_time", "_aug_step_times"} +_BATCH_TIMING_KEYS = { + "_worker_batch_time", + "_worker_aug_time", + "_worker_io_time", + "_worker_aug_step_times", + "_worker_id", +} + + +def custom_collate_fn(batch): + """ + Collate function that works like default_collate for all keys other than "text_token_ids", "images", and "video". + For "text_token_ids", "images", and "video" it simply returns them in a list, instead of stacking them as a tensor. + """ + list_collate_keys = { + "text_token_ids", + "images", + "video", + "action", + "domain_id", + "sequence_plan", + "sound", + "raw_action_dim", + "image_size", + } + + # Handle the case where the batch is already a dictionary (e.g. column-wise batching) + if isinstance(batch, dict): + return {key: (value if key in list_collate_keys else default_collate(value)) for key, value in batch.items()} + + # Handle standard list of samples + elem = batch[0] + if isinstance(elem, dict): + + # Some Action datasets add optional metadata keys (for example + # ``additional_view_description`` for concat-view captions) only for a + # subset of samples. PyTorch can batch such samples together when + # DataLoader batch_size > 1; collating only elem's keys and indexing + # every sample by that key turns the optional field into a fatal + # KeyError. Use the union of keys and skip optional keys that are not + # present in every sample. Required training keys still fail loudly via + # downstream assertions if actually missing. + result = {} + keys = set().union(*(d.keys() for d in batch)) + for key in keys: + if key in _TIMING_KEYS: + continue + values = [d.get(key) for d in batch] + if any(value is None for value in values): + continue + if key in list_collate_keys: + result[key] = values + else: + result[key] = default_collate(values) + result.update(_aggregate_worker_timing(batch)) + return result + else: + return default_collate(batch) + + +def _aggregate_worker_timing(samples: list[dict]) -> dict: + """Extract per-sample timing keys, aggregate into per-batch scalars.""" + info: dict[str, float | int] = {} + if "_sample_time" in samples[0]: + info["_worker_batch_time"] = sum(s.get("_sample_time", 0.0) for s in samples) + if "_aug_time" in samples[0]: + aug_total = sum(s.get("_aug_time", 0.0) for s in samples) + info["_worker_aug_time"] = aug_total + if "_worker_batch_time" in info: + info["_worker_io_time"] = info["_worker_batch_time"] - aug_total + if "_aug_step_times" in samples[0]: + agg: dict[str, float] = {} + for s in samples: + for step_name, t in s.get("_aug_step_times", {}).items(): + agg[step_name] = agg.get(step_name, 0.0) + t + info["_worker_aug_step_times"] = agg + worker_info = torch.utils.data.get_worker_info() + info["_worker_id"] = worker_info.id if worker_info is not None else 0 + return info + + +@dataclass +class _PackingMetrics: + """Per-batch packing statistics collected during the packing loop. + + Also serves as the single source of truth for packing-related metric names + via ``STATS_SPEC``, which the dataloading monitor callback consumes to + drive accumulation and logging. + """ + + current_sequence_length: int = 0 + num_samples: int = 0 + dropped_count: int = 0 + from_buffer: int = 0 + from_workers: int = 0 + + STATS_SPEC: ClassVar[list[tuple[str, str, str]]] = [ + # (batch_key, wandb_suffix, aggregation_type) + ("_num_tokens", "token_fraction", "scalar"), + ("_num_samples", "samples_per_batch", "list"), + ("_from_buffer", "from_buffer", "list"), + ("_from_workers", "from_workers", "list"), + ("_buffer_size", "buffer_size", "list"), + ("_dropped_count", "dropped", "scalar"), + ] + + def attach_to(self, output_batch: dict, buffer_size: int) -> None: + """Write packing statistics into the output batch dict.""" + output_batch["_num_tokens"] = self.current_sequence_length + output_batch["_num_samples"] = self.num_samples + output_batch["_from_buffer"] = self.from_buffer + output_batch["_from_workers"] = self.from_workers + output_batch["_buffer_size"] = buffer_size + output_batch["_dropped_count"] = self.dropped_count + + +class JointDataLoader(webdataset.WebLoader): + r""" + A joint dataloader that supports loading both images and videos. + """ + + _DEFAULT_LOOKAHEAD_LIMIT: ClassVar[int] = 10 + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None, + max_samples_per_batch: int | None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + prewarm: bool = True, + default_lookahead_limit: int = _DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, + ): + """ + Initialize the JointDataLoader with multiple datasets. + + The effective mini-batch size can be controlled with either max_sequence_length or + max_samples_per_batch. To use max_sequence_length, max_samples_per_batch needs to be None. + Vice versa, to use max_samples_per_batch, max_sequence_length needs to be None. + max_sequence_length and max_samples_per_batch cannot both be None simultaneously. + + Args: + dataloaders: key - dataset_name; value - {"dataloader": dataloader, "ratio": data_ratio} + tokenizer_spatial_compression_factor: The spatial compression factor of the tokenizer. + tokenizer_temporal_compression_factor: The temporal compression factor of the tokenizer. + patch_spatial: Spatial pathification factor. + max_samples_per_batch: Max number of samples per packed batch (alternative to max_sequence_length). + sound_latent_fps: Sound tokenizer latent rate in Hz (e.g. 25). If 0, sound tokens are not counted. + audio_sample_rate: Audio sample rate in Hz (e.g. 48000). Used with sound_latent_fps to estimate + sound token count. + default_lookahead_limit: Packing-loop look-ahead fallback for dataloaders not in + ``lookahead_limits``. + lookahead_limits: Optional ``{dataset_name: int}`` per-dataloader override. + + Example: + joint_loader = IterativeJointDataLoader( + dataloaders{ + "image_data": { + "dataloader": webdataset.WebLoader(...), + "ratio": 4, + }, + "video_data": { + "dataloader": torch.utils.data.DataLoader(...), + "ratio": 1, + }, + } + ) + """ + self.dataloader_list, self.dataset_name_list, self.data_ratios = [], [], [] + self.lookahead_limits: list[int] = [] + self.tokenizer_spatial_compression_factor = tokenizer_spatial_compression_factor + self.tokenizer_temporal_compression_factor = tokenizer_temporal_compression_factor + self.patch_spatial = patch_spatial + self.max_sequence_length = max_sequence_length + self.max_samples_per_batch = max_samples_per_batch + self.sound_latent_fps = sound_latent_fps + self.audio_sample_rate = audio_sample_rate + self.default_lookahead_limit = int(default_lookahead_limit) + + assert (self.max_sequence_length is None) != (self.max_samples_per_batch is None), ( + "Exactly one of max_sequence_length or max_samples_per_batch must be None, but not both." + ) + + _lookahead_overrides: Dict[str, int] = dict(lookahead_limits) if lookahead_limits else {} + unknown = set(_lookahead_overrides) - set(dataloaders) + assert not unknown, f"lookahead_limits references unknown dataloaders {unknown}; valid: {sorted(dataloaders)}" + + for dataset_name, dataloader_data in dataloaders.items(): + assert set(dataloader_data.keys()) == {"dataloader", "ratio"}, f"Invalid config: {dataloader_data}" + if dataloader_data["ratio"] <= 0: + continue + self.dataset_name_list.append(dataset_name) + self.dataloader_list.append(instantiate(dataloader_data["dataloader"], collate_fn=custom_collate_fn)) + self.data_ratios.append(dataloader_data["ratio"]) + self.lookahead_limits.append(int(_lookahead_overrides.get(dataset_name, self.default_lookahead_limit))) + + self.global_id = 0 + self.ratio_sum = sum(self.data_ratios) + + total = self.ratio_sum if self.ratio_sum > 0 else 1.0 + lines = [f"JointDataLoader: {len(self.dataset_name_list)} streams"] + for name, ratio in zip(self.dataset_name_list, self.data_ratios): + lines.append(f" {name}: ratio={ratio:.4g} ({ratio / total:.1%})") + log.info("\n".join(lines)) + + self.data_len = 0 + self.dataloaders = [iter(dataloader) for dataloader in self.dataloader_list] + self.buffers = [deque() for _ in range(len(self.dataloader_list))] + for data in self.dataloader_list: + self.data_len += len(data) + + # Pre-warm all dataloaders: force worker process spawning and first + # batch loading so that slow dataset initialisation (e.g. action + # datasets with spawn workers) happens here rather than mid-training + # where it would cause NCCL collective timeouts. + if prewarm: + self._prewarm_dataloaders() + else: + log.info( + "JointDataLoader: prewarm DISABLED (debug mode); first iteration may incur per-stream cold-load cost" + ) + + def _prewarm_dataloaders(self) -> None: + """Force all dataloader iterators to spawn workers and produce one batch. + + The first ``next()`` call on an ``InfiniteDataLoader`` iterator triggers + ``DataLoader.__iter__()`` which spawns worker processes. For action + dataloaders using ``multiprocessing_context='spawn'``, each worker must + fully initialise heavy datasets (BridgeOrigLeRobotDataset, EMBODIMENT_A, etc.) + from scratch. If this happens lazily during training, the resulting + delay (potentially minutes) causes NCCL collective timeouts when faster + ranks enter the forward pass while slower ranks are still loading data. + + By pulling one batch from every dataloader here — before any training + iteration — we ensure all workers are alive and warmed up. The fetched + samples are pushed into the per-dataloader buffer so they are consumed + normally by the first iteration that selects that dataloader. + + A ``dist.barrier()`` at the end synchronises all ranks so that training + only begins once every rank has finished pre-warming. + """ + import time + + for i, (name, dl_iter) in enumerate(zip(self.dataset_name_list, self.dataloaders)): + t0 = time.monotonic() + try: + batch = next(dl_iter) + except StopIteration: + log.warning(f"Pre-warm: dataloader {name!r} is empty, skipping") + continue + elapsed = time.monotonic() - t0 + + # Split the collated batch into individual samples and push them + # into the buffer — identical to the splitting logic in + # _get_next_sample — so the samples are not wasted. + is_image_batch = "images" in batch + input_images_or_videos = batch["images" if is_image_batch else "video"] + batch_size = len(input_images_or_videos) + + for j in range(batch_size): + sample = {} + for k, v in batch.items(): + if k in _BATCH_TIMING_KEYS: + sample[k] = v + elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS: + elem = v[j] + if isinstance(elem, list): + sample[k] = elem + else: + sample[k] = v[j : j + 1] + elif isinstance(v, list): + sample[k] = v[j] + elif isinstance(v, torch.Tensor) and v.dim() > 0: + sample[k] = v[j : j + 1] + else: + sample[k] = v[j : j + 1] + self.buffers[i].append(sample) + + log.info( + f"Pre-warm: dataloader {name!r} ready — {batch_size} samples buffered in {elapsed:.1f}s", + rank0_only=False, + ) + + # Synchronise so training only starts once every rank is warmed up. + if torch.distributed.is_initialized(): + log.info("Pre-warm: waiting at barrier for all ranks …") + torch.distributed.barrier() + log.info("Pre-warm: all ranks ready") + + def _compute_num_tokens_per_sample(self, data_batch: dict) -> int: + """ + This function computes the number of tokens per sample in the data batch. + This includes text + vision generation tokens + action tokens. + + Args: + data_batch (dict): The data batch containing the text tokens. + + Returns: + int: The number of tokens per sample. + """ + + # The token sequence we have is + # [] + # The spatial dimension of image tokens is compressed by + # vae spatial downsampling factor + pathification + # The temporal dimension of image tokens is compressed by + # vae temporal downsampling factor + # Action tokens have 1 token per time step (no spatial dimension) + + text_token_ids = data_batch["text_token_ids"] + if isinstance(text_token_ids, list): + num_text_tokens = text_token_ids[0].shape[0] + else: + num_text_tokens = text_token_ids.shape[1] + + num_tokens = num_text_tokens + 1 + + # Vision part + is_image_batch = "images" in data_batch + input_images_or_videos = data_batch["images" if is_image_batch else "video"] + + # iterate over all the media in the batch + for media in input_images_or_videos if isinstance(input_images_or_videos, list) else [input_images_or_videos]: + if is_image_batch: + _, H, W = media.shape + T = 1 + else: + _, T, H, W = media.shape + + vae_spatial_downsample = self.tokenizer_spatial_compression_factor * self.patch_spatial + vae_temporal_downsample = self.tokenizer_temporal_compression_factor + + latent_h_shape = H // vae_spatial_downsample + latent_w_shape = W // vae_spatial_downsample + latent_t_shape = 1 + (T - 1) // vae_temporal_downsample + + num_vision_tokens = latent_h_shape * latent_w_shape * latent_t_shape + 2 + num_tokens += num_vision_tokens + + # Action part: each action time step is 1 token. + # Action tensor shape is (T_action, D) per sample; stored as a single-element list. + if "action" in data_batch: + list_of_actions = data_batch["action"] + for action in list_of_actions: + # skip None actions + if action is None: + continue + num_action_tokens = action.shape[0] + num_tokens += num_action_tokens + + # Sound part — estimate sound tokens from audio waveform length + if self.sound_latent_fps > 0 and "sound" in data_batch: + sound_data = data_batch["sound"] + if isinstance(sound_data, list) and len(sound_data) > 0: + first_sound = sound_data[0] + # Unwrap nested list if needed + if isinstance(first_sound, list): + first_sound = first_sound[0] + if first_sound is not None and isinstance(first_sound, torch.Tensor): + num_audio_samples = first_sound.shape[-1] + audio_duration = num_audio_samples / self.audio_sample_rate + num_sound_tokens = int(audio_duration * self.sound_latent_fps) + num_tokens += num_sound_tokens + + return num_tokens + + # Keys whose value per sample is a list of tensors to be flattened into one list in the batch + _FLATTEN_LIST_KEYS = {"image_size"} + + def _update_output_batch(self, output_batch: dict, output: dict): + for key, value in output.items(): + if key in _BATCH_TIMING_KEYS: + if key not in output_batch: + output_batch[key] = value + elif key in self._FLATTEN_LIST_KEYS and isinstance(value, list): + if key not in output_batch: + output_batch[key] = value + else: + output_batch[key].extend(value) + elif key not in output_batch: + output_batch[key] = [value] + else: + output_batch[key].append(value) + + def __len__(self) -> int: + return self.data_len + + # Keys where each sample may hold multiple tensors (e.g. multiple video + # clips in a packed sequence). Kept as single-element lists per sample + # via v[i:i+1] so that _update_output_batch yields list[list[Tensor]]. + _MULTI_ITEM_KEYS = {"text_token_ids", "images", "video", "action", "sound"} + + def _get_next_sample(self, index_id: int) -> dict: + """Pop the next single-sample dict from the buffer for the given dataloader. + + If the buffer is empty, fetches the next collated batch from the inner + dataloader and splits it into individual samples. + + Splitting rules: + - Multi-item list values (keys in ``_MULTI_ITEM_KEYS``): sliced + via ``v[i:i+1]`` to yield a single-element list ``[tensor]``. + A packed sequence can contain multiple items per key. + - Per-sequence metadata list values (all other list keys, e.g. + ``sequence_plan``, ``domain_id``): direct-indexed via ``v[i]`` + to yield the bare element. + - Tensor values ``(B, ...)``: sliced to ``(1, ...)`` via + ``v[i : i + 1]`` to preserve the batch dimension. + + After ``_update_output_batch`` accumulates samples, the packed output + batch has the following shapes: + - Multi-item keys (``text_token_ids``, ``video``, ``images``, + ``action``): ``list[list[Tensor]]`` — each inner list has one + element from one sub-sample. + - Per-sequence metadata keys (``sequence_plan``, ``domain_id``, + ``dataset_name``, etc.): ``list[element]`` — flat list. + - Tensor-origin keys: ``list[Tensor(1, ...)]``. + + Args: + index_id: Index of the dataloader to fetch from. + + Returns: + A single-sample dictionary. + """ + buffer = self.buffers[index_id] + if not buffer: + try: + batch = next(self.dataloaders[index_id]) + except StopIteration: + raise + + is_image_batch = "images" in batch + input_images_or_videos = batch["images" if is_image_batch else "video"] + batch_size = len(input_images_or_videos) + + for i in range(batch_size): + sample = {} + for k, v in batch.items(): + if k in _BATCH_TIMING_KEYS: + sample[k] = v + elif isinstance(v, list) and k in self._MULTI_ITEM_KEYS: + # For multi-item keys (images, video, etc.), the collated + # value is a list with one element per sample. If the element + # is itself a list (e.g. image editing: [src, tgt]), use v[i] + # directly to avoid wrapping it in a redundant single-element + # list. Otherwise keep the v[i:i+1] slice so that + # _update_output_batch produces list[list[Tensor]]. + elem = v[i] + if isinstance(elem, list): + sample[k] = elem + else: + sample[k] = v[i : i + 1] + elif isinstance(v, list): + sample[k] = v[i] + else: + sample[k] = v[i : i + 1] + buffer.append(sample) + + return buffer.popleft() + + def set_start_iteration(self, iteration: int): + self.global_id = iteration + + def __iter__(self): + raise NotImplementedError("__iter__ function is not implemented yet") + + +class IterativeJointDataLoader(JointDataLoader): + r""" + An iterative joint dataloader that supports loading multiple modalities. + + The behavior depends on the ``seed`` parameter: + + - **seed is not None** (Default): + The modality is randomly selected at each iteration based on the probability distribution + derived from the ratios. The random state is seeded with ``seed + global_id``, ensuring + that all ranks select the same modality at the same iteration (assuming synchronized global_id). + This prevents load imbalance due to mixed resolutions across ranks. + + - **seed is None**: + The modality selection follows a deterministic round-robin pattern based on the ratios. + For example, with 2 modalities (image and video) and ratio 2:1: + - Iterations 0, 1: all ranks process images + - Iteration 2: all ranks process videos + - ... and so on. + This also ensures all ranks process the same modality at the same iteration. + """ + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + seed: int | None = 42, + prewarm: bool = True, + default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, + ): + super().__init__( + dataloaders, + tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor, + patch_spatial, + max_sequence_length, + max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + prewarm=prewarm, + default_lookahead_limit=default_lookahead_limit, + lookahead_limits=lookahead_limits, + ) + self.seed = seed + # Calculate probabilities for random sampling + total_ratio = sum(self.data_ratios) + self.data_probs = np.array([ratio / total_ratio for ratio in self.data_ratios]) + + def __iter__(self): + while True: + if self.seed is not None: + rng = np.random.RandomState(self.seed + self.global_id) + index_id = rng.choice(len(self.dataloader_list), p=self.data_probs) + else: + data_id = self.global_id % self.ratio_sum + index_id = self._get_dataloader_index(data_id) + + metrics = _PackingMetrics() + output_batch = dict() + skipped_samples = deque() + lookahead_limit = self.lookahead_limits[index_id] + lookahead_count = 0 + + while True: + # Check max samples limit first + if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch: + break + + # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop. + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + had_buffer = len(self.buffers[index_id]) > 0 + try: + output = self._get_next_sample(index_id) + except StopIteration: + break # No more data in this dataloader + + if had_buffer: + metrics.from_buffer += 1 + else: + metrics.from_workers += 1 + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.info( + f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + metrics.dropped_count += 1 + continue + + # current_sequence_length > 0 and selected sample is too large to fit in the remaining space. + # Instead of stopping immediately (creating large padding), we buffer this large sample + # and try to find a smaller one that fits in the remaining space. + skipped_samples.append(output) + lookahead_count += 1 + continue + + metrics.current_sequence_length += num_tokens_in_current_sample + metrics.num_samples += 1 + output["dataset_name"] = self.dataset_name_list[index_id] + self._update_output_batch(output_batch, output) + + # Add back skipped samples to the buffer for the next batch. + # appendleft puts item at HEAD. So we insert S3, then S2, then S1. + for sample in reversed(skipped_samples): + self.buffers[index_id].appendleft(sample) + + if len(output_batch) == 0: + return + + metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id])) + self.global_id += 1 + yield output_batch + + def _get_dataloader_index(self, data_id): + """Maps global id to the corresponding dataloader index based on ratio.""" + for i, r in enumerate(self.data_ratios): + if data_id < r: + return i + data_id -= r + raise ValueError("Invalid data_id") + + +class RankPartitionedDataLoader: + """Assigns each rank to exactly one dataset based on ratios. + + For N GPUs with datasets having ratios r_1:r_2:...:r_k, the first + N * r_1 / sum(r) ranks are assigned dataset 1, the next N * r_2 / sum(r) + ranks are assigned dataset 2, etc. Each rank instantiates a single + PyTorch DataLoader for its assigned dataset. + + The sharding information (``shard_world_size`` and ``shard_rank``) is set + on each dataset so that it shards data only across ranks that share the + same dataset, rather than across the full world. + + Example: + With 128 GPUs and datasets ``{"video": {"dataset": ..., "ratio": 3}, + "image": {"dataset": ..., "ratio": 1}}``: + + - Ranks 0-95 -> video (shard_world_size=96, shard_rank=0..95) + - Ranks 96-127 -> image (shard_world_size=32, shard_rank=0..31) + """ + + def __init__( + self, + datasets: dict[str, dict[str, Any]], + **dataloader_kwargs: Any, + ): + """ + Args: + datasets: Mapping of dataset name to config dict with keys: + + - ``"dataset"`` (required): a lazy config or dataset instance. + - ``"ratio"`` (required): positive int weight. + - ``"dataloader_kwargs"`` (optional): dict of keyword arguments + that override the top-level ``**dataloader_kwargs`` for this + dataset only (e.g. different ``num_workers`` or ``batch_size``). + + **dataloader_kwargs: Default kwargs forwarded to + ``torch.utils.data.DataLoader``. ``collate_fn`` defaults to + ``custom_collate_fn`` if not given. + """ + world_size = torch.distributed.get_world_size() + rank = torch.distributed.get_rank() + log.info(f"RankPartitionedDataLoader: world_size: {world_size} and rank: {rank}", rank0_only=False) + + _VALID_KEYS = {"dataset", "ratio", "dataloader_kwargs"} + names: list[str] = [] + dataset_configs: list[Any] = [] + ratios: list[int] = [] + per_dataset_kwargs: list[dict[str, Any]] = [] + for name, cfg in datasets.items(): + extra = set(cfg.keys()) - _VALID_KEYS + assert not extra, f"Dataset {name!r}: unexpected keys {extra}. Allowed: {_VALID_KEYS}" + if cfg["ratio"] <= 0: + log.warning( + f"RankPartitionedDataLoader: Skipping dataset {name} with ratio {cfg['ratio']}", rank0_only=False + ) + continue + names.append(name) + dataset_configs.append(cfg["dataset"]) + ratios.append(cfg["ratio"]) + per_dataset_kwargs.append(cfg.get("dataloader_kwargs", {})) + + assert len(names) > 0, "No datasets with positive ratios provided." + assert world_size >= len(names), ( + f"world_size ({world_size}) must be >= number of datasets ({len(names)}) " + f"so each dataset gets at least one rank." + ) + + total_ratio = sum(ratios) + ideal = [r / total_ratio * world_size for r in ratios] + allocations = [max(1, int(q)) for q in ideal] + remaining = world_size - sum(allocations) + if remaining > 0: + remainders = sorted(range(len(ratios)), key=lambda i: ideal[i] - allocations[i], reverse=True) + for j in range(remaining): + allocations[remainders[j]] += 1 + elif remaining < 0: + deficit = -remaining + while deficit > 0: + best = max( + (i for i in range(len(allocations)) if allocations[i] > 1), + key=lambda i: (allocations[i] - ideal[i], allocations[i]), + ) + allocations[best] -= 1 + deficit -= 1 + + expected_ratios = [r / total_ratio for r in ratios] + actual_ratios = [a / world_size for a in allocations] + lines = [f"RankPartitionedDataLoader allocation ({world_size} GPUs):"] + start = 0 + for i, (name, alloc) in enumerate(zip(names, allocations)): + end = start + alloc - 1 + lines.append( + f" {name} (ratio {ratios[i]}): ranks {start}-{end} ({alloc} GPUs) " + f"| expected {expected_ratios[i]:.2%}, actual {actual_ratios[i]:.2%}" + ) + start += alloc + log.info("\n".join(lines), rank0_only=False) + + cumulative = 0 + my_dataset_idx = -1 + for i, alloc in enumerate(allocations): + if rank < cumulative + alloc: + my_dataset_idx = i + break + cumulative += alloc + assert my_dataset_idx >= 0 + + shard_rank = rank - cumulative + shard_world_size = allocations[my_dataset_idx] + + dataset: Any = instantiate(dataset_configs[my_dataset_idx]) + dataset.shard_world_size = shard_world_size + dataset.shard_rank = shard_rank + dataset.shard_id = my_dataset_idx + + merged_kwargs = {**dataloader_kwargs, **per_dataset_kwargs[my_dataset_idx]} + merged_kwargs.setdefault("collate_fn", custom_collate_fn) + self.dataloader = torch.utils.data.DataLoader(dataset, **merged_kwargs) + self.dataset_name = names[my_dataset_idx] + self.dataset = dataset + + def __iter__(self): + return iter(self.dataloader) + + def __len__(self) -> int: + return len(self.dataloader) + + +class PackingDataLoader(JointDataLoader): + """Packs multiple samples from a single dataloader into token-budget-constrained batches. + + Unlike the other ``JointDataLoader`` subclasses which manage multiple + dataloaders with configurable ratios, this class wraps a single dataloader + and greedily packs consecutive samples until the token budget + (``max_sequence_length``) or sample count limit (``max_samples_per_batch``) + is reached. + """ + + def __init__( + self, + dataloader: torch.utils.data.DataLoader | webdataset.WebLoader, + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + dataset_name: str = "default", + lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, + ): + """ + Args: + dataloader: A single dataloader (or lazy config) to draw samples from. + tokenizer_spatial_compression_factor: Spatial compression factor of the tokenizer. + tokenizer_temporal_compression_factor: Temporal compression factor of the tokenizer. + patch_spatial: Spatial patchification factor. + max_sequence_length: Max total tokens per packed batch. Mutually exclusive with + ``max_samples_per_batch``. + max_samples_per_batch: Max number of samples per packed batch. Mutually exclusive + with ``max_sequence_length``. + sound_latent_fps: Sound tokenizer latent rate in Hz. If 0, sound tokens are not counted. + audio_sample_rate: Audio sample rate in Hz. + dataset_name: Name tag attached to every sample in the output batch. + lookahead_limit: Packing-loop look-ahead for the wrapped dataloader. + """ + wrapped = {dataset_name: {"dataloader": dataloader, "ratio": 1}} + super().__init__( + dataloaders=wrapped, + tokenizer_spatial_compression_factor=tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor=tokenizer_temporal_compression_factor, + patch_spatial=patch_spatial, + max_sequence_length=max_sequence_length, + max_samples_per_batch=max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + lookahead_limits={dataset_name: int(lookahead_limit)}, + ) + + def __iter__(self): + inner = self.dataloader_list[0] + ds_name = getattr(inner, "dataset_name", self.dataset_name_list[0]) + + while True: + current_sequence_length = 0 + num_samples = 0 + output_batch: dict = {} + + skipped_samples: deque = deque() + # PackingDataLoader wraps a single dataloader, so lookahead_limits has one entry. + lookahead_limit = self.lookahead_limits[0] + lookahead_count = 0 + + while True: + if self.max_samples_per_batch is not None and num_samples >= self.max_samples_per_batch: + break + + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + try: + output = self._get_next_sample(0) + except StopIteration: + break + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.error( + f"PackingDataLoader: Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + continue + + skipped_samples.append(output) + lookahead_count += 1 + continue + + current_sequence_length += num_tokens_in_current_sample + num_samples += 1 + output["dataset_name"] = ds_name + self._update_output_batch(output_batch, output) + + for sample in reversed(skipped_samples): + self.buffers[0].appendleft(sample) + + if len(output_batch) == 0: + return + + self.global_id += 1 + yield output_batch + + +class RandomJointDataLoader(JointDataLoader): + r""" + A random joint dataloader that supports loading multiple modalities with stochastic sampling. + + In this dataloader, the modality is randomly selected at each iteration based on the + probability distribution derived from the ratios. Each rank independently samples a + modality, so different ranks may process different modalities at the same iteration. + + For example, with 2 modalities (image and video) and ratio 2:1: + - Each iteration has 66.7% probability of selecting images + - Each iteration has 33.3% probability of selecting videos + - The selection is independent across iterations and ranks + + Note: Unlike IterativeJointDataLoader, this does not guarantee synchronized modality + selection across ranks. + """ + + def __init__( + self, + dataloaders: Dict[str, Dict[str, Union[torch.utils.data.DataLoader, webdataset.WebLoader, int]]], + tokenizer_spatial_compression_factor: int, + tokenizer_temporal_compression_factor: int, + patch_spatial: int, + max_sequence_length: int | None = None, + max_samples_per_batch: int | None = None, + sound_latent_fps: float = 0, + audio_sample_rate: int = 48000, + default_lookahead_limit: int = JointDataLoader._DEFAULT_LOOKAHEAD_LIMIT, + lookahead_limits: Dict[str, int] | None = None, + ): + super().__init__( + dataloaders, + tokenizer_spatial_compression_factor, + tokenizer_temporal_compression_factor, + patch_spatial, + max_sequence_length, + max_samples_per_batch, + sound_latent_fps=sound_latent_fps, + audio_sample_rate=audio_sample_rate, + default_lookahead_limit=default_lookahead_limit, + lookahead_limits=lookahead_limits, + ) + + # Convert data ratios to probabilities + self.data_ratios = np.array([ratio / sum(self.data_ratios) for ratio in self.data_ratios]) + + def __iter__(self): + while True: + index_id = np.random.choice(len(self.dataloader_list), p=self.data_ratios) + + metrics = _PackingMetrics() + output_batch = dict() + skipped_samples = deque() + lookahead_limit = self.lookahead_limits[index_id] + lookahead_count = 0 + + while True: + # Check max samples limit first + if self.max_samples_per_batch is not None and metrics.num_samples >= self.max_samples_per_batch: + break + + # If we have started packing and tried lookahead_limit times to find a fitting sample but failed, stop. + if len(output_batch) > 0 and lookahead_count >= lookahead_limit: + break + + had_buffer = len(self.buffers[index_id]) > 0 + try: + output = self._get_next_sample(index_id) + except StopIteration: + break # No more data in this dataloader + + if had_buffer: + metrics.from_buffer += 1 + else: + metrics.from_workers += 1 + + num_tokens_in_current_sample = self._compute_num_tokens_per_sample(output) + + if ( + self.max_sequence_length is not None + and metrics.current_sequence_length + num_tokens_in_current_sample >= self.max_sequence_length + ): + if len(output_batch) == 0: + # This case happens when current_sequence_length = 0 and num_tokens_in_current_sample > self.max_sequence_length + # In this case, we should simply discard the current sample and get the next sample. + log.info( + f"Discarding oversized sample with {num_tokens_in_current_sample} tokens. Max sequence length: {self.max_sequence_length}", + rank0_only=False, + ) + metrics.dropped_count += 1 + continue + + # current_sequence_length > 0 and selected sample is too large to fit in the remaining space. + # Instead of stopping immediately (creating large padding), we buffer this large sample + # and try to find a smaller one that fits in the remaining space. + skipped_samples.append(output) + lookahead_count += 1 + continue + + metrics.current_sequence_length += num_tokens_in_current_sample + metrics.num_samples += 1 + output["dataset_name"] = self.dataset_name_list[index_id] + self._update_output_batch(output_batch, output) + + # Add back skipped samples to the buffer for the next batch. + # appendleft puts item at HEAD. So we insert S3, then S2, then S1. + for sample in reversed(skipped_samples): + self.buffers[index_id].appendleft(sample) + + if len(output_batch) == 0: + return + + metrics.attach_to(output_batch, buffer_size=len(self.buffers[index_id])) + yield output_batch diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/__init__.py b/cosmos_training/cosmos/data/vfm/local_datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3275c792f991ec8ef140f376699284f54a003e88 GIT binary patch literal 249 zcmZ8b%MHRX3~eZt5K;%>LKZ-bzzR`l)2dY(M|KM8B(MUrFa{H#a_R|3YB}&E`@LuR z_ssKc!U&(Njr@w~ALE)Uw`53m;`B&x#cqr}(_Px00vcJ-ReHcsfo*_f0n{;hgw_;c z@#?I}sDQe0jjLr(21yRu)iJ_6q`(_T&|_hi%5P#`Tk4?YrRt=GLR%Fr8NQ+vFAhrW Z{V;6~9hAK_N6h$9!9h{HQy~PVtiI|GOnU$T literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/helper.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db2bd593d15bae77db3e3239b15d8249f83b34c2 GIT binary patch literal 9781 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 R3X`gxGFcxxNFCXj{{ab_myrMf literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/sft_dataset.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/local_datasets/__pycache__/sft_dataset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cc0c4e6f4060cb2b87d132c74a6e73a8835a462 GIT binary patch literal 30422 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 WxQ6BoYg?v`E3|(Zpip)``hNfrxm@x9 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/helper.py b/cosmos_training/cosmos/data/vfm/local_datasets/helper.py new file mode 100644 index 00000000..013d588f --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/local_datasets/helper.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared helpers for local datasets (S3, video decoding, aspect ratio).""" + +import io +import json +import subprocess +import time +from collections.abc import Generator +from pathlib import Path +from typing import Any + +import numpy as np +from boto3.s3.transfer import TransferConfig +from botocore.config import Config + +from cosmos.utils import log + +client_config = Config( + response_checksum_validation="when_required", + request_checksum_calculation="when_required", + connect_timeout=10, + read_timeout=5, +) +transfer_config = TransferConfig(use_threads=True, max_concurrency=8, multipart_chunksize=8 * 1024 * 1024) + + +def parse_s3_url(s3_url: str) -> tuple[str, str]: + s3_url = s3_url.removeprefix("s3://") + bucket, key = s3_url.split("/", 1) + return bucket, key + + +def download_from_s3(s3_client: Any, s3_url: str, max_tries: int = 20) -> bytes | None: + """Download a file from S3.""" + if not s3_url.startswith("s3://"): + return Path(s3_url).read_bytes() + tries = 0 + while True: + tries += 1 + try: + bucket, key = parse_s3_url(s3_url) + buffer = io.BytesIO() + s3_client.download_fileobj(Bucket=bucket, Key=key, Fileobj=buffer, Config=transfer_config) + data = buffer.getvalue() + return data + except Exception as e: + log.error(f"Error downloading from S3 (try {tries}): {e}\n{s3_url}") + if tries >= max_tries: + return None + time.sleep(1) + + +def get_video_metadata(video_path: str) -> dict: + """ + Get video metadata using ffprobe. + + Args: + video_path: Path to the video file + + Returns: + Dictionary containing width, height, fps, and total_frames + """ + cmd = [ + "ffprobe", + "-v", + "quiet", + "-print_format", + "json", + "-show_streams", + "-select_streams", + "v:0", + video_path, + ] + result = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, check=True, text=True) + probe_data = json.loads(result.stdout) + + # Decode output + stream = probe_data["streams"][0] + width = int(stream["width"]) + height = int(stream["height"]) + fps_parts = stream["r_frame_rate"].split("/") + video_fps = float(fps_parts[0]) / float(fps_parts[1]) + if "nb_frames" in stream: + total_frames = int(stream["nb_frames"]) + else: + duration = float(stream.get("duration") or 0) + total_frames = int(duration * video_fps) + + return dict(width=width, height=height, fps=video_fps, total_frames=total_frames) + + +def ffmpeg_decode_video( + video_path: str, scale_hw: tuple[int, int] | None = None, num_threads: int = 1 +) -> Generator[np.ndarray, None, None]: + """ + Decode video frames using ffmpeg and yield HWC uint8 RGB frames. + + Args: + video_path: Path to the video file + scale_hw: Tuple of width and height to scale the video to (default: None) + + Yields: + np.ndarray: HWC uint8 RGB frames + """ + if scale_hw is None: + metadata = get_video_metadata(video_path) + out_width = metadata["width"] + out_height = metadata["height"] + else: + out_height, out_width = scale_hw + + # Calculate frame size in bytes + frame_size = out_width * out_height * 3 # 3 channels (RGB) + + # Build ffmpeg command to decode and output raw RGB frames + ffmpeg_cmd = [ + "ffmpeg", + "-loglevel", + "quiet", + "-threads", + str(num_threads), + "-filter_threads", + str(num_threads), + "-filter_complex_threads", + str(num_threads), + "-i", + video_path, + "-threads", + str(num_threads), + "-filter_threads", + str(num_threads), + "-filter_complex_threads", + str(num_threads), + "-pix_fmt", + "rgb24", + "-sws_flags", + "bicubic+accurate_rnd", # lanczos too much ringing on graphics + *(["-vf", f"scale={scale_hw[1]}:{scale_hw[0]}"] if scale_hw else []), # WH + "-f", + "rawvideo", + "-vsync", + "0", + "-", + ] + + process = subprocess.Popen( + ffmpeg_cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, # Set to None to print errors + bufsize=-1, + ) + + try: + while True: + raw_frame = process.stdout.read(frame_size) + + if len(raw_frame) != frame_size: + assert len(raw_frame) == 0, f"Incomplete frame: {len(raw_frame)} bytes" + break + + frame = np.frombuffer(raw_frame, dtype=np.uint8) + frame = frame.reshape((out_height, out_width, 3)) + + yield frame + finally: + process.stdout.close() + process.wait() + + +def get_aspect_ratio(width: int, height: int) -> str: + """Compute aspect ratio bucket from width and height.""" + ratio = width / height + + if ratio < 0.65: + return "9,16" # 0.5625 + elif ratio < 0.88: + return "3,4" # 0.75 + elif ratio < 1.16: + return "1,1" # 1.0 + elif ratio < 1.55: + return "4,3" # 1.3333 + else: + return "16,9" # 1.7778 + + +def save_video_frames_to_mp4( + frames: np.ndarray | Any, + output_path: str, + fps: float = 24.0, + overlay_frame_id: bool = False, + fps_to_show: float | None = None, +) -> None: + """Encode video frames to MP4 using FFmpeg. + + Args: + frames: Video frames as numpy (T, H, W, 3) or torch tensor (C, T, H, W), uint8. + output_path: Path for the output .mp4 file. + fps: Output video frame rate. + overlay_frame_id: If True, draw frame index (0, 1, ...) on each frame via FFmpeg drawtext. + fps_to_show: If provided, draw the FPS value on the video instead of the actual FPS. + """ + cpu_fn = getattr(frames, "cpu", None) + if callable(cpu_fn): + frames = cpu_fn().numpy() # type: ignore[union-attr] + frames = np.asarray(frames, dtype=np.uint8) + if frames.ndim == 4 and frames.shape[0] == 3: + # CTHW -> THWC + frames = np.transpose(frames, (1, 2, 3, 0)) + if frames.ndim != 4 or frames.shape[-1] != 3: + raise ValueError("frames must be (T, H, W, 3) or (C, T, H, W) uint8") + t, h, w, _ = frames.shape + cmd = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-s", + f"{w}x{h}", + "-r", + str(fps), + "-i", + "pipe:0", + ] + if overlay_frame_id: + # %{n} = frame index (0-based); add fps and resolution as literal text + drawtext_frame = "drawtext=text='%{n}':x=10:y=10:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + drawtext_fps = ( + f"drawtext=text='fps: {fps_to_show or fps}':x=10:y=40:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + ) + drawtext_res = f"drawtext=text='{w}x{h}':x=10:y=70:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.6" + cmd += ["-vf", ",".join([drawtext_frame, drawtext_fps, drawtext_res])] + cmd += [ + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + output_path, + ] + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + _, stderr = process.communicate(input=frames.tobytes()) + if process.returncode != 0: + log.error(f"FFmpeg failed: {stderr.decode()}") + raise RuntimeError(f"FFmpeg exited with {process.returncode}") diff --git a/cosmos_training/cosmos/data/vfm/local_datasets/sft_dataset.py b/cosmos_training/cosmos/data/vfm/local_datasets/sft_dataset.py new file mode 100644 index 00000000..db648b98 --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/local_datasets/sft_dataset.py @@ -0,0 +1,691 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# SFT dataset loader — reads video metadata + captions from a JSONL file on S3. +import gzip +import hashlib +import io +import json +import os +import random +import tempfile +from pathlib import Path +from typing import Any, Optional + +import boto3 +import numpy as np +import torch + +from cosmos.utils.flags import INTERNAL +from cosmos.utils.lazy_config import instantiate as lazy_instantiate +from cosmos.utils import log +from cosmos.data.vfm.local_datasets.helper import ( + client_config, + download_from_s3, + ffmpeg_decode_video, + get_aspect_ratio, + get_video_metadata, + parse_s3_url, +) +from cosmos.data.vfm.sequence_packing import SequencePlan, add_special_tokens +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO +from cosmos.model.vfm.vlm.qwen3_vl.utils import tokenize_caption + +_MAX_NUM_TOKENS = 1024 +_DURATION_TEMPLATE = "The video is {duration:.1f} seconds long and is of {fps:.0f} FPS." +_RESOLUTION_TEMPLATE = "This video is of {height}x{width} resolution." + +# Caption types available in the SFT JSONL. +# Format: {model}_{style} +# model: qwen3_235b | qwen3_32b | qwen3p5_397b +# style: short | temporal | descriptive | dense +CAPTION_TYPES_AND_WEIGHTS: dict[str, float] = { + # short: 10% total + "qwen3_235b_short": 0.1, + "qwen3_32b_short": 0.1, + "qwen3p5_397b_short": 0.1, + # descriptive: 20% total + "qwen3_235b_descriptive": 0.2, + "qwen3_32b_descriptive": 0.2, + "qwen3p5_397b_descriptive": 0.2, + # dense: 70% total + "qwen3_235b_dense": 0.7, + "qwen3_32b_dense": 0.7, + "qwen3p5_397b_dense": 0.7, + # temporal: 0% total + "qwen3_235b_temporal": 0.0, + "qwen3_32b_temporal": 0.0, + "qwen3p5_397b_temporal": 0.0, +} +CAPTION_TYPES = list(CAPTION_TYPES_AND_WEIGHTS.keys()) +CAPTION_WEIGHTS = list(CAPTION_TYPES_AND_WEIGHTS.values()) + + +class SFTDataset(torch.utils.data.IterableDataset): + """Dataset for loading SFT video clips with captions from JSONL metadata on S3.""" + + def __init__( + self, + metadata: list[dict], + num_video_frames: int, + resolution: str, + s3_credentials: dict, + temporal_interval_mode: str = "entire_chunk", + frame_selection_mode: str = "center", + tokenizer_config: Optional[Any] = None, + cfg_dropout_rate: float = 0.0, + use_system_prompt: bool = False, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + cfg_dropout_keep_metadata: bool = False, + caption_suffix: str = "", + conditioning_fps: float = 24, + conditioning_fps_noise_std: float = 0.0, + conditioning_config: dict[int, float] | None = None, + temporal_compression_factor: int = 4, + ): + assert temporal_interval_mode in ("force_one", "max_30fps", "entire_chunk"), ( + f"Unknown temporal_interval_mode={temporal_interval_mode!r}" + ) + assert frame_selection_mode in ("center", "first", "random"), ( + f"Unknown frame_selection_mode={frame_selection_mode!r}" + ) + assert temporal_compression_factor >= 1, "temporal_compression_factor must be >= 1" + self.metadata = metadata + self.num_video_frames = num_video_frames + self.resolution = resolution + self.s3_credentials = s3_credentials + self.temporal_interval_mode = temporal_interval_mode + self.frame_selection_mode = frame_selection_mode + self.tokenizer_config = tokenizer_config + self.cfg_dropout_rate = cfg_dropout_rate + self.use_system_prompt = use_system_prompt + self.append_duration_fps_timestamps = append_duration_fps_timestamps + self.append_resolution_info = append_resolution_info + self.cfg_dropout_keep_metadata = cfg_dropout_keep_metadata + self.caption_suffix = caption_suffix.strip() + self.conditioning_fps = conditioning_fps + self.conditioning_fps_noise_std = conditioning_fps_noise_std + + self.temporal_compression_factor = temporal_compression_factor + self.conditioning_config: dict[int, float] | None = None + if conditioning_config is not None: + total_prob = sum(conditioning_config.values()) + assert total_prob > 0, "conditioning_config probabilities must sum to a positive number" + self.conditioning_config = {k: v / total_prob for k, v in conditioning_config.items()} + log.info(f"Conditioning config: {self.conditioning_config}") + # They will be set by the RankPartitionedDataLoader + self.shard_world_size = None + self.shard_rank = None + self.shard_id = 0 + self.is_initialized = False + self.output_sizes = VIDEO_RES_SIZE_INFO[resolution] + + self.vlm_tokenizer = lazy_instantiate(self.tokenizer_config) + self.vlm_tokenizer, _ = add_special_tokens(self.vlm_tokenizer) + + def __len__(self): + return len(self.metadata) + + def _tokenize_caption(self, caption: str) -> tuple[list[int], str]: + text_ids = tokenize_caption( + caption, + self.vlm_tokenizer, + is_video=True, + use_system_prompt=self.use_system_prompt, + ) + if len(text_ids) > _MAX_NUM_TOKENS: + log.warning(f"Text ids are too long, truncating: {len(text_ids)} > {_MAX_NUM_TOKENS}") + text_ids = text_ids[:_MAX_NUM_TOKENS] + return text_ids, caption + + def process_one_sample(self, metadata: dict) -> dict | None: + """Process a single SFT sample: download, decode, and prepare for training. + + A random t2w_window is picked from the video's list of windows each time. + """ + windows = metadata["t2w_windows"] + win_idx = random.randrange(len(windows)) + t2w_window = windows[win_idx] + window_start = t2w_window["start_frame"] + window_end = t2w_window["end_frame"] + + # Compute output resolution + input_w, input_h = metadata["width"], metadata["height"] + target_w, target_h = self.output_sizes[metadata["aspect_ratio"]] + resize_ratio = max(target_w / input_w, target_h / input_h) + resize_h, resize_w = (round(input_h * resize_ratio), round(input_w * resize_ratio)) + crop_y, crop_x = (round((resize_h - target_h) / 2), round((resize_w - target_w) / 2)) + + video_bytes = download_from_s3(self.s3_client, metadata["s3_path"]) + if video_bytes is None: + log.warning(f"Failed to download video from S3: {metadata['s3_path']}") + return None + + # Decode all frames to (T, H, W, 3) + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=True) as tmp_input: + tmp_input.write(video_bytes) + tmp_input.flush() + input_video_path = tmp_input.name + video_info = get_video_metadata(input_video_path) + original_fps = video_info["fps"] + total_frames = video_info["total_frames"] + + # Constrain to the t2w window + actual_end = min(window_end, total_frames - 1) + frames_in_window = actual_end - window_start + 1 + + if self.num_video_frames == -1: + # Native chunk mode: use start/end/interval directly from the window + temporal_interval = t2w_window["temporal_interval"] + start_frame = window_start + end_frame = actual_end + else: + if frames_in_window < self.num_video_frames: + log.warning( + f"Not enough frames in window: {metadata['uuid']}, " + f"frames_in_window: {frames_in_window}, required: {self.num_video_frames}" + ) + return None + + # Compute temporal interval + if self.temporal_interval_mode == "force_one": + temporal_interval = 1 + elif self.temporal_interval_mode == "max_30fps": + temporal_interval = max(1, int(original_fps / 30.0)) + elif self.temporal_interval_mode == "entire_chunk": + temporal_interval = frames_in_window // self.num_video_frames + temporal_interval = max(1, temporal_interval) + else: + raise ValueError(f"Unknown temporal_interval_mode: {self.temporal_interval_mode}") + + num_frames_before_downsample = (self.num_video_frames - 1) * temporal_interval + 1 + if self.frame_selection_mode == "first": + start_frame = window_start + elif self.frame_selection_mode == "center": + start_frame = window_start + (frames_in_window - num_frames_before_downsample) // 2 + elif self.frame_selection_mode == "random": + max_offset = frames_in_window - num_frames_before_downsample + start_frame = window_start + random.randint(0, max(0, max_offset)) + else: + raise ValueError(f"Unknown frame_selection_mode: {self.frame_selection_mode}") + end_frame = start_frame + num_frames_before_downsample - 1 + + fps = original_fps / temporal_interval + + video_chunk = [] + for idx, frame in enumerate( + ffmpeg_decode_video(input_video_path, scale_hw=(resize_h, resize_w), num_threads=2) + ): + if idx < start_frame: + continue + elif idx <= end_frame: + if (idx - start_frame) % temporal_interval == 0: + video_chunk.append(frame) + else: + break + + if not video_chunk: + log.warning( + f"No frames decoded for sample: {metadata['uuid']} " + f"(start={start_frame}, end={end_frame}, path={metadata['s3_path']})" + ) + return None + + video_chunk = np.stack(video_chunk, axis=0) # [T,H,W,3] + + # Truncate temporally to temporal_compression_factor * N + 1 + target_t = (video_chunk.shape[0] - 1) // self.temporal_compression_factor * self.temporal_compression_factor + 1 + + # Apply spatial center crop and temporal truncation + video_chunk = video_chunk[:target_t, crop_y : crop_y + target_h, crop_x : crop_x + target_w] # [T,H,W,3] + + # THWC -> CTHW + video_chunk = np.transpose(video_chunk, (3, 0, 1, 2)) # [3,T,H,W] + video = torch.from_numpy(np.ascontiguousarray(video_chunk)).to(torch.uint8) # [3,T,H,W] + padding_mask = torch.zeros((1, target_h, target_w), dtype=torch.float32) + # image_size: [target_h, target_w, orig_h, orig_w] in pixel space, for the model to crop the video + image_size = torch.tensor([target_h, target_w, target_h, target_w], dtype=torch.float32) + + available_types = [ct for ct in CAPTION_TYPES if ct in t2w_window] + if "qwen3_32b_rewrite-dense" in t2w_window: + caption_key = "qwen3_32b_rewrite-dense" + elif "caption" in t2w_window: + caption_key = "caption" + elif available_types: + available_weights = [CAPTION_TYPES_AND_WEIGHTS[ct] for ct in available_types] + caption_key = random.choices(available_types, weights=available_weights, k=1)[0] + else: + log.warning( + f"No known caption key found in t2w_window for sample {metadata['uuid']}. " + f"Keys: {list(t2w_window)}. Skipping sample." + ) + return None + caption = t2w_window[caption_key] + caption = caption.strip().rstrip(".") + "." + + num_decoded_frames = video.shape[1] + cond_fps = fps if self.conditioning_fps < 0 else self.conditioning_fps + if self.conditioning_fps_noise_std > 0: + noise_factor = np.exp(np.random.randn() * self.conditioning_fps_noise_std) + cond_fps = cond_fps * noise_factor + + if self.caption_suffix: + caption = (caption + " " + self.caption_suffix).strip() + + # CFG dropout: when cfg_dropout_keep_metadata is True, dropout fires + # before appending resolution/duration/FPS so that metadata text is + # preserved even under unconditional guidance. + if self.cfg_dropout_keep_metadata and self.cfg_dropout_rate > 0: + if random.random() < self.cfg_dropout_rate: + caption = "" + + if self.append_duration_fps_timestamps: + duration = num_decoded_frames / cond_fps + suffix = _DURATION_TEMPLATE.format(duration=duration, fps=cond_fps) + caption = caption + " " + suffix + if self.append_resolution_info: + suffix = _RESOLUTION_TEMPLATE.format(height=target_h, width=target_w) + caption = caption + " " + suffix + caption = caption.strip() + + if not self.cfg_dropout_keep_metadata and self.cfg_dropout_rate > 0: + if random.random() < self.cfg_dropout_rate: + caption = "" + text_ids, caption = self._tokenize_caption(caption) + + ret = dict( + __key__=f"{metadata['uuid']}_w{win_idx}", + __url__=metadata["s3_path"], + fps=original_fps, + n_orig_video_frames=total_frames, + chunk_index=win_idx, + frame_start=start_frame, + frame_end=end_frame, + num_frames=video.shape[1], + video=video, + num_multiplier=temporal_interval, + conditioning_fps=cond_fps, + padding_mask=padding_mask, + image_size=image_size, + ai_caption=caption, + sampled_caption_style=caption_key, + text_token_ids=torch.tensor(text_ids), + ) + + if self.conditioning_config is not None: + num_frames_pixel = video.shape[1] + t_latent = 1 + (num_frames_pixel - 1) // self.temporal_compression_factor + frames_options = list(self.conditioning_config.keys()) + weights = list(self.conditioning_config.values()) + num_cond = random.choices(frames_options, weights=weights, k=1)[0] + num_cond = min(num_cond, t_latent - 1) + ret["sequence_plan"] = SequencePlan( + has_text=True, + has_vision=True, + condition_frame_indexes_vision=list(range(num_cond)), + ) + + return ret + + def __iter__(self): + assert not self.is_initialized, "Dataset can only be initialized once." + assert len(self.metadata) > 0, "Did not find any data." + + self.s3_client = boto3.client( + "s3", + **self.s3_credentials, + config=client_config, + ) + # Ranks of the same pp/tp/cp group will have the same dp rank and thus share the same group id. + # zhao: Cosmos3 does not support TP/SP/CP + if self.shard_world_size is not None: + train_world_size = self.shard_world_size + train_rank = self.shard_rank + log.info(f"Using shard_world_size: {train_world_size} and shard_rank: {train_rank}", rank0_only=False) + else: + train_world_size = torch.distributed.get_world_size() + train_rank = torch.distributed.get_rank() + train_dp_rank = train_rank + train_num_dp_groups = train_world_size + train_dp_group_size = 1 + + # Get data worker rank. Each trainer have multiple dataloaders + worker_info = torch.utils.data.get_worker_info() + if worker_info is not None: + worker_rank = worker_info.id + total_data_ranks = worker_info.num_workers * train_num_dp_groups + data_rank = worker_rank + train_dp_rank * worker_info.num_workers + seed = worker_info.seed + else: + log.warning("No data worker info found. Using default worker rank and number of workers.", rank0_only=False) + total_data_ranks = train_num_dp_groups + data_rank = train_dp_rank + seed = 42 + + log.info( + f"train_world_size: {train_world_size}; " + f"train_rank: {train_rank}; " + f"train_dp_rank: {train_dp_rank}; " + f"train_num_dp_groups: {train_num_dp_groups}; " + f"train_dp_group_size: {train_dp_group_size}; " + f"worker_info: {worker_info}; " + f"total_data_ranks: {total_data_ranks}; " + f"data_rank: {data_rank}; " + f"seed: {seed}" + f"shard_id: {self.shard_id}; " + f"shard_world_size: {self.shard_world_size}; " + f"shard_rank: {self.shard_rank}", + rank0_only=False, + ) + + # Make sure len(self.metadata) is divisible by self.num_groups + multiplier = max(1, total_data_ranks * 50 // len(self.metadata)) + log.info(f"Dataset multiplier: {multiplier}", rank0_only=False) + self.metadata = self.metadata * multiplier # reduce bias caused by sharding + num_pad = total_data_ranks - len(self.metadata) % total_data_ranks + self.metadata = self.metadata + self.metadata[:num_pad] + # Deterministic shuffle based on the sha256 hash of uuid + # Note that the repeated samples are grouped together. + # Split list to keep only the data for this rank + if True: # This gives more diversity + random.Random(self.shard_id).shuffle(self.metadata) + log.info(f"Shuffled metadata for shard {self.shard_id}", rank0_only=False) + self.metadata = self.metadata[data_rank::total_data_ranks] + else: + # Keep the repeated samples together to aid cache hits. + self.metadata.sort(key=lambda x: hashlib.sha256(x["s3_path"].encode("utf-8")).hexdigest()) + # Equally chunk the list (guaranteed to be divisible by total_data_ranks) + chunk_size = len(self.metadata) // total_data_ranks + start = data_rank * chunk_size + end = (data_rank + 1) * chunk_size + log.info( + f"DRank {data_rank} has got a chunk {start}-{end} from {len(self.metadata)} data.", rank0_only=False + ) + self.metadata = self.metadata[start:end] + num_unique_s3_paths = len(set(metadata["s3_path"] for metadata in self.metadata)) + log.info( + f"DRank {data_rank} has {len(self.metadata)} data with {num_unique_s3_paths} unique s3_paths.", + rank0_only=False, + ) + + self.is_initialized = True + + # Make sure the data within a DRank is identical + rng = random.Random(data_rank + self.shard_id * 12345) + while True: + rng.shuffle(self.metadata) + for metadata in self.metadata: + sample = self.process_one_sample(metadata) + if sample is None: + log.warning(f"Failed to process sample {metadata['uuid']}, skipping...") + continue + yield sample + + +def _flatten_metadata_by_window(metadata_list: list[dict]) -> list[dict]: + """Expand metadata so each entry maps to exactly one t2w_window. + + Each output dict is a shallow copy of the original whose ``t2w_windows`` + list contains a single window. The ``uuid`` is suffixed with ``_w{idx}`` + so every entry has a unique identifier. + """ + flat: list[dict] = [] + for entry in metadata_list: + for win_idx, window in enumerate(entry["t2w_windows"]): + flat.append( + { + **entry, + "uuid": f"{entry['uuid']}_w{win_idx}", + "t2w_windows": [window], + } + ) + return flat + + +def _load_sft_metadata_from_s3( + s3_client, + jsonl_url: str, + min_frames: int, + uuid_prefix: str = "", + min_short_edge: int = 0, +) -> list[dict]: + """Load SFT metadata from a single JSONL file on S3. + + Returns one entry per video. Each entry keeps only the windows whose frame + span is at least *min_frames*; videos with no qualifying windows are dropped. + + Args: + s3_client: Boto3 S3 client + jsonl_url: S3 URL to the JSONL metadata file + min_frames: Minimum number of frames required per window + uuid_prefix: Prefix prepended to each uuid for disambiguation when + multiple JSONL files are loaded + min_short_edge: Drop videos whose shortest spatial edge (min of width, + height) is below this value. 0 disables the filter. + """ + log.info(f"Downloading SFT metadata from {jsonl_url}", rank0_only=False) + metadata_list: list[dict] = [] + num_raw_records = 0 + num_raw_windows = 0 + num_filtered_duration = 0 + num_filtered_windows = 0 + num_filtered_short_edge = 0 + + with io.BytesIO() as buffer: + if jsonl_url.startswith("s3://"): + bucket, key = parse_s3_url(jsonl_url) + s3_client.download_fileobj(Bucket=bucket, Key=key, Fileobj=buffer) + else: + path = Path(jsonl_url).absolute() + jsonl_url = str(path) + buffer.write(path.read_bytes()) + buffer.seek(0) + log.info("Finished downloading. Decoding...", rank0_only=False) + + line_iter = gzip.open(buffer, "rb") if jsonl_url.endswith(".gz") else buffer + for line in line_iter: + num_raw_records += 1 + record = json.loads(line.decode("utf-8")) + uuid = f"{uuid_prefix}{record['uuid']}" if uuid_prefix else record["uuid"] + if record["duration"] > 61.0: + print(f"Skipping video with too long duration: {uuid}, {record['duration']} > 61.0") + num_filtered_duration += 1 + continue + if min_short_edge > 0 and min(record["width"], record["height"]) < min_short_edge: + num_filtered_short_edge += 1 + continue + + windows = record.get("t2w_windows") + if not windows: + continue + + kept_windows = [] + for window in windows: + num_raw_windows += 1 + frames_in_window = window["end_frame"] - window["start_frame"] + 1 + if frames_in_window < min_frames: + num_filtered_windows += 1 + else: + kept_windows.append(window) + + if not kept_windows: + continue + + s3_path = record["s3_path"] + if "://" not in s3_path and not s3_path.startswith("/"): + # Relative path to the JSONL file + s3_path = f"{os.path.dirname(jsonl_url)}/{s3_path}" + + aspect_ratio = get_aspect_ratio(record["width"], record["height"]) + metadata_list.append( + { + "uuid": uuid, + "s3_path": s3_path, + "width": record["width"], + "height": record["height"], + "nb_frames": record.get("nb_frames"), + "framerate": record.get("framerate"), + "aspect_ratio": aspect_ratio, + "t2w_windows": kept_windows, + } + ) + + log.info( + f"Finished decoding SFT metadata from {jsonl_url}. " + f"Records: {num_raw_records}, " + f"Duration > 61s: {num_filtered_duration}, " + f"Short edge < {min_short_edge}: {num_filtered_short_edge}, " + f"Windows: {num_raw_windows}, Windows < {min_frames}f: {num_filtered_windows}, " + f"Videos kept: {len(metadata_list)}" + ) + return metadata_list + + +def get_sft_dataset( + jsonl_paths: str | list[str] = "s3://nv-00-10206-vfm/cosmos3_video_sft/human_1k/captions_full.jsonl", + resolution: str = "720", + num_video_frames: int = 93, + temporal_interval_mode: str = "entire_chunk", + frame_selection_mode: str = "center", + tokenizer_config: Optional[Any] = None, + cfg_dropout_rate: float = 0.1, + use_system_prompt: bool = False, + append_duration_fps_timestamps: bool = True, + append_resolution_info: bool = True, + cfg_dropout_keep_metadata: bool = False, + sample_by_window: bool = False, + min_short_edge: int = 0, + caption_suffix: str = "", + conditioning_fps: float = 24, + conditioning_fps_noise_std: float = 0.0, + conditioning_config: dict[int, float] | None = None, + temporal_compression_factor: int = 4, + **kwargs, +) -> SFTDataset: + """Create SFT video dataset from one or more JSONL files on S3. + + Args: + jsonl_paths: S3 path(s) to JSONL metadata file(s). A single string or + a list of strings. When multiple files are given, their samples are + concatenated and each file's uuids are prefixed with ``/`` + to avoid collisions. + resolution: Output resolution (e.g., "720", "480") + num_video_frames: Number of frames to extract from each video. + Videos with fewer frames are skipped at decode time. + Use -1 to take native chunks from the t2w_window metadata. + temporal_interval_mode: How to compute the temporal interval between sampled frames. + "force_one" — always 1 (consecutive frames at original fps). + "max_30fps" — smallest interval that keeps effective fps <= 30. + "entire_chunk" — spread num_video_frames evenly across the whole window. + frame_selection_mode: Where to select frames within the window. + "center" — center-crop temporally (default). + "first" — take the first num_video_frames from the window start. + tokenizer_config: Config for the tokenizer + cfg_dropout_rate: Dropout rate for the caption + use_system_prompt: Whether to use the system prompt during tokenization + append_duration_fps_timestamps: If True, appends duration/FPS text to captions + append_resolution_info: If True, appends resolution text to captions + cfg_dropout_keep_metadata: If True, CFG dropout fires before appending + duration/FPS/resolution text so that metadata is preserved during + unconditional guidance. If False (default), dropout fires after + and clears the entire caption including metadata. + sample_by_window: If True, each t2w_window is treated as a separate + sample (the dataset length equals the total number of windows). + If False (default), each video uuid is one sample and a random + window is chosen on every access. + min_short_edge: Drop videos whose shortest spatial edge (min of width, + height) is below this value. 0 (default) disables the filter. + caption_suffix: Text appended to every caption before the + duration/FPS/resolution templates, e.g. + ``"Overall, the video is of poor quality."``. Empty string + (default) disables the suffix. + conditioning_fps: FPS value used for duration/FPS conditioning. + A positive value is used directly (default 24). A negative + value (e.g. ``-1``) means the actual effective FPS + (``original_fps / temporal_interval``) is used instead. + conditioning_fps_noise_std: Standard deviation of log-normal + multiplicative noise applied to ``conditioning_fps``. The FPS + is multiplied by ``exp(N(0, std))``. 0.0 (default) disables + the noise. + conditioning_config: Weighted distribution mapping latent-frame counts + to unnormalized probabilities for image-to-video conditioning. + Example: ``{0: 0.7, 1: 0.2, 2: 0.1}``. ``None`` disables + conditioning (all frames are generation targets). + temporal_compression_factor: VAE temporal compression factor used to + convert pixel frame count to latent frame count. + Returns: + SFTDataset instance + """ + log.info(f"Unknown kwargs for get_sft_dataset: {kwargs}") + assert resolution in VIDEO_RES_SIZE_INFO.keys(), "The provided resolution cannot be found in VIDEO_RES_SIZE_INFO." + + if isinstance(jsonl_paths, str): + jsonl_paths = [jsonl_paths] + + if INTERNAL: + with open("credentials/gcs.secret", "r") as f: + credentials = json.load(f) + else: + credentials = {} + + s3_client = boto3.client("s3", **credentials) + + metadata_list: list[dict] = [] + for idx, jsonl_url in enumerate(jsonl_paths): + prefix = f"{idx}/" if len(jsonl_paths) > 1 else "" + metadata_list.extend( + _load_sft_metadata_from_s3( + s3_client, + jsonl_url, + min_frames=61, + uuid_prefix=prefix, + min_short_edge=min_short_edge, + ) + ) + + total_windows = sum(len(m["t2w_windows"]) for m in metadata_list) + log.info( + f"Finished loading metadata from {len(jsonl_paths)} file(s). " + f"Total videos: {len(metadata_list)}, total windows: {total_windows}" + ) + + if sample_by_window: + metadata_list = _flatten_metadata_by_window(metadata_list) + log.info(f"sample_by_window=True: flattened to {len(metadata_list)} samples (one per window)") + + # Deterministic shuffle based on the sha256 hash of uuid + metadata_list.sort(key=lambda x: hashlib.sha256(x["uuid"].encode("utf-8")).hexdigest()) + + dataset = SFTDataset( + metadata=metadata_list, + num_video_frames=num_video_frames, + resolution=resolution, + s3_credentials=credentials, + temporal_interval_mode=temporal_interval_mode, + frame_selection_mode=frame_selection_mode, + tokenizer_config=tokenizer_config, + cfg_dropout_rate=cfg_dropout_rate, + use_system_prompt=use_system_prompt, + append_duration_fps_timestamps=append_duration_fps_timestamps, + append_resolution_info=append_resolution_info, + cfg_dropout_keep_metadata=cfg_dropout_keep_metadata, + caption_suffix=caption_suffix, + conditioning_fps=conditioning_fps, + conditioning_fps_noise_std=conditioning_fps_noise_std, + conditioning_config=conditioning_config, + temporal_compression_factor=temporal_compression_factor, + ) + return dataset diff --git a/cosmos_training/cosmos/data/vfm/processors/__init__.py b/cosmos_training/cosmos/data/vfm/processors/__init__.py new file mode 100644 index 00000000..1b06e89e --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/processors/__init__.py @@ -0,0 +1,176 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from types import SimpleNamespace +from typing import Optional + +from transformers import PreTrainedTokenizerFast + +from cosmos.data.vfm.processors.nemotron3densevl_processor import Nemotron3DenseVLProcessor +from cosmos.data.vfm.processors.nemotronvl_processor import NemotronVLProcessor +from cosmos.data.vfm.processors.qwen3vl_processor import Qwen3VLProcessor +from cosmos.model.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer + +_VARIANT_TO_CREDENTIALS = { + "s3": ("credentials/s3_training.secret", "checkpoints-us-east-1"), + "gcp": ("credentials/gcp_checkpoint.secret", "nv-00-10206-checkpoint"), + # "hf" => no S3 backing store: pass empty credentials/bucket so the downloader + # falls back to a direct HuggingFace Hub download (matches the legacy + # ``download_tokenizer_files(model_name, "hf")`` behavior on origin/main, which + # simply returned the model name and let from_pretrained pull from HF). + "hf": ("", ""), +} + +# S3 prefix under which HuggingFace model files are stored in the checkpoint buckets. +_LLM_S3_PREFIX = "cosmos3/pretrained/huggingface" + + +class LLMTokenizerProcessor: + """Unified processor interface wrapper for LLM-only (no-vision) tokenizers. + + Exposes the same interface as the VLM processors so all augmentors and + model code can treat LLM-only and VLM configs uniformly. + """ + + is_unified_processor: bool = True + + def __init__(self, tokenizer): + from cosmos.data.vfm.sequence_packing import add_special_tokens + + tokenizer, _ = add_special_tokens(tokenizer) + self.processor = SimpleNamespace(tokenizer=tokenizer) + + def tokenize_text( + self, + caption: str, + is_video: bool = False, + use_system_prompt: bool = False, + system_prompt: Optional[str] = None, + ) -> list[int]: + from cosmos.model.vfm.vlm.qwen3_vl.utils import tokenize_caption + + return tokenize_caption( + caption, + self.processor.tokenizer, + is_video=is_video, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + + +def _patch_nemotron_llm_tokenizer_vision_tokens(destination_dir: str) -> None: + """Remap reserved placeholder tokens to vision special tokens in-place. + + The Nemotron LLM tokenizer reserves ```` / ```` + at IDs 20/21 -- the same slots the VLM tokenizer uses for + ``<|vision_start|>`` / ``<|vision_end|>``. Renaming them here keeps + every vision-token ID inside the original vocab_size (131072) so no + embedding-layer resize is needed during FSDP training. The function is + idempotent: re-applying it after the tokens are already renamed is a no-op. + """ + remap = {"": "<|vision_start|>", "": "<|vision_end|>"} + + tokenizer_json_path = os.path.join(destination_dir, "tokenizer.json") + if os.path.exists(tokenizer_json_path): + with open(tokenizer_json_path) as f: + data = json.load(f) + for entry in data.get("added_tokens", []): + if entry["content"] in remap: + entry["content"] = remap[entry["content"]] + vocab = data.get("model", {}).get("vocab", {}) + for old_name, new_name in remap.items(): + if old_name in vocab: + vocab[new_name] = vocab.pop(old_name) + with open(tokenizer_json_path, "w") as f: + json.dump(data, f) + + tokenizer_config_path = os.path.join(destination_dir, "tokenizer_config.json") + if os.path.exists(tokenizer_config_path): + with open(tokenizer_config_path) as f: + tc_data = json.load(f) + for entry in tc_data.get("added_tokens_decoder", {}).values(): + if entry.get("content") in remap: + entry["content"] = remap[entry["content"]] + with open(tokenizer_config_path, "w") as f: + json.dump(tc_data, f) + + +def _download_llm_tokenizer( + tokenizer_type: str, + credentials: str, + bucket: str, + cache_dir: Optional[str] = None, +) -> str: + from cosmos.utils.vfm.vlm.pretrained_models_downloader import maybe_download_hf_model_from_s3 + + return maybe_download_hf_model_from_s3( + tokenizer_type, + credentials=credentials, + bucket=bucket, + include_model_weights=False, + cache_dir=cache_dir, + s3_prefix=_LLM_S3_PREFIX, + ) + + +def build_processor( + tokenizer_type: str, + config_variant: Optional[str] = None, + credentials: Optional[str] = None, + bucket: Optional[str] = None, + cache_dir: Optional[str] = None, +): + if credentials is None or bucket is None: + if config_variant is None: + config_variant = "s3" + if config_variant not in _VARIANT_TO_CREDENTIALS: + raise ValueError(f"config_variant must be one of {list(_VARIANT_TO_CREDENTIALS)}, got {config_variant!r}") + variant_credentials, variant_bucket = _VARIANT_TO_CREDENTIALS[config_variant] + credentials = credentials if credentials is not None else variant_credentials + bucket = bucket if bucket is not None else variant_bucket + elif config_variant is not None: + raise ValueError("Provide either config_variant or (credentials, bucket), not both") + if "Qwen/Qwen3-VL" in tokenizer_type or "Siglip2-Qwen3-1.7B" in tokenizer_type: + return Qwen3VLProcessor(tokenizer_type, credentials=credentials, bucket=bucket, cache_dir=cache_dir) + elif "nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16" in tokenizer_type: + return NemotronVLProcessor(tokenizer_type, credentials=credentials, bucket=bucket, cache_dir=cache_dir) + elif "NVIDIA-Nemotron-3-Dense-VL" in tokenizer_type or "Qwen3-2B-ViT" in tokenizer_type: + return Nemotron3DenseVLProcessor(tokenizer_type, credentials=credentials, bucket=bucket, cache_dir=cache_dir) + elif "Qwen/Qwen3-0.6B" in tokenizer_type: + local_path = _download_llm_tokenizer(tokenizer_type, credentials, bucket, cache_dir) + return LLMTokenizerProcessor(Qwen2Tokenizer.from_pretrained(local_path)) + elif "Nemotron/NVIDIA-Nemotron-3-2B-BF16" in tokenizer_type: + local_path = _download_llm_tokenizer(tokenizer_type, credentials, bucket, cache_dir) + _patch_nemotron_llm_tokenizer_vision_tokens(local_path) + return LLMTokenizerProcessor(PreTrainedTokenizerFast.from_pretrained(local_path, trust_remote_code=True)) + else: + raise ValueError(f"Tokenizer type {tokenizer_type} not supported") + + +def build_processor_lazy(*args, **kwargs): + """LazyCall wrapper that resolves ``build_processor`` on this module at call time. + + LazyCall captures its target at config-construction time, so a direct + ``L(build_processor)`` would freeze the original function reference and + bypass any later ``monkeypatch.setattr`` on this module's + ``build_processor`` attribute. This wrapper performs a fresh module-level + lookup on every call, so test fixtures patching ``build_processor`` are + honored when the config is instantiated. + """ + import sys + + return sys.modules[__name__].build_processor(*args, **kwargs) diff --git a/cosmos_training/cosmos/data/vfm/processors/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/processors/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6bc116b7909c70cd9a0ab0ebe7d582993be784a GIT binary patch literal 8120 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 XsCvn(uSWj~A@SrS9iWfXL^A&mmlk(O literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/processors/__pycache__/nemotron3densevl_processor.cpython-312.pyc b/cosmos_training/cosmos/data/vfm/processors/__pycache__/nemotron3densevl_processor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f91019b56518eeb39b4417831c932da4fb17e4a GIT binary patch literal 13411 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 |h^rW$t7Aeox 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/data/vfm/processors/nemotron3densevl_processor.py b/cosmos_training/cosmos/data/vfm/processors/nemotron3densevl_processor.py new file mode 100644 index 00000000..3a4b84eb --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/processors/nemotron3densevl_processor.py @@ -0,0 +1,286 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Dict, List, Optional + +import numpy as np +import torch +from PIL import Image +from qwen_vl_utils.vision_process import smart_resize +from transformers.models.auto.processing_auto import AutoProcessor + +from cosmos.utils import log +from cosmos.data.vfm.sequence_packing import add_special_tokens +from cosmos.model.vfm.vlm.qwen3_vl.utils import tokenize_caption +from cosmos.utils.vfm.vlm.pretrained_models_downloader import maybe_download_hf_model_from_s3 + + +def convert_string_content_to_list_content(messages: List[Dict]) -> List[Dict]: + """ + Convert the string content to a list of dicts. + """ + for message_id, message in enumerate(messages): + if isinstance(message["content"], str): + messages[message_id]["content"] = [{"type": "text", "text": message["content"]}] + return messages + + +def maybe_parse_video_content( + messages: List[Dict], +) -> tuple[int, Optional[list[float]], Optional[list[int]], Optional[list[list[int]]]]: + """ + Convert the string content to a list of dicts. + """ + num_video = 0 + video_fps = [] + video_total_num_frames = [] + video_frames_indices = [] + for message_id, message in enumerate(messages): + if isinstance(message["content"], list): + for sub_content in message["content"]: + if sub_content.get("type", "") == "video" and isinstance(sub_content["video"], list): + num_video += 1 + fps = sub_content.get("fps", None) + if fps is None: + log.critical( + f"fps is None for video {sub_content}. Better to set the fps explicitly", rank0_only=False + ) + video_fps.append(fps) + video_total_num_frames.append(len(sub_content["video"])) + video_frames_indices.append(list(range(video_total_num_frames[-1]))) + return num_video, video_fps, video_total_num_frames, video_frames_indices + + +class Nemotron3DenseVLProcessor: + # This is a wrapper around the AutoProcessor class to add some helper functions + is_unified_processor: bool = True # sentinel for TextTokenizerTransform duck-type check + + def __init__( + self, + name="Qwen/Qwen3-VL-2B-Init", + credentials: str = "./credentials/s3_training.secret", + bucket: str = "checkpoints-us-east-1", + cache_dir: str = None, + ): + self.name = name + if os.path.isdir(name): + model_name_or_path_local = name + else: + model_name_or_path_local = maybe_download_hf_model_from_s3( + name, credentials, bucket, include_model_weights=False + ) + + self.processor = AutoProcessor.from_pretrained(model_name_or_path_local, trust_remote_code=True) + log.info("Successfully loaded processor from local cache") + add_special_tokens(self.processor.tokenizer) + + if hasattr(self.processor, "image_token"): + self.image_token_id = self.processor.tokenizer.convert_tokens_to_ids(self.processor.image_token) + else: + self.image_token_id = None + if hasattr(self.processor, "video_token"): + self.video_token_id = self.processor.tokenizer.convert_tokens_to_ids(self.processor.video_token) + else: + self.video_token_id = None + self.eos_id = self.processor.tokenizer.eos_token_id + self.pad_id = self.processor.tokenizer.pad_token_id + self.vision_end_id = self.processor.tokenizer.convert_tokens_to_ids("") + + # Helper attributes for the dataloader video decoding function + self.shortest_edge = self.processor.image_processor.size["shortest_edge"] + self.min_height_width = int(np.sqrt(self.shortest_edge)) + self.patch_size = self.processor.video_processor.patch_size + self.temporal_patch_size = self.processor.video_processor.temporal_patch_size + self.merge_size = self.processor.video_processor.merge_size + self.use_smart_resize = True + if self.pad_id is None: + self.pad_id = self.eos_id + + def apply_chat_template( + self, + messages, + add_generation_prompt=False, + return_tensors="pt", + tokenize=True, + **kwargs, + ): + """ + Return: + inputs: dict + input_ids: torch.Tensor, shape: (N_token) + attention_mask: torch.Tensor, shape: (N_token) + texts: str, the raw text + image_sizes: torch.Tensor, shape (N_img, 2) + pixel_values: torch.Tensor, shape (N_img_patch, 3, 224, 224) + """ + + # messages = [msg for msg in messages if msg.get("role") != "system"] + assert tokenize, "tokenize must be True" + assert return_tensors == "pt", "return_tensors must be pt" + # Note: this tokenizer does not support "content": str, it always expect "content" entry to be a list of dicts + messages = convert_string_content_to_list_content(messages) + kwargs = {} + for message_id, message in enumerate(messages): + if isinstance(message["content"], list): + for sub_content in message["content"]: + if sub_content.get("type", "") == "image": + image = sub_content["image"] + max_pixels = sub_content.get("max_pixels", self.processor.image_processor.size["longest_edge"]) + min_pixels = sub_content.get("min_pixels", self.processor.image_processor.size["shortest_edge"]) + assert isinstance(image, Image.Image), ( + "image must be a url string for now, not support list of images for one content" + ) + width, height = image.size + resized_height, resized_width = smart_resize( + height, + width, + factor=32, + min_pixels=min_pixels, + max_pixels=max_pixels, + ) + image = image.resize((resized_width, resized_height)) + sub_content["image"] = image + + num_video, video_fps, video_total_num_frames, video_frames_indices = maybe_parse_video_content(messages) + if num_video > 0: + # Here we add the args to avoid the error: + # File "/usr/local/lib/python3.12/dist-packages/transformers/video_processing_utils.py", line 321, in _decode_and_sample_videos + # raise ValueError( + # ValueError: Sampling frames from a list of images is not supported! Set `do_sample_frames=False`. + # kwargs["videos_kwargs"] = dict(do_sample_frames=False) + assert num_video == 1, "only support one video for now" + fps = video_fps[0] + total_num_frames = video_total_num_frames[0] + frames_indices = video_frames_indices[0] + kwargs.update( + { + "do_sample_frames": False, + "video_metadata": dict(fps=fps, total_num_frames=total_num_frames, frames_indices=frames_indices), + } + ) + + inputs = self.processor.apply_chat_template( + messages, + tokenize=tokenize, + add_generation_prompt=add_generation_prompt, + return_dict=True, + return_tensors=return_tensors, + # padding="max_length", + # max_length=16000, + # truncation=False, + **kwargs, + ) + + # Convert batch features into single features + # By default, the processor returns a batch of features, but we use processor in dataloader, so we need to convert it to single features + inputs["input_ids"] = inputs["input_ids"][0] # [N_token] + inputs["attention_mask"] = inputs["attention_mask"][0] # [N_token] + num_image_tokens = inputs["input_ids"] == self.image_token_id # [N_token] + num_video_tokens = inputs["input_ids"] == self.video_token_id # [N_token] + return inputs + + def add_assistant_tokens_mask(self, tokens): + """ + Add a mask to the assistant tokens. + This is used to mask out tokens that are not generated by the assistant (e.g., system prompts, user prompts, chat templates), such that in the loss computation, only the tokens generated by the assistant are used. + If there are multiple turns in the conversation, the mask will mask all the assistant tokens in each turn. + + Args: + tokens (Union[List[int], torch.Tensor]): The tokens to add the mask to. + Returns: + Union[List[bool], torch.Tensor]: The mask. True for tokens generated by the assistant (i.e. should apply loss on), False for tokens not generated by the assistant. + """ + if isinstance(tokens, torch.Tensor) and tokens.ndim == 2: + mask = torch.stack( + [self.add_assistant_tokens_mask(tokens[i]) for i in range(tokens.shape[0])] + ) # [B,N_token] + assert mask.shape == tokens.shape + return mask + np_tokens = tokens.cpu().numpy() if isinstance(tokens, torch.Tensor) else np.array(tokens) + assert np_tokens.ndim == 1 + + # Constants defining bos, eos and fixed offsets. + BOS_TOKEN = "<|im_start|>" + EOS_TOKEN = "<|im_end|>" + ROLE = "assistant" + # Offsets: skip the bos + "assistant\n" (always 3 tokens) and include the eos (+1) for supervision + START_OFFSET = 3 + END_OFFSET = 1 + + # Retrieve token IDs for the markers and the role. + bos_token_id = self.processor.tokenizer.convert_tokens_to_ids(BOS_TOKEN) + eos_token_id = self.processor.tokenizer.convert_tokens_to_ids(EOS_TOKEN) + role_id = self.processor.tokenizer.convert_tokens_to_ids(ROLE) + role_ids = self.processor.tokenizer.encode( + ROLE, add_special_tokens=False + ) # In case the role_id corresponds to multiple tokens, decode it back to string for accurate comparison + think_start_id = self.processor.tokenizer.convert_tokens_to_ids("") + think_end_id = self.processor.tokenizer.convert_tokens_to_ids("") + + # Locate all positions where the start and end markers appear. + start_indices = np.where(np_tokens == bos_token_id)[0] + end_indices = np.where(np_tokens == eos_token_id)[0] + + # Initialize the mask with False values. + masks = np.zeros_like(np_tokens, dtype=bool) + assert len(start_indices) == len(end_indices) + # For each pair of bos/eos, check if the role is 'assistant' + # and apply the mask accordingly. + for start, end in zip(start_indices, end_indices): + end_pos = None + if np_tokens[start + 1] == role_id: + # Mask tokens from after the assistant header (start+3) to include the end marker (end+1) + masks[start + START_OFFSET : end + END_OFFSET] = True + end_pos = start + START_OFFSET + elif all(np_tokens[start + 1 : start + 1 + len(role_ids)] == role_ids): + masks[start + START_OFFSET + len(role_ids) - 1 : end + END_OFFSET] = True + end_pos = start + START_OFFSET + len(role_ids) - 1 + if end_pos is not None and np_tokens[end_pos] == think_start_id: + masks[end_pos] = False + if np_tokens[end_pos + 1] == think_end_id: + masks[end_pos + 1] = False + + assert masks.shape == np_tokens.shape + if isinstance(tokens, torch.Tensor): + return torch.from_numpy(masks) + else: + return masks.tolist() + + def tokenize_text( + self, + caption: str, + is_video: bool = False, + use_system_prompt: bool = False, + system_prompt: Optional[str] = None, + ) -> list[int]: + """Tokenize a text caption using the underlying tokenizer. + + Delegates to tokenize_caption so VFM diffusion augmentors and VLM dataloaders + share the same tokenization logic through a single processor object. + """ + return tokenize_caption( + caption, + self.processor.tokenizer, + is_video=is_video, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + + def encode(self, *args, **kwargs): + return self.processor.encode(*args, **kwargs) + + def decode(self, *args, **kwargs): + return self.processor.decode(*args, **kwargs) diff --git a/cosmos_training/cosmos/data/vfm/processors/nemotronvl_processor.py b/cosmos_training/cosmos/data/vfm/processors/nemotronvl_processor.py new file mode 100644 index 00000000..6b2d54dc --- /dev/null +++ b/cosmos_training/cosmos/data/vfm/processors/nemotronvl_processor.py @@ -0,0 +1,590 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Dict, List, Optional + +import numpy as np +import torch +from PIL import Image +from transformers.models.auto.processing_auto import AutoProcessor +from transformers.processing_utils import VideosKwargs +from transformers.video_utils import VideoMetadata + +from cosmos.utils import log +from cosmos.data.vfm.sequence_packing import add_special_tokens +from cosmos.model.vfm.vlm.qwen3_vl.utils import tokenize_caption +from cosmos.utils.vfm.vlm.pretrained_models_downloader import maybe_download_hf_model_from_s3 + +nemotron_chat_template = """ +{%- set ns = namespace(enable_thinking=false, has_sys_prompt=false, non_tool_system_content='', has_video=false, explicit_think_requested=false) -%} +{%- set msg = namespace(content='') -%} +{%- for message in messages -%} + {%- if message['role'] == 'system' -%} + {%- set ns.has_sys_prompt = true -%} + {# Extract system content without tool flags #} + {%- if message['content'] is string -%} + {%- set ns.non_tool_system_content = message['content'].replace('', '<_end_think>').replace('/think', '').replace('/no_think', '').replace('<_end_think>', '').strip() -%} + {%- else -%} + {%- set ns.non_tool_system_content = '' -%} + {%- for content in message['content'] -%} + {%- if content['type'] == 'text' -%} + {%- set ns.non_tool_system_content = ns.non_tool_system_content + content['text'].replace('', '<_end_think>').replace('/think', '').replace('/no_think', '').replace('<_end_think>', '') -%} + {%- endif -%} + {%- endfor -%} + {%- set ns.non_tool_system_content = ns.non_tool_system_content.strip() -%} + {%- endif -%} + {%- endif -%} + {# Check for video content in all messages #} + {%- if message['content'] is not string -%} + {%- for content in message['content'] -%} + {%- if content['type'] == 'video' or content['type'] == 'video_url' -%} + {%- set ns.has_video = true -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- if message['content'] is string -%} + {%- if message['role'] == 'user' or message['role'] == 'system' -%} + {%- if '/think' in message['content'].replace('', '') -%} + {%- set ns.enable_thinking = true -%} + {%- set ns.explicit_think_requested = true -%} + {%- elif '/no_think' in message['content'] -%} + {%- set ns.enable_thinking = false -%} + {%- endif -%} + {%- endif -%} + {%- else -%} + {%- for content in message['content'] -%} + {%- if content['type'] == 'text' -%} + {%- if message['role'] == 'user' or message['role'] == 'system' -%} + {%- if '/think' in content['text'].replace('', '') -%} + {%- set ns.enable_thinking = true -%} + {%- set ns.explicit_think_requested = true -%} + {%- elif '/no_think' in content['text'] -%} + {%- set ns.enable_thinking = false -%} + {%- endif -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} +{%- endfor -%} + +{{- bos_token -}} +{%- if messages[0]['role'] != 'system' -%} + {{- 'System\n' -}} +{%- else -%} + {{- 'System\n' + ns.non_tool_system_content }} +{%- endif -%} + +{%- if tools -%} + {%- if ns.non_tool_system_content != '' -%} + {{- '\n\n' -}} + {%- endif -%} + {{- 'You can use the following tools to assist the user if required:\n' -}} + {{- '[' -}} + {%- for tool in tools -%} + {{- (tool.function if tool.function is defined else tool) | tojson -}} + {{- ', ' if not loop.last else '' -}} + {%- endfor -%} + {{- ']\n\n' -}} + + {{- 'If you decide to call any tool(s), use the following format:\n' -}} + {{- '[{"name": "tool_name1", "arguments": "tool_args1"}, ' -}} + {{- '{"name": "tool_name2", "arguments": "tool_args2"}]\n\n' -}} + + {{- 'The user will execute tool-calls and return responses from tool(s) in this format:\n' -}} + {{- '[{"response": "tool_response1"}, ' -}} + {{- '{"response": "tool_response2"}]\n\n' -}} + + {{- 'Based on the tool responses, you can call additional tools if needed, ' -}} + {{- 'correct tool calls if any errors are found, or just respond to the user.' -}} +{%- endif -%} +{{- '\n' -}} + +{%- set messages = messages[1:] if messages[0]['role'] == 'system' else messages -%} + +{# Prevent no user or assistant message #} +{%- if messages|length == 0 -%} + {%- set messages = [{'role': 'user', 'content': ''}] -%} +{%- endif -%} + +{%- for message in messages %} + {%- if message['content'] is string -%} + {%- set msg.content = message['content'].replace('', '<_end_think>').replace('/think', '').replace('/no_think', '').replace('<_end_think>', '').strip() -%} + {%- else -%} + {%- set msg.content = '' -%} + {%- set mm_content = '' -%} + {%- set counters = namespace(images=0, videos=0) -%} + + {%- for content in message['content'] -%} + {%- if content['type'] == 'image' -%} + {%- set counters.images = counters.images + 1 -%} + {%- elif content['type'] == 'video' -%} + {%- set counters.videos = counters.videos + 1 -%} + {%- elif content['type'] == 'text' -%} + {%- set msg.content = msg.content + content['text'] -%} + {%- endif -%} + {%- endfor -%} + {%- if '' in msg.content -%} + {%- set counters.images = 0 -%} + {%- endif -%} + {%- if '

;y;fX#o5z2&pe8=e@&y9%@Hx9I5!BRRiHIZ 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 Ab^rhX literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/__init__.py b/cosmos_training/cosmos/model/vfm/algorithm/loss/__init__.py new file mode 100644 index 00000000..21565269 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/algorithm/loss/__init__.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Loss functions used by VFM (rectified flow) and VLM (next-token CE) training paths.""" + +__all__: list[str] = [] + +from cosmos.model.vfm.algorithm.loss.cross_entropy import cross_entropy_loss + +__all__ += ["cross_entropy_loss"] + +from cosmos.model.vfm.algorithm.loss.load_balancing import compute_load_balancing_loss + +__all__ += ["compute_load_balancing_loss"] + +from cosmos.model.vfm.algorithm.loss.time_weight import TrainTimeWeight + +__all__ += ["TrainTimeWeight"] + +from cosmos.model.vfm.algorithm.loss.flow_matching import compute_flow_matching_loss + +__all__ += ["compute_flow_matching_loss"] diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b002277242b7915aa816e3173e89c9981223030 GIT binary patch literal 927 zcmaiyO>fgM7{{IUC0)~M;!V>)RM~CWwB!p+To7<6Tqs3~gsd!a(+tOsY?sm<(726H zfcOY}7A~#0jpM2x5!8>ejqalmWTOdEcq;Po5a zq?>8eZx+09gKttRZTYQ&H%mUToYv{fE|=0Cj991=&ZIq(B(jeucK_*fdruIcg9ONc z^08wFEVBDu*kI(nI#m2EVfN!Ej;%zHu!O~S7O0_ga}(o$QjC9UtB#T14h7tT2~&b+ z6HH+tpe%apLCDi=qzJ$u!p8v(SO_Q0V`}r5t)4!=m!#x1N#dar^*O&^=4y2~4OBQ> zrkW7)!f9(M5f3pI8vybuO{B^zQi*&EG&06jQ253;KMc7{xpY$=5$e7hq;5cCE)q3N zU9H$%F;VEx-E}#uu7oy}AFNAK;1rMlTU{@0r2s;E|B9|)MAu(DIs&`I8_2PjaiIpB zCR}=R4PGYr2|RMSh6j)Ym(S_J&mg2X30SPVS|nkf5hnRagrp?PjK#4<4Lzu#x2&P3 zYv^GNKZ?TG%_i54BM2K&@<`kPr3Q5Q28Lf1!!Uk#kl`B__qNZ>^SkHar=9O;`x138 xkU4GHQ)_pIcF)X9)GvTDwf1LdUxQ}_csR8>Gt|+bSAe~#<;;-t(G<7fhJU&4C}#iw literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/cross_entropy.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/cross_entropy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aab109a924794192c59d18dd91def48827ceca81 GIT binary patch literal 4284 zcmbtXO>7&-6<+>|A}P^7p=|k2;}~wqNF-&)L2Sixkw|vZAIpN|7^sS5G2{-(m6p5o z%&sH~RHFb5-~dGf0|o3BMc|-^Ca{6}RHW#k0dnlctWro=q^Oag2j5iKz4+3;H_Ig{ zx9Fi+i8F6zXWqQO`QF<<4GhE;c#xO2u(p<1NCuSt_MaB@ssHS?J}vT-$ihG) zoQ*k+u%b*|B!(pfDO;Re`@v;mbE=!VS;@!avlh3RM#|i(k&-29mQcBouUm#*YUJfQ z#`A_zQ+-;L?G8;v!z`(SmYG^&@l2hTmS}~Eg2yPwjWz`=3m(ftn@ypIWucd4y^#f} zu2vc0Omj-MZkZ@{%`qscIl?A2YL}{H*U5yf(vod7h;5NNXM*u%<|$mKT(=wXBC}T* zGs$d~3P#3BrXwJ!Q@ffa)YRH9Pm_#Y)di_pnqx32%Zk+_3$7RBtBs!%joFOXbQ7#s zY>-l2t#He!djgmxHW7~#GBrz>Ns9)o$@nYsLSJK-b@WUF;%3VoU733^oMwYtGLsqfMk^lYn$TS^ve5H`xZ zqT5x;uj4H^QnEmmqy->Uq>(jUJY!|#HDj5KK*&F9`N_$S>F1qV$KVB$sTfv~8mg&E z%~YwGAfx0I%bm>j6zynZGMXT!ci+2-b1&ykjh`dB=>?bN@Dd|s z-C%?kE%18$5yD?K;3lYPTAs@U3}aX7Ui{9Kq8>oX7HI&?z~kQq5iFYskAe4s2$=5$ z%fVrIy%^|O-zM+2`^7fc_O8%^O=U6CJLAz>@NS#aT{sJ$^=K1Lljnu7{eRE%`L6dh z!|z^ehX32?zQkH+XYr+gmGzGg%V-;3M@oX2N9W)FB2 za2xcgZr_Y)(R+Oztz*qtGq?s8npeKD6tNyD`cbZ>mqCWT-Lrklmwi31h1L_z(7TuH z{r0Tc5kzHQB>DdCHwwE(HsZQb< zA(t`70aoCBFS06K)&Vr&Mt{Qujrb(bM!r|C%uT(aPR(7JpI0x=T%Efz`$JNM1kNB8 zW-jF=4ygc6+^RRcU1h_f_KT-Ui3%GNBeHvySA>_uaAa5&-4+uh^V%4>I!3N#iQGc~ z&IM?IS(DQh%&p8ImSaN(0{s9HAvaiMyGB<%z>&OYkr_U(EU-rW>+(UvnUH(912E=6 zmaet&3b*^utR-~B8c0Tnl2{_>6%$TWbyH)jgu>ZuwPZM&Uj?CY5GH#>fzdN*ctXNH zXct3JBZ+#2FWb1~n`Av;B_<3u`D@WB4=vcT9U+kib${F4qdws8~??S8D~7 zzp%?E`Fg|Mw`)c!$+MuuDfo$t5IGyl&Ut0%$S+^LE$#$wi`$oOzP**sx6=8|^!Qf# zrB?c-`^8Vc^~cfQkA7DCLt!)hy^Wde^kG^B1+4g5NF|M;KDi%(+8p1mKu^Zq;A$wOO7(n^xel)Z>RP@7|OO%*$1&KFc~^NuM?>h)ghN2rYJ4xlh$&Zt*p)*_QbFK8b&GaiM^bj;stS@894^r=^ zZV%o&^V`W!CbveU5>7&bDzLjb~V)B|A2 z3@&S*s%lnARo%F%mK}g*rm7q(12<|n>JV@oODCSjnZ2G!co5f%4qOyB(KFy!Xt?{m zMW&fAJElzS)Nq5Q>BhTbjs=ShsZSLC0@4|1p2p65AC?#RSr{Th2?5PcC=dwzme^toel#vI@2srNfliOqC_>rZ*b_SK`z9%V|lS2I)K-%Cl literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/flow_matching.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/flow_matching.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63f03b69334db7419e1fe29ebfdee3392d4c8f66 GIT binary patch literal 5360 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/load_balancing.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/algorithm/loss/__pycache__/load_balancing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93af22c37cc81d9e3311266e5a8b7103bc098dda GIT binary patch literal 3255 zcmcH*+inv_bk<(q6OtGl0!f-lN{LO0EufK*xu`9as(}D1po##p*4}Zv&R)#yI%!fz z3RR^l>O)hh5=eb0kFCN7^dETfHkE9xS{14Efj3KuREd}N%&s?fLXwI~9oe&I&dj;Y zxz5wh&M<;uZ1{@leF*)*7Vd~Nf%OUi50QvOPDWGg>ybIdGv!gdQ(lg(y)v)(rhGQc z%YG#=6#&>r0wT8%KuGkHU=s?EkQgMs<|;%`@qjC?^>udhVKMv#KNXqh5|I~d^J>Cl z1uotss;*Hha9yB=B*+QQ3ST6%QjSa#y#TAomG`eq5zQPxQe)|ylLx5WJ0ad%q=CMUT;FY7{wmv+djJ-7WsdX>;IqR`oXgM z+ck1vSFehHuib?RSU+gQT>4_K3$?y&yL~OJ3sr)Z0JDzUphQS`3XTqA7#B39R5l1U3IxlVAmXeb3u;bMr(vnKB~+Mp1?V~!augV2L6)7fdNOQtxC9&=Nlf@I`rz$y=0H6`#Y6%E zYV#@Uj<7rxa)zX-Sj%HWn}J-#RD-L6UYesS`Lsl+VY4&}5^M|V433Y;T27Ehu#=jJ z<5>HTeA_O{IP1WATGqh6o4lJhe~(Vv}3WjC~a!V0cw4}0oZ{da^d<_V~y(KC{P&0Nc&`gPH+1Ij1GoH7J zWo(43BumCTi#NvH_QYP1wwsc6Yj8YogHlc+svD*|XX&OkPS85U4%`gyo$2re$ek>~ zZp=dpx-r^vQ(JFx7&{0^5WSGZ*Hn2PtAvP>3H5js~#mLs~W4o!f#rY1A?Npqr zno1a}_C#O|ZM)jUyt-uV{tPQukz{YvGf|uk@&NLcx~XkIdHzpAF4-3YtbjGkD!T<;mI^~BAd_|imuBv~7oFh?e;{Lr&N=StttfkO>H>h7<09jyj0SL2u0 zVD38l9FNxUNfV!}cMRA2-u$(1=)2I$XOHjH-WoUIcW`_ozz>K23Zh8w^4njHH9{!# z#zq+R4%T9$W^A|2Q~pQ-Y_>tHvO z=%9z;WA)i#mlP$41u4tgXVQsFJAj>_J+j-GWLxQ=@bo}d((!ZB_Hz>aoSc=Fg^DSMql$Q#u1J5O%gy+0y4!geuCFN0@+V_7EWX#$q)&7YC|g>Qt}11(l|URofLb9V(I^2+Xytmx&f4o8cV^tk z!AKFx!5}2)DM)Q4jvx-DC;kX7CgBjRdO+%_w@^ny;?y_0-jr4u$#349_r5pp?fb^B zip6QbB@fKjeG}kMWu(iPC}RtiZScS&0aQVQE8uBc;OWn`B%(_C#5Vp?Mx~w{B~qUl z<8!EN1BEK_psG!Qm+=VI8ye;@tA?jjvsL&QnCR#?wv1}FZ`#?Yw9IRMxa@c6D)pOd zGRmySdp*5GVN^lN9#>Ip!$9*$`%3_h7v0eY+5lWUPu75-MGh@!rRqn!rzy0^eyf_D zoZlsVu#HNT=QJt651Uc87y1pxI~!kJ3xmEjv8;dzVI@(PFD%jPb{Ut{v;5Gye%aPI zesYLPA@c=wMSYEWy?}CCj|@SB2A@K293oUtmSuhT#R+;sa_Th1x}$P6Sl~0^)S2ku z*5#~4>oT?7?`xP>s_%wHIz2(TaQd~dseW<-a#@##g7tWvrYDDpN!kwcXyE+V=s2~Y z$+$1qI*w}1kvI)E>2Imqk8-YyH%ZssIPt$>Ev24g*ogQV#U|_*pfLOB)=vKP-rT3d z%Wsb_42=i*y;Dm=<4tbiZK?e5_PyJW{hiX{D7P}QS9WtNuS<(##6pzeRQ7oIpV_N# z8P9$b6`cLi6HWLqFArJMN@SPxG}e=h!8Sq`JHY+J2XT(EZIiF1XD6zUyG(#NQE2tZ5ov;On&ok 1): + Per-rank mean CE loss × loss_scaling_factor. + Rationale: each CP rank sees a different segment of the sequence; computing + a weighted-mean here would require knowing each rank's valid-token count, + which is expensive. The simpler per-rank mean × scaling is consistent with + cosmos-rl's implementation. + + CP disabled (cp_group is None or cp_group.size() == 1): + Sum CE loss / (global_n_valid_tokens + 1e-8) × (num_dp_workers × scaling). + The ×num_dp_workers compensates for FSDP's gradient averaging across DP + ranks, ensuring the effective gradient equals the gradient of the global + mean loss even with unbalanced per-rank token counts. + Reference: async_safe_ce:97-109 in the source file above. +""" + +from __future__ import annotations + +from typing import Optional + +import torch +import torch.distributed as dist +import torch.nn.functional as F + + +def cross_entropy_loss( + logits: torch.Tensor, + labels: torch.Tensor, + loss_scaling_factor: float = 1.0, + dp_group: Optional[dist.ProcessGroup] = None, + cp_group: Optional[dist.ProcessGroup] = None, + ignore_index: int = -100, +) -> torch.Tensor: + """Next-token-prediction CE loss with DP/CP group reduction. + + Matches the behavior of cosmos_rl.policy.trainer.llm_trainer.sft_trainer.async_safe_ce + with the TORCH_CROSS_ENTROPY backend (F.cross_entropy with float32 cast). + + Args: + logits: (B, T, V) float tensor — raw model output before softmax. + labels: (B, T) long tensor — ground-truth token ids. + Positions equal to ignore_index are excluded from the loss. + loss_scaling_factor: scalar multiplied into the returned loss. + dp_group: FSDP data-parallel shard group for loss normalization. + None = no DP reduction (single-GPU or replicate-only). + cp_group: Context-parallel group. If size > 1, use per-rank mean. + None = no CP reduction. + ignore_index: label value to exclude (default -100). + + Returns: + Scalar loss tensor. + """ + # Shift for next-token prediction: predict token[t+1] using hidden state[t]. + # logits[:, :-1] aligns with labels[:, 1:]. + # Reference: async_safe_ce:63-73 (output[:, :-1], target[:, 1:]) + shifted_logits = logits[:, :-1].contiguous().view(-1, logits.size(-1)) + shifted_labels = labels[:, 1:].contiguous().view(-1) + + if cp_group is not None and cp_group.size() > 1: + # CP path: each rank sees a different sequence segment. + # Use simple mean reduction; nan_to_num handles fully-ignored batches. + # Reference: async_safe_ce:74-88 + loss = F.cross_entropy( + shifted_logits.float(), + shifted_labels, + ignore_index=ignore_index, + reduction="mean", + ) + loss = torch.nan_to_num(loss, nan=0.0) + return loss * loss_scaling_factor + + # No-CP path: per-token loss, then normalize over the global valid-token count. + # Reference: async_safe_ce:89-109 + per_token_loss = F.cross_entropy( + shifted_logits.float(), + shifted_labels, + ignore_index=ignore_index, + reduction="none", + ) + n_valid_tokens = (shifted_labels != ignore_index).sum() + num_dp_workers = 1 + if dp_group is not None: + dist.all_reduce(n_valid_tokens, op=dist.ReduceOp.SUM, group=dp_group) + num_dp_workers = dist.get_world_size(group=dp_group) + + loss = per_token_loss.sum() / (n_valid_tokens + 1e-8) * (num_dp_workers * loss_scaling_factor) + return loss diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/flow_matching.py b/cosmos_training/cosmos/model/vfm/algorithm/loss/flow_matching.py new file mode 100644 index 00000000..948c61ff --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/algorithm/loss/flow_matching.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Rectified-flow matching loss (vision / action / sound modalities). + +Extracted from OmniMoTModel._compute_flow_matching_loss. The loss math is +unchanged; the only structural change is that ``tensor_kwargs_fp32`` is now +passed explicitly instead of being read from ``self``. +""" + +from __future__ import annotations + +import torch + +from cosmos.model.vfm.diffusion.rectified_flow import RectifiedFlow + + +def compute_flow_matching_loss( + pred: list[torch.Tensor], + target: list[torch.Tensor], + condition_mask: list[torch.Tensor], + timesteps: torch.Tensor, + has_valid_tokens: bool, + rectified_flow: RectifiedFlow, + tensor_kwargs_fp32: dict, + loss_scale: float | None = None, + raw_action_dim: list[torch.Tensor] | None = None, + normalize_by_active: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + """Compute flow matching loss for a modality. + + Args: + pred: Predicted velocity field (list of tensors, one per sample). + target: Target velocity field (list of tensors, one per sample). + Under rectified flow the target is ``v = eps - x0``. + condition_mask: Mask where 1 = clean/conditioning, 0 = noisy/generation (list of tensors). + timesteps: Diffusion timesteps for time weighting. Shape [B,1] for + base/teacher_forcing (all frames share one timestep) or [B,T_max] + for diffusion_forcing (per-frame independent timesteps). Time weights + are applied per-frame before averaging, so non-uniform weight functions + are handled correctly. + has_valid_tokens: Whether this modality has valid noisy tokens. + rectified_flow: The rectified flow object for time weighting. + tensor_kwargs_fp32: Dict of dtype/device kwargs forwarded to + ``rectified_flow.train_time_weight``. + loss_scale: Optional per-modality loss scale. Falls back to the global + ``rectified_flow_training_config.loss_scale`` when *None*. + (Currently unused inside the function body — scaling is applied at the + call site in ``OmniMoTModel._compute_losses``. Kept in the signature + to preserve the original API.) + normalize_by_active: When True, normalize per-instance loss by the count of + active (noisy) elements rather than all elements. Preserves the + ``sum / active_count`` semantics needed for distillation critics where + conditioned frames contribute no signal and should not dilute the + denominator. + + Returns: + tuple: A tuple containing two elements: + - Flow matching loss (or dummy loss for gradient consistency). + - Per-instance loss (or dummy loss for gradient consistency). + """ + if not has_valid_tokens: + # Dummy loss to maintain backward graph consistency across ranks + dummy_loss = 0.0 * sum(p.sum() for p in pred) + return dummy_loss, dummy_loss.unsqueeze(0) # make per-instance loss 1-D + + # condition_mask[i] is T-first with trailing singletons: [T,1,1] vision, [T,1] action. + # tw_i gets the same shape so w(σ_t) broadcasts element-wise over non-T dims. + per_instance_losses = [] + per_instance_weighted_losses = [] + + for i in range(len(pred)): + T_i = condition_mask[i].shape[0] + sqerr_i = (pred[i] - target[i]) ** 2 # vision:[C,T,H,W] action/sound:[T,D] + noisy_mask_i = 1.0 - condition_mask[i] # vision:[T,1,1] action/sound:[T,1] + if raw_action_dim is not None and raw_action_dim[i] is not None: + sqerr_i = sqerr_i[:, : raw_action_dim[i]] + if normalize_by_active: + active_count = (noisy_mask_i.sum() * (sqerr_i.numel() // noisy_mask_i.numel())).clamp(min=1) + per_instance_losses.append((sqerr_i * noisy_mask_i).sum() / active_count) # [] + else: + per_instance_losses.append((sqerr_i * noisy_mask_i).mean()) # [] + + ts_i = timesteps[i, :T_i] if timesteps.dim() > 1 else timesteps[i] # DF:[T_i] TF:[1] + tw_i = rectified_flow.train_time_weight(ts_i, tensor_kwargs_fp32) # DF:[T_i] TF:[1] + tw_i = tw_i.reshape(-1, *([1] * (condition_mask[i].ndim - 1))) # vision:[T_i,1,1] action/sound:[T_i,1] + if normalize_by_active: + per_instance_weighted_losses.append((sqerr_i * tw_i * noisy_mask_i).sum() / active_count) + else: + per_instance_weighted_losses.append((sqerr_i * tw_i * noisy_mask_i).mean()) + + per_instance_loss = torch.stack(per_instance_losses) # [B] + per_instance_weighted_loss = torch.stack(per_instance_weighted_losses) # [B] + return ( + per_instance_weighted_loss.mean(), # [] + per_instance_loss, # [B] + ) diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/load_balancing.py b/cosmos_training/cosmos/model/vfm/algorithm/loss/load_balancing.py new file mode 100644 index 00000000..2ed83aa3 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/algorithm/loss/load_balancing.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from torch.distributed.tensor import DTensor, Partial +from torch.distributed.tensor.device_mesh import DeviceMesh + +from cosmos.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import LBLMetadata + + +def compute_load_balancing_loss( + lbl_metadata: LBLMetadata | None, + coeff: float | None, + method: str, + device_mesh: DeviceMesh | None, +) -> torch.Tensor | None: + """ + Compute the load balancing loss. We compute the load balancing loss + for each layer, and then average the loss across all layers. + + For computing the load balancing loss for each layer, we can either + use the fraction of tokens routed to each expert for this rank ("local" method), or + use the fraction of tokens routed to each expert across all ranks ("global" method). + + Args: + lbl_metadata: The load balancing metadata. Contains the following tensors + - num_tokens_per_expert: [num_layers, num_experts] - The number of + tokens routed to each expert for this rank for each layer. + - num_tokens: [num_layers, 1] - The total number of tokens in the + batch for each layer. + - mean_router_prob_per_expert: [num_layers, num_experts] - The average + probability of routing to each expert for this rank for each layer. + coeff: The coefficient for the load balancing loss. + method: The method for the load balancing loss. Can be "local" or "global". + device_mesh: The device mesh. Only needed if method is "global". + + Returns: + The load balancing loss. None if lbl_metadata is None or coeff is None. + """ + if lbl_metadata is None or coeff is None: + return None + assert method in ["local", "global"], "Invalid method" + + num_tokens_per_expert = lbl_metadata.num_tokens_per_expert + num_experts = num_tokens_per_expert.shape[-1] + num_tokens = lbl_metadata.num_tokens + mean_router_prob_per_expert = lbl_metadata.mean_router_prob_per_expert + + if method == "global": + # Note that these collectives must be executed outside a torch compiled region + # since torch compile could reorder the collectives and cause deadlocks. + assert device_mesh is not None, "MoE models require multiple GPUs." + + num_tokens_per_expert = DTensor.from_local( + num_tokens_per_expert, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + num_tokens = DTensor.from_local( + num_tokens, + device_mesh=device_mesh, + placements=[Partial()] * device_mesh.ndim, + ).full_tensor() + + # Compute the fraction of tokens routed to each experts. + # Summing over all experts should be equal to self.top_k. + mean_tokens_per_expert = num_tokens_per_expert.float() / num_tokens.float() + + lbl = torch.mean(torch.sum(mean_tokens_per_expert * mean_router_prob_per_expert, dim=-1) * num_experts) + return lbl * coeff diff --git a/cosmos_training/cosmos/model/vfm/algorithm/loss/time_weight.py b/cosmos_training/cosmos/model/vfm/algorithm/loss/time_weight.py new file mode 100644 index 00000000..8766159d --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/algorithm/loss/time_weight.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch + + +class TrainTimeWeight: + def __init__( + self, + noise_scheduler, + weight: str = "uniform", + ): + # Map reweighting -> uniform to support inference for existing checkpoints. + if weight == "reweighting": + weight = "uniform" + + self.weight = weight + self.noise_scheduler = noise_scheduler + + assert self.weight == "uniform", "Only uniform loss weight is supported in RF" + + def __call__(self, t, tensor_kwargs) -> torch.Tensor: # t: [B], returns [B] + if self.weight == "uniform": + wts = torch.ones_like(t) # [B] + else: + raise NotImplementedError(f"Time weight '{self.weight}' is not implemented.") + + return wts diff --git a/cosmos_training/cosmos/model/vfm/diffusion/__pycache__/rectified_flow.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/__pycache__/rectified_flow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cfe535191a87f04ca5b22cc021bd7ed884774d6 GIT binary patch literal 10159 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/diffusion/rectified_flow.py b/cosmos_training/cosmos/model/vfm/diffusion/rectified_flow.py new file mode 100644 index 00000000..0627fd4d --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/diffusion/rectified_flow.py @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable + +import torch +import torch.distributed +from diffusers import FlowMatchEulerDiscreteScheduler + +from cosmos.model.vfm.algorithm.loss.time_weight import TrainTimeWeight + + +class TrainTimeSampler: + _WAVER_MODE_S = 1.29 + + def __init__( + self, + distribution: str = "uniform", + ): + self.distribution = distribution + + @torch.no_grad() + def __call__( + self, + batch_size: int, + device: torch.device = torch.device("cpu"), + dtype: torch.dtype = torch.float32, + generator: torch.Generator | None = None, + ) -> torch.Tensor: + """ + Sample time tensor for training + + Returns: + torch.Tensor: Time tensor, shape (batch_size,) + """ + if self.distribution == "uniform": + t = torch.rand((batch_size,), generator=generator).to(device=device, dtype=dtype) # [B] + elif self.distribution == "logitnormal": + t = torch.sigmoid(torch.randn((batch_size,), generator=generator)).to(device=device, dtype=dtype) # [B] + elif self.distribution == "waver": + u = torch.rand((batch_size,), dtype=torch.float32, generator=generator) # [B] + t = 1.0 - u - self._WAVER_MODE_S * (torch.cos(torch.pi / 2.0 * u) ** 2 - 1 + u) # [B] + t = t.to(device=device, dtype=dtype) # [B] + else: + raise NotImplementedError(f"Time distribution '{self.dist}' is not implemented.") + + return t # [B] + + +class RectifiedFlow: + def __init__( + self, + velocity_field: Callable, + train_time_distribution: TrainTimeSampler | str = "uniform", + train_time_weight_method: str = "uniform", + use_dynamic_shift: bool = False, + shift: int = 3, + device: torch.device = torch.device("cpu"), + dtype: torch.dtype = torch.float32, + ): + r"""Initialize the RectifiedFlow class. + + Args: + velocity_field (`Callable`): + A function that predicts the velocity given the current state and time. + train_time_distribution (`TrainTimeSampler` or `str`, *optional*, defaults to `"uniform"`): + Distribution for sampling training times. + Can be an instance of `TrainTimeSampler` or a string specifying the distribution type. + train_time_weight (`TrainTimeWeight` or `str`, *optional*, defaults to `"uniform"`): + Weight applied to training times. + Can be an instance of `TrainTimeWeight` or a string specifying the weight type. + """ + self.velocity_field = velocity_field + self.train_time_sampler: TrainTimeSampler = ( + train_time_distribution + if isinstance(train_time_distribution, TrainTimeSampler) + else TrainTimeSampler(train_time_distribution) + ) + + if use_dynamic_shift: + self.noise_scheduler = FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=use_dynamic_shift) + else: + self.noise_scheduler = FlowMatchEulerDiscreteScheduler(shift=shift) + self.train_time_weight = TrainTimeWeight(self.noise_scheduler, train_time_weight_method) + + self.device = torch.device(device) if isinstance(device, str) else device + self.dtype = torch.dtype(dtype) if isinstance(dtype, str) else dtype + + def sample_train_time(self, batch_size: int, iteration: int | None = None) -> torch.Tensor: + r"""This method calls the `TrainTimeSampler` to sample training times. + + Args: + batch_size: Number of time values to sample. + iteration: When provided, sampling uses a local generator seeded from + ``(iteration, rank)`` so results are identical across independent runs + regardless of prior global RNG state. + + Returns: + t (`torch.Tensor`): + A tensor of sampled training times with shape `(batch_size,)`, + matching the class specified `device` and `dtype`. + """ + generator = None + if iteration is not None and torch.are_deterministic_algorithms_enabled(): + rank = torch.distributed.get_rank() if torch.distributed.is_initialized() else 0 + generator = torch.Generator() + generator.manual_seed(iteration * 65536 + rank) + time = self.train_time_sampler(batch_size, device=self.device, dtype=self.dtype, generator=generator) + return time + + def get_discrete_timestamp(self, u, tensor_kwargs): + r"""This method map time from 0,1 to discrete steps""" + + indices = (u.squeeze() * self.noise_scheduler.config.num_train_timesteps).long() # [B] + timesteps = self.noise_scheduler.timesteps.to(**tensor_kwargs)[indices] # [B] + return timesteps.unsqueeze(0) if timesteps.ndim == 0 else timesteps # [B] + + def get_sigmas(self, timesteps, tensor_kwargs): # timesteps: [B], returns [B] + sigmas = self.noise_scheduler.sigmas.to(**tensor_kwargs) # [N_timesteps+1] + schedule_timesteps = self.noise_scheduler.timesteps.to(**tensor_kwargs) # [N_timesteps] + step_indices = [(schedule_timesteps == t).nonzero().squeeze().tolist() for t in timesteps] + assert len(step_indices) == timesteps.shape[0], "Number of indices do not match the given timesteps." + sigma = sigmas[step_indices].flatten() # [B] + + return sigma # [B] + + def get_interpolation( + self, + x_0: list[torch.Tensor], # each element: [B,C,T,H,W] or [B,D1,...,Dn] + x_1: list[torch.Tensor], # each element: [B,C,T,H,W] or [B,D1,...,Dn] + t: list[torch.Tensor], # each element: [B] or [B,1,1,1,1] + ): + r""" + This method computes interpolation `X_t` and their time derivatives `dotX_t` at the specified time points `t`. + Note that `x_0` is the noise, and `x_1` is the clean data. This is aligned with the notation in the recified flow community, + but different from the notation in the diffusion community. + + Args: + x_0 (`torch.Tensor`): + noise, shape `(B, D1, D2, ..., Dn)`, where `B` is the batch size, and `D1, D2, ..., Dn` are the data dimensions. + x_1 (`torch.Tensor`): + clean data, with the same shape as `x_0` + t (`torch.Tensor`): + A tensor of time steps with values in `[0, 1]`. Can be shape `(B,)` or + pre-broadcast to `(B, 1, T, ..., 1)` matching `x_1`'s dimensionality along batch and temporal dimension. + + Returns: + (x_t, dot_x_t) (`Tuple[torch.Tensor, torch.Tensor]`): + - x_t (`torch.Tensor`): The interpolated state, with shape `(B, D1, D2, ..., Dn)`. + - dot_x_t (torch.Tensor): The time derivative of the interpolated state, with the same shape as `x_t`. + """ + assert len(x_0) == len(x_1), "x_0 and x_1 must have the same length." + assert len(x_0) == len(t), "Batch size of x_0 and x_1 must match." + assert len(t) == len(x_1), "Batch size of t must match x_1." + + x_t = [] + dot_x_t = [] + for i in range(len(x_0)): + x_t.append(x_0[i] * t[i] + x_1[i] * (1 - t[i])) # [B,C,T,H,W]; t[i] broadcasts [B] or [B,1,1,1,1] + dot_x_t.append(x_0[i] - x_1[i]) # [B,C,T,H,W] + + return x_t, dot_x_t # each list element: [B,C,T,H,W] diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__init__.py b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..953705c4429c97ad82cb61a0e4e85f593742f6d3 GIT binary patch literal 254 zcmZ8bO9}!p44wJ`5j=<+a{=)PUZIRLtyDTqNm^x2;>rVf7LVZxWL&v21$ANI<>iIE zM{bM7ypl@K>7nqwwEr3=vN>0MwN%?{MI%u|xp`f{XHI0m@(ChRvgjQ$G{~J$Bs`cV zeYM`zY;>a91sC>^*$_ithkZf%m^4Nc7&A5IyZJdju^qO?!_juoI%_jK48V04eaX{W d(v#M!*uQ3LInCZ|Xe!?-0hi%1R!Y%WrZ=4KPLTir literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/edm.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/edm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..674158a4619398a7b4be850c956cd3ab84422458 GIT binary patch literal 13730 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∋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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/fm_solvers_unipc.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/fm_solvers_unipc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6b3487ceee5fd4788207552afd62c2085db7844 GIT binary patch literal 35407 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 z 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 TbJcF>m}#F||35kMu{8e=4+0El literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/unipc.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/unipc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f559554797f58ff8528f6bd14e8c6098efda3bf4 GIT binary patch literal 5905 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 Hv;F@EK8ls# literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/utils.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/diffusion/samplers/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47cdd76cfd8e83a999d5daeff4ed046e8e121e8d GIT binary patch literal 3892 zcmbVPUvL{o8Q(kowPjh3<=W7suu#%kiY(bqXF@ef8W)$ilw=sEyx8$r7u`xe`*bIE zPm*P%;_2ie8A4;z3~?z8?o1!zaTvnzKp7qwhDTnM+R*eSGZc8CZ=ucu$rIn+A6ZJ; z33O-fba%V^eZT#Ezy0>ppTps91kJeTTYRSrp?~rhxA~oiz31TJ8j_JLsHiB+2t{#5 z6p$u*oMcq2oZR=x%~})0W|^!#Mo|iziV?6 z&Ak9fU;gE8tD>au|?zTVDfF1 zi>+?Xmg`?YmnXca9-v2zh&!Vmkp0}I(dT~I27kQKFBk{xCs1490y3iRTRrdwdNleF zlfYTPEWp+9R_!NG!`tYpqF7h?J0SAuvW6v`bHs^tEk|&PP|OIC6Ijt?vaDzY4EEKt2%SirJENzPDfHZIpUhsynu3ZXTd z6l;|WQiWI^nN%cLC0YRx#LvW&_z{SXd+5Z5QX~xjWA&qE+;zeL=Z^ zF@(`xMH;{Ih-Ua&?Vtkg(=7o(aBa_^p~!Iw7(2AA7TqXQJPUE)IS@1fZz(?n}^VzXcc!TTc z=v=IWlzm9nnCD>3^P_Y7=w9VJ+7+NH4uyy1e`6wzhk=ce%fRgwBtSV+M{@&kcnbSvNnJ3U;|(GAp!_DvudiOQzNgdiq29wofYAy zMD;~Tj_&*Xd5MvEph?j{J*hG%n5AlxR^XT6Oq$nAL}PlH=7{^HHN!Dm)McWkD*0lH z>$S{)?i8dQ2nE8MfVerr@IF^gb%rdQ(hTUUJ}t> zxH%ICXREXX#Dp#=y?Oq6Id9@VF;kPJ+ z(yUXl+fI4y&~~7MwwOS9{TCA{FX>F5Vvr*H91Ok)vl(m_hrxl&=6A2Yx^ej0?5%-} z!`%}@!WN2kn;u9(rr%D`rU!DR=_%eQX+AjsuC{~yA? z3;#Z{J2Ao^kCz@_@yH!szj!Gj-t`S33qZ#CZj z!C!k*8(+E1*2aERY4)a8Pr>a@sP|&^hnbDB_d|!8-g8Yn-8?(foStod>D&&yh7R5C z8@d)+54{;)efgu_zSUEA!u@N{?}VTF1c|<9FABGZlfUZO>e=r7AbRW_@20Rhwl=Z$ zy^RCw#`>vd^w`DVt?04a!N`@~OTFuK{k4s$_1FFqeCDnXCjUE#f)Uuvg-z+L#T$or zqoa2s0}#<0xtqtX7k39TJCRH?n7JM5Up;gGlOYrty}#Fk2ENXC;Qlz=KlxSfp8|c8 z&j!!Le5T*1l^}Id5A;z^rKLQJSjyAK9$d)c{y{JUX{xM}6Z8mt torch.Tensor: + """ + Generate a sequence of reverse time steps. + + Args: + t_min (float): The minimum time value. + t_max (float): The maximum time value. + num_steps (int): The number of time steps to generate. + ts_order (Union[int, float]): The order of the time step progression. + is_forward (bool, optional): If True, returns the sequence in forward order. Defaults to False. + + Returns: + torch.Tensor: A tensor containing the generated time steps in reverse or forward order. + + Raises: + ValueError: If `t_min` is not less than `t_max`. + TypeError: If `ts_order` is not an integer or float. + """ + if t_min >= t_max: + raise ValueError("t_min must be less than t_max") + + if not isinstance(ts_order, (int, float)): + raise TypeError("ts_order must be an integer or float") + + step_indices = torch.arange(num_steps + 1, dtype=torch.float64) # [num_steps+1] + time_steps = ( + t_max ** (1 / ts_order) + step_indices / num_steps * (t_min ** (1 / ts_order) - t_max ** (1 / ts_order)) + ) ** ts_order # [num_steps+1] + + if is_forward: + return time_steps.flip(dims=(0,)) # [num_steps+1] + + return time_steps # [num_steps+1] + + +class EDMSampler(torch.nn.Module): + def __init__(self, cfg: Optional[EDMSamplerConfig] = None): + super().__init__() + if cfg is None: + cfg = EDMSamplerConfig() + self.cfg = cfg + + @torch.no_grad() + def forward( + self, + x0_fn: Callable, + x_sigma_max: torch.Tensor, # [B,StateShape] + num_steps: int = 35, + sigma_min: float = 0.002, + sigma_max: float = 80, + rho: float = 7, + S_churn: float = 0, + S_min: float = 0, + S_max: float = float("inf"), + S_noise: float = 1, + solver_option: str = "2ab", + ) -> torch.Tensor: # [B,StateShape] + in_dtype = x_sigma_max.dtype + + def float64_x0_fn(x_B_StateShape: torch.Tensor, t_B: torch.Tensor) -> torch.Tensor: + return x0_fn(x_B_StateShape.to(in_dtype), t_B.to(in_dtype)).to(torch.float64) + + is_multistep = is_multi_step_fn_supported(solver_option) + is_rk = is_runge_kutta_fn_supported(solver_option) + assert is_multistep or is_rk, f"Only support multistep or Runge-Kutta method, got {solver_option}" + + solver_cfg = SolverConfig( + s_churn=S_churn, + s_t_max=S_max, + s_t_min=S_min, + s_noise=S_noise, + is_multi=is_multistep, + rk=solver_option, + multistep=solver_option, + ) + timestamps_cfg = SolverTimestampConfig(nfe=num_steps, t_min=sigma_min, t_max=sigma_max, order=rho) + sampler_cfg = EDMSamplerConfig(solver=solver_cfg, timestamps=timestamps_cfg, sample_clean=True) + + return self._forward_impl(float64_x0_fn, x_sigma_max, sampler_cfg).to(in_dtype) + + @torch.no_grad() + def _forward_impl( + self, + denoiser_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + noisy_input_B_StateShape: torch.Tensor, + sampler_cfg: Optional[EDMSamplerConfig] = None, + callback_fns: Optional[List[Callable]] = None, + ) -> torch.Tensor: + """ + Internal implementation of the forward pass. + + Args: + denoiser_fn: Function to denoise the input. + noisy_input_B_StateShape: Input tensor with noise. + sampler_cfg: Configuration for the sampler. + callback_fns: List of callback functions to be called during sampling. + + Returns: + torch.Tensor: Denoised output tensor. + """ + sampler_cfg = self.cfg if sampler_cfg is None else sampler_cfg + solver_order = 1 if sampler_cfg.solver.is_multi else int(sampler_cfg.solver.rk[0]) + num_timestamps = sampler_cfg.timestamps.nfe // solver_order + + sigmas_L = get_rev_ts( + sampler_cfg.timestamps.t_min, sampler_cfg.timestamps.t_max, num_timestamps, sampler_cfg.timestamps.order + ).to(noisy_input_B_StateShape.device) # [L] + + if self.cfg.convert_sigmas_to_rf: + sigmas_L = sigmas_L / (1 + sigmas_L) # [L] + + denoised_output = differential_equation_solver( + denoiser_fn, sigmas_L, sampler_cfg.solver, callback_fns=callback_fns + )(noisy_input_B_StateShape) # [B,StateShape] + + if sampler_cfg.sample_clean: + # Override denoised_output with fully denoised version + ones = torch.ones( + denoised_output.size(0), device=denoised_output.device, dtype=denoised_output.dtype + ) # [B] + denoised_output = denoiser_fn(denoised_output, sigmas_L[-1] * ones) # [B,StateShape] + + return denoised_output # [B,StateShape] + + +def fori_loop(lower: int, upper: int, body_fun: Callable[[int, Any], Any], init_val: Any) -> Any: + """ + Implements a for loop with a function. + + Args: + lower: Lower bound of the loop (inclusive). + upper: Upper bound of the loop (exclusive). + body_fun: Function to be applied in each iteration. + init_val: Initial value for the loop. + + Returns: + The final result after all iterations. + """ + val = init_val + for i in range(lower, upper): + # Add log during sampling to meet APS job health requirement of one log every 2mins + if i % 10 == 0: + log.info(f"fori_loop: {i}") + val = body_fun(i, val) + return val + + +def differential_equation_solver( + x0_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor], + sigmas_L: torch.Tensor, # [L] + solver_cfg: SolverConfig, + callback_fns: Optional[List[Callable]] = None, +) -> Callable[[torch.Tensor], torch.Tensor]: + """ + Creates a differential equation solver function. + + Args: + x0_fn: Function to compute x0 prediction. + sigmas_L: Tensor of sigma values with shape [L,]. + solver_cfg: Configuration for the solver. + callback_fns: Optional list of callback functions. + + Returns: + A function that solves the differential equation. + """ + num_step = len(sigmas_L) - 1 + + if solver_cfg.is_multi: + update_step_fn = get_multi_step_fn(solver_cfg.multistep) + else: + update_step_fn = get_runge_kutta_fn(solver_cfg.rk) + + eta = min(solver_cfg.s_churn / (num_step + 1), math.sqrt(1.2) - 1) + + def sample_fn(input_xT_B_StateShape: torch.Tensor) -> torch.Tensor: + """ + Samples from the differential equation. + + Args: + input_xT_B_StateShape: Input tensor with shape [B, StateShape]. + + Returns: + Output tensor with shape [B, StateShape]. + """ + ones_B = torch.ones( + input_xT_B_StateShape.size(0), device=input_xT_B_StateShape.device, dtype=torch.float64 + ) # [B] + + def step_fn( + i_th: int, state: Tuple[torch.Tensor, Optional[List[torch.Tensor]]] + ) -> Tuple[torch.Tensor, Optional[List[torch.Tensor]]]: + input_x_B_StateShape, x0_preds = state # [B,StateShape] + sigma_cur_0, sigma_next_0 = sigmas_L[i_th], sigmas_L[i_th + 1] # scalar, scalar + + # algorithm 2: line 4-6 + if solver_cfg.s_t_min < sigma_cur_0 < solver_cfg.s_t_max: + hat_sigma_cur_0 = sigma_cur_0 + eta * sigma_cur_0 # scalar + input_x_B_StateShape = input_x_B_StateShape + ( + hat_sigma_cur_0**2 - sigma_cur_0**2 + ).sqrt() * solver_cfg.s_noise * torch.randn_like(input_x_B_StateShape) # [B,StateShape] + sigma_cur_0 = hat_sigma_cur_0 # scalar + + if solver_cfg.is_multi: + x0_pred_B_StateShape = x0_fn(input_x_B_StateShape, sigma_cur_0 * ones_B) # [B,StateShape]; sigma: [B] + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_pred_B_StateShape, x0_preds + ) # [B,StateShape] + else: + output_x_B_StateShape, x0_preds = update_step_fn( + input_x_B_StateShape, sigma_cur_0 * ones_B, sigma_next_0 * ones_B, x0_fn + ) # [B,StateShape] + + if callback_fns: + for callback_fn in callback_fns: + callback_fn(**locals()) + + return output_x_B_StateShape, x0_preds + + x_at_eps, _ = fori_loop(0, num_step, step_fn, [input_xT_B_StateShape, None]) + return x_at_eps # [B,StateShape] + + return sample_fn diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/fixed_step.py b/cosmos_training/cosmos/model/vfm/diffusion/samplers/fixed_step.py new file mode 100644 index 00000000..11f692e5 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/diffusion/samplers/fixed_step.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixed-step sampler for DMD2-distilled student models. + +Uses an explicit, fixed sigma schedule (t_list) baked in at construction time. +Each step predicts x0 via a single velocity forward pass, then either: + - ODE: Euler step x_next = x_t + (sigma_next - sigma_cur) * v + - SDE: re-noise x0 to sigma_next with fresh noise + +This is incompatible with multi-step solvers (UniPC, EDM) because DMD2 students +are trained as one-shot denoisers at specific discrete sigmas, not as smooth +score functions. + +When ``shift`` is passed at call time, the schedule is derived dynamically via +the flow-matching shift formula (same as UniPC): + sigmas = shift * s / (1 + (shift - 1) * s), s = linspace(sigma_max, sigma_min, num_steps) +In this case ``num_steps`` is required. Otherwise ``self.t_list`` is used. +""" + +import torch + +from cosmos.model.vfm.diffusion.samplers.utils import run_multiseed + + +class FixedStepSampler: + def __init__( + self, + t_list: list[float], + sample_type: str = "ode", + num_train_timesteps: float = 1000.0, + ) -> None: + assert len(t_list) >= 1, "t_list must have at least 1 entry" + assert sample_type in ("ode", "sde"), f"sample_type must be 'ode' or 'sde', got {sample_type}" + # Auto-append 0.0 if not present (convention: t_list in config excludes final step) + self.t_list = t_list if t_list[-1] == 0.0 else t_list + [0.0] + assert len(self.t_list) >= 2, "t_list must have at least 2 entries after appending 0.0" + self.sample_type = sample_type + self.num_train_timesteps = num_train_timesteps + + def _build_t_list(self, num_steps: int, shift: float, device: torch.device) -> list[float]: + """Compute a shifted sigma schedule with ``num_steps`` integration steps.""" + sigma_max = 1.0 + sigma_min = 1.0 / self.num_train_timesteps + sigmas = torch.linspace(sigma_max, sigma_min, num_steps, device=device) + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) + return sigmas.tolist() + [0.0] + + def __call__( + self, + velocity_fn, + noise: torch.Tensor | list[torch.Tensor], + num_steps: int | None = None, + shift: float | None = None, + seed: int | list[int] | None = None, + ) -> torch.Tensor | list[torch.Tensor]: + """Run the fixed-step sampling loop. + + Matches the UniPC sampler call signature so both can be used + interchangeably in ``generate_samples_from_batch``. + + ``noise`` and ``seed`` must both be single values or both be lists + (of the same length). When lists are provided, each element + corresponds to one independent sample; the return value is then a + list of denoised tensors. When single values are provided, a + single tensor is returned. + + Args: + velocity_fn: ``velocity_fn(noise=..., timestep=...) -> velocity``. + noise: Initial noise. Either a single ``torch.Tensor`` of shape + ``(D,)`` or a ``list[torch.Tensor]`` where each element has + shape ``(D,)``. + seed: RNG seed for SDE mode. Either a single ``int`` or a + ``list[int]`` with the same length as ``noise``. + num_steps: Number of denoising steps. Required when ``shift`` is + given; optional otherwise (asserted to equal + ``len(t_list) - 1`` when provided). + shift: When set, derive the sigma schedule dynamically using the + flow-matching shift formula instead of ``self.t_list``. + + Returns: + Denoised sample(s). A single ``torch.Tensor`` when ``noise`` is a + tensor, or a ``list[torch.Tensor]`` when ``noise`` is a list. + """ + if isinstance(noise, list): + device = noise[0].device + else: + device = noise.device + + if shift is not None: + assert num_steps is not None, "num_steps is required when shift is provided" + t_list = self._build_t_list(num_steps, shift, device) + else: + if num_steps is not None: + assert num_steps == len(self.t_list) - 1, ( + f"num_steps={num_steps} must match the schedule length len(t_list)-1={len(self.t_list) - 1}" + ) + t_list = self.t_list + + latent = noise + + for step_idx, (sigma_cur, sigma_next) in enumerate( + zip(t_list[:-1], t_list[1:]), + ): + timestep = torch.tensor(sigma_cur * self.num_train_timesteps, device=device) + v_pred = velocity_fn(latent, timestep.reshape(1, 1)) + + def _sde_step(seed: int | None, latent: torch.Tensor, v_pred: torch.Tensor) -> torch.Tensor: + x0_pred = latent - sigma_cur * v_pred + + if sigma_next > 0: + if self.sample_type == "ode": + # Euler ODE step + latent = latent + (sigma_next - sigma_cur) * v_pred + else: + if seed is not None: + torch.manual_seed(seed + step_idx) + eps_fresh = torch.randn_like(x0_pred) + latent = (1.0 - sigma_next) * x0_pred + sigma_next * eps_fresh + else: + latent = x0_pred + return latent + + latent = run_multiseed( + _sde_step, + seed=seed, + latent=latent, + v_pred=v_pred, + ) + + return latent diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/fm_solvers_unipc.py b/cosmos_training/cosmos/model/vfm/diffusion/samplers/fm_solvers_unipc.py new file mode 100644 index 00000000..606b9bbd --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/diffusion/samplers/fm_solvers_unipc.py @@ -0,0 +1,768 @@ +# Copied from https://github.com/huggingface/diffusers/blob/v0.31.0/src/diffusers/schedulers/scheduling_unipc_multistep.py +# Convert unipc for flow matching +# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved. + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput +from diffusers.utils import deprecate + + +class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin): + """ + `UniPCMultistepScheduler` is a training-free framework designed for the fast sampling of diffusion models. + + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic + methods the library implements for all schedulers such as loading and saving. + + Args: + num_train_timesteps (`int`, defaults to 1000): + The number of diffusion steps to train the model. + solver_order (`int`, default `2`): + The UniPC order which can be any positive integer. The effective order of accuracy is `solver_order + 1` + due to the UniC. It is recommended to use `solver_order=2` for guided sampling, and `solver_order=3` for + unconditional sampling. + prediction_type (`str`, defaults to "flow_prediction"): + Prediction type of the scheduler function; must be `flow_prediction` for this scheduler, which predicts + the flow of the diffusion process. + thresholding (`bool`, defaults to `False`): + Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such + as Stable Diffusion. + dynamic_thresholding_ratio (`float`, defaults to 0.995): + The ratio for the dynamic thresholding method. Valid only when `thresholding=True`. + sample_max_value (`float`, defaults to 1.0): + The threshold value for dynamic thresholding. Valid only when `thresholding=True` and `predict_x0=True`. + predict_x0 (`bool`, defaults to `True`): + Whether to use the updating algorithm on the predicted x0. + solver_type (`str`, default `bh2`): + Solver type for UniPC. It is recommended to use `bh1` for unconditional sampling when steps < 10, and `bh2` + otherwise. + lower_order_final (`bool`, default `True`): + Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can + stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10. + disable_corrector (`list`, default `[]`): + Decides which step to disable the corrector to mitigate the misalignment between `epsilon_theta(x_t, c)` + and `epsilon_theta(x_t^c, c)` which can influence convergence for a large guidance scale. Corrector is + usually disabled during the first few steps. + solver_p (`SchedulerMixin`, default `None`): + Any other scheduler that if specified, the algorithm becomes `solver_p + UniC`. + use_karras_sigmas (`bool`, *optional*, defaults to `False`): + Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`, + the sigmas are determined according to a sequence of noise levels {σi}. + use_exponential_sigmas (`bool`, *optional*, defaults to `False`): + Whether to use exponential sigmas for step sizes in the noise schedule during the sampling process. + timestep_spacing (`str`, defaults to `"linspace"`): + The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and + Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information. + steps_offset (`int`, defaults to 0): + An offset added to the inference steps, as required by some model families. + final_sigmas_type (`str`, defaults to `"zero"`): + The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final + sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0. + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + solver_order: int = 2, + prediction_type: str = "flow_prediction", + shift: Optional[float] = 1.0, + use_dynamic_shifting=False, + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + predict_x0: bool = True, + solver_type: str = "bh2", + lower_order_final: bool = True, + disable_corrector: List[int] = [], + solver_p: SchedulerMixin = None, + timestep_spacing: str = "linspace", + steps_offset: int = 0, + final_sigmas_type: Optional[str] = "zero", # "zero", "sigma_min" + ): + if solver_type not in ["bh1", "bh2"]: + if solver_type in ["midpoint", "heun", "logrho"]: + self.register_to_config(solver_type="bh2") + else: + raise NotImplementedError(f"{solver_type} is not implemented for {self.__class__}") + + self.predict_x0 = predict_x0 + # setable values + self.num_inference_steps = None + alphas = np.linspace(1, 1 / num_train_timesteps, num_train_timesteps)[::-1].copy() + sigmas = 1.0 - alphas + sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32) # [num_train_timesteps] + + if not use_dynamic_shifting: + # when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # [num_train_timesteps] # pyright: ignore + + self.sigmas = sigmas # [num_train_timesteps] + self.timesteps = sigmas * num_train_timesteps # [num_train_timesteps] + + self.model_outputs = [None] * solver_order + self.timestep_list = [None] * solver_order + self.lower_order_nums = 0 + self.disable_corrector = disable_corrector + self.solver_p = solver_p + self.last_sample = None + self._step_index = None + self._begin_index = None + + self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication + self.sigma_min = self.sigmas[-1].item() + self.sigma_max = self.sigmas[0].item() + + @property + def step_index(self): + """ + The index counter for current timestep. It will increase 1 after each scheduler step. + """ + return self._step_index + + @property + def begin_index(self): + """ + The index for the first timestep. It should be set from pipeline with `set_begin_index` method. + """ + return self._begin_index + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index + def set_begin_index(self, begin_index: int = 0): + """ + Sets the begin index for the scheduler. This function should be run from pipeline before the inference. + + Args: + begin_index (`int`): + The begin index for the scheduler. + """ + self._begin_index = begin_index + + # Modified from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler.set_timesteps + def set_timesteps( + self, + num_inference_steps: Union[int, None] = None, + device: Union[str, torch.device] = None, + sigmas: Optional[List[float]] = None, + mu: Optional[Union[float, None]] = None, + shift: Optional[Union[float, None]] = None, + use_kerras_sigma: bool = False, + ): + """ + Sets the discrete timesteps used for the diffusion chain (to be run before inference). + Args: + num_inference_steps (`int`): + Total number of the spacing of the time steps. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + if self.config.use_dynamic_shifting and mu is None: + raise ValueError(" you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`") + + if use_kerras_sigma: + # force to use the exact sigma used in edm sampler + sigma_max = 200 + sigma_min = 0.01 + rho = 7 + sigmas = np.arange(num_inference_steps + 1) / num_inference_steps + min_inv_rho = sigma_min ** (1 / rho) + max_inv_rho = sigma_max ** (1 / rho) + sigmas = (max_inv_rho + sigmas * (min_inv_rho - max_inv_rho)) ** rho + sigmas = sigmas / (1 + sigmas) + else: + if sigmas is None: + sigmas = np.linspace(self.sigma_max, self.sigma_min, num_inference_steps + 1).copy()[:-1] # pyright: ignore + + if self.config.use_dynamic_shifting: + sigmas = self.time_shift(mu, 1.0, sigmas) # pyright: ignore + else: + if shift is None: + shift = self.config.shift + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # pyright: ignore + + if self.config.final_sigmas_type == "sigma_min": + sigma_last = ((1 - self.alphas_cumprod[0]) / self.alphas_cumprod[0]) ** 0.5 + elif self.config.final_sigmas_type == "zero": + sigma_last = 0 + else: + raise ValueError( + f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}" + ) + + timesteps = sigmas * self.config.num_train_timesteps + sigmas = np.concatenate([sigmas, [sigma_last]]).astype(np.float32) # pyright: ignore + + self.sigmas = torch.from_numpy(sigmas) # [num_inference_steps+1] + self.timesteps = torch.from_numpy(timesteps).to(device=device, dtype=torch.int64) # [num_inference_steps] + + self.num_inference_steps = len(timesteps) + + self.model_outputs = [ + None, + ] * self.config.solver_order + self.lower_order_nums = 0 + self.last_sample = None + if self.solver_p: + self.solver_p.set_timesteps(self.num_inference_steps, device=device) + + # add an index counter for schedulers that allow duplicated timesteps + self._step_index = None + self._begin_index = None + self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication + + # Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample + def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor: + """ + "Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the + prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by + s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing + pixels from saturation at each step. We find that dynamic thresholding results in significantly better + photorealism as well as better image-text alignment, especially when using very large guidance weights." + + https://arxiv.org/abs/2205.11487 + """ + dtype = sample.dtype + batch_size, channels, *remaining_dims = sample.shape + + if dtype not in (torch.float32, torch.float64): + sample = sample.float() # upcast for quantile calculation, and clamp not implemented for cpu half + + # Flatten sample for doing quantile calculation along each image + sample = sample.reshape(batch_size, channels * np.prod(remaining_dims)) # [B,C*spatial] + + abs_sample = sample.abs() # "a certain percentile absolute pixel value" # [B,C*spatial] + + s = torch.quantile(abs_sample, self.config.dynamic_thresholding_ratio, dim=1) # [B] + s = torch.clamp( + s, min=1, max=self.config.sample_max_value + ) # When clamped to min=1, equivalent to standard clipping to [-1, 1] # [B] + s = s.unsqueeze(1) # [B,1] + sample = ( + torch.clamp(sample, -s, s) / s + ) # "we threshold xt0 to the range [-s, s] and then divide by s" # [B,C*spatial] + + sample = sample.reshape(batch_size, channels, *remaining_dims) # [B,C,...] + sample = sample.to(dtype) + + return sample + + # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler._sigma_to_t + def _sigma_to_t(self, sigma): + return sigma * self.config.num_train_timesteps + + def _sigma_to_alpha_sigma_t(self, sigma): + return 1 - sigma, sigma + + # Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.set_timesteps + def time_shift(self, mu: float, sigma: float, t: torch.Tensor): + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + def convert_model_output( + self, + model_output: torch.Tensor, + *args, + sample: torch.Tensor = None, + **kwargs, + ) -> torch.Tensor: + r""" + Convert the model output to the corresponding type the UniPC algorithm needs. + + Args: + model_output (`torch.Tensor`): + The direct output from the learned diffusion model. + timestep (`int`): + The current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + + Returns: + `torch.Tensor`: + The converted model output. + """ + timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None) + if sample is None: + if len(args) > 1: + sample = args[1] + else: + raise ValueError("missing `sample` as a required keyward argument") + if timestep is not None: + deprecate( + "timesteps", + "1.0.0", + "Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + + sigma = self.sigmas[self.step_index] + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma) + + # print("sigma_t ==>", self.step_index, sigma, sigma_t, alpha_t, sample.shape, model_output.shape) + if self.predict_x0: + if self.config.prediction_type == "flow_prediction": + sigma_t = self.sigmas[self.step_index] + x0_pred = sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`," + " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler." + ) + + if self.config.thresholding: + x0_pred = self._threshold_sample(x0_pred) + # print("self.config.thresholding", self.config.thresholding) + return x0_pred + else: + if self.config.prediction_type == "flow_prediction": + sigma_t = self.sigmas[self.step_index] + epsilon = sample - (1 - sigma_t) * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`," + " `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler." + ) + + if self.config.thresholding: + sigma_t = self.sigmas[self.step_index] + x0_pred = sample - sigma_t * model_output + x0_pred = self._threshold_sample(x0_pred) + epsilon = model_output + x0_pred + + return epsilon + + def multistep_uni_p_bh_update( + self, + model_output: torch.Tensor, + *args, + sample: torch.Tensor = None, + order: int = None, # pyright: ignore + **kwargs, + ) -> torch.Tensor: + """ + One step for the UniP (B(h) version). Alternatively, `self.solver_p` is used if is specified. + + Args: + model_output (`torch.Tensor`): + The direct output from the learned diffusion model at the current timestep. + prev_timestep (`int`): + The previous discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + order (`int`): + The order of UniP at this timestep (corresponds to the *p* in UniPC-p). + + Returns: + `torch.Tensor`: + The sample tensor at the previous timestep. + """ + prev_timestep = args[0] if len(args) > 0 else kwargs.pop("prev_timestep", None) + if sample is None: + if len(args) > 1: + sample = args[1] + else: + raise ValueError(" missing `sample` as a required keyward argument") + if order is None: + if len(args) > 2: + order = args[2] + else: + raise ValueError(" missing `order` as a required keyward argument") + if prev_timestep is not None: + deprecate( + "prev_timestep", + "1.0.0", + "Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + model_output_list = self.model_outputs + + s0 = self.timestep_list[-1] + m0 = model_output_list[-1] + x = sample + + if self.solver_p: + x_t = self.solver_p.step(model_output, s0, x).prev_sample + return x_t + + sigma_t, sigma_s0 = self.sigmas[self.step_index + 1], self.sigmas[self.step_index] # pyright: ignore + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t) + alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0) + + lambda_t = torch.log(alpha_t) - torch.log(sigma_t) + lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0) + + h = lambda_t - lambda_s0 + device = sample.device + + rks = [] + D1s = [] + for i in range(1, order): + si = self.step_index - i # pyright: ignore + mi = model_output_list[-(i + 1)] + alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si]) + lambda_si = torch.log(alpha_si) - torch.log(sigma_si) + rk = (lambda_si - lambda_s0) / h + rks.append(rk) + D1s.append((mi - m0) / rk) # pyright: ignore + + rks.append(1.0) + rks = torch.tensor(rks, device=device) # [order] + + R = [] + b = [] + + hh = -h if self.predict_x0 else h + h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1 + h_phi_k = h_phi_1 / hh - 1 + + factorial_i = 1 + + if self.config.solver_type == "bh1": + B_h = hh + elif self.config.solver_type == "bh2": + B_h = torch.expm1(hh) + else: + raise NotImplementedError() + + for i in range(1, order + 1): + R.append(torch.pow(rks, i - 1)) # [order] + b.append(h_phi_k * factorial_i / B_h) + factorial_i *= i + 1 + h_phi_k = h_phi_k / hh - 1 / factorial_i + + R = torch.stack(R) # [order,order] + b = torch.tensor(b, device=device) # [order] + + if len(D1s) > 0: + D1s = torch.stack(D1s, dim=1) # [B,order-1,C,T,H,W] + # for order 2, we use a simplified version + if order == 2: + rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device) # [1] + else: + rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1]).to(device).to(x.dtype) # [order-1] + else: + D1s = None + + if self.predict_x0: + x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) # [B,C,T,H,W] # pyright: ignore + else: + pred_res = 0 + x_t = x_t_ - alpha_t * B_h * pred_res # [B,C,T,H,W] + else: + x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + pred_res = torch.einsum("k,bkc...->bc...", rhos_p, D1s) # [B,C,T,H,W] # pyright: ignore + else: + pred_res = 0 + x_t = x_t_ - sigma_t * B_h * pred_res # [B,C,T,H,W] + + x_t = x_t.to(x.dtype) # [B,C,T,H,W] + return x_t # [B,C,T,H,W] + + def multistep_uni_c_bh_update( + self, + this_model_output: torch.Tensor, + *args, + last_sample: torch.Tensor = None, + this_sample: torch.Tensor = None, + order: int = None, # pyright: ignore + **kwargs, + ) -> torch.Tensor: + """ + One step for the UniC (B(h) version). + + Args: + this_model_output (`torch.Tensor`): + The model outputs at `x_t`. + this_timestep (`int`): + The current timestep `t`. + last_sample (`torch.Tensor`): + The generated sample before the last predictor `x_{t-1}`. + this_sample (`torch.Tensor`): + The generated sample after the last predictor `x_{t}`. + order (`int`): + The `p` of UniC-p at this step. The effective order of accuracy should be `order + 1`. + + Returns: + `torch.Tensor`: + The corrected sample tensor at the current timestep. + """ + this_timestep = args[0] if len(args) > 0 else kwargs.pop("this_timestep", None) + if last_sample is None: + if len(args) > 1: + last_sample = args[1] + else: + raise ValueError(" missing`last_sample` as a required keyward argument") + if this_sample is None: + if len(args) > 2: + this_sample = args[2] + else: + raise ValueError(" missing`this_sample` as a required keyward argument") + if order is None: + if len(args) > 3: + order = args[3] + else: + raise ValueError(" missing`order` as a required keyward argument") + if this_timestep is not None: + deprecate( + "this_timestep", + "1.0.0", + "Passing `this_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`", + ) + + model_output_list = self.model_outputs + + m0 = model_output_list[-1] + x = last_sample + x_t = this_sample + model_t = this_model_output + + sigma_t, sigma_s0 = self.sigmas[self.step_index], self.sigmas[self.step_index - 1] # pyright: ignore + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t) + alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0) + + lambda_t = torch.log(alpha_t) - torch.log(sigma_t) + lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0) + + h = lambda_t - lambda_s0 + device = this_sample.device + + rks = [] + D1s = [] + for i in range(1, order): + si = self.step_index - (i + 1) # pyright: ignore + mi = model_output_list[-(i + 1)] + alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si]) + lambda_si = torch.log(alpha_si) - torch.log(sigma_si) + rk = (lambda_si - lambda_s0) / h + rks.append(rk) + D1s.append((mi - m0) / rk) # pyright: ignore + + rks.append(1.0) + rks = torch.tensor(rks, device=device) # [order] + + R = [] + b = [] + + hh = -h if self.predict_x0 else h + h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1 + h_phi_k = h_phi_1 / hh - 1 + + factorial_i = 1 + + if self.config.solver_type == "bh1": + B_h = hh + elif self.config.solver_type == "bh2": + B_h = torch.expm1(hh) + else: + raise NotImplementedError() + + for i in range(1, order + 1): + R.append(torch.pow(rks, i - 1)) # [order] + b.append(h_phi_k * factorial_i / B_h) + factorial_i *= i + 1 + h_phi_k = h_phi_k / hh - 1 / factorial_i + + R = torch.stack(R) # [order,order] + b = torch.tensor(b, device=device) # [order] + + if len(D1s) > 0: + D1s = torch.stack(D1s, dim=1) # [B,order-1,C,T,H,W] + else: + D1s = None + + # for order 1, we use a simplified version + if order == 1: + rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device) # [1] + else: + rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype) # [order] + + if self.predict_x0: + x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) # [B,C,T,H,W] + else: + corr_res = 0 + D1_t = model_t - m0 # [B,C,T,H,W] + x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t) # [B,C,T,H,W] + else: + x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0 # [B,C,T,H,W] + if D1s is not None: + corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s) # [B,C,T,H,W] + else: + corr_res = 0 + D1_t = model_t - m0 # [B,C,T,H,W] + x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t) # [B,C,T,H,W] + x_t = x_t.to(x.dtype) # [B,C,T,H,W] + return x_t # [B,C,T,H,W] + + def index_for_timestep(self, timestep, schedule_timesteps=None): + if schedule_timesteps is None: + schedule_timesteps = self.timesteps + + indices = (schedule_timesteps == timestep).nonzero() + + # The sigma index that is taken for the **very** first `step` + # is always the second index (or the last index if there is only 1) + # This way we can ensure we don't accidentally skip a sigma in + # case we start in the middle of the denoising schedule (e.g. for image-to-image) + pos = 1 if len(indices) > 1 else 0 + + return indices[pos].item() + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler._init_step_index + def _init_step_index(self, timestep): + """ + Initialize the step_index counter for the scheduler. + """ + + if self.begin_index is None: + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + self._step_index = self.index_for_timestep(timestep) + else: + self._step_index = self._begin_index + + def step( + self, + model_output: torch.Tensor, + timestep: Union[int, torch.Tensor], + sample: torch.Tensor, + return_dict: bool = True, + generator=None, + ) -> Union[SchedulerOutput, Tuple]: + """ + Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with + the multistep UniPC. + + Args: + model_output (`torch.Tensor`): + The direct output from learned diffusion model. + timestep (`int`): + The current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + A current instance of a sample created by the diffusion process. + return_dict (`bool`): + Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`. + + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned, otherwise a + tuple is returned where the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if self.step_index is None: + self._init_step_index(timestep) + + # print("self.step_index ==> ", self.step_index) + + use_corrector = ( + self.step_index > 0 and self.step_index - 1 not in self.disable_corrector and self.last_sample is not None # pyright: ignore + ) + + model_output_convert = self.convert_model_output(model_output, sample=sample) + + if use_corrector: + sample = self.multistep_uni_c_bh_update( + this_model_output=model_output_convert, + last_sample=self.last_sample, + this_sample=sample, + order=self.this_order, + ) + + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self.timestep_list[i] = self.timestep_list[i + 1] + + self.model_outputs[-1] = model_output_convert + self.timestep_list[-1] = timestep # pyright: ignore + + if self.config.lower_order_final: + this_order = min(self.config.solver_order, len(self.timesteps) - self.step_index) # pyright: ignore + else: + this_order = self.config.solver_order + + self.this_order = min(this_order, self.lower_order_nums + 1) # warmup for multistep + assert self.this_order > 0 + + self.last_sample = sample + prev_sample = self.multistep_uni_p_bh_update( + model_output=model_output, # pass the original non-converted model output, in case solver-p is used + sample=sample, + order=self.this_order, + ) + + if self.lower_order_nums < self.config.solver_order: + self.lower_order_nums += 1 + + # upon completion increase step index by one + self._step_index += 1 # pyright: ignore + + if not return_dict: + return (prev_sample, model_output_convert) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.Tensor, *args, **kwargs) -> torch.Tensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.Tensor`): + The input sample. + + Returns: + `torch.Tensor`: + A scaled input sample. + """ + return sample + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.add_noise + def add_noise( + self, + original_samples: torch.Tensor, + noise: torch.Tensor, + timesteps: torch.IntTensor, + ) -> torch.Tensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + schedule_timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + # begin_index is None when the scheduler is used for training or pipeline does not implement set_begin_index + if self.begin_index is None: + step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timesteps] + elif self.step_index is not None: + # add_noise is called after first denoising step (for inpainting) + step_indices = [self.step_index] * timesteps.shape[0] + else: + # add noise is called before first denoising step to create initial latent(img2img) + step_indices = [self.begin_index] * timesteps.shape[0] + + sigma = sigmas[step_indices].flatten() # [B] + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) # [B,1,...] broadcast-ready + + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma) + noisy_samples = alpha_t * original_samples + sigma_t * noise # [B,C,T,H,W] + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/unipc.py b/cosmos_training/cosmos/model/vfm/diffusion/samplers/unipc.py new file mode 100644 index 00000000..46490e1d --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/diffusion/samplers/unipc.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Optional + +import attrs +import torch + +from cosmos.utils.config import make_freezable +from cosmos.utils.progress_bar import progress_bar +from cosmos.model.vfm.diffusion.samplers.fm_solvers_unipc import FlowUniPCMultistepScheduler +from cosmos.model.vfm.diffusion.samplers.utils import run_multiseed + + +@make_freezable +@attrs.define(slots=False) +class UniPCSamplerConfig: + num_train_timesteps: int = 1000 + shift: float = 1.0 + use_dynamic_shifting: bool = False + + +class UniPCSampler(torch.nn.Module): + def __init__(self, cfg: Optional[UniPCSamplerConfig] = None, tensor_kwargs: Optional[dict] = None): + super().__init__() + if cfg is None: + cfg = UniPCSamplerConfig() + self.cfg = cfg + self.tensor_kwargs = tensor_kwargs + + @torch.no_grad() + def forward( + self, + velocity_fn: Callable, + noise: torch.Tensor | list[torch.Tensor], + num_steps: int = 35, + shift: float | None = None, + seed: int | list[int] | None = None, + ) -> torch.Tensor | list[torch.Tensor]: + """Run the UniPC multi-step sampling loop. + + ``noise`` and ``seed`` must both be single values or both be lists + (of the same length). When lists are provided, each element + corresponds to one independent sample with its own RNG generator + and scheduler; the return value is then a list of denoised tensors. + When single values are provided, a single tensor is returned. + + Args: + velocity_fn: ``velocity_fn(noise=..., timestep=...) -> velocity``. + noise: Initial noise. Either a single ``torch.Tensor`` of shape + ``(C, T, H, W)`` or a ``list[torch.Tensor]`` where each + element has shape ``(C, T, H, W)``. + seed: RNG seed. Either a single ``int`` or a ``list[int]`` with + the same length as ``noise``. + num_steps: Number of denoising steps. + shift: Flow-matching shift factor. Defaults to ``self.cfg.shift``. + + Returns: + Denoised sample(s). A single ``torch.Tensor`` when ``noise`` is a + tensor, or a ``list[torch.Tensor]`` when ``noise`` is a list. + """ + if shift is None: + shift = self.cfg.shift + assert isinstance(shift, float), "Shift must be a float" + + def _init_sample_scheduler(seed: int | None) -> tuple[torch.Generator, FlowUniPCMultistepScheduler]: + seed_g = torch.Generator(device=self.tensor_kwargs["device"]) + if seed is not None: + seed_g.manual_seed(seed) + sample_scheduler = FlowUniPCMultistepScheduler( + num_train_timesteps=self.cfg.num_train_timesteps, + shift=self.cfg.shift, + use_dynamic_shifting=self.cfg.use_dynamic_shifting, + ) + sample_scheduler.set_timesteps(num_steps, device=self.tensor_kwargs["device"], shift=shift) + return seed_g, sample_scheduler + + seed_g, sample_scheduler = run_multiseed(_init_sample_scheduler, seed=seed) + + timesteps = sample_scheduler[0].timesteps if isinstance(sample_scheduler, list) else sample_scheduler.timesteps + latent = noise + + for timestep in progress_bar(timesteps, desc="Sampling", total=len(timesteps)): + velocity_pred = velocity_fn(latent, timestep.reshape(1, 1)) + + def _scheduler_step( + seed_g: torch.Generator, + sample_scheduler: FlowUniPCMultistepScheduler, + velocity_pred: torch.Tensor, + latent: torch.Tensor, + ) -> torch.Tensor: + # multistep_uni_p_bh_update and multistep_uni_c_bh_update both use einsum patterns + # like "k,bkc...->bc...", which expect the tensor to have at least shape + # [B, C, ...] — where b is the batch dimension. Therefore, we need to unsqueeze + # the latent tensor to [B, C, ...] before passing it to the scheduler. + return sample_scheduler.step( + model_output=velocity_pred, + timestep=timestep, + sample=latent.unsqueeze(0), + return_dict=False, + generator=seed_g, + )[0].squeeze(0) + + latent = run_multiseed( + _scheduler_step, + seed_g=seed_g, + sample_scheduler=sample_scheduler, + velocity_pred=velocity_pred, + latent=latent, + ) + + return latent diff --git a/cosmos_training/cosmos/model/vfm/diffusion/samplers/utils.py b/cosmos_training/cosmos/model/vfm/diffusion/samplers/utils.py new file mode 100644 index 00000000..8e9cda0b --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/diffusion/samplers/utils.py @@ -0,0 +1,67 @@ +from typing import Any, Callable + + +def run_multiseed(fn: Callable, **kwargs: list[Any] | Any) -> Any: + """Run a callable once per seed, indexing all list kwargs in lockstep. + + All keyword arguments must be **either** all lists or all non-lists. + Mixing is not allowed. + + - **All non-list**: ``fn`` is called once with the kwargs as-is, and its + return value is passed through directly. + - **All list**: every list must have the same length *N*. ``fn`` is called + *N* times — call *i* receives ``{k: v[i] for k, v in kwargs}``. + Results are collected into a list. If ``fn`` returns a tuple, the + results are transposed into a tuple of lists. + + Args: + fn: Callable to invoke per seed. + **kwargs: Keyword arguments for ``fn``. Must be **all lists** (one + element per seed, all the same length) or **all non-lists** (single + call). + + Returns: + - All non-list kwargs: the raw return value of ``fn``. + - All list kwargs, ``fn`` returns a tuple: a ``tuple`` of lists, + transposed across calls. + - All list kwargs, ``fn`` returns non-tuple: a ``list`` of return + values. + + Raises: + AssertionError: If kwargs mix lists and non-lists, or if list kwargs + have differing lengths. + + Examples: + Single call (no lists):: + + run_multiseed(lambda x, y: x + y, x=1, y=2) # returns 3 + + Multiple calls with all-list kwargs:: + + run_multiseed(lambda x, y: x * y, x=[1, 2, 3], y=[10, 20, 30]) + # returns [10, 40, 90] + + Tuple return transposition:: + + run_multiseed(lambda x: (x, -x), x=[1, 2]) + # returns ([1, 2], [-1, -2]) + """ + all_list = all(isinstance(v, list) for v in kwargs.values()) + all_non_list = all(not isinstance(v, list) for v in kwargs.values()) + assert all_list or all_non_list, "All kwargs must be lists or all must be non-lists, cannot mix" + + if all_non_list: + return fn(**kwargs) + + lengths = {len(v) for v in kwargs.values()} + assert len(lengths) == 1, f"All list arguments must have the same length, got {lengths}" + num_calls = lengths.pop() + + results = [] + for i in range(num_calls): + kwargs_i = {k: v[i] for k, v in kwargs.items()} + results.append(fn(**kwargs_i)) + + if results and isinstance(results[0], tuple): + return tuple(list(items) for items in zip(*results)) + return results diff --git a/cosmos_training/cosmos/model/vfm/mot/__init__.py b/cosmos_training/cosmos/model/vfm/mot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32be5add8e152e993682eaa5a3ff2cdc0fd124c9 GIT binary patch literal 239 zcmZ8b%MHRX3~eY#2&sc`!3!WpV1=lZv})DFm7RdP1XpHY7RF!#R8BqNNG%7RWWVR< zkNnKCO(O80RTKFY(?5o#IQOJaw(9suXd%@QXT}?}fjt>0`3fD9FR`tWp+L4m9bsb% zKiB${v(cXN+SRUtz=jz5+*L9Bw4|{!jxkVSX6Lstoway2EnG?q7CmS!=e*Xtt{>NX V>4nxDXeI9{36A2ODy1kD^#!kZNOb@J literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/attention.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/attention.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d946a6106e55dcf8deb5fb85664ef4de862a509a GIT binary patch literal 15029 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 z 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 Olx&wC?L=&H4F4P2=a&%x literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/context_parallel_utils.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/context_parallel_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05fc08a51f4a5f7b8c599c40034eb474f2434e5a GIT binary patch literal 16067 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 MIr5|)8xR`*Z-1J=c>n+a literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/cosmos3_vfm_network.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/cosmos3_vfm_network.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c803f3bdb847b1498f285325c6c7ac5e9a13757d GIT binary patch literal 48904 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/modeling_utils.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/modeling_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1745941cd5d00786406c697382191e4e5133690 GIT binary patch literal 21841 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_unified_mot.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_unified_mot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1dd7f2d4e84bc239d76b5e556695b7a75e67951 GIT binary patch literal 3804 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 P%XdF`_emf0_^SU6K|Q#l literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_vfm_network.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/parallelize_vfm_network.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f93e92ebec8fc03822738566844ee251537cdf4 GIT binary patch literal 7429 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 LewIcH-}3(ge3`KM literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/__pycache__/unified_3dmrope_utils.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/mot/__pycache__/unified_3dmrope_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8be9887281fbed09b6a063979334173ad648e48e GIT binary patch literal 8622 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 QRa;{{nSH(=$1ySe4~4IA8vp>t9y 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 NTD$L44tLhW{(pASM~MIc literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/mot/attention.py b/cosmos_training/cosmos/model/vfm/mot/attention.py new file mode 100644 index 00000000..50bab349 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/attention.py @@ -0,0 +1,430 @@ +import torch +from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention + +from cosmos.model.attention import ( + attention, + merge_attentions, + multi_dimensional_attention_varlen, +) +from cosmos.model.attention.masks import CausalType +from cosmos.model.vfm.utils.memory import KVToStore, MemoryValue + +flex_attention = torch.compile(flex_attention) + + +class SplitInfo: + def __init__( + self, + split_lens: list[int], + attn_modes: list[str], + sample_lens: list[int], + actual_len: int, + is_three_way: bool = False, + vision_token_shapes: list[tuple[int, int, int]] | None = None, + action_token_shapes: list[tuple[int, ...]] | None = None, + num_action_tokens_per_supertoken: int = 0, + null_action_supertokens: bool = False, + ): + """ + Actual len is the actual non-padded length of the packed sequence. + It's used to trim split_lens, attn_modes and sample_lens, which were + originally padded to max sequence length (likely for flex attention). + """ + assert sum(sample_lens) == sum(split_lens), ( + f"Sum of new sample lens {sum(sample_lens)} is not equal to sum of new split lens {sum(split_lens)}" + ) + + max_causal_len = 0 + max_full_len = 0 + for split_len, attn_mode in zip(split_lens, attn_modes): + if attn_mode == "causal": + max_causal_len = max(max_causal_len, split_len) + elif attn_mode == "full": + max_full_len = max(max_full_len, split_len) + + self.max_causal_len = max_causal_len + self.max_full_len = max_full_len + self.max_sample_len = max(sample_lens) + + self.split_lens = split_lens + self.attn_modes = attn_modes + self.sample_lens = sample_lens + + self.is_three_way = is_three_way + self.vision_token_shapes = vision_token_shapes + self.action_token_shapes = action_token_shapes + self.num_action_tokens_per_supertoken = num_action_tokens_per_supertoken + self.null_action_supertokens = null_action_supertokens + + +AttentionMaskType = BlockMask | SplitInfo + + +_dotproduct_attention_cache = {} + + +from cosmos.data.vfm.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, + create_sparse_mask, + factored_from_joint_sequence, + from_joint, + from_mode_splits, + generate_natten_metadata, + generate_temporal_causal_natten_metadata, + get_all_seq, + get_causal_seq, + get_full_only_seq, + joint_from_joint_sequence, +) + + +def two_way_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, +): + """ + Performs two-way attention with causal and full attention. + """ + + causal_q, causal_q_offsets = get_causal_seq(packed_query_states) + causal_k, causal_k_offsets = get_causal_seq(packed_key_states) + causal_v, _ = get_causal_seq(packed_value_states) + full_q, full_q_offsets = get_full_only_seq(packed_query_states) + + sample_offsets = packed_query_states["sample_offsets"] + + use_dont_care_mask = causal_q_offsets is causal_k_offsets + + + causal_res = attention( + causal_q.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=causal_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_causal_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + is_causal=True, + causal_type=CausalType.DontCare if use_dont_care_mask else CausalType.TopLeft, + ) # [1,N_und,heads,head_dim] + + # [1,N_und,heads,head_dim] -> [N_und,heads,head_dim] -> [N_und,heads*head_dim] + causal_out = causal_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_und,heads*head_dim] + + full_res = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + get_all_seq(packed_key_states).unsqueeze(0), # [1,N_all,heads,head_dim] + get_all_seq(packed_value_states).unsqueeze(0), # [1,N_all,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=sample_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_sample_len"], + ) # [1,N_full,heads,head_dim] + + # [1,N_full,heads,head_dim] -> [N_full,heads,head_dim] -> [N_full,heads*head_dim] + full_out = full_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_full,heads*head_dim] + + out_all = from_mode_splits(causal_out, full_out, packed_query_states) + return out_all + + +def three_way_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + natten_metadata: dict | None, + attention_meta: SplitInfo | None = None, +): + """ + Performs three-way attention, with understanding and generations attentions fully decomposed, + and allows sparsity / multi-dimensional masking in the generation tower. + + When attention_meta is provided with null_action_supertokens=True, zeros V for the first + num_action_tokens_per_supertoken tokens of each sample's GEN sequence (null action + supertokens for temporal causal training). The metadata encodes is_causal=(True, False): + causal across T supertokens, full within each supertoken S. + + NOTE: the three-way decomposition is only done so we can handle sparsity in the gen tower, + but a KEY assumption is that the "full" tokens all correspond to the same modality! + We should be careful when extending this to beyond t2i and t2v. + """ + + causal_q, causal_q_offsets = get_causal_seq(packed_query_states) + causal_k, causal_k_offsets = get_causal_seq(packed_key_states) + causal_v, _ = get_causal_seq(packed_value_states) + full_q, full_q_offsets = get_full_only_seq(packed_query_states) + full_k, full_k_offsets = get_full_only_seq(packed_key_states) + full_v, _ = get_full_only_seq(packed_value_states) + + sample_offsets = packed_query_states["sample_offsets"] + + if attention_meta is not None and attention_meta.null_action_supertokens: + # Zero V for the first num_action_tokens_per_supertoken tokens of each + # sample's GEN sequence (null action supertokens at t=0). + # out_i = Σ_j softmax(QKᵀ/√d)_j · V_j — terms with V_j=0 contribute exactly 0 to the output, + # regardless of attention weights. Softmax mass is still allocated to these positions (not + # redistributed), so this differs from hard key masking, but the output contribution is 0. + full_v = full_v.clone() + starts = full_q_offsets[:-1].long() # [B] + null_positions = ( + starts.unsqueeze(1) + torch.arange(attention_meta.num_action_tokens_per_supertoken, device=starts.device) + ).reshape(-1) + full_v[null_positions] = 0 + + use_dont_care_mask = causal_q_offsets is causal_k_offsets + + + causal_res = attention( + causal_q.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=causal_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_causal_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + is_causal=True, + causal_type=CausalType.DontCare if use_dont_care_mask else CausalType.TopLeft, + ) # [1,N_und,heads,head_dim] + # [1,N_und,heads,head_dim] -> [N_und,heads,head_dim] -> [N_und,heads*head_dim] + causal_out = causal_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_und,heads*head_dim] + + # If there's no metadata, it's a dense layer + if natten_metadata is None: + full_sa, full_sa_lse = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + full_k.unsqueeze(0), # [1,N_full,heads,head_dim] + full_v.unsqueeze(0), # [1,N_full,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=full_k_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_full_len"], + return_lse=True, + ) # full_sa: [1,N_full,heads,head_dim], full_sa_lse: [1,N_full,heads] + else: + assert natten_metadata is not None + full_sa, full_sa_lse = multi_dimensional_attention_varlen( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + full_k.unsqueeze(0), # [1,N_full,heads,head_dim] + full_v.unsqueeze(0), # [1,N_full,heads,head_dim] + metadata=natten_metadata, + return_lse=True, + ) # full_sa: [1,N_full,heads,head_dim], full_sa_lse: [1,N_full,heads] + + full_ca, full_ca_lse = attention( + full_q.unsqueeze(0), # [1,N_full,heads,head_dim] + causal_k.unsqueeze(0), # [1,N_und,heads,head_dim] + causal_v.unsqueeze(0), # [1,N_und,heads,head_dim] + cumulative_seqlen_Q=full_q_offsets, + cumulative_seqlen_KV=causal_k_offsets, + max_seqlen_Q=packed_query_states["max_full_len"], + max_seqlen_KV=packed_query_states["max_causal_len"], + return_lse=True, + ) # full_ca: [1,N_full,heads,head_dim], full_ca_lse: [1,N_full,heads] + + assert full_sa.shape == full_ca.shape + full_res, _ = merge_attentions( + outputs=[full_sa, full_ca], lse_tensors=[full_sa_lse, full_ca_lse], torch_compile=False + ) # [1,N_full,heads,head_dim] + + # [1,N_full,heads,head_dim] -> [N_full,heads,head_dim] -> [N_full,heads*head_dim] + full_out = full_res.squeeze(0).flatten(-2, -1) # type: ignore # [N_full,heads*head_dim] + + out_all = from_mode_splits(causal_out, full_out, packed_query_states) + return out_all + + +def pad_sequence(tensor, pad_size): + """ + Pad a tensor along the second-to-last dimension. + + Args: + tensor: Input tensor to pad + pad_size: Number of padding elements to add + + Returns: + Padded tensor with zeros added along dim=-2 + """ + if pad_size <= 0: + return tensor + pad_shape = list(tensor.shape) + pad_shape[-2] = pad_size + padding = torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device) + return torch.cat([tensor, padding], dim=-2) # [...,S+pad_size,...] + + +def block_flex_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask, + block_size: int | None = None, +): + packed_queries = get_all_seq(packed_query_states) # [N,heads,head_dim] + packed_keys = get_all_seq(packed_key_states) # [N,heads,head_dim] + packed_values = get_all_seq(packed_value_states) # [N,heads,head_dim] + max_num_tokens = packed_query_states["max_num_tokens"] + + num_attention_heads = packed_queries.shape[1] + head_dim = packed_queries.shape[2] + + # Handle block mask attention with flex_attention + pad_size = max_num_tokens - packed_queries.shape[0] + packed_queries_padded = pad_sequence(packed_queries.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + packed_keys_padded = pad_sequence(packed_keys.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + packed_values_padded = pad_sequence(packed_values.permute(1, 0, 2), pad_size) # [heads,max_num_tokens,head_dim] + + packed_attn_output = flex_attention( + packed_queries_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + packed_keys_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + packed_values_padded.unsqueeze(0), # [1,heads,max_num_tokens,head_dim] + enable_gqa=True, + block_mask=attention_mask, + ) # [1,heads,max_num_tokens,head_dim] + assert isinstance(packed_attn_output, torch.Tensor) + + end_index = packed_attn_output.shape[2] - pad_size + packed_attn_output = packed_attn_output[0, :, :end_index, :] # [heads,N,head_dim] + packed_attn_output = packed_attn_output.transpose(0, 1).reshape( + -1, num_attention_heads * head_dim + ) # [N,heads*head_dim] + + return from_joint(packed_attn_output, packed_query_states) + + +def dispatch_attention( + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask | SplitInfo, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, +) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + assert memory_value is None, "Base dispatch_attention does not handle MemoryValue" + if isinstance(attention_mask, SplitInfo) and attention_mask.is_three_way: + output = three_way_attention( + packed_query_states, + packed_key_states, + packed_value_states, + natten_metadata=natten_metadata, + attention_meta=attention_mask, + ) + elif isinstance(attention_mask, SplitInfo): + output = two_way_attention(packed_query_states, packed_key_states, packed_value_states) + else: + output = block_flex_attention(packed_query_states, packed_key_states, packed_value_states, attention_mask) + return output, None + + +def build_packed_sequence( + joint_attn_implementation: str, + *, + packed_sequence: torch.Tensor, + attn_modes: list[str], + split_lens: list[int], + sample_lens: list[int], + packed_und_token_indexes: torch.LongTensor, + packed_gen_token_indexes: torch.LongTensor, + num_heads: int, + head_dim: int, + num_layers: int, + token_shapes: list[tuple[int, int, int]] | None = None, + natten_parameter_list: list | None = None, + block_size: int = 128, + is_image_batch: bool = False, + cp_world_size: int = 1, + video_temporal_causal: bool = False, + use_rolling_kv_cache: bool = False, + vision_token_shapes: list[tuple[int, int, int]] | None = None, + action_token_shapes: list[tuple[int, ...]] | None = None, + num_action_tokens_per_supertoken: int = 0, + null_action_supertokens: bool = False, + pad_for_cuda_graphs: bool = False, +) -> tuple[FactoredSequencePack | JointSequencePack, AttentionMaskType, list | None]: + """ + Build the model input pack and attention meta for joint attention. + Returns a tuple: (input_pack, attention_meta). + """ + device = packed_sequence.device + natten_metadata_list = None + if joint_attn_implementation == "flex": + sparse_mask = create_sparse_mask(sample_lens, split_lens, attn_modes, device) + seqlen = sum(sample_lens) + attention_meta = create_block_mask( + sparse_mask, + B=1, + H=num_heads, + Q_LEN=seqlen, + KV_LEN=seqlen, + device=device, + BLOCK_SIZE=block_size, + _compile=True, + ) + make_pack = joint_from_joint_sequence + elif joint_attn_implementation == "two_way": + attention_meta = SplitInfo( + split_lens=split_lens, + attn_modes=attn_modes, + sample_lens=sample_lens, + actual_len=int(packed_sequence.shape[0]), + ) + make_pack = factored_from_joint_sequence + elif joint_attn_implementation == "three_way": + attention_meta = SplitInfo( + split_lens=split_lens, + attn_modes=attn_modes, + sample_lens=sample_lens, + actual_len=int(packed_sequence.shape[0]), + is_three_way=True, + vision_token_shapes=vision_token_shapes, + action_token_shapes=action_token_shapes, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + null_action_supertokens=null_action_supertokens, + ) + make_pack = factored_from_joint_sequence + # The rolling KV-cache path implements temporal causality in + # three_way_attention_with_kv_cache; skip NATTEN metadata. + if not use_rolling_kv_cache: + # Temporal causal: encode (T, S) supertoken layout; spatial NATTEN: encode (H, W) layout. + if video_temporal_causal: + natten_metadata_list = generate_temporal_causal_natten_metadata( + vision_token_shapes=vision_token_shapes, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + num_layers=num_layers, + head_dim=head_dim, + device=device, + dtype=packed_sequence.dtype, + requires_grad=packed_sequence.requires_grad, + ) + else: + natten_metadata_list = generate_natten_metadata( + token_shapes=token_shapes, + head_dim=head_dim, + num_layers=num_layers, + device=device, + dtype=packed_sequence.dtype, + requires_grad=packed_sequence.requires_grad, + natten_parameter_list=natten_parameter_list, + ) + else: + raise ValueError( + f"Invalid joint_attn_implementation: {joint_attn_implementation}. " + "Must be 'two_way', 'three_way', or 'flex'." + ) + + input_pack = make_pack( + packed_sequence=packed_sequence, + attn_modes=attn_modes, + split_lens=split_lens, + sample_lens=sample_lens, + packed_und_token_indexes=packed_und_token_indexes.to(device), + packed_gen_token_indexes=packed_gen_token_indexes.to(device), + is_image_batch=is_image_batch, + cp_world_size=cp_world_size, + pad_for_cuda_graphs=pad_for_cuda_graphs, + ) + # Not needed anymore, can cause recompilations. + input_pack.pop("split_lens", None) + input_pack.pop("attn_modes", None) + return input_pack, attention_meta, natten_metadata_list diff --git a/cosmos_training/cosmos/model/vfm/mot/context_parallel_utils.py b/cosmos_training/cosmos/model/vfm/mot/context_parallel_utils.py new file mode 100644 index 00000000..abeeee23 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/context_parallel_utils.py @@ -0,0 +1,427 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Context Parallelism Utilities. + +Integration Guide: +------------------ +1. Shard the Input Sequence: + Call `get_context_parallel_sharded_sequence` at the start of the forward pass to split + the global input pack into local shards. + + ```python + input_pack, position_ids = get_context_parallel_sharded_sequence( + attn_implementation, input_pack, position_ids, parallel_dims + ) + ``` + +2. Apply Context Parallel Attention: + Use `context_parallel_attention` inside your attention block. It handles All-to-All + communication (gather seq, scatter heads -> attn -> gather heads, scatter seq). + + ```python + output, kv_to_store = context_parallel_attention( + cp_mesh, query_pack, key_pack, value_pack, mask, local_attn_func + ) + ``` + +3. Gather Final Hidden States (Optional): + Use `get_context_parallel_last_hidden_state` if the full global sequence is needed for + loss or post-processing. +""" + +from typing import Callable + +import torch +import torch.distributed as dist +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor, Replicate, Shard +from torch.nn.attention.flex_attention import BlockMask + +from cosmos.data.vfm.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, + from_mode_splits, + get_all_seq, + get_causal_seq, + get_full_only_seq, + get_gen_position_ids, + get_gen_seq, + get_und_position_ids, + get_und_seq, +) +from cosmos.model.vfm.mot.attention import SplitInfo +from cosmos.model.vfm.utils.memory import KVToStore, MemoryValue +from cosmos.utils.vfm.parallelism import ParallelDims + + +def _pad_to_N(N, x: torch.Tensor) -> torch.Tensor: + assert x.shape[0] <= N + padded = x.new_zeros((N, *x.shape[1:])) + padded[: x.shape[0]] = x + return padded + + +def _filter_and_rebase_sparse_index( + global_indices: torch.Tensor, + start_offset: int, + end_offset: int, +) -> torch.Tensor: + """Filters sparse global indices to the local physical slice and shifts them to local 0-based coordinates.""" + + # Keep only global indices that fall within [start_offset, end_offset) + mask = (global_indices >= start_offset) & (global_indices < end_offset) + local_global_indices = global_indices[mask] + + # Subtract the start_offset to make them local (0-based) + local_rebased_indices = local_global_indices - start_offset + + return local_rebased_indices + + +def get_context_parallel_sharded_sequence( + attn_implementation: str, + input_pack: FactoredSequencePack, + position_ids: torch.Tensor, + parallel_dims: ParallelDims | None, +) -> tuple[FactoredSequencePack, torch.Tensor]: + """ + Splits the full input_pack into a local shard for Context Parallelism. + """ + if parallel_dims is None or not parallel_dims.cp_enabled: + return input_pack, position_ids + + assert attn_implementation in ("two_way", "three_way"), ( + f"Context parallel is only supported for two_way and three_way joint attention modes, " + f"got {attn_implementation!r}" + ) + cp_mesh = parallel_dims.cp_mesh + cp_group = cp_mesh.get_group() + rank = dist.get_rank(cp_group) + world_size = dist.get_world_size(cp_group) + + text_seq = get_und_seq(input_pack) + gen_seq = get_gen_seq(input_pack) + assert text_seq.shape[0] % world_size == 0, "text_seq.shape[0] must be divisible by world_size" + assert gen_seq.shape[0] % world_size == 0, "gen_seq.shape[0] must be divisible by world_size" + + text_len = text_seq.shape[0] + text_shard_len = text_len // world_size + text_shard = text_seq.narrow(0, rank * text_shard_len, text_shard_len) + + gen_len = gen_seq.shape[0] + gen_shard_len = gen_len // world_size + gen_shard = gen_seq.narrow(0, rank * gen_shard_len, gen_shard_len) + + text_position_ids = get_und_position_ids(position_ids, input_pack) + gen_position_ids = get_gen_position_ids(position_ids, input_pack) + + # Handle 3D mRoPE position IDs: shape (3, L) + is_mrope = position_ids.dim() == 2 and position_ids.shape[0] == 3 + if is_mrope: + text_position_ids = text_position_ids.transpose(0, 1) # [text_len,3] + gen_position_ids = gen_position_ids.transpose(0, 1) # [gen_len,3] + + # pad to N + text_position_ids = _pad_to_N(text_seq.shape[0], text_position_ids) + gen_position_ids = _pad_to_N(gen_seq.shape[0], gen_position_ids) + + text_position_ids_shard = text_position_ids.narrow(0, rank * text_shard_len, text_shard_len) + gen_position_ids_shard = gen_position_ids.narrow(0, rank * gen_shard_len, gen_shard_len) + + # create local pack + local_pack = from_mode_splits(text_shard, gen_shard, input_pack, is_sharded=True) + local_position_ids = torch.cat( + [text_position_ids_shard, gen_position_ids_shard], dim=0 + ) # [text_shard_len+gen_shard_len] or [text_shard_len+gen_shard_len,3] + + if is_mrope: + local_position_ids = local_position_ids.transpose(0, 1) # [3,text_shard_len+gen_shard_len] + + return local_pack, local_position_ids + + +def get_context_parallel_last_hidden_state( + packed_outputs: FactoredSequencePack, + parallel_dims: ParallelDims | None, +) -> torch.Tensor: + if parallel_dims is None or not parallel_dims.cp_enabled: + return get_all_seq(packed_outputs) + + # since unpatchify assumes full images, for now using all_gather to gather the predictions from all context parallel ranks + # This step can be removed once we make unpatchify work with context parallel local sequences + und_hidden_seq = get_und_seq(packed_outputs) # [text_shard_len,hidden_size] + gen_hidden_seq = get_gen_seq(packed_outputs) # [gen_shard_len,hidden_size] + + gathered_und_seq = all_gather_tensor( + und_hidden_seq, gather_dim=0, cp_mesh=parallel_dims.cp_mesh + ) # [text_len,hidden_size] + gathered_gen_seq = all_gather_tensor( + gen_hidden_seq, gather_dim=0, cp_mesh=parallel_dims.cp_mesh + ) # [gen_len,hidden_size] + + gathered_hidden_pack = from_mode_splits(gathered_und_seq, gathered_gen_seq, packed_outputs, is_sharded=False) + last_hidden_state = get_all_seq(gathered_hidden_pack) + return last_hidden_state + + +def context_parallel_broadcast( + data: torch.Tensor | dict[str, torch.Tensor], parallel_dims: ParallelDims, iteration: int +) -> torch.Tensor | dict[str, torch.Tensor]: + """ + Broadcasts a tensor or a dictionary of tensors to all ranks in the context parallel group. + """ + rank = parallel_dims.cp_rank + cp_world_size = parallel_dims.cp_mesh.size() + cp_data_batch_owner = iteration % cp_world_size + + broadcast_list = [None] + if rank == cp_data_batch_owner: + broadcast_list = [data] + + global_src_rank = dist.get_global_rank(parallel_dims.cp_mesh.get_group(), cp_data_batch_owner) + dist.broadcast_object_list(broadcast_list, src=global_src_rank, group=parallel_dims.cp_mesh.get_group()) + local_data = broadcast_list[0] + assert local_data is not None + return local_data + + +def all_to_all_tensor( + local_input: torch.Tensor, + scatter_dim: int, + gather_dim: int, + cp_mesh: "DeviceMesh", +) -> torch.Tensor: + """ + All-to-all via DTensor redistribute. + Input placement: Shard(gather_dim) -> The dimension we are about to gather was split. + Output placement: Shard(scatter_dim) -> The dimension we are about to scatter will be split. + """ + # Wrap local tensor as DTensor with current placement + # gather_dim is the dimension that is currently sharded locally (so we can gather it to full) + global_dt = DTensor.from_local(local_input, cp_mesh, [Shard(gather_dim)], run_check=False) + + # Redistribute to new placement (shard scatter_dim) + new_dt = global_dt.redistribute(cp_mesh, [Shard(scatter_dim)]) + + # Convert back to local + return new_dt.to_local() + + +def all_gather_tensor( + local_input: torch.Tensor, + gather_dim: int, + cp_mesh: "DeviceMesh", +) -> torch.Tensor: + """ + All-gather via DTensor redistribute. + Input placement: Shard(gather_dim) -> The dimension we are about to gather was split. + Output placement: Replicate() -> Full copy on each rank. + """ + # Wrap local tensor as DTensor with current placement + global_dt = DTensor.from_local(local_input, cp_mesh, [Shard(gather_dim)], run_check=False) + + # Redistribute to new placement (Replicate) + new_dt = global_dt.redistribute(cp_mesh, [Replicate()]) + + # Convert back to local + return new_dt.to_local() + + +def gather_seq_scatter_heads( + x: torch.Tensor, + seq_dim: int, + head_dim: int, + cp_mesh: DeviceMesh, +) -> torch.Tensor: + """ + A func to sync embedding input with alltoall in sequence parallel. + gather sequence dimension and scatter head dim: + For example, when seq_dim is 0, head_dim is 1, the transformation is: + [z, seq/n, h, ...] -> [z, seq, h/n, ...] + Args: + x: shape of [z, seq, h, ...] + seq_dim: the dimension to gather + head_dim: the dimension to scatter + cp_mesh: ulysses sequence parallelism size + Returns: + torch.Tensor: shape of gathered and scattered tensor + """ + return all_to_all_tensor(x, head_dim, seq_dim, cp_mesh) + + +def gather_heads_scatter_seq( + x: torch.Tensor, + head_dim: int, + seq_dim: int, + cp_mesh: DeviceMesh, +) -> torch.Tensor: + """ + A func to sync attention result with alltoall in sequence parallel. + gather head dimension and scatter seq dim: + For example, when seq_dim is 0, head_dim is 1, the transformation is: + [seq, h/n, ...] -> [seq/n, h, ...] + + Args: + x (torch.Tensor): shape of [bsz, seq, h/n, ...] + head_dim (int): the dimension to gather + seq_dim (int): the dimension to scatter + cp_mesh (DeviceMesh): ulysses sequence parallelism size + splits (List[torch.Tensor], optional): Manual splits for variable length scattering + + Returns: + torch.Tensor: shape of [bsz, seq/n, h, ...] + """ + return all_to_all_tensor(x, seq_dim, head_dim, cp_mesh) + + +def context_parallel_attention( + cp_mesh: DeviceMesh, + packed_query_states: FactoredSequencePack, + packed_key_states: FactoredSequencePack, + packed_value_states: FactoredSequencePack, + attention_mask: BlockMask | SplitInfo, + attention_function: Callable, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, +) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + """Ulysses-style context parallel attention for packed und+gen sequences. + + Each rank holds a sequence shard [S/cp, H, D]. Two all-to-all calls convert + between seq-sharded and head-sharded representations: + 1. gather seq, scatter heads → [S, H/cp, D] (head-sharded) + 2. run attention on full sequence with reduced heads + 3. gather heads, scatter seq → [S/cp, H, D] (seq-sharded) + + When ``memory_value`` is present, produces head-sharded ``kv_to_store`` + from the post-all-to-all K/V tensors for the caller to write back via + ``MemoryState.write_for_layer()``. Does **not** write to any cache + directly. + + Args: + cp_mesh: Device mesh for context parallelism. + packed_query_states: Packed Q for both und and gen tokens, seq-sharded [S/cp, H, D]. + packed_key_states: Packed K for both und and gen tokens, seq-sharded [S/cp, H, D]. + packed_value_states: Packed V for both und and gen tokens, seq-sharded [S/cp, H, D]. + attention_mask: Block mask or split info describing causal/full attention pattern. + attention_function: Callable implementing the actual attention kernel. + natten_metadata: Optional neighborhood attention metadata. + memory_value: Optional memory value for KV-cache training / AR inference. + + Returns: + (output_pack, kv_to_store): + output_pack: Packed attention output, seq-sharded [S/cp, H, D]. + kv_to_store: Head-sharded ``(gen_k, gen_v, und_k, und_v)`` when + ``memory_value`` is present, ``None`` otherwise. + """ + cp_group = cp_mesh.get_group() + cp_world_size = torch.distributed.get_world_size(cp_group) + assert cp_world_size > 1, "Context parallel world size must be greater than 1" + q_und_seq, _ = get_causal_seq(packed_query_states) # [text_shard_len,H,head_dim] + q_gen_seq, _ = get_full_only_seq(packed_query_states) # [gen_shard_len,H,head_dim] + k_und_seq, _ = get_causal_seq(packed_key_states) # [text_shard_len,H,head_dim] + k_gen_seq, _ = get_full_only_seq(packed_key_states) # [gen_shard_len,H,head_dim] + v_und_seq, _ = get_causal_seq(packed_value_states) # [text_shard_len,H,head_dim] + v_gen_seq, _ = get_full_only_seq(packed_value_states) # [gen_shard_len,H,head_dim] + + # Check that number of heads is divisible by CP world size + assert q_und_seq.shape[1] % cp_world_size == 0, ( + f"Query heads ({q_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + assert k_und_seq.shape[1] % cp_world_size == 0, ( + f"Key heads ({k_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + assert v_und_seq.shape[1] % cp_world_size == 0, ( + f"Value heads ({v_und_seq.shape[1]}) must be divisible by context parallel world size ({cp_world_size})" + ) + + + # when doing AR-inference with a KV-cache. + + # all2all: gather sequence, scatter heads → head-sharded + q_und_seq = gather_seq_scatter_heads( + q_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + q_gen_seq = gather_seq_scatter_heads( + q_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + k_und_seq = gather_seq_scatter_heads( + k_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + k_gen_seq = gather_seq_scatter_heads( + k_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + v_und_seq = gather_seq_scatter_heads( + v_und_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [text_len,H_local,head_dim] + v_gen_seq = gather_seq_scatter_heads( + v_gen_seq, seq_dim=0, head_dim=1, cp_mesh=cp_mesh + ) # [gen_len,H_local,head_dim] + + # Build head-sharded kv_to_store when memory is active. + kv_to_store: KVToStore | None = None + if memory_value is not None: + und_len = packed_key_states["_num_causal_tokens"] + gen_len = packed_key_states["_num_full_tokens"] + kv_to_store = ( + k_gen_seq[:gen_len].unsqueeze(0), + v_gen_seq[:gen_len].unsqueeze(0), + k_und_seq[:und_len].unsqueeze(0), + v_und_seq[:und_len].unsqueeze(0), + ) + + q_und_seq_len = q_und_seq.shape[0] + q_gen_seq_len = q_gen_seq.shape[0] + meta = dict(packed_query_states) + packed_query_states_ = from_mode_splits(q_und_seq, q_gen_seq, meta, is_sharded=False) + packed_key_states_ = from_mode_splits(k_und_seq, k_gen_seq, meta, is_sharded=False) + packed_value_states_ = from_mode_splits(v_und_seq, v_gen_seq, meta, is_sharded=False) + + # dispatch_attention returns (output, kv_to_store | None) + attn_output_pack_hp, _inner_kv_to_store = attention_function( + packed_query_states_, + packed_key_states_, + packed_value_states_, + attention_mask, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + attn_output_und_hp = get_und_seq(attn_output_pack_hp) # [text_len,H_local,head_dim] + attn_output_gen_hp = get_gen_seq(attn_output_pack_hp) # [gen_len,H_local,head_dim] + + attn_output_und_hp = attn_output_und_hp[:q_und_seq_len].contiguous() # [text_len,H_local,head_dim] + attn_output_gen_hp = attn_output_gen_hp[:q_gen_seq_len].contiguous() # [gen_len,H_local,head_dim] + + # all2all: gather heads, scatter seq → seq-sharded + attn_output_und_sp = gather_heads_scatter_seq( + attn_output_und_hp, + seq_dim=0, + head_dim=1, + cp_mesh=cp_mesh, + ) # [text_shard_len,H,head_dim] + attn_output_gen_sp = gather_heads_scatter_seq( + attn_output_gen_hp, + seq_dim=0, + head_dim=1, + cp_mesh=cp_mesh, + ) # [gen_shard_len,H,head_dim] + + final_output_pack_sp = from_mode_splits( + attn_output_und_sp, attn_output_gen_sp, packed_query_states, is_sharded=True + ) + + return final_output_pack_sp, kv_to_store diff --git a/cosmos_training/cosmos/model/vfm/mot/cosmos3_vfm_network.py b/cosmos_training/cosmos/model/vfm/mot/cosmos3_vfm_network.py new file mode 100644 index 00000000..fedddbcc --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/cosmos3_vfm_network.py @@ -0,0 +1,1118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import List, Optional, Tuple + +import torch +from torch import nn +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_utils import PreTrainedModel + +from cosmos.data.vfm.sequence_packing import ModalityData, PackedSequence, verify_natten_parameter_list +from cosmos.model.vfm.mot.attention import build_packed_sequence +from cosmos.model.vfm.mot.context_parallel_utils import ( + get_context_parallel_last_hidden_state, + get_context_parallel_sharded_sequence, +) +from cosmos.model.vfm.mot.domain_aware_linear import DomainAwareLinear +from cosmos.model.vfm.mot.modeling_utils import ( + FlattenedSinCosPositionEmbedding, + TimestepEmbedder, + VideoRopePosition3DEmb, +) +from cosmos.model.vfm.utils.memory import MemoryState + + +class Cosmos3VFMNetworkConfig(PretrainedConfig): + def __init__( + self, + vision_gen=True, + action_gen=False, + sound_gen=False, + vlm_config=None, + latent_patch_size=2, + latent_downsample_factor=8, + latent_channel_size=16, + position_embedding_type="3d_rope", + max_latent_h=32, + max_latent_w=32, + max_latent_t=32, + rope_h_extrapolation_ratio=1.0, + rope_w_extrapolation_ratio=1.0, + rope_t_extrapolation_ratio=1.0, + enable_fps_modulation=False, + base_fps=24, + vit_max_num_patch_per_side=70, + connector_act="gelu_pytorch_tanh", + interpolate_pos=False, + timestep_shift=1.0, + timestep_scale=0.001, + predict_text_tokens=False, + joint_attn_implementation="two_way", + action_dim=32, + num_embodiment_domains=32, + temporal_compression_factor_vision=4, + temporal_compression_factor_action=1, + natten_parameter_list=None, + video_temporal_causal=False, + # Sound generation parameters + sound_dim: int | None = None, + temporal_compression_factor_sound=1, + sound_latent_fps: int = 25, + **kwargs, + ): + self.vision_gen = vision_gen + self.sound_gen = sound_gen + self.vlm_config = vlm_config + self.latent_patch_size = latent_patch_size + self.latent_downsample_factor = latent_downsample_factor + self.latent_channel_size = latent_channel_size + self.position_embedding_type = position_embedding_type + self.max_latent_h = max_latent_h + self.max_latent_w = max_latent_w + self.max_latent_t = max_latent_t + self.rope_h_extrapolation_ratio = rope_h_extrapolation_ratio + self.rope_w_extrapolation_ratio = rope_w_extrapolation_ratio + self.rope_t_extrapolation_ratio = rope_t_extrapolation_ratio + self.enable_fps_modulation = enable_fps_modulation + self.base_fps = base_fps + self.vit_max_num_patch_per_side = vit_max_num_patch_per_side + self.connector_act = connector_act + self.interpolate_pos = interpolate_pos + self.timestep_shift = timestep_shift + self.timestep_scale = timestep_scale + self.predict_text_tokens = predict_text_tokens + self.joint_attn_implementation = joint_attn_implementation + self.temporal_compression_factor_vision = temporal_compression_factor_vision + self.natten_parameter_list = natten_parameter_list + self.video_temporal_causal = video_temporal_causal + + # action related parameters + self.action_gen = action_gen # whether to generate action tokens + self.action_dim = action_dim + self.num_embodiment_domains = num_embodiment_domains + self.temporal_compression_factor_action = temporal_compression_factor_action + if self.action_gen: + assert self.vision_gen, ( + "Action generation requires visual generation! We do NOT support action only training!" + ) + + # sound related parameters + self.sound_dim = sound_dim + self.temporal_compression_factor_sound = temporal_compression_factor_sound + self.sound_latent_fps = sound_latent_fps + if self.sound_gen: + assert self.vision_gen, ( + "Sound generation requires visual generation! We do NOT support sound only training!" + ) + + super().__init__(**kwargs) + + +class Cosmos3VFMNetwork(PreTrainedModel): + config_class = Cosmos3VFMNetworkConfig + base_model_prefix = "cosmos3" + + def __init__(self, language_model, config: Cosmos3VFMNetworkConfig): + super().__init__(config) + self.language_model = language_model + self.hidden_size = config.vlm_config.hidden_size + self.use_moe = "Mo" in config.vlm_config.layer_module + self.num_heads = config.vlm_config.num_attention_heads + self.num_kv_heads = config.vlm_config.num_key_value_heads + self.head_dim = config.vlm_config.head_dim + self.num_hidden_layers = config.vlm_config.num_hidden_layers + self.predict_text_tokens = config.predict_text_tokens + + if config.natten_parameter_list is not None and config.joint_attn_implementation != "three_way": + raise NotImplementedError( + f"Sparsity is only supported with 'three_way' attention, but got {config.joint_attn_implementation=}, " + "and 'natten_parameter_list' was not None." + ) + self.natten_parameter_list = verify_natten_parameter_list( + config.natten_parameter_list, num_layers=self.num_hidden_layers + ) + + if config.video_temporal_causal and config.joint_attn_implementation != "three_way": + raise ValueError( + f"video_temporal_causal=True requires joint_attn_implementation='three_way', " + f"but got {config.joint_attn_implementation!r}." + ) + self.video_temporal_causal = config.video_temporal_causal + self.pad_for_cuda_graphs = False + + if config.vision_gen: + self.latent_patch_size = config.latent_patch_size + self.timestep_shift = config.timestep_shift + self.timestep_scale = config.timestep_scale + self.latent_downsample = config.latent_downsample_factor * config.latent_patch_size + self.max_latent_h = config.max_latent_h + self.max_latent_w = config.max_latent_w + self.max_latent_t = config.max_latent_t + self.latent_channel = config.latent_channel_size + self.patch_latent_dim = self.latent_patch_size**2 * self.latent_channel + + self.time_embedder = TimestepEmbedder(self.hidden_size) + self.vae2llm = nn.Linear(self.patch_latent_dim, self.hidden_size) + self.llm2vae = nn.Linear(self.hidden_size, self.patch_latent_dim) + + assert config.position_embedding_type in ["3d_rope", "flattened_sin_cos", "unified_3d_mrope"] + if config.position_embedding_type == "3d_rope": + self.latent_pos_embed = VideoRopePosition3DEmb( + head_dim=self.hidden_size, + len_h=self.max_latent_h, + len_w=self.max_latent_w, + len_t=self.max_latent_t, + h_extrapolation_ratio=config.rope_h_extrapolation_ratio, + w_extrapolation_ratio=config.rope_w_extrapolation_ratio, + t_extrapolation_ratio=config.rope_t_extrapolation_ratio, + enable_fps_modulation=config.enable_fps_modulation, # fps_modulation scales RoPE by fps. By default, disable FPS RoPE modulation. + base_fps=config.base_fps, + base_temporal_compression_factor=config.temporal_compression_factor_vision, + temporal_compression_factor=config.temporal_compression_factor_vision, + ) + elif config.position_embedding_type == "flattened_sin_cos": + self.latent_pos_embed = FlattenedSinCosPositionEmbedding( + max_latent_h=self.max_latent_h, max_latent_w=self.max_latent_w, hidden_size=self.hidden_size + ) + elif config.position_embedding_type == "unified_3d_mrope": + # No additive position embedding - position info is in 3D position IDs for attention + self.latent_pos_embed = None + else: + raise ValueError(f"Unknown position_embedding_type: {config.position_embedding_type!r}") + + if config.action_gen: + self.action_dim = config.action_dim + self.num_embodiment_domains = config.num_embodiment_domains + self.action2llm = DomainAwareLinear(self.action_dim, self.hidden_size, self.num_embodiment_domains) + self.llm2action = DomainAwareLinear(self.hidden_size, self.action_dim, self.num_embodiment_domains) + + if config.position_embedding_type == "3d_rope": + self.action_pos_embed = VideoRopePosition3DEmb( + head_dim=self.hidden_size, + len_h=1, + len_w=1, + len_t=self.max_latent_t * config.temporal_compression_factor_vision, + h_extrapolation_ratio=config.rope_h_extrapolation_ratio, + w_extrapolation_ratio=config.rope_w_extrapolation_ratio, + t_extrapolation_ratio=config.rope_t_extrapolation_ratio, + enable_fps_modulation=config.enable_fps_modulation, + base_fps=config.base_fps, + base_temporal_compression_factor=config.temporal_compression_factor_vision, # vision compression factor is used for base tps + temporal_compression_factor=config.temporal_compression_factor_action, # Action is at frame rate (no temporal compression) + ) + elif config.position_embedding_type == "unified_3d_mrope": + # No additive position embedding - position info is in 3D position IDs for attention + self.action_pos_embed = None + else: + raise ValueError(f"Unknown position_embedding_type: {config.position_embedding_type!r}") + + self.action_modality_embed = nn.Parameter(torch.zeros(self.hidden_size)) + + if config.sound_gen: + self.sound_dim = config.sound_dim + self.sound2llm = nn.Linear(config.sound_dim, self.hidden_size) + self.llm2sound = nn.Linear(self.hidden_size, config.sound_dim) + self.sound_modality_embed = nn.Parameter(torch.zeros(self.hidden_size)) + + self.config = config + self.parallel_dims = None + + def init_weights(self, buffer_device: torch.device | None): + if self.config.vision_gen or self.config.action_gen or self.config.sound_gen: + self.time_embedder._init_weights() + + if self.config.vision_gen: + std = 1.0 / math.sqrt(self.patch_latent_dim) + torch.nn.init.trunc_normal_(self.vae2llm.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.vae2llm.bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2vae.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2vae.bias) + + if self.latent_pos_embed is not None: + self.latent_pos_embed._init_weights() + + if self.config.action_gen: + # DomainAwareLinear uses embeddings for weights, so we initialize them differently + # action2llm: input_size=action_dim, output_size=hidden_size + std = 1.0 / math.sqrt(self.action_dim) + torch.nn.init.trunc_normal_(self.action2llm.fc.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.action2llm.bias.weight) + + # llm2action: input_size=hidden_size, output_size=action_dim + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2action.fc.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2action.bias.weight) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.action_modality_embed, std=std, a=-3 * std, b=3 * std) + + if self.action_pos_embed is not None: + self.action_pos_embed._init_weights() + + if self.config.sound_gen: + # sound2llm: input_size=sound_dim, output_size=hidden_size + std = 1.0 / math.sqrt(self.sound_dim) + torch.nn.init.trunc_normal_(self.sound2llm.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.sound2llm.bias) + + # llm2sound: input_size=hidden_size, output_size=sound_dim + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.llm2sound.weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.llm2sound.bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.sound_modality_embed, std=std, a=-3 * std, b=3 * std) + + self.language_model.init_weights(buffer_device=buffer_device) + + def patchify_and_pack_latents( + self, tokens_vision: torch.Tensor, token_shapes_vision: List[Tuple[int, int, int]] + ) -> tuple[torch.Tensor, List[Tuple[int, int, int]]]: + p = self.latent_patch_size + # Patchify and pack the latents + packed_latent = [] + original_latent_shapes = [] # Store original shapes for unpadding later + + # C, T, H, W + for latent, (t, h, w) in zip(tokens_vision, token_shapes_vision): + latent = latent.squeeze(0) # [C,T,H,W] + + # Get original latent dimensions + _, t_actual, h_actual, w_actual = latent.shape + original_latent_shapes.append((t_actual, h_actual, w_actual)) + + # Compute padded dimensions (must be divisible by p) + h_padded = ((h_actual + p - 1) // p) * p + w_padded = ((w_actual + p - 1) // p) * p + + # Zero-pad if dimensions are not divisible by p + if h_padded != h_actual or w_padded != w_actual: + padded = torch.zeros( + (self.latent_channel, t_actual, h_padded, w_padded), + device=latent.device, + dtype=latent.dtype, + ) # [C,T,H_padded,W_padded] + padded[:, :, :h_actual, :w_actual] = latent + latent = padded # [C,T,H_padded,W_padded] + + # Compute number of patches after padding + h_patches = h_padded // p + w_patches = w_padded // p + + # Patchify + latent = latent.reshape( + self.latent_channel, t_actual, h_patches, p, w_patches, p + ) # [C,T,h_patches,p,w_patches,p] + latent = torch.einsum("cthpwq->thwpqc", latent).reshape( + -1, p * p * self.latent_channel + ) # [T*h_patches*w_patches,patch_latent_dim] + packed_latent.append(latent) + + # We assumed latents we get to the network is already noised + packed_latent = torch.cat(packed_latent, dim=0) # [total_vision_patches,patch_latent_dim] + return packed_latent, original_latent_shapes + + def unpatchify_and_unpack_latents( + self, + packed_mse_preds: torch.Tensor, + token_shapes_vision: List[Tuple[int, int, int]], + noisy_frame_indexes_vision: list[torch.Tensor], + original_latent_shapes: List[Tuple[int, int, int]] | None = None, + ) -> list[torch.Tensor]: + p = self.latent_patch_size + unpatchified_latents = [] + + # Split packed_mse_preds back into individual latents based on token_shapes_vision + start_idx = 0 + for i, (t_c, h_c, w_c) in enumerate(token_shapes_vision): + # Get original shape for unpadding (if provided) + if original_latent_shapes is not None: + t_orig, h_orig, w_orig = original_latent_shapes[i] + # Compute padded dimensions used during patchify + h_padded = ((h_orig + p - 1) // p) * p + w_padded = ((w_orig + p - 1) // p) * p + h_patches = h_padded // p + w_patches = w_padded // p + else: + # Fallback: use token shapes directly (assumes no padding was needed) + t_orig, h_orig, w_orig = t_c, h_c * p, w_c * p + h_patches, w_patches = h_c, w_c + + # noisy_frame_indexes_vision is a list of tensors, each with shape (T,), + # where the values are the noisy frame indices. + noisy_frame_indexes = noisy_frame_indexes_vision[i] + t_n = len(noisy_frame_indexes) + + # Initialize with the original shape (after unpadding), zeros for clean frames + output_tensor = torch.zeros( + (self.latent_channel, t_c, h_orig, w_orig), + device=packed_mse_preds.device, + dtype=packed_mse_preds.dtype, + ) # [C,T,H_orig,W_orig] + num_patches = t_n * h_patches * w_patches + if num_patches > 0: + end_idx = start_idx + num_patches + # Extract patches for this latent + latent_patches = packed_mse_preds[start_idx:end_idx] # [num_patches,patch_latent_dim] + # Reshape back to [t_n, h_patches, w_patches, p, p, channels] + latent_patches = latent_patches.reshape( + t_n, h_patches, w_patches, p, p, self.latent_channel + ) # [T_n,h_patches,w_patches,p,p,C] + # Invert the einsum operation: "thwpqc->cthpwq" + latent = torch.einsum("thwpqc->cthpwq", latent_patches) # [C,T_n,h_patches,p,w_patches,p] + # Reshape back to [channels, t_n, h_padded, w_padded] + latent = latent.reshape( + self.latent_channel, t_n, h_patches * p, w_patches * p + ) # [C,T_n,H_padded,W_padded] + + # Crop to original dimensions (unpad the zeros) + latent = latent[:, :, :h_orig, :w_orig] # [C,T_n,H_orig,W_orig] + + # Fill only the noisy frame positions using the actual mask indices + output_tensor[:, noisy_frame_indexes] = latent + + start_idx = end_idx + + unpatchified_latents.append(output_tensor.unsqueeze(0)) # [1,C,T,H,W] + + # Return list of unpatchified latents (supports variable shapes) + return unpatchified_latents + + def pack_action( + self, + tokens_action: list[torch.Tensor], + token_shapes_action: list[tuple[int, ...]], + domain_id_action: list[torch.Tensor], + ) -> tuple[torch.Tensor, torch.Tensor]: + """Pack variable-length action tokens into a 1D sequence for transformer input. + + Args: + tokens_action: List of action tensors, each [T_i, action_dim] (T_i may vary). + token_shapes_action: List of (T_i,) tuples per sample. + domain_id_action: List of domain ID tensors, each of shape [1]. + + Returns: + Tuple of (packed_tokens, per_token_domain_id): + packed_tokens: [total_action_tokens, action_dim] + per_token_domain_id: [total_action_tokens] + """ + packed: list[torch.Tensor] = [] + domain_ids: list[torch.Tensor] = [] + for tokens, shape, d_id in zip(tokens_action, token_shapes_action, domain_id_action): + T = shape[0] + packed.append(tokens[:T]) + domain_ids.append(d_id.expand(T)) + return torch.cat(packed, dim=0), torch.cat(domain_ids, dim=0) + + def unpack_action( + self, + packed_action_preds: torch.Tensor, + token_shapes_action: list[tuple[int, ...]], + noisy_frame_indexes_action: list[torch.Tensor], + ) -> list[torch.Tensor]: + """Unpack action predictions back into per-sample action tensors. + + Args: + packed_action_preds: Packed action predictions of shape (total_noisy_tokens, action_dim) + token_shapes_action: Per-sample token shapes, each (T_i,) tuple. + noisy_frame_indexes_action: List of tensors, each with shape (Tn_i,), where the values + are the noisy frame indices for sample i. + + Returns: + List of per-sample tensors, each of shape (T_i, action_dim), with predictions + placed at noisy positions. Clean positions are left as zeros. + """ + unpacked: list[torch.Tensor] = [] + start_idx = 0 + for shape, noisy_frame_indexes in zip(token_shapes_action, noisy_frame_indexes_action): + T = shape[0] + output = torch.zeros( + (T, self.action_dim), + device=packed_action_preds.device, + dtype=packed_action_preds.dtype, + ) + t_n = len(noisy_frame_indexes) + if t_n > 0: + end_idx = start_idx + t_n + output[noisy_frame_indexes] = packed_action_preds[start_idx:end_idx] + start_idx = end_idx + unpacked.append(output) + return unpacked + + def pack_sound_latents( + self, + tokens_sound: list[torch.Tensor], + token_shapes_sound: list[tuple[int, int, int]], + ) -> torch.Tensor: + """Pack sound latents into a 1D sequence for transformer input. + + Args: + tokens_sound: List of sound latent tensors, each [C, T] + token_shapes_sound: List of (T, 1, 1) tuples per sample + + Returns: + Packed tensor of shape [total_sound_tokens, C] + """ + packed = [] + for sound, shape in zip(tokens_sound, token_shapes_sound): + T = shape[0] + # sound: [C, T] → take first T frames → [C, T] + # Then permute to [T, C] for packing + sound_tokens = sound[:, :T].permute(1, 0) # [T,C] + packed.append(sound_tokens) + return torch.cat(packed, dim=0) # [total_sound_tokens,C] + + def unpack_sound_latents( + self, + packed_sound_preds: torch.Tensor, + token_shapes_sound: list[tuple[int, int, int]], + noisy_frame_indexes_sound: list[torch.Tensor], + ) -> list[torch.Tensor]: + """Unpack sound predictions back into per-sample sound latents. + + Args: + packed_sound_preds: Packed sound predictions of shape (total_noisy_tokens, sound_dim) + token_shapes_sound: List of (T, 1, 1) tuples per sample + noisy_frame_indexes_action: List of tensors, each with shape (T_i,), where the values + are the noisy frame indices. T_i <= max_T. + + Returns: + List of per-sample tensors, each [C, T], with predictions placed at noisy positions. + Clean positions are left as zeros. + """ + unpacked = [] + start_idx = 0 + for shape, noisy_frame_indexes in zip(token_shapes_sound, noisy_frame_indexes_sound): + T = shape[0] + # Initialize output with zeros for clean positions + output = torch.zeros( + (self.sound_dim, T), + device=packed_sound_preds.device, + dtype=packed_sound_preds.dtype, + ) + + t_n = len(noisy_frame_indexes) + + if t_n > 0: + end_idx = start_idx + t_n + # packed_sound_preds: [total_noisy_tokens, C] → transpose and fill at noisy positions + output[:, noisy_frame_indexes] = packed_sound_preds[ + start_idx:end_idx + ].T # packed_sound_preds[...]: [T_n,C] → .T: [C,T_n] + start_idx = end_idx + + unpacked.append(output) + return unpacked + + def _encode_text( + self, + packed_seq: PackedSequence, + ) -> tuple[torch.Tensor, torch.dtype]: + """Embed text tokens and initialize packed_sequence. + + Args: + packed_seq: PackedSequence containing text_ids and text_indexes. + + Returns: + tuple of (packed_sequence, target_dtype) where packed_sequence has text embeddings filled in. + """ + packed_text_embedding = self.language_model.model.embed_tokens(packed_seq.text_ids) # [N_text,hidden_size] + packed_sequence = packed_text_embedding.new_zeros( + size=(packed_seq.sequence_length, self.hidden_size) + ) # [N_total,hidden_size] + packed_sequence[packed_seq.text_indexes] = ( + packed_text_embedding # [N_text,hidden_size] scattered into [N_total,hidden_size] + ) + return packed_sequence, packed_text_embedding.dtype + + def _encode_vision( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps: Optional[torch.Tensor] = None, + ) -> List[Tuple[int, int, int]] | None: + """Project vision tokens and fill into packed_sequence. + + Args: + packed_seq: PackedSequence containing vision tokens and metadata. + packed_sequence: The packed sequence tensor to fill vision embeddings into (modified in-place). + target_dtype: Target dtype for embeddings (typically from text embedding). + fps: Optional FPS tensor for RoPE modulation. + + Returns: + Original latent shapes before padding (for unpadding during decode), or None if no vision tokens. + """ + if packed_seq.vision is None or packed_seq.vision.tokens is None: + # No vision tokens in this batch + return None + + vision = packed_seq.vision + assert vision.tokens is not None # Type narrowing (checked above but reassignment loses it) + assert vision.token_shapes is not None + assert isinstance(vision.sequence_indexes, torch.Tensor) + assert isinstance(vision.timesteps, torch.Tensor) + assert isinstance(vision.mse_loss_indexes, torch.Tensor) + + packed_tokens_vision, original_latent_shapes = self.patchify_and_pack_latents( + vision.tokens, vision.token_shapes + ) # packed_tokens_vision: [total_vision_patches,patch_latent_dim] + + packed_tokens_vision = self.vae2llm(packed_tokens_vision) # [total_vision_patches,hidden_size] + + # Add absolute position embedding only when NOT using unified 3D mRoPE + # (3D mRoPE provides positional information via rotary embeddings instead) + if self.latent_pos_embed is not None: + latent_token_pos_emb = self.latent_pos_embed(vision.token_shapes, fps=fps).to( + target_dtype + ) # [total_vision_patches,hidden_size] + packed_tokens_vision = packed_tokens_vision + latent_token_pos_emb # [total_vision_patches,hidden_size] + + has_noisy_vision = vision.mse_loss_indexes.numel() > 0 + + if has_noisy_vision: + timesteps_vision = vision.timesteps * self.timestep_scale # [N_noisy_frames_vision] + + # Timesteps are computed in FP32 for numerical stability. + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_vision = self.time_embedder( + timesteps_vision + ) # [N_noisy_frames_vision,hidden_size] + packed_timestep_embeds_vision = packed_timestep_embeds_vision.to( + target_dtype + ) # [N_noisy_frames_vision,hidden_size] + + packed_tokens_vision = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_vision, + packed_timestep_embeds=packed_timestep_embeds_vision, + noisy_frame_indexes=vision.noisy_frame_indexes, + token_shapes=vision.token_shapes, + ) # [total_vision_patches,hidden_size] + + packed_sequence[vision.sequence_indexes] = ( + packed_tokens_vision # [total_vision_patches,hidden_size] scattered into [N_total,hidden_size] + ) + return original_latent_shapes + + def _decode_vision( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + original_latent_shapes: List[Tuple[int, int, int]] | None = None, + ) -> None: + """Decode vision tokens from hidden states and update output_dict. + + Args: + packed_seq: PackedSequence containing mse_loss_indexes_vision and token_shapes_vision. + last_hidden_state: Hidden states from the transformer. + output_dict: Output dictionary to update with mse_preds (modified in-place). + original_latent_shapes: Original latent shapes before padding (for unpadding). + """ + vision = packed_seq.vision + # Check if no vision or no noisy vision tokens + has_noisy_vision = ( + vision is not None + and vision.tokens is not None + and isinstance(vision.mse_loss_indexes, torch.Tensor) + and vision.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_vision: + # No noisy vision tokens present. The model is predicting actions + # given clean vision tokens. We need to execute a dummy forward to maintain + # computation graph consistency across ranks (FSDP should torch all weights). + preds_vision = torch.zeros( + [1, self.patch_latent_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,patch_latent_dim] + preds_vision = self.vae2llm(preds_vision) # [1,hidden_size] + preds_vision = self.llm2vae(preds_vision) # [1,patch_latent_dim] + # Return a list of per-sample zero tensors with correct shapes (e.g. (C, T, H, W)), + # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_vision + # gets properly-shaped tensors. Without this, the dummy tensor (1, patch_latent_dim) + # would cause a size mismatch when concatenating vision+action velocities. + # When vision is None (no vision in batch), fall back to [preds_vision] purely for + # gradient graph consistency — it won't be iterated over. + if vision is not None and vision.tokens is not None: + preds_vision_list = [torch.zeros_like(tok) for tok in vision.tokens] + # Inject dummy forward's computation graph so vae2llm/llm2vae params + # stay in the autograd graph (zeros_like creates detached tensors). + preds_vision_list[0] = preds_vision_list[0] + 0.0 * preds_vision.sum() + else: + preds_vision_list = [preds_vision] + output_dict.update(preds_vision=preds_vision_list) + else: + assert vision is not None # Type narrowing + assert isinstance(vision.mse_loss_indexes, torch.Tensor) + assert vision.noisy_frame_indexes is not None + preds_vision = self.llm2vae( + last_hidden_state[vision.mse_loss_indexes] + ) # [total_noisy_vision_patches,patch_latent_dim] + preds_vision = self.unpatchify_and_unpack_latents( + preds_vision, + token_shapes_vision=vision.token_shapes, + noisy_frame_indexes_vision=vision.noisy_frame_indexes, + original_latent_shapes=original_latent_shapes, + ) + output_dict.update(preds_vision=preds_vision) + + def _encode_action( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps_action: Optional[torch.Tensor] = None, + ) -> None: + """Encode action tokens and fill into packed_sequence.""" + if packed_seq.action is None or packed_seq.action.tokens is None: + # No action tokens in this batch + return + + action: ModalityData = packed_seq.action + assert action.token_shapes is not None + assert isinstance(action.sequence_indexes, torch.Tensor) + assert isinstance(action.timesteps, torch.Tensor) + assert isinstance(action.mse_loss_indexes, torch.Tensor) + + # Pack variable-length action tokens into a 1D sequence (same pattern as pack_sound_latents) + packed_tokens_action, per_token_domain_id = self.pack_action( + action.tokens, action.token_shapes, action.domain_id + ) + packed_tokens_action = self.action2llm(packed_tokens_action, per_token_domain_id) + + # Add additive position embedding only if not using unified_3d_mrope + if self.action_pos_embed is not None: + # VideoRopePosition3DEmb expects shapes as (t, h, w). For actions we use a 1x1 spatial grid. + action_shapes_3d = [(ts[0], 1, 1) for ts in action.token_shapes] + action_token_pos_emb = self.action_pos_embed( + action_shapes_3d, + fps=fps_action, + start_frame_offset=1, + ).to(target_dtype) # [B_action*T_action,hidden_size] + packed_tokens_action = packed_tokens_action + action_token_pos_emb # [B_action*T_action,hidden_size] + + packed_tokens_action = packed_tokens_action + self.action_modality_embed.view( + 1, -1 + ) # [B_action*T_action,hidden_size] + + has_noisy_actions = action.mse_loss_indexes.numel() > 0 + if has_noisy_actions: + timesteps_action = action.timesteps * self.timestep_scale # [N_noisy_frames_action] + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_action = self.time_embedder( + timesteps_action + ) # [N_noisy_frames_action,hidden_size] + packed_timestep_embeds_action = packed_timestep_embeds_action.to( + target_dtype + ) # [N_noisy_frames_action,hidden_size] + + packed_tokens_action = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_action, + packed_timestep_embeds=packed_timestep_embeds_action, + noisy_frame_indexes=action.noisy_frame_indexes, + token_shapes=action.token_shapes, + ) # [B_action*T_action,hidden_size] + + packed_sequence[action.sequence_indexes] = ( + packed_tokens_action # [B_action*T_action,hidden_size] scattered into [N_total,hidden_size] + ) + + def _decode_action( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + ) -> None: + """Decode action tokens from hidden states and update output_dict.""" + action = packed_seq.action + # Check if no action or no noisy action tokens + has_noisy_action = ( + action is not None + and action.tokens is not None + and isinstance(action.mse_loss_indexes, torch.Tensor) + and action.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_action: + # dummy forward to maintain computation graph consistency across ranks + preds_action = torch.zeros( + [1, self.action_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,action_dim] + dummy_domain_id = torch.zeros([1], device=last_hidden_state.device, dtype=torch.long) # [1] + preds_action = self.action2llm(preds_action, dummy_domain_id) + self.action_modality_embed.view( + 1, -1 + ) # [1,hidden_size] + preds_action = self.llm2action(preds_action, dummy_domain_id) # [1,action_dim] + # Return a list of per-sample zero tensors with correct shapes (e.g. (T, action_dim)), + # so downstream code (_get_velocity, _compute_flow_matching_loss) that iterates over preds_action + # gets properly-shaped tensors. Without this, the dummy tensor (1, action_dim) + # would cause a size mismatch when concatenating vision+action velocities. + if action is not None and action.tokens is not None: + preds_action_list = [torch.zeros_like(tok) for tok in action.tokens] + # Inject dummy forward's computation graph so DomainAwareLinear params + # stay in the autograd graph (zeros_like creates detached tensors). + preds_action_list[0] = preds_action_list[0] + 0.0 * preds_action.sum() + # When action is None (no action in batch), fall back to [preds_action] purely for + # gradient graph consistency — it won't be iterated over. + else: + preds_action_list = [preds_action] + output_dict.update(preds_action=preds_action_list) + else: + assert action is not None # Type narrowing + assert isinstance(action.mse_loss_indexes, torch.Tensor) + assert action.condition_mask is not None + assert len(action.domain_id) > 0 + + action_hidden_states = last_hidden_state[action.mse_loss_indexes] # [total_noisy_action_tokens,hidden_size] + + # Build per-token domain IDs for the noisy tokens (same expansion logic as pack_action) + domain_ids: list[torch.Tensor] = [] + for nfi, d_id in zip(action.noisy_frame_indexes, action.domain_id): + domain_ids.append(d_id.expand(len(nfi))) + per_token_domain_id = torch.cat(domain_ids, dim=0) + + preds_action = self.llm2action( + action_hidden_states, per_token_domain_id + ) # [total_noisy_action_tokens,action_dim] + preds_action = self.unpack_action(preds_action, action.token_shapes, action.noisy_frame_indexes) + output_dict.update(preds_action=preds_action) + + def _encode_sound( + self, + packed_seq: PackedSequence, + packed_sequence: torch.Tensor, + target_dtype: torch.dtype, + fps_sound: Optional[torch.Tensor] = None, + ) -> None: + """Encode sound tokens and fill into packed_sequence. + + Args: + packed_seq: PackedSequence containing sound tokens and metadata. + packed_sequence: The packed sequence tensor to fill sound embeddings into (modified in-place). + target_dtype: Target dtype for embeddings (typically from text embedding). + fps_sound: FPS tensor for RoPE modulation. Should be the sound latent rate (e.g., 25 Hz). + """ + if packed_seq.sound is None or packed_seq.sound.tokens is None: + # No sound tokens in this batch + return + + sound = packed_seq.sound + assert sound.token_shapes is not None + assert isinstance(sound.sequence_indexes, torch.Tensor) + assert isinstance(sound.timesteps, torch.Tensor) + assert isinstance(sound.mse_loss_indexes, torch.Tensor) + + # Pack sound latents: list of [C, T] tensors → [total_tokens, C] + packed_tokens_sound = self.pack_sound_latents( + sound.tokens, sound.token_shapes + ) # [total_sound_tokens,sound_dim] + packed_tokens_sound = packed_tokens_sound.to(target_dtype) # [total_sound_tokens,sound_dim] + + # Project sound tokens + modality embedding + + # No additive position embedding is used (unlike legacy video which keeps one for backward compat). + packed_tokens_sound = ( + self.sound2llm(packed_tokens_sound) + self.sound_modality_embed + ) # [total_sound_tokens,hidden_size] + + has_noisy_sound = sound.mse_loss_indexes.numel() > 0 + if has_noisy_sound: + timesteps_sound = sound.timesteps * self.timestep_scale # [N_noisy_frames_sound] + with torch.autocast("cuda", enabled=True, dtype=torch.float32): + packed_timestep_embeds_sound = self.time_embedder(timesteps_sound) # [N_noisy_frames_sound,hidden_size] + packed_timestep_embeds_sound = packed_timestep_embeds_sound.to( + target_dtype + ) # [N_noisy_frames_sound,hidden_size] + + packed_tokens_sound = _apply_timestep_embeds_to_noisy_tokens( + packed_tokens=packed_tokens_sound, + packed_timestep_embeds=packed_timestep_embeds_sound, + noisy_frame_indexes=sound.noisy_frame_indexes, + token_shapes=sound.token_shapes, + ) # [total_sound_tokens,hidden_size] + + packed_sequence[sound.sequence_indexes] = ( + packed_tokens_sound # [total_sound_tokens,hidden_size] scattered into [N_total,hidden_size] + ) + + def _decode_sound( + self, + packed_seq: PackedSequence, + last_hidden_state: torch.Tensor, + output_dict: dict, + ) -> None: + """Decode sound tokens from hidden states and update output_dict. + + Args: + packed_seq: PackedSequence containing sound modality data. + last_hidden_state: Hidden states from the transformer. + output_dict: Output dictionary to update with preds_sound (modified in-place). + """ + sound = packed_seq.sound + # Check if no sound or no noisy sound tokens + has_noisy_sound = ( + sound is not None + and sound.tokens is not None + and isinstance(sound.mse_loss_indexes, torch.Tensor) + and sound.mse_loss_indexes.numel() > 0 + ) + if not has_noisy_sound: + # dummy forward to maintain computation graph consistency across ranks + preds_sound = torch.zeros( + [1, self.sound_dim], device=last_hidden_state.device, dtype=last_hidden_state.dtype + ) # [1,sound_dim] + preds_sound = self.sound2llm(preds_sound) + self.sound_modality_embed # [1,hidden_size] + preds_sound = self.llm2sound(preds_sound) # [1,sound_dim] + if sound is not None and sound.tokens is not None: + preds_sound_list = [torch.zeros_like(tok) for tok in sound.tokens] + preds_sound_list[0] = preds_sound_list[0] + 0.0 * preds_sound.sum() + else: + preds_sound_list = [preds_sound] + output_dict.update(preds_sound=preds_sound_list) + else: + assert sound is not None # Type narrowing + assert isinstance(sound.mse_loss_indexes, torch.Tensor) + assert sound.condition_mask is not None + preds_sound = self.llm2sound( + last_hidden_state[sound.mse_loss_indexes] + ) # [total_noisy_sound_tokens,sound_dim] + preds_sound = self.unpack_sound_latents( + preds_sound, sound.token_shapes, sound.noisy_frame_indexes + ) # list of [C,T] per sample + output_dict.update(preds_sound=preds_sound) + + def forward( + self, + packed_seq: PackedSequence, + fps_vision: Optional[torch.Tensor] = None, + fps_action: Optional[torch.Tensor] = None, + fps_sound: Optional[torch.Tensor] = None, + memory: MemoryState | None = None, + ) -> dict: + """ + Forward pass for Cosmos3VFMNetwork. + + Args: + packed_seq: PackedSequence containing all packed tensors and metadata. + See PackedSequence dataclass for field details. + fps_vision: Optional FPS tensor for vision RoPE modulation. + fps_action: Optional FPS tensor for action RoPE modulation. + fps_sound: Optional FPS tensor for sound RoPE modulation (e.g., sound_latent_fps=25). + memory: Optional MemoryState for persistent KV-cache memory + (AR inference or rolling-KV-cache training). Built by + ``OmniMoTModel.build_memory_state()``. + + Returns: + dict with keys: + - "preds_vision": list[Tensor[C,T,H,W]], one per sample. + - "preds_action": Velocity predictions for action tokens (if action_gen). + - "preds_sound": Velocity predictions for sound tokens (if sound_gen). + - "last_hidden_state": Last hidden state from the transformer. + - "lbl_metadata_*": Load balancing metadata. + - "ce_preds": Cross-entropy predictions (if predict_text_tokens is True). + """ + # Note: During inference with @torch.no_grad(), model may be in training mode + # This is intentional for proper batch norm / dropout behavior + # assert self.training, "Cosmos3VFMNetwork only supports training mode" + + packed_sequence, target_dtype = self._encode_text(packed_seq) # packed_sequence: [N_total,hidden_size] + + # encode vision tokens + original_latent_shapes: List[Tuple[int, int, int]] | None = None + if self.config.vision_gen: + original_latent_shapes = self._encode_vision(packed_seq, packed_sequence, target_dtype, fps_vision) + + # encode action tokens + if self.config.action_gen: + self._encode_action(packed_seq, packed_sequence, target_dtype, fps_action) + + # encode sound tokens + if self.config.sound_gen: + self._encode_sound(packed_seq, packed_sequence, target_dtype, fps_sound) + + assert self.use_moe + assert packed_seq.attn_modes is not None + assert packed_seq.split_lens is not None + + # Get all generation sequence indexes for MoE routing + # IMPORTANT: Include ALL latent tokens (video + action + sound), not just generation targets. + # Condition tokens still need to be routed to diffusion experts; they are excluded from + # LOSS computation, not from routing. + all_gen_indexes = [] + if packed_seq.vision is not None: + assert packed_seq.vision.token_shapes is not None + assert isinstance(packed_seq.vision.sequence_indexes, torch.Tensor) + all_gen_indexes.append(packed_seq.vision.sequence_indexes) + if packed_seq.action is not None and isinstance(packed_seq.action.sequence_indexes, torch.Tensor): + all_gen_indexes.append(packed_seq.action.sequence_indexes) + if packed_seq.sound is not None and isinstance(packed_seq.sound.sequence_indexes, torch.Tensor): + all_gen_indexes.append(packed_seq.sound.sequence_indexes) + vision_sequence_indexes = torch.cat(all_gen_indexes, dim=0) if all_gen_indexes else None # [N_gen_tokens] + + # When temporal causal is enabled the buffer is [action_t0, vision_t0, action_t1, vision_t1, ...]. + # After torch.cat([vision_indexes, action_indexes]) the interleaved order is lost; sorting restores it. + if self.video_temporal_causal: + assert packed_seq.sound is None, "Sound generation is not supported with video_temporal_causal=True." + if vision_sequence_indexes is not None: + vision_sequence_indexes = vision_sequence_indexes.sort().values # [N_gen_tokens] + + vision_token_shapes = packed_seq.vision.token_shapes if packed_seq.vision else None + + # The packer is the single source of truth for the supertoken layout. + # ``num_action_tokens_per_supertoken`` is stamped onto ``packed_seq`` by + # ``_pack_supertokens_temporal_causal`` (= tcf when actions are packed + # inline, 0 otherwise) and read unchanged by the attention builder, the + # NATTEN metadata generator, and the rolling KV-cache state — keeping + # all downstream supertoken geometry automatically in sync with the pack. + num_action_tokens_per_supertoken = packed_seq.num_action_tokens_per_supertoken + + input_pack, attention_meta, natten_metadata_list = build_packed_sequence( + self.config.joint_attn_implementation, + packed_sequence=packed_sequence, + attn_modes=packed_seq.attn_modes, + split_lens=packed_seq.split_lens, + sample_lens=packed_seq.sample_lens, + packed_und_token_indexes=packed_seq.text_indexes, + packed_gen_token_indexes=vision_sequence_indexes, + num_heads=self.num_heads, + is_image_batch=packed_seq.is_image_batch, + head_dim=self.head_dim, + num_layers=self.num_hidden_layers, + token_shapes=packed_seq.vision.token_shapes, + natten_parameter_list=self.natten_parameter_list, + cp_world_size=self.parallel_dims.cp_size if self.parallel_dims else 1, + video_temporal_causal=self.video_temporal_causal, + use_rolling_kv_cache=memory is not None and memory.uses_rolling_kv_cache, + vision_token_shapes=vision_token_shapes, + action_token_shapes=packed_seq.action.token_shapes if packed_seq.action else None, + num_action_tokens_per_supertoken=num_action_tokens_per_supertoken, + null_action_supertokens=packed_seq.null_action_supertokens, + pad_for_cuda_graphs=self.pad_for_cuda_graphs, + ) + + input_pack, packed_position_ids = get_context_parallel_sharded_sequence( + attn_implementation=self.config.joint_attn_implementation, + input_pack=input_pack, + position_ids=packed_seq.position_ids, + parallel_dims=self.parallel_dims, + ) + + packed_outputs, lbl_metadata = self.language_model( + input_pack, + attention_mask=attention_meta, + position_ids=packed_position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + last_hidden_state = get_context_parallel_last_hidden_state( + packed_outputs=packed_outputs, + parallel_dims=self.parallel_dims, + ) # [N_total,hidden_size] + output_dict = dict() + + # decode vision tokens + if self.config.vision_gen: + self._decode_vision(packed_seq, last_hidden_state, output_dict, original_latent_shapes) + + # decode action tokens + if self.config.action_gen: + self._decode_action(packed_seq, last_hidden_state, output_dict) + + # decode sound tokens + if self.config.sound_gen: + self._decode_sound(packed_seq, last_hidden_state, output_dict) + + output_dict.update(last_hidden_state=last_hidden_state) + for lbl_metadata_key, lbl_metadata_value in lbl_metadata.items(): + output_dict.update({f"lbl_metadata_{lbl_metadata_key}": lbl_metadata_value}) + if self.predict_text_tokens: + packed_ce_preds = self.language_model.lm_head( + last_hidden_state[packed_seq.ce_loss_indexes] + ) # [N_ce_tokens,vocab_size] + output_dict["ce_preds"] = packed_ce_preds + + return output_dict + + +def _apply_timestep_embeds_to_noisy_tokens( + packed_tokens: torch.Tensor, + packed_timestep_embeds: torch.Tensor, + noisy_frame_indexes: List[torch.Tensor], + token_shapes: list[tuple[int, ...]], +) -> torch.Tensor: + """Apply timestep embeddings to noisy tokens. + Tn is the number of noisy frames for a given sample. + Tc is the number of clean frames for a given sample. + T is the total number of frames for a given sample. + T = Tn + Tc + + Args: + packed_tokens: The packed tokens to apply timestep embeddings to. + packed_timestep_embeds: The packed timestep embeddings to apply. + noisy_frame_indexes: The frame indices to apply timestep embeddings to + (list of tensors, each with shape (Tn,)). + token_shapes: The token shapes for each sample. Each entry is a tuple + shaped like ``(T, ...)`` where trailing dimensions represent the spatial grid. + + Returns: + The packed tokens with timestep embeddings applied to the noisy tokens. + """ + + # Handle variable token shapes by processing each sample's noisy_frame_indexes individually. + # The noisy indices are first expanded to cover the entire spatial grid of each frame. + # + # For video frames, the spatial grid is (H, W). + # For action frames, the spatial grid is (). + # For sound frames, the spatial grid is (1, 1). + # + # The noisy indices are then flattened into a single tensor overall. When flattening, + # we must ensure that the noisy indices from each sample are unique by adding the + # cumulative sum of the token shapes of previous samples to the noisy indices for + # a given sample. + start_noisy_index = 0 + flattened_noisy_frame_indexes = [] + + for noisy_indexes_i, token_shape_i in zip(noisy_frame_indexes, token_shapes): + assert noisy_indexes_i.numel() <= token_shape_i[0] + spatial_numel_i = math.prod(token_shape_i[1:]) + spatial_indexes_i = torch.arange(spatial_numel_i, device=packed_tokens.device) # [spatial_numel_i] + noisy_indexes_i = ( + (noisy_indexes_i * spatial_numel_i).unsqueeze(-1).expand(-1, spatial_numel_i) + ) # [Tn_i,spatial_numel_i] + noisy_indexes_i = noisy_indexes_i.clone() + spatial_indexes_i + start_noisy_index # [Tn_i,spatial_numel_i] + flattened_noisy_frame_indexes.append(noisy_indexes_i.flatten()) # [Tn_i*spatial_numel_i] + start_noisy_index += math.prod(token_shape_i) + + flattened_noisy_frame_indexes = torch.cat(flattened_noisy_frame_indexes, dim=0) # [total_noisy_patches] + + assert packed_tokens.dim() == 2 + assert packed_timestep_embeds.dim() == 2 + assert packed_timestep_embeds.shape[1] == packed_tokens.shape[1] + assert packed_timestep_embeds.shape[0] <= packed_tokens.shape[0] + assert flattened_noisy_frame_indexes.dim() == 1 + assert flattened_noisy_frame_indexes.shape[0] == packed_timestep_embeds.shape[0] + + flattened_noisy_frame_indexes = flattened_noisy_frame_indexes.unsqueeze(-1).expand( + -1, + packed_tokens.shape[1], + ) # [total_noisy_patches,hidden_size] + + return packed_tokens.scatter_add( + dim=0, + index=flattened_noisy_frame_indexes, + src=packed_timestep_embeds, + ) # [total_tokens,hidden_size] diff --git a/cosmos_training/cosmos/model/vfm/mot/domain_aware_linear.py b/cosmos_training/cosmos/model/vfm/mot/domain_aware_linear.py new file mode 100644 index 00000000..d4f86c27 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/domain_aware_linear.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Domain-aware linear layer for multi-embodiment robot learning. + +This module provides a linear layer with domain-conditioned parameters, +where each domain (embodiment) has its own weight and bias vectors. + +Based on the X-VLA implementation: +https://github.com/2toinf/X-VLA/blob/main/models/transformer.py +""" + +import torch +from torch import nn + + +class DomainAwareLinear(nn.Module): + """Linear layer with domain-conditioned parameters (per-sample). + + Each domain has its own weight and bias vectors, stored in embeddings. + During forward pass, weights are retrieved based on per-sample domain IDs. + + This enables learning domain-specific transformations for different robot + embodiments while sharing the overall model architecture. + """ + + def __init__(self, input_size: int, output_size: int, num_domains: int = 50) -> None: + """Initialize the domain-aware linear layer. + + Args: + input_size: Dimension of input features. + output_size: Dimension of output features. + num_domains: Number of domains (embodiments) to support. + """ + super().__init__() + self.input_size = input_size + self.output_size = output_size + self.num_domains = num_domains + + # Store per-domain weights as embeddings: [num_domains, output_size * input_size] + self.fc = nn.Embedding(num_domains, output_size * input_size) + # Store per-domain biases as embeddings: [num_domains, output_size] + self.bias = nn.Embedding(num_domains, output_size) + + # Initialize weights + nn.init.xavier_uniform_(self.fc.weight) + nn.init.zeros_(self.bias.weight) + + def forward(self, x: torch.Tensor, domain_id: torch.LongTensor) -> torch.Tensor: + """Forward pass with domain-specific weights. + + Args: + x: Input tensor of shape [B, I] or [B, T, I] where B is batch size, + T is sequence length, and I is input_size. + domain_id: Domain indices of shape [B], one per sample in the batch. + + Returns: + Output tensor of shape [B, O] or [B, T, O] where O is output_size. + """ + B = domain_id.shape[0] + + # Retrieve per-sample weights: [B, input_size, output_size] + W = self.fc(domain_id).view(B, self.input_size, self.output_size) # [B,input_size,output_size] + + # Retrieve per-sample biases: [B, output_size] + b = self.bias(domain_id).view(B, self.output_size) # [B,output_size] + + if x.dim() == 2: + # 2D input: [B, I] @ [B, I, O] -> [B, O] + return ( + torch.bmm(x.unsqueeze(1), W).squeeze(1) + b + ) # [B,1,input_size] @ [B,input_size,output_size] -> [B,output_size] + else: + # 3D input: [B, T, I] @ [B, I, O] -> [B, T, O] + # Bias [B, O] -> [B, 1, O] for broadcasting + return torch.bmm(x, W) + b.unsqueeze( + 1 + ) # [B,T,input_size] @ [B,input_size,output_size] -> [B,T,output_size] diff --git a/cosmos_training/cosmos/model/vfm/mot/modeling_utils.py b/cosmos_training/cosmos/model/vfm/mot/modeling_utils.py new file mode 100644 index 00000000..23f2239c --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/modeling_utils.py @@ -0,0 +1,407 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Optional + +import numpy as np +import torch +from einops import rearrange, repeat +from torch import nn +from torch.distributed import ProcessGroup +from transformers.activations import ACT2FN + +from cosmos.data.vfm.sequence_packing import ModalityData + + +def has_noisy_tokens(modality_data: ModalityData | None) -> bool: + """Check if a modality has valid noisy tokens for loss computation.""" + return ( + modality_data is not None + and modality_data.tokens is not None + and isinstance(modality_data.mse_loss_indexes, torch.Tensor) + and modality_data.mse_loss_indexes.numel() > 0 + ) + + +# -------------------------------------------------------- +# 2D sine-cosine position embedding (flattened) +# References: +# DiT: https://github.com/facebookresearch/DiT/blob/main/models.py +# -------------------------------------------------------- +def get_2d_sincos_pos_embed( + embed_dim: int, grid_size_h: int, grid_size_w: int, cls_token: bool = False, extra_tokens: int = 0 +) -> np.ndarray: + grid_h = np.arange(grid_size_h, dtype=np.float32) + grid_w = np.arange(grid_size_w, dtype=np.float32) + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + + grid = grid.reshape([2, 1, grid_size_h, grid_size_w]) + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token and extra_tokens > 0: + pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0) + return pos_embed + + +def get_2d_sincos_pos_embed_from_grid(embed_dim: int, grid: np.ndarray) -> np.ndarray: + assert embed_dim % 2 == 0 + + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # [H*W,D/2] + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # [H*W,D/2] + + emb = np.concatenate([emb_h, emb_w], axis=1) # [H*W,D] + return emb + + +def get_1d_sincos_pos_embed_from_grid(embed_dim: int, pos: np.ndarray) -> np.ndarray: + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size [M] + out: [M,D] + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=np.float64) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # [D/2] + + pos = pos.reshape(-1) # [M] + out = np.einsum("m,d->md", pos, omega) # [M,D/2], outer product + + emb_sin = np.sin(out) # [M,D/2] + emb_cos = np.cos(out) # [M,D/2] + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # [M,D] + return emb + + +class FlattenedSinCosPositionEmbedding(nn.Module): + # This module creates a flattened sin-cos position embedding for a given number of patches per side. + # Indices are created for 2D array and flattened into 1D array. + + def __init__(self, max_latent_h: int, max_latent_w: int, hidden_size: int, interpolate_pos: bool = False): + super().__init__() + self.max_latent_h = max_latent_h + self.max_latent_w = max_latent_w + self.hidden_size = hidden_size + self.interpolate_pos = interpolate_pos + self.pos_embed = nn.Parameter(torch.zeros(max_latent_h * max_latent_w, hidden_size), requires_grad=False) + self._init_weights() + + def _get_flattened_position_ids_extrapolate(self, latent_dim_h: int, latent_dim_w: int) -> torch.Tensor: + coords_h = torch.arange(0, latent_dim_h) # [H] + coords_w = torch.arange(0, latent_dim_w) # [W] + pos_ids = (coords_h[:, None] * self.max_latent_w + coords_w).flatten() # [H*W] + return pos_ids + + def _get_flattened_position_ids_interpolate(self, latent_dim_h: int, latent_dim_w: int) -> torch.Tensor: + boundaries = torch.arange(1 / self.max_latent_w, 1.0, 1 / self.max_latent_w) # [max_latent_w-1] + fractional_coords_h = torch.arange(0, 1 - 1e-6, 1 / latent_dim_h) # [H] + fractional_coords_w = torch.arange(0, 1 - 1e-6, 1 / latent_dim_w) # [W] + bucket_coords_h = torch.bucketize(fractional_coords_h, boundaries, right=True) # [H] + bucket_coords_w = torch.bucketize(fractional_coords_w, boundaries, right=True) # [W] + pos_ids = (bucket_coords_h[:, None] * self.max_latent_w + bucket_coords_w).flatten() # [H*W] + return pos_ids + + def _create_flattened_position_ids_packed(self, token_shapes_vision: list[tuple[int, int]]) -> torch.Tensor: + flattened_position_ids = [] + for t, h, w in token_shapes_vision: + if self.interpolate_pos: + flattened_position_ids.append(self._get_flattened_position_ids_interpolate(h, w)) # [H*W] + else: + flattened_position_ids.append(self._get_flattened_position_ids_extrapolate(h, w)) # [H*W] + flattened_position_ids_packed = torch.cat(flattened_position_ids, dim=0) # [N_vision] + return flattened_position_ids_packed + + def _init_weights(self): + # Initialize (and freeze) pos_embed by sin-cos embedding: + pos_embed = get_2d_sincos_pos_embed( + embed_dim=self.hidden_size, grid_size_h=self.max_latent_h, grid_size_w=self.max_latent_w + ) + self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float()) + + def forward(self, token_shapes_vision: list[tuple[int, int]], fps: Optional[torch.Tensor] = None) -> torch.Tensor: + # First create 2D index array + flattened_position_ids_packed = self._create_flattened_position_ids_packed(token_shapes_vision) # [N_vision] + return self.pos_embed[flattened_position_ids_packed] # [N_vision,hidden_size] + + +# -------------------------------------------------------- +# 2D / 3D RoPE Position Embedding +# -------------------------------------------------------- + + +class VideoRopePosition3DEmb(nn.Module): + def __init__( + self, + *, # enforce keyword arguments + head_dim: int, + len_h: int, + len_w: int, + len_t: int, + base_fps: int = 24, + base_temporal_compression_factor: int = 4, + temporal_compression_factor: int = 4, + h_extrapolation_ratio: float = 1.0, + w_extrapolation_ratio: float = 1.0, + t_extrapolation_ratio: float = 1.0, + enable_fps_modulation: bool = False, + **kwargs, # used for compatibility with other positional embeddings; unused in this class + ): + del kwargs + super().__init__() + self.base_tps = base_fps / base_temporal_compression_factor + self.temporal_compression_factor = temporal_compression_factor + self.max_h = len_h + self.max_w = len_w + self.max_t = len_t + self.enable_fps_modulation = enable_fps_modulation + dim = head_dim + dim_h = dim // 6 * 2 + dim_w = dim_h + dim_t = dim - 2 * dim_h + assert dim == dim_h + dim_w + dim_t, f"bad dim: {dim} != {dim_h} + {dim_w} + {dim_t}" + + self.register_buffer( + "dim_spatial_range", + torch.arange(0, dim_h, 2)[: (dim_h // 2)].float() / dim_h, + persistent=True, + ) + self.register_buffer( + "dim_temporal_range", + torch.arange(0, dim_t, 2)[: (dim_t // 2)].float() / dim_t, + persistent=True, + ) + self._dim_h = dim_h + self._dim_t = dim_t + + self.h_ntk_factor = h_extrapolation_ratio ** (dim_h / (dim_h - 2)) + self.w_ntk_factor = w_extrapolation_ratio ** (dim_w / (dim_w - 2)) + self.t_ntk_factor = t_extrapolation_ratio ** (dim_t / (dim_t - 2)) + self._init_weights() + + def _init_weights(self) -> None: + dim_h = self._dim_h + dim_t = self._dim_t + + self.dim_spatial_range = ( + torch.arange(0, dim_h, 2)[: (dim_h // 2)].float().to(self.dim_spatial_range.device) / dim_h + ) + self.dim_temporal_range = ( + torch.arange(0, dim_t, 2)[: (dim_t // 2)].float().to(self.dim_spatial_range.device) / dim_t + ) + + def enable_context_parallel(self, process_group: ProcessGroup): + pass + + def disable_context_parallel(self): + pass + + def generate_embeddings( + self, + latent_shape: torch.Size, + input_fps: Optional[torch.Tensor] = None, + h_ntk_factor: Optional[float] = None, + w_ntk_factor: Optional[float] = None, + t_ntk_factor: Optional[float] = None, + start_frame_offset: int = 0, + ): + """ + Generate embeddings for the given input size. + + Args: + latent_shape (torch.Size): Input tensor size (Batch, Time, Height, Width). + input_fps (Optional[torch.Tensor], optional): Frames per second. Defaults to None. + h_ntk_factor (Optional[float], optional): Height NTK factor. If None, uses self.h_ntk_factor. + w_ntk_factor (Optional[float], optional): Width NTK factor. If None, uses self.w_ntk_factor. + t_ntk_factor (Optional[float], optional): Time NTK factor. If None, uses self.t_ntk_factor. + start_frame_offset (int, optional): Offset for frame indices. Use 1 for action embeddings + so that action frame indices start at 1 instead of 0. Defaults to 0. + + Returns: + Not specified in the original code snippet. + """ + if input_fps is not None: + tps = input_fps / self.temporal_compression_factor + else: + tps = None + + h_ntk_factor = h_ntk_factor if h_ntk_factor is not None else self.h_ntk_factor + w_ntk_factor = w_ntk_factor if w_ntk_factor is not None else self.w_ntk_factor + t_ntk_factor = t_ntk_factor if t_ntk_factor is not None else self.t_ntk_factor + assert h_ntk_factor is not None and w_ntk_factor is not None and t_ntk_factor is not None + + h_theta = 10000.0 * h_ntk_factor + w_theta = 10000.0 * w_ntk_factor + t_theta = 10000.0 * t_ntk_factor + + h_spatial_freqs = 1.0 / (h_theta ** self.dim_spatial_range.float()) # [dim_h/2] + w_spatial_freqs = 1.0 / (w_theta ** self.dim_spatial_range.float()) # [dim_w/2] + temporal_freqs = 1.0 / (t_theta ** self.dim_temporal_range.float()) # [dim_t/2] + + B, T, H, W = latent_shape + assert H <= self.max_h and W <= self.max_w, ( + f"Input dimensions (H={H}, W={W}) exceed the maximum dimensions (max_h={self.max_h}, max_w={self.max_w})" + ) + + # Re-allocate buffer if current video needs more indices than what we have for self.seq + # Only rellocate when needed. + max_needed = max(T, H, W) + seq = torch.arange(max_needed, device=self.dim_spatial_range.device, dtype=torch.float) + + half_emb_h = torch.outer(seq[:H], h_spatial_freqs) # [H,dim_h/2] + half_emb_w = torch.outer(seq[:W], w_spatial_freqs) # [W,dim_w/2] + + # Frame indices for the embedding (always 0, 1, 2, ...) + frame_indices = seq[:T] # [T] + + if self.enable_fps_modulation: + uniform_tps = tps is None or tps.shape == (1,) + assert uniform_tps or B == 1 or T == 1, ( + "For video batch, B should be 1 for non-uniform fps. For image batch, T should be 1." + ) + + # apply sequence scaling in temporal dimension + if tps is None: # image case + assert T == 1, "T should be 1 for image batch." + half_emb_t = torch.outer(frame_indices, temporal_freqs) # [T,dim_t/2] + else: + # Calculate scaled time indices + # Apply start_frame_offset to the time calculation (not frame indices) + # This allows one to manipulate the start frame index of embeddings for cross-modality alignment. + scaled_time = (frame_indices + start_frame_offset) / tps[:1] * self.base_tps # [T] + half_emb_t = torch.outer(scaled_time, temporal_freqs) # [T,dim_t/2] + else: + half_emb_t = torch.outer(frame_indices, temporal_freqs) # [T,dim_t/2] + + rope_embed = torch.cat( + [ + repeat(half_emb_t, "t d -> t h w d", h=H, w=W), # [T,H,W,dim_t/2] + repeat(half_emb_h, "h d -> t h w d", t=T, w=W), # [T,H,W,dim_h/2] + repeat(half_emb_w, "w d -> t h w d", t=T, h=H), # [T,H,W,dim_w/2] + ] + * 2, + dim=-1, + ) # [T,H,W,head_dim] + + return rearrange(rope_embed, "t h w d -> (t h w) d").float() # [T*H*W,head_dim] + + def forward( + self, + token_shapes_vision: list[tuple[int, int, int]], + fps: Optional[torch.Tensor] = None, + start_frame_offset: int = 0, + ) -> torch.Tensor: + """ + With CP, the function assume that the input tensor is already split. + It delegates the embedding generation to generate_embeddings function. + + Args: + token_shapes_vision: List of (t, h, w) tuples for each latent. + fps: Frames per second tensor. + start_frame_offset: Offset for frame indices. Use 1 for action embeddings + so that action frame indices start at 1 instead of 0. Defaults to 0. + """ + + embeddings_packed = [] + for i, latent_shape in enumerate(token_shapes_vision): + # latent_shape: (t, h, w) + shape = (1, latent_shape[0], latent_shape[1], latent_shape[2]) + + # Extract FPS for this specific video + video_fps = None + if fps is not None: + assert i < fps.shape[0], f"Index {i} out of bounds for fps tensor of shape {fps.shape}" + video_fps = fps[i : i + 1] + + embeddings = self.generate_embeddings(shape, input_fps=video_fps, start_frame_offset=start_frame_offset) + embeddings_packed.append(embeddings) + + embeddings_packed = torch.cat(embeddings_packed, dim=0) # [N_vision,head_dim] + return embeddings_packed + + @property + def seq_dim(self): + return 0 + + +# -------------------------------------------------------- +# TimestepEmbedder +# Reference: +# DiT: https://github.com/facebookresearch/DiT/blob/main/models.py +# -------------------------------------------------------- +class TimestepEmbedder(nn.Module): + """ + Embeds scalar timesteps into vector representations. + """ + + def __init__(self, hidden_size, frequency_embedding_size=256): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True), + ) + self.frequency_embedding_size = frequency_embedding_size + self.hidden_size = hidden_size + + def _init_weights(self): + std = 1.0 / math.sqrt(self.frequency_embedding_size) + torch.nn.init.trunc_normal_(self.mlp[0].weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.mlp[0].bias) + + std = 1.0 / math.sqrt(self.hidden_size) + torch.nn.init.trunc_normal_(self.mlp[2].weight, std=std, a=-3 * std, b=3 * std) + torch.nn.init.zeros_(self.mlp[2].bias) + + @staticmethod + def timestep_embedding(t, dim, max_period=10000): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + half = dim // 2 + freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to( + device=t.device + ) # [D/2] + args = t[:, None].float() * freqs[None] # [N,D/2] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) # [N,D] + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) # [N,D+1] + return embedding + + def forward(self, t): + t_freq = self.timestep_embedding(t, self.frequency_embedding_size) # [N,frequency_embedding_size] + t_emb = self.mlp(t_freq) # [N,hidden_size] + return t_emb + + +class MLPconnector(nn.Module): + def __init__(self, in_dim: int, out_dim: int, hidden_act: str): + super().__init__() + self.activation_fn = ACT2FN[hidden_act] + self.fc1 = nn.Linear(in_dim, out_dim) + self.fc2 = nn.Linear(out_dim, out_dim) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.fc1(hidden_states) # [N,out_dim] + hidden_states = self.activation_fn(hidden_states) # [N,out_dim] + hidden_states = self.fc2(hidden_states) # [N,out_dim] + return hidden_states diff --git a/cosmos_training/cosmos/model/vfm/mot/parallelize_unified_mot.py b/cosmos_training/cosmos/model/vfm/mot/parallelize_unified_mot.py new file mode 100644 index 00000000..191f825a --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/parallelize_unified_mot.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn as nn +from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + checkpoint_wrapper as ptd_checkpoint_wrapper, +) +from torch.distributed.fsdp import fully_shard + +from configs.base.defaults.model_config import ParallelismConfig +from cosmos.utils.vfm.parallelism import ParallelDims + + +def apply_ac(model: nn.Module): + """Apply activation checkpointing to the model.""" + + for layer_id, block in model.model.layers.named_children(): + block = ptd_checkpoint_wrapper(block, preserve_rng_state=True) + model.model.layers.register_module(layer_id, block) + + +def apply_compile(model: nn.Module, config: ParallelismConfig): + """ + Apply torch.compile to each TransformerBlock, which makes compilation efficient due to + repeated structure. Alternatively one can compile the whole model (after applying DP). + """ + compile_options = {} + if config.max_autotune_pointwise: + compile_options["max_autotune_pointwise"] = True + if config.coordinate_descent_tuning: + compile_options["coordinate_descent_tuning"] = True + + for layer_id, block in model.model.layers.named_children(): + block = torch.compile( + block, + fullgraph=True, + dynamic=config.compile_dynamic, + mode="reduce-overhead" if config.use_cuda_graphs else None, + options=compile_options or None, + ) + model.model.layers.register_module(layer_id, block) + + +def apply_fsdp( + model: nn.Module, + parallel_dims: ParallelDims, +): + """ + Apply data parallelism (via FSDP2) to the model. + + Args: + model (nn.Module): The model to apply data parallelism to. + parallel_dims (ParallelDims): The device mesh to use for data parallelism and expert parallel. + """ + for _, block in model.model.layers.named_children(): + fully_shard(block, mesh=parallel_dims.dp_mesh) + + +def parallelize_unified_mot( + model: nn.Module, + parallel_dims: ParallelDims | None, + config: ParallelismConfig, +) -> nn.Module: + """Optimize the model using FSDP, activation checkpointing, and torch.compile. + + FSDP reduces memory usage by sharding the model parameters across multiple GPUs. + Activation checkpointing reduces memory usage by selectively checkpointing only + the outputs of each layer. Torch.compile compiles the model for faster training. + """ + if config.use_activation_checkpointing: + apply_ac(model) + if config.use_torch_compile: + apply_compile(model, config) + if parallel_dims is not None and parallel_dims.dp_enabled: + apply_fsdp(model, parallel_dims) + return model diff --git a/cosmos_training/cosmos/model/vfm/mot/parallelize_vfm_network.py b/cosmos_training/cosmos/model/vfm/mot/parallelize_vfm_network.py new file mode 100644 index 00000000..179106ce --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/parallelize_vfm_network.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable + +import torch +from torch.distributed.fsdp import fully_shard +from torch.nn.attention.flex_attention import BlockMask + +from configs.base.defaults.model_config import ParallelismConfig +from cosmos.data.vfm.sequence_packing import ( + FactoredSequencePack, + JointSequencePack, +) +from cosmos.model.vfm.mot.attention import SplitInfo, dispatch_attention +from cosmos.model.vfm.mot.context_parallel_utils import context_parallel_attention +from cosmos.model.vfm.mot.parallelize_unified_mot import parallelize_unified_mot +from cosmos.model.vfm.utils.memory import KVToStore, MemoryValue +from cosmos.utils.vfm.parallelism import ParallelDims + + +class ContextParallelDispatch(torch.nn.Module): + """CP-aware wrapper for the installed attention dispatch function. + + Installed on ``PackedAttentionMoT.dispatch_attention_fn`` when context + parallelism is enabled, replacing whatever dispatch function was there + previously. The call signature of :meth:`forward` matches + ``dispatch_attention`` so the two are interchangeable. + + All paths delegate to :func:`context_parallel_attention`, which wraps + the inner ``wrapped_dispatch`` with Ulysses-style all-to-all + communication. This includes the AR frame 1+ gen-only path — the inner + dispatch routes to ``attention_AR_gen_only`` which operates on the + head-sharded tensors produced by the all-to-all. + + All cache writes flow through the ``MemoryState`` interface; neither this + class nor the CP attention functions write to the cache directly. + """ + + def __init__( + self, + cp_mesh, + wrapped_dispatch: Callable = dispatch_attention, + ): + super().__init__() + self.cp_mesh = cp_mesh + self.wrapped_dispatch = wrapped_dispatch + + def forward( + self, + packed_query_states: FactoredSequencePack | JointSequencePack, + packed_key_states: FactoredSequencePack | JointSequencePack, + packed_value_states: FactoredSequencePack | JointSequencePack, + attention_mask: BlockMask | SplitInfo, + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + ) -> tuple[FactoredSequencePack | JointSequencePack, KVToStore | None]: + if memory_value is not None and not memory_value.supports_context_parallel_attention: + raise ValueError("Context-parallel doesn't work when training with a KV-cache.") + + return context_parallel_attention( + self.cp_mesh, + packed_query_states, + packed_key_states, + packed_value_states, + attention_mask, + attention_function=self.wrapped_dispatch, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + +def apply_compile(model: torch.nn.Module, config: ParallelismConfig): + """Apply torch.compile to the VFM encode/decode heads. + + The MoT-side ``compile_dynamic`` knob on ``ParallelismConfig`` intentionally + does **not** propagate here. The VFM encode/decode paths have no graph + breaks and their input shapes are stable across a prompt, so we always + trace them as a single dynamic graph (``fullgraph=True, dynamic=True``). + This keeps AR inference (which sets ``compile_dynamic=False`` on MoT for + shape-specialized kernels) from accidentally regressing the VFM compile. + """ + + inductor_options = {} + if config.max_autotune_pointwise: + inductor_options["max_autotune_pointwise"] = True + if config.coordinate_descent_tuning: + inductor_options["coordinate_descent_tuning"] = True + + compile_options = { + "fullgraph": True, + "dynamic": True, + "mode": "reduce-overhead" if config.use_cuda_graphs else None, + "options": inductor_options or None, + } + + model._encode_text = torch.compile(model._encode_text, **compile_options) + model._encode_vision = torch.compile(model._encode_vision, **compile_options) + model._encode_action = torch.compile(model._encode_action, **compile_options) + model._decode_vision = torch.compile(model._decode_vision, **compile_options) + model._decode_action = torch.compile(model._decode_action, **compile_options) + return model + + +def context_parallel_unified_mot( + model: torch.nn.Module, + parallel_dims: ParallelDims | None, +) -> torch.nn.Module: + for i in range(len(model.model.layers)): + attn = model.model.layers[i].self_attn + cp_dispatch = ContextParallelDispatch( + parallel_dims.cp_mesh, + wrapped_dispatch=attn.dispatch_attention_fn, + ) + attn.dispatch_attention_fn = cp_dispatch + attn.cp_mesh = parallel_dims.cp_mesh + + return model + + +def parallelize_vfm_network( + model: torch.nn.Module, + parallel_dims: ParallelDims | None, + config: ParallelismConfig, +) -> torch.nn.Module: + """Optimize the model using FSDP, CP, activation checkpointing, and torch.compile. + + FSDP reduces memory usage by sharding the model parameters across multiple GPUs. + Activation checkpointing reduces memory usage by selectively checkpointing only + the outputs of each layer. Torch.compile compiles the model for faster training. + """ + if parallel_dims is not None and parallel_dims.cp_enabled: + model.parallel_dims = parallel_dims + model.language_model = context_parallel_unified_mot( + model.language_model, + parallel_dims=parallel_dims, + ) + + model.language_model = parallelize_unified_mot( + model.language_model, + parallel_dims=parallel_dims, + config=config, + ) + + if config.use_torch_compile and config.compiled_region == "all": + model = apply_compile(model, config) + + if parallel_dims is not None and parallel_dims.dp_enabled: + # Collect parameters to ignore during FSDP wrapping + ignored_params = set() + if model.latent_pos_embed is not None: + ignored_params.update(model.latent_pos_embed.parameters()) + + model = fully_shard( + module=model, + mesh=parallel_dims.dp_mesh, + ignored_params=ignored_params, + ) + + return model diff --git a/cosmos_training/cosmos/model/vfm/mot/unified_3dmrope_utils.py b/cosmos_training/cosmos/model/vfm/mot/unified_3dmrope_utils.py new file mode 100644 index 00000000..30f9cd96 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/unified_3dmrope_utils.py @@ -0,0 +1,206 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for generating 3D multi-modal RoPE (mRoPE) position IDs. + +3D mRoPE uses three axes (temporal, height, width) for position embedding, +following the Qwen3VL design for multi-modal RoPE: + +- **Text tokens**: All three axes share the same monotonically increasing position IDs. + For example: (0,0,0), (1,1,1), (2,2,2), ... +- **Vision tokens** (image/video latents): Creates a local 3D grid (T, H, W) with a + temporal offset. For each frame t in [0, T), for each row h in [0, H), for each + column w in [0, W), the position is (temporal_offset + t, h_offset, w_offset). + +The ``reset_spatial_indices`` flag controls spatial axis behavior: +- ``True`` (default): Spatial (H, W) indices start from 0 for each vision segment, + giving the model absolute spatial position within each image/video. +- ``False`` (Qwen2VL-style): All axes are offset by ``temporal_offset``. + +After each segment, the ``temporal_offset`` is updated to ``max(all_positions) + 1`` +(Qwen3VL design), ensuring subsequent segments start at a non-overlapping position. + +**FPS Modulation** (optional): +When ``fps`` is provided, the temporal position IDs are scaled to reflect real time +rather than just frame indices. The formula is: + scaled_time = (frame_index + start_frame_offset) / tps * base_tps +where: + tps = fps / temporal_compression_factor + base_tps = base_fps / base_temporal_compression_factor + +This ensures that videos with different FPS values have comparable temporal position +embeddings, allowing the model to understand temporal relationships across different +video sources. +""" + +import math + +import torch + + +def get_3d_mrope_ids_text_tokens( + num_tokens: int, + temporal_offset: int | float, + use_float_positions: bool = False, +) -> tuple[torch.Tensor, int | float]: + """Generate 3D mRoPE position IDs for text tokens. + + For text tokens, all three axes (temporal, height, width) share the same + monotonically increasing position IDs, starting from ``temporal_offset``. + + Args: + num_tokens: Number of text tokens. + temporal_offset: Current temporal offset to start from. Can be float when + FPS modulation is enabled for vision tokens. + use_float_positions: If ``True``, generate float position IDs (for consistency + with FPS-modulated vision tokens). If ``False``, generate integer IDs. + + Returns: + Tuple of: + - Position IDs tensor of shape ``(3, num_tokens)`` where each row is identical. + - Updated temporal offset (``temporal_offset + num_tokens``). + """ + if use_float_positions: + # Float mode: for consistency with FPS-modulated vision tokens + ids = torch.arange(num_tokens, dtype=torch.float32) + temporal_offset # [num_tokens] + else: + # Integer mode (default) + ids = torch.arange(num_tokens, dtype=torch.long) + int(temporal_offset) # [num_tokens] + + mrope_ids = ids.unsqueeze(0).expand(3, -1).contiguous() # [3,num_tokens] + next_temporal_offset = temporal_offset + num_tokens + return mrope_ids, next_temporal_offset + + +def get_3d_mrope_ids_vae_tokens( + grid_t: int, + grid_h: int, + grid_w: int, + temporal_offset: int | float, + reset_spatial_indices: bool = True, + fps: float | None = None, + base_fps: float = 24.0, + temporal_compression_factor: int = 4, + base_temporal_compression_factor: int | None = None, + start_frame_offset: int = 0, +) -> tuple[torch.Tensor, int | float]: + """Generate 3D mRoPE position IDs for VAE vision tokens (image/video latents). + + Creates a 3D position grid for vision tokens with shape ``(T, H, W)``, then flattens + to produce position IDs for each axis. The flattening order is T-major: + for each temporal frame, iterate over height then width. + + Args: + grid_t: Number of temporal frames in the latent grid. + grid_h: Height of the latent grid (after patchification). + grid_w: Width of the latent grid (after patchification). + temporal_offset: Current temporal offset. Always applied to the temporal axis. + When ``reset_spatial_indices=False``, also applied to spatial axes. + Can be float when FPS modulation is enabled. + reset_spatial_indices: If ``True``, spatial (height, width) indices start from 0 + for each vision segment, giving the model absolute spatial position + within each image/video. If ``False``, spatial indices are also offset by + ``temporal_offset`` (Qwen2VL-style behavior). + fps: Frames per second of the video. ``None`` disables fps modulation + (integer positions); pass the real fps for fps-scaled, possibly + fractional positions. Honored at grid_t=1 too (per-frame AR packs), + where it collapses to ``scaled_t[0] = temporal_offset``. + base_fps: Base FPS for normalization. Default is 24.0. + temporal_compression_factor: VAE temporal compression factor. Default is 4. + base_temporal_compression_factor: Base temporal compression factor. If ``None``, + defaults to ``temporal_compression_factor`` (typical case where base matches actual). + start_frame_offset: Offset added to frame indices before FPS scaling. + Use 1 for action embeddings so they start at frame 1 instead of 0. + + Returns: + Tuple of: + - Position IDs tensor of shape ``(3, grid_t * grid_h * grid_w)``. + Row 0: temporal axis (float if FPS modulation enabled, else long). + Row 1: height axis (long), Row 2: width axis (long). + - Updated temporal offset for the next segment. When FPS modulation is + enabled, this is a float representing the next scaled time position. + Otherwise, it's ``max(all_positions) + 1`` (Qwen3VL design). + """ + # Enabled whenever fps is provided, including grid_t=1 (per-frame AR packs). + # Callers that want integer positions (e.g. images) pass fps=None. + fps_modulation_enabled = fps is not None + + # Default base_temporal_compression_factor to temporal_compression_factor if not specified + effective_base_tcf = ( + base_temporal_compression_factor + if base_temporal_compression_factor is not None + else temporal_compression_factor + ) + + if fps_modulation_enabled: + # FPS modulation: scale temporal indices to reflect real time + # tps = tokens per second (fps divided by temporal compression) + # base_tps = base tokens per second + tps = fps / temporal_compression_factor + base_tps = base_fps / effective_base_tcf + + # Frame indices: 0, 1, 2, ..., grid_t-1 + frame_indices = torch.arange(grid_t, dtype=torch.float32) # [grid_t] + + # Apply FPS scaling: scaled_time = (frame_index + start_frame_offset) / tps * base_tps + scaled_t = (frame_indices + start_frame_offset) / tps * base_tps + temporal_offset # [grid_t] + + # Expand temporal indices for all spatial positions + t_index = scaled_t.view(-1, 1).expand(-1, grid_h * grid_w).flatten() # [grid_t*grid_h*grid_w] + t_dtype = torch.float32 + else: + # No FPS modulation: use integer frame indices + # Apply start_frame_offset for cross-modality alignment (e.g., action tokens start at frame 1) + t_index = ( + ( + torch.arange(grid_t, dtype=torch.long).view(-1, 1).expand(-1, grid_h * grid_w).flatten() + ) # [grid_t*grid_h*grid_w] + + int(temporal_offset) + + start_frame_offset + ) + t_dtype = torch.long + + # Height axis: for each temporal frame, cycles through h values, each repeated w times + h_index = ( + torch.arange(grid_h, dtype=torch.long).view(1, -1, 1).expand(grid_t, -1, grid_w).flatten() + ) # [grid_t*grid_h*grid_w] + + # Width axis: for each temporal frame and height, cycles through w values + w_index = ( + torch.arange(grid_w, dtype=torch.long).view(1, 1, -1).expand(grid_t, grid_h, -1).flatten() + ) # [grid_t*grid_h*grid_w] + + if not reset_spatial_indices: + # Qwen2VL-style: offset all axes by temporal_offset (use int for spatial) + spatial_offset = int(temporal_offset) + h_index = h_index + spatial_offset # [grid_t*grid_h*grid_w] + w_index = w_index + spatial_offset # [grid_t*grid_h*grid_w] + + # Stack into (3, T*H*W) tensor + # Note: When FPS modulation is enabled, temporal axis is float, spatial axes are long + # We convert h_index and w_index to the same dtype as t_index for stacking + if fps_modulation_enabled: + mrope_ids = torch.stack( + [t_index, h_index.to(torch.float32), w_index.to(torch.float32)], dim=0 + ) # [3,grid_t*grid_h*grid_w] + else: + mrope_ids = torch.stack([t_index, h_index, w_index], dim=0) # [3,grid_t*grid_h*grid_w] + + # Compute next temporal offset: max position + 1 + # Use the actual computed positions to handle FPS modulation correctly + max_position = mrope_ids.max().item() + next_temporal_offset = math.ceil(max_position) + 1 + + return mrope_ids, next_temporal_offset diff --git a/cosmos_training/cosmos/model/vfm/mot/unified_mot.py b/cosmos_training/cosmos/model/vfm/mot/unified_mot.py new file mode 100644 index 00000000..1538f707 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/mot/unified_mot.py @@ -0,0 +1,1041 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from dataclasses import dataclass +from typing import Optional, Tuple + +import torch +from torch import nn +from transformers.utils import ModelOutput + +from cosmos.utils import log +from cosmos.data.vfm.sequence_packing import ( + FactoredSequencePack, + from_joint, + from_und_gen_splits, + get_device_and_dtype, + get_gen_seq, + get_und_seq, + set_gen_seq, + set_und_seq, + zeros_like, +) +from cosmos.model.vfm.mot.attention import ( + AttentionMaskType, + dispatch_attention, +) +from cosmos.model.vfm.utils.memory import KVToStore, MemoryState, MemoryValue + +# Nemotron 3 Dense VL imports +from cosmos.model.vfm.vlm.nemotron_3_dense_vl.configuration_nemotron_3_dense_vl import ( + Nemotron3DenseVLTextConfig as _Nemotron3DenseVLTextConfig, +) +from cosmos.model.vfm.vlm.nemotron_3_dense_vl.nemotron_3_dense_vl import ( + MultiModalRotaryEmbedding, + Nemotron3DenseVLMLP, + Nemotron3DenseVLPreTrainedModel, + Nemotron3DenseVLRMSNorm, + apply_rotary_pos_emb_partial, +) + +# Qwen3-VL imports +from cosmos.model.vfm.vlm.qwen3_vl.configuration_qwen3_vl import ( + Qwen3VLTextConfig as _Qwen3VLTextConfig, +) +from cosmos.model.vfm.vlm.qwen3_vl.qwen3_vl import ( + Qwen3VLPreTrainedModel, + Qwen3VLTextMLP, + Qwen3VLTextRMSNorm, + Qwen3VLTextRotaryEmbedding, +) +from cosmos.model.vfm.vlm.qwen3_vl.qwen3_vl import ( + apply_rotary_pos_emb as qwen3_vl_apply_rotary_pos_emb, +) + +# Qwen3-VL-MoE imports +from cosmos.model.vfm.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeTextConfig as _Qwen3VLMoeTextConfig, +) +from cosmos.model.vfm.vlm.qwen3_vl_moe.qwen3_vl_moe import ( + LBLMetadata, + Qwen3VLMoePreTrainedModel, + Qwen3VLMoeTextMLP, + Qwen3VLMoeTextRMSNorm, + Qwen3VLMoeTextRotaryEmbedding, + Qwen3VLMoeTextSparseMoeBlock, +) + +# Torch optimization settings +torch._dynamo.config.cache_size_limit = 512 +torch._dynamo.config.accumulated_cache_size_limit = 4096 + +# ----------------------------------------------------------------------------- +# Unified MoT (Mixture of Transformers) implementation supporting: +# - Qwen3-VL Dense, Qwen3-VL MoE, and Nemotron 3 Dense VL +# +# Shared components: +# - PackedAttentionMoT (config-driven QK norm and RoPE) +# - MoTDecoderLayer (used by all variants) +# - _impl_* (shared init/forward) +# +# Variant-specific wrapper classes are needed for different PreTrainedModel bases. +# Sub-layer classes (MLP, RMSNorm, RotaryEmbedding, RoPE fn) are selected via LayerTypes. +# ----------------------------------------------------------------------------- + + +class LayerTypes: + def __init__(self, variant: str): + self.variant = variant + if variant == "qwen3_vl_moe": + self.mlp = Qwen3VLMoeTextMLP + self.rms_norm = Qwen3VLMoeTextRMSNorm + self.rotary_embedding = Qwen3VLMoeTextRotaryEmbedding + self.apply_rotary_pos_emb = qwen3_vl_apply_rotary_pos_emb + elif variant == "nemotron_dense": + self.mlp = Nemotron3DenseVLMLP + self.rms_norm = Nemotron3DenseVLRMSNorm + self.rotary_embedding = MultiModalRotaryEmbedding + self.apply_rotary_pos_emb = apply_rotary_pos_emb_partial + elif variant == "qwen3_vl_dense": + self.mlp = Qwen3VLTextMLP + self.rms_norm = Qwen3VLTextRMSNorm + self.rotary_embedding = Qwen3VLTextRotaryEmbedding + self.apply_rotary_pos_emb = qwen3_vl_apply_rotary_pos_emb + else: + raise ValueError(f"Unknown LayerTypes variant: {variant!r}") + + @property + def is_moe(self) -> bool: + return self.variant == "qwen3_vl_moe" + + +class NaiveCache: + def __init__(self, num_layers): + self.key_cache = {k: None for k in range(num_layers)} + self.value_cache = {k: None for k in range(num_layers)} + + @property + def num_layers(self): + return len(self.key_cache) + + @property + def seq_lens(self): + if self.key_cache[0] is not None: + return self.key_cache[0].shape[0] + else: + return 0 + + +@dataclass +class BaseOutputWithPast(ModelOutput): + packed_query_sequence: torch.FloatTensor = None + past_key_values: Optional[NaiveCache] = None + + +# Qwen3-VL MoT (Mixture of Tokens) implementation +# Combines Qwen3-VL vision-language capabilities with MoT dual-pathway architecture + + +class Qwen3VLTextConfig(_Qwen3VLTextConfig): + r""" + Qwen3VLTextConfig with MoT-specific parameters. + Extends Qwen3VLTextConfig for text component MoT support with comprehensive configuration. + """ + + def __init__( + self, + # MoT-specific parameters with comprehensive defaults + qk_norm_for_text: bool = False, # Whether to apply QK norm in the understanding (text) pathway + qk_norm_for_diffusion: bool = True, # Whether to apply QK norm in the generation (diffusion) pathway + freeze_und: bool = False, # Freeze understanding pathway + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = True, + **kwargs, + ): + # Store MoT-specific parameters + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file): + """ + Enhanced from_json_file that handles both nested and flat configs. + + For nested configs (with text_config section), extracts the text_config. + For flat configs, loads directly. + """ + + # Load the raw JSON + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + + # Check if this is a nested config with text_config section + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + # Extract the text_config section for nested configs + log.debug("Detected nested config, extracting text_config section") + config_dict = config_dict["text_config"] + else: + # Use the config as-is for flat configs + log.debug("Detected flat config, using directly") + + # Create config from the (potentially extracted) dict + return cls(**config_dict) + + +# Qwen3-VL-MoE MoT (Mixture of Tokens) implementation +# Combines Qwen3-VL-MoE vision-language capabilities with MoT dual-pathway architecture + + +class Qwen3VLMoeTextConfig(_Qwen3VLMoeTextConfig): + r""" + Qwen3VLMoeTextConfig with MoT-specific parameters. + Extends Qwen3VLMoeTextConfig for text component MoT support with comprehensive configuration. + """ + + def __init__( + self, + # MoT-specific parameters with comprehensive defaults + qk_norm_for_text: bool = False, # Whether to apply QK norm in the understanding (text) pathway + qk_norm_for_diffusion: bool = True, # Whether to apply QK norm in the generation (diffusion) pathway + freeze_und: bool = False, # Freeze understanding pathway + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = True, + **kwargs, + ): + # Store MoT-specific parameters + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file): + """ + Enhanced from_json_file that handles both nested and flat configs. + For nested configs (with text_config section), extracts the text_config. + For flat configs, loads directly. + """ + + # Load the raw JSON + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + + # Check if this is a nested config with text_config section + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + # Extract the text_config section for nested configs + log.debug("Detected nested config, extracting text_config section") + config_dict = config_dict["text_config"] + else: + # Use the config as-is for flat configs + log.debug("Detected flat config, using directly") + + # Create config from the (potentially extracted) dict + return cls(**config_dict) + + +# Nemotron 3 Dense VL MoT config + +_NEMOTRON_MOT_TEXT_CONFIG_KEYS = { + "vocab_size", + "tie_word_embeddings", + "hidden_size", + "intermediate_size", + "num_hidden_layers", + "num_attention_heads", + "head_dim", + "num_key_value_heads", + "mlp_hidden_act", + "attention_bias", + "mlp_bias", + "initializer_range", + "layer_norm_epsilon", + "residual_in_fp32", + "use_cache", + "num_logits_to_keep", + "pad_token_id", + "bos_token_id", + "eos_token_id", + "sliding_window", + "max_position_embeddings", + "attention_dropout", + "hidden_dropout", + "enable_rope", + "rope_scaling", + "rope_theta", + "enable_mrope", + "mrope_section", + "torch_dtype", +} + + +class Nemotron3DenseVLTextConfig(_Nemotron3DenseVLTextConfig): + """MoT-enabled config for the Nemotron 3 Dense VL text backbone. + + Extends the upstream ``Nemotron3DenseVLTextConfig`` with MoT-specific + fields (per-pathway QK normalisation, freeze control, decoder layer class). + Supports both the VLM nested config and the flat LLM config format. + """ + + def __init__( + self, + qk_norm_for_text: bool = False, + qk_norm_for_diffusion: bool = True, + freeze_und: bool = False, + layer_module: str = "MoTDecoderLayer", + tie_word_embeddings: bool = False, + **kwargs, + ) -> None: + self.qk_norm_for_text = qk_norm_for_text + self.qk_norm_for_diffusion = qk_norm_for_diffusion + self.freeze_und = freeze_und + self.layer_module = layer_module + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + @classmethod + def from_json_file(cls, json_file: str) -> "Nemotron3DenseVLTextConfig": + """Load config from a JSON file, handling both VLM nested and flat LLM formats.""" + with open(json_file, encoding="utf-8") as reader: + config_dict = json.load(reader) + if "text_config" in config_dict and isinstance(config_dict["text_config"], dict): + log.debug("Detected nested config, extracting text_config section") + config_dict = dict(config_dict["text_config"]) + else: + log.debug("Detected flat config, using directly") + if config_dict.get("num_hidden_layers") == 56: + # Upstream VLM stores attention and MLP as separate alternating blocks (56 total); + # MoT combines both into standard transformer layers (28 total). + config_dict = {**config_dict, "num_hidden_layers": 28} + filtered = {k: v for k, v in config_dict.items() if k in _NEMOTRON_MOT_TEXT_CONFIG_KEYS} + return cls(**filtered) + + +# ----------------------------------------------------------------------------- +# Common layers between Qwen3VL Dense, MoE, and Nemotron 3 Dense VL models +# ----------------------------------------------------------------------------- + + +class PackedAttentionMoT(nn.Module): + """ + Dual-pathway packed attention for MoT architectures. + Implements understanding and generation pathways with separate projections. + + Used for Qwen3VL (Dense), Qwen3VL-MoE, and Nemotron 3 Dense VL variants. + QK normalisation and RoPE function are selected via ``layer_types`` and config + attributes (``qk_norm_for_text`` / ``qk_norm_for_diffusion``). + """ + + def __init__(self, config, layer_idx: int, layer_types: LayerTypes): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.hidden_size = config.hidden_size + self.num_attention_heads = config.num_attention_heads + self.num_key_value_heads = config.num_key_value_heads + self.num_key_value_groups = self.num_attention_heads // self.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + + eps = config.rms_norm_eps + + # Understanding pathway projections + self.q_proj = nn.Linear(self.hidden_size, self.num_attention_heads * self.head_dim, bias=config.attention_bias) + self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) + self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) + self.o_proj = nn.Linear(self.num_attention_heads * self.head_dim, self.hidden_size, bias=config.attention_bias) + + # Understanding pathway QK norm + if config.qk_norm_for_text: + self.q_norm = layer_types.rms_norm(self.head_dim, eps=eps) + self.k_norm = layer_types.rms_norm(self.head_dim, eps=eps) + else: + self.q_norm = nn.Identity() + self.k_norm = nn.Identity() + + # Generation pathway QK norm + if config.qk_norm_for_diffusion: + self.q_norm_moe_gen = layer_types.rms_norm(self.head_dim, eps=eps) + self.k_norm_moe_gen = layer_types.rms_norm(self.head_dim, eps=eps) + else: + self.q_norm_moe_gen = nn.Identity() + self.k_norm_moe_gen = nn.Identity() + + # Generation pathway linear projections + self.q_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj_moe_gen = nn.Linear( + self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj_moe_gen = nn.Linear( + self.num_attention_heads * self.head_dim, self.hidden_size, bias=config.attention_bias + ) + + self._apply_rotary_pos_emb = layer_types.apply_rotary_pos_emb + self.dispatch_attention_fn = dispatch_attention + self.cp_mesh = None + + def forward( + self, + pack: FactoredSequencePack, + attention_mask: AttentionMaskType, + packed_position_embeddings: Tuple[FactoredSequencePack, FactoredSequencePack], + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + ) -> tuple[FactoredSequencePack, KVToStore | None]: + """Forward pass with optional memory-augmented attention. + + When ``memory_value`` is provided, ``dispatch_attention_fn`` routes to + the appropriate attention kernel (e.g. three-way KV-cache attention + for training, or AR inference concat + dense attention). + + ``kv_to_store`` is produced when ``memory_value`` is present: + ``(gen_k, gen_v, und_k, und_v)`` for the caller to write back via + ``MemoryState.write_for_layer()``. The tensors are passed with + gradients attached; each ``MemoryState`` decides whether to detach + (e.g. for truncated BPTT) or keep gradients (e.g. teacher forcing). + + Args: + pack: Packed sequence with und/gen tokens + attention_mask: Attention mask (BlockMask or SplitInfo) + packed_position_embeddings: RoPE embeddings (cos, sin) + natten_metadata: Optional NATTEN metadata for neighborhood attention. + memory_value: Optional read-only tensor container for memory-augmented attention. + """ + + q_und_in = self.q_proj(get_und_seq(pack)) # [N_und,num_heads*head_dim] + q_gen_in = self.q_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_heads*head_dim] + + k_und_in = self.k_proj(get_und_seq(pack)) # [N_und,num_kv_heads*head_dim] + k_gen_in = self.k_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_kv_heads*head_dim] + + v_und_in = self.v_proj(get_und_seq(pack)) # [N_und,num_kv_heads*head_dim] + v_gen_in = self.v_proj_moe_gen(get_gen_seq(pack)) # [N_gen,num_kv_heads*head_dim] + + q_und = q_und_in.view(-1, self.num_attention_heads, self.head_dim) # [N_und,num_heads,head_dim] + k_und = k_und_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_und,num_kv_heads,head_dim] + v_und = v_und_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_und,num_kv_heads,head_dim] + + q_gen = q_gen_in.view(-1, self.num_attention_heads, self.head_dim) # [N_gen,num_heads,head_dim] + k_gen = k_gen_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_gen,num_kv_heads,head_dim] + v_gen = v_gen_in.view(-1, self.num_key_value_heads, self.head_dim) # [N_gen,num_kv_heads,head_dim] + + q_und = self.q_norm(q_und) # [N_und,num_heads,head_dim] + k_und = self.k_norm(k_und) # [N_und,num_kv_heads,head_dim] + + q_gen = self.q_norm_moe_gen(q_gen) # [N_gen,num_heads,head_dim] + k_gen = self.k_norm_moe_gen(k_gen) # [N_gen,num_kv_heads,head_dim] + + if self.config.freeze_und: + q_und = q_und.detach() + k_und = k_und.detach() + v_und = v_und.detach() + + packed_cos = packed_position_embeddings[0] + packed_sin = packed_position_embeddings[1] + + q_und_, k_und_ = self._apply_rotary_pos_emb( + q_und, + k_und, + get_und_seq(packed_cos), + get_und_seq(packed_sin), + unsqueeze_dim=1, + ) # q_und_: [N_und,num_heads,head_dim], k_und_: [N_und,num_kv_heads,head_dim] + q_gen_, k_gen_ = self._apply_rotary_pos_emb( + q_gen, + k_gen, + get_gen_seq(packed_cos), + get_gen_seq(packed_sin), + unsqueeze_dim=1, + ) # q_gen_: [N_gen,num_heads,head_dim], k_gen_: [N_gen,num_kv_heads,head_dim] + + packed_query_states_ = from_und_gen_splits(q_und_, q_gen_, pack) # [N_und+N_gen,num_heads,head_dim] + packed_key_states_ = from_und_gen_splits(k_und_, k_gen_, pack) # [N_und+N_gen,num_kv_heads,head_dim] + packed_value_states_ = from_und_gen_splits(v_und, v_gen, pack) # [N_und+N_gen,num_kv_heads,head_dim] + + packed_attn_output, kv_to_store = self.dispatch_attention_fn( + packed_query_states_, + packed_key_states_, + packed_value_states_, + attention_mask, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + + # Produce kv_to_store for MemoryState.write_for_layer() when the + # dispatch didn't already provide one (e.g. standard or AR frame-0 + # non-CP paths). CP dispatch returns head-sharded kv_to_store + # directly, so kv_to_store is already non-None in that case. + # + # Gradient detach is NOT done here; each MemoryState.write_for_layer() + # decides its own gradient policy (e.g. detach for truncated BPTT, + # keep gradients for teacher forcing). + if memory_value is not None and kv_to_store is None: + und_len = pack["_num_causal_tokens"] + gen_len = pack["_num_full_tokens"] + kv_to_store = ( + k_gen_[:gen_len].unsqueeze(0), + v_gen[:gen_len].unsqueeze(0), + k_und_[:und_len].unsqueeze(0), + v_und[:und_len].unsqueeze(0), + ) + + # Apply projections directly to get final results + und_seq = self.o_proj(get_und_seq(packed_attn_output)) # [N_und,hidden_size] + gen_seq = self.o_proj_moe_gen(get_gen_seq(packed_attn_output)) # [N_gen,hidden_size] + return from_und_gen_splits(und_seq, gen_seq, pack), kv_to_store # [N_und+N_gen,hidden_size] + + +def _impl_init( + self, config: Qwen3VLTextConfig | Qwen3VLMoeTextConfig | Nemotron3DenseVLTextConfig, layer_types: LayerTypes +): + """ + Common implementation for Qwen3VLTextModel, Qwen3VLMoeTextModel, and Nemotron3DenseVLTextModel __init__. + """ + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + assert "Mo" in config.layer_module, "Only MoT layers are supported" + + # Text configuration for decoder layers + + # Embeddings from Qwen3VL base + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + + self.layers = nn.ModuleList( + [MoTDecoderLayer(config, layer_idx, layer_types) for layer_idx in range(config.num_hidden_layers)] + ) + + # Layer norm and rotary embeddings (text-only optimized) + self.norm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + # Pathway-specific normalization + self.norm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + # Rotary embedding (text-only optimized) + self.rotary_emb = layer_types.rotary_embedding(config) + + # Initialize weights and apply final processing + self.post_init() + + +def _impl_init_taylorseer(self, cache_dic=None, current=None): + """ + Initialize TaylorSeer acceleration attributes. + Common implementation for Qwen3VLTextModel.init_taylorseer and Qwen3VLMoeTextModel.init_taylorseer + """ + self.cache_dic = cache_dic or {} + self.current = current or { + "step": 0, + "type": "full", + "stream": "layers_stream", + "layer": 0, + "module": "total", + "activated_steps": [0], + } + # Enable TaylorSeer flag + self.enable_taylorseer = True + + +def _impl_forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, +) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + """ + Training forward pass - Attempted port from qwen2_mot + Common implementation for Qwen3VLTextModel.forward_train and Qwen3VLMoeTextModel.forward_train + + Args: + pack: Packed sequence + attention_mask: Attention mask + position_ids: Position IDs + natten_metadata_list: Optional per-layer NATTEN metadata. + memory: Optional MemoryState for persistent memory across forward passes. + """ + + # Create position embeddings (Qwen3 style) - squeeze once at model level + # tensor below is only used for its dtype and device + device, dtype = get_device_and_dtype(pack) + _meta_tensor = torch.tensor([], dtype=dtype, device=device) + cos, sin = self.rotary_emb( + _meta_tensor, position_ids=position_ids.unsqueeze(0) if position_ids.ndim == 1 else position_ids.unsqueeze(1) + ) # if ndim == 2, then the mrope position_ids is (3, seq_len), we need to put batch dimension in the middle to make it compatible with the rotary_emb + # cos, sin: [1,N,head_dim] (1D pos_ids) or [3,1,N,head_dim] (mrope pos_ids) + cos = cos.squeeze(0) # [N,head_dim] or [3,N,head_dim] + sin = sin.squeeze(0) # [N,head_dim] or [3,N,head_dim] + position_embeddings = ( + from_joint(cos, pack), + from_joint(sin, pack), + ) + + # Tracking the load balancing loss across all layers. For dense models, lbl_metadata_all + # will be a dictionary with empty lists for each pathway. For MoE models, the lists + # for each pathway will be populated with the load balancing loss metadata for each layer. + lbl_metadata_all = dict(und=[], gen=[]) + + hidden_states = pack + + # --- MemoryState: per-step init (outside compile) --- + if memory is not None: + memory.init(hidden_states, device) + + # Derive gen_only once (outside compile) if using MemoryState + memory_gen_only = memory.is_gen_only() if memory is not None else False + + for i, decoder_layer in enumerate(self.layers): + # MemoryState: produce read-only MemoryValue for this layer (outside compile) + memory_value = memory.read_for_layer(i) if memory is not None else None + + hidden_states, lbl_metadata_dict, kv_to_store = decoder_layer( + hidden_states, + attention_mask, + position_embeddings, + natten_metadata=None if natten_metadata_list is None else natten_metadata_list[i], + memory_value=memory_value, + gen_only=memory_gen_only, + ) + + # MemoryState: store K/V produced by this layer (outside compile) + if kv_to_store is not None and memory is not None: + memory.write_for_layer(i, kv_to_store) + + for pathway, lbl_metadata in lbl_metadata_dict.items(): + lbl_metadata_all[pathway].append(lbl_metadata) + + # Compute the load balancing loss across all layers. For dense models, final_lbl_metadata + # will be an empty dictionary. For MoE models, it will be a dictionary with the stacked + # load balancing loss metadata for each pathway. + final_lbl_metadata: dict[str, LBLMetadata] = dict() + for pathway, lbl_metadata_list in lbl_metadata_all.items(): + if len(lbl_metadata_list) > 0: + num_tokens_per_expert = torch.stack( + [lbl_metadata.num_tokens_per_expert for lbl_metadata in lbl_metadata_list] + ) # [num_layers,num_experts] + num_tokens = torch.stack([lbl_metadata.num_tokens for lbl_metadata in lbl_metadata_list]) # [num_layers] + mean_router_prob_per_expert = torch.stack( + [lbl_metadata.mean_router_prob_per_expert for lbl_metadata in lbl_metadata_list] + ) # [num_layers,num_experts] + final_lbl_metadata[pathway] = LBLMetadata( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + mean_router_prob_per_expert=mean_router_prob_per_expert, + ) + + hidden_states_out = zeros_like(hidden_states) + set_und_seq(hidden_states_out, self.norm(get_und_seq(hidden_states))) # [N_und,hidden_size] + set_gen_seq(hidden_states_out, self.norm_moe_gen(get_gen_seq(hidden_states))) # [N_gen,hidden_size] + + return hidden_states_out, final_lbl_metadata + + +def _run_mlp( + mlp: torch.nn.Module, + input: torch.Tensor, +) -> tuple[torch.Tensor, LBLMetadata | None]: + if isinstance(mlp, Qwen3VLMoeTextSparseMoeBlock): + ( + output_tensor, + lbl_metadata, + ) = mlp(input) + else: + output_tensor = mlp(input) + lbl_metadata = None + return output_tensor, lbl_metadata + + +class MoTDecoderLayer(nn.Module): + """ + Unified MoT (Mixture of Transformers) decoder layer. + Features dual-pathway attention for understanding vs generation. + + This is used for both Dense and MoE models. + """ + + def __init__( + self, + config: Qwen3VLTextConfig | Qwen3VLMoeTextConfig | Nemotron3DenseVLTextConfig, + layer_idx: int, + layer_types: LayerTypes, + ): + super().__init__() + self.hidden_size = config.hidden_size + self.freeze_und = config.freeze_und + self.self_attn = PackedAttentionMoT(config, layer_idx, layer_types) + + if ( + hasattr(config, "mlp_only_layers") + and (layer_idx not in config.mlp_only_layers) + and (config.num_experts > 0 and (layer_idx + 1) % config.decoder_sparse_step == 0) + ): + self.mlp = Qwen3VLMoeTextSparseMoeBlock(config) + self.mlp_moe_gen = Qwen3VLMoeTextSparseMoeBlock(config) + else: + self.mlp = layer_types.mlp(config) + self.mlp_moe_gen = layer_types.mlp(config) + + self.input_layernorm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.input_layernorm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm_moe_gen = layer_types.rms_norm(config.hidden_size, eps=config.rms_norm_eps) + + def forward( + self, + input: FactoredSequencePack, + attention_mask, + packed_position_embeddings: Tuple[FactoredSequencePack, FactoredSequencePack], + natten_metadata: dict | None = None, + memory_value: MemoryValue | None = None, + gen_only: bool = False, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata], KVToStore | None]: + """Forward pass with MoT routing and optional memory-augmented attention. + + Returns a 3-tuple: ``(hidden_states, lbl_metadata_dict, kv_to_store)``. + ``kv_to_store`` is non-None when ``memory_value`` is provided, + containing ``(gen_k, gen_v, und_k, und_v)`` to be written back by + ``MemoryState.write_for_layer()`` outside the ``torch.compile`` + boundary. + + Args: + input: Packed sequence with und/gen tokens + attention_mask: Attention mask + packed_position_embeddings: RoPE embeddings (cos, sin) + natten_metadata: Optional NATTEN metadata for neighborhood attention. + memory_value: Read-only tensor container from MemoryState.read_for_layer(). + gen_only: When True, skip the understanding pathway (und K/V come from cache). + """ + # Pre-Attention layernorm + pack_norm_out = from_und_gen_splits( + self.input_layernorm(get_und_seq(input)), # [N_und,hidden_size] + self.input_layernorm_moe_gen(get_gen_seq(input)), # [N_gen,hidden_size] + input, + ) # [N_und+N_gen,hidden_size] + + # Self Attention + Residual + kv_to_store: KVToStore | None = None + if gen_only: + assert natten_metadata is None + # gen_only: skip und, compute gen tokens only (und K/V come from cache) + _gen_norm = get_gen_seq(pack_norm_out) + gen_pack = from_und_gen_splits( + _gen_norm.new_empty(0, _gen_norm.shape[-1]), + _gen_norm, + pack_norm_out, + ) + + # Build position embeddings whose und length matches gen_pack's + # und length (always 0). Required when the outer pack carries + # a padded causal_seq (``pad_for_cuda_graphs=True``): without + # this, the und RoPE inside ``PackedAttentionMoT.forward`` + # would broadcast cos/sin of shape ``(MAX_CAUSAL_LEN, head_dim)`` + # onto a length-0 ``q_und`` / ``k_und`` and crash. When the + # outer pack is unpadded (eager AR path), the und cos/sin + # already have length 0 and this slice is a no-op. + _cos, _sin = packed_position_embeddings + _empty_cos_und = get_und_seq(_cos)[:0] + _empty_sin_und = get_und_seq(_sin)[:0] + gen_position_embeddings = ( + from_und_gen_splits(_empty_cos_und, get_gen_seq(_cos), _cos), + from_und_gen_splits(_empty_sin_und, get_gen_seq(_sin), _sin), + ) + + pack_attn_out, kv_to_store = self.self_attn( + gen_pack, + attention_mask, + gen_position_embeddings, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + gen_attn_out = get_gen_seq(pack_attn_out) + residual_und = gen_attn_out.new_empty(0, gen_attn_out.shape[-1]) + residual_gen = get_gen_seq(input) + gen_attn_out + else: + # STANDARD PATH: Process both und and gen tokens + pack_attn_out, kv_to_store = self.self_attn( + pack_norm_out, + attention_mask, + packed_position_embeddings, + natten_metadata=natten_metadata, + memory_value=memory_value, + ) + residual_und = get_und_seq(input) + get_und_seq(pack_attn_out) # [N_und,hidden_size] + residual_gen = get_gen_seq(input) + get_gen_seq(pack_attn_out) # [N_gen,hidden_size] + + # Pre-MLP layernorm and processing + lbl_metadata_dict: dict[str, LBLMetadata] = dict() + + if gen_only: + # gen_only: skip und, compute gen tokens only + ln_out_und = residual_gen.new_empty(0, residual_gen.shape[-1]) + ln_out_gen = self.post_attention_layernorm_moe_gen(residual_gen) + + # UNPAD MLP INPUT (gen only) + gen_len = pack_attn_out["_num_full_tokens"] + ln_out_gen_unpadded = ln_out_gen[:gen_len] # [N_gen_unpadded,hidden_size] + + # Run MLP (gen only) + mlp_out_gen_unpadded, lbl_metadata_gen = _run_mlp(self.mlp_moe_gen, ln_out_gen_unpadded) + # mlp_out_gen_unpadded: [N_gen_unpadded,hidden_size] + + # PAD MLP OUTPUT (gen only) + mlp_out_gen = torch.cat([mlp_out_gen_unpadded, ln_out_gen[gen_len:]], dim=0) # [N_gen,hidden_size] + + # Build metadata dict (no und metadata in optimized path) + if lbl_metadata_gen is not None: + lbl_metadata_dict["gen"] = lbl_metadata_gen + + # Final output with residual (gen only) + mlp_out_und_seq = residual_gen.new_empty(0, residual_gen.shape[-1]) + mlp_out_gen_seq = residual_gen + mlp_out_gen + else: + # STANDARD PATH: Process both und and gen tokens + ln_out_und = self.post_attention_layernorm(residual_und) # [N_und,hidden_size] + ln_out_gen = self.post_attention_layernorm_moe_gen(residual_gen) # [N_gen,hidden_size] + + # UNPAD MLP INPUT =============== + + # artificial expert inbalance due to routing padding tokens. + gen_len = pack_attn_out["_num_full_tokens"] + und_len = pack_attn_out["_num_causal_tokens"] + ln_out_und_unpadded = ln_out_und[:und_len] # [N_und_unpadded,hidden_size] + ln_out_gen_unpadded = ln_out_gen[:gen_len] # [N_gen_unpadded,hidden_size] + + mlp_out_und_unpadded, lbl_metadata_und = _run_mlp(self.mlp, ln_out_und_unpadded) + # mlp_out_und_unpadded: [N_und_unpadded,hidden_size] + mlp_out_gen_unpadded, lbl_metadata_gen = _run_mlp(self.mlp_moe_gen, ln_out_gen_unpadded) + # mlp_out_gen_unpadded: [N_gen_unpadded,hidden_size] + + # PAD MLP OUTPUT =============== + mlp_out_und = torch.cat([mlp_out_und_unpadded, ln_out_und[und_len:]], dim=0) # [N_und,hidden_size] + mlp_out_gen = torch.cat([mlp_out_gen_unpadded, ln_out_gen[gen_len:]], dim=0) # [N_gen,hidden_size] + + if lbl_metadata_und is not None: + lbl_metadata_dict["und"] = lbl_metadata_und + if lbl_metadata_gen is not None: + lbl_metadata_dict["gen"] = lbl_metadata_gen + + mlp_out_und_seq = residual_und + mlp_out_und # [N_und,hidden_size] + mlp_out_gen_seq = residual_gen + mlp_out_gen # [N_gen,hidden_size] + + return from_und_gen_splits(mlp_out_und_seq, mlp_out_gen_seq, input), lbl_metadata_dict, kv_to_store + + +# Backward-compat alias: serialized checkpoint configs reference the old name. +Qwen3VLTextMoTDecoderLayer = MoTDecoderLayer + + +class Qwen3VLTextModel(Qwen3VLPreTrainedModel): + """ + Qwen3VL text model for MoT with dense MLPs. + This is a wrapper around the _impl_forward defined above, + specialized for dense models. + """ + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("qwen3_vl_dense")) + + def init_taylorseer(self, cache_dic=None, current=None): + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Qwen3VLMoeTextModel(Qwen3VLMoePreTrainedModel): + """ + Qwen3VL text model for MoT with MoE MLPs. + This is a wrapper around the _impl_* helpers defined above, + specialized for MoE models. + """ + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("qwen3_vl_moe")) + + def init_taylorseer(self, cache_dic=None, current=None): + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Qwen3VLTextForCausalLM(Qwen3VLPreTrainedModel): + """ + Qwen3VL text causal language model for MoT. + This variant is used for dense-only MLP models. + """ + + _tied_weights_keys = ["lm_head.weight"] + + def __init__(self, config: Qwen3VLTextConfig): + super().__init__(config) + self.model = Qwen3VLTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def init_moe(self) -> None: + """Initialize MoE/MoT weights by copying understanding to generation pathway.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" in name: + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + """Training forward pass - simplified to match qwen3_mot""" + outputs = self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + return outputs + + +class Qwen3VLMoeTextForCausalLM(Qwen3VLMoePreTrainedModel): + """ + Qwen3VL text causal language model for MoT with MoE on the generation pathway. + This variant is used for MoE MLP models. + """ + + _tied_weights_keys = ["lm_head.weight"] + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + self.model = Qwen3VLMoeTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def init_moe(self) -> None: + """Initialize MoE/MoT weights by copying understanding to generation pathway.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" in name: + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, torch.Tensor]]: + """Training forward pass - simplified to match qwen3_mot""" + + outputs = self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) + + return outputs + + +# ----------------------------------------------------------------------------- +# Nemotron 3 Dense VL MoT model wrappers +# ----------------------------------------------------------------------------- + + +class Nemotron3DenseVLTextModel(Nemotron3DenseVLPreTrainedModel): + """Nemotron 3 Dense VL text model adapted for MoT training.""" + + def __init__(self, config: Nemotron3DenseVLTextConfig) -> None: + super().__init__(config) + _impl_init(self, config, layer_types=LayerTypes("nemotron_dense")) + + def init_taylorseer(self, cache_dic=None, current=None) -> None: + _impl_init_taylorseer(self, cache_dic=cache_dic, current=current) + + def forward(self, *args, **kwargs): + return _impl_forward(self, *args, **kwargs) + + +class Nemotron3DenseVLTextForCausalLM(Nemotron3DenseVLPreTrainedModel): + """Causal LM head on top of the Nemotron 3 Dense VL MoT text model.""" + + _tied_weights_keys: list[str] = [] + + def __init__(self, config: Nemotron3DenseVLTextConfig) -> None: + super().__init__(config) + self.model = Nemotron3DenseVLTextModel(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + self.post_init() + + def init_moe(self) -> None: + """Copy understanding-pathway weights into the generation-pathway parameters.""" + state_dict = self.state_dict() + for name, param in self.named_parameters(): + if "moe_gen" not in name: + continue + original_name = name.replace("_moe_gen", "").replace("_checkpoint_wrapped_module.", "") + if original_name in state_dict: + param.data.copy_(state_dict[original_name].data) + elif any(norm_key in original_name for norm_key in ("q_norm", "k_norm")): + # qk_norm_for_text=False → q_norm/k_norm are nn.Identity() with no parameters; + # the moe_gen counterpart (q_norm_moe_gen) is a real RMSNorm, so skip init here. + pass + else: + raise ValueError(f"Could not find {original_name} in state_dict for initialization of {name}") + + def forward( + self, + pack: FactoredSequencePack, + attention_mask, + position_ids: torch.Tensor, + natten_metadata_list: list | None = None, + memory: MemoryState | None = None, + ) -> tuple[FactoredSequencePack, dict[str, LBLMetadata]]: + return self.model( + pack=pack, + attention_mask=attention_mask, + position_ids=position_ids, + natten_metadata_list=natten_metadata_list, + memory=memory, + ) diff --git a/cosmos_training/cosmos/model/vfm/omni_mot_model.py b/cosmos_training/cosmos/model/vfm/omni_mot_model.py new file mode 100644 index 00000000..2d1b9a20 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/omni_mot_model.py @@ -0,0 +1,3023 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +from contextlib import contextmanager +from typing import Any, Callable, Dict, Mapping, Optional, Tuple + +import numpy as np +import torch +import torch.distributed as dist +from einops import rearrange +from torch.distributed._composable.fsdp import FSDPModule +from torch.nn.modules.module import _IncompatibleKeys + +from cosmos.utils.flags import DEVICE, TRAINING, Device +from cosmos.utils.lazy_config import LazyDict +from cosmos.utils.lazy_config import instantiate as lazy_instantiate +from cosmos.model._base import ImaginaireModel +from cosmos.utils import log, misc +from cosmos.utils.count_params import count_params +from cosmos.utils.timer import Timer +from cosmos.model.vfm.algorithm.loss.flow_matching import compute_flow_matching_loss +from cosmos.model.vfm.algorithm.loss.load_balancing import compute_load_balancing_loss +from configs.base.defaults.model_config import OmniMoTModelConfig +from cosmos.data.vfm.sequence_packing import ( + PackedSequence, + SequencePlan, + add_special_tokens, + build_sequence_plans_from_data_batch, + pack_input_sequence, +) +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO +from cosmos.model.vfm.diffusion.rectified_flow import RectifiedFlow +from cosmos.model.vfm.diffusion.samplers.edm import EDMSampler +from cosmos.model.vfm.diffusion.samplers.fixed_step import FixedStepSampler +from cosmos.model.vfm.diffusion.samplers.unipc import UniPCSampler, UniPCSamplerConfig +from cosmos.model.vfm.mot.cosmos3_vfm_network import Cosmos3VFMNetwork, Cosmos3VFMNetworkConfig +from cosmos.model.vfm.mot.modeling_utils import has_noisy_tokens +from cosmos.model.vfm.mot.parallelize_vfm_network import parallelize_vfm_network +from cosmos.model.vfm.utils.data_and_condition import ( + GenerationDataClean, + GenerationDataNoised, + _expand_per_sample_to_per_vision_item, + build_dense_sound_schedule, + unwrap_and_densify, +) +from cosmos.model.vfm.utils.memory import MemoryState +from cosmos.model.vfm.utils.safetensors_loader import load_language_model as load_language_model_safetensors +from cosmos.model.vfm.vlm.qwen3_vl.utils import tokenize_caption +from cosmos.model.vfm.tokenizers.interface import VideoTokenizerInterface +from cosmos.utils.vfm.data_utils import get_vision_data_resolution +from cosmos.utils.vfm.dtensor_helper import DTensorFastEmaModelUpdater +from cosmos.utils.vfm.model_weights_stats import WeightTrainingStat +from cosmos.utils.vfm.parallelism import ParallelDims + + +class OmniMoTModel(ImaginaireModel): + """ + Mixture of Transformers (MoT) model to be trained with the flow matching objective + for visual / sound / action generation. + """ + + def __init__(self, config: OmniMoTModelConfig): + super().__init__() + self.config = config + log.info(f"OmniMoTModel: config {self.config}") + # 0. Set up precision + self.set_precision() + + # 1. Set data keys and data information + self.set_up_data_key() + + # 2. Text, vision, audio, action tokenizers + self.set_up_tokenizers() + + # 3. FSDP setup. Note: call this before building the model. + self.set_up_parallelism() + + # 4. Build the denoiser network + self.set_up_model() + + # 5. Set up training time scheduler and inference time sampler + self.set_up_scheduler_and_sampler() + + self.log_enc_time_every_n = config.log_enc_time_every_n + + def set_precision(self) -> None: + self.precision = getattr(torch, self.config.parallelism.precision) + self.tensor_kwargs = {"device": DEVICE, "dtype": self.precision} + self.tensor_kwargs_fp32 = {"device": DEVICE, "dtype": torch.float32} + log.warning(f"OmniMoTModel: precision {self.precision}") + + # Disable TF32 for CUDA matrix multiplications since this may impact model quality. + torch.backends.cudnn.allow_tf32 = torch.backends.cuda.matmul.allow_tf32 = False + + def set_up_data_key(self) -> None: + + self.input_video_key = self.config.input_video_key # by default it is video key for Video diffusion model + self.input_image_key = self.config.input_image_key + self.input_caption_key = self.config.input_caption_key + + @misc.timer("OmniMoTModel: set_up_tokenizers") + def set_up_tokenizers(self) -> None: + """ + Variable names follow the naming convention: + - tokenizer__gen if used for generation branch + - tokenizer__und if used for understanding branch + """ + # 1. Text tokenizer + self.vlm_config = self.config.vlm_config + _vlm_proc = lazy_instantiate(self.vlm_config.tokenizer) + vlm_tokenizer = ( + _vlm_proc.processor.tokenizer if getattr(_vlm_proc, "is_unified_processor", False) else _vlm_proc + ) + vlm_tokenizer, special_tokens = add_special_tokens(vlm_tokenizer) + self.vlm_tokenizer = vlm_tokenizer + + self.llm_special_tokens = special_tokens + self.llm_special_tokens["eos_token_id"] = vlm_tokenizer.eos_token_id + + # 2. Vision tokenizer (images/videos) for generation. + self.tokenizer_vision_gen: VideoTokenizerInterface = lazy_instantiate(self.config.tokenizer) + assert self.tokenizer_vision_gen.latent_ch == self.config.state_ch, ( + f"vision tokenizer latent_ch {self.tokenizer_vision_gen.latent_ch} != state_shape {self.config.state_ch}" + ) + if hasattr(self.tokenizer_vision_gen, "reset_dtype"): + self.tokenizer_vision_gen.reset_dtype() + + # 3. Sound/audio tokenizer (optional) + if self.config.sound_gen: + assert self.config.sound_tokenizer is not None, "sound_tokenizer must be provided when sound_gen is True" + self.tokenizer_sound_gen = lazy_instantiate(self.config.sound_tokenizer) + assert self.config.sound_dim is not None, "sound_dim must be provided when sound_gen is True" + assert self.tokenizer_sound_gen.latent_ch == self.config.sound_dim, ( + f"sound tokenizer latent_ch {self.tokenizer_sound_gen.latent_ch} != sound_dim {self.config.sound_dim}" + ) + if hasattr(self.tokenizer_sound_gen, "reset_dtype"): + self.tokenizer_sound_gen.reset_dtype() + log.info(f"Sound tokenizer initialized: {type(self.tokenizer_sound_gen).__name__}") + else: + self.tokenizer_sound_gen = None + + + + def build_net(self, dtype: torch.dtype): + # Build model network and parallelize it. + with torch.device("meta"): + assert self.vlm_config.model_instance is not None, "Model instance should be specified" + + language_model = lazy_instantiate(self.vlm_config.model_instance) + + # (i.e., roughly [0, num_train_timesteps]). The MoT network expects to internally + # rescale timesteps before embedding; avoid hard-coding 1e-3 by computing it from + # the configured scheduler resolution. + num_train_timesteps = self.config.rectified_flow_inference_config.num_train_timesteps + network_config = Cosmos3VFMNetworkConfig( + vlm_config=language_model.config, + latent_patch_size=self.config.diffusion_expert_config.patch_spatial, + latent_downsample_factor=self.config.latent_downsample_factor, + latent_channel_size=self.config.state_ch, + max_latent_h=self.config.diffusion_expert_config.max_vae_latent_side_after_patchify, + max_latent_w=self.config.diffusion_expert_config.max_vae_latent_side_after_patchify, + max_latent_t=self.config.state_t, + rope_h_extrapolation_ratio=self.config.diffusion_expert_config.rope_h_extrapolation_ratio, + rope_w_extrapolation_ratio=self.config.diffusion_expert_config.rope_w_extrapolation_ratio, + rope_t_extrapolation_ratio=self.config.diffusion_expert_config.rope_t_extrapolation_ratio, + enable_fps_modulation=self.config.diffusion_expert_config.enable_fps_modulation, + base_fps=self.config.diffusion_expert_config.base_fps, + vision_gen=self.config.vision_gen, + action_gen=self.config.action_gen, + sound_gen=self.config.sound_gen, + position_embedding_type=self.config.diffusion_expert_config.position_embedding_type, + joint_attn_implementation=self.config.joint_attn_implementation, + timestep_scale=1.0 / float(num_train_timesteps) * self.config.diffusion_expert_config.timestep_range, + action_dim=self.config.max_action_dim, + num_embodiment_domains=self.config.num_embodiment_domains, + temporal_compression_factor_vision=self.tokenizer_vision_gen.temporal_compression_factor, + natten_parameter_list=self.config.natten_parameter_list, + video_temporal_causal=self.config.video_temporal_causal, + # Sound generation parameters + sound_dim=self.config.sound_dim, + sound_latent_fps=self.config.sound_latent_fps, + ) + network_config._attn_implementation_internal = "eager" + net = Cosmos3VFMNetwork( + language_model=language_model, + config=network_config, + ) + net.pad_for_cuda_graphs = self.config.parallelism.use_cuda_graphs + + # Inject LoRA BEFORE FSDP wrap, while still on meta device. The + # injector must see unsharded Linear shapes; injecting post-FSDP causes + # lora_B to be created at the per-rank shard size and crashes at + # forward time. See `OmniMoTModel.add_lora` for details. + if getattr(self.config, "lora_enabled", False): + net = self.add_lora( + net, + lora_rank=self.config.lora_rank, + lora_alpha=self.config.lora_alpha, + lora_target_modules=self.config.lora_target_modules, + ) + + self.install_attention_dispatch(net) + + net = parallelize_vfm_network( + net, + parallel_dims=self.parallel_dims, + config=self.config.parallelism, + ) + + with misc.timer("meta to cuda and broadcast model states"): + net = net.to(dtype=dtype) + net.to_empty(device=DEVICE) + if DEVICE == Device.CUDA: + # Weight initialization is not needed for other devices (cpu, + # meta), since they are only for checkpoint conversion and smoke + # tests. + net.init_weights(buffer_device=DEVICE) + if getattr(self.config, "lora_enabled", False): + self._init_lora_weights_post_materialization(net) + + return net + + def load_pretrained_model_if_needed(self): + """ + This function is used to load the pretrained model weights from HF if needed. + + 1. If self.vlm_config.load_pretrained is False, we skip loading the pretrained + model weights. + 2. If self.vlm_config.load_pretrained is True, and + self.config.diffusion_expert_config.load_weights_from_pretrained is True, + we load the understanding pathway weights from HF, and copy them to the + generation pathway. + 3. If self.vlm_config.load_pretrained is True, and + self.config.diffusion_expert_config.load_weights_from_pretrained is False, + we load the understanding pathway weights from HF, but do not copy them to + the generation pathway. This is used when we warm-start from a load_path + (but no previous checkpoint exists), and we want to switch the understanding + pathway weights to a new model (e.g., Qwen3-VL to Cosmos-Reason2). + """ + if not self.vlm_config.load_pretrained: + return + + def _load_language_model(net: torch.nn.Module): + load_language_model_safetensors( + model=net.language_model, + checkpoint_path=self.vlm_config.checkpoint_path, + credential_path=self.vlm_config.credential_path, + parallel_dims=self.parallel_dims, + checkpoint_format=getattr(self.vlm_config, "vlm_checkpoint_format", None), + ) + + # When specified, we load pretrained LLM weights. + log.info(f"Loading understanding pathway weights from {self.vlm_config.checkpoint_path}") + _load_language_model(self.net) + if self.config.ema.enabled: + _load_language_model(self.net_ema) + log.info("Successfully loaded understanding pathway weights.") + + if self.config.diffusion_expert_config.load_weights_from_pretrained: + log.info("Copying understanding pathway weights to generation pathway.") + self.net.language_model.init_moe() + if self.config.ema.enabled: + self.net_ema.language_model.init_moe() + log.info("Successfully copied understanding pathway weights to generation pathway.") + + @misc.timer("OmniMoTModel: set_up_model") + def set_up_model(self): + assert hasattr(self, "parallel_dims"), "parallel_dims must be set" + config = self.config + with misc.timer("Creating PyTorch model and ema if enabled"): + self.net = self.build_net(dtype=self.precision) + self._param_count = count_params(self.net, verbose=False) + + if config.ema.enabled: + self.net_ema = self.build_net(dtype=torch.float32) + self.net_ema.requires_grad_(False) + + self.net_ema_worker = DTensorFastEmaModelUpdater() + + + s = config.ema.rate + self.ema_exp_coefficient = np.roots([1, 7, 16 - s**-2, 12 - s**-2]).real.max() + + self.net_ema_worker.copy_to(src_model=self.net, tgt_model=self.net_ema) + + self.set_up_memory() + + torch.cuda.empty_cache() + + def install_attention_dispatch(self, net: torch.nn.Module) -> None: + """Install a custom attention dispatch function on the network. + + Called during ``build_net()`` after the network is constructed but + before parallelization. The base implementation is a no-op; + ``OmniMoTCausalModel`` overrides this to install + ``dispatch_attention_with_memory`` on every attention layer. + """ + pass + + def set_up_memory(self) -> None: + """Initialize memory state used during training (e.g. KV caches). + + The base implementation is a no-op. ``OmniMoTCausalModel`` overrides + this to allocate a KV cache. + """ + pass + + def set_up_parallelism(self) -> None: + """Set up the fsdp for the model.""" + if not torch.distributed.is_initialized(): + self.parallel_dims = None + return + + self.parallel_dims = ParallelDims( + enable_inference_mode=self.config.parallelism.enable_inference_mode, + world_size=torch.distributed.get_world_size(), + dp_shard=self.config.parallelism.data_parallel_shard_degree, + cfgp=self.config.parallelism.cfg_parallel_shard_degree, + cp=self.config.parallelism.context_parallel_shard_degree, + ) + self.parallel_dims.build_meshes(device_type=DEVICE) + + def set_up_scheduler_and_sampler(self): + # Get shift value - support both int and dict-based resolution lookup + # For scheduler initialization, use model's configured resolution + shift_config = self.config.rectified_flow_training_config.shift + if isinstance(shift_config, int): + shift = shift_config + else: + # shift set in RectifiedFlow is only used during inference. + # So, set it to the resolution of the model. + # This part gets executed only when we specify shift as a dict + # This is needed during multi-resolution training. + shift_dict = dict(shift_config) + resolution = self.config.resolution + if resolution not in shift_dict: + raise ValueError( + f"Resolution '{resolution}' not found in shift dict. Available resolutions: {list(shift_dict.keys())}" + ) + shift = shift_dict[resolution] + + # Rectified Flow timestep scheduler and sampler for training (separate for image and video) + if self.config.vision_gen: + self.rectified_flow_image = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_image_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + self.rectified_flow_video = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_video_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + if self.config.action_gen: + self.rectified_flow_action = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_action_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + if self.config.sound_gen: + self.rectified_flow_sound = RectifiedFlow( + velocity_field=self.net, + train_time_distribution=self.config.rectified_flow_training_config.train_time_sound_distribution, + use_dynamic_shift=self.config.rectified_flow_training_config.use_dynamic_shift, + shift=shift, + train_time_weight_method=self.config.rectified_flow_training_config.train_time_weight, + device=torch.device(DEVICE), + dtype=self.tensor_kwargs_fp32["dtype"], + ) + + # Denoising sampler (solver) for inference + assert self.config.rectified_flow_inference_config.scheduler_type in ["unipc", "edm"] + if self.config.rectified_flow_inference_config.scheduler_type == "unipc": + unipc_sampler_config = UniPCSamplerConfig( + num_train_timesteps=self.config.rectified_flow_inference_config.num_train_timesteps, + shift=self.config.rectified_flow_inference_config.shift, + use_dynamic_shifting=self.config.rectified_flow_inference_config.use_dynamic_shifting, + ) + self.sampler = UniPCSampler(cfg=unipc_sampler_config, tensor_kwargs=self.tensor_kwargs) + else: + self.sampler = EDMSampler() + + # Fixed-step sampler for distilled models (None for base models) + if self.config.fixed_step_sampler_config is not None: + cfg = self.config.fixed_step_sampler_config + self.fixed_step_sampler = FixedStepSampler( + t_list=list(cfg.t_list), + sample_type=cfg.sample_type, + num_train_timesteps=float(self.config.rectified_flow_inference_config.num_train_timesteps), + ) + else: + self.fixed_step_sampler = None + + def init_optimizer_scheduler( + self, optimizer_config: LazyDict, scheduler_config: LazyDict + ) -> tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LRScheduler]: + """Creates the optimizer and scheduler for the model. + + Args: + optimizer_config (LazyDict): The lazy config for the optimizer. + scheduler_config (LazyDict): The lazy config for the learning rate scheduler. + + Returns: + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + """ + + optimizer = lazy_instantiate(optimizer_config, model=self.net) + scheduler = lazy_instantiate(scheduler_config, optimizer=optimizer) + return optimizer, scheduler + + def _derive_include_end_of_generation_token(self) -> bool: + impl = self.config.joint_attn_implementation + assert impl in ("flex", "two_way", "three_way"), ( + f"Invalid joint_attn_implementation: {impl}. Must be 'flex', 'two_way', or 'three_way'." + ) + return impl == "flex" + + # ------------------------ training hooks ------------------------ + def on_before_zero_grad( + self, optimizer: torch.optim.Optimizer, scheduler: torch.optim.lr_scheduler.LRScheduler, iteration: int + ) -> None: + """ + update the net_ema + """ + del scheduler, optimizer + + if self.config.ema.enabled: + # calculate beta for EMA update + ema_beta = self.ema_beta(iteration) + self.net_ema_worker.update_average(self.net, self.net_ema, beta=ema_beta) + + # ------------------------ helpers ------------------------ + + def _pack_input_sequence( + self, + sequence_plans: list[SequencePlan], + input_text_indexes: list[list[int]], + gen_data_clean: GenerationDataClean, + input_timesteps: torch.Tensor, + include_end_of_generation_token: bool = False, + skip_text_tokens: bool = False, + initial_mrope_temporal_offset: int | float = 0, + ) -> PackedSequence: + """Wrap ``pack_input_sequence`` with all config-derived args pre-filled. + + Centralises the 9 config-derived positional/embedding args so callers only + supply the four per-call arguments (sequence_plans, text tokens, data, timesteps) + plus three optional flags. + """ + assert self.tokenizer_vision_gen is not None + return pack_input_sequence( + sequence_plans=sequence_plans, + input_text_indexes=input_text_indexes, + gen_data_clean=gen_data_clean, + input_timesteps=input_timesteps, + special_tokens=self.llm_special_tokens, + latent_patch_size=self.config.diffusion_expert_config.patch_spatial, + skip_text_tokens=skip_text_tokens, + include_end_of_generation_token=include_end_of_generation_token, + position_embedding_type=self.config.diffusion_expert_config.position_embedding_type, + unified_3d_mrope_reset_spatial_ids=self.config.diffusion_expert_config.unified_3d_mrope_reset_spatial_ids, + unified_3d_mrope_temporal_modality_margin=self.config.diffusion_expert_config.unified_3d_mrope_temporal_modality_margin, + enable_fps_modulation=self.config.diffusion_expert_config.enable_fps_modulation, + base_fps=float(self.config.diffusion_expert_config.base_fps), + temporal_compression_factor=self.tokenizer_vision_gen.temporal_compression_factor, + video_temporal_causal=self.config.video_temporal_causal, + action_dim=self.config.max_action_dim, + initial_mrope_temporal_offset=initial_mrope_temporal_offset, + ) + + # ------------------------ training ------------------------ + + def memory_init_training( + self, + gen_data_clean: GenerationDataClean, + data_batch: dict[str, torch.Tensor], + input_text_indexes: list[list[int]], + ) -> tuple[GenerationDataClean, dict]: + """Prepare the memory for a single training step. + + Called at the start of ``training_step`` to give the causal subclass + an injection point for memory-based segment handling (frame trimming, + segment bookkeeping, cache resets, packing overrides). + + The base implementation returns *gen_data_clean* unmodified and a + default memory_info dict that does not support memory-backed training. + + The ``skip_text`` and ``initial_temporal`` offset fields are required, + and are used for both sequence packing and memory. + + Returns: + ``(gen_data_clean, memory_info)`` where *memory_info* is a dict with keys: + ``skip_text``, ``initial_temporal_offset`` + """ + return gen_data_clean, { + "skip_text": False, + "initial_temporal_offset": 0, + } + + def build_memory_state( + self, + packed_seq: PackedSequence, + memory_info: dict, + ) -> MemoryState | None: + """Construct a ``MemoryState`` from a packed sequence and context dict. + + Called after packing in ``training_step()``, and before ``denoise()`` + in AR inference. The base implementation returns ``None`` (no + persistent memory). ``OmniMoTCausalModel`` overrides this to build + the appropriate ``ARMemoryState`` or ``KVCacheTrainMemoryState``. + + Args: + packed_seq: The packed multi-modal sequence produced by + ``_pack_input_sequence``. + memory_info: Context dict returned by ``memory_init_training()`` + (for the training path) or constructed by the AR inference + caller. See ``memory_init_training()`` for the base keys. + """ + return None + + def pre_noise_memory_hook( + self, + packed_sequence: PackedSequence, + gen_data_clean: GenerationDataClean, + memory_info: dict, + ) -> dict: + """Hook called after sequence packing and before noising. Returns (possibly updated) memory_info. + + The packed sequence still contains clean tokens at this point. + Override in subclasses to run a clean forward pass (e.g. for teacher forcing). + """ + return memory_info + + def training_step( + self, data_batch: dict[str, torch.Tensor], iteration: int + ) -> tuple[dict[str, torch.Tensor], torch.Tensor]: + """ + Performs a single training step for the rectified-flow (flow-matching) model. + + This method executes one iteration of the model's training. It involves: + 1. Tokenizing generation modalities (vision/action/sound) into latents (tokens). + 2. Sampling a training timestep (t) for each modality and constructing noised latents (xt) + per the rectified-flow formulation. + 3. Packing text + generation tokens into a single sequence and running the MoT network to predict + the flow field velocity at the given t. + 4. Computing flow-matching loss (plus optional auxiliary load-balancing losses). + + Args: + data_batch (dict): raw data batch draw from the training data loader. + iteration (int): Current iteration number. + + Returns: + tuple: A tuple containing two elements: + - dict: additional data that used to debug / logging / callbacks + - Tensor: The computed loss for the training step as a PyTorch Tensor. + + """ + if self.parallel_dims is None or self.parallel_dims.cp_rank == 0: + self._update_train_stats(data_batch) + + # Load, apply dropout, and tokenize input captions + input_text_indexes = self._load_and_tokenize_text_data(data_batch, iteration) + + # Build sequence plans if not present. SequencePlan has the conditioning information. + sequence_plans = build_sequence_plans_from_data_batch( + data_batch=data_batch, + input_video_key=self.input_video_key, + input_image_key=self.input_image_key, + ) + + # Get data from raw data batch and tokenize into corresponding tokens for *generation* task + # The unnoised, tokenized data for the generation task. + gen_data_clean = self.get_data_and_condition(data_batch, iteration=iteration) + + gen_data_clean, memory_info = self.memory_init_training(gen_data_clean, data_batch, input_text_indexes) + + # Compute resolution per sample for per-sample shift lookup + # image_size[i] may be (1, 4) from IterativeJointDataLoader or (4,) from custom_collate_fn. + if "image_size" in data_batch: + data_resolutions = [] + for i in range(gen_data_clean.batch_size): + img_size = data_batch["image_size"][i] + if img_size.dim() == 2: + img_size = img_size[0] + target_h = int(img_size[0].item()) + target_w = int(img_size[1].item()) + data_resolutions.append(get_vision_data_resolution((target_h, target_w))) + else: + data_resolutions = None + + # Calculate number of tokens per sample (before 2x2 merge) for dynamic shift + # gen_data_clean.x0_tokens_vision: B, C, T, H, W + assert all(x.shape[0] == 1 for x in gen_data_clean.x0_tokens_vision), ( + "Batch size must be 1 for individual samples" + ) + num_tokens_per_sample = [x.shape[2] * x.shape[3] * x.shape[4] for x in gen_data_clean.x0_tokens_vision] + + # Sample a random noise level (sigma) and corresponding interpolation coefficient ("timesteps" in RF) + # Apply shift per sample based on each sample's resolution + num_vision_latent_frames = [x.shape[2] for x in gen_data_clean.x0_tokens_vision] + timesteps_vision, sigmas_vision = self._get_train_noise_level_vision( + batch_size=gen_data_clean.batch_size, + is_image_batch=gen_data_clean.is_image_batch, + resolutions=data_resolutions, + num_vision_latent_frames=num_vision_latent_frames, + num_tokens=num_tokens_per_sample, + iteration=iteration, + ) # [B, T_vis] each + + # Optional independent action schedule (sampled from rectified_flow_action with + # action-specific shift/high-sigma overrides). Only active when the config opts in and + # the batch contains action data. + # + # Mixed-batch indexing: gen_data_clean.x0_tokens_action (and every packed_sequence.action.* + # field) is *dense* — one entry per sample with has_action=True, in the original batch order + # but skipping non-action samples. To feed each dense action entry its sample's sigma, we + # sample σ for the full batch and reindex with action_sample_indices (the batch positions + # of action-bearing samples). This avoids the mismatch that happens when, e.g., batch + # sample 1 has action but the dense entry 0 would otherwise read σ from batch position 0. + rf_cfg = self.config.rectified_flow_training_config + action_sample_indices = [i for i, plan in enumerate(sequence_plans) if plan.has_action] + if rf_cfg.independent_action_schedule and action_sample_indices: + ts_full, sg_full = self._get_train_noise_level_action( + batch_size=gen_data_clean.batch_size, iteration=iteration + ) # [B, 1] each + idx = torch.tensor(action_sample_indices, dtype=torch.long) # [n_action] + timesteps_action = ts_full[idx] # [n_action, 1] + sigmas_action = sg_full[idx] # [n_action, 1] + else: + timesteps_action, sigmas_action = (None, None) + + # Optional independent sound schedule: sample a scalar sound sigma per batch + # slot, then reindex to the dense audio-bearing subset. + sound_sample_indices = [i for i, plan in enumerate(sequence_plans) if getattr(plan, "has_sound", False)] + if getattr(rf_cfg, "independent_sound_schedule", False) and sound_sample_indices: + ts_sound_full, sg_sound_full = self._get_train_noise_level_sound( + batch_size=gen_data_clean.batch_size + ) # [B,1] each + timesteps_sound, sigmas_sound = build_dense_sound_schedule( + sequence_plans, + gen_data_clean.x0_tokens_sound, + ts_sound_full, + sg_sound_full, + ) # [n_sound,1], [n_sound,1] + else: + timesteps_sound, sigmas_sound = (None, None) + + # Broadcast timesteps/sigmas across CP group to ensure consistency + if self.parallel_dims is not None and self.parallel_dims.cp_enabled: + src_rank = 0 # use cp rank 0 to broadcast timesteps/sigmas + cp_group = self.parallel_dims.cp_mesh.get_group() + global_src_rank = torch.distributed.get_global_rank(cp_group, src_rank) + timesteps_vision = timesteps_vision.contiguous() + sigmas_vision = sigmas_vision.contiguous() + torch.distributed.broadcast(timesteps_vision, src=global_src_rank, group=cp_group) + torch.distributed.broadcast(sigmas_vision, src=global_src_rank, group=cp_group) + if sigmas_action is not None: + timesteps_action = timesteps_action.contiguous() + sigmas_action = sigmas_action.contiguous() + torch.distributed.broadcast(timesteps_action, src=global_src_rank, group=cp_group) + torch.distributed.broadcast(sigmas_action, src=global_src_rank, group=cp_group) + if sigmas_sound is not None: + timesteps_sound = timesteps_sound.contiguous() # [n_sound,1] + sigmas_sound = sigmas_sound.contiguous() # [n_sound,1] + torch.distributed.broadcast(timesteps_sound, src=global_src_rank, group=cp_group) + torch.distributed.broadcast(sigmas_sound, src=global_src_rank, group=cp_group) + + if timesteps_sound is None: + # Sound tensors are dense over audio-bearing samples, while the vision timestep/sigma schedule + # is indexed by original batch position. Reindex here so mixed audio/no-audio batches use each + # sound sample's own schedule for noising and loss weighting. + timesteps_sound, sigmas_sound = build_dense_sound_schedule( + sequence_plans, + gen_data_clean.x0_tokens_sound, + timesteps_vision, + sigmas_vision, + ) # [n_sound,T_vis] or None, [n_sound,T_vis] or None + + packed_sequence = self._pack_input_sequence( + sequence_plans, + input_text_indexes, + gen_data_clean, + timesteps_vision.cpu(), + skip_text_tokens=memory_info["skip_text"], + initial_mrope_temporal_offset=memory_info["initial_temporal_offset"], + ) + + # Under independent_action_schedule, overwrite the vision-based action timestep the + # packer injected with the action timestep, so the denoiser's action timestep embedding + # matches the sigma used to noise action tokens. + if timesteps_action is not None and packed_sequence.action is not None: + action_has_noisy_tokens = any(nfi.numel() > 0 for nfi in packed_sequence.action.noisy_frame_indexes) + if action_has_noisy_tokens: + sample_ts = timesteps_action.squeeze(1).cpu() # [n_action] + packed_sequence.action.timesteps = torch.cat( + [ + sample_ts[i : i + 1].expand(nfi.numel()) + for i, nfi in enumerate(packed_sequence.action.noisy_frame_indexes) + ] + ).to(dtype=torch.float32) # [N_action_noisy] + else: + timesteps_action, sigmas_action = (None, None) + + # Under independent_sound_schedule, overwrite the vision-based sound timestep the packer + # injected with the sound timestep, so the denoiser's sound timestep embedding matches + # the sigma used to noise sound tokens. + if ( + getattr(rf_cfg, "independent_sound_schedule", False) + and timesteps_sound is not None + and packed_sequence.sound is not None + ): + sound_has_noisy_tokens = any(nfi.numel() > 0 for nfi in packed_sequence.sound.noisy_frame_indexes) + if sound_has_noisy_tokens: + sample_ts = timesteps_sound.squeeze(1).cpu() # [n_sound] + packed_sequence.sound.timesteps = torch.cat( + [ + sample_ts[i : i + 1].expand(nfi.numel()) + for i, nfi in enumerate(packed_sequence.sound.noisy_frame_indexes) + ] + ).to(dtype=torch.float32) # [N_sound_noisy] + else: + timesteps_sound, sigmas_sound = (None, None) + + # For image editing (multi-item vision), expand per-sample timesteps/sigmas to + # per-vision-item so downstream noise/loss indexing matches the flat x0_tokens_vision + # list. No-op when num_vision_items_per_sample is None (standard T2I/T2V/policy cases). + # Conditioning items get sigma=0 via their condition_mask, so the actual timestep value + # for them does not matter. + timesteps_vision = _expand_per_sample_to_per_vision_item( + timesteps_vision, gen_data_clean.num_vision_items_per_sample + ) # [B_items, T_vis] + sigmas_vision = _expand_per_sample_to_per_vision_item( + sigmas_vision, gen_data_clean.num_vision_items_per_sample + ) # [B_items, T_vis] + + memory_info = self.pre_noise_memory_hook(packed_sequence, gen_data_clean, memory_info) + + # Flow matching/diffusion forward process: noise the input signal with the sampled noise level + gen_data_noised = self._add_noise_to_input( + gen_data_clean, + packed_sequence, + sigmas_vision, + sigmas_action=sigmas_action, + sigmas_sound=sigmas_sound, + ) + self._replace_clean_with_noised(packed_sequence, gen_data_noised) + + # Move packed sequence to CUDA + packed_sequence.to_cuda() + + # Network forward pass + memory = self.build_memory_state(packed_sequence, memory_info) # pylint: disable=assignment-from-none + out_net = self.denoise( + data_batch_packed=packed_sequence, + fps_vision=gen_data_clean.fps_vision, + fps_action=gen_data_clean.fps_action, + fps_sound=gen_data_clean.fps_sound, + memory=memory, + ) + + loss, losses_dict = self._compute_losses( + out_net=out_net, + data_batch_packed=packed_sequence, + gen_data_noised=gen_data_noised, + timesteps=timesteps_vision, + is_image_batch=gen_data_clean.is_image_batch, + timesteps_action=timesteps_action, + timesteps_sound=timesteps_sound, + ) + + # Pixel-space video shapes for VAE FLOPs estimation in callbacks (e.g. MFU). + _vae_pixel_shapes: list[tuple[int, int, int]] = [] + if gen_data_clean.raw_state_vision is not None: + for _v in gen_data_clean.raw_state_vision: + if _v is not None: + assert _v.dim() in [4, 5], ( + "Currently only [C, T, H, W] and [B, C, T, H, W] formats are supported for the VAE encoding." + ) + t_h_w = ( + (int(_v.shape[2]), int(_v.shape[3]), int(_v.shape[4])) + if _v.dim() == 5 + else (int(_v.shape[1]), int(_v.shape[2]), int(_v.shape[3])) + ) + _vae_pixel_shapes.append(t_h_w) + + _vision_tokens = len(packed_sequence.vision.sequence_indexes) if packed_sequence.vision else 0 + _action_tokens = len(packed_sequence.action.sequence_indexes) if packed_sequence.action else 0 + _sound_tokens = len(packed_sequence.sound.sequence_indexes) if packed_sequence.sound else 0 + + output_batch = { + "x0": gen_data_clean.x0_tokens_vision, + "xt": gen_data_noised.xt_tokens_vision, + "sigma": sigmas_vision, # [B_items, T_vis] + "model_pred": out_net["preds_vision"], + "condition_mask_vision": packed_sequence.vision.condition_mask if packed_sequence.vision else None, + "condition_mask_action": packed_sequence.action.condition_mask if packed_sequence.action else None, + "und_token_length": packed_sequence.text_indexes.shape[0], + "gen_token_length": packed_sequence.sequence_length - packed_sequence.text_indexes.shape[0], + "vision_token_length": _vision_tokens, + "action_token_length": _action_tokens, + "sound_token_length": _sound_tokens, + "is_image_batch": gen_data_clean.is_image_batch, + "batch_size": gen_data_clean.batch_size, + "split_lens": packed_sequence.split_lens, + "attn_modes": packed_sequence.attn_modes, + "vae_pixel_shapes": _vae_pixel_shapes, + **losses_dict, + } + if sigmas_action is not None: + output_batch["sigma_action"] = sigmas_action # [n_action, 1] — dense over action-bearing samples + if getattr(rf_cfg, "independent_sound_schedule", False) and sigmas_sound is not None: + output_batch["sigma_sound"] = sigmas_sound # [n_sound, 1] — dense over sound-bearing samples + + return output_batch, loss + + def _compute_flow_matching_loss( + self, + pred: list[torch.Tensor], + target: list[torch.Tensor], + condition_mask: list[torch.Tensor], + timesteps: torch.Tensor, + has_valid_tokens: bool, + rectified_flow: RectifiedFlow, + loss_scale: float | None = None, + raw_action_dim: list[torch.Tensor] | None = None, + normalize_by_active: bool = False, + ) -> torch.Tensor: + """Compute flow matching loss for a modality. + + Args: + pred: Predicted velocity field (list of tensors, one per sample). + target: Target velocity field (list of tensors, one per sample). + Under rectified flow the target is ``v = eps - x0``. + condition_mask: Mask where 1 = clean/conditioning, 0 = noisy/generation (list of tensors). + timesteps: Diffusion timesteps for time weighting. Shape [B,1] for + base/teacher_forcing (all frames share one timestep) or [B,T_max] + for diffusion_forcing (per-frame independent timesteps). Time weights + are applied per-frame before averaging, so non-uniform weight functions + are handled correctly. + has_valid_tokens: Whether this modality has valid noisy tokens. + rectified_flow: The rectified flow object for time weighting. + loss_scale: Optional per-modality loss scale. Falls back to the global + ``rectified_flow_training_config.loss_scale`` when *None*. + normalize_by_active: When True, normalize per-instance loss by the count of + active (noisy) elements rather than all elements. Preserves the + ``sum / active_count`` semantics needed for distillation critics where + conditioned frames contribute no signal and should not dilute the + denominator. + + Returns: + tuple: A tuple containing two elements: + - Flow matching loss (or dummy loss for gradient consistency). + - Per-instance loss (or dummy loss for gradient consistency). + """ + return compute_flow_matching_loss( + pred=pred, + target=target, + condition_mask=condition_mask, + timesteps=timesteps, + has_valid_tokens=has_valid_tokens, + rectified_flow=rectified_flow, + tensor_kwargs_fp32=self.tensor_kwargs_fp32, + loss_scale=loss_scale, + raw_action_dim=raw_action_dim, + normalize_by_active=normalize_by_active, + ) + + def _compute_losses( + self, + out_net: dict, + data_batch_packed: PackedSequence, + gen_data_noised: GenerationDataNoised, + timesteps: torch.Tensor, + is_image_batch: bool, + timesteps_action: torch.Tensor | None = None, + timesteps_sound: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + """Compute flow matching loss and auxiliary load balancing losses. + + ``timesteps_action`` is an optional ``[n_action, 1]`` override for the action loss + time-weighting — dense over action-bearing samples, matching ``data_batch_packed.action.*``. + When None, action reuses ``timesteps`` (vision timesteps, legacy behavior). Set by + ``training_step`` under ``independent_action_schedule=True``. + + ``timesteps_sound`` is an optional dense sound timestep tensor, matching + ``data_batch_packed.sound.*``. When None, sound reuses ``timesteps``. + """ + total_loss = 0.0 + losses_dict = {} + # ts_action shape: vision fallback [B_items, T_vis] (legacy) or [n_action, 1] (independent). + ts_action = timesteps if timesteps_action is None else timesteps_action # [B_items,T_vis] or [n_action,1] + # ts_sound shape: vision fallback [B_items,T_vis] or dense sound schedule [n_sound,...]. + ts_sound = timesteps if timesteps_sound is None else timesteps_sound # [B_items,T_vis] or [n_sound,...] + + rf_cfg = self.config.rectified_flow_training_config + normalize_by_active = rf_cfg.normalize_loss_by_active + if self.config.vision_gen: + assert data_batch_packed.vision is not None, "Vision packed data required when vision_gen is True" + assert isinstance(data_batch_packed.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for loss computation" + ) + rectified_flow_vision = self.rectified_flow_image if is_image_batch else self.rectified_flow_video + + fm_loss_vision, fm_loss_vision_per_instance = self._compute_flow_matching_loss( + pred=out_net["preds_vision"], + target=gen_data_noised.vt_target_vision, + condition_mask=data_batch_packed.vision.condition_mask, + timesteps=timesteps, + has_valid_tokens=has_noisy_tokens(data_batch_packed.vision), + rectified_flow=rectified_flow_vision, + normalize_by_active=normalize_by_active, + ) + loss_scale = ( + rf_cfg.image_loss_scale if is_image_batch and rf_cfg.image_loss_scale is not None else rf_cfg.loss_scale + ) + total_loss += fm_loss_vision * loss_scale + losses_dict["flow_matching_loss_vision"] = fm_loss_vision + losses_dict["flow_matching_loss_vision_per_instance"] = fm_loss_vision_per_instance + else: + losses_dict["flow_matching_loss_vision"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + if self.config.action_gen: + if data_batch_packed.action is not None: + assert isinstance(data_batch_packed.action.condition_mask, list), ( + "Action condition mask must be a list of tensors for loss computation" + ) + assert gen_data_noised.vt_target_action is not None, "Action targets required when action_gen is True" + fm_loss_action, _ = self._compute_flow_matching_loss( + pred=out_net["preds_action"], + target=gen_data_noised.vt_target_action, + condition_mask=data_batch_packed.action.condition_mask, + timesteps=ts_action, + has_valid_tokens=has_noisy_tokens(data_batch_packed.action), + rectified_flow=self.rectified_flow_action, + raw_action_dim=data_batch_packed.action.raw_action_dim, + normalize_by_active=normalize_by_active, + ) + + # Yihuai: In case the video loss is too large (1.5) and covers the action loss (0.05), we scale up the action loss to match the video loss to improve action precision. + total_loss += fm_loss_action * rf_cfg.action_loss_weight + losses_dict["flow_matching_loss_action"] = fm_loss_action + else: + # No action data in this batch. Connect the network's dummy preds_action + # to the loss so action-specific params + # (llm2action, action2llm, action_modality_embed) stay in the backward + # graph. Without this, FSDP reduce-scatter / DDP all-reduce will hang + # when other ranks do have action data. + dummy_loss = 0.0 * sum(p.sum() for p in out_net["preds_action"]) + total_loss += dummy_loss + losses_dict["flow_matching_loss_action"] = dummy_loss + else: + losses_dict["flow_matching_loss_action"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + if self.config.sound_gen: + if data_batch_packed.sound is not None: + assert isinstance(data_batch_packed.sound.condition_mask, list), ( + "Sound condition mask must be a list of tensors for loss computation" + ) + assert gen_data_noised.vt_target_sound is not None, "Sound targets required when sound_gen is True" + # Sound preds/targets are (C, T); condition_mask is (T, 1) — transpose to (1, T) for broadcasting + fm_loss_sound, _ = self._compute_flow_matching_loss( + pred=out_net["preds_sound"], + target=gen_data_noised.vt_target_sound, + condition_mask=[m.T for m in data_batch_packed.sound.condition_mask], + timesteps=ts_sound, + has_valid_tokens=has_noisy_tokens(data_batch_packed.sound), + rectified_flow=self.rectified_flow_sound, + normalize_by_active=normalize_by_active, + ) + loss_scale = rf_cfg.sound_loss_scale if rf_cfg.sound_loss_scale is not None else rf_cfg.loss_scale + total_loss += fm_loss_sound * loss_scale + losses_dict["flow_matching_loss_sound"] = fm_loss_sound + else: + # No sound data in this batch. Connect the network's dummy preds_sound + # to the loss so sound-specific params (sound2llm, llm2sound, + # sound_modality_embed) stay in the backward graph. Without this, + # FSDP gradient reduce hangs when other ranks do have sound data. + dummy_loss = 0.0 * sum(p.sum() for p in out_net["preds_sound"]) + total_loss += dummy_loss + losses_dict["flow_matching_loss_sound"] = dummy_loss + else: + losses_dict["flow_matching_loss_sound"] = torch.tensor(0.0, **self.tensor_kwargs_fp32) + + # 2. Load balancing auxiliary losses + for load_balancing_type in ["und", "gen"]: + lbl_metadata = out_net.get(f"lbl_metadata_{load_balancing_type}", None) + if lbl_metadata is None: + continue + load_balancing_loss = compute_load_balancing_loss( + lbl_metadata, + coeff=getattr(self.config.lbl, f"coeff_{load_balancing_type}"), + method=self.config.lbl.method, + device_mesh=self.parallel_dims.dp_mesh if self.parallel_dims else None, + ) + if load_balancing_loss is not None: + total_loss += load_balancing_loss + losses_dict[f"aux_loss_{load_balancing_type}"] = load_balancing_loss + + return total_loss, losses_dict + + def _update_train_stats(self, data_batch: dict[str, torch.Tensor]) -> None: + is_image = self.is_image_batch(data_batch) + input_key = self.input_image_key if is_image else self.input_video_key + if isinstance(self.net, WeightTrainingStat): + val = data_batch[input_key] + # For image editing data_batch[input_key] is a list-of-lists, not a tensor. + sample_count = len(val) if isinstance(val, list) else val.shape[0] + if is_image: + self.net.accum_image_sample_counter += sample_count + else: + self.net.accum_video_sample_counter += sample_count + + def _load_and_tokenize_text_data(self, data_batch: dict[str, torch.Tensor], iteration: int) -> list[list[int]]: + """ + Load and tokenize the text data from the data batch. + + Args: + data_batch (dict[str, torch.Tensor]): The data batch. + iteration (int): The current iteration number. + + Returns: + list[torch.Tensor]: The input text tokens. + """ + input_text_indexes = [] + + input_captions = data_batch[self.input_caption_key] + input_text_tokens = data_batch["text_token_ids"] + if isinstance(input_text_tokens, list): + # Convert text tokens to list of lists of ints + input_text_tokens = [tokens.tolist() for x in input_text_tokens for tokens in x] + else: + input_text_tokens = [tokens.squeeze(0).tolist() for tokens in input_text_tokens] + + return input_text_tokens + + def _get_train_noise_level_vision( + self, + batch_size: int, + is_image_batch: bool, + num_vision_latent_frames: list[int], + resolutions: list[str] | str | None = None, + num_tokens: list[int] | None = None, + iteration: int | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Sample the rectified flow interpolation coefficient (timesteps), optionally adjust the sampled + timesteps with high sigma strategy, and obtain the corresponding normalized timestep. + + Args: + batch_size: Batch size for sampling timesteps. + is_image_batch: Whether this is an image batch (vs video). + num_vision_latent_frames: Per-sample vision latent frame counts [T_0, ..., T_{B-1}]. + For causal_training_strategy="diffusion_forcing", resamples B*T_max independent + times and returns tensors of shape [B,T_max]. For base/TF strategies, ignored — + returns shape [B,1] (all frames share the same sigma). + resolutions: Resolution string(s) (e.g., "256", "512") for dict-based shift lookup. + Can be a single string (applied to all samples) or a list of strings (one per sample). + If None, defaults to self.config.resolution (can be used for other modalities). + num_tokens: Number of tokens for each sample (before 2x2 merge). Needed for dynamic shift. + + Returns: + (timesteps, sigmas): Both [B,1] for TF/base, or [B,T_max] for diffusion_forcing. + """ + + + rectified_flow = self.rectified_flow_image if is_image_batch else self.rectified_flow_video + + assert not self.config.rectified_flow_training_config.use_discrete_rf, ( + "Discrete RF is not supported for Cosmos3" + ) + # Continuous RF implementation + max_timestep = rectified_flow.noise_scheduler.config.num_train_timesteps + + # Get shift value(s) - support both int and dict-based resolution lookup + shift_config = self.config.rectified_flow_training_config.shift + if isinstance(shift_config, int): + # Int-based shift: use directly for all samples + shifts = torch.full((batch_size,), shift_config, dtype=torch.float32) + else: + # Convert to plain dict to avoid traceback-based memory leaks when GC is disabled + # (OmegaConf's `in` operator uses exception control flow internally). + shift_dict = dict(shift_config) + if not is_image_batch and "dynamic_shift_base_num_tokens_video" in shift_dict: + # Dynamic shift based on token count + assert num_tokens is not None and len(num_tokens) == batch_size + base_num_tokens = shift_dict["dynamic_shift_base_num_tokens_video"] + shifts = torch.sqrt(torch.tensor(num_tokens, dtype=torch.float32) / base_num_tokens) + elif is_image_batch and "dynamic_shift_base_num_tokens_image" in shift_dict: + assert num_tokens is not None and len(num_tokens) == batch_size + base_num_tokens = shift_dict["dynamic_shift_base_num_tokens_image"] + shifts = torch.sqrt(torch.tensor(num_tokens, dtype=torch.float32) / base_num_tokens) + else: + # Dict-based shift: lookup per sample + if resolutions is None: + raise ValueError("Resolutions must be provided when shift is a dict") + + # Normalize to list format + if isinstance(resolutions, str): + resolutions = [resolutions] * batch_size + + assert len(resolutions) == batch_size, ( + f"Number of resolutions ({len(resolutions)}) must match batch_size ({batch_size})" + ) + + # Lookup shift per sample + shifts_list = [] + for resolution in resolutions: + if resolution not in shift_dict: + raise ValueError( + f"Resolution '{resolution}' not found in shift dict. Available resolutions: {list(shift_dict.keys())}" + ) + shifts_list.append(shift_dict[resolution]) + shifts = torch.tensor(shifts_list, dtype=torch.float32) + + # Sample noise times: B×T_max for DF (one per video latent frame), B×1 for base/TF + if self.config.causal_training_strategy == "diffusion_forcing": + # T_max = max(num_vision_latent_frames) across the batch; trailing entries for shorter + # sequences are unused (sliced away in _add_noise_to_input). + T_max = max(num_vision_latent_frames) + t_raw = ( + rectified_flow.sample_train_time(batch_size * T_max, iteration=iteration) + .to(**self.tensor_kwargs_fp32) + .reshape(batch_size, T_max) + ) # [B,T_max] + else: + t_raw = ( + rectified_flow.sample_train_time(batch_size, iteration=iteration) + .to(**self.tensor_kwargs_fp32) + .unsqueeze(1) + ) # [B,1] + + # Apply shift and scale: t_raw ∈ [0,1] → timesteps ∈ [0,max_timestep] + # shifts.unsqueeze(1) → [B,1], broadcasts with both [B,1] (base/TF) and [B,T_max] (DF) + t = 1 - t_raw # [B,1] or [B,T_max] + shifts_2d = shifts.unsqueeze(1).to(t_raw.device) # [B,1], broadcasts with [B,1] and [B,T_max] + timesteps = shifts_2d * t / (1 + (shifts_2d - 1) * t) * max_timestep # [B,1] or [B,T_max] + + if self.config.rectified_flow_training_config.use_high_sigma_strategy: + timesteps = self._apply_high_noise_strategy(timesteps, max_timestep) # [B,1] or [B,T_max] + + sigmas = timesteps / max_timestep # [B,1] for base/TF, [B,T_max] for DF + return timesteps, sigmas + + def _apply_high_noise_strategy(self, timesteps: torch.Tensor, max_timestep: int) -> torch.Tensor: + """ + Update the sampled RF timesteps to shift the distribution towards higher noise levels (high sigmas). + + Args: + timesteps (torch.Tensor): Input timesteps. Shape [B,1] for base/TF or [B,T_max] for DF. + max_timestep (int): The maximum timestep value. + + Returns: + torch.Tensor: Timesteps with the same shape as input — [B,1] or [B,T_max]. + """ + mask = ( + torch.rand(timesteps.shape, device=timesteps.device) + < self.config.rectified_flow_training_config.high_sigma_ratio + ) + new_timesteps = ( + torch.rand(timesteps.shape, device=timesteps.device).type_as(timesteps) + * ( + self.config.rectified_flow_training_config.high_sigma_timesteps_max + - self.config.rectified_flow_training_config.high_sigma_timesteps_min + ) + + self.config.rectified_flow_training_config.high_sigma_timesteps_min + ) + timesteps = torch.where(mask, new_timesteps, timesteps) + + return timesteps + + def _get_train_noise_level_action( + self, batch_size: int, iteration: int | None = None + ) -> tuple[torch.Tensor, torch.Tensor]: + """Sample ``(timesteps, sigmas)`` of shape ``[batch_size, 1]`` from ``rectified_flow_action``. + + This helper is locally-scoped: it just draws ``batch_size`` independent σ values and + applies action-specific shift / high-sigma config. The caller decides what ``batch_size`` + means semantically — ``training_step`` passes the full batch size and then reindexes to + the dense action-bearing subset with ``action_sample_indices``. + + ``shift_action`` must be an int (or ``None`` to inherit ``shift``). Dict-keyed + per-resolution shifts are vision-only — multi-resolution action training would need + per-sample lookup, which this helper does not implement; if the global ``shift`` is a + dict and ``shift_action`` is None, this raises so the user sets shift_action explicitly. + ``use_high_sigma_strategy_action`` toggles the high-σ strategy for action; when on, the + global ``high_sigma_ratio`` / ``_min`` / ``_max`` apply. σ is a shared scalar per input + slot (no per-frame σ for action). + """ + rf_cfg = self.config.rectified_flow_training_config + rf = self.rectified_flow_action + max_timestep = rf.noise_scheduler.config.num_train_timesteps # int + + # Resolve shift. shift_action, when provided, must be an int. + if rf_cfg.shift_action is not None: + if not isinstance(rf_cfg.shift_action, int): + raise ValueError( + f"shift_action must be an int; got {type(rf_cfg.shift_action).__name__}. " + "Dict-keyed per-resolution shifts are vision-only." + ) + shift_val = rf_cfg.shift_action # int + elif isinstance(rf_cfg.shift, int): + shift_val = rf_cfg.shift # inherit the global int shift + else: + raise ValueError( + "shift_action=None requires the global `shift` to be an int. When `shift` is a " + f"dict (multi-resolution vision training), set shift_action explicitly as an int. " + f"Got shift={rf_cfg.shift!r}." + ) + + t_raw = ( + rf.sample_train_time(batch_size, iteration=iteration).to(**self.tensor_kwargs_fp32).unsqueeze(1) + ) # [B,1] + t = 1 - t_raw # [B,1] + shifts_2d = torch.full((batch_size, 1), shift_val, dtype=torch.float32, device=t_raw.device) # [B,1] + timesteps = shifts_2d * t / (1 + (shifts_2d - 1) * t) * max_timestep # [B,1] + + if rf_cfg.use_high_sigma_strategy_action: + timesteps = self._apply_high_noise_strategy(timesteps, max_timestep) # [B,1] + + sigmas = timesteps / max_timestep # [B,1] + return timesteps, sigmas + + def _get_train_noise_level_sound(self, batch_size: int) -> tuple[torch.Tensor, torch.Tensor]: + """Sample ``(timesteps, sigmas)`` of shape ``[batch_size, 1]`` from ``rectified_flow_sound``. + + Sound uses a shared scalar sigma per audio-bearing sample, then training_step + reindexes the full-batch samples to the dense sound tensor list. + """ + rf_cfg = self.config.rectified_flow_training_config + rf = self.rectified_flow_sound + max_timestep = rf.noise_scheduler.config.num_train_timesteps # int + + # Resolve shift. shift_sound, when provided, must be an int. + if rf_cfg.shift_sound is not None: + if not isinstance(rf_cfg.shift_sound, int): + raise ValueError( + f"shift_sound must be an int; got {type(rf_cfg.shift_sound).__name__}. " + "Dict-keyed per-resolution shifts are vision-only." + ) + shift_val = rf_cfg.shift_sound # int + elif isinstance(rf_cfg.shift, int): + shift_val = rf_cfg.shift # inherit the global int shift + else: + raise ValueError( + "shift_sound=None requires the global `shift` to be an int. When `shift` is a " + f"dict (multi-resolution vision training), set shift_sound explicitly as an int. " + f"Got shift={rf_cfg.shift!r}." + ) + + t_raw = rf.sample_train_time(batch_size).to(**self.tensor_kwargs_fp32).unsqueeze(1) # [B,1] + t = 1 - t_raw # [B,1] + shifts_2d = torch.full((batch_size, 1), shift_val, dtype=torch.float32, device=t_raw.device) # [B,1] + timesteps = shifts_2d * t / (1 + (shifts_2d - 1) * t) * max_timestep # [B,1] + + if rf_cfg.use_high_sigma_strategy_sound: + timesteps = self._apply_high_noise_strategy(timesteps, max_timestep) # [B,1] + + sigmas = timesteps / max_timestep # [B,1] + return timesteps, sigmas + + def _add_noise_to_input( + self, + gen_data_clean: GenerationDataClean, + packed_sequence: PackedSequence, + sigmas: torch.Tensor, + sigmas_action: torch.Tensor | None = None, + sigmas_sound: torch.Tensor | None = None, + ) -> GenerationDataNoised: + """ + Diffusion / Flow matching forward process: apply noise of given noise level (sigmas) to input data. + + Args: + gen_data_clean (GenerationDataClean): The input dataclass containing the clean data *latents* (tokens). + packed_sequence (PackedSequence): Packed sequence with condition masks attached to modalities. + sigmas (torch.Tensor): The noise levels. Shape [B,1] for base/teacher_forcing (all video + latent frames share the same sigma) or [B,T_max] for diffusion_forcing (per-latent-frame + independent sigma). T_max is the number of video latent frames (temporally compressed + tokens), not RGB frames. In all modes, sigmas are multiplied by (1 - condition_mask) + so conditioning latent frames get sigma_eff=0 and only non-conditioned frames contribute + to the loss. + sigmas_action: Optional ``[n_action, 1]`` override for action noising — dense over + action-bearing samples, matching ``packed_sequence.action.*``. When None, action + reuses ``sigmas`` (vision σ, legacy behavior). Set by ``training_step`` when + ``independent_action_schedule=True``. + sigmas_sound: Optional dense sound sigma tensor matching ``packed_sequence.sound.*``. + When None, sound reuses ``sigmas``. + + Returns: + GenerationDataNoised: A dataclass containing the noise, noisy data (xt), and velocity field (vt). + """ + # Action sigma defaults to the shared vision sigma (legacy behavior). + # Legacy (sigmas_action=None): vision σ of shape [B_items, T_vis]. + # Independent (sigmas_action provided): dense action σ of shape [n_action, 1]. + sigmas_for_action = sigmas if sigmas_action is None else sigmas_action # [B_items,T_vis] or [n_action,1] + # Sound uses a dense view of the per-sample vision schedule so mixed audio/no-audio + # batches do not index full-batch sigmas with dense sound positions. + sigmas_for_sound = sigmas if sigmas_sound is None else sigmas_sound # [B_items,T_vis] or [n_sound,...] + # Vision + x0_vision = gen_data_clean.x0_tokens_vision # list of [C,T,H,W] + epsilon_vision = [ + torch.randn(x0_vision_i.size()).to(**self.tensor_kwargs_fp32) for x0_vision_i in x0_vision + ] # list of [C,T,H,W] + + # Derive noisy mask (1 for noised, 0 for clean) for sigmas computation + assert packed_sequence.vision is not None, "Packed vision data required for noise scheduling" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for noise scheduling" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for noise scheduling" + ) + + # Compute sigmas per vision item (supports variable shapes). + # For image editing, x0_tokens_vision is a flat list with multiple items per sample + # and sigmas has already been expanded to match (see _expand_per_sample_to_per_vision_item). + # Conditioning latent frames are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # view(-1,1,1)[:T_latent]: for base/TF sigmas[i] is (1,), view gives (1,1,1) and the slice is a no-op; + # for DF sigmas[i] is (T_max,) — one sigma per video latent frame — view gives (T_max,1,1) + # and [:T_latent] slices to (T_latent,1,1) matching the per-item latent frame count. + num_vision_items = len(packed_sequence.vision.condition_mask) + noisy_mask_vision = [1.0 - cond_mask for cond_mask in packed_sequence.vision.condition_mask] + sigmas_vision = [ + sigmas[i].view(-1, 1, 1)[: x0_vision[i].shape[2]] * noisy_mask_vision[i] for i in range(num_vision_items) + ] + rectified_flow_vision = ( + self.rectified_flow_image if gen_data_clean.is_image_batch else self.rectified_flow_video + ) + xt_vision, vt_vision = rectified_flow_vision.get_interpolation( + epsilon_vision, x0_vision, sigmas_vision + ) # list of [C,T,H,W], list of [C,T,H,W] + + xt_vision = [ + xt_vision_i.to(**self.tensor_kwargs) for xt_vision_i in xt_vision + ] # list of [C,T,H,W]; to make tensor compatible with the precision of the model + + # Action (x0_tokens_action is already a dense list with no None entries). + # Gate on action_gen: the dataset may emit action tensors for models that + # don't consume them (e.g. camera dataset on a vision-only config), in + # which case packed_sequence.action is None and we must skip this block. + x0_action = gen_data_clean.x0_tokens_action # list of [T,action_dim] + if self.config.action_gen and x0_action is not None and len(x0_action) > 0: + assert packed_sequence.action is not None, "Packed action data required when action tokens exist" + assert packed_sequence.action.condition_mask is not None, ( + "Action condition mask required when action tokens exist" + ) + action_batch_size = len(packed_sequence.action.condition_mask) + all_actions_are_conditioning = all( + torch.all(condition_mask == 1).item() for condition_mask in packed_sequence.action.condition_mask + ) + if all_actions_are_conditioning: + epsilon_action = [ + torch.zeros(x0_action_i.size(), **self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + sigmas_action = [ + torch.zeros_like(condition_mask, dtype=torch.float32, device=condition_mask.device) + for condition_mask in packed_sequence.action.condition_mask + ] # list of [T,1] + xt_action = [ + x0_action_i.to(**self.tensor_kwargs) for x0_action_i in x0_action + ] # list of [T,action_dim] + vt_action = [ + torch.zeros(x0_action_i.size(), **self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + else: + epsilon_action = [ + torch.randn(x0_action_i.size()).to(**self.tensor_kwargs_fp32) for x0_action_i in x0_action + ] # list of [T,action_dim] + # Conditioning action timesteps are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # Action timesteps are aligned 1-to-1 with video latent frames, not RGB frames. + # view(-1,1)[:T_i]: for base/TF sigmas[i] is (1,) → (1,1), slice is a no-op; + # for DF sigmas[i] is (T_max,) → (T_max,1) → (T_i,1) per-action-timestep sigmas. + # condition_mask[i] shape [T_i,1]; result broadcasts with x0 shape [T_i,C]. + sigmas_action = [ + sigmas_for_action[i].view(-1, 1)[: x0_action[i].shape[0]] + * (1.0 - packed_sequence.action.condition_mask[i]) + for i in range(action_batch_size) + ] # list of [T_i,1] + xt_action, vt_action = self.rectified_flow_action.get_interpolation( + epsilon_action, x0_action, sigmas_action + ) # list of [T,action_dim], list of [T,action_dim] + xt_action = [ + xt_action_i.to(**self.tensor_kwargs) for xt_action_i in xt_action + ] # list of [T,action_dim]; to make tensor compatible with the precision of the model + for i in range(len(xt_action)): + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + xt_action[i][:, gen_data_clean.raw_action_dim[i] :] = 0 + + else: + epsilon_action = None + sigmas_action = None + xt_action = None + vt_action = None + + # Sound (x0_tokens_sound is a list of [C, T] tensors, or None) + x0_sound = gen_data_clean.x0_tokens_sound # list of [sound_channels,T_sound] + if x0_sound is not None and len(x0_sound) > 0: + assert packed_sequence.sound is not None, "Packed sound data required when sound tokens exist" + assert packed_sequence.sound.condition_mask is not None, ( + "Sound condition mask required when sound tokens exist" + ) + sound_batch_size = len(packed_sequence.sound.condition_mask) + epsilon_sound = [ + torch.randn(x0_i.size()).to(**self.tensor_kwargs_fp32) for x0_i in x0_sound + ] + # Conditioning frames are zeroed via (1 - condition_mask) in all modes (base/TF/DF). + # view(-1,1)[:T_sound].T: for base/TF sigmas[i] is (1,) → (1,1) → no-op → (1,1); + # for DF sigmas[i] is (T_max,) → (T_max,1) → (T_sound,1) → (1,T_sound). + # condition_mask[i] shape [T_sound,1]; .T gives [1,T_sound]; result broadcasts with x0 [C,T_sound]. + sigmas_sound = [ + sigmas_for_sound[i].view(-1, 1)[: x0_sound[i].shape[1]].T + * (1.0 - packed_sequence.sound.condition_mask[i].T) + for i in range(sound_batch_size) + ] + xt_sound, vt_sound = self.rectified_flow_sound.get_interpolation(epsilon_sound, x0_sound, sigmas_sound) + xt_sound = [xt_i.to(**self.tensor_kwargs) for xt_i in xt_sound] + else: + epsilon_sound = None + sigmas_sound = None + xt_sound = None + vt_sound = None + + # create the GenerationDataNoised object + gen_data_noised = GenerationDataNoised( + batch_size=gen_data_clean.batch_size, + # vision + epsilon_vision=epsilon_vision, + xt_tokens_vision=xt_vision, + vt_target_vision=vt_vision, + sigmas_vision=sigmas_vision, + # action + epsilon_action=epsilon_action, + xt_tokens_action=xt_action, + vt_target_action=vt_action, + sigmas_action=sigmas_action, + raw_action_dim=gen_data_clean.raw_action_dim, + # sound + epsilon_sound=epsilon_sound, + xt_tokens_sound=xt_sound, + vt_target_sound=vt_sound, + sigmas_sound=sigmas_sound, + ) + + return gen_data_noised + + def _replace_clean_with_noised( + self, + packed_sequence: PackedSequence, + gen_data_noised: GenerationDataNoised, + ) -> None: + """Replace packed clean tokens with noised tokens.""" + if packed_sequence.vision is not None: + packed_sequence.vision.tokens = gen_data_noised.xt_tokens_vision + if packed_sequence.action is not None and gen_data_noised.xt_tokens_action is not None: + action_all_conditioning = all( + torch.all(condition_mask == 1).item() for condition_mask in packed_sequence.action.condition_mask + ) + if not action_all_conditioning: + packed_sequence.action.tokens = gen_data_noised.xt_tokens_action + if packed_sequence.sound is not None and gen_data_noised.xt_tokens_sound is not None: + packed_sequence.sound.tokens = gen_data_noised.xt_tokens_sound + + # ------------------------ Inference Utils ------------------------ + def _get_inference_text_tokens( + self, data_batch: dict, has_negative_prompt: bool + ) -> tuple[list[list[int]], list[list[int]]]: + """Tokenize conditional and unconditional captions for inference.""" + use_system_prompt = self.vlm_config.use_system_prompt + system_prompt: str | None = data_batch.get("system_prompt") + + cond_tokens = [ + tokenize_caption( + c, + self.vlm_tokenizer, + is_video=False, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + for c in data_batch[self.input_caption_key] + ] + + if has_negative_prompt: + neg_key = "neg_" + self.input_caption_key + assert neg_key in data_batch, f"Negative prompt ({neg_key}) not found" + uncond_captions = data_batch[neg_key] + else: + uncond_captions = [""] * len(cond_tokens) + + uncond_tokens = [ + tokenize_caption( + c, + self.vlm_tokenizer, + is_video=False, + use_system_prompt=use_system_prompt, + system_prompt=system_prompt, + ) + for c in uncond_captions + ] + return cond_tokens, uncond_tokens + + def _prepare_inference_data( + self, + data_batch: dict, + seed: list[int], + has_negative_prompt: bool = False, + ) -> tuple[ + list[SequencePlan], + GenerationDataClean, + list[list[int]], + list[list[int]], + list[torch.Tensor], + ]: + """ + Prepare all data needed for inference sampling. + Mirrors training_step's data preparation flow. + + This method: + 1. Builds sequence plans (conditioning information) + 2. Gets data and condition (encodes vision) + 3. Tokenizes text (conditional and unconditional for CFG) + 4. Builds a packed sequence to fetch conditioning masks + 5. Initializes noise with conditioning applied (as lists for variable shapes) + 6. If action_gen is True, concatenates action noise with vision noise + + Args: + data_batch: Raw data batch from dataloader. + seed: Random seed(s) for noise generation. + has_negative_prompt: If True, use negative prompt for unconditional branch. + + Returns: + Tuple of: + - sequence_plans: List of SequencePlan objects + - gen_data_clean: GenerationDataClean with encoded tokens + - cond_text_tokens: Conditional text tokens + - uncond_text_tokens: Unconditional text tokens (for CFG) + - initial_noise: List of noise tensors (one per sample), each containing + flattened vision (and optionally action) noise concatenated + """ + # 1. Build sequence plans (same as training) + sequence_plans = build_sequence_plans_from_data_batch( + data_batch=data_batch, + input_video_key=self.input_video_key, + input_image_key=self.input_image_key, + ) + + # 2. Get data and condition (same as training) + # This encodes vision to x0_tokens + gen_data_clean = self.get_data_and_condition(data_batch) + + num_items_per_sample = gen_data_clean.num_vision_items_per_sample # None for standard T2I/T2V + + # 3. Tokenize text (similar to training's _load_and_tokenize_text_data) + cond_text_tokens, uncond_text_tokens = self._get_inference_text_tokens(data_batch, has_negative_prompt) + + # 4. Build packed sequence to fetch conditioning masks + mask_timesteps = torch.zeros((gen_data_clean.batch_size,), dtype=torch.float32) # [B] + packed_sequence = self._pack_input_sequence( + sequence_plans, + cond_text_tokens, + gen_data_clean, + mask_timesteps, + include_end_of_generation_token=self._derive_include_end_of_generation_token(), + ) + + # 5. Initialize vision noise with conditioning + assert packed_sequence.vision is not None, "Packed vision data required for inference noise" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for inference noise" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for inference noise" + ) + assert gen_data_clean.x0_tokens_vision is not None, "Vision data required for inference noise" + n_sample = ( + len(gen_data_clean.x0_tokens_vision) + if gen_data_clean.num_vision_items_per_sample is None + else len(gen_data_clean.num_vision_items_per_sample) + ) + + assert len(seed) == n_sample, ( + f"Seed list length {len(seed)} must have the same length as the number of samples {n_sample}" + ) + + # For image2image, num_items_per_sample could be > 1 (multi-vision), + # so we need to repeat the seed for each vision item. + seed_dict = {"vision": [], "action": [], "sound": []} + for sample_idx in range(n_sample): + num_vision_items = num_items_per_sample[sample_idx] if num_items_per_sample is not None else 1 + seed_dict["vision"].extend([seed[sample_idx]] * num_vision_items) + seed_dict["action"].append(seed[sample_idx]) + seed_dict["sound"].append(seed[sample_idx]) + + # Generate noise and apply conditioning per vision item (supports variable shapes) + noise_vision_list: list[torch.Tensor] = [] + for i, (x0_token, cond_mask) in enumerate( + zip(gen_data_clean.x0_tokens_vision, packed_sequence.vision.condition_mask, strict=True) + ): + pure_noise_i = misc.arch_invariant_rand( + tuple(x0_token.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["vision"][i], # Different seed per sample for diversity + ) # [C,T,H,W] + noise_i = cond_mask * x0_token.to(**self.tensor_kwargs) + (1.0 - cond_mask) * pure_noise_i # [C,T,H,W] + noise_vision_list.append(noise_i) + + # 6. Initialize action noise if action_gen is True + has_action = self.config.action_gen and any(plan.has_action for plan in sequence_plans) + noise_action_list: list[torch.Tensor] | None = None + + if has_action: + assert gen_data_clean.x0_tokens_action is not None, "Action data required when sequence plan has action" + assert packed_sequence.action is not None, "Packed action data required when action_gen is True" + assert packed_sequence.action.condition_mask is not None, "Action condition mask required" + assert isinstance(packed_sequence.action.condition_mask, list), ( + "Action condition mask must be a list of tensors for inference noise" + ) + + # Generate action noise per sample (x0_tokens_action is already dense, no None entries) + noise_action_list = [] + for i, (x0_action, cond_mask_action) in enumerate( + zip(gen_data_clean.x0_tokens_action, packed_sequence.action.condition_mask, strict=True) + ): + pure_noise_action_i = misc.arch_invariant_rand( + tuple(x0_action.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["action"][i], # Different seed per sample for diversity + ) # [T,action_dim] + noise_action_i = ( + cond_mask_action * x0_action.to(**self.tensor_kwargs) + + (1.0 - cond_mask_action) * pure_noise_action_i + ) + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + noise_action_i[:, gen_data_clean.raw_action_dim[i] :] = 0 + noise_action_list.append(noise_action_i) + + # 7. Initialize sound noise if sound_gen is True + has_sound = self.config.sound_gen and any(plan.has_sound for plan in sequence_plans) + noise_sound_list: list[torch.Tensor] | None = None + + if has_sound: + assert gen_data_clean.x0_tokens_sound is not None, "Sound data required when sequence plan has sound" + assert packed_sequence.sound is not None, "Packed sound data required when sound_gen is True" + assert packed_sequence.sound.condition_mask is not None, "Sound condition mask required" + assert isinstance(packed_sequence.sound.condition_mask, list), ( + "Sound condition mask must be a list of tensors for inference noise" + ) + + noise_sound_list = [] + for i, (x0_sound, cond_mask_sound) in enumerate( + zip(gen_data_clean.x0_tokens_sound, packed_sequence.sound.condition_mask, strict=True) + ): + pure_noise_sound_i = misc.arch_invariant_rand( + tuple(x0_sound.shape), + self.tensor_kwargs["dtype"], + self.tensor_kwargs["device"], + seed_dict["sound"][i], # Different seed per sample for diversity + ) # [sound_channels,T_sound] + # cond_mask_sound is (T, 1), x0_sound is (C, T) — transpose mask for broadcasting + noise_sound_i = ( + cond_mask_sound.T * x0_sound.to(**self.tensor_kwargs) + + (1.0 - cond_mask_sound.T) * pure_noise_sound_i + ) # [sound_channels,T_sound] + noise_sound_list.append(noise_sound_i) + + # 8. Concatenate vision, action, and sound noise per sample (flattened) + # Order: [vision | action (if present) | sound (if present)] + # noise_action_list and noise_sound_list are dense (only modality-having samples), + # so we use separate indexes. + initial_noise: list[torch.Tensor] = [] + idx_vision = 0 + idx_action = 0 + idx_sound = 0 + + for i in range(n_sample): + parts = [] + + # Flatten and concatenate all vision items for this sample + num_vis = num_items_per_sample[i] if num_items_per_sample is not None else 1 + for _ in range(num_vis): + parts.append(noise_vision_list[idx_vision].reshape(-1)) + idx_vision += 1 + + if noise_action_list is not None and sequence_plans[i].has_action: + parts.append(noise_action_list[idx_action].reshape(-1)) + idx_action += 1 + + if noise_sound_list is not None and sequence_plans[i].has_sound: + parts.append(noise_sound_list[idx_sound].reshape(-1)) + idx_sound += 1 + + initial_noise.append(torch.cat(parts, dim=0)) # [N_tokens_flat] + + return ( + sequence_plans, + gen_data_clean, + cond_text_tokens, + uncond_text_tokens, + initial_noise, + ) + + def _get_velocity( + self, + *, + net: torch.nn.Module | None = None, + noise_x: list[torch.Tensor], + timestep: torch.Tensor, + text_tokens: list[list[int]], + sequence_plans: list[SequencePlan], + gen_data_clean: GenerationDataClean, + skip_text_tokens: bool = False, + ) -> list[torch.Tensor]: + """ + Compute velocity prediction for a single sampling step. + + This method handles the full pipeline for one denoising step: + 1. Splits flattened noise_x into vision (and action) parts per sample + 2. Packs the input sequence with current noisy latents + 3. Runs the network via self.denoise() + 4. Applies velocity masks (zeroes out conditioned parts) + 5. Returns flattened velocities (concatenated vision + action per sample) + + Args: + noise_x: List of noisy latents, each containing concatenated + vision (and optionally action) noise. + len(noise_x) == B, noise_x[i] is shape (D) + timestep: Current timestep for each sample + text_tokens: Tokenized text for each sample + sequence_plans: Pre-computed sequence plans (from _prepare_inference_data) + gen_data_clean: Pre-computed clean data (from _prepare_inference_data) + skip_text_tokens: If True, skip text tokens (for CFG unconditional branch) + + Returns: + Stacked flattened velocity tensors (one per sample), each containing + concatenated vision (and optionally action) velocity + """ + n_samples = len(noise_x) + is_image_batch = gen_data_clean.is_image_batch + has_action = self.config.action_gen and any(plan.has_action for plan in sequence_plans) + num_items = gen_data_clean.num_vision_items_per_sample # None for standard T2I/T2V + has_sound = self.config.sound_gen and any(plan.has_sound for plan in sequence_plans) + + # Split flattened noise_x into vision, action, and sound parts per sample + # Order must match _prepare_inference_data: [vision | action (if present) | sound (if present)] + noise_x_vision: list[torch.Tensor] = [] + noise_x_action: list[torch.Tensor] | None = [] if has_action else None + noise_x_sound: list[torch.Tensor] | None = [] if has_sound else None + + vision_offset = 0 # tracks position in the flat x0_tokens_vision list + idx_action = 0 + idx_sound = 0 + for i in range(n_samples): + n_vis = num_items[i] if num_items is not None else 1 + offset = 0 + for j in range(n_vis): + vision_shape = gen_data_clean.x0_tokens_vision[vision_offset + j].shape + vision_dim = int(torch.prod(torch.tensor(vision_shape))) + noise_vision_ij = noise_x[i][offset : offset + vision_dim].reshape(vision_shape) + noise_x_vision.append(noise_vision_ij) + offset += vision_dim + vision_offset += n_vis + + if has_action and noise_x_action is not None: + assert gen_data_clean.x0_tokens_action is not None + action_shape = gen_data_clean.x0_tokens_action[idx_action].shape + action_dim = int(torch.prod(torch.tensor(action_shape))) + noise_x_action.append(noise_x[i][offset : offset + action_dim].reshape(action_shape)) # [T,action_dim] + offset += action_dim + idx_action += 1 + + # Extract sound if present for this sample + if has_sound and noise_x_sound is not None and sequence_plans[i].has_sound: + assert gen_data_clean.x0_tokens_sound is not None + sound_shape = gen_data_clean.x0_tokens_sound[idx_sound].shape + sound_dim = int(torch.prod(torch.tensor(sound_shape))) + noise_x_sound.append( + noise_x[i][offset : offset + sound_dim].reshape(sound_shape) + ) # [sound_channels,T_sound] + offset += sound_dim + idx_sound += 1 + + gen_data_for_packing = GenerationDataClean( + batch_size=n_samples, + is_image_batch=is_image_batch, + raw_state_vision=gen_data_clean.raw_state_vision, + x0_tokens_vision=noise_x_vision, + fps_vision=gen_data_clean.fps_vision, + # Action fields + raw_state_action=gen_data_clean.raw_state_action if has_action else None, + x0_tokens_action=noise_x_action if has_action else None, + action_domain_id=gen_data_clean.action_domain_id if has_action else None, + fps_action=gen_data_clean.fps_action if has_action else None, + raw_action_dim=gen_data_clean.raw_action_dim if has_action else None, + # Sound fields + raw_state_sound=gen_data_clean.raw_state_sound if has_sound else None, + x0_tokens_sound=noise_x_sound if has_sound else None, + fps_sound=gen_data_clean.fps_sound if has_sound else None, + num_vision_items_per_sample=num_items, + ) + + packed_sequence = self._pack_input_sequence( + sequence_plans, + text_tokens, + gen_data_for_packing, + timestep.cpu(), + include_end_of_generation_token=self._derive_include_end_of_generation_token(), + skip_text_tokens=skip_text_tokens, + ) + + # Set the actual noisy latents (as lists) + if packed_sequence.vision is not None: + packed_sequence.vision.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_vision] + + if has_action and noise_x_action is not None: + assert packed_sequence.action is not None, "packed_sequence.action must exist when has_action is True" + packed_sequence.action.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_action] + packed_sequence.action.domain_id = gen_data_clean.action_domain_id + + if has_sound and noise_x_sound is not None: + assert packed_sequence.sound is not None, "packed_sequence.sound must exist when has_sound is True" + packed_sequence.sound.tokens = [x.to(**self.tensor_kwargs) for x in noise_x_sound] + + packed_sequence.to_cuda() + + # --- Network forward --- + fps_action = gen_data_clean.fps_action if has_action else None + fps_sound = gen_data_clean.fps_sound if has_sound else None + out = self.denoise( + net=net, + data_batch_packed=packed_sequence, + fps_vision=gen_data_clean.fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + ) + + # --- Apply velocity masks --- + # Zero out velocity for conditioned parts (they don't change during sampling) + assert packed_sequence.vision is not None, "packed_sequence.vision must exist for velocity masking" + assert packed_sequence.vision.condition_mask is not None, "Vision condition mask required for masking" + assert isinstance(packed_sequence.vision.condition_mask, list), ( + "Vision condition mask must be a list of tensors for masking" + ) + # Compute noisy_mask per sample (supports variable shapes) + noisy_mask_vision = [1.0 - cond_mask for cond_mask in packed_sequence.vision.condition_mask] + + # Apply velocity mask per element - check if each sample has noisy tokens + velocity_vision: list[torch.Tensor] = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_vision"], noisy_mask_vision)): + # pred: [C,T,H,W], noisy_mask: [T,1,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + # Apply mask to prediction + velocity_vision.append(pred * noisy_mask.to(dtype=pred.dtype, device=pred.device)) # [C,T,H,W] + else: + # All tokens are conditioned - velocity should be zero + velocity_vision.append(torch.zeros_like(pred)) # [C,T,H,W] + + # Handle action velocity + velocity_action: list[torch.Tensor] | None = None + if ( + has_action + and packed_sequence.action is not None + and packed_sequence.action.condition_mask is not None + and isinstance(packed_sequence.action.condition_mask, list) + ): + noisy_mask_action = [1.0 - cond_mask for cond_mask in packed_sequence.action.condition_mask] + + velocity_action = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_action"], noisy_mask_action)): + # pred: [T,action_dim], noisy_mask: [T,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + v = pred * noisy_mask.to(dtype=pred.dtype, device=pred.device) # [T,action_dim] + else: + v = torch.zeros_like(pred) # [T,action_dim] + if gen_data_clean.raw_action_dim is not None and gen_data_clean.raw_action_dim[i] is not None: + v[:, gen_data_clean.raw_action_dim[i] :] = 0 + velocity_action.append(v) + + # Handle sound velocity + velocity_sound: list[torch.Tensor] | None = None + if ( + has_sound + and packed_sequence.sound is not None + and packed_sequence.sound.condition_mask is not None + and isinstance(packed_sequence.sound.condition_mask, list) + ): + noisy_mask_sound = [1.0 - cond_mask for cond_mask in packed_sequence.sound.condition_mask] + + velocity_sound = [] + for i, (pred, noisy_mask) in enumerate(zip(out["preds_sound"], noisy_mask_sound)): + # pred: [sound_channels,T_sound], noisy_mask: [T_sound,1] + has_noisy_tokens_i = noisy_mask.sum() > 0 + if has_noisy_tokens_i: + # noisy_mask is (T, 1), pred is (C, T) — transpose mask for broadcasting + velocity_sound.append( + pred * noisy_mask.T.to(dtype=pred.dtype, device=pred.device) + ) # [sound_channels,T_sound] + else: + velocity_sound.append(torch.zeros_like(pred)) # [sound_channels,T_sound] + + # Concatenate vision, action, and sound velocities per sample (flattened) + # Order must match _prepare_inference_data: [vision | action | sound] + velocity_output: list[torch.Tensor] = [] + vis_offset = 0 + idx_action = 0 + idx_sound = 0 + for i in range(n_samples): + parts = [] + n_vis = num_items[i] if num_items is not None else 1 + + for _ in range(n_vis): + parts.append(velocity_vision[vis_offset].reshape(-1)) + vis_offset += 1 + + if velocity_action is not None and sequence_plans[i].has_action: + parts.append(velocity_action[idx_action].reshape(-1)) + idx_action += 1 + + if velocity_sound is not None and sequence_plans[i].has_sound: + parts.append(velocity_sound[idx_sound].reshape(-1)) + idx_sound += 1 + + velocity_output.append(torch.cat(parts, dim=0)) # [N_tokens_flat] + + return velocity_output + + def _remove_padding_from_latent( + self, x0_tokens_vision: list[torch.Tensor], frame_size: list[torch.Tensor] + ) -> list[torch.Tensor]: + """ + Remove reflection padding from encoded latent vision tokens. + + Each sample in the batch may have different original dimensions, so we process + each sample individually and return a list of latents with varying spatial sizes. + + The padding coordinates are scaled down by the spatial compression factor since + we're operating in latent space. + + Args: + x0_tokens_vision (list[torch.Tensor]): List of encoded latent tensors, + each of shape (1, C, T, H_latent, W_latent) + where H_latent, W_latent include scaled padding. + frame_size (list[torch.Tensor]): List of tensors, each of shape (1,4) or (4,) containing + [target_h, target_w, orig_h, orig_w] for each sample (in pixel space). + + Returns: + list[torch.Tensor]: List of cropped latent tokens, each of shape (1, C, T, H_latent_cropped, W_latent_cropped). + Each element may have different spatial sizes based on original image dimensions. + """ + batch_size = len(x0_tokens_vision) + spatial_factor = self.tokenizer_vision_gen.spatial_compression_factor + cropped_latents = [] + for i in range(batch_size): + # frame_size: [target_h, target_w, orig_h, orig_w] in pixel space + # Normalize: frame_size[i] may be (1, 4) from IterativeJointDataLoader + # or (4,) when loaded from safetensors in the eval/export path. + fs = frame_size[i] + if fs.dim() == 2: + fs = fs[0] + orig_h = int(fs[2].item()) + orig_w = int(fs[3].item()) + + # Scale to latent space + if orig_h // spatial_factor == 0 or orig_w // spatial_factor == 0: + log.warning( + f"Zero-sized latent found: orig_h: {orig_h}, orig_w: {orig_w}, spatial_factor: {spatial_factor}" + ) + + orig_h_latent = max(orig_h // spatial_factor, 1) + orig_w_latent = max(orig_w // spatial_factor, 1) + + # Crop to remove padding: x0_tokens_vision[i] shape is (1, C, T, H, W) + cropped_latent = x0_tokens_vision[i][:, :, :, :orig_h_latent, :orig_w_latent].contiguous() + cropped_latents.append(cropped_latent) + + return cropped_latents + + def _run_classifier_free_guidance( + self, + cond_tokens: list[list[int]], + uncond_tokens: list[list[int]], + skip_text_tokens_for_cfg: bool, + single_velocity_fn: Callable[[list[list[int]], bool], list[torch.Tensor]], + ) -> tuple[list[torch.Tensor], list[torch.Tensor]]: + """Run classifier-free guidance, optionally in parallel via CFG parallelism. + + Args: + cond_tokens: Tokenized text for the conditional branch. + uncond_tokens: Tokenized text for the unconditional branch. + skip_text_tokens_for_cfg: If True, skip text tokens in the + unconditional branch. + single_velocity_fn: Computes velocity for a given set of tokens. + Accepts ``(tokens, skip_text_tokens)`` and returns a list of + velocity tensors (one per sample). + + Returns: + A tuple ``(cond_v, uncond_v)`` where each element is a list of + velocity tensors (one per sample). + """ + if self.parallel_dims is None or not self.parallel_dims.cfgp_enabled: + return ( + single_velocity_fn(cond_tokens, False), + single_velocity_fn(uncond_tokens, skip_text_tokens_for_cfg), + ) + + cfgp_rank = self.parallel_dims.cfgp_rank + cfgp_size = self.parallel_dims.cfgp_size + cfgp_group = self.parallel_dims.cfgp_mesh.get_group() + cfgp_peer = (cfgp_rank + 1) % cfgp_size + + if cfgp_rank == 0: + v_list = single_velocity_fn(cond_tokens, False) + else: + v_list = single_velocity_fn(uncond_tokens, skip_text_tokens_for_cfg) + + other_v_list = [torch.empty_like(v_i) for v_i in v_list] + + ops: list[dist.P2POp] = [] + for v_i, other_v_i in zip(v_list, other_v_list): + ops.append(dist.P2POp(op=dist.isend, tensor=v_i, group_peer=cfgp_peer, group=cfgp_group)) + ops.append(dist.P2POp(op=dist.irecv, tensor=other_v_i, group_peer=cfgp_peer, group=cfgp_group)) + + reqs = dist.batch_isend_irecv(ops) + for req in reqs: + req.wait() + + if cfgp_rank == 0: + return v_list, other_v_list + else: + return other_v_list, v_list + + @torch.no_grad() + def generate_samples_from_batch( + self, + data_batch: Dict, + net: torch.nn.Module | None = None, + sampler: Any | None = None, + guidance: float = 1.5, + guidance_interval: Optional[list[float]] = None, + seed: list[int] | int = 1, + n_sample: int | None = None, + has_negative_prompt: bool = False, + num_steps: int = 35, + shift: float = 5.0, + sigma_max: float = 80.0, + skip_text_tokens_for_cfg: bool = False, + normalize_cfg: bool = False, + **kwargs, + ) -> dict[str, list[torch.Tensor]]: + """ + Generate samples from the batch. Based on given batch, it will automatically determine + whether to generate image or video samples. + + This method follows the same structure as training_step: + 1. Build sequence plans + 2. Get data and condition (encode vision) + 3. Initialize noise with conditioning (as lists for variable shapes) + 4. Run sampling loop with velocity function + 5. Return latents as lists (supports variable shapes) + + Args: + data_batch (dict): Raw data batch from the dataloader. + guidance (float): Classifier-free guidance weight. + guidance_interval (list[float] | None): Optional timestep interval to apply guidance. + For the timesteps (ranging between 0-1000) that fall between the interval, we perform CFG, otherwise, we skip the unconditional generation. + seed (list[int] | int): Random seeds for noise generation. For all new use-cases, + we use a list of seeds, one for each sample. The length of the list must match + the number of samples. Legacy use-cases use a single integer seed which is + incremented by 1 for each sample. But this is not supported anymore, and will + raise an error if used. + n_sample (int | None): Number of samples to generate; defaults to batch size. + has_negative_prompt (bool): If True, use negative prompt for unconditional branch. + num_steps (int): Number of sampling steps for the diffusion process. + shift (float): Time shift parameter for the sampler. + sigma_max (float): Maximum sigma for the EDM sampler. + skip_text_tokens_for_cfg (bool): If True, skip text tokens in unconditional branch. + normalize_cfg (bool): If True, normalize the CFG output. + + Returns: + Dict with keys: + - "vision": List of vision latent tensors (one per sample, variable shapes) + - "action": List of action latent tensors or None (only present when action_gen=True and has_action) + + Raises: + ValueError: If the number of samples does not match the number of noise tensors or seeds. + ValueError: If the seed is a single integer. This is not supported anymore: `seed` must be + a list of integers, one for each sample. + """ + if isinstance(seed, int): + raise ValueError( + "Single integer seed is not supported anymore: `seed` must be a list of integers, one for each sample." + ) + assert isinstance(seed, list) + + if self.parallel_dims is not None and self.parallel_dims.cp_enabled: + seed = _broadcast_seed(seed, self.parallel_dims.cp_mesh.get_group(), self.parallel_dims.cp_rank) + + if self.parallel_dims is not None and self.parallel_dims.cfgp_enabled: + seed = _broadcast_seed(seed, self.parallel_dims.cfgp_mesh.get_group(), self.parallel_dims.cfgp_rank) + + # Prepare all data (initial noise as list of flattened tensors per sample) + ( + sequence_plans, + gen_data_clean, + cond_tokens, + uncond_tokens, + initial_noise, + ) = self._prepare_inference_data(data_batch, seed, has_negative_prompt) + + if n_sample is not None: + assert n_sample == len(initial_noise), ( + f"Number of samples {n_sample} must match number of noise tensors {len(initial_noise)}" + ) + else: + n_sample = len(initial_noise) + + assert n_sample == len(seed), f"Number of samples {n_sample} must match number of seeds {len(seed)}" + + # Create a velocity function for a single sample (for use with self.sampler). + + def velocity_fn(noise_x: list[torch.Tensor], timestep: torch.Tensor) -> list[torch.Tensor]: + # len(noise_x) == B, noise_x[i] is shape (D) + # timestep is shape (B, 1) + torch.compiler.cudagraph_mark_step_begin() + + assert timestep.ndim == 2, f"timestep must be 2D, got {timestep.shape}" + assert timestep.shape == (1, 1), f"timestep must be (1, 1), got {timestep.shape}" + + # Expand timestep to (B, 1) + timestep = timestep.repeat(len(noise_x), 1) + + def _single_velocity_fn(tokens: list[list[int]], skip_text_tokens: bool): + return self._get_velocity( + net=net, + noise_x=noise_x, + timestep=timestep, + text_tokens=tokens, + sequence_plans=sequence_plans, + gen_data_clean=gen_data_clean, + skip_text_tokens=skip_text_tokens, + ) + + # Skip unconditional branch when outside the guidance interval + needs_cfg = guidance != 1.0 + if needs_cfg and guidance_interval is not None: + assert len(guidance_interval) == 2, f"guidance_interval must be [lo, hi], got {guidance_interval}" + t_lo, t_hi = guidance_interval + needs_cfg = t_lo < timestep[0].item() < t_hi + + if not needs_cfg: + return _single_velocity_fn(cond_tokens, skip_text_tokens=False) + + cond_v, uncond_v = self._run_classifier_free_guidance( + cond_tokens=cond_tokens, + uncond_tokens=uncond_tokens, + skip_text_tokens_for_cfg=skip_text_tokens_for_cfg, + single_velocity_fn=_single_velocity_fn, + ) + + v_pred = [u_i + guidance * (c_i - u_i) for c_i, u_i in zip(cond_v, uncond_v)] + + if normalize_cfg: + v_pred = [ + v_i * (torch.norm(c_i) / (torch.norm(v_i) + 1e-8)).clamp(min=0.0, max=1.0) + for v_i, c_i in zip(v_pred, cond_v) + ] + + return v_pred + + # Run sampler for all samples at once. + sampler = sampler or self.sampler + scheduler_type = self.config.rectified_flow_inference_config.scheduler_type + if scheduler_type == "unipc": + log.info(f"Using sampler: UniPC (shift={shift}, num_steps={num_steps})") + else: + log.info(f"Using sampler: EDM (sigma_max={sigma_max}, num_steps={num_steps})") + + if scheduler_type == "unipc": + latents = sampler( + velocity_fn, + initial_noise, + num_steps=num_steps, + shift=shift, + seed=seed, + ) + else: + # EDM Sampler + chunk_sizes = [_x.shape[0] for _x in initial_noise] + initial_noise = torch.cat(initial_noise, dim=0) + + def x0_fn(noise_x: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor: + assert sigma.ndim == 0, f"sigma must be 0D, got {sigma.shape}" + timestep_rf = sigma * float(self.config.rectified_flow_inference_config.num_train_timesteps) + + # Convert noise_x to list of tensors for velocity_fn, and then + # concatenate the results back into a single tensor. + _noise_x = list(torch.split(noise_x, chunk_sizes, dim=0)) + _velocity_pred = velocity_fn(_noise_x, timestep_rf.reshape(1, 1)) + velocity_pred = torch.cat(_velocity_pred, dim=0) + + x0_pred = noise_x - sigma * velocity_pred + return x0_pred + + latents = sampler( + x0_fn, + initial_noise, + num_steps=num_steps, + sigma_max=sigma_max, + sigma_min=0.002, + solver_option="2ab", + ) + latents = list(torch.split(latents, chunk_sizes, dim=0)) + + # Split flattened latents back into vision, action, and sound + # Mirror the per-sample logic from _prepare_inference_data: + # Order: [vision | action (if present) | sound (if present)] + # action/sound lists are dense (only modality-having samples), so use separate indexes. + result_vision: list[torch.Tensor] = [] + result_action: list[torch.Tensor] = [] + result_sound: list[torch.Tensor] = [] + idx_vision = 0 + idx_action = 0 + idx_sound = 0 + num_vision_items = gen_data_clean.num_vision_items_per_sample + + for i in range(n_sample): + offset = 0 + + # Extract vision + n_vis = num_vision_items[i] if num_vision_items is not None else 1 + for j in range(n_vis): + vision_shape = gen_data_clean.x0_tokens_vision[idx_vision + j].shape + vision_dim = int(torch.prod(torch.tensor(vision_shape))) + if j == n_vis - 1: # the last vision item is the only target for each sample. + + result_vision.append(latents[i][offset : offset + vision_dim].reshape(vision_shape)) + else: # the other vision items are the condition inputs that we don't need to return + pass + offset += vision_dim + idx_vision += n_vis + + # Extract action if present + if self.config.action_gen and sequence_plans[i].has_action: + assert gen_data_clean.x0_tokens_action is not None + action_shape = gen_data_clean.x0_tokens_action[idx_action].shape + action_dim = int(torch.prod(torch.tensor(action_shape))) + result_action.append(latents[i][offset : offset + action_dim].reshape(action_shape)) + offset += action_dim + idx_action += 1 + + # Extract sound if present + if self.config.sound_gen and sequence_plans[i].has_sound: + assert gen_data_clean.x0_tokens_sound is not None + sound_shape = gen_data_clean.x0_tokens_sound[idx_sound].shape + sound_dim = int(torch.prod(torch.tensor(sound_shape))) + result_sound.append(latents[i][offset : offset + sound_dim].reshape(sound_shape)) + offset += sound_dim + idx_sound += 1 + + result: dict[str, list[torch.Tensor]] = {"vision": result_vision} + if self.config.action_gen and len(result_action) > 0: + result["action"] = result_action + if self.config.sound_gen and len(result_sound) > 0: + result["sound"] = result_sound + return result + + def _extract_condition_images_for_visualization( + self, + gen_data_clean: GenerationDataClean, + sequence_plans: list[SequencePlan], + n_samples: int, + ) -> list[torch.Tensor | None]: + """Extract condition images from gen_data_clean for visualization. + + For image editing, raw_state_vision is a flat list of individually-encoded + images (e.g. [src1, tgt1, src2, tgt2, ...]). The first vision item for + each sample is the condition (source) image. This method extracts it and + resizes to match the target for side-by-side display. + + Args: + gen_data_clean: Clean data containing raw vision states. + sequence_plans: Sequence plans for each sample. + n_samples: Number of samples to process. + + Returns: + List of condition image tensors (one per sample with condition frames). + """ + condition_images: list[torch.Tensor | None] = [] + + if gen_data_clean.num_vision_items_per_sample is not None: + # Multi-item (image editing): raw_state_vision is flat [src1, tgt1, src2, tgt2, ...] + vision_offset = 0 + for i in range(n_samples): + num_items = gen_data_clean.num_vision_items_per_sample[i] + if num_items >= 2: + cond_frame = gen_data_clean.raw_state_vision[vision_offset] # (1, C, 1, H_s, W_s) + target_frame = gen_data_clean.raw_state_vision[vision_offset + 1] # (1, C, 1, H_t, W_t) + # Resize condition frame to match target size for visualization + if cond_frame.shape[-2:] != target_frame.shape[-2:]: + cond_frame = torch.nn.functional.interpolate( + cond_frame.squeeze(2), # (1, C, H, W) + size=target_frame.shape[-2:], + mode="bilinear", + align_corners=False, + ).unsqueeze(2) # (1, C, 1, H, W) + condition_images.append(cond_frame) + else: + condition_images.append(None) + vision_offset += num_items + else: + # Standard single-item mode: check condition_frame_indexes_vision + for i in range(n_samples): + plan = sequence_plans[i] + if len(plan.condition_frame_indexes_vision) > 0 and gen_data_clean.raw_state_vision is not None: + raw_vision = gen_data_clean.raw_state_vision[i] # (1, C, T, H, W) + condition_images.append(raw_vision[:, :, 0:1, :, :]) + else: + condition_images.append(None) + + return condition_images + + def _slice_gen_data_clean(self, gen_data_clean: GenerationDataClean, start: int, limit: int) -> GenerationDataClean: + """Extract a subset of GenerationDataClean for inference. + + The samples in [start:limit] are extracted from the original GenerationDataClean. + + For image editing (``num_vision_items_per_sample`` is set), the sample index refers to + the *real sample* index. The method computes the correct slice of the flat + ``x0_tokens_vision`` / ``raw_state_vision`` lists using the item counts and + preserves ``num_vision_items_per_sample`` on the returned subset so that + downstream packing works correctly. + + Args: + gen_data_clean: GenerationDataClean to slice. + start: Start index of the slice. + limit: Limit index of the slice. + + Returns: + Sliced GenerationDataClean. + """ + # x0_tokens_action can be an empty list (e.g. image2video mode), not just None + has_action = bool(gen_data_clean.x0_tokens_action) + has_sound = bool(gen_data_clean.x0_tokens_sound) + + # Determine vision slice for this sample + num_items = gen_data_clean.num_vision_items_per_sample + if num_items is not None: + # Multi-item mode: compute flat-list offset + vis_start = sum(num_items[:start]) # number of all the vision tokens before the start + vis_end = sum(num_items[:limit]) + subset_x0_vision = gen_data_clean.x0_tokens_vision[vis_start:vis_end] + subset_raw_vision = ( + gen_data_clean.raw_state_vision[vis_start:vis_end] if gen_data_clean.raw_state_vision else None + ) + subset_num_items = num_items[start:limit] + else: + # Standard single-item mode + subset_x0_vision = gen_data_clean.x0_tokens_vision[start:limit] + subset_raw_vision = ( + gen_data_clean.raw_state_vision[start:limit] if gen_data_clean.raw_state_vision else None + ) + subset_num_items = None + fps_vision = gen_data_clean.fps_vision[start:limit] if gen_data_clean.fps_vision is not None else None + + if has_action: + subset_raw_action = ( + gen_data_clean.raw_state_action[start:limit] if gen_data_clean.raw_state_action else None + ) + x0_tokens_action = gen_data_clean.x0_tokens_action[start:limit] + fps_action = gen_data_clean.fps_action[start:limit] if gen_data_clean.fps_action is not None else None + action_domain_id = gen_data_clean.action_domain_id[start:limit] if gen_data_clean.action_domain_id else None + raw_action_dim = gen_data_clean.raw_action_dim[start:limit] if gen_data_clean.raw_action_dim else None + else: + subset_raw_action = None + x0_tokens_action = None + fps_action = None + action_domain_id = None + raw_action_dim = None + + if has_sound: + subset_raw_sound = gen_data_clean.raw_state_sound[start:limit] if gen_data_clean.raw_state_sound else None + x0_tokens_sound = gen_data_clean.x0_tokens_sound[start:limit] + fps_sound = gen_data_clean.fps_sound[start:limit] if gen_data_clean.fps_sound is not None else None + else: + subset_raw_sound = None + x0_tokens_sound = None + fps_sound = None + + return GenerationDataClean( + batch_size=limit - start, + is_image_batch=gen_data_clean.is_image_batch, + raw_state_vision=subset_raw_vision, + raw_state_action=subset_raw_action, + raw_state_sound=subset_raw_sound, + x0_tokens_vision=subset_x0_vision, + x0_tokens_action=x0_tokens_action, + x0_tokens_sound=x0_tokens_sound, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + action_domain_id=action_domain_id, + raw_action_dim=raw_action_dim, + num_vision_items_per_sample=subset_num_items, + ) + + @torch.no_grad() + def validation_step(self, data_batch: dict[str, torch.Tensor], iteration: int): + pass + + @torch.no_grad() + def forward(self, xt, t): + pass + + def get_data_and_condition(self, data_batch: dict[str, torch.Tensor], iteration: int = 1) -> GenerationDataClean: + """ + - Get raw data of different modalities from databatch + - Tokenize into corresponding latents + - Load other conditioning information if any (fps, etc.) + """ + # Detect whether any sample has multiple vision items (e.g. image editing). + # If so, track the count per sample before all vision items from this batch are flattened into a list. + is_image_batch = self.is_image_batch(data_batch) + sample_vision_list = data_batch[self.input_image_key if is_image_batch else self.input_video_key] + + + # we should always get this information here during training. If we can read this field + # from data_batch it means we are in the visualization callback: + if "num_vision_items_per_sample" not in data_batch: + # Each element must be a list/tuple of tensors (not a bare tensor) to count + # as multi-vision. A bare tensor's len() returns its first dim size (e.g. C=3), + # which would incorrectly trigger the multi-vision path for regular video batches. + has_multiple_vision_per_sample = any( + isinstance(v, (list, tuple)) and len(v) > 1 for v in sample_vision_list + ) + num_vision_items_per_sample: list[int] | None = ( + [len(v) for v in sample_vision_list] if has_multiple_vision_per_sample else None + ) + + # information is only stored in the GenerationDataClean object which will be discarded + # outside the training loop. Error will be raised when the data batch is passed to the + # visualization callbacks. + data_batch["num_vision_items_per_sample"] = num_vision_items_per_sample + + # if has_multiple_vision_per_sample, this means that the input media is a list of lists of tensors, we need to flatten it to a list of tensors + if has_multiple_vision_per_sample: + media_key = self.input_video_key if not is_image_batch else self.input_image_key + data_batch[media_key] = [item.unsqueeze(0) for sublist in sample_vision_list for item in sublist] + if data_batch[media_key][0].dtype == torch.float32 and not is_image_batch: + data_batch["is_preprocessed"] = ( + True # for video batch, is_processed = True means the video data is normalized. However, for the image batch, is_processed = True means the image data is augmented with a temporal dimension. + ) + else: + num_vision_items_per_sample = data_batch["num_vision_items_per_sample"] + + batch_size = ( + len(sample_vision_list) if num_vision_items_per_sample is None else len(num_vision_items_per_sample) + ) + + log_enc_time = False + timer = None + if TRAINING: + import wandb + + log_enc_time = iteration % self.log_enc_time_every_n == 0 and wandb.run + if log_enc_time: + timer = Timer(unit="s") + timer.start() + # Vision (image/video) raw state and tokenized latent state + self._normalize_video_databatch_inplace(data_batch) + self._augment_image_dim_inplace(data_batch) # converts each image tensor to (1, C, 1, H, W) + raw_state_vision = data_batch[self.input_image_key if is_image_batch else self.input_video_key] + x0_tokens_vision = [ + self.encode(raw_state_vision_i).contiguous().float() for raw_state_vision_i in raw_state_vision + ] + + frame_size = data_batch.get("image_size", None) + if frame_size is not None: + x0_tokens_vision = self._remove_padding_from_latent(x0_tokens_vision, frame_size) + + # Action – extract dense action / domain_id without mutating data_batch, + # so downstream callbacks can still read the original per-sample domain_ids. + raw_state_action, action_domain_id = self._normalize_action_databatch(data_batch) + x0_tokens_action = raw_state_action + raw_action_dim = data_batch.get("raw_action_dim", None) + + # Sound/audio - normalize, encode if present and sound_gen is enabled + self._normalize_sound_databatch_inplace(data_batch) + raw_state_sound = data_batch.get("sound", None) + if raw_state_sound is not None and self.tokenizer_sound_gen is not None: + x0_tokens_sound = [self.encode_sound(s).contiguous().float() for s in raw_state_sound] + else: + x0_tokens_sound = None + + # We pass the conditioning FPS along to the denoising function + # It will not be used for RoPE FPS modulation unless enabled in the training config + # Note: conditioning_fps from data is converted to TPS via temporal_compression_factor + # in VideoRopePosition3DEmb. + fps_raw = data_batch.get("conditioning_fps", None) + if isinstance(fps_raw, list): + fps_raw = torch.stack(fps_raw).flatten() # list of scalar tensors -> (B,) + fps_vision = fps_raw.to(**self.tensor_kwargs) if fps_raw is not None else None + fps_action = fps_raw.to(**self.tensor_kwargs) if fps_raw is not None else None + + # Sound FPS for RoPE alignment (constant, from config) + if x0_tokens_sound is not None: + sound_batch_size = len(x0_tokens_sound) + fps_sound = torch.full( + (sound_batch_size,), + self._get_sound_fps_for_rope(), + dtype=torch.float32, + ).to(**self.tensor_kwargs) + else: + fps_sound = None + + if TRAINING and log_enc_time and timer is not None: + timer.end() + elapsed = timer.get_cuda_time() + h, w = raw_state_vision[0].shape[-2], raw_state_vision[0].shape[-1] + resolution_label = "unknown" + for res_name, aspect_ratios in VIDEO_RES_SIZE_INFO.items(): + if (h, w) in aspect_ratios.values(): + resolution_label = res_name + if res_name == "704": + # 720 shares some aspect ratios with 704 (e.g., 1:1 at 960x960); prefer 720. + if (h, w) in VIDEO_RES_SIZE_INFO.get("720", {}).values(): + resolution_label = "720" + break + wandb.log( + { + f"timer/encoding_{resolution_label}p": elapsed, + "timer/encoding": elapsed, + }, + step=iteration, + ) + return GenerationDataClean( + batch_size=batch_size, + is_image_batch=is_image_batch, + raw_state_vision=raw_state_vision, + raw_state_action=raw_state_action, + raw_state_sound=raw_state_sound, + x0_tokens_vision=x0_tokens_vision, + x0_tokens_action=x0_tokens_action, + x0_tokens_sound=x0_tokens_sound, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + action_domain_id=action_domain_id, + num_vision_items_per_sample=num_vision_items_per_sample, + raw_action_dim=raw_action_dim, + ) + + def _normalize_video_databatch_inplace( + self, data_batch: dict[str, torch.Tensor], input_key: str | None = None + ) -> None: + """ + Normalizes video data in-place on a CUDA device to reduce data loading overhead. + + This function modifies the video data tensor within the provided data_batch dictionary + in-place, scaling the uint8 data from the range [0, 255] to the normalized range [-1, 1]. + + Args: + data_batch (dict[str, Tensor]): A dictionary containing the video data under a specific key. + This tensor is expected to be on a CUDA device and have dtype of torch.uint8. + input_key (str | None): The key for the video tensor in the data_batch. Defaults to + `self.input_video_key` if not provided. + + Side Effects: + Modifies the tensor at `input_key` within `data_batch` in-place. + + Note: + This operation is performed directly on the CUDA device to avoid the overhead associated + with moving data to/from the GPU. Ensure that the tensor is already on the appropriate device + and has the correct dtype (torch.uint8) to avoid unexpected behaviors. + """ + IS_PREPROCESSED_KEY = "is_preprocessed" + input_key = self.input_video_key if input_key is None else input_key + # only handle video batch + if input_key in data_batch: + if IS_PREPROCESSED_KEY in data_batch and data_batch[IS_PREPROCESSED_KEY] is True: + for i in range(len(data_batch[input_key])): + assert torch.is_floating_point(data_batch[input_key][i]), "Video data is not in float format." + assert torch.all((data_batch[input_key][i] >= -1.0001) & (data_batch[input_key][i] <= 1.0001)), ( + f"Video data is not in the range [-1, 1]. get data range " + f"[{data_batch[input_key][i].min()}, {data_batch[input_key][i].max()}]" + ) + else: + for i in range(len(data_batch[input_key])): + item = data_batch[input_key][i] + if isinstance(item, torch.Tensor): + item = [item] + assert item[0].dtype == torch.uint8, "Video data is not in uint8 format." + data_batch[input_key][i] = torch.stack(item).to(**self.tensor_kwargs) / 127.5 - 1.0 + data_batch[IS_PREPROCESSED_KEY] = True + + def _normalize_action_databatch( + self, data_batch: dict[str, torch.Tensor] + ) -> tuple[list[torch.Tensor] | None, list[torch.Tensor] | None]: + """Extract dense action and domain_id lists from the data batch. + + The joint dataloader produces action and domain_id data as + ``[[tensor], [None], [tensor], ...]`` (each sample wrapped in a + single-element list). This method unwraps inner lists and filters + out ``None`` entries to produce dense lists suitable for the model, + **without mutating** ``data_batch``. + + Returns: + (dense_action, dense_domain_id): Each is a list of device tensors + containing only non-None entries, or ``None`` if all entries are + ``None`` / the key is absent. + """ + dense_action = unwrap_and_densify(data_batch.get("action", None), self.tensor_kwargs) + dense_domain_id = unwrap_and_densify( + data_batch.get("domain_id", None), {"device": self.tensor_kwargs["device"]} + ) + return dense_action, dense_domain_id + + def _normalize_sound_databatch_inplace(self, data_batch: dict[str, torch.Tensor]) -> None: + """Flatten and densify nested sound lists in-place. + + The joint dataloader produces sound data as + ``[[tensor], [None], [tensor], ...]`` (each sample wrapped in a single-element + list). This method: + + 1. Unwraps inner lists: ``[[t], [None], [t]]`` -> ``[t, None, t]`` + 2. Clears ``sequence_plan.has_sound`` for samples whose sound is ``None`` + (kept aligned by ``custom_collate_fn`` preserving ``None`` placeholders). + 3. Filters out None entries: ``[t, None, t]`` -> ``[t, t]`` + 4. Moves tensors to the model device. + 5. Sets ``data_batch["sound"]`` to ``None`` if no valid sound data remains. + + Alignment invariant: ``custom_collate_fn`` keeps the ``"sound"`` key + as a list with ``None`` placeholders for samples that lack audio (e.g. + audio-extraction failures), so the unwrapped ``raw_state_sound`` is + 1:1 with ``sequence_plan``. ``SoundSequencePlanBuilder`` already sets + each plan's ``has_sound`` according to that sample's actual sound + presence, so clearing flags for ``None`` slots here is just defensive. + """ + raw_state_sound = data_batch.get("sound", None) + sequence_plans = data_batch.get("sequence_plan", None) + sound_enabled = self.tokenizer_sound_gen is not None + + def _disable_sound_on_plans() -> None: + if isinstance(sequence_plans, list): + for plan in sequence_plans: + if hasattr(plan, "has_sound"): + plan.has_sound = False + plan.condition_frame_indexes_sound = [] + + if not isinstance(raw_state_sound, list) or len(raw_state_sound) == 0: + # No sound entries at all (image-only batches, or every sample + # came from a non-audio stream). Defensively clear has_sound on + # any plan that somehow has it set so packing does not look up + # missing tensors. + _disable_sound_on_plans() + data_batch["sound"] = None + return + + # Unwrap single-element inner lists produced by IterativeJointDataLoader + if isinstance(raw_state_sound[0], list): + raw_state_sound = [item[0] if isinstance(item, list) else item for item in raw_state_sound] + + if not sound_enabled: + # Model is not configured for sound generation: drop tensors and + # clear any has_sound flags so packing skips the sound path. + _disable_sound_on_plans() + data_batch["sound"] = None + return + + if isinstance(sequence_plans, list): + if len(sequence_plans) == len(raw_state_sound): + # Expected path: 1:1 alignment between plans and per-sample + # sound slots. Clear has_sound where the per-sample tensor + # is None so sequence_packing's idx_sound counter stays in + # sync with the filtered dense list. + for plan, sound in zip(sequence_plans, raw_state_sound, strict=True): + if hasattr(plan, "has_sound") and sound is None: + plan.has_sound = False + plan.condition_frame_indexes_sound = [] + else: + # Length mismatch can only happen if some upstream code path + # (e.g. a stale collate that drops "sound" when any sample is + # None) leaves the dense list shorter than the plans. Without + # 1:1 alignment we cannot safely associate tensors with plans, + # so we conservatively disable sound for the whole batch. + # This trades a small amount of training signal for guaranteed + # correctness — better than silently feeding sound from one + # sample into another sample's plan. + log.warning( + f"Sound/plan length mismatch ({len(sequence_plans)} plans vs " + f"{len(raw_state_sound)} sound entries). Disabling sound for " + "this batch. Check that custom_collate_fn preserves the " + "'sound' key with None placeholders." + ) + _disable_sound_on_plans() + data_batch["sound"] = None + return + + # Filter out None entries (samples without audio) and move to device. + # After the alignment step above, the remaining dense list has the + # same cardinality as plans with has_sound=True. + raw_state_sound = [ + s.to(self.tensor_kwargs["device"]) for s in raw_state_sound if s is not None + ] # list of [C,T_audio] + + if len(raw_state_sound) == 0: + _disable_sound_on_plans() + data_batch["sound"] = None + else: + data_batch["sound"] = raw_state_sound + + def _augment_image_dim_inplace(self, data_batch: dict[str, torch.Tensor], input_key: str = None) -> None: + """ + Augments image tensors by adding a temporal dimension (B, C, H, W) -> (B, C, 1, H, W). + + Args: + data_batch (dict[str, Tensor]): A dictionary containing the image data. + input_key (str | None): The key for the image tensor. Defaults to `self.input_image_key`. + + Side Effects: + Modifies the tensor at `input_key` within `data_batch` in-place. + """ + IS_PREPROCESSED_KEY = "is_preprocessed" + + input_key = self.input_image_key if input_key is None else input_key + if input_key in data_batch: + # Check if the data has already been augmented and avoid re-augmenting + if IS_PREPROCESSED_KEY in data_batch and data_batch[IS_PREPROCESSED_KEY] is True: + for i in range(len(data_batch[input_key])): + assert data_batch[input_key][i].shape[2] == 1, ( + f"Image data is claimed be augmented while its shape is {data_batch[input_key][i].shape} for sample {i}" + ) + return + else: + new_image_tensor_list = [] + for i in range(len(data_batch[input_key])): + for img_tensor in data_batch[input_key][i]: + img_tensor = rearrange(img_tensor, "c h w -> 1 c 1 h w").contiguous() + if img_tensor.dtype == torch.uint8: + img_tensor = img_tensor.to(**self.tensor_kwargs) / 127.5 - 1.0 + new_image_tensor_list.append(img_tensor) + data_batch[input_key] = new_image_tensor_list + data_batch[IS_PREPROCESSED_KEY] = True + + # ------------------ Checkpointing ------------------ + + def state_dict(self, prefix: str = "", **kwargs) -> Dict[str, Any]: + final_state_dict = self.net.state_dict(prefix=prefix + "net.", **kwargs) + if self.config.ema.enabled: + ema_state_dict = self.net_ema.state_dict(prefix=prefix + "net_ema.", **kwargs) + final_state_dict.update(ema_state_dict) + return final_state_dict + + def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True, assign: bool = False): + """ + Loads a state dictionary into the model and optionally its EMA counterpart. + + Parameters: + state_dict (Mapping[str, Any]): A dictionary containing separate state + dictionaries for the model and potentially for an EMA version of the model + under the keys 'net' and 'net_ema', respectively. + strict (bool, optional): If True, the method will enforce that the keys in + the state dict match exactly those in the model and EMA model (if applicable). + Defaults to True. + assign (bool, optional): If True and in strict mode, will assign the state dictionary + directly rather than matching keys one-by-one. This is typically used when loading + parts of state dicts or using customized loading procedures. Defaults to False. + """ + if not strict: + raise ValueError("Strict mode is required for OmniMoTModel load_state_dict") + if assign: + raise ValueError("Assign mode is not supported for OmniMoTModel load_state_dict") + + _reg_state_dict = collections.OrderedDict() + _ema_state_dict = collections.OrderedDict() + for k, v in state_dict.items(): + if k.startswith("net."): + _reg_state_dict[k.replace("net.", "")] = v + elif k.startswith("net_ema."): + _ema_state_dict[k.replace("net_ema.", "")] = v + + state_dict = _reg_state_dict + + reg_results: _IncompatibleKeys = self.net.load_state_dict(_reg_state_dict, strict=True, assign=False) + missing_keys = reg_results.missing_keys + unexpected_keys = reg_results.unexpected_keys + + if self.config.ema.enabled: + ema_results: _IncompatibleKeys = self.net_ema.load_state_dict(_ema_state_dict, strict=True, assign=False) + missing_keys += ema_results.missing_keys + unexpected_keys += ema_results.unexpected_keys + else: + assert len(_ema_state_dict) == 0, f"EMA is disabled but EMA state dict is not empty: {len(_ema_state_dict)}" + + return _IncompatibleKeys(missing_keys=missing_keys, unexpected_keys=unexpected_keys) + + # ------------------ public methods ------------------ + + def ema_beta(self, iteration: int) -> float: + """ + Calculate the beta value for EMA update. + weights = weights * beta + (1 - beta) * new_weights + + Args: + iteration (int): Current iteration number. + + Returns: + float: The calculated beta value. + """ + iteration = iteration + self.config.ema.iteration_shift + if iteration < 1: + return 0.0 + return (1 - 1 / (iteration + 1)) ** (self.ema_exp_coefficient + 1) + + def model_param_stats(self) -> Dict[str, int]: + return {"total_learnable_param_num": self._param_count} + + def is_image_batch(self, data_batch: dict[str, torch.Tensor]) -> bool: + """Check if the data_batch contains images (vs. videos). + + We handle two types of data_batch: one from a joint_dataloader where "dataset_name" can + differentiate image_batch and video_batch, another from a single dataloader which we + assume as video_data by default. + """ + is_image = self.input_image_key in data_batch + is_video = self.input_video_key in data_batch + assert is_image != is_video, ( + "Only one of the input_image_key or input_video_key should be present in the data_batch." + ) + return is_image + + def denoise( + self, + net: torch.nn.Module | None = None, + data_batch_packed: PackedSequence | None = None, + fps_vision: torch.Tensor | None = None, + fps_action: torch.Tensor | None = None, + fps_sound: torch.Tensor | None = None, + memory: MemoryState | None = None, + ) -> dict: + """ + Runs the MoT network on a packed multi-modal sequence to predict velocity (v) targets. + + Args: + data_batch_packed: PackedSequence from `pack_input_sequence(...)`. + fps_vision: Optional FPS tensor used for RoPE FPS modulation (if enabled in config). + fps_action: Optional FPS tensor used for action RoPE FPS modulation (if enabled in config). + fps_sound: Optional FPS tensor for sound RoPE modulation (e.g., sound_latent_fps=25). + memory: Optional pre-built MemoryState for autoregressive generation + or KV-cache training. + + Returns: + dict containing: + - "preds_vision": list[Tensor[C,T,H,W]], one per sample. + - "preds_action": Velocity prediction for action modality (if action_gen enabled). + - "preds_sound": Velocity prediction for sound modality (if sound_gen enabled). + - "lbl_metadata_und": Load balancing metadata for understanding pathway (if present). + - "lbl_metadata_gen": Load balancing metadata for generation pathway (if present). + """ + net = net or self.net + out_net = net( + packed_seq=data_batch_packed, + fps_vision=fps_vision, + fps_action=fps_action, + fps_sound=fps_sound, + memory=memory, + ) + output_dict = dict() + output_dict["preds_vision"] = out_net["preds_vision"] + if self.config.action_gen and "preds_action" in out_net: + output_dict["preds_action"] = out_net["preds_action"] + if self.config.sound_gen and "preds_sound" in out_net: + output_dict["preds_sound"] = out_net["preds_sound"] + for key, value in out_net.items(): + if "lbl_metadata_" in key: + output_dict[key] = value + + return output_dict + + @torch.no_grad() + def encode(self, state: torch.Tensor) -> torch.Tensor: + return self.tokenizer_vision_gen.encode(state) + + @torch.no_grad() + def decode(self, latent: torch.Tensor) -> torch.Tensor: + return self.tokenizer_vision_gen.decode(latent) + + @torch.no_grad() + def encode_sound(self, waveform: torch.Tensor) -> torch.Tensor: + """Encode audio waveform into latent tokens. + + Args: + waveform: Audio tensor of shape (C, N). A batch dim is added/removed + internally since AVAE expects (B, C, N). + Mono audio is duplicated to stereo if the tokenizer expects 2 channels. + """ + assert self.tokenizer_sound_gen is not None, "Sound tokenizer not initialized" + # Ensure correct number of channels (AVAE typically expects stereo) + expected_channels = self.tokenizer_sound_gen.audio_channels + if waveform.shape[0] == 1 and expected_channels == 2: + waveform = waveform.repeat(2, 1) # mono → stereo + elif waveform.shape[0] > expected_channels: + waveform = waveform[:expected_channels] + # AVAE expects (B, C, N) + latent = self.tokenizer_sound_gen.encode(waveform.unsqueeze(0)) # [1,sound_channels,T_sound] + return latent.squeeze(0) # [sound_channels,T_sound] + + @torch.no_grad() + def decode_sound(self, latent: torch.Tensor) -> torch.Tensor: + """Decode sound latent tokens back to waveform. + + Args: + latent: Sound latent tensor of shape (C, T). A batch dim is added/removed + internally since AVAE expects (B, C, T). + """ + assert self.tokenizer_sound_gen is not None, "Sound tokenizer not initialized" + # AVAE expects (B, C, T) + waveform = self.tokenizer_sound_gen.decode(latent.unsqueeze(0)) # [1,audio_channels,N_samples] + return waveform.squeeze(0) # [audio_channels,N_samples] + + def _get_sound_fps_for_rope(self) -> float: + """Compute the sound FPS to pass to RoPE for temporal alignment with video. + + Returns the sound tokenizer's latent rate (e.g., 25 Hz for 48kHz/1920 hop). + This is passed as input_fps to the sound RoPE's generate_embeddings(), where + the FPS modulation formula aligns sound indices with video indices. + """ + return float(self.config.sound_latent_fps) + + def get_video_height_width(self) -> Tuple[int, int]: + return VIDEO_RES_SIZE_INFO[self.config.resolution]["9,16"] + + def get_video_latent_height_width(self) -> Tuple[int, int]: + height, width = VIDEO_RES_SIZE_INFO[self.config.resolution]["9,16"] + return ( + height // self.tokenizer_vision_gen.spatial_compression_factor, + width // self.tokenizer_vision_gen.spatial_compression_factor, + ) + + def get_num_video_latent_frames(self) -> int: + return self.config.state_t + + @contextmanager + def ema_scope(self, context=None, is_cpu=False): + if self.config.ema.enabled: + # https://github.com/pytorch/pytorch/issues/144289 + for module in self.net.modules(): + if isinstance(module, FSDPModule): + module.reshard() + self.net_ema_worker.cache(self.net.parameters(), is_cpu=is_cpu) + self.net_ema_worker.copy_to(src_model=self.net_ema, tgt_model=self.net) + if context is not None: + log.info(f"{context}: Switched to EMA weights") + try: + yield None + finally: + if self.config.ema.enabled: + for module in self.net.modules(): + if isinstance(module, FSDPModule): + module.reshard() + self.net_ema_worker.restore(self.net.parameters()) + if context is not None: + log.info(f"{context}: Restored training weights") + + def add_lora( + self, + network: torch.nn.Module, + lora_rank: int, + lora_alpha: int, + lora_target_modules: str, + ) -> torch.nn.Module: + """Pre-FSDP LoRA injection — see :func:`inject_lora_pre_fsdp` for details.""" + from cosmos.utils.vfm.lora import inject_lora_pre_fsdp + + self.lora_alpha = lora_alpha + return inject_lora_pre_fsdp( + network, + lora_rank=lora_rank, + lora_alpha=lora_alpha, + lora_target_modules=lora_target_modules, + ) + + def _init_lora_weights_post_materialization(self, network: torch.nn.Module) -> None: + """Post-materialization LoRA init — see :func:`init_lora_weights_post_materialization`.""" + from cosmos.utils.vfm.lora import init_lora_weights_post_materialization + + init_lora_weights_post_materialization(network) + + +def _broadcast_seed(seed: list[int], group: dist.ProcessGroup, rank: int) -> list[int]: + global_src_rank = torch.distributed.get_global_rank(group, 0) + + if rank == 0: + seed_tensor = torch.tensor(seed, dtype=torch.int64, device=DEVICE) # [len(seed)] + else: + seed_tensor = torch.zeros(len(seed), dtype=torch.int64, device=DEVICE) # [len(seed)] + + torch.distributed.broadcast(seed_tensor, src=global_src_rank, group=group) + return seed_tensor.tolist() diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/flux_vae_8x8.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/flux_vae_8x8.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ece8001d3d35de285bde2dc093a0adba380e3549 GIT binary patch literal 26899 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 O;?OnqzXHDb?EVMX`S3FU literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/tokenization_qwen2.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/tokenization_qwen2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd2c4b3abd67a1fa38caca609d78496b3c70a847 GIT binary patch literal 16104 zcmc(GeQ;FQmFIi?-K~~dpXdV#9w00=h!!8VF&N7cAOSH5u!KJZxb@WUNovvRmfw3q zNO^Lo3^OCeWRT)y5F2MLSEXv?H9H}-*%~q*6+50xIWxOcEw?i2eZ$mvDygl_A6q3p zvQAuETl+ir_3Lg4VK!B%+6TJtzWZ_Rz2}~L&bjBD{)fUsABRVK;2iv$r#bEy^kF^D zOy+(Pnd_Xyb#oFg*O z+!-qv@^|}(3cCwgxhqyQ6zC4{oQ;zMgMu}1?k$(R&zoP}rIK6nNZz+?-DQ$bDv0vk z<&wX<0zIAYXF5oQS2!!*y-q5+!gW{LxFehtxWY-ra^d^-46Ld#Td-t&V^`vW9FLBR zS~L+CA~8`_WmV`)D8lJcIlepeD{9>l`%_xMPX_{%E{^+;=e%5qm>}Y9c=F8|?y_bxfz3k{y~kV_AuEmpN`c-uQfUe}k$wXnKPp>kSw2H>5YjVtRuV z)X%i%j^?qDT8I?ED8cmSo@e)X0ys9`#1o-Z!-Z~rH_H=$hOOk z;cMk5zg7#$r`s6DGB`&&8|GFB=(ekS9O!kJyJm~n`nibhJS){` zx7ed+u|8wF8gO8~m~DYotD&{|=zy|%M>+wI$KQl)lnWM!&tSuR0-hITZA6KyLM*Ck zLZVL?(fW2gBlM1GvLMDKK@^6>;b9QJpe2Nncr=obWI@#wl&B%$W#otDNVG2+5o572 zLA;oVN@{+W(SfKYtHWYMZjL14nv#gIQ6mGQB1SY>Q3Y*47J7$efkEySl|EHS#6v#T ztP6FDd=br}y|G*~RTxbu7trkC8FdXrF}^HfMPrE(Auh|bhM0)6Dr{lv%d8=92GKaR zRRng1*|^a@)_YWpYqV~*Ea6PY3$)|`nKj3m6+}%?6GO5#K=_G8FQ7!&weujlfHw(} zsEMkqF-S5{IWo+qLv2MRF@ln(ULcOeMwH};jKLxaELQB7Ljqd})*=dOv_H-;jQU|& zp>}9NYhkmFCzK&E)+DInSd*}4_X`Y}CZiI%ths9gqShpg$^u(bERnb{GR(%+L?Bib zdSz`?#+-AzkCAE4t9G3A`JCTm@NfKz2>91@?tk*G`MPPo{+zu&`f-OsmA~T9F6cHK zL|}$f5##-`VNWQM;R0a+FNQrbpcoD*8qo~HsY7@TD~UllqFLYJUSL105jzLUn@3boQS%svr#~&z zDV)F*RBS$@PXd$ckpVjdijOs4>>Fy%GH}((GU*d0?!G%TJZ2Qru>picID zPyII}m$?U3oUiDr{H;T?_P36{9huwmoq_q18?jW;_LR5jruM_Ozut0N`m5mIc79y` z(W}YRFMfDF^<-zNxog4OwfOSc#dG3fubk`~O!Zz^92!o(K9U-`m>e5l7#dHV`%3C% zeZi|=KA83vOtnr{Ami~~ZoN}jI{oCEXYX+iXW1m5F7Qtu{Q9$Lf6=vLQ^%&otH-UZ zG<$kxV9~!Z?Jt}znQEVEUG}2KvX5I|_fOus>C;!YOp4!lBI&KWd=SZ}4E4;IWmfbd3KYBUF>O33Lc?TGX+j-b73o z7Ng)gIP)TL5h=rIUPe~HaZ}`BMOJadh;M*7k(Q9_tO|`L6gn|o(HW$N-AuooBAsG%zvlOpvL|4PxFKc1e zQfb6@JN{HkKu&(Hu=vfk%Wdi6^6Aqv?#stmX8Ofx&ppoOte$kFJ%!i2Q{L%)vr@8j z%c7?~U0yZiU3Q{!*~R%wE+4-SNL+*x>#@Ym{l7=aA7j5{4oV|O3S+ljc62*wfBJ3R zE-#n=PpX*Y`k+2u@pijqkK~lSeKyH;#bNC+lt?#}x&q0A)Gv9F7D_&(MN$FMfaFJ7 zY(kXQNriHWR3w+mB}5fc0AEhItk1*7D85n%1+r2qk;|k~v?*h4oN|Sk>vKtESDa8H z%K@=+lDourSAi<3j3TBRc3FCu_+OzvvyD2Gh=Z|=L^KcuYh$u-(*HujLGTfYgbkf; zZ1hDrCLE;fDNxdmwm1YANFhT`+Y*bD*v)2N4E9T)o6`8G{0R zh#9apk|-oBNghL-42y~eaoER%Becp$Y|V5o6t;2PHWyA%nqG} z)VTSm9E&CBAjhQ5!LysA@!=6I3>{?idDi;uzP&s5HVM0)*}L!j8Z&9!EIbat?p^zK zXCaUn0wx$}OF+w)paFqhWC-9@XdNoh7?TKl0>sWrN&x3kHCdFJu>BE{sx#Z0-8q2} zk$<~UMj)cI8qo?eipH_QQP2q6jk7(c9=^czoChI9BLmPapg3T30aA`JL?VYFlt5p? z&Pq^j&XAC_rmx7LBn;)L1B1+MI9EAjs0%9Dca|i8D zHu(bM8VtsnuJ#?zgoI96&LV-)u95f!C?}+wK7PFibm(S@K25%)=`9bfx(mAk zXz|1-I2b_;39V)Z8wO4TA|>hvZLQkBh#02|a4e7`bX}6Oz?>KtjvZziCwh{+Mj6@= z)vZ95Is42J$}Oyy^tMMVY!zM6%zN{BpenyU0`7^y%yo2NdeBT`xYE#_gG>r5d3SDD z`3r_!O+SyvC*2W9O<6gg?DB_2>5*&v-3K$V1@D{2vT zzp>`UdwLUzSkD(6w01G3%Bv3k(G9xiE9EB(pur#caEk?z*YMRNQh;>0&2t?t6G(tC8>YrT) zt%YI9GBV+Q0l^F|xr#zSsw)Wucd=-epse0f0z#cgAJfUqF^4lYFW?AQX)$IZj(F1V z3s)Jy=5n(99p{UUo45E!ESSzrh8yDS42Tz_I1~?^Otv}`;>rOtms#^xZnB3yp=ZT` z=pl|lyVsLFQy>x)#D$*BCO{Rla*dl;blw~ko?EePXB$@3_nZ$2CjmBl^4le{i|}HlKeb)14Y!+u38;kd)$_MNN0Y77RdEGhT z%vtAj*JEI~)`a1jaOJ^pJO+kiO&E>|M;;9KV_>+~gyEiWYgyZ#jw{Vgr-g~9xQT+C zJ#H``rx~2-1@C9BzB(7C`!IVXSm33sl@!-iZqRSl<=%sVT;9rA*C$v7{nrX3_9)o} zJsfuqyENgSDAWse|68`JJ_i@EPZUo0F)C`!hpd%h>pI726;`%=f=1#+%8V#M`6)^74}7*o93_nhfDTXxlx{3 zx!Fk=H7(wm{OTT8e`dB1`a9aYV3Bdf$f`YTxD@m4&d4ysXBt>x3y?l{GW+Ozyy0Q@ zZKw>)#53;Q!fe!u_#+}5E{kStnJu`qBe!8TDt#;;D zrWNKNhul*{O*Wh+=6x6xW)HGPC`9IllUd76wjQO165?lu9SSJ$F)W8+inyQ} zPUh<~Y>I5SBZ;A5^2n8(dat zT_l{K6??;BcF_%ojf(I}vr#x4hmi-qQDi`%m%;LxflpOuyci#(_`}$ zw?;mYk}se8x&Qp9B{j=#&RhDR=>JDEKUY*by=`uEv9NiWY2;KD!WnHWhqssj}tW^0|_^)>(db*9}LivSre7)t_Eh zi&w#&+NM-(XwtFhsZNJ>FNL082tB_PYFh}kr9wxRYLC7v&583Rv#00y8(pc|qh`a3 z%1Pgy!jkFj^X|pMoy(llyZ6?KJL~J`&ZpMzftkHoZ8Tqs&2>pi(s8!SA~r4mt_^#_s{HK+|ZmV z+ld0trv)X~_D}6ku4|m%x>&IDj=yxtzi|Q9{j$pIEi)}kWsM7EjmgqrGB}(pe*J;n zR#^1F!+DFQ+zXznWzOsMrOT?*>o(^82iww{p1K=A&2ljpsJLH(+-G}%@w>De|N9q} zEx*0*;_8m^pE)pZhL+D&5}QA%Tz{;_{!wM&u?E-Q3WdjZxPIzv-Fj?~{bx1KWA*Nz z?QtGE0LBKB7u=95yasmkroa15NLHw*++ao^q9XfPBCV5qkB7Nk=|GKg7|DCQat<$* z)H1;|sb}2vDwz6s^{S&`9z?22QLhptm$_dC;6}NAVCFz7AS6A4LLz*{WJwcdlO=Kgxr4liATCjB<93&D;$wUaAbBDt}?jSHM;?HZlAcslc<4B)5EXv^?q6Asyv^K+BO9eNrt;^p%6Mmuu@@K-!HZI)&};m{oz zHLLqtusHbZ6+Zrtx`S!V&IuQEP`6~C@aUeL?>sl+1*}fF=7m>&FzZjx!GcEO$&TpN z-D~N`a7{s9so|`ZLEHCYG?bF)(w$5@eR}QDCC4}<2b;rI>Va?cVf{?4wgzJoyI=K? zrnD-m04^97Rp5zNKcY`&MtSq3@v!PI#0`J-S}sCD&uY^XZrz<9jgZ5rRZn#mB9KVcSqv#cjr~^YIKy#XoK#K z6~~&0p3r@txCvd9r}(uoDxFp5v6C1aRdH62sXCor(gA9OUpb z`4$~Q!4&47dk)0JpCi3h{g8|_!b zA*>v3S!iP_?B;;tiOZv5a*`?$YQ+z#I2T+`R>Lv*xE-q=FMSvjchFY749x~NXXS;m`lP@9PN01H%$xe8<4$q)Z2en-N$0->%F|_4lgB?TD!o%t zHCvIYe|fRumC5#WpklV`o!4%>wh-8!F0M$|Zk{?Z**5)jy0UumXu7y$vNc^=_N@qj zM8COX+2$^(NmtdrNZut4qxuc8aJN{dFHGIq^HJ_`c*ohB!m9}}gpL*-sg7%*_@<<~@ zTh>FS7A9EAv0)@wz@Tk8BhFf~9?W13(U}W+g2_On*a2zH5G*Dw!y5)B(IO-3WqtZB`~tk))Ezx(}Mi)STH0+5YGVC7Icl5cG0nc zP9lhz7=w_pQUM_h?!n)EVu;`7U*LEW(cF1(uL*w2wieei#qpo=*LjRp;lQ=}KiWrm zQ}y7LBY+dM8@A9+WdpJ%FQQQdiy3(j^viL$Kb4m8?W@k9W&F?QNmkAhD5q&ycOQ5qBIT4Wd?{a{B3I^GJVqs7qvQmVpu=RB4)RWz7eRBjijG=_7vhDP z0Ff-VA5!|VHEx>d1o{HwpRzklMdkJGneI8qpLyQ(%-5$XnsIsYp5Z6k(&g)}ADKCF zpY2O%&-Y{R#cuCSg%07GP+Nr3ilWIj1dkL}F8S*g@V~C>cKMI1epL0r*1y`gw69}f zU&qI4YG3DKUDwb3T?iurP%yvZnxv=t3$)71m>Uu7T7(h?`_AA zi+)s;YB-V#98G$TK8$NRy!3aUtP#H(t}R@F8S&%KGxksp)o7;Ssc31~a{FA}14Q>{XA12)MPbF>tpK zHQ^z3I2+DuM>wxze1C3Bat>q)EFrN$QEMz>H?uoL!Eo=IWg+DXKr#j!Z%JqdV7y`#uFY-SOqfMXI|+mb*a z)acY3X@AAC-Bs-SRM?Y-#b?`*yIy>Odj~4+7Eq3Jd422z8AYmiQ4UK{1*YkQGG+uQ zGKUPK8RK^(dq^+_sacHkoI(og2tLhcE#+=7AFr8qx#OqSXpr)m-fO9EkpJ*5*uI-R zWaKY+%!q5KbUOA*akAKa3>JqMQ1e^kN=`b??7nRsN&D}41lL0&IJB&HP`A_XX>NM0 z-^9Qx>C~NrWU5;AXi)#mXRzGL<=$X4Yxw-=7dn#vX5sswS$d=*w$cn!#BYDtKZ_aR zI{?3JZf=f;<_62aT62?%EvdqD8ZDPSm{X0F&%M9M=O>k1;ck{%^MUFQ1&Y~vJQH5{ zkGvPau$1+ARI`>>uuOM=YRdDe238w*pVpjt#afpDScWnY$q16Jc4+=C676x#@Tv$y2*Wl2F9n3jh$4qYH4=>)ywCK06=l6& z!p_ic-$Qyj(cpbLJfw4&VcyLA!6KtgwVXF>7cS-*G8F~Q$9JycT#V%~8zx)n!}gAL zVrp~?yUZnDJ)1mte&N;EE}QlZMJ0$8od{lm{+0tm&bIOH{ldXhZDu$jX*5xaz!ecY zL;*XHO)yG~Xo&F$38xUaIoqnY16~m&)re&%Av7+|0SOzvc&t+3>vjN!{*`cQ2mqO-2$6r-v76 zhws~M>wJ^G2QD~5zM%E+!s*u+YG1cndzde2HZp%?vG7UqCPnz$Jbw6a?a0)TZ!uF5 zTs8U){Z{J-<;kvB7t6bo=f#C`anavPPNoQt+fPa9q?6o0+p=JM-u1U#m^fQd^|nR- z_B-2m-KzhK&<{dOyW1Cbx2JX=UFdjiaeMe4=P6vjP~0%>n|%S+rr8%hDQmb>UUR); zrXyX`e9JelV^RlB@JOpq@DJ?vign8#uC8$!asKPB`)B;Ko$tJI_Vdp?tE)d(i2?s)+tclZ z_K%)9=xMLE|6QT8eVzO7s-5jy-7D-hBo>&eL(v5Ju8y^Yxdd@+v1YoO>o#I^%=X#Q zeh7xng3M24=+{z)H|uMMPQ-3eq*yWyMBL4kHT6yc}fW>>koAAJyb(D52pgVpeW+^6n&F#Bt3OZ*@5TXor(Ybyl_QmS%>o1G(k$* z!86<2+fDOHHTi{^HWW5|;V^;~5yK5%vOgSteMF39YTV&4!hFMFfz7kO{Efe;=aUAzOtQ~QqqM#^$wEXT;cB8b~-)F z63FG0v2;^<>$Y^==3iGF;hnza7j2xYXxYX) zH_aZp$KiF~ZWY|~@XqJ&c^aMN_g>;X&YFAcY|dl+J)h0laL?y(mM@obfVB5OI<)6* zK`~&txDA_^-IVii4bAh~yYXc&efhY?re)Tmi1QWxI)JjfPpo%7F&|mx@cQ8B$DJRZ zVA!g*Qr9G&O6NmbnloATbdsxBc5oG6rqa7E&Q|aT>+d?s_`3T} d&Q`cgxqG1kzT`pemmK_tWJTkzIm)nE{$J5T?E3%! literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt1_vae_4x8x8.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt1_vae_4x8x8.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13b5f108fc3504549b3a27351ed88e00e145b85c GIT binary patch literal 40495 zcmeIb34B{edMEfEZUP`cfCMk`07YHYL5VtSN!CT3)@jp{d{74r5gvXr1iCvBKW62OgoPqn*so{<0~Fg?d%>s_IpJ^%zxp0St3ir0_88i9JlA7tHM1Cfx230`wSa{U-PRsk ztBr+?-S(cs*1{e~tAjn8x}7~mtwk(s?soOKTipm-T8nkuE-p~;5*M%r4PA@mrsaO$ z(u?6%56fvoPCIfg&E=3oO3pHtvk*BQ$hksJ&;2Sn%UMn*auz+H%nFv%g`DmOl zi;=VB0XgTeoF3#XeL&7Cma_~w%O8-lI#BTv*IL8!S0ewM2js6U;N*XS`ao6C8>kM} zwHpF8FBx0svQlbMiuWx*2)WjJ7FUP3xk0zmkMqv!8s>dM85;Pkt#_ciyREM`96T33 zfad}s=obWkZ$}Uh`tH7t23^+D5e)kfcPi@&o$qb)_4EaT-M&!R9}b3)%N+EF&igw1 z5G;GTGZ5@M)ORY_+c_8%cB5FK-QO1cgj&=_Rmx5R6mF&wF~13b4sxx0fCCWot@>fE zoe$_BxE9|-xoxApa&Srr)cTD@>$ly_CYi_baJ z*F6yG?CWi0No~AbEq=}TbK-Ai3f}MVm$k77o+28phWZACwxArNydf3K0CD-5_MYWo8RSCC z&-!~;_J^B%XZ%6m>T_$)t!?Z-FSyXqL5~`08s!h4kEBo#-Y;L|ZdwXQ%t=dmbY8;J z5LD8qZr>`!bT>b8b?`-&h|IX$Qw*P3~hx-!GwSM?S z;@I(oKbZFSB>g>!-oB*2KY6S_A%qg;;bFst!c3WW*zh&`3@YNMT$0tX_xE~+KZMffgLUKAdc(skhiIkk(C zCHYDGjp!nLhc2MMW{}a%5FgP8_;b2*B8=<0?6cpXr+^5>Wz+|4x($d4(_Cb(t4I#z zej_L|SAN(nr;GsRzt%uARcNf+fD|3 z=LBktfDhq9)VG)g-WP>(idz70uw)jjNyRLt+`j>jK+Rn`8oo3%Hk5MDA2w%f?u4x- zw)%~YS2j+#lJ%=5{5R?!&6HMO>Kf~cx!x$hQl2VZJiO~>k>^suSV8pBSaWn~ym_jq zakx2SFCJ}vc~5lV>=>;ZeI{kE9_BxGmW-Soc_Df_ z<*Z9s>I6b3SNQ{1&4dGb{XIe80IQFX`#?8^?LOb>0e`oYV)pq0eQiD;n@Q{wvxO2K zXbX0C`+Qeubw}>4<<1dis#b1cJ>X$EZw{<$B<&H&ftozJrG0wfs!DDS3V8D>IJhlIzueVR=X;Z_zGRqp}S*BDoVg@WB z{Vgd5IA0$E9qI)Q8Zt(7f(ek$M~tuWe`x9j)VK3PCP2Rm=SKK|e$YbnAcvLjKYPN3 za`jyfIh6a2m}IDye^D~Z7hS|4G(L3c%8ZfUMk}>SS7!Kmk)EiYF|Qi*0yQ(|HRy8T zpNmD`+AIaYSJ>F}VX%@dY&+@i?FGZo-YJA=nzi}6gIQf~Z?@n`zW}%ck08{cjI5!r zHyFwqJN!L8{;U!FLtiLsIML}3HCWj66RN2Vk5E=qXOF)l7!o$pgZqTg=MS{`!I7K* z=wil)gnG)#W+(L<%}zLJf-+*@lEFkF1XOV7JUkU4n6gy>!riSTh)t)4ak-8lt=5wo&~12-t?4RIy!dr_4#iszOp!eI5l@w%Cnjh ze!O4oRp#fg&Yq=hagS60}LKM`EPJS72+C9!aV$B3uq?# zyNMg5{uLf2?=kXd=m189SA~V}LiF8UuL_42DMKXT&n=W~6Fi_#XVJwkjC>*56gQ+7 zt-rBo{gtNcx+&-O>*4nY-y2LiTM~z!O&>a*#DAwqm%EbwuB5XoVd)Z4pZ?IHomWl5 z5)>zpfFdj-k6?qLH(@!2NFE8PP})k0BySBR5a|L+`-RS8+{yn<32!{bi>Qop%tB@I zn9kH7MUBcBd&`5>1xf=7GoUZQa3Agu`Fr}igW9lg6lE|*0l%Uf0XiZS29i#QF3-t` zO-3eN!~*BJS%j1y++$@E2HwujbKVo}-F^OW(<5H+jKth|F&^Gt2nxYa7{8qzCr==v zleqooTf$Rm=q%V)>We)D|F*^qWNC7n$Zbt&iSgk?1w5YX?uai*fRhj=HYkhdLw zA;Qxb;?Hyjo;bjiu06nOk)l1o0dPwle7i1ScnJd?FoJFw119(;HuPo;J!tV$ zL22lzCuB#Q7(LIF!>6H-U`n6Cl-a|5nGegkI3~XU0OSD&B?C&x$p(ySDdnOJ-KjwSu<@mBGnxt0~%f`W`E5%d@IdHPB6t zZvOXbOPH8M=FoTa1?bO!Env@m0a~uKjJ3RwwS4CTTAt&kOXV7JzX69lIaQ_Ht5?kd z=e42-_NR-r>WHd5rPaB5RjL2oTb0+dwtzcOe68dy^e?&Dy{c*#@zi-vhVR)h)ZjS) z)qtTFTtwD7AYsszfn0c1AZriwo$VDT7*bSEs{+CS)Y71nnV2oYQ+OUMl+fI=;5mO= z7`WNa_!l3v8ju~b1`!m`k&1G7GNNVx+@1JeD62+Q^|}#9boX(CgEK4HOJZT$7skTW*jI8-$i^) zo|9+RQREpi3>k+^L*^mNy>twOVMcl3hY{T<|D_Kh{WHmbr~q`PfW*reO9IeQL<%+_ zOsTSNAYd2-U6nNeqkQjMz_d_fe?=$yVWr(RRjioSZlsd6SQxqOL)M73OM{6Gur~w7 zQ@5c{uH=V;ZT@*ia9L}hLT&|GLyRXQVVH9=DC1T#`0anXXZJynkH_Z zli!F~p=~iA7%@f65zAX9F>#1j^-N{n3wTuE zSb}6VTuAV1qN4#_U}sSGd(c>ZO9M~LMN2h=5_v{H%m^OL^{F^B zF(+kwBK7FQU=B^#Q;oTXPE&@tStEIm+L_K&;h^PNgkF0nD20Sjc!2R0483RdPX+r$ zJq+U$nlV~T_+~uEugXt&+q_}#N$*+jvQ6GaC%oYXMBs6-T=hU{N=7J5`LM`f6hZe- zQajQ0ta)4Ww#Rq)4rPsP-C(ao?MfY0H4f{u3qsNIK_dN^7F0$4-svGsV@>wz18L;>DQ-OGfuZLt{^5N~)qQuk4LA zB}?Z0&fZ%!wXgB7HNAc|cJfN+JFa({6W%q+nn%(#8N)mX~MovPlQuHKid-uJ;XKRW*5@l^HE#PMLFsy)%&b4RbMuZ)_( z@08a*T#Yxy8gEw5O;;~VRxgWpq^ciDSFcM}ubW)>ZsT_vQQHTPCaMl%r9)Nv+*tkC z3-S3k%9dxUyq6D*ABZ=ls+NuOx8$dI*JSb4zEstwD4$ud?Cx%RW$!JFL&11KtS;6P z=i^Or|AZ@5zG}4jW_k5x+qf;(^4jTm9d*GwP4D;<3m;9+Upra%r_Gb6zgPFYmOtbF ztSPZ!UvmBa#JmHECtFhGPhk*emDqgM7(WeNK>2EC%->LPH((;sstYgVLd9s!)BYSxbKnznLSZgPw}ULSbl{FU<)izg4h zy_`Pu1Ko%EKWn-1WXtvC$(2tf4joQ3fzy31wXii+egq$;-M1NM(XfTFgR}HVglHKf zI0XifF@lK5cSE``=85RE7C^Pobwnp4G$EwXTL$)2OX)rfeM9vh(6t*G+rW3_K0pa+m7vKCsruw|w42vr3AO6Z^*k1U^`bVG z5s3zC)^wIsbYW5UGthEx)+8clp$#7-D&O_N^TDhk;15If-qzQD-X|((tqe714NM5M zk?#{r%j(*i@VAm7WuceK>4TTGfoWo>L}S$qf)iIS1Dq~?_}hy@Oo$f~bK z_L>+=dhgI{@PM*QD=(cIJ2h;(X|Y~3kC-nyMjWGGn6lJoD(9!M=vBEmRk?J;IjkGr zI?9hWjrwnTDla`Z_FPnmtxS0q3|lj$-n(DEwRp|2Eo~{ss_(0=SDT}!2^B@Fla_@U zi~XW)L`q9rYLb?kD5_hcu0DL}{Mh+RUmW}5N0tTmnQ7PWGt;y*`Ivo*2&AQ{*P2^- zvriFX(07uP*{QtQM=DO{wBU-CbV-Rfdk#WfM*XVtIYQ`)miqwZO=3TqrWHj6ShSOoz;!Wr^Nu_A=X3zMfzEMb5i z&yvShJPJf!=Nv_xCy!Q8gfEbHf;?p=2}5`c5fW8q4dZRHcGdJ)Iq1=vH7oys-u)AJ z%Ct$BE=ZOxxEucOIeALwTvLwS2w?^y6c8%pHeV&p8lUd_#Km}s6@HN!O-U7xn|R^!6IDK9WC6xI6l4vn1Rh)$B5X@sX%>jz+t z1S2JB30ke#SC*np*+Ps>X8?=nTf6((PPHk3in2&&M}^P)5l}OaT!?JWFq_gqJlO?A&)))U%qm3w`~_#`LcYV1#3o#S%J+ z)wZ^Quw;KCe1+crDkYf%;w$2+8A<$vCDc0C(pkW#C|>^+vW5sfK=G>9w3b#u#cG3$ zf*(6niWPG%7mgR+w3m!N9osQwUwqT}zM7mI^D%FPwIB8@KV-J8o>)|1Ep$>G(4f{#5x| zEP)qJ9Xy;qcszOVc+1g4)*LY8Dr46s;IT^Nw%n`Geo>A1LVucQrMtBb7 zl^5kOW5L&4#nc^gUoMJ5o?UhO+eBlS;+cFjy;x9}Ypx=oYh*%!vu9`&swu&BCAwgVV zu?xRN;SqRQYiFpl7uts2wji5nLV%Kj_xTgMc=XhtPqCU25y!K7ko6T{7r0RH0vSMwz zVr#Nu>-DBo#g25vVjM@(K8tDU!T*@F!c2{X*w7`uXX(5(WQ^D>8aSvQ0VL@TWMP6q=y{nnku} zTNmqT4W1EfvMV-Z9nIk|t!?%7GKxL8%1VPOSr(Hi>@vTc!F#qdeA4Up0>C=DgWi)t ze?T%emMf<=5L#hqYAGg49+{VHHe^JQzgQoZ$Qvn(<%s}7WT0ZnJwR%v#cQ4|Uh7n( zPbu>u-d4Y-pt!KYWM~m?O~k=z8Fto$?zEYmkMHD7>L|n@hDJa<%iDdOT zq-0IuKHuq6XR-z|H&*fD0up6Q6ViEWSej7DgJrWvUm=+{kr*N@kVi7b#YxagPj&R* zEBk4Q@@8o{A+7??C=TjxH8M{z`7Yl5PAj>4l zblE~E2S2hbVsJf6YePhdL6RNtMOvF7m?CNsJsF-ZE_mpVjSH&zM0>;QFBXgVQN@OWJ)j={}lrA5XgjNp~O-gfhN2;SQwSeIsUZ zl>zo@g^skHEVQAG7XQzf4TaWW9?IWB$3>|HX}dRR_r_eY=Gf_!y&-LHO4?!4HsODJ z{-kRX>hF}j8LybQLYMrCqPJejcx@=PokgQ-MF!1%ru#fPuse;6pwqZliQXWbWXGY+(;&MbWdQ-jY7%IH51B7dp`A zc4|KCo6RuRw+t2CqsDrrBuAtOSd>sBoT^r9z&cotZ?<0ex`7MN7t^78Tp)*XzY*JS zld&4DSTcA2+_~O*!J+Q0utqQB@*=0YXQduSt+`RZZyUqQZ^&|%El`t2>E~k7^k$KZW5kG5BtkQJE#y53 z4|Ez?4pTbx9e7|}@04%_4z|5b zWm)H`1S@LIlw)%m_Ndh?rOga;GIJKtZ+WKv5&HFl&eYF~701q9srhr~ifHqwe$@Yp zIpG9r3olbtJZuCrTRIm@Cx_y7#P&|QraX^P$#}J_0$S&J%dWLe%$o>J>LwZ{4_&Xj zK6mojRO8Onyj`Di_JV2rR4nm>kkxhdsY{3&NBtb=q}G$&m&H(50I^^Fsy zlV#Tzrt0@h746Lwmq+&|ix*}}YSSePk|hgX|KdbPYT>4-lFgY4@0e-20B=t_Aas6X zTh#K({#bLeWPS>3w+$O6TW&OL9{b|;8{lor(+6{wx)v`mno0upJQ+K5g|O=Xiy$^EMMaSbT}tt9l5 z=VVMDG3KeMj8F@@@-pL*DXv%xWy}eF9b3U7h7FVojHsA0&Qe_Ug1gWzPi_I@`-#oU zbD+7HILDRd}TAj(=uWvMs=Q&_>)@)rq( z_pHl%S6vOPuF6~;atL+ZxT`J_IG9uFnyo5hD-$tJ_j5!1u9@j1uaa0 zdXN*ohN>H!532|jt|2=kOad(&2)|9AO5t!b z-Yl(5d*&xS^Fh`~!z)@6{g`y5P^FfqN>+~XIgM!n@=O=uol8f?j!18aI^pUm=kkd? ziPd`^{3>i8{%Na|CF>?_9r6`|J@Wn=!5^`^kZdsyg+&>MGwZ6(l$B>nO0!kV!N2^% zwbE#vKFKSp#*%(~hbDTJ~WxO1p5C+I-|Ya&I2 z{1qzU9!x^MQ+?xug?p{{Nx#nA1B82p=~4;zCY5miMU`-GfpA~IgnO+T8*(*5xVOqI zhy1;yjtJNW!E!;kzY5`=WGUvfAVd=Gb7J;=bLYhD7aZ6jSSZ)6s)O)Oo|9Q>6=XZ0 z0G|cZDmYT$c$S#$I3P;W{}P3ZT3(EMzCD6yDt>?x{(!uv;N3&Y765Abf@;>K$kOt5 zI^k`~Mr2>)4AxTUS@OP2-WYj|QTRg&{SkTJCU1hgz3^ZwF73<<<>Y3@OgvC*ewWI4 znY=$C?_ZPmD0$b(`#0o$g}kqlH%T5-Ezk%)gzT)KU`2Lr=kcNo-TL`t=em_7JJ*2X z)Gvvzys|V_GqLD9t0$kDT>W-qvTnuIS-!bH-khu=?j{Ka zLz1oM$4lb=t7Q{)lPy!8%_LjT5<}-jPmeE*t&a0o6mj&>2X*hsBB>wy;gTenb|p@A zC+`wZp?vm~4Hw*3On1x{({Dk&-27W-qOPd_)zVl~oPX_+Ohs+1X?)J836_`9P56d6 z%Tk_3MUJmX7kRNV{Cai#%*5I2=2YF|Q$@RTa(s2VWFAcEUq3#vD>Z+^RLREs%kc}> zPP%R^d~7Uoy^7>`x$yG%!b!ta-Nvb+O*uKf8g0K*!&O#Yt{$(B7bGe$HWwV|KT#%s zeNp_;$u(1Ro4>#E{YT$>^xv)fVRz!l3GjW1j*~dSXX-#WRWcxohSH;_+TGcaG9YGWV-Q=;ndYy~y4ArzUg%L*(bhgjpvx8W8~*mz#MW9*#ccnGzLs~U%Vr6_BD#^3vN^A4Yu zhC&6lUXg4xgW=p(yx&V;LU1qE&hc$mDXjaWMT+EvTyqJlTrt*iLF} zJA=xf(^^izqPH2}28U8?VhGxW+)}z+hV&ay6#2-nkRf$`T4q(rd<1*Usg{Y^f`HVf zA~`+xJF87fIt=N+qBbL&C?qnVcIBeYB!_wzC)OpG>KSQ(r|( zjhrw@(*dW5Fbn|ubWx>+Ju`JOlZs0Hqvj^$7SA(-8t%sy2dJ^kH>l^PF-J} z-gYFp?Fbh6uuxY{YkOoZkG0SFSov&-AI9k*&rR|v4+^5A;OY<$v!RrSBo6D%GB1`< zgYjo4zIgo$?1ZweQ;sFXsapn{4GOR@n2Q^VQzz`7}7_$KUbepfG(pD#lpDimoCft&~D zX_IXvD-k)>?UUL`iaZ*0Y)+7n4bY8pfCxw&Cg+d|7FM)5NO>P9kF`vA45cclN&a+{ z0LviNjy$wN*}uo)#q9xjmndDHL#%>8b4gIdN}x7mzOeV9YK7&q))%AgtUn0-(wC!$ z?vYQ<@sHSc%l$W2ynur>uy&lYoHbu~4-CWoM-Xr#^xQQ<4>K6I~fO-_=d=4I7Fw3e}O>0^(T-TH%O&Gc&J6%Hi%+XdYqN8Dj1Zt^iWfo4@ z4GUiB4J}(W20>L)(W)QX-3^@uF_v)DqeS+p-hV(TC~c^q*O6-qP5DfR=ooMuwRWa6c$Tp$YAa%<0nQeS)k-XhR6ViASz{*-rSviF za8^%S>3|hk{~l%(u3AT{p0gV{Tb(P zR5VO;`T}dl@yNvKiMDBuU*E~YFYM%J^oUP58*W=Tdo815Rah;ave(?g?wA(v8Hc0c zbnU7n{+Fyu7OfgKe(Wk89eAbcQ_fbfVz@aow?0-pyd5t{^%8OH9_x-ZiL2W2W@Tl& ze)aW|bo0Sv^TEXN6Y1mq$>aT_-Ko;kV|s`cIaE5aCr~xKVWmRl9L~$$-v&CCdrOQ!w8NOq>9JbD*JWi)R zl=d_vJqT`#pRb8#u}mrWAmbW7{*p=Fo=kl;CxDa4hwslf#f5bzUf;w>UmxJu0L`+S~ z!*aQdPon)B8gx%Qa-Olf%93oZ!73XWqiQ)?VS>SDy~g+~vglTcjNnyxw&=PER8Y~g zs>(I0`7A7E5^sa#dvGj@R#{a7r?qDVTP$9X0_sd5q{3_}e zO>Okx@b(J_G$~9Ij#Fy7IuKv?j{i?xiTUf3^^bjE_>tp72fO?N*Ix7yk?B_vnOO5{ z{x@Afulz*w+s62AI?5`s;IZVq4Ih-y9@E5$_VkJV#NUH4gsNtq`^298v;Y8DJ z7v<5+jMF`A6$LzLQeXf$j?g7N&ve6k(4pzLZ-wc5WTtgUpSur6Q%F%PRe(|q*yI7m zF2d8)2H@%r2&)moX2UxCWc9&w{aGvQ%LBda95i_wzruZF^`US8+4@7uB@)sW;w0W5 zER{Ks+0ynp0;!eAfF1mf;)|Ulol$+XW4tisSTL+dOxjVCbksycDM$UV9(^==aO8Y+ z<;WKkmg>7Zi)2gIS(6Anq=Eq_H{n_Ys0I#lIohJtS?FAY)=5#c+fSx4)PYLp8?ocY zs47!K-pt9svJ{Aphmwmrz5{=7o32ZN)mzZz#LS8K;^zoKOVy1dZiT-@SW;JH4V^u> z-=;7?R}u8!bd^xJvn|IW3ZEcrzS07x@v?(*b6leMyotu(W8Sjbjm{^VTzmnx^Q;u~sVy@9`-;$0rimyslE>D&$Pdb+;EX&2=Qxkg{ zIyQW_5KzN9NCTRZ@8pgKDN|C}W+m0l6#&zg(S4;mRj~@nJV&N-PPAAI)Zj#nd2cjc zX-rnoc3;QGIH#B07L=-floW}T8HQ8E(A?_W-$9EI#&&|Ko>l=3+k|4P|r3F8W4hd?V9N= z2@0SLqc@q;g3y4Xd|1QrTuyNoD8Y?hM!L}pDl-fHlhF_C!?+iVUodI0rV$-Bi%GMM z=XxW#P4hByEuC6_4Dhe$_2u?Kfn}557EA#JUFhra9%NV0c(-@9x3l}_yiJYgmU>q< zp7VkU@IJdwJR;(g=g~*P?~}*4+y99|YvAGB5i+2>_kzsJl==gBIC(x8_T$Dr!Antu z6@O(6ty;f)c~%!%g>&l7Cve6pHne5kp;h^_la8^Ml6~w-H#;;iK5V9AS9c6raN7sn z1m^`l!oIjrsI#M&ZNdG7n2xJZ_Ry}iwy;Q4S^b1&vKE}bfTg1Fd4a|{YsJAULO4X{ zWC%2UaS0d76+rG0?Cfpt%Q`7SyhspR{7@iK1z`hu>)|yNWSu!Y!tjdNGhWK^3-VZ3 z=-b*mgzwYi{|ygk6`tx3Yq00I-AdGi;bu6&hH3o-;r6ExU~5Mf0P|ti4%OHs-*+KaOGJXL>wCv0^Dpm}@eYLOPu= z+CJ>~$TI(9Tk*^5qpsiDoN?BqoefE6Lwwjbng|8K*r5fzMf!DpK#s>wJXlUY%5T;k$|3&!ym#22abg~pOQ>V&_0O-LD0Ze zWjV$Tq>_J)A1V+@CbZ}ZQ>E2~&`V=M8+SlaU2rIgrXq6>c++~WsRkG zL0IW`t=o6ImgH)XLjen`i|7NKy0nNz_@jHYc&V6M?ROE2ndjs-LPz#hX`_N|$R2Kz zvteU`vezEQHpLNp7ZH#=*F}O`p2G$P(#S9uDU^F(Vz%l~jz*uV`ev>CZtfRH)JtQ? za~Gb~ES()u&(0NiQ29ih^4zTQhYdKv2;xfNfrg@vKly|6?>DUf%Xd5Y{Vx?h@$LWf z-Hul+bAIo5!(YGK@s+=7nXLKBzunyN-S8{rhNu7iyB)({X-$3iXa9L~$JQUUfAsUN zfA?<3;T4Oo7AJnWxntiSSa;4J$ZqcV`QKf=ePjPS?{@s|iwnN@)i-{+xns;Z_{dwE z((iWIkVE(C2k&-VKjLk^-S(f~?bz?Ru;)J+5}P~X&-~fS@4tQHU9`yTi|+GWFm35L zkQ_Vu_uD?(+)+y{`90m14&8T+?vl;MEgkQztKaj5- zr43s;9RKr*@x@VYO9wJ6-?zZJrQ`OUJ8X6D&_Lr4^AUP&Do80jE+p3&nmyD5RlA%^+lg=dQ5Wv#l z@W+skv<$UWCm~$i8!BG1oYf;fYw?4Jq#Fw*NtJHd3`loxhO*|9{t(VI5X8?TSZDh# zl&cg)l9DZ&O<`6A8XThe#T%?bx&XHyycg>~)@5EQsfIq1^@WQFFT<}{xgTt%>$ z`E;JkX&u(znTLbq=sLt$N&N8SBU2??@k+r>tkr-6is|EPCQGM^w;;|!Yg}{|ZE-C8 z#ORm@!E%>N>`^}1~=;3cx#!i2;R+YRnxnd_J@48(`RbWS;eaYRr zBAO8di!)Edn=N<01k#|TFVeCQ%AtKZB%@ZQ0i9*KN!6{QDv7Dz3vr4xjK{#F!G!a^ zrn> zXo<;8l^QpYfYTk?)Tm_p34tsvz5FHFkKJ%@is6L9OOK8{dTCwKz3Fe+_10gmtm0w# zqz^%ZL^4L!su(_ngc(GH0P>O=3CrJP5DG~J_nHp6?{33Qs?dx4bb+NCbwHuib)_o} zZTN!6QVTXtxi_Vpn-Z2y;^0y5$&M&L8@%&OIn0d7zmAuFS?Osj!}~sUD;txH?T<5a*Rk-#28yc}1#I>7cyO)Eeiu z3Xl`>4Anz9AFD#aXc-`RRRvDWyEFN0#ZT8aj(B8gwuNdPE;*MwWfJb&?iTB91 zrQSnJy^k;TK66YI;i-rN5Z4)AttA}F@G8Ui1SX=^nV1+M3QyAfXH8-S!vBsq5E+~h zOoT6M!ljuw3oU;Ufb@NXg%TKv8i&6i2>LSOfn}{+36mCax*3iEJ34aoHjECQ3jVrSyvZx3D_OgwfV<#}Rw=VxYm z7i&lE@=F!4c3his%^NmloSwAPi-TEG&bbN8+|O(*6RYM|A!515o1I%-`tP}nTPw|N zn(#{f@=p$4$*fp8U+dxERRbogM3@wDoB7_bbORmDI20L-bb}$`KT_l>bZ*urawDQ~ zB_VkR-y*4|R}~NvS0w*00->{r$99im&!x3vYi9vaivmE{9CsM@D6228A72krIjYN4 z)?8jZzBpaEBw4v6zIDRzZ>(=y6C0jLRX#bqmjI}R0H|#4rQWgL*t4n9#$j8=S&5sI z32u-ExM|^YaHH3MPjB36Hy;r0pfyn$GfqO!W23N55Z@?NHoA3+p53&82h6uC2xu7ld=V(&;VpdEJ+ zW@^2e6>Dx6AK;B3r50}Gn&|>DT*OsYPrJmhn_JvCBj)+^c^z-u3?+-H_{GD&dF)Ha ze#7^v&T6cm(Q_uxG=*UAqE0g#=diTfI)ublQrp_lEX8@G&`d8qM3wQ&3 z5ZCMBPme!C5ULXVnV={Ul_f?fOAG;P(9m82Rf)0H6g0!iUpd=Hw)PJ=phmF?MG2U|oO3=0w`Q!*06P7}tFUv$hih zP^pG}#H=%}I%{D(Hr6|`4t5}>FED_E#c+JEsF-x03I_Xqv+N17V%%&?HDbiGl_4yg z;6_eZEkJMxLhIVsD_=7@N4EIWNNiKnT%yU0OMpNQsG$WF*d}A87K;OhAsg&a)Vd|o zCZTH4lC%bm#|ectn1vf77TBUl`UsRmc!xQrzmnoPl7eB)@wZ6H^2>K(!IcqA zMk^Pf>YUC@piXEo3tK668+mNb*5OTIEj==-+KdnmDI=>_w@)Vi?8eGZE@y2Y?A=urWjJGCm zh<#yZF0OQ+vee4IH$8K1y2~y#jx}PhbEc$@1eK~KnWcO06q-wE1A!GMz0@wFg+LJ* z%;KQYZgE5#5g_hai!ak+-DRVFo!E1%-}CrYHW-5yTHng}5;s7JZJh_Da28)292rbI zYjL>nU}Q8lW!Z! z(p7pm$k2q<)&Ze+*3J<{VE8}KS@-M~LKbTP@j+SBWaiY`Whu(vT$3MA7LX=xjJ#LdNb!;9)aHBvn6iCE*RHQX zo(I$eb@e^IUr`nT)DQ*t+jDkxM*-W+)H zea`z4#?Bp4B*|JKar@dK(1IipR1mThF(ngEDz#>>I4P?ZS$t8)^JCZd!(=`W@H3vR8o2q#xS@KNch%bF4m^>0pAL&aT z>C3_dxoPy&=-}w7L{Z&sz3wRQ__@O~(lOeRa@5>5>Pl*<77-f?tI=<>rl3NN7XwG# zO9qm58qxJOY^RYB2EY(b%X3<)7&cY4@+$N=&|TqfTdQ?wHgh>A~cQ z&E4-)Jd<$P+-0+v&1c5+&@5*2S-$RoKvHOd7a~Nj%v>fFYR+7dgf-C2{X4vYnd_Nz z>FC(eSlyT}H!Y!IfNguacwVx2UdB}xohOQgOOusL)0L}}m8&P7PE~G5x;9)lyl;Ka zn%;6Sx#eJb%aP=kBZ;FYQd`=RMQxcyOVf)sCl_r_%-cL|v?yX7p*6WX?+a#TBl(A5ZD98!5*Z6q3J8{9QSm9D-sbe8MH}N z4mN1vSk0`D(!hyq1FUxwCG%pZlO>DdEy?1>q_Z($X%xT8MTJZCh^T$mXHk<>pG7xD z5b?!Ffek~LeU@;TV2g(*f6g=cpHo6@6gyA#HH+@`HTgN$)5nkz6QHoD4hQ%a&5yg1 z&c*ng+_%hPmXUWB4+<9Uz$b)-HQ7*sB!j_5t>)ZY7TbtM&4}D6PC$Av8%#|QcW_Zx z<#WPJE*V^|)r9+qe9dTsaFmK84rEpjAX4msN(Ao3^~s_EFA7b03~IJS9EjNx?V#qc zY31BO!$ydiNeyY|+$7T--rZK@6Wh90ZY#Au&uJ40SrILNiB5Q$x}%xA{Lk@7fS&g~ znv+U1>u&lW>S^hNm<7+g^x}=l#T(V?`ux_@_stmKU1GunE@h?WX@NnuK&g1JS_>GI z?U2bq|p0VsL?01Mbe(qHc-%?$On1N!E2% z)T{WyeK1a=`^B}}Wgs2PV7XRHDCMxa?zyb)12}2GN&TyMpR}L}qG`^~ox%}HV=Kkw z`Fh56ltjxa!eivo>Sor0n@q4G4O=!QIPawdCe77Rp*P86Ts0%b4ECO*EQ+o{AjOK{ zCy&NQpd(QP955=413{C>L~o)v@;<{Lg)Sn%G?P^|q?Rm);jowVk@bt{|B5x4#Y-|v zmfkKaH!i#7DW5jt7kWo%D9seY#8fcPh3urWEToWwtFD`NQmBYRE((zr zbGn#9CEWZ)(;f#0#ZqrNJO2;&XJ~i>i!cR>)|zO8>igfRw8^Tlg%g6G2BohR5zzfi~E2UgK-CmVZv1wwAPl|XuIY#-=tqiF`C z0c$QS%%fbUU}tY%e@Gylk+rw=g?jozjhK|(A<=5GTpiUIz?zlNc>-7S1en1kv!2YB z%*qq=htB&t`$YT8`BD-)yr%I?drzY>3q!bbSyFQf#3u^@)}C#!tj7^H#K~t(`$fxu zm6W`byuIXonY=Obu8{X#^4=%!L-KM{%Ld36ReB8K85UG#!LKYEM7x7c!dc|Qe{eDI zr}aF~|H8}h^*`h6KjA!o!PO?X+MjS$|Bbk=rcxMkkpRXEyitdrV!y$O*Ngf$pqq<84V+D6WdwCaL3aFwe{OJL`5eY@-cz)gR z3EH7^hokWHIX&m8$XFd2TTy1!n#`Q4+qQMQ2@|)mX}W+yR<5*ShT=Xu$9MAloZB4v Vcb?Th&-3+(%BEj%6kr(d{{h=PD}4X} literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt2_vae_4x16x16.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/tokenizers/__pycache__/wan2pt2_vae_4x16x16.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ad2a8fa09edd84779d5e7d406d847032862ca1b GIT binary patch literal 70448 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/interface.py b/cosmos_training/cosmos/model/vfm/tokenizers/interface.py new file mode 100644 index 00000000..96190181 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/tokenizers/interface.py @@ -0,0 +1,236 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import Optional + +import torch + +from cosmos.utils.env_parsers.cred_env_parser import CRED_ENVS + + +class VideoTokenizerInterface(ABC): + def __init__(self, object_store_credential_path_pretrained: Optional[str] = None): + assert object_store_credential_path_pretrained is None or isinstance( + object_store_credential_path_pretrained, str + ) + if object_store_credential_path_pretrained is None: + self.backend_args = None + elif os.path.exists(object_store_credential_path_pretrained) or CRED_ENVS.APP_ENV in ["prod", "dev", "stg"]: + self.backend_args = { + "backend": "s3", + "path_mapping": None, + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + raise FileNotFoundError( + f"Invalid object_store_credential_path_pretrained: {object_store_credential_path_pretrained} and APP_ENV is not prod/dev/stg" + ) + + @abstractmethod + def reset_dtype(self): + """ + Reset the dtype of the model to the dtype its weights were trained with or quantized to. + """ + pass + + @abstractmethod + def encode(self, state: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def decode(self, latent: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def get_latent_num_frames(self, num_pixel_frames: int) -> int: + pass + + @abstractmethod + def get_pixel_num_frames(self, num_latent_frames: int) -> int: + pass + + @property + @abstractmethod + def spatial_compression_factor(self) -> int: + pass + + @property + @abstractmethod + def temporal_compression_factor(self) -> int: + pass + + @property + @abstractmethod + def spatial_resolution(self) -> int: + pass + + @property + @abstractmethod + def pixel_chunk_duration(self): + pass + + @property + @abstractmethod + def latent_chunk_duration(self): + pass + + @property + @abstractmethod + def latent_ch(self) -> int: + pass + + def compile_encode( + self, + warmup_resolutions: Sequence[str], + output_dir: str, + aspect_ratio: str | None = None, + ) -> None: + """AOT-compile the tokenizer for the given resolutions. + + Subclasses that support AOT compilation should override this method. + The default raises ``NotImplementedError``. + + Args: + warmup_resolutions: Resolution keys to compile for. + output_dir: Root directory where compiled artifacts are stored + (typically ``config.job.path_local``). + aspect_ratio: If given, only compile this single aspect ratio. + """ + raise NotImplementedError(f"{type(self).__name__} does not support compilation") + + @property + def is_chunk_overlap(self): + return False + + @property + def is_causal(self): + return True + + +class AudioTokenizerInterface(ABC): + """Abstract interface for audio tokenizers.""" + + def __init__(self, object_store_credential_path_pretrained: Optional[str] = None): + assert object_store_credential_path_pretrained is None or isinstance( + object_store_credential_path_pretrained, str + ) + if object_store_credential_path_pretrained is None: + self.backend_args = None + elif os.path.exists(object_store_credential_path_pretrained) or CRED_ENVS.APP_ENV in ["prod", "dev", "stg"]: + self.backend_args = { + "backend": "s3", + "path_mapping": None, + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + raise FileNotFoundError( + f"Invalid object_store_credential_path_pretrained: {object_store_credential_path_pretrained} and APP_ENV is not prod/dev/stg" + ) + + @abstractmethod + def reset_dtype(self): + """ + Reset the dtype of the model to the dtype its weights were trained with or quantized to. + """ + pass + + @abstractmethod + def encode(self, audio: torch.Tensor, force_pad: bool = False) -> torch.Tensor: + """ + Encode audio waveform to latent representation. + + Args: + audio: Input audio tensor of shape [B, C, T] where: + B = batch size, C = audio channels, T = time samples + force_pad: Whether to force padding to match compression factor + + Returns: + Latent tensor of shape [B, latent_ch, T'] + """ + pass + + @abstractmethod + def decode(self, latent: torch.Tensor) -> torch.Tensor: + """ + Decode latent representation to audio waveform. + + Args: + latent: Latent tensor of shape [B, latent_ch, T'] + + Returns: + Audio tensor of shape [B, C, T] + """ + pass + + @abstractmethod + def get_latent_num_samples(self, num_audio_samples: int) -> int: + """ + Calculate the number of latent time samples from audio samples. + + Args: + num_audio_samples: Number of audio samples + + Returns: + Number of latent time samples + """ + pass + + @abstractmethod + def get_audio_num_samples(self, num_latent_samples: int) -> int: + """ + Calculate the number of audio samples from latent samples. + + Args: + num_latent_samples: Number of latent time samples + + Returns: + Number of audio samples + """ + pass + + @property + @abstractmethod + def temporal_compression_factor(self) -> int: + """ + Temporal compression factor (downsampling ratio). + audio_samples = latent_samples * temporal_compression_factor + """ + pass + + @property + @abstractmethod + def sample_rate(self) -> int: + """Audio sample rate in Hz.""" + pass + + @property + @abstractmethod + def audio_channels(self) -> int: + """Number of audio channels (e.g., 1 for mono, 2 for stereo).""" + pass + + @property + @abstractmethod + def latent_ch(self) -> int: + """Number of latent channels.""" + pass + + @property + def is_causal(self) -> bool: + """Whether the model is causal (for streaming).""" + return False diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/tokenization_qwen2.py b/cosmos_training/cosmos/model/vfm/tokenizers/tokenization_qwen2.py new file mode 100644 index 00000000..a14cb1de --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/tokenizers/tokenization_qwen2.py @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Source Repository: https://github.com/ByteDance-Seed/Bagel +# This is adapted from modeling/qwen2/tokenization_qwen2.py +# Commit Hash: 7026cfa0a4df274460d0b0b990117398a4ec6fca + +"""Tokenization classes for Qwen2.""" + +import json +import os +import unicodedata +from functools import lru_cache +from typing import Optional, Tuple + +import regex as re +from transformers.tokenization_utils import AddedToken, PreTrainedTokenizer +from transformers.utils import logging + +logger = logging.get_logger(__name__) + +VOCAB_FILES_NAMES = { + "vocab_file": "vocab.json", + "merges_file": "merges.txt", +} + + +MAX_MODEL_INPUT_SIZES = {"qwen/qwen-tokenizer": 32768} + +PRETOKENIZE_REGEX = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+""" + + +@lru_cache() +# Copied from transformers.models.gpt2.tokenization_gpt2.bytes_to_unicode +def bytes_to_unicode(): + """ + Returns list of utf-8 byte and a mapping to unicode strings. We specifically avoids mapping to whitespace/control + characters the bpe code barfs on. + + The reversible bpe codes work on unicode strings. This means you need a large # of unicode characters in your vocab + if you want to avoid UNKs. When you're at something like a 10B token dataset you end up needing around 5K for + decent coverage. This is a significant percentage of your normal, say, 32K bpe vocab. To avoid that, we want lookup + tables between utf-8 bytes and unicode strings. + """ + bs = list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1)) + list(range(ord("®"), ord("ÿ") + 1)) + cs = bs[:] + n = 0 + for b in range(2**8): + if b not in bs: + bs.append(b) + cs.append(2**8 + n) + n += 1 + cs = [chr(n) for n in cs] + return dict(zip(bs, cs)) + + +# Copied from transformers.models.gpt2.tokenization_gpt2.get_pairs +def get_pairs(word): + """ + Return set of symbol pairs in a word. + + Word is represented as tuple of symbols (symbols being variable-length strings). + """ + pairs = set() + prev_char = word[0] + for char in word[1:]: + pairs.add((prev_char, char)) + prev_char = char + return pairs + + +class Qwen2Tokenizer(PreTrainedTokenizer): + """ + Construct a Qwen2 tokenizer. Based on byte-level Byte-Pair-Encoding. + + Same with GPT2Tokenizer, this tokenizer has been trained to treat spaces like parts of the tokens so a word will + be encoded differently whether it is at the beginning of the sentence (without space) or not: + + ```python + >>> from transformers import Qwen2Tokenizer + + >>> tokenizer = Qwen2Tokenizer.from_pretrained("Qwen/Qwen-tokenizer") + >>> tokenizer("Hello world")["input_ids"] + [9707, 1879] + + >>> tokenizer(" Hello world")["input_ids"] + [21927, 1879] + ``` + This is expected. + + You should not use GPT2Tokenizer instead, because of the different pretokenization rules. + + This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to + this superclass for more information regarding those methods. + + Args: + vocab_file (`str`): + Path to the vocabulary file. + merges_file (`str`): + Path to the merges file. + errors (`str`, *optional*, defaults to `"replace"`): + Paradigm to follow when decoding bytes to UTF-8. See + [bytes.decode](https://docs.python.org/3/library/stdtypes.html#bytes.decode) for more information. + unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this + token instead. + bos_token (`str`, *optional*): + The beginning of sequence token. Not applicable for this tokenizer. + eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The end of sequence token. + pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`): + The token used for padding, for example when batching sequences of different lengths. + clean_up_tokenization_spaces (`bool`, *optional*, defaults to `False`): + Whether or not the model should cleanup the spaces that were added when splitting the input text during the + tokenization process. Not applicable to this tokenizer, since tokenization does not add spaces. + split_special_tokens (`bool`, *optional*, defaults to `False`): + Whether or not the special tokens should be split during the tokenization process. The default behavior is + to not split special tokens. This means that if `<|endoftext|>` is the `eos_token`, then `tokenizer.tokenize("<|endoftext|>") = + ['<|endoftext|>`]. Otherwise, if `split_special_tokens=True`, then `tokenizer.tokenize("<|endoftext|>")` will be give `['<', + '|', 'endo', 'ft', 'ext', '|', '>']`. This argument is only supported for `slow` tokenizers for the moment. + """ + + vocab_files_names = VOCAB_FILES_NAMES + model_input_names = ["input_ids", "attention_mask"] + + def __init__( + self, + vocab_file, + merges_file, + errors="replace", + unk_token="<|endoftext|>", + bos_token=None, + eos_token="<|endoftext|>", + pad_token="<|endoftext|>", + clean_up_tokenization_spaces=False, + split_special_tokens=False, + **kwargs, + ): + # Qwen vocab does not contain control tokens; added tokens need to be special + bos_token = ( + AddedToken(bos_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(bos_token, str) + else bos_token + ) + eos_token = ( + AddedToken(eos_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(eos_token, str) + else eos_token + ) + unk_token = ( + AddedToken(unk_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(unk_token, str) + else unk_token + ) + pad_token = ( + AddedToken(pad_token, lstrip=False, rstrip=False, special=True, normalized=False) + if isinstance(pad_token, str) + else pad_token + ) + + with open(vocab_file, encoding="utf-8") as vocab_handle: + self.encoder = json.load(vocab_handle) + self.decoder = {v: k for k, v in self.encoder.items()} + self.errors = errors # how to handle errors in decoding + self.byte_encoder = bytes_to_unicode() + self.byte_decoder = {v: k for k, v in self.byte_encoder.items()} + bpe_merges = [] + with open(merges_file, encoding="utf-8") as merges_handle: + for i, line in enumerate(merges_handle): + line = line.strip() + if (i == 0 and line.startswith("#version:")) or not line: + continue + bpe_merges.append(tuple(line.split())) + self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges)))) + + # (esp. for texts of language that do not use space between word, e.g. Chinese); technically + # not a memory leak but appears as one. + # GPT2Tokenizer has the same problem, so let's be consistent. + self.cache = {} + + self.pat = re.compile(PRETOKENIZE_REGEX) + + if kwargs.get("add_prefix_space", False): + logger.warning_once( + f"{self.__class__.__name} does not support `add_prefix_space`, setting it to True has no effect." + ) + + super().__init__( + errors=errors, + bos_token=bos_token, + eos_token=eos_token, + pad_token=pad_token, + unk_token=unk_token, + clean_up_tokenization_spaces=clean_up_tokenization_spaces, + split_special_tokens=split_special_tokens, + **kwargs, + ) + + @property + def vocab_size(self) -> int: + return len(self.encoder) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.get_vocab + def get_vocab(self): + return dict(self.encoder, **self.added_tokens_encoder) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.bpe + def bpe(self, token): + if token in self.cache: + return self.cache[token] + word = tuple(token) + pairs = get_pairs(word) + + if not pairs: + return token + + while True: + bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float("inf"))) + if bigram not in self.bpe_ranks: + break + first, second = bigram + new_word = [] + i = 0 + while i < len(word): + try: + j = word.index(first, i) + except ValueError: + new_word.extend(word[i:]) + break + else: + new_word.extend(word[i:j]) + i = j + + if word[i] == first and i < len(word) - 1 and word[i + 1] == second: + new_word.append(first + second) + i += 2 + else: + new_word.append(word[i]) + i += 1 + new_word = tuple(new_word) + word = new_word + if len(word) == 1: + break + else: + pairs = get_pairs(word) + word = " ".join(word) + self.cache[token] = word + return word + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._tokenize + def _tokenize(self, text): + """Tokenize a string.""" + bpe_tokens = [] + for token in re.findall(self.pat, text): + token = "".join( + self.byte_encoder[b] for b in token.encode("utf-8") + ) # Maps all our bytes to unicode strings, avoiding control tokens of the BPE (spaces in our case) + bpe_tokens.extend(bpe_token for bpe_token in self.bpe(token).split(" ")) + return bpe_tokens + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._convert_token_to_id + def _convert_token_to_id(self, token): + """Converts a token (str) in an id using the vocab.""" + return self.encoder.get(token, self.encoder.get(self.unk_token)) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer._convert_id_to_token + def _convert_id_to_token(self, index): + """Converts an index (integer) in a token (str) using the vocab.""" + return self.decoder.get(index) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.convert_tokens_to_string + def convert_tokens_to_string(self, tokens): + """Converts a sequence of tokens (string) in a single string.""" + text = "".join(tokens) + text = bytearray([self.byte_decoder[c] for c in text]).decode("utf-8", errors=self.errors) + return text + + def decode( + self, + token_ids, + skip_special_tokens: bool = False, + clean_up_tokenization_spaces: Optional[bool] = False, + spaces_between_special_tokens: bool = False, + **kwargs, + ) -> str: + # `spaces_between_special_tokens` defaults to True for _decode in slow tokenizers + # and cannot be configured elsewhere, but it should default to False for Qwen2Tokenizer + return super().decode( + token_ids, + skip_special_tokens=skip_special_tokens, + clean_up_tokenization_spaces=clean_up_tokenization_spaces, + spaces_between_special_tokens=spaces_between_special_tokens, + **kwargs, + ) + + # Copied from transformers.models.gpt2.tokenization_gpt2.GPT2Tokenizer.save_vocabulary + def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]: + if not os.path.isdir(save_directory): + logger.error(f"Vocabulary path ({save_directory}) should be a directory") + return + vocab_file = os.path.join( + save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab_file"] + ) + merge_file = os.path.join( + save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["merges_file"] + ) + + with open(vocab_file, "w", encoding="utf-8") as f: + f.write(json.dumps(self.encoder, indent=2, sort_keys=True, ensure_ascii=False) + "\n") + + index = 0 + with open(merge_file, "w", encoding="utf-8") as writer: + writer.write("#version: 0.2\n") + for bpe_tokens, token_index in sorted(self.bpe_ranks.items(), key=lambda kv: kv[1]): + if index != token_index: + logger.warning( + f"Saving vocabulary to {merge_file}: BPE merge indices are not consecutive." + " Please check that the tokenizer is not corrupted!" + ) + index = token_index + writer.write(" ".join(bpe_tokens) + "\n") + index += 1 + + return vocab_file, merge_file + + def prepare_for_tokenization(self, text, **kwargs): + text = unicodedata.normalize("NFC", text) + return (text, kwargs) diff --git a/cosmos_training/cosmos/model/vfm/tokenizers/wan2pt2_vae_4x16x16.py b/cosmos_training/cosmos/model/vfm/tokenizers/wan2pt2_vae_4x16x16.py new file mode 100644 index 00000000..6c892cac --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/tokenizers/wan2pt2_vae_4x16x16.py @@ -0,0 +1,1694 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved. + +import os +import time +from collections.abc import Callable, Generator, Mapping, Sequence +from contextlib import contextmanager, nullcontext + +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange + +from cosmos.utils.flags import DEVICE, INTERNAL, TRAINING +from cosmos.utils import log +from cosmos.utils.distributed import get_rank, sync_model_states +from cosmos.utils.easy_io import easy_io +from cosmos.data.vfm.utils import VIDEO_RES_SIZE_INFO +from cosmos.model.vfm.tokenizers.interface import VideoTokenizerInterface +from cosmos.utils.vfm.data_utils import get_vision_data_resolution + +# For sequential decoding, CACHE_T is the number of frames to cache. +CACHE_T = 2 + + +def _contiguous_clone(t: torch.Tensor) -> torch.Tensor: + """Return a contiguous copy of *t* using exactly one allocation. + + When *t* is already contiguous, ``.contiguous()`` would be a no-op that + returns the *same* tensor (sharing storage), so we need ``.clone()``. + When *t* is non-contiguous, ``.contiguous()`` already allocates a fresh + tensor with independent storage — no extra ``.clone()`` needed. + """ + if t.is_contiguous(): + return t.clone() + return t.contiguous() + + +def _update_cache_and_apply( + x: torch.Tensor, + layer: "CausalConv3d", + feat_cache: list, + feat_idx: list[int], +) -> torch.Tensor: + """Apply a CausalConv3d with temporal cache management. + + Saves the last CACHE_T frames of ``x`` as the new cache entry and, + when the current chunk has fewer than 2 frames, prepends the last + cached frame so the cache always spans 2 frames. + + Note that feat_idx is a list with a single element, which stores + the index of the current CausalConv3d layer. List is used here so + feat_idx can be mutated in place, and the caller can pass in a reference + to the list. + """ + idx = feat_idx[0] + cache_x = _contiguous_clone(x[:, :, -CACHE_T:, :, :]) + if cache_x.shape[2] < 2 and feat_cache[idx] is not None: + cache_x = torch.cat( + [ + feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), + cache_x, + ], + dim=2, + ) # [B,C,2,H,W] + x = layer(x, feat_cache[idx]) + feat_cache[idx] = cache_x + feat_idx[0] += 1 + return x + + +class CausalConv3d(nn.Conv3d): + """ + Causal 3d convolution. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._padding = ( + self.padding[2], + self.padding[2], + self.padding[1], + self.padding[1], + 2 * self.padding[0], + 0, + ) + self.padding = (0, 0, 0) + + def forward(self, x, cache_x=None): # x: [B,C,T,H,W] + padding = list(self._padding) + if cache_x is not None and self._padding[4] > 0: + cache_x = cache_x.to(x.device) + x = torch.cat([cache_x, x], dim=2) # [B,C,T+cache_T,H,W] + padding[4] -= cache_x.shape[2] + x = F.pad(x, padding) # [B,C,T_padded,H_padded,W_padded] + + return super().forward(x) # [B,out_C,T_out,H_out,W_out] + + +class RMS_norm(nn.Module): + def __init__(self, dim, channel_first=True, images=True, bias=False): + super().__init__() + broadcastable_dims = (1, 1, 1) if not images else (1, 1) + shape = (dim, *broadcastable_dims) if channel_first else (dim,) + + self.channel_first = channel_first + self.scale = dim**0.5 + self.gamma = nn.Parameter(torch.ones(shape)) + self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0 + + def forward(self, x): + return F.normalize(x, dim=(1 if self.channel_first else -1)) * self.scale * self.gamma + self.bias + + +class Upsample(nn.Upsample): + def forward(self, x): + """ + Fix bfloat16 support for nearest neighbor interpolation. + """ + return super().forward(x.float()).type_as(x) + + +class Resample(nn.Module): + def __init__(self, dim, mode): + assert mode in ( + "none", + "upsample2d", + "upsample3d", + "downsample2d", + "downsample3d", + ) + super().__init__() + self.dim = dim + self.mode = mode + + # layers + if mode == "upsample2d": + self.resample = nn.Sequential( + Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"), + nn.Conv2d(dim, dim, 3, padding=1), + ) + elif mode == "upsample3d": + self.resample = nn.Sequential( + Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"), + nn.Conv2d(dim, dim, 3, padding=1), + ) + self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0)) + elif mode == "downsample2d": + self.resample = nn.Sequential(nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))) + elif mode == "downsample3d": + self.resample = nn.Sequential(nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))) + self.time_conv = CausalConv3d(dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)) + else: + self.resample = nn.Identity() + + def forward(self, x, feat_cache=None, feat_idx=[0]): # x: [B,C,T,H,W] + b, c, t, h, w = x.size() + if self.mode == "upsample3d": + if feat_cache is not None: + idx = feat_idx[0] + if feat_cache[idx] is None: + # First frame: skip time_conv, seed cache with zeros so the next call sees a real tensor + feat_cache[idx] = torch.zeros(b, c, CACHE_T, h, w, device=x.device, dtype=x.dtype) # [B,C,2,H,W] + feat_idx[0] += 1 + else: + cache_x = _contiguous_clone(x[:, :, -CACHE_T:, :, :]) # [B,C,<=2,H,W] + if cache_x.shape[2] < 2: + cache_x = torch.cat( + [ + feat_cache[idx][:, :, -1, :, :].unsqueeze(2), + cache_x, + ], + dim=2, + ) # [B,C,2,H,W] + x = self.time_conv(x, feat_cache[idx]) # [B,C*2,T,H,W] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + x = x.reshape(b, 2, c, t, h, w) # [B,2,C,T,H,W] + x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3) # [B,C,T,2,H,W] + x = x.reshape(b, c, t * 2, h, w) # [B,C,T*2,H,W] + t = x.shape[2] + x = rearrange(x, "b c t h w -> (b t) c h w") # [B*T,C,H,W] + x = self.resample(x) # [B*T,C_out,H_out,W_out] + x = rearrange(x, "(b t) c h w -> b c t h w", t=t) # [B,C_out,T,H_out,W_out] + + if self.mode == "downsample3d": + # Important for torch.compile: when we're *not* doing streaming/cache-based inference + # (feat_cache is None), we still need to apply the temporal downsample conv. + if feat_cache is None: + # `time_conv` has kernel (3,1,1), stride 2 in time, and no internal temporal padding. + # In the streaming path, we effectively provide left temporal context via cached frames. + # For the non-streaming path, pad 2 frames on the left so: + # - the conv is always valid (T>=3) + # - the output temporal length matches the shortcut path's ceil(T/2) behavior + x = F.pad(x, (0, 0, 0, 0, 2, 0)) # [B,C,T+2,H_out,W_out] + x = self.time_conv(x) # [B,C,T//2+1,H_out,W_out] + else: + idx = feat_idx[0] + if feat_cache[idx] is None: + # First call for this layer in a streaming/windowed pass. + # The baseline streaming path primes caches with a single-frame chunk (T==1), + # where skipping time_conv preserves both correctness and shape alignment. + # + # If this is ever called with T>1 (non-standard chunking), fall back to a padded + # time_conv so the main path stays compatible with the shortcut path. + if x.shape[2] == 1: + feat_cache[idx] = _contiguous_clone(x) + else: + cache_x = _contiguous_clone(x[:, :, -1:, :, :]) # [B,C,1,H_out,W_out] + x_in = F.pad(x, (0, 0, 0, 0, 2, 0)) # [B,C,T+2,H_out,W_out] + x = self.time_conv(x_in) # [B,C,T//2+1,H_out,W_out] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + else: + cache_x = _contiguous_clone(x[:, :, -1:, :, :]) # [B,C,1,H_out,W_out] + x_cat = torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2) # [B,C,T+1,H_out,W_out] + t_cat = x_cat.shape[2] + if t_cat < 3: + x_cat = F.pad(x_cat, (0, 0, 0, 0, 3 - t_cat, 0)) # [B,C,3,H_out,W_out] + x = self.time_conv(x_cat) # [B,C,T//2+1,H_out,W_out] + feat_cache[idx] = cache_x + feat_idx[0] += 1 + return x + + +class ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout=0.0): + super().__init__() + self.in_dim = in_dim + self.out_dim = out_dim + + # layers + self.residual = nn.Sequential( + RMS_norm(in_dim, images=False), + nn.SiLU(), + CausalConv3d(in_dim, out_dim, 3, padding=1), + RMS_norm(out_dim, images=False), + nn.SiLU(), + nn.Dropout(dropout), + CausalConv3d(out_dim, out_dim, 3, padding=1), + ) + self.shortcut = CausalConv3d(in_dim, out_dim, 1) if in_dim != out_dim else nn.Identity() + + def forward(self, x, feat_cache=None, feat_idx=[0]): + h = self.shortcut(x) + for layer in self.residual: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + return x + h + + +class AttentionBlock(nn.Module): + """ + Causal self-attention with a single head. + """ + + def __init__(self, dim): + super().__init__() + self.dim = dim + + # layers + self.norm = RMS_norm(dim) + self.to_qkv = nn.Conv2d(dim, dim * 3, 1) + self.proj = nn.Conv2d(dim, dim, 1) + + # zero out the last layer params + nn.init.zeros_(self.proj.weight) + + def forward(self, x): # x: [B,C,T,H,W] + identity = x + b, c, t, h, w = x.size() + x = rearrange(x, "b c t h w -> (b t) c h w") # [B*T,C,H,W] + x = self.norm(x) # [B*T,C,H,W] + # compute query, key, value + q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3, -1).permute(0, 1, 3, 2).contiguous().chunk(3, dim=-1) + # q,k,v: [B*T,1,H*W,C] + + # apply attention + x = F.scaled_dot_product_attention( + q, + k, + v, + ) # [B*T,1,H*W,C] + x = x.squeeze(1).permute(0, 2, 1).contiguous().reshape(b * t, c, h, w) # [B*T,C,H,W] + + # output + x = self.proj(x) # [B*T,C,H,W] + x = rearrange(x, "(b t) c h w-> b c t h w", t=t) # [B,C,T,H,W] + return x + identity # [B,C,T,H,W] + + +def patchify(x, patch_size): # x: [B,C,H,W] or [B,C,T,H,W] -> [B,C*p^2,H//p,W//p] or [B,C*p^2,T,H//p,W//p] + if patch_size == 1: + return x + # Fast path: patch_size==2 is the only one used in this tokenizer. + # Implement it with pure view/permute/reshape to be maximally torch.compile-friendly. + if patch_size == 2: + if x.dim() == 4: + b, c, h, w = x.shape + x = x.view(b, c, h // 2, 2, w // 2, 2) # [B,C,H//2,2,W//2,2] + x = x.permute(0, 1, 5, 3, 2, 4).contiguous() # [B,C,2,2,H//2,W//2] + return x.view(b, c * 4, h // 2, w // 2) # [B,C*4,H//2,W//2] + if x.dim() == 5: + b, c, f, h, w = x.shape + x = x.view(b, c, f, h // 2, 2, w // 2, 2) # [B,C,T,H//2,2,W//2,2] + x = x.permute(0, 1, 6, 4, 2, 3, 5).contiguous() # [B,C,2,2,T,H//2,W//2] + return x.view(b, c * 4, f, h // 2, w // 2) # [B,C*4,T,H//2,W//2] + if x.dim() == 4: + x = rearrange(x, "b c (h q) (w r) -> b (c r q) h w", q=patch_size, r=patch_size) # [B,C*p^2,H//p,W//p] + elif x.dim() == 5: + x = rearrange( + x, + "b c f (h q) (w r) -> b (c r q) f h w", + q=patch_size, + r=patch_size, + ) # [B,C*p^2,T,H//p,W//p] + else: + raise ValueError(f"Invalid input shape: {x.shape}") + + return x + + +def unpatchify(x, patch_size): # x: [B,C*p^2,H,W] or [B,C*p^2,T,H,W] -> [B,C,H*p,W*p] or [B,C,T,H*p,W*p] + if patch_size == 1: + return x + + if x.dim() == 4: + x = rearrange(x, "b (c r q) h w -> b c (h q) (w r)", q=patch_size, r=patch_size) # [B,C,H*p,W*p] + elif x.dim() == 5: + x = rearrange( + x, + "b (c r q) f h w -> b c f (h q) (w r)", + q=patch_size, + r=patch_size, + ) # [B,C,T,H*p,W*p] + return x + + +class AvgDown3D(nn.Module): + def __init__( + self, + in_channels, + out_channels, + factor_t, + factor_s=1, + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.factor_t = factor_t + self.factor_s = factor_s + self.factor = self.factor_t * self.factor_s * self.factor_s + + assert in_channels * self.factor % out_channels == 0 + self.group_size = in_channels * self.factor // out_channels + + def forward( + self, x: torch.Tensor + ) -> torch.Tensor: # x: [B,C,T,H,W] -> [B,out_channels,T//factor_t,H//factor_s,W//factor_s] + pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t + pad = (0, 0, 0, 0, pad_t, 0) + x = F.pad(x, pad) # [B,C,T_padded,H,W] + B, C, T, H, W = x.shape + x = x.view( + B, + C, + T // self.factor_t, + self.factor_t, + H // self.factor_s, + self.factor_s, + W // self.factor_s, + self.factor_s, + ) # [B,C,T//ft,ft,H//fs,fs,W//fs,fs] + x = x.permute(0, 1, 3, 5, 7, 2, 4, 6).contiguous() # [B,C,ft,fs,fs,T//ft,H//fs,W//fs] + x = x.view( + B, + C * self.factor, + T // self.factor_t, + H // self.factor_s, + W // self.factor_s, + ) # [B,C*factor,T//ft,H//fs,W//fs] + x = x.view( + B, + self.out_channels, + self.group_size, + T // self.factor_t, + H // self.factor_s, + W // self.factor_s, + ) # [B,out_channels,group_size,T//ft,H//fs,W//fs] + x = x.mean(dim=2) # [B,out_channels,T//ft,H//fs,W//fs] + return x + + +class DupUp3D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + factor_t, + factor_s=1, + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + + self.factor_t = factor_t + self.factor_s = factor_s + self.factor = self.factor_t * self.factor_s * self.factor_s + + assert out_channels * self.factor % in_channels == 0 + self.repeats = out_channels * self.factor // in_channels + + def forward( + self, x: torch.Tensor, first_chunk=False + ) -> torch.Tensor: # x: [B,in_channels,T,H,W] -> [B,out_channels,T*factor_t,H*factor_s,W*factor_s] + x = x.repeat_interleave(self.repeats, dim=1) # [B,in_channels*repeats,T,H,W] + x = x.view( + x.size(0), + self.out_channels, + self.factor_t, + self.factor_s, + self.factor_s, + x.size(2), + x.size(3), + x.size(4), + ) # [B,out_channels,ft,fs,fs,T,H,W] + x = x.permute(0, 1, 5, 2, 6, 3, 7, 4).contiguous() # [B,out_channels,T,ft,H,fs,W,fs] + x = x.view( + x.size(0), + self.out_channels, + x.size(2) * self.factor_t, + x.size(4) * self.factor_s, + x.size(6) * self.factor_s, + ) # [B,out_channels,T*ft,H*fs,W*fs] + if first_chunk: + x = x[:, :, self.factor_t - 1 :, :, :] # [B,out_channels,T*ft-ft+1,H*fs,W*fs] + return x + + +class Down_ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout, mult, temperal_downsample=False, down_flag=False): + super().__init__() + + # Shortcut path with downsample + self.avg_shortcut = AvgDown3D( + in_dim, + out_dim, + factor_t=2 if temperal_downsample else 1, + factor_s=2 if down_flag else 1, + ) + + # Main path with residual blocks and downsample + downsamples = [] + for _ in range(mult): + downsamples.append(ResidualBlock(in_dim, out_dim, dropout)) + in_dim = out_dim + + # Add the final downsample block + if down_flag: + mode = "downsample3d" if temperal_downsample else "downsample2d" + downsamples.append(Resample(out_dim, mode=mode)) + + self.downsamples = nn.Sequential(*downsamples) + + def forward(self, x, feat_cache=None, feat_idx=[0]): + # Avoid cloning the full activation (extra kernel + bandwidth). + # None of the downstream modules are in-place, so taking the shortcut first is safe. + x_shortcut = self.avg_shortcut(x) + for module in self.downsamples: + x = module(x, feat_cache, feat_idx) + + return x + x_shortcut + + +class Up_ResidualBlock(nn.Module): + def __init__(self, in_dim, out_dim, dropout, mult, temperal_upsample=False, up_flag=False): + super().__init__() + # Shortcut path with upsample + if up_flag: + self.avg_shortcut = DupUp3D( + in_dim, + out_dim, + factor_t=2 if temperal_upsample else 1, + factor_s=2 if up_flag else 1, + ) + else: + self.avg_shortcut = None + + # Main path with residual blocks and upsample + upsamples = [] + for _ in range(mult): + upsamples.append(ResidualBlock(in_dim, out_dim, dropout)) + in_dim = out_dim + + # Add the final upsample block + if up_flag: + mode = "upsample3d" if temperal_upsample else "upsample2d" + upsamples.append(Resample(out_dim, mode=mode)) + + self.upsamples = nn.Sequential(*upsamples) + + def forward(self, x, feat_cache=None, feat_idx=[0], first_chunk=False): + x_shortcut = self.avg_shortcut(x, first_chunk) if self.avg_shortcut is not None else None + for module in self.upsamples: + x = module(x, feat_cache, feat_idx) + if x_shortcut is not None: + return x + x_shortcut + return x + + +class Encoder3d(nn.Module): + def __init__( + self, + dim=128, + z_dim=4, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_downsample=[False, True, True], + dropout=0.0, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_downsample = temperal_downsample + + # dimensions + dims = [dim * u for u in [1] + dim_mult] + scale = 1.0 + + # init block + self.conv1 = CausalConv3d(12, dims[0], 3, padding=1) + + # downsample blocks + downsamples = [] + for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): + t_down_flag = temperal_downsample[i] if i < len(temperal_downsample) else False + downsamples.append( + Down_ResidualBlock( + in_dim=in_dim, + out_dim=out_dim, + dropout=dropout, + mult=num_res_blocks, + temperal_downsample=t_down_flag, + down_flag=i != len(dim_mult) - 1, + ) + ) + scale /= 2.0 + self.downsamples = nn.Sequential(*downsamples) + + # middle blocks + self.middle = nn.Sequential( + ResidualBlock(out_dim, out_dim, dropout), + AttentionBlock(out_dim), + ResidualBlock(out_dim, out_dim, dropout), + ) + + # output blocks + self.head = nn.Sequential( + RMS_norm(out_dim, images=False), + nn.SiLU(), + CausalConv3d(out_dim, z_dim, 3, padding=1), + ) + + def forward(self, x, feat_cache=None): # x: [B,12,T,H//2,W//2] -> [B,z_dim,T//4,H//16,W//16] + feat_idx = [0] + + if feat_cache is not None: + x = _update_cache_and_apply(x, self.conv1, feat_cache, feat_idx) # [B,dim,T,H//2,W//2] + else: + x = self.conv1(x) # [B,dim,T,H//2,W//2] + + # downsamples + for layer in self.downsamples: + if feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T//4,H//16,W//16] + + # middle + for layer in self.middle: + if isinstance(layer, ResidualBlock) and feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T//4,H//16,W//16] + + # head + for layer in self.head: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + + return x # [B,z_dim,T//4,H//16,W//16] + + +class Decoder3d(nn.Module): + def __init__( + self, + dim=128, + z_dim=4, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_upsample=[True, True, False], + dropout=0.0, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_upsample = temperal_upsample + + # dimensions + dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]] + + # init block + self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1) + + # middle blocks + self.middle = nn.Sequential( + ResidualBlock(dims[0], dims[0], dropout), + AttentionBlock(dims[0]), + ResidualBlock(dims[0], dims[0], dropout), + ) + + # upsample blocks + upsamples = [] + for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): + t_up_flag = temperal_upsample[i] if i < len(temperal_upsample) else False + upsamples.append( + Up_ResidualBlock( + in_dim=in_dim, + out_dim=out_dim, + dropout=dropout, + mult=num_res_blocks + 1, + temperal_upsample=t_up_flag, + up_flag=i != len(dim_mult) - 1, + ) + ) + self.upsamples = nn.Sequential(*upsamples) + + # output blocks + self.head = nn.Sequential( + RMS_norm(out_dim, images=False), + nn.SiLU(), + CausalConv3d(out_dim, 12, 3, padding=1), + ) + + def forward(self, x, feat_cache=None, first_chunk=False): # x: [B,z_dim,T,H,W] -> [B,12,T*4,H*8,W*8] + feat_idx = [0] + + if feat_cache is not None: + x = _update_cache_and_apply(x, self.conv1, feat_cache, feat_idx) # [B,dim*dim_mult[-1],T,H,W] + else: + x = self.conv1(x) # [B,dim*dim_mult[-1],T,H,W] + + for layer in self.middle: + if isinstance(layer, ResidualBlock) and feat_cache is not None: + x = layer(x, feat_cache, feat_idx) + else: + x = layer(x) + # x: [B,dim*dim_mult[-1],T,H,W] + + # upsamples + for layer in self.upsamples: + if feat_cache is not None: + x = layer(x, feat_cache, feat_idx, first_chunk) + else: + x = layer(x) + # x: [B,dec_dim,T*4,H*8,W*8] + + # head + for layer in self.head: + if isinstance(layer, CausalConv3d) and feat_cache is not None: + x = _update_cache_and_apply(x, layer, feat_cache, feat_idx) + else: + x = layer(x) + return x # [B,12,T*4,H*8,W*8] + + +def count_conv3d(model: nn.Module) -> int: + return sum(1 for m in model.modules() if isinstance(m, CausalConv3d)) + + +class WanVAE_(nn.Module): + def __init__( + self, + dim=160, + dec_dim=256, + z_dim=48, + dim_mult=[1, 2, 4, 4], + num_res_blocks=2, + attn_scales=[], + temperal_downsample=[False, True, True], + dropout=0.0, + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, + ): + super().__init__() + self.dim = dim + self.z_dim = z_dim + self.dim_mult = dim_mult + self.num_res_blocks = num_res_blocks + self.attn_scales = attn_scales + self.temperal_downsample = temperal_downsample + self.temperal_upsample = temperal_downsample[::-1] + self.temporal_window = temporal_window + self._encode_exact_durations: set[int] = set(encode_exact_durations or []) + + # modules + self.encoder = Encoder3d( + dim, + z_dim * 2, + dim_mult, + num_res_blocks, + attn_scales, + self.temperal_downsample, + dropout, + ) + self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1) + self.conv2 = CausalConv3d(z_dim, z_dim, 1) + self.decoder = Decoder3d( + dec_dim, + z_dim, + dim_mult, + num_res_blocks, + attn_scales, + self.temperal_upsample, + dropout, + ) + + self._enc_conv_num = count_conv3d(self.encoder) + self._dec_conv_num = count_conv3d(self.decoder) + self._dec_cache: list[torch.Tensor | None] = self._new_dec_cache() + + def _new_enc_cache(self) -> list: + """Fresh per-layer cache for the encoder (one slot per CausalConv3d).""" + return [None] * self._enc_conv_num + + def _new_dec_cache(self) -> list: + """Fresh per-layer cache for the decoder (one slot per CausalConv3d).""" + return [None] * self._dec_conv_num + + def forward(self, x, scale): + mu = self.encode(x, scale) + x_recon = self.decode(mu, scale, clear_decoder_cache=True) + return x_recon, mu + + def _normalize_latent(self, z: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Normalize the latent.""" + assert len(scale) == 2, "scale must be a tuple with two tensors" + s0 = scale[0].view(1, self.z_dim, 1, 1, 1) + s1 = scale[1].view(1, self.z_dim, 1, 1, 1) + return (z - s0) * s1 # [B,z_dim,T,H,W] + + def _denormalize_latent(self, z: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Invert the normalization applied by _encode_features_to_mu.""" + assert len(scale) == 2, "scale must be a tuple with two tensors" + s0 = scale[0].view(1, self.z_dim, 1, 1, 1) + s1 = scale[1].view(1, self.z_dim, 1, 1, 1) + return z / s1 + s0 # [B,z_dim,T,H,W] + + def _encode_chunk_impl( + self, + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + scale: tuple[torch.Tensor, torch.Tensor], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + """Run the encoder on one temporal chunk and normalize the output. + + Defined as an instance method (not a closure inside ``encode``) so + that ``_ChunkEncodeForAOT`` can wrap it for ``torch.export``. + + Note: Since ``feat_cache`` is mutated in-place by the encoder (each + ``CausalConv3d`` layer overwrites its slot), we pass a shallow copy + to preserve the original cache for compilation. + """ + feat_cache = list(feat_cache) + + assert all(c is None or c.is_contiguous() for c in feat_cache) + assert x_chunk.is_contiguous() + + out = self.encoder(x_chunk, feat_cache=feat_cache) + + assert out.is_contiguous() + assert all(c is None or c.is_contiguous() for c in feat_cache) + + # Project encoder features through conv1, split to mu/log_var, and normalize. + mu, _log_var = self.conv1(out).chunk(2, dim=1) + return self._normalize_latent(mu, scale), feat_cache + + def encode(self, x: torch.Tensor, scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor: + """Chunked causal encoding that converts pixel-space video to latent space. + + The Wan 2.2 VAE encoder uses causal 3D convolutions, which means each + ``CausalConv3d`` layer needs temporal context from previous frames. For + long videos, running the full sequence at once would create intermediate + tensors of shape ``(B*T, C, H, W)`` that can exceed Triton's int32 + indexing limit. Instead, we split the video into fixed-size temporal + chunks and maintain a per-layer feature cache (``feat_cache``) so each + chunk can access the causal context from previous chunks. + + **Encoding strategy:** + + 1. The first frame is encoded alone (the "key-frame prime") to seed + the causal caches with initial state. + 2. Subsequent frames are processed in chunks of ``temporal_window`` (e.g. + 68 frames). Each chunk reads from and writes to the shared + ``feat_cache`` list, which stores the last ``CACHE_T=2`` frames of + activations per ``CausalConv3d`` layer. + 3. The per-chunk latents are concatenated along the temporal axis. + + **AOT compilation:** + + When ``_aot_chunk_fns`` has been installed (via + :meth:`Wan2pt2VAEInterface.compile_encode`), + each chunk is dispatched to a pre-compiled ``.pt2`` function keyed by + ``(T_chunk, H_patch, W_patch, cache_t)``. Padding ensures every chunk + (except possibly the last, handled by ``should_pad``) has exactly + ``temporal_window`` frames, keeping the set of compiled input shapes small. + + Args: + x: Pixel-space video tensor of shape ``[B, 3, T, H, W]``. + ``T`` must satisfy ``T == 1`` or ``(T - 1) % 4 == 0`` (the + 4x temporal compression constraint). Use ``pad_video_batch`` + to pad to a valid length before calling. + scale: Tuple of ``(mean, inv_std)`` tensors, each of shape + ``[z_dim]``. Used to normalize the latent: ``(z - mean) * inv_std``. + + Returns: + Normalized latent tensor of shape ``[B, z_dim, T_latent, H//16, W//16]`` + where ``T_latent = 1 + (T - 1) // 4``. Always corresponds to the + *original* (unpadded) input length, even when internal padding was applied. + """ + T, H, W = x.shape[2], x.shape[3], x.shape[4] + + # ``temporal_window`` can be a per-resolution mapping (e.g. + # {"256": 68, "480": 32, "720": 16}) from a Hydra/OmegaConf config, + # which arrives as a ``DictConfig`` (not a plain ``dict``). + # Using ``Mapping`` catches both. + if isinstance(self.temporal_window, Mapping): + resolution = get_vision_data_resolution((H, W)) + temporal_window = self.temporal_window[resolution] + else: + temporal_window = self.temporal_window + + assert T == 1 or (T - 1) % 4 == 0, ( + f"Input temporal length must be 4n+1 (got {T}). " + "Use pad_video_batch to pad before encoding, check wan2pt2_vae_4x16x16_test on how to use it." + ) + + # The 4x temporal compression maps T pixel frames → ceil-like latent frames. + # For T=1 (single image), latent_T=1. For T=4n+1, latent_T=n+1. + latent_T = 1 + (T - 1) // 4 + + # Certain short-clip durations (e.g. robotics datasets with T=17) can be + # encoded at their exact length, avoiding the overhead of padding to the + # next chunk boundary. All other lengths are padded so that each chunk + # has exactly ``temporal_window`` frames, giving the compiled function a + # fixed input shape per {resolution, aspect_ratio} bucket. + should_pad = T not in self._encode_exact_durations + + if should_pad: + # Pad T to ``1 + k * temporal_window`` so that after removing the 1-frame + # prime, the remaining frames divide evenly into ``temporal_window``-sized chunks. + T = 1 + ((T - 1 + temporal_window - 1) // temporal_window) * temporal_window + x = F.pad(x, (0, 0, 0, 0, 0, T - x.shape[2])) + + # One cache slot per CausalConv3d layer in the encoder, initially all None. + enc_cache = self._new_enc_cache() + + # Patchify merges each 2×2 spatial patch into the channel dim: + # [B, 3, T, H, W] → [B, 12, T, H//2, W//2]. + x = patchify(x, patch_size=2) + + aot_chunk_fns: dict | None = getattr(self, "_aot_chunk_fns", None) + H_patch, W_patch = x.shape[3], x.shape[4] + + def _run_chunk( + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + """Encode one chunk, through the AOT path or eager fallback. + + If AOT-compiled chunk functions are installed, the function is + looked up by ``(T_chunk, H_patch, W_patch, cache_t)`` where + ``cache_t`` is the minimum temporal extent of the cache tensors + (0 = all-None prime, 1 = post-prime, 2 = steady state). + + Both full-size and remainder chunks (from ``encode_exact_durations``) + are dispatched through the AOT path when a matching compiled + function exists; uncompiled shapes fall back to eager. + """ + is_prime = feat_cache[0] is None + + # Ensure contiguity so AOT-compiled functions receive tensors + # with deterministic strides regardless of the source slice. + x_chunk = x_chunk.contiguous() + + if aot_chunk_fns is not None: + cache_t = 0 if is_prime else feat_cache[0].shape[2] + aot_key = (x_chunk.shape[2], H_patch, W_patch, cache_t) + aot_fn = aot_chunk_fns.get(aot_key) + + if aot_fn is not None: + return aot_fn(x_chunk, feat_cache) + + return self._encode_chunk_impl(x_chunk, feat_cache, scale) + + # --- Chunked encoding loop --- + # Chunk 0: single-frame "key-frame prime" to seed all causal caches. + out, enc_cache = _run_chunk(x[:, :, :1], feat_cache=enc_cache) + outs = [out] + + # Chunks 1..N: process the remaining frames in fixed-size windows. + for start in range(1, T, temporal_window): + x_chunk = x[:, :, start : start + temporal_window] + out, enc_cache = _run_chunk(x_chunk, feat_cache=enc_cache) + outs.append(out) + + final_out = torch.cat(outs, dim=2) if len(outs) > 1 else outs[0] + + # If we padded the input, trim the latent back to the original length. + if should_pad: + final_out = final_out[:, :, :latent_T] + return final_out + + def decode( + self, + z, + scale, + clear_decoder_cache: bool, + ): # z: [B,z_dim,T_latent,H_latent,W_latent] -> [B,3,T,H,W] + if clear_decoder_cache: + self._dec_cache = self._new_dec_cache() + + z = self._denormalize_latent(z, scale) + x = self.conv2(z) # [B,z_dim,T_latent,H_latent,W_latent] + + parts = [] + for i in range(x.shape[2]): + first_chunk = (i == 0) and all(c is None for c in self._dec_cache) + parts.append( + self.decoder( + x[:, :, i : i + 1], + feat_cache=self._dec_cache, + first_chunk=first_chunk, + ) + ) + + if clear_decoder_cache: + self._dec_cache = self._new_dec_cache() + + decoded = unpatchify(torch.cat(parts, dim=2), patch_size=2) # [B,3,T,H,W] + return decoded # [B,3,T,H,W] + + def clear_decoder_cache(self) -> None: + self._dec_cache = self._new_dec_cache() + + +def _video_vae( + pretrained_path=None, + device="cpu", + object_store_credential_path_pretrained="", + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, +): + """ + Autoencoder3d adapted from Wan 2.2. + """ + # init model + with torch.device("meta"): + model = WanVAE_( + temporal_window=temporal_window, + encode_exact_durations=encode_exact_durations, + ) + if not TRAINING: + model.to_empty(device=device) + + if pretrained_path is None: + model.to_empty(device=device) + else: + if get_rank() == 0: + if not INTERNAL: + from cosmos.utils.checkpoint_db import download_checkpoint_v2 + + pretrained_path = download_checkpoint_v2(pretrained_path) + if pretrained_path.startswith("s3://"): + backend_args = { + "backend": "s3", + "s3_credential_path": object_store_credential_path_pretrained, + } + else: + backend_args = None + + ckpt = easy_io.load( + pretrained_path, + backend_args=backend_args, + map_location=device, + ) + + # load checkpoint + log.info(f"loading {pretrained_path}") + model.load_state_dict(ckpt, assign=TRAINING) + else: + model.to_empty(device=device) + # Ensure all params/buffers are contiguous on every rank before + # `sync_model_states` performs its shape+stride verification. + # `assign=TRAINING` on rank 0 replaces params with loaded tensor objects, + # whose storage may have different strides than the `to_empty`-initialized + # tensors on other ranks. Without this, `_verify_param_shape_across_processes` + # raises: "params[N] ... appears not to match strides of the same param in process 0". + for p in model.parameters(): + if not p.is_contiguous(): + p.data = p.data.contiguous() + for b in model.buffers(): + if not b.is_contiguous(): + b.data = b.data.contiguous() + sync_model_states(model) + + return model + + +class WanVAE: + def __init__( + self, + z_dim=48, + vae_pth="", + object_store_credential_path_pretrained="", + dtype=torch.bfloat16, + device=DEVICE, + is_amp=True, + temporal_window: int | Mapping[str, int] = 4, + encode_exact_durations: list[int] | None = None, + ): + self.dtype = dtype + self.device = device + + # Wan 2.2 mean and std values (48 dimensions) + mean = [ + -0.2289, + -0.0052, + -0.1323, + -0.2339, + -0.2799, + 0.0174, + 0.1838, + 0.1557, + -0.1382, + 0.0542, + 0.2813, + 0.0891, + 0.1570, + -0.0098, + 0.0375, + -0.1825, + -0.2246, + -0.1207, + -0.0698, + 0.5109, + 0.2665, + -0.2108, + -0.2158, + 0.2502, + -0.2055, + -0.0322, + 0.1109, + 0.1567, + -0.0729, + 0.0899, + -0.2799, + -0.1230, + -0.0313, + -0.1649, + 0.0117, + 0.0723, + -0.2839, + -0.2083, + -0.0520, + 0.3748, + 0.0152, + 0.1957, + 0.1433, + -0.2944, + 0.3573, + -0.0548, + -0.1681, + -0.0667, + ] + std = [ + 0.4765, + 1.0364, + 0.4514, + 1.1677, + 0.5313, + 0.4990, + 0.4818, + 0.5013, + 0.8158, + 1.0344, + 0.5894, + 1.0901, + 0.6885, + 0.6165, + 0.8454, + 0.4978, + 0.5759, + 0.3523, + 0.7135, + 0.6804, + 0.5833, + 1.4146, + 0.8986, + 0.5659, + 0.7069, + 0.5338, + 0.4889, + 0.4917, + 0.4069, + 0.4999, + 0.6866, + 0.4093, + 0.5709, + 0.6065, + 0.6415, + 0.4944, + 0.5726, + 1.2042, + 0.5458, + 1.6887, + 0.3971, + 1.0600, + 0.3943, + 0.5537, + 0.5444, + 0.4089, + 0.7468, + 0.7744, + ] + + mean = torch.tensor(mean, dtype=dtype, device=device) # [z_dim] + std = torch.tensor(std, dtype=dtype, device=device) # [z_dim] + self.scale = (mean, 1.0 / std) + + # init model + self.model = _video_vae( + pretrained_path=vae_pth, + object_store_credential_path_pretrained=object_store_credential_path_pretrained, + device=device, + temporal_window=temporal_window, + encode_exact_durations=encode_exact_durations, + ) + + self.model = self.model.eval().requires_grad_(False) + self.is_amp = is_amp + if not is_amp: + self.model = self.model.to(dtype=dtype) + self.context = nullcontext() + else: + self.context = torch.amp.autocast("cuda", dtype=dtype) + + def count_param(self) -> int: + return sum(p.numel() for p in self.model.parameters()) + + @torch.no_grad() + def encode(self, videos: torch.Tensor) -> torch.Tensor: + """Encode a batch of videos. + + AOT-compiled chunk functions (if installed on ``self.model`` via + :meth:`Wan2pt2VAEInterface.compile_encode`) + are dispatched from inside ``WanVAE_.encode`` at the per-chunk level, + preserving the chunked encoding loop and temporal padding logic. + + Args: + videos: Tensor of shape ``[B, C, T, H, W]``. + + Returns: + Tensor of shape ``[B, z_dim, T//4, H//16, W//16]``. + """ + in_dtype = videos.dtype + with self.context: + if not self.is_amp: + videos = videos.to(self.dtype) + latent = self.model.encode(videos, self.scale) + latent = latent.to(in_dtype) + return latent + + @torch.no_grad() + def decode(self, zs: torch.Tensor, clear_decoder_cache: bool = True) -> torch.Tensor: + """Decode a batch of latent tensors. + + Args: + zs: Tensor of shape ``[B, z_dim, T, H, W]``. + clear_decoder_cache: Whether to clear the decoder cache between decode calls. + + Returns: + Tensor of shape ``[B, C, T, H, W]``. + """ + in_dtype = zs.dtype + with self.context: + if not self.is_amp: + zs = zs.to(self.dtype) + video_recon = self.model.decode(zs, self.scale, clear_decoder_cache) + video_recon = video_recon.to(in_dtype) + return video_recon + + +# --------------------------------------------------------------------------- +# AOT compilation helpers +# --------------------------------------------------------------------------- + +_ShapeInfo = tuple[int, int, int] # (chunk_frames, H_patch, W_patch) +_AOTChunkKey = tuple[int, int, int, int] # (T_chunk, H_patch, W_patch, cache_t) + + +class _ChunkEncodeForAOT(torch.nn.Module): + """Wrapper around ``WanVAE_._encode_chunk_impl`` for ``torch.export``. + + Absorbs the ``(mean, inv_std)`` scale as registered buffers so the + exported signature is just ``(x_chunk, feat_cache)``. A separate + wrapper instance (and export) is created per ``cache_t`` because the + ``None``-pattern in ``feat_cache`` differs between ``cache_t=0`` + (all ``None``) and ``cache_t>=1`` (some tensors, some ``None``). + """ + + def __init__( + self, + vae_model: torch.nn.Module, + scale_mean: torch.Tensor, + scale_inv_std: torch.Tensor, + ) -> None: + super().__init__() + self.vae = vae_model + self.register_buffer("scale_mean", scale_mean.clone()) + self.register_buffer("scale_inv_std", scale_inv_std.clone()) + + def forward( + self, + x_chunk: torch.Tensor, + feat_cache: list[torch.Tensor | None], + ) -> tuple[torch.Tensor, list[torch.Tensor | None]]: + return self.vae._encode_chunk_impl( + x_chunk, + feat_cache, + (self.scale_mean, self.scale_inv_std), + ) + + +def _collect_warmup_shapes( + tokenizer: "Wan2pt2VAEInterface", + warmup_resolutions: Sequence[str], + aspect_ratio: str | None = None, +) -> list[_ShapeInfo]: + """Return ``[(chunk_frames, H_patch, W_patch), ...]`` for all warmup shapes. + + Expands *warmup_resolutions* into concrete spatial shapes using + ``VIDEO_RES_SIZE_INFO``. Each resolution may have multiple aspect + ratios (e.g. ``"16,9"``, ``"9,16"``, ``"1,1"``); optionally filtered + to a single ratio via *aspect_ratio*. ``chunk_frames`` is looked up + from the tokenizer (scalar or per-resolution dict). Spatial + dimensions are halved to account for patchify (``patch_size=2``). + """ + all_shapes: list[_ShapeInfo] = [] + for res_key in warmup_resolutions: + if res_key not in VIDEO_RES_SIZE_INFO: + raise ValueError(f"Resolution {res_key} not found in VIDEO_RES_SIZE_INFO") + + if isinstance(tokenizer.encode_chunk_frames, Mapping): + if res_key not in tokenizer.encode_chunk_frames: + raise ValueError(f"Resolution {res_key} not found in tokenizer.encode_chunk_frames") + + res_dict = VIDEO_RES_SIZE_INFO[res_key] + if aspect_ratio is not None: + if aspect_ratio not in res_dict: + raise ValueError(f"Aspect ratio {aspect_ratio} not found in resolution {res_key}") + res_dict = {aspect_ratio: res_dict[aspect_ratio]} + + for H, W in res_dict.values(): + if isinstance(tokenizer.encode_chunk_frames, Mapping): + chunk_frames = tokenizer.encode_chunk_frames[res_key] + else: + chunk_frames = tokenizer.encode_chunk_frames + + H_patch, W_patch = H // 2, W // 2 + all_shapes.append((chunk_frames, H_patch, W_patch)) + return all_shapes + + +class Wan2pt2VAEInterface(VideoTokenizerInterface): + def __init__( + self, + bucket_name: str = "", + object_store_credential_path_pretrained: str = "", + vae_path: str = "", + chunk_duration: int = 93, + keep_decoder_cache: bool = False, + use_streaming_encode: bool = False, + # Granularity of the encoding chunks. Larger values result in higher TensorCore utilization, + # and lower values result in lower memory usage. To optimize for speed and memory usage, + # use a dictionary of chunk frames, one for each resolution. If a single integer is provided, + # it will be used for all resolutions. + encode_chunk_frames: int | Mapping[str, int] = 4, + # Exact frame durations that get encoded without padding. Useful for short-clip datasets + # (e.g. robotics) where the standard bucketing would inflate the input by many multiples + # (e.g. 17 frames → 69 with encode_chunk_frames=68). Must be a list of integers. + encode_exact_durations: list[int] | None = None, + # Compression factors for spatial and temporal dimensions (4x16x16 tokenizer). + spatial_compression_factor: int = 16, + temporal_compression_factor: int = 4, + # Deprecated arguments. This is kept for backwards compatibility + # with older configurations. + temporal_window: int | None = None, + encode_bucket_multiple: int | None = None, + ): + # Remove temporal_window and encode_bucket_multiple once they have been + # removed from the uploaded HuggingFace checkpoint. + if temporal_window is not None: + log.warning("temporal_window is deprecated; remove it.") + del temporal_window + + if encode_bucket_multiple is not None: + log.warning("encode_bucket_multiple is deprecated; remove it.") + del encode_bucket_multiple + + # Remove special handling for encode_chunk_frames once the uploaded + # HuggingFace checkpoint has been updated to use a dictionary of chunk frames, + # one for each resolution. + if isinstance(encode_chunk_frames, int): + encode_chunk_frames = {"256": 68, "480": 24, "720": 12} + assert isinstance(encode_chunk_frames, Mapping) + + assert all(c % 4 == 0 for c in encode_chunk_frames.values()), "encode_chunk_frames must be a multiple of 4" + + self.chunk_duration = chunk_duration + + # Local-path support: skip the s3:// prefix when bucket_name is empty + # so OSS users can point vae_path at an absolute local file. + vae_path_full = f"s3://{bucket_name}/{vae_path}" if bucket_name else vae_path + self.model = WanVAE( + dtype=torch.bfloat16, + is_amp=False, + vae_pth=vae_path_full, + object_store_credential_path_pretrained=object_store_credential_path_pretrained, + temporal_window=encode_chunk_frames, + encode_exact_durations=encode_exact_durations, + ) + self.encode_chunk_frames = encode_chunk_frames + self.encode_exact_durations = encode_exact_durations + + # When True, keep the decoder cache between decode calls. + self._keep_decoder_cache = keep_decoder_cache + + # When True, always use the streaming/chunked encode path (correct but typically slower). + self.use_streaming_encode = use_streaming_encode + + self._spatial_compression_factor = spatial_compression_factor + self._temporal_compression_factor = temporal_compression_factor + + @property + def dtype(self) -> torch.dtype: + return self.model.dtype + + def reset_dtype(self) -> None: + pass + + @contextmanager + def use_cached_decoder(self) -> Generator[None, None, None]: + """Enable decoder-cache reuse for sequential decode calls.""" + self.model.model.clear_decoder_cache() + self._keep_decoder_cache = True + try: + yield + finally: + self.model.model.clear_decoder_cache() + self._keep_decoder_cache = False + + def encode(self, state: torch.Tensor) -> torch.Tensor: + """Encode a batch of videos. + + Args: + state: Tensor of shape ``[B, C, T, H, W]``. + + Returns: + Tensor of shape ``[B, z_dim, T//4, H//16, W//16]``. + """ + return self.model.encode(state) + + def decode(self, latent: torch.Tensor) -> torch.Tensor: + """Decode a batch of latent tensors. + + Args: + latent: Tensor of shape ``[B, z_dim, T, H, W]``. + + Returns: + Tensor of shape ``[B, C, T, H, W]``. + """ + return self.model.decode( + latent, + clear_decoder_cache=not self._keep_decoder_cache, + ) # [B,3,T,H,W] + + @torch.no_grad() + def compile_encode( + self, + warmup_resolutions: Sequence[str], + output_dir: str, + aspect_ratio: str | None = None, + ) -> None: + """AOT-compile the tokenizer's chunk-level encode for every resolution. + + Compiles ``WanVAE_._encode_chunk_impl`` for each + ``(resolution, aspect_ratio, cache_t)`` variant, producing ``.pt2`` + packages that are loaded on all ranks for zero-overhead dispatch + during training. + + **Variant enumeration** — for each resolution, three standard + ``cache_t`` variants (prime, post-prime, steady-state) are compiled. + When ``encode_exact_durations`` is configured, additional remainder + variants are appended. + + **Distribution** — individual variants are assigned round-robin + across ranks. Reference caches are built lazily. + + **Shared weights** — packages are compiled with + ``package_constants_in_so=False``. After loading, each runner + receives the same encoder weights via + ``load_constants(user_managed=True)``. + + Compiled functions are installed as ``self.model.model._aot_chunk_fns`` + for dispatch by ``_run_chunk`` inside ``WanVAE_.encode``. + + Args: + warmup_resolutions: Resolution keys (e.g. ``["256", "480", "720"]``). + output_dir: Root directory under which compiled ``.pt2`` packages + are written (an ``aot_tokenizer/`` subdirectory will be + created). Typically the job's local output path + (``config.job.path_local``). + aspect_ratio: If given, only compile this single aspect ratio per + resolution instead of all available ratios. + """ + import torch._inductor + import torch.distributed as dist + + log.info(f"AOT chunk-level warmup for resolutions: {warmup_resolutions}", rank0_only=False) + start_time = time.time() + + save_dir = os.path.join(output_dir, "aot_tokenizer") + + all_shapes = _collect_warmup_shapes(self, warmup_resolutions, aspect_ratio) + + is_distributed = dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1 + rank = dist.get_rank() if is_distributed else 0 + world_size = dist.get_world_size() if is_distributed else 1 + + wanvae = self.model # WanVAE (plain class) + wanvae_model = wanvae.model # WanVAE_ (nn.Module) + scale = wanvae.scale # (mean, 1/std) + n_cache_slots = wanvae_model._enc_conv_num + + if rank == 0: + log.info(f"Saving AOT compiled packages to {save_dir}") + os.makedirs(save_dir, exist_ok=True) + if is_distributed: + dist.barrier() + + # -- Helper functions -------------------------------------------------- + + def _rand_cache(cache: list[torch.Tensor | None]) -> list[torch.Tensor | None]: + return [torch.rand_like(c) if c is not None else None for c in cache] + + def _rand_input(t: int, h: int, w: int) -> torch.Tensor: + return torch.rand((1, 12, t, h, w), dtype=torch.bfloat16, device="cuda") + + def _compile_variant( + wrapper: _ChunkEncodeForAOT, + aot_key: _AOTChunkKey, + ref_cache: list[torch.Tensor | None], + ) -> str | None: + """Export + compile one variant, returning the .pt2 path or None.""" + t_chunk, H_patch, W_patch, cache_t = aot_key + pkg_name = f"chunk_ct{cache_t}_{t_chunk}f_{H_patch}x{W_patch}.pt2" + pkg_path = os.path.join(save_dir, pkg_name) + + if os.path.exists(pkg_path): + log.info(f"Rank {rank}: reusing cached {pkg_name}", rank0_only=False) + return pkg_path + + t0 = time.time() + try: + exported = torch.export.export( + wrapper, + (_rand_input(t_chunk, H_patch, W_patch), _rand_cache(ref_cache)), + strict=False, + ) + torch._inductor.aoti_compile_and_package( + exported, + package_path=pkg_path, + inductor_configs={"aot_inductor.package_constants_in_so": False}, + ) + log.info( + f"Rank {rank}: AOT compiled cache_t={cache_t} " + f"{t_chunk}f {H_patch}x{W_patch} in {time.time() - t0:.1f}s", + rank0_only=False, + ) + return pkg_path + except Exception as e: + log.warning( + f"Rank {rank}: AOT compile failed for cache_t={cache_t} {t_chunk}f {H_patch}x{W_patch}: {e}", + rank0_only=False, + ) + return None + + # -- Enumerate all variant keys and distribute across ranks ------------ + + all_variant_keys: list[tuple[_AOTChunkKey, _ShapeInfo]] = [] + seen_keys: set[_AOTChunkKey] = set() + for chunk_frames, H_patch, W_patch in all_shapes: + for cache_t in (0, 1, 2): + t_chunk = 1 if cache_t == 0 else chunk_frames + aot_key: _AOTChunkKey = (t_chunk, H_patch, W_patch, cache_t) + + assert aot_key not in seen_keys, f"Duplicate AOT key: {aot_key}" + seen_keys.add(aot_key) + all_variant_keys.append((aot_key, (chunk_frames, H_patch, W_patch))) + + for T in sorted(self.encode_exact_durations or []): + remaining = T - 1 + if remaining <= 0: + continue + remainder = remaining % chunk_frames + if remainder == 0: + continue + n_full = remaining // chunk_frames + cache_t = 1 if n_full == 0 else 2 + aot_key = (remainder, H_patch, W_patch, cache_t) + + if aot_key not in seen_keys: + seen_keys.add(aot_key) + all_variant_keys.append((aot_key, (chunk_frames, H_patch, W_patch))) + + my_variant_keys = [v for i, v in enumerate(all_variant_keys) if i % world_size == rank] + log.info( + f"Rank {rank}: assigned {len(my_variant_keys)}/{len(all_variant_keys)} variants (world_size={world_size})", + rank0_only=False, + ) + + # -- Build reference caches lazily, only for this rank's shapes -------- + + wrapper = _ChunkEncodeForAOT(wanvae_model, scale[0], scale[1]) + wrapper.eval() + + def _get_ref_caches( + chunk_frames: int, + H_patch: int, + W_patch: int, + ) -> dict[int, list[torch.Tensor | None]]: + cache_ct0: list[torch.Tensor | None] = [None] * n_cache_slots + _, cache_ct1 = wanvae_model._encode_chunk_impl( + _rand_input(1, H_patch, W_patch), + list(cache_ct0), + scale, + ) + _, cache_ct2 = wanvae_model._encode_chunk_impl( + _rand_input(chunk_frames, H_patch, W_patch), + list(cache_ct1), + scale, + ) + return {0: cache_ct0, 1: cache_ct1, 2: cache_ct2} + + ref_cache_map: dict[_ShapeInfo, dict[int, list[torch.Tensor | None]]] = {} + + my_pkg_paths: dict[_AOTChunkKey, str] = {} + for aot_key, shape_info in my_variant_keys: + cache_t = aot_key[3] + if shape_info not in ref_cache_map: + ref_cache_map[shape_info] = _get_ref_caches(*shape_info) + ref_cache = ref_cache_map[shape_info][cache_t] + pkg_path = _compile_variant(wrapper, aot_key, ref_cache) + if pkg_path is not None: + my_pkg_paths[aot_key] = pkg_path + + # -- Gather .pt2 paths from every rank so all ranks can load all variants. + if is_distributed: + gathered: list[dict[_AOTChunkKey, str] | None] = [None] * world_size + dist.all_gather_object(gathered, my_pkg_paths) + pkg_paths: dict[_AOTChunkKey, str] = {} + for rank_paths in gathered: + if rank_paths: + pkg_paths.update(rank_paths) + dist.barrier() + else: + pkg_paths = my_pkg_paths + + # -- Load every .pt2 package and bind to the existing encoder weights. -- + device_index = torch.cuda.current_device() + state_dict = wrapper.state_dict() + + loaded_fns: dict[_AOTChunkKey, Callable] = {} + for key, pkg_path in pkg_paths.items(): + try: + fn = torch._inductor.aoti_load_package(pkg_path, device_index=device_index) + + required_keys = set(fn.get_constant_fqns()) + constants_map = {k: v for k, v in state_dict.items() if k in required_keys} + fn.load_constants(constants_map, check_full_update=True, user_managed=True) + + loaded_fns[key] = fn + except Exception as e: + log.warning( + f"Rank {rank}: failed to load {pkg_path}: {e}", + rank0_only=False, + ) + + wanvae_model._aot_chunk_fns = loaded_fns + + log.info( + f"Rank {rank}: AOT compiled {len(my_pkg_paths)}, " + f"loaded {len(loaded_fns)}/{len(all_variant_keys)} chunk variants, " + f"time: {time.time() - start_time:.2f}s", + rank0_only=False, + ) + + # Clean up .pt2 files so stale packages don't persist across restarts. + if is_distributed: + dist.barrier() + if rank == 0: + import shutil + + try: + shutil.rmtree(save_dir) + log.info(f"Cleaned up AOT cache dir: {save_dir}") + except OSError as e: + log.warning(f"Failed to clean AOT cache dir {save_dir}: {e}") + + if not loaded_fns: + raise RuntimeError("AOT compilation produced no loadable functions") + + def get_latent_num_frames(self, num_pixel_frames: int) -> int: + return 1 + (num_pixel_frames - 1) // 4 + + def get_pixel_num_frames(self, num_latent_frames: int) -> int: + return (num_latent_frames - 1) * 4 + 1 + + @property + def spatial_compression_factor(self) -> int: + return self._spatial_compression_factor + + @property + def temporal_compression_factor(self) -> int: + return self._temporal_compression_factor + + @property + def pixel_chunk_duration(self) -> int: + return self.chunk_duration + + @property + def latent_chunk_duration(self) -> int: + return self.get_latent_num_frames(self.chunk_duration) + + @property + def latent_ch(self) -> int: + return 48 + + @property + def spatial_resolution(self) -> int: + return 512 + + @property + def name(self) -> str: + return "wan2pt2_tokenizer" diff --git a/cosmos_training/cosmos/model/vfm/utils/__init__.py b/cosmos_training/cosmos/model/vfm/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/utils/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a47e2f94adcd358a41c9dbb71ded4117f354016 GIT binary patch literal 241 zcmZ8b%MHRX3~eY#2&sc`!3!WpV1=lZv`UpGk)43L1vh437RF!#R8BqNNG%7RWWVR< zkNhl(O~weHRh#&g(m#eJJNK;5w&M86aKY7(X2u(sNC6epbOrA))Xr!}P&w+DJi1m@ zVXoz=Bqalu+G^WCB!vWhX`2*bT9VEyODeQBNN0IAT4qC-tYQ(veMjmpQ znW1I5RH1FVSb1Tr_!H*TNe%%L9q{Apg=co1$H0ErQ2u+c?h<7>6;v-Sfo#T z&b>1fEmM|(1$IE5nYs7ebI<+z&d>aNQ&UKQ%l0;J5>J#zVulq;+K7smF>Epm7;KIu%oEKF2CxY7W$j3__@tZIz)#Ysj-rzI5 z0X0Nr>Yr+M8`s=!Hwc32XEaP3)yQUiozRB!Wn{JKC(>w8ZLTj-Z2=jfb$*apw`Y*k z3Ub0~8x3T1KNP zQ59PuN=_wdBd2P%X5@&Lv#FU;(v(b3QWGd8mU5F4dlLS}4Vu$#Lqp3}Y-(ZCq+y0~ zhGs#b4rMvptIHTBQMF8_V1d|VMmMH=vx=Rb)N&KVHWe)g4~fuKY>2EV|J}j2$pp<& zQ(>xP4VCJN&>wM)W1KgzSSfGIF$0t=9ZJ>=bI2H9otl!zPOSqkYJK%D|#(Dv6+2xMZ=T$wa4K803 zNR^)IuhFe(-aumb<^Mbos%ViAGlST>d z=8rKs-nPybbX)6%-K^-O=cb0|Ax%<^9ePb4YOxEc}K zqCdX$MRaFH+PQMk1@!I`s%V*2&JryarawPdo(ZSf< zh55q0;+^7>T5jpCNHJ)7{FVQ^UYn1h@5J2j!r7(vkM?}9ryPCxVYD3WeJq_Yi8pEW z?M!3~J5$e1@oW#3H>s5nqc*MOmPj6Ltk0p!uEpZEMIH@q!N)C)JPK}AJ$B^ruCM+ z!RoZ$gc8M^ptj31>@->0L{_QVq9#v+=6upyddJX=WkUNJOi_?f>*LGz%B7K z;I9nYW82Mk1c44DJCN)|f~#*LlA3#wK#(MuhmiCji6J?Ri6}E)Lruw>z#Xh;pF1L{ek5JDWi6!*mEh87Mk#_z|MqvdS}D^eV!0pIY#IaFf#L^*n>A|+PheTdE! z)ma`Xcf=}EU){22wOe^E^x|YVesBbwplzgXOv8q;9NDCH!OW%II1*R{p%MP*{;v(N zsI(}|i8G?@#-4&u6znRu7gJi>eNa9nVe_Xi)vpOf@gK#%6(__if{iw#f zv#?gL)b^mVNbA{t%mYQ?tZ>hl_Du+B-*wnIv%Z>kzB(-eMIV=z6N|q4zPr06VHR33 zIeu3T?8RAs(LX2Nm0=wI=Z^zqbb<^CT1cj^C|!Y&=n7~f!hhB`?LGOYg_xxHU`np$ zrcEVJa@4Xh+yggPr@d6CS&Z<|abm$A$!I!4*x1;xk)vZ{1ZquGhOd|`OGxv=7=VN1_`P<42 z8K+gHKp8Twn6xSgg>gON1ma|c^yCf8(#G`}ciLiXJ&2L1s++7zBd`=Gz`M>V=}DIj z!WK3tTud^lSj3uC@~B5oB9VxJYSo6-lh;q)qBGNmsS=oo0&WFnhTv4Hut{K&#>SX8 zo#4)N!Zyans!~t|ZykTot+M&+?kC;pf~s`S!rp5nfxSdA?laU{Z!nL`-I#Vv-Yrur&v6OD;{B)pC5c;rMl$iv`R+*3W><39HA1#_-$^R$8`y9&TSqe|X|? zn5?vREch4Y`!YlqOPxSUEeA@C2fl0w%^&~qt%a^16+a9uA76g|!Ksf=Jl@&2a{l7) zo8S7C{o&!|5h&QzH*YVT`J3raAeQJaHNQ3Q2l7SFk>5v7FCTsQ{x4?dN9Hfz8@V%5 zZrZnSq0+Rk6gfS24#d5?*vcT~v+k`ngp47GQkFi$-Df5c zG^wJ`aGY!e-RC4?`pIx1I}WB4L_9SRj1c(ja&w1+#>=yw$KnKj&=VLnRU5<9CbS$R)m*(HVV#2!Jx3N;nHv1&7A_WWf)v_d)|om16LK%qk(E58 zT~y}WL#7$>&NpJR`8u2!cD$WN%1#DVoe-aJNLbx+g4i|NxQ>{z@YwT7sX1jular3LSG5~VBh>1aMYXH?_IicX=!k|zq}{$yGZg$1Jtbgq1D$R zA<}m5^*gUG+^U30Ng^x&ahfgqet`nUr}DazvsmT}JZ~+FZeSifZysT#!9lEEm`h{m zHpo2h8qcJ=M;8uxwpgOUhgoVTjwIu6-+g|<>0o%M;*=&4zGt77I%ln z#=sHe3Ea~oW&u3$<02ct4)lPV z)M}2w!wei?4UXwJoH+C-5v*P4=LZc?-J1kr8q@p?JHe{ngUoHYJy<(vz~}-5Az8@H z5t9~&!F0T23trULk34jw>Y3cm3wH>p{rM*Xg-Ts?h|GMk97d{)hRvxbd6U^O|v8h`?@7jpQoul?nWBLm z?VU>pKZ<`4FSj3hc%a;#SPeiav}kR|H2rD=zBLL>+nD$uzJ);A{KY~=8D4&hA}K{pQH+_vCAPYK-Yt~7#2pwZm~f9Qdk<5yX8@( zTd}{(VRf|9U9tU2SnJlrs;E8KOa;#0|NrQq{56Q{HpEKN`LW|yA3FYuD1Tat_xLo~ zs@t?*Yxuf4|Jq%fR$6ub*P`R>_q9>nGc1UVPy_A>%?47ju~0hDMnr~<#7L?sCNMJ; z?6y2)SP^N=8VSX;;ok9Hz258Hw)5^8ZbYxg0?jyUk0yrqL(yh;re-K3Ci@QrHZwzR zSQwmOvvylfj1GL!Zj*e-F;aQMJx`9tK}eGNauWGmP4^hPiF0bEgdWRC2M}z}ss#6g z=!kjT8w#J{&{`^8-|qG3i;IK0A)gA>>$Mn~%1P)mvJ|$3IgPL?-@1P&@4*(C!e%-ajE4b_J%pU$JG*9rV=_5N zfrR+&BN4gBJB(f@I}FC7U-UX@!JN=;q9WL-KV1s2I@*Qnqm;GwlIITze`&o1(S#=--13m0XJ?6lA-@~fEe z(;-~eMiTZN#VP27;*T-_g%1Q$0yqr}xe&6~$7XlT6}e5wxS`Z#ceGm)c0V~0N(H_z zB67XU!J$}<9TXF8@KD410mX;f`fDbt*MlKw$2-Y}KMFJs8ef{}gs%T72!4i}w) zKJAc{feZtwJD$`dyv9n>Vbb4jpKIYFfd>(G>r6EAd}qp|Blx zs@5S+5ff0aP_sr&Ep2wt(Qlx6=sYV|Dz)=UxKi1?D53X^dht41a(Ui(fG;oQ zjoM<~KqguBOSw!^#Yo*R=SEQC{f1x3jeufcq?#M4Pgh#aH2>Ig%wL16+>T4}C6zr_ zjniEEukz0!vM%OO!IE-JoS@fpsZ>$PJmtzq08r-E7+)^FtS>N0Sl_paf1&o-%6v)v zWqg3`5QDV!63MWl5=s#miU~>%?RF#(>Zr3+brEWZo=B91R?`K#WNTbf&6Nb(!U1y<_SKPo zH_Hwrb7(V`YwNOp*P<{6SHU(~d^8cX6==&1P&i=g-%G~sU<&BYJa3RFUz`Mvf9P>1We|cn){Id-1FD$XMAl&NQPq`aIIwZ&~HOs+z z8SBbHoqByt)Wg|H%+%PcP$qU?_waLQ{qz5u{u9cP#>}V`(7nh`W|QgMSVGy)7bPPNh0#`ZySC zcCdeV_}&5Cj5twXD=eX@K;7fmIJ1Tk#LtM!Vi>i+;5w{fugIr-AK)(P(*;Edu}jX8 z@f`YOV8=O}H)58t4CfV6(@j)#5e8Ppvuk-^FP##ST@qrn?rdDK@qf^&D&i}%vLARx zeI4DiCOiPw>NsU9{Hp_B?FF$M$6g6l(hghR0|5+E)VHbGqRFe-eK4NYrY{?4S`GD8 z>eGKY)D>!8qozX5H>kNr&0EyGP0dwmwy9a-TlD`7Ma>WJr;pKqZxs{1wfVeQt2Bvk zZJtx_d=tIr#cDQ&9+)AGm9sJQ&YPFd1AnVlu8=(NH}qouM!UDxtpda!2NPUX!5+^tJ|!RyErNOSJA9uA;ydD#Ix$5{ilE;_84*5911&z);eo#uJXS$p--w;u2OYUkIL-!^~K{GIrH_$2(p zdw=@ij~{&Y>id6b-uz_syy_GiFNzz5%|}}o4qi~R*f^(w7jM5)xbkT03kNT&@;?C; C?TmT= literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/utils/__pycache__/safetensors_loader.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/utils/__pycache__/safetensors_loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f68b20f0abeb679ed9fe1f332442c66a8c8877a GIT binary patch literal 48538 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 Jqy$_Z006A??YaN} literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/utils/data_and_condition.py b/cosmos_training/cosmos/model/vfm/utils/data_and_condition.py new file mode 100644 index 00000000..58456c01 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/utils/data_and_condition.py @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unified data and condition interface where we save the tokenized states and/or +noised latent states for diffusion/flow-matching training. +Used for the VFM generation model. +""" + +from dataclasses import dataclass + +import torch + + +@dataclass(slots=True) +class GenerationDataClean: + """ + Container for tokenized states and conditioning info (clean states) + for the multi-modal (vision, sound, action) MoT training. + Used for the VFM generation model. + """ + + batch_size: int + # Vision (list of per-sample tensors) + is_image_batch: bool + raw_state_vision: list[torch.Tensor] | None = None # raw state in pixel space + x0_tokens_vision: list[torch.Tensor] | None = None # tokenized latent state + fps_vision: torch.Tensor | None = None + + # Image editing: number of vision items per sample. + # When set, x0_tokens_vision is a flat list of individually-encoded image latents + # (e.g. [src1, tgt1, src2, tgt2, ...]) and this field records how many items belong + # to each sample (e.g. [2, 2, ...]). None for standard T2I/T2V (one item per sample). + num_vision_items_per_sample: list[int] | None = None + + # Audio (Sound) + raw_state_sound: torch.Tensor | None = None + x0_tokens_sound: torch.Tensor | None = None + fps_sound: torch.Tensor | None = None + + # Action (dense list of per-sample tensors, only action-having samples) + raw_state_action: list[torch.Tensor] | None = None + x0_tokens_action: list[torch.Tensor] | None = None + fps_action: torch.Tensor | None = None + action_domain_id: list[torch.Tensor] | None = None # per-sample domain IDs, None when no action samples + raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation + + +@dataclass(slots=True) +class GenerationDataNoised: + """Container for states after noise addition, along with other + helper attributes for the flow-matching (gt velocity and noise) + for the multi-modal (vision, sound, action) MoT training. + Used for the VFM generation model. + """ + + batch_size: int + # Vision + epsilon_vision: torch.Tensor # unit gaussian noise tensor + xt_tokens_vision: torch.Tensor # tokens added with noise level t per flow-matching formulation + vt_target_vision: torch.Tensor # gt rectified flow field + sigmas_vision: torch.Tensor | None = None # SNR to add to the vision tokens + + # Audio (Sound) + epsilon_sound: torch.Tensor | None = None + xt_tokens_sound: torch.Tensor | None = None + vt_target_sound: torch.Tensor | None = None + sigmas_sound: torch.Tensor | None = None + + # Action + epsilon_action: torch.Tensor | None = None + xt_tokens_action: torch.Tensor | None = None + vt_target_action: torch.Tensor | None = None + sigmas_action: torch.Tensor | None = None + raw_action_dim: list[torch.Tensor] | None = None # raw action dimension, used adding masks to loss calculation + + +def unwrap_and_densify(raw: list | torch.Tensor | None, to_kwargs: dict) -> list[torch.Tensor] | None: + """Unwrap nested single-element lists and filter ``None`` entries. + + The joint dataloader can produce data as nested single-element lists + (e.g. ``[[t1], [None], [t2]]``). This helper flattens the nesting, + drops ``None`` entries, and moves the remaining tensors to the target + device/dtype. + + Args: + raw: The raw value from ``data_batch``. May be ``None``, a bare + tensor, or a (possibly nested) list of tensors / ``None`` s. + Each tensor in the list has shape ``(...)``. + to_kwargs: Keyword arguments forwarded to ``torch.Tensor.to`` + (e.g. ``{"device": "cuda"}`` or ``{"device": "cuda", "dtype": torch.bfloat16}``). + + Returns: + A dense list of device tensors each with shape ``(...)``, or ``None`` + if the input is ``None`` or every entry is ``None``. + + Examples: + >>> unwrap_and_densify([[t1], [None], [t2]], {"device": "cuda"}) + [t1.cuda(), t2.cuda()] + >>> unwrap_and_densify(None, {"device": "cuda"}) + None + """ + if raw is None: + return None + if not isinstance(raw, list): + return [raw.to(**to_kwargs)] # list of 1 tensor: [(...)] + # Unwrap single-element inner lists: [[t], [None]] -> [t, None] + if len(raw) > 0 and isinstance(raw[0], list): + raw = [item[0] if isinstance(item, list) else item for item in raw] + # Filter None entries and move to device + dense = [x.to(**to_kwargs) for x in raw if x is not None] # list of B tensors: [(...), ...] + return dense if dense else None + + +def _expand_per_sample_to_per_vision_item( + tensor: torch.Tensor, # [B,...] + num_vision_items_per_sample: list[int] | None, +) -> torch.Tensor: # [N_vision_items,...] + """Expand a per-sample tensor to a per-vision-item tensor. + + For image editing, each sample may contribute multiple vision items + (e.g. source + target). This helper repeats each sample's value for + all of its vision items so that downstream indexing by vision-item + position works correctly. + + Args: + tensor: Per-sample tensor of shape ``(N, ...)``. + num_vision_items_per_sample: Number of vision items per sample, + e.g. ``[2, 2, ...]``. If ``None``, the tensor is returned as-is + (standard single-item-per-sample case). + + Returns: + Tensor of shape ``(sum(num_vision_items_per_sample), ...)``, or the + original tensor when ``num_vision_items_per_sample`` is ``None``. + """ + if num_vision_items_per_sample is None: + return tensor # [B,...] + expanded = [] + for sample_idx, num_items in enumerate(num_vision_items_per_sample): + for _ in range( + num_items + ): # torch.stack(tensor[idx].repeat(num_vision_items_per_sample[idx]) for idx in range(len(num_vision_items_per_sample))) + expanded.append(tensor[sample_idx]) # [...] + return torch.stack(expanded) # [N_vision_items,...] + + +def build_dense_sound_schedule( + sequence_plans: list, + x0_tokens_sound: list[torch.Tensor] | None, + timesteps: torch.Tensor, # [B,...] + sigmas: torch.Tensor, # [B,...] +) -> tuple[torch.Tensor | None, torch.Tensor | None]: + """Reindex per-sample schedules to match the dense sound tensor list. + + Sound tensors are dense over samples with ``has_sound=True``, while input + timesteps/sigmas are indexed by original batch position. This helper maps + dense sound entry ``i`` back to its source sample's schedule row. + """ + sound_sample_indices = [i for i, plan in enumerate(sequence_plans) if getattr(plan, "has_sound", False)] + num_sound_tensors = 0 if x0_tokens_sound is None else len(x0_tokens_sound) + assert len(sound_sample_indices) == num_sound_tensors, ( + "Sound tensor count must match sequence plans with has_sound=True. " + f"Got {num_sound_tensors} sound tensor(s) for {len(sound_sample_indices)} sound plan(s)." + ) + + if not sound_sample_indices: + return None, None + + idx_sound = torch.tensor(sound_sample_indices, dtype=torch.long, device=timesteps.device) # [n_sound] + return timesteps[idx_sound], sigmas[idx_sound] # [n_sound,...], [n_sound,...] diff --git a/cosmos_training/cosmos/model/vfm/utils/memory.py b/cosmos_training/cosmos/model/vfm/utils/memory.py new file mode 100644 index 00000000..9a6189bf --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/utils/memory.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Abstract interfaces for persistent memory in the MoT transformer stack. + +``MemoryState`` is a *mutable* Python object that lives **outside** the +``torch.compile`` boundary. It is responsible for reading cached tensors +(``read_for_layer``) and writing new tensors back (``write_for_layer``). + +``MemoryValue`` is a *read-only* tensor container that is safe to pass +**into** a compiled decoder layer. Concrete implementations are plain +dataclasses whose fields are tensors (or None). No methods on +``MemoryValue`` should mutate state. + +``KVToStore`` is a type alias for the 4-tuple of tensors +``(gen_k, gen_v, und_k, und_v)`` returned by each compiled layer so +the caller can write them back into the cache outside the compile boundary. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import torch + +# (gen_k, gen_v, und_k, und_v) returned by each compiled layer for the caller +# to write back into the cache outside the torch.compile boundary. +KVToStore = tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] + + +@dataclass +class MemoryValue(ABC): + """Read-only tensor container safe to pass into ``torch.compile``. + + Concrete subclasses (e.g. ``ARMemoryValue``, ``KVTrainMemoryValue``) + are plain dataclasses of tensors. No methods on this class should + mutate state or perform non-trivial computation. + """ + + @property + def supports_context_parallel_attention(self) -> bool: + """Whether this memory value is compatible with context-parallel attention. + + Overridden by ``KVTrainMemoryValue`` to return ``False``. Used by + ``ContextParallelDispatch`` to reject an unsupported combination + without importing the concrete subclass. + """ + return True + + +class MemoryState(ABC): + """Mutable persistent memory that lives outside ``torch.compile``. + + The outer loop in ``_impl_forward`` calls ``read_for_layer`` before + each decoder layer and ``write_for_layer`` after. The ``MemoryState`` + object itself is **never** passed into a compiled region. + """ + + @abstractmethod + def init(self, hidden_states: dict, device: torch.device) -> None: + """Initialization per training step. + + Called once before any transformer layers are processed. + + Args: + hidden_states: The packed sequence (``FactoredSequencePack``). + device: Target device for any new tensors. + """ + + @abstractmethod + def read_for_layer(self, layer_idx: int) -> MemoryValue: + """Produce a read-only tensor snapshot for *layer_idx*. + + Used to retrieve KV values from the cache. + The returned ``MemoryValue`` is passed into the compiled decoder + layer as ``memory_value``. + """ + + @abstractmethod + def write_for_layer(self, layer_idx: int, kv_to_store: KVToStore) -> None: + """Store the K/V tensors produced by *layer_idx* back into the cache. + + Called outside the ``torch.compile`` boundary. + """ + + @abstractmethod + def is_gen_only(self) -> bool: + """Return ``True`` when only the generation pathway should run. + + When ``True``, the decoder layer assumes that the text caption has + already been processed and cached in the MemoryState object. + Used for autoregressive frame-by-frame generation of video. + """ + + @property + def uses_rolling_kv_cache(self) -> bool: + """Whether this memory uses the rolling KV-cache / compile-safe path. + + When ``True``, the network skips NATTEN metadata computation because + temporal causality is handled inside three-way attention instead. + """ + return False diff --git a/cosmos_training/cosmos/model/vfm/utils/safetensors_loader.py b/cosmos_training/cosmos/model/vfm/utils/safetensors_loader.py new file mode 100644 index 00000000..570ba8d3 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/utils/safetensors_loader.py @@ -0,0 +1,1160 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Distributed safetensors loading and HF→Cosmos3 weight conversion. + +Layered API +----------- + +Three layers of functionality, lowest first: + +1. **Multi-rank checkpoint I/O** — :class:`MultiRankCheckpointLoader` distributes + safetensors file reads across the FSDP ``dp_shard`` ranks and then + broadcasts each tensor to every rank. It is checkpoint-format-agnostic: + it just yields ``(name, tensor)`` pairs from the raw HF state dict. + +2. **Name / weight conversion** — Per-family converters translate raw HF + parameter names (and optionally shard the tensor along FSDP / EP axes) + into the Cosmos3 VFM layout: + + - :func:`convert_weight_from_qwen3_hf` — Qwen3 VL / LLM (dense + MoE). + - :func:`convert_weight_from_nemotron_vl_hf` — Nemotron-3 Dense VL (hybrid 56-block layout). + - :func:`convert_weight_from_nemotron_llm_hf` — Nemotron-3 pure LLM. + + For the generic VLM path, :func:`_make_name_converter` consumes the model's + ``_checkpoint_conversion_mapping`` (transformers v4) or falls back to + suffix-lookup against the model's own state dict (transformers v5). + +3. **High-level loaders** — Composing the above: + + - :func:`load_language_model` — loads HF text-tower weights into the MoT + language model. Auto-detects the checkpoint format + (:func:`detect_vlm_checkpoint_format`). + - :func:`load_vlm_model` — generic loader for HF VLM checkpoints into an + FSDP-wrapped ``HFModel``; honors a skip-pattern overlay and the + ``fsdp_offload`` mode. + +Borrowed from cosmos_rl's ``MultiRankWeightLoader`` (renamed to +``MultiRankCheckpointLoader`` here) with modifications for loading from +S3 / GCS and support for Cosmos3 VFM models. +https://github.com/nvidia-cosmos/cosmos-rl/blob/main/cosmos_rl/utils/multi_rank_weight_loader.py +""" + +import os +import re +import time +from collections.abc import Callable, Iterator + +import torch +import torch.distributed as dist +from safetensors.torch import load as load_safetensors +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor import DTensor + +from cosmos.utils.flags import INTERNAL +from cosmos.utils import log +from cosmos.utils.easy_io import easy_io +from cosmos.utils.vfm.parallelism import ParallelDims + +# Prefixes stripped when matching checkpoint keys to model state-dict keys. +# Order matters: longest first. For each model key, the longest matching +# prefix is stripped (yielding the most specific tail) before we record it +# in the lookup table. The trailing empty string acts as a default that +# leaves keys without any known prefix unchanged. +# Ref: cosmos-rl cosmos_rl/policy/model/hf_models/__init__.py:465-472. +_VLM_KEY_PREFIXES: tuple[str, ...] = ( + "model.language_model.model.", + "model.language_model.", + "language_model.model.", + "language_model.", + "model.", + "", +) + +_HF_URI_PREFIX = "hf://" + + +def _looks_like_hf_repo_id(checkpoint_path: str) -> bool: + """Return True for unambiguous bare Hugging Face repo IDs. + + Explicit ``hf://`` paths are handled separately. For bare paths, require the + common ``namespace/repo`` shape so local relative paths such as ``ckpt`` are + not silently treated as Hub repos. + """ + if os.path.exists(os.path.expanduser(checkpoint_path)): + return False + if checkpoint_path.startswith(("/", "./", "../", "~")): + return False + if "://" in checkpoint_path: + return False + return re.fullmatch(r"[\w.-]+/[\w.-]+", checkpoint_path) is not None + + +def _download_hf_checkpoint(checkpoint_path: str) -> str: + """Download safetensors from Hugging Face Hub and return the local snapshot path.""" + from huggingface_hub import snapshot_download + + repo_id = checkpoint_path.removeprefix(_HF_URI_PREFIX) + hf_home = os.environ.get("HF_HOME") + cache_dir = os.path.join(hf_home, "hub") if hf_home else None + token = os.environ.get("HF_TOKEN") + log.info(f"Resolving Hugging Face checkpoint: {repo_id}", rank0_only=False) + local_path = snapshot_download( + repo_id=repo_id, + token=token, + cache_dir=cache_dir, + allow_patterns=["*.safetensors", "*.safetensors.index.json"], + ) + log.info(f"Resolved Hugging Face checkpoint {repo_id} to {local_path}", rank0_only=False) + return local_path + + +def _is_hf_checkpoint_candidate(checkpoint_path: str) -> bool: + return checkpoint_path.startswith(_HF_URI_PREFIX) or _looks_like_hf_repo_id(checkpoint_path) + + +def _make_backend_args(checkpoint_path: str, credential_path: str | None) -> dict[str, str | None] | None: + if checkpoint_path.startswith("s3://"): + return { + "backend": "s3", + "s3_credential_path": credential_path, + } + return None + + +def _list_safetensors_files( + checkpoint_path: str, + backend_args: dict[str, str | None] | None, +) -> list[str]: + return list( + easy_io.list_dir_or_file( + checkpoint_path, + list_dir=False, + list_file=True, + suffix="safetensors", + recursive=False, + backend_args=backend_args, + ) + ) + + +def _get_local_rank_and_size(device_mesh: DeviceMesh) -> tuple[int, int]: + """Get the local rank and size of a device mesh. + + Args: + device_mesh: The device mesh to get the attributes from. + + Returns: + A tuple of (local rank, size). + """ + return device_mesh.get_local_rank(), device_mesh.size() + + +def _shard_tensor_on_fsdp_mesh( + tensor: torch.Tensor, + parallel_dims: ParallelDims | None, +) -> torch.Tensor: + """Slice ``tensor`` along dim 0 according to the FSDP ``dp_shard`` mesh. + + Returns the rank-local shard when ``dp_shard`` is enabled, otherwise the + full tensor (made contiguous). Requires that ``tensor.shape[0]`` is + divisible by ``dp_shard_size`` — this is a hard requirement of the even- + split semantics; uneven splits should go through :func:`_shard_first_dim`. + + Args: + tensor: The tensor to shard. + parallel_dims: Parallel dims object, or None for single-rank. + + Returns: + Contiguous rank-local shard (or full tensor if dp_shard is disabled). + """ + if parallel_dims is None or not parallel_dims.dp_shard_enabled: + return tensor.contiguous() + + fsdp_rank, fsdp_size = _get_local_rank_and_size(parallel_dims.dp_shard_mesh) + if tensor.shape[0] % fsdp_size != 0: + raise ValueError(f"Shard shape {tensor.shape} is not divisible by dp_shard_size {fsdp_size} on dim 0") + shard = tensor.chunk(chunks=fsdp_size, dim=0)[fsdp_rank] + return shard.contiguous() + + +def _get_dp_shard_mesh(parallel_dims: ParallelDims | None) -> DeviceMesh | None: + """Get the dp_shard mesh from the parallel dimensions. + + Args: + parallel_dims: The parallel dimensions to use for the conversion. + + Returns: + The dp_shard mesh, or None if dp_shard is not enabled. + """ + if parallel_dims is not None and parallel_dims.dp_shard_enabled: + return parallel_dims.dp_shard_mesh + else: + return None + + +def _build_model_key_by_tail(state_dict: dict) -> dict[str, str]: + """Build a ``tail → model_key`` lookup table for suffix-based key matching. + + For each model key, strip the longest matching prefix in + ``_VLM_KEY_PREFIXES`` and record ``tail -> model_key``. The longest + prefix yields the shortest, most specific tail. The trailing empty + prefix in ``_VLM_KEY_PREFIXES`` ensures keys with no known prefix map + to themselves as their own tail. + """ + table: dict[str, str] = {} + for model_key in state_dict: + for pfx in _VLM_KEY_PREFIXES: + if model_key.startswith(pfx): + tail = model_key[len(pfx) :] + if tail and tail not in table: + table[tail] = model_key + break + return table + + +def _is_moe_vlm(model: torch.nn.Module) -> bool: + """Detect whether an HF VLM is a Mixture-of-Experts model. + + MoE VLMs (Qwen3-VL-30B-A3B, Qwen3-VL-235B-A22B) need replicated-gate + + FSDP-fused-expert shard rules that load_vlm_model does NOT yet implement. + Callers use this to raise NotImplementedError before sharding. + + Detection sources (any one is sufficient): + - ``model.config.text_config.num_experts`` (if present and non-None) + - ``model.config.text_config.num_local_experts`` (if present and non-None) + - Same attributes on ``model.config`` directly (text-only fallback) + - Any state-dict key containing ``.mlp.experts.`` + """ + text_cfg = getattr(model.config, "text_config", None) or model.config + for attr in ("num_experts", "num_local_experts"): + value = getattr(text_cfg, attr, None) + if value is not None and value != 0: + return True + for name in model.state_dict().keys(): + if ".mlp.experts." in name: + return True + return False + + +def _make_name_converter( + state_dict: dict, + hf_conv_map: dict[str, str] | None, +) -> Callable[[str], str]: + """Return a callable that maps checkpoint keys to model keys. + + Two strategies, matching cosmos-rl's flow: + 1. If ``hf_conv_map`` is non-empty (transformers v4 pre-computed pattern + mapping), apply each pattern/replacement as a regex substitution and + return on the first match (no further fallback). + 2. Otherwise (transformers v5 or no map), use a direct-match against the + model's state dict, then a longest-prefix-stripped suffix lookup + through ``_VLM_KEY_PREFIXES``. Names that match nothing are returned + unchanged (the caller is responsible for filtering / raising). + """ + model_key_by_tail = _build_model_key_by_tail(state_dict) + + def convert(name: str) -> str: + if hf_conv_map: + for pattern, replacement in hf_conv_map.items(): + if re.search(pattern, name): + return re.sub(pattern, replacement, name) + return name + if name in state_dict: + return name + for pfx in _VLM_KEY_PREFIXES: + if name.startswith(pfx): + tail = name[len(pfx) :] + if tail and tail in model_key_by_tail: + return model_key_by_tail[tail] + return name + + return convert + + +class MultiRankCheckpointLoader: + """Multi-rank loader for model weights stored as safetensors files. + + Files in the checkpoint directory are statically partitioned across the + ranks of the ``dp_shard`` sub-mesh by ``file_idx % world_size``. Each + rank reads its assigned files locally and the per-tensor data is later + broadcast (via :meth:`broadcast_tensor`) so every rank ends up with the + full tensor before sharding. + + When constructed with ``dp_shard_mesh=None`` the loader degrades to a + single-rank fallback: ``world_size = 1``, every rank reads every file, + and broadcasts are no-ops. + + Renamed from cosmos-rl's ``MultiRankWeightLoader`` and extended to load + from S3 / GCS via easy_io and to support Cosmos3 VFM models. + https://github.com/nvidia-cosmos/cosmos-rl/blob/main/cosmos_rl/utils/multi_rank_weight_loader.py + """ + + # Mapping from dtype to integer for broadcasting + DTYPE_TO_INT = { + torch.float32: 0, + torch.float16: 1, + torch.bfloat16: 2, + torch.int64: 3, + torch.int32: 4, + torch.int8: 5, + torch.uint8: 6, + torch.float8_e4m3fn: 7, + torch.float8_e5m2: 8, + } + # Mapping from integer to dtype for broadcasting + INT_TO_DTYPE = {v: k for k, v in DTYPE_TO_INT.items()} + + def __init__(self, dp_shard_mesh: DeviceMesh | None): + """Initialize the multi-rank weight loader. + + Args: + dp_shard_mesh: 1-D ``dp_shard`` mesh, or None if dp_shard is not + enabled. Callers should obtain this via + :func:`_get_dp_shard_mesh` so the ``parallel_dims is None`` and + ``dp_shard <= 1`` cases collapse to the single-rank fallback. + """ + if dp_shard_mesh is None: + self.group = None + self.rank = 0 + self.world_size = 1 + else: + self.group = dp_shard_mesh.get_group() + self.rank = dp_shard_mesh.get_local_rank() + self.world_size = dp_shard_mesh.size() + + def load_files_parallel( + self, + checkpoint_path: str, + credential_path: str | None, + loading_device: torch.device, + ) -> tuple[ + dict[str, torch.Tensor], + dict[str, tuple[list, int]], + set[str], + ]: + """ + Load safetensors files in parallel across ranks. + + Args: + checkpoint_path: Path to the model directory. Local paths and S3 + URIs are tried first; if no safetensors are found, explicit + ``hf://org/model`` Hub URIs and bare ``org/model`` repo IDs + fall back to Hugging Face. + credential_path: Path to the credential file for S3/GCS. + loading_device: Device to load tensors on. + + Returns: + Tuple of (rank_tensors, rank_tensor_metadata, weights_of_ckpt_names): + - rank_tensors: Dict mapping tensor names to tensors loaded by this rank. + - rank_tensor_metadata: Dict mapping tensor names to (shape, dtype_int) tuples. + - weights_of_ckpt_names: Set of all tensor names found by this rank. + """ + rank_tensors = {} # {tensor_name: tensor_data} for this rank + rank_tensor_metadata = {} # {tensor_name: (shape, dtype)} for this rank + weights_of_ckpt_names = set() + + backend_args = _make_backend_args(checkpoint_path, credential_path) + + log.info(f"Loading safetensors files from: {checkpoint_path}", rank0_only=False) + log.info(f"Credential path: {credential_path}", rank0_only=False) + list_error: Exception | None = None + if checkpoint_path.startswith(_HF_URI_PREFIX): + safetensors_files: list[str] = [] + else: + try: + safetensors_files = _list_safetensors_files(checkpoint_path, backend_args) + except Exception as exc: + if not _is_hf_checkpoint_candidate(checkpoint_path): + raise + list_error = exc + safetensors_files = [] + + if not safetensors_files: + if _is_hf_checkpoint_candidate(checkpoint_path): + original_checkpoint_path = checkpoint_path + # Multi-node: serialize through global rank 0 so we don't race on the + # shared HF cache. snapshot_download's per-blob locks are unreliable on + # NFS/lustre under concurrent access; without this gate the snapshot + # dir can end up with only config.json and the listing below fails. + if dist.is_initialized() and dist.get_world_size() > 1: + if dist.get_rank() == 0: + checkpoint_path = _download_hf_checkpoint(checkpoint_path) + dist.barrier() + if dist.get_rank() != 0: + checkpoint_path = _download_hf_checkpoint(checkpoint_path) + dist.barrier() + else: + checkpoint_path = _download_hf_checkpoint(checkpoint_path) + backend_args = None + log.info( + "No local/S3 safetensors found; falling back to Hugging Face checkpoint " + f"{original_checkpoint_path} -> {checkpoint_path}", + rank0_only=False, + ) + safetensors_files = _list_safetensors_files(checkpoint_path, backend_args) + elif list_error is not None: + raise list_error + + if not safetensors_files: + raise FileNotFoundError(f"No .safetensors files found in checkpoint path: {checkpoint_path}") + + for file_idx, file_path in enumerate(safetensors_files): + file_rank = file_idx % self.world_size + if self.rank == file_rank: + log.info(f"Loading safetensors file: {file_path}", rank0_only=False) + full_path = easy_io.join_path(checkpoint_path, file_path, backend_args=backend_args) + # Download the file + weights_data = easy_io.get(full_path, backend_args=backend_args) + state_dict = load_safetensors(weights_data) + for name, tensor in state_dict.items(): + # Names are stored RAW here; per-checkpoint name + # conversion (see _make_name_converter / the + # convert_weight_from_*_hf functions) is applied later + # by the caller after broadcast. + weights_of_ckpt_names.add(name) + rank_tensors[name] = tensor.to(device=loading_device) + rank_tensor_metadata[name] = ( + list(tensor.shape), + self.DTYPE_TO_INT.get(tensor.dtype, 0), + ) + + return rank_tensors, rank_tensor_metadata, weights_of_ckpt_names + + def gather_tensor_names_and_build_mapping( + self, weights_of_ckpt_names: set[str], rank_tensors: dict[str, torch.Tensor] + ) -> tuple[set[str], dict[str, int]]: + """ + Gather all tensor names from all ranks and build a tensor-to-rank mapping. + + Args: + weights_of_ckpt_names: Set of tensor names found by this rank. + rank_tensors: Dict of tensors loaded by this rank. + + Returns: + Tuple of (all_tensor_names, tensor_to_rank_map): + - all_tensor_names: Set of all tensor names across all ranks. + - tensor_to_rank_map: Dict mapping tensor names to the rank that loaded them. + """ + if self.world_size > 1: + # all_gather_object requires output list to be pre-initialized with world_size + all_tensor_names_lists: list[list[str] | None] = [None] * self.world_size + dist.all_gather_object(all_tensor_names_lists, list(weights_of_ckpt_names), group=self.group) + # Flatten the list and create a set + all_tensor_names = set() + for names_list in all_tensor_names_lists: + if names_list is not None: + all_tensor_names.update(names_list) + + # Build tensor-to-rank mapping: gather which rank has which tensors + # Create a dict mapping tensor_name -> rank for this rank + local_tensor_to_rank = {name: self.rank for name in rank_tensors.keys()} + all_tensor_to_rank_dicts: list[dict[str, int] | None] = [None] * self.world_size + dist.all_gather_object(all_tensor_to_rank_dicts, local_tensor_to_rank, group=self.group) + + # Merge all dicts into a global ``tensor_name -> rank`` map. + # Duplicates aren't expected (each tensor lives in exactly one + # file, which is owned by exactly one rank), but if they do + # occur the lowest rank wins. + tensor_to_rank_map = {} + for rank_idx, tensor_dict in enumerate(all_tensor_to_rank_dicts): + if tensor_dict is not None: + for tensor_name in tensor_dict: + if tensor_name not in tensor_to_rank_map: + tensor_to_rank_map[tensor_name] = rank_idx + else: + all_tensor_names = weights_of_ckpt_names + tensor_to_rank_map = {name: 0 for name in rank_tensors.keys()} + + return all_tensor_names, tensor_to_rank_map + + def broadcast_tensor( + self, + name: str, + tensor_rank: int, + rank_tensors: dict[str, torch.Tensor], + rank_tensor_metadata: dict[str, tuple[list, int]], + device: torch.device, + ) -> torch.Tensor: + """ + Broadcast a tensor from the rank that has it to all ranks. + + Args: + name: Name of the tensor to broadcast. + tensor_rank: Rank that has the tensor. + rank_tensors: Dict of tensors loaded by this rank. + rank_tensor_metadata: Dict of tensor metadata (shape, dtype) for this rank. + device: Device to create tensors on. + + Returns: + The broadcasted tensor (same on all ranks). + """ + # Get tensor from the rank that has it + if self.rank == tensor_rank: + ckpt_tensor = rank_tensors[name] + tensor_shape, tensor_dtype_int = rank_tensor_metadata[name] + # Move tensor from CPU to GPU if needed (tensors are loaded to CPU to avoid OOM) + ckpt_tensor = ckpt_tensor.to(device=device) + else: + ckpt_tensor = None + tensor_shape = [] + tensor_dtype_int = 0 + + # Broadcast tensor metadata (shape, dtype) from the rank that has it + if self.world_size > 1: + # Ensure all ranks participate in broadcast + if self.rank == tensor_rank: + shape_len = len(tensor_shape) + shape_len_tensor = torch.tensor([shape_len], dtype=torch.long, device=device) + shape_tensor = torch.tensor(tensor_shape, dtype=torch.long, device=device) + dtype_int_tensor = torch.tensor([tensor_dtype_int], dtype=torch.long, device=device) + else: + shape_len_tensor = torch.zeros(1, dtype=torch.long, device=device) + shape_tensor = None # Will be created after knowing shape_len + dtype_int_tensor = torch.zeros(1, dtype=torch.long, device=device) + + # Broadcast shape length first + dist.broadcast(shape_len_tensor, group=self.group, group_src=tensor_rank) + shape_len = shape_len_tensor.item() + + # Create shape_tensor with correct size for all ranks + if self.rank != tensor_rank: + shape_tensor = torch.zeros(shape_len, dtype=torch.long, device=device) + + # Broadcast shape values + dist.broadcast(shape_tensor, group=self.group, group_src=tensor_rank) + + # Broadcast dtype + dist.broadcast(dtype_int_tensor, group=self.group, group_src=tensor_rank) + + if self.rank != tensor_rank: + tensor_shape = shape_tensor.cpu().tolist() + tensor_dtype = self.INT_TO_DTYPE.get(dtype_int_tensor.item(), torch.float32) + ckpt_tensor = torch.empty(tensor_shape, dtype=tensor_dtype, device=device) + + # Broadcast the actual tensor data + dist.broadcast(ckpt_tensor, group=self.group, group_src=tensor_rank) + + # Ensure ckpt_tensor is not None + if ckpt_tensor is None: + raise ValueError( + f"Failed to get tensor {name} on rank {self.rank}. " + f"tensor_rank={tensor_rank}, world_size={self.world_size}, " + f"group={self.group}" + ) + + return ckpt_tensor + + def iterate_tensors( + self, + all_tensor_names: set[str], + tensor_to_rank_map: dict[str, int], + rank_tensors: dict[str, torch.Tensor], + rank_tensor_metadata: dict[str, tuple[list, int]], + device: torch.device, + ) -> Iterator[tuple[str, torch.Tensor]]: + """ + Iterate over all tensors, broadcasting them as needed. + + Args: + all_tensor_names: Set of all tensor names across all ranks. + tensor_to_rank_map: Dict mapping tensor names to the rank that loaded them. + rank_tensors: Dict of tensors loaded by this rank. + rank_tensor_metadata: Dict of tensor metadata (shape, dtype) for this rank. + device: Device to create tensors on. + + Yields: + Tuple of (tensor_name, tensor) for each tensor. + """ + for name in sorted(all_tensor_names): + tensor_rank = tensor_to_rank_map.get(name) + if tensor_rank is None: + continue + + tensor = self.broadcast_tensor(name, tensor_rank, rank_tensors, rank_tensor_metadata, device) + yield name, tensor + + +def convert_weight_from_qwen3_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """Map Qwen3 VL / LLM HF weights to the Cosmos3 VFM layout and shard them. + + Steps: + + 1. Strip the ``model.language_model.`` prefix (so keys from the VL + checkpoint variant collapse onto the LLM key namespace). + 2. Classify the resulting ``dest_name`` against two allowlists: + + - **used_patterns** — embeddings, norms, attention/MLP projections, + fused ``mlp.experts.{gate_up_proj,down_proj}``, and + ``mlp.gate.weight``. Matching keys are kept. + - **discarded_patterns** — currently just ``model.visual.*`` (the + vision tower is loaded separately). Matching keys are dropped + and the function returns ``(None, None)``. + + A key that matches neither raises :class:`ValueError`. + 3. Shard kept tensors along dim 0 on the FSDP ``dp_shard`` mesh via + :func:`_shard_tensor_on_fsdp_mesh`. Expert parallelism is **not** + handled here — fused expert tensors flow through the same FSDP + sharding as the rest, which is correct only for ``ep == 1``; the + moe-mesh sharding path will need to be added back when EP support + lands. + + Args: + tensor: Raw HF tensor. + name: HF parameter name (with ``model.`` / ``model.language_model.`` + prefix as it appears in the safetensors checkpoint). + parallel_dims: Parallel dims; ``None`` skips sharding. + + Returns: + Tuple ``(dest_name, sharded_tensor)`` in the Cosmos3 layout, or + ``(None, None)`` if the tensor is intentionally discarded. + """ + dest_name = name.replace("model.language_model.", "model.") + + used_patterns = [ + r"^lm_head\.weight$", + r"^model\.embed_tokens\.weight$", + r"^model\.norm\.weight$", + r"^model\.layers\.(\d+)\.(input_layernorm|post_attention_layernorm)\.weight$", + r"^model\.layers\.(\d+)\.self_attn\.(q_norm|k_norm|v_norm)\.weight$", + r"^model\.layers\.(\d+)\.self_attn\.(q_proj|k_proj|v_proj|o_proj)\.weight$", + r"^model\.layers\.(\d+)\.mlp\.(gate_proj|up_proj|down_proj)\.weight$", + r"^model\.layers\.(\d+)\.mlp\.experts\.(gate_up_proj|down_proj)$", + r"^model\.layers\.(\d+)\.mlp\.gate\.weight$", + ] + + discarded_patterns = [ + r"^model\.visual\.", + ] + + def _is_used_pattern(dest_name: str) -> bool: + for used_pattern in used_patterns: + if re.search(used_pattern, dest_name) is not None: + return True + + for discarded_pattern in discarded_patterns: + if re.search(discarded_pattern, dest_name) is not None: + return False + + raise ValueError(f"Unexpected weight found in checkpoint: {dest_name}") + + if _is_used_pattern(dest_name): + return dest_name, _shard_tensor_on_fsdp_mesh(tensor, parallel_dims) + + return None, None + + +def convert_weight_from_nemotron_vl_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """Map Nemotron VLM HF keys (56 hybrid blocks) to Cosmos3 VFM MoT keys (28 paired layers). + + The Nemotron 3 Dense VL checkpoint (NVIDIA-Nemotron-3-Dense-VL-2B-BF16-Alignment) + uses a hybrid layout with 56 alternating attention and MLP blocks, where: + + - Even-indexed blocks (0, 2, 4, ...) contain attention (``mixer.q/k/v/o_proj``) + - Odd-indexed blocks (1, 3, 5, ...) contain MLP (``mixer.up/down_proj``) + - Each block has a ``norm.weight`` (pre-attention or post-attention layer norm) + + The MoT model uses a standard layout with 28 paired layers, each containing both + attention and MLP sub-modules. + + Weight mapping (HF → MoT):: + + model.visual.*, model.projector.*, model.multi_modal_projector.* + → skipped (vision weights, loaded separately) + + model.lm_head.weight / lm_head.weight → lm_head.weight + model.language_model.embeddings.weight → model.embed_tokens.weight + model.language_model.norm_f.weight → model.norm.weight + + model.language_model.layers.{2i}.norm.weight + → model.layers.{i}.input_layernorm.weight + model.language_model.layers.{2i+1}.norm.weight + → model.layers.{i}.post_attention_layernorm.weight + + model.language_model.layers.{2i}.mixer.{q,k,v,o}_proj.weight + → model.layers.{i}.self_attn.{q,k,v,o}_proj.weight + + model.language_model.layers.{2i+1}.mixer.{up,down}_proj.weight + → model.layers.{i}.mlp.{up,down}_proj.weight + """ + if name.startswith("model.visual.") or name.startswith("model.projector."): + return None, None + if name.startswith("model.multi_modal_projector."): + return None, None + + dest_name: str | None = None + if name == "lm_head.weight" or name == "model.lm_head.weight": + dest_name = "lm_head.weight" + elif name == "model.language_model.embeddings.weight": + dest_name = "model.embed_tokens.weight" + elif name == "model.language_model.norm_f.weight": + dest_name = "model.norm.weight" + else: + # Layer norm: even idx → pre-attention (input_layernorm), odd idx → post-attention + m = re.match(r"model\.language_model\.layers\.(\d+)\.norm\.weight", name) + if m is not None: + idx = int(m.group(1)) + paired = idx // 2 + if idx % 2 == 0: + dest_name = f"model.layers.{paired}.input_layernorm.weight" + else: + dest_name = f"model.layers.{paired}.post_attention_layernorm.weight" + else: + # Attention projections: must be at even indices + m = re.match( + r"model\.language_model\.layers\.(\d+)\.mixer\.(q_proj|k_proj|v_proj|o_proj)\.weight", + name, + ) + if m is not None: + idx = int(m.group(1)) + if idx % 2 != 0: + raise ValueError(f"Expected attention block at even layer index, got {name}") + paired = idx // 2 + dest_name = f"model.layers.{paired}.self_attn.{m.group(2)}.weight" + else: + # MLP projections: must be at odd indices + m = re.match( + r"model\.language_model\.layers\.(\d+)\.mixer\.(up_proj|down_proj)\.weight", + name, + ) + if m is not None: + idx = int(m.group(1)) + if idx % 2 != 1: + raise ValueError(f"Expected MLP block at odd layer index, got {name}") + paired = idx // 2 + dest_name = f"model.layers.{paired}.mlp.{m.group(2)}.weight" + + if dest_name is None: + raise ValueError(f"Unexpected Nemotron checkpoint tensor: {name}") + + return dest_name, _shard_tensor_on_fsdp_mesh(tensor, parallel_dims) + + +def convert_weight_from_nemotron_llm_hf( + tensor: torch.Tensor, + name: str, + parallel_dims: ParallelDims | None, +) -> tuple[str | None, torch.Tensor | None]: + """Map Nemotron pure-LLM HF keys (CosmosNemotronForCausalLM) to MoT language model keys. + + The Nemotron 3 LLM checkpoint (NVIDIA-Nemotron-3-2B-BF16) uses a standard + decoder-only layout with 28 layers, each containing attention and MLP. The key + names are already close to the MoT model's expected layout, so most keys pass + through with minimal renaming. + + Weight mapping (HF → MoT):: + + model.embeddings.weight → model.embed_tokens.weight + lm_head.weight → lm_head.weight + model.norm.weight → model.norm.weight + + model.layers.{i}.input_layernorm.weight → (unchanged) + model.layers.{i}.post_attention_layernorm.weight → (unchanged) + model.layers.{i}.self_attn.{q,k,v,o}_proj.weight → (unchanged) + model.layers.{i}.mlp.{up,down}_proj.weight → (unchanged) + """ + if name == "model.embeddings.weight": + dest_name = "model.embed_tokens.weight" + elif name in ("lm_head.weight", "model.lm_head.weight"): + dest_name = "lm_head.weight" + elif name == "model.norm.weight": + dest_name = "model.norm.weight" + elif re.match(r"model\.layers\.\d+\.(input_layernorm|post_attention_layernorm)\.weight", name): + dest_name = name + elif re.match(r"model\.layers\.\d+\.self_attn\.(q_proj|k_proj|v_proj|o_proj)\.weight", name): + dest_name = name + elif re.match(r"model\.layers\.\d+\.mlp\.(up_proj|down_proj)\.weight", name): + dest_name = name + else: + raise ValueError(f"Unexpected Nemotron LLM checkpoint tensor: {name}") + + return dest_name, _shard_tensor_on_fsdp_mesh(tensor, parallel_dims) + + +def _shard_first_dim(tensor: torch.Tensor, world_size: int, rank: int) -> torch.Tensor: + """Slice a tensor along dim 0 for FSDP sharding. + + Matches cosmos-rl weight_converter.py:71-79 semantics: even splits use + tensor_split; uneven splits use ceil-divide with the last rank getting + the remainder (may be smaller than average). This layout must match + FSDP2's local_view shape per rank — caller asserts shape equality. + """ + tensor = tensor.contiguous() + row_size = tensor.shape[0] + if world_size == 1: + return tensor + if row_size % world_size == 0: + return tensor.tensor_split(world_size, dim=0)[rank].contiguous() + avg = (row_size + world_size - 1) // world_size + start = rank * avg + end = min(start + avg, row_size) + return tensor[start:end].contiguous() + + +def detect_vlm_checkpoint_format(all_tensor_names: set[str]) -> str: + """Detect the checkpoint family from its tensor key set. + + Detection rules (first match wins): + + - ``"nemotron_3_dense_vl"`` — any key shaped like + ``model.language_model.layers.*.mixer.q_proj.*``. This is the hybrid + 56-block layout where attention and MLP live in alternating blocks + under ``mixer.``. + - ``"nemotron_3_llm"`` — checkpoints that expose + ``model.embeddings.weight`` (Nemotron's pure LLM key for the input + embedding; Qwen3 uses ``model.embed_tokens.weight``). + - ``"qwen3"`` — default; covers Qwen3 VL and Qwen3 LLM (dense and MoE). + + The resulting tag is consumed by :func:`load_language_model` to dispatch + to the matching ``convert_weight_from_*_hf`` converter. + """ + for n in all_tensor_names: + if "model.language_model.layers." in n and ".mixer.q_proj" in n: + return "nemotron_3_dense_vl" + if "model.embeddings.weight" in all_tensor_names: + return "nemotron_3_llm" + return "qwen3" + + +def load_language_model( + model: torch.nn.Module, + checkpoint_path: str, + credential_path: str | None, + parallel_dims: ParallelDims | None, + checkpoint_format: str | None = None, +) -> set[str]: + """ + Universal language model loading function using SafeTensors (.safetensors) format. + Handles key remapping for "model.language_model." -> "model." by default. + + Args: + model: The language model to load weights into. + checkpoint_path: Path to checkpoint containing .safetensors files. + credential_path: Path to S3 credentials + parallel_dims: ParallelDims object to use for parallel loading. + If None, the loading is done in a single rank. + checkpoint_format: ``"qwen3"``, ``"nemotron_3_dense_vl"``, ``"nemotron_3_llm"``, or None to auto-detect. + + Returns: + Set of model state-dict keys successfully loaded from the checkpoint. + """ + if not INTERNAL: + from cosmos.utils.checkpoint_db import download_checkpoint, sanitize_uri + + checkpoint_path = download_checkpoint(sanitize_uri(checkpoint_path)) + + start_time = time.time() + log.info(f"load_language_model: loading weights from {checkpoint_path}") + + lm_state_dict = {} + for name, tensor in model.state_dict().items(): + # Remove the original module (torch compiled module) and checkpoint wrapped module prefixes. + final_name = name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", "") + lm_state_dict[final_name] = tensor + + # Initialize multi-rank weight loader + loader = MultiRankCheckpointLoader(_get_dp_shard_mesh(parallel_dims)) + + # Step 1: Load files in parallel + rank_tensors, rank_tensor_metadata, weights_of_ckpt_names = loader.load_files_parallel( + checkpoint_path=checkpoint_path, + credential_path=credential_path, + loading_device="cpu", + ) + + # Step 2: Gather tensor names and build mapping + all_tensor_names, tensor_to_rank_map = loader.gather_tensor_names_and_build_mapping( + weights_of_ckpt_names, rank_tensors + ) + + resolved_format = checkpoint_format or detect_vlm_checkpoint_format(all_tensor_names) + log.info(f"Language model checkpoint format: {resolved_format}", rank0_only=False) + + # Step 3: Process each tensor + keys_loaded = set() + for name, tensor in loader.iterate_tensors( + all_tensor_names, + tensor_to_rank_map, + rank_tensors, + rank_tensor_metadata, + device="cuda", + ): + if resolved_format == "nemotron_3_dense_vl": + dest_name, dest_weight = convert_weight_from_nemotron_vl_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + elif resolved_format == "nemotron_3_llm": + dest_name, dest_weight = convert_weight_from_nemotron_llm_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + elif resolved_format == "qwen3": + dest_name, dest_weight = convert_weight_from_qwen3_hf( + tensor=tensor, + name=name, + parallel_dims=parallel_dims, + ) + else: + raise ValueError(f"Unexpected checkpoint format: {resolved_format}") + + if dest_name is None: + # This is due to the visual weights of VLM models. + continue + + # If the weight is not found in the language model's state dict, then the weight is + # unexpected. The unexpected weights should be from the visual part of the VLM (already + # handled by the previous check). All weights in the language part should be used by + # the Cosmos3 VFM. + if dest_name not in lm_state_dict: + raise ValueError( + f"Unexpected weight found in checkpoint: {name}, " + f"language model's corresponding weight {dest_name} not found." + ) + + target_tensor = lm_state_dict[dest_name] + is_dist_tensor = isinstance(target_tensor, DTensor) + local_view = target_tensor.to_local() if is_dist_tensor else target_tensor + + if dest_weight.device != local_view.device: + dest_weight = dest_weight.to(local_view.device) + + assert local_view.shape == dest_weight.shape, ( + f"Shape mismatch: {local_view.shape} != {dest_weight.shape} " + f"for {dest_name} with original shape {target_tensor.shape}" + ) + with torch.no_grad(): + local_view.data.copy_(dest_weight) + + keys_loaded.add(dest_name) + + # Perform more error checking to ensure the checkpoint is valid. If the keys are missing, + # then the missing keys should be from the generation pathway. All keys from the + # understanding pathway must be present in the checkpoint. Additionally, for 2B and 4B + # dense Qwen VLMs, the `lm_head.weight` key is not present in the checkpoint. For these + # models, the input embedding and generation layer share the same params due to + # `tie_word_embeddings` being set to True in the configs. For the 0.6B LLM, 8B and 32B dense + # VLMs, and the 30B and 235B MoE VLMs, the `lm_head.weight` key is present in the + # checkpoint. + keys_missing = set(lm_state_dict.keys()) - keys_loaded + tie = getattr(model.config, "tie_word_embeddings", False) + real_keys_missing = {k for k in keys_missing if not ("_moe_gen" in k or (tie and "lm_head.weight" in k))} + if real_keys_missing: + raise ValueError( + f"load_language_model: {len(real_keys_missing)} required model " + f"parameter(s) not found in checkpoint '{checkpoint_path}'. " + f"First up to 10: {sorted(real_keys_missing)[:10]}" + ) + + log.info( + f"load_language_model: successfully loaded {len(keys_loaded)} tensors " + f"from {checkpoint_path} in {time.time() - start_time:.1f}s" + ) + return keys_loaded + + +def load_vlm_model( + model: torch.nn.Module, + checkpoint_path: str, + credential_path: str | None, + parallel_dims: ParallelDims | None, + tensor_names_to_skip: list[str] | None = None, + extra_skip_patterns: list[str] | None = None, +) -> set[str]: + """Load a HF VLM checkpoint (safetensors) into an FSDP-wrapped HFModel. + + Local paths and S3 URIs are tried first; if no safetensors are found, + explicit ``hf://org/model`` Hub URIs and bare ``org/model`` repo IDs fall + back to Hugging Face. + + Both ``tensor_names_to_skip`` and ``extra_skip_patterns`` are lists of + regex patterns applied to the RESOLVED model key (post-name_converter). + Phase-5 skips any model key matched by either list; Phase-6's + completeness check tolerates missing model keys matched by either + list. The two kwargs are semantically identical — separate names let + call sites distinguish "model-type fixed skips" (from + ``_tensor_names_to_skip_for``) from "overlay-specific skips" (from + ``VLMModel._init_vlm`` for the pretrain_weights_path_llm overlay). + + Cosmos-rl-style universal loader — no per-family hand-coded key mapping. + Resolves the FSDP shard sub-group via :func:`_get_dp_shard_mesh`, which + reads ``parallel_dims.dp_shard_mesh`` (the 1-D ``dp_shard`` sub-mesh + populated by ``ParallelDims.build_meshes()``). ``cp`` and ``cfgp`` live + in their own overlay meshes and do NOT participate in checkpoint sharding. + + Preconditions: + - ``parallelize()`` has been called on the HFModel (parameters are DTensors). + - ``HFModel.tie_embeddings()`` has been called before this function so that + tied ``lm_head.weight`` / ``embed_tokens.weight`` share DTensor storage. + - When ``parallel_dims`` is provided AND ``parallel_dims.dp_shard > 1``, + ``parallel_dims.build_meshes()`` MUST have been called by the caller. + Otherwise ``dp_shard_mesh`` returns None and the loader silently falls + back to single-rank loading — every rank reads every file and slices + locally, which is correct for ``dp_shard <= 1`` but a silent perf / + correctness regression for FSDP runs. Pass ``parallel_dims=None`` + explicitly for the single-process / unit-test fallback. + + Raises: + NotImplementedError: for MoE VLMs (not yet supported — see spec §2.2). + ValueError: when the checkpoint is missing a required model parameter. + + Returns: + Set of model state-dict keys successfully loaded from the checkpoint. + """ + start_time = time.time() + log.info(f"Loading VLM weights in safetensors format from: {checkpoint_path}") + + # Phase 1: canonical model state dict with compile/FSDP wrapper prefixes stripped. + vlm_state_dict = { + name.replace("_orig_mod.", "").replace("_checkpoint_wrapped_module.", ""): tensor + for name, tensor in model.state_dict().items() + } + + # Phase 2+3: suffix-lookup table + name converter. + hf_conv_map = getattr(model, "_checkpoint_conversion_mapping", None) + name_converter = _make_name_converter( + vlm_state_dict, + hf_conv_map=hf_conv_map if hf_conv_map else None, + ) + + # Phase 4: MoE precheck — fail early rather than silently mis-shard. + if _is_moe_vlm(model): + raise NotImplementedError( + "load_vlm_model does not yet support MoE VLMs " + "(e.g. Qwen3-VL-30B-A3B, Qwen3-VL-235B-A22B). Expected follow-up MR " + "ports cosmos-rl's is_moe_mlp_fused_into_dp_shard / replicated-gate " + "handling. Use a dense VLM checkpoint (2B, 4B, 8B, 32B) until then." + ) + + # Detect fsdp_offload mode by inspecting a sample parameter's device. In + # offload mode, the FSDP-materialized local_views live on CPU; routing the + # loader's distributed broadcast through CUDA would materialize the full + # checkpoint tensor on GPU transiently (defeats the point of offload). Use + # a single-rank fallback in that case: every rank reads every file on CPU, + # slices locally, no broadcast. I/O-redundant but memory-safe, matching + # the pre-MR _load_vlm_weights behavior under offload. + sample_target = next(iter(vlm_state_dict.values())) if vlm_state_dict else None + sample_local = sample_target.to_local() if isinstance(sample_target, DTensor) else sample_target + offload_mode = sample_local is not None and sample_local.device.type == "cpu" + + # Pick the loader's group: single-rank (no broadcast) in offload mode to + # keep memory off-GPU; the dp_shard sub-mesh otherwise. + loader = MultiRankCheckpointLoader(_get_dp_shard_mesh(parallel_dims) if not offload_mode else None) + rank_tensors, rank_tensor_meta, ckpt_names = loader.load_files_parallel( + checkpoint_path=checkpoint_path, + credential_path=credential_path if credential_path else "", + loading_device="cpu", + ) + all_tensor_names, tensor_to_rank = loader.gather_tensor_names_and_build_mapping( + ckpt_names, + rank_tensors, + ) + + # Phase 5: per-tensor copy. Skip patterns match the MODEL key (post- + # name_converter), not the raw ckpt key — this matches cosmos-rl's + # semantics and avoids fragility with prefix variations. The two lists + # are concatenated; they share Phase-5 skip + Phase-6 tolerance + # semantics. + _all_skip_patterns = (tensor_names_to_skip or []) + (extra_skip_patterns or []) + skip_patterns = [re.compile(p) for p in _all_skip_patterns] + keys_loaded: set[str] = set() + skipped_model_keys: set[str] = set() + + # Broadcast/iterate device: CUDA (NCCL) unless we're in offload mode, in + # which case everything stays on CPU. In offload mode the loader group + # is single-rank, so iterate_tensors doesn't actually broadcast — device + # just controls where the tensor is yielded. + if offload_mode or not torch.cuda.is_available(): + target_device = "cpu" + else: + target_device = "cuda" + + # Resolve the shard axis for the FSDP slicing. Even in offload mode we + # still need the real (shard_rank, shard_size) from parallel_dims so each + # rank takes its own FSDP slice — we just route the LOAD/BROADCAST through + # world_size=1 (single-rank fallback) to avoid the GPU spike. + dp_shard_mesh = _get_dp_shard_mesh(parallel_dims) + if dp_shard_mesh is not None: + shard_rank = dp_shard_mesh.get_local_rank() + shard_size = dp_shard_mesh.size() + else: + shard_rank = 0 + shard_size = 1 + + for ckpt_name, tensor in loader.iterate_tensors( + all_tensor_names, + tensor_to_rank, + rank_tensors, + rank_tensor_meta, + device=target_device, + ): + dest_name = name_converter(ckpt_name) + + if any(p.fullmatch(dest_name) for p in skip_patterns): + skipped_model_keys.add(dest_name) + continue + + if dest_name not in vlm_state_dict: + continue # extra checkpoint key — ignore + + target = vlm_state_dict[dest_name] + is_dtensor = isinstance(target, DTensor) + local_view = target.to_local() if is_dtensor else target + + # Slice using the REAL FSDP shard_rank/shard_size derived from + # parallel_dims.dp_shard_mesh, NOT loader.rank/world_size. In + # offload mode those two differ: the loader runs single-rank + # (world_size=1) but FSDP still shards across N ranks. + shard = _shard_first_dim(tensor, shard_size, shard_rank) + if shard.device != local_view.device: + shard = shard.to(local_view.device) + + if shard.shape != local_view.shape: + raise ValueError( + f"Shape mismatch for {dest_name}: local_view={tuple(local_view.shape)}, shard={tuple(shard.shape)}" + ) + with torch.no_grad(): + local_view.data.copy_(shard) + keys_loaded.add(dest_name) + + # Phase 6: completeness check with tied-embedding AND skip-list tolerance. + missing = set(vlm_state_dict) - keys_loaded - skipped_model_keys + + # Also tolerate missing model keys that match a skip pattern directly — + # handles the case where the ckpt doesn't contain the key at all, so the + # Phase 5 loop never saw it and skipped_model_keys didn't accumulate it. + missing = {k for k in missing if not any(p.fullmatch(k) for p in skip_patterns)} + tie = getattr(model.config, "tie_word_embeddings", False) + real_missing = {k for k in missing if not (tie and "lm_head.weight" in k)} + if real_missing: + raise ValueError( + f"load_vlm_model: {len(real_missing)} required model parameter(s) " + f"not found in checkpoint '{checkpoint_path}'. First up to 10: " + f"{sorted(real_missing)[:10]}" + ) + log.info( + f"load_vlm_model: loaded {len(keys_loaded)} tensors from {checkpoint_path} in {time.time() - start_time:.1f}s" + ) + return keys_loaded diff --git a/cosmos_training/cosmos/model/vfm/vlm/__init__.py b/cosmos_training/cosmos/model/vfm/vlm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/vlm/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1401bf9dc5f7c216e41468cbe162ddc1495f8fa GIT binary patch literal 239 zcmZ8b%MF4+5M@OYV?rTboCb^~*un;JA&~u$nOzJ`c(n#=u?#CPc;e)20v>$H%zN|Z zlX>R(HevWr)<%BC^pA1P&OI5Doj5%bTyZtVndt^PkVgd-U7>U6E2kSrP&zV99lBPP zeyQcTASFE(jcsfVffN!9g{@=wc}coXS#kkOwK%_x`K)mlrJ=73^xA-we9lX`?}lk} V;9k(`h#9{pI4FvDDulq4)fcZ=NOS-I literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__init__.py b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..044a5a146e7128a2477bc12b5b0ba90c10edc529 GIT binary patch literal 259 zcmZ8bL2AQ54D{ADl!70UOLgeA^a=ffa2z?QocJNDb&n<7sGxCCvoN}rbk{&uB zX(VPOo$C6%Fr-^G7&-6`tjvC`zOx>zCq>mLl~-I}&Zlah>|7vD4atVXG-BrE| zyOd0o2Ny6<2WSffXkZ8Ep$!5Qg@N9B>ZRxQVxs}FUG$J3EehS%l>-<#^}Qjvb__R% zq9e`B_syHxdGp@Po8e!Q$pnGqJc-^sJ3`3kNct(-14qXJ+#`%^5vH(^p)^8UA%%p< zRl>q|2#fI0gK)R+R=6E4#}3|k!!qmoc1viEZkcM`66ywTSdIX)u6~=FHdo)it~&gd zqgJ)rZq+imy4=)s!I`?#t_q#0Rl}<7+Um+h)pj(KX@aSS*5<-4uO8xLWyKqPOK?YM zy2)90!dh30Zh4_cjuHwXKZONwvZcTxLQL5Tv(Q##ko>QItOSdWvM7(S7}Nuhryx&54j&8}~vu$8<^ZYf)-@00R^ zH`{l{x^%_c*EhitT?-yC#A;3Y?<&`!xKPn|y1A`t zj$@vx(35hE)nRUJGy3o#)1m44)>19iK%Ic z2IWm#H!RZ|72MWYOEai$(t2}!%^PgN!fRS>2L_;qhPACbHgznz%X!lqZi0S}wF_(2 znKxXuP}FDqd6u#b9gRcxb(2~9-gHB|MVpolYQb`!v)$OUMKEDCtro1SXR1CQ;-*$L zI0Z5HhOwk}4Hjo^dkM*$9qwq}aIafK_Djl&+URFk>X2iJ+74w-8@>^PL)sE%E#KRU zSj8a^K|cBz&gT2dJIP6!xCdMpeq6tD=eUbrEo69pCtWHDgLh^;2P_3F8L$yxsep|FgOJ)yoe5y0 z0h9TU=snG1vVM58DLWZ%K)1WSoSBOPSy->L&*LRZ95Z4a=AZ+GF)8Gv_i}H5xq8x&WC`SY;FK~!J^#pq7cch#b=rqX$ zx=5gB1Uf{Z9|W3Tpw$H$TcC{vnpU7i1sYOF4bW@?ttDo$z%_|17C9{Duu!qUZ7K>_ z%wti6!V5R8rdUQ4wOdUtyf}qJtUHvJRWD+5qyB$a@&#kOOlPU-GsR!fIkvm62?&>i zl-3N*wkfq$Sb_TW>Gqd@tQalZ5xi2j_bW|HICaa=ExS^)>;_a#VcmpN-siNcK@5|Z zFhqz71T1$?X6GpLz=qjdj2$u=Unw9);Tvl52I6VzqvnZ=^~0t!DRjR>@v2e-Pw}V zv9DXddG*}G)vF);@XtTMUbBJfrQ^T7_M2-DE8qFk{$ESqMOoqR@#MP$kK!{vi47*) z^g{1nU4J|X4Ss@*OdbsZIs)U6S`dGMAjX(`gm!~yO>RQ{VuH7Ya!|Tt!0jks4&U47 z=1TX?aKqY!HwEMHPH=YY)v_WFCRD5N5gWg*ic?U+h10NuYx7{~YdraU$9M26c=ExS zF)}*w$;jfPk;RAcMX?5rrWAL4+++gMUTF{PJ7dk9R)5`-i9hxOipr5LFZy5yd$ifZ=CDVC4K4WSIoMH#ax7k=*&@-lfeA zFJ7%1mgbx~Qy#&HVQLLdsh6M>c6$r1Sn3T^`XhJ|^dtk6GOLDN!N(yvQP3%n*} zfq~Kf3>NtG4*6Te{B-QZ?Q4FVWD0Jn>}KZN!h%~Uy2S-|CgT=MZhpa?E4k^sTU>HW z%Wh%Ooz1)RMK_z1|9N+z>@Kai#YI8w8~;S@>d{Amei z$oRCMmT;D23w}nzEXmFLISKP5Gw07qsFH~pzaZf}$*F!(!V;OD^%o>uM7Sj3GQzTi z$6@08?1O}VLef{r{DQwC;VRBpk?u92x(e_g`UWIA(y^SxdFRY}i~$yxs! z5}pN#|C)r~Bs1CjZ@kz3_31~`Oa3|8b{@?ot?9q6kVNW>ONwlNGelx3KNN~C-RJ)% zP#uM#;R!a#(;$7m0oUzi+$JgnV$+5nXblM5t6z<=Ek`$OfychPS@36I_Y-(jzl@*n pZsUvM22ea8HeRKESW%S6Dp9!dPjcZ&?7E^XJuIC1f*_E?{{sP#&0+um literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/nemotron_3_dense_vl.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/__pycache__/nemotron_3_dense_vl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b7a304c8e34eecdfacfaae5c563457a3fc2fb92 GIT binary patch literal 12034 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 aZ#&*}Jf;wSEYTkN0zDu99feOqbpH#SPiRH} literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json new file mode 100644 index 00000000..3f1075eb --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configs/Nemotron-2B-Dense-VL.json @@ -0,0 +1,31 @@ +{ + "vocab_size": 131072, + "tie_word_embeddings": false, + "hidden_size": 2048, + "intermediate_size": 9216, + "num_hidden_layers": 28, + "num_attention_heads": 16, + "head_dim": 128, + "num_key_value_heads": 8, + "mlp_hidden_act": "relu2", + "attention_bias": false, + "mlp_bias": false, + "initializer_range": 0.02, + "layer_norm_epsilon": 1e-05, + "residual_in_fp32": false, + "use_cache": true, + "num_logits_to_keep": 1, + "pad_token_id": 0, + "bos_token_id": 1, + "eos_token_id": 11, + "sliding_window": null, + "max_position_embeddings": 131072, + "attention_dropout": 0.0, + "hidden_dropout": 0.0, + "enable_rope": true, + "rope_scaling": null, + "rope_theta": 100000000, + "enable_mrope": true, + "mrope_section": [24, 20, 20], + "torch_dtype": "bfloat16" +} diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py new file mode 100644 index 00000000..e4786731 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/configuration_nemotron_3_dense_vl.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Configuration for Nemotron 3 Dense VL text backbone (paired hybrid blocks -> standard layers).""" + +from transformers.configuration_utils import PretrainedConfig + + +class Nemotron3DenseVLTextConfig(PretrainedConfig): + """Text config for Nemotron-H style language model after pairing attn+MLP blocks (28 effective layers).""" + + model_type = "nemotron_3_dense_vl_text" + + def __init__( + self, + vocab_size: int = 131072, + tie_word_embeddings: bool = False, + hidden_size: int = 2048, + intermediate_size: int = 9216, + num_hidden_layers: int = 28, + num_attention_heads: int = 16, + head_dim: int = 128, + num_key_value_heads: int = 8, + mlp_hidden_act: str = "relu2", + attention_bias: bool = False, + mlp_bias: bool = False, + initializer_range: float = 0.02, + layer_norm_epsilon: float = 1e-5, + residual_in_fp32: bool = False, + use_cache: bool = True, + num_logits_to_keep: int = 1, + pad_token_id: int = 0, + bos_token_id: int = 1, + eos_token_id: int = 11, + sliding_window: int | None = None, + max_position_embeddings: int = 131072, + attention_dropout: float = 0.0, + hidden_dropout: float = 0.0, + enable_rope: bool = True, + rope_scaling: dict | None = None, + rope_theta: float = 100_000_000.0, + enable_mrope: bool = True, + mrope_section: list[int] | None = None, + torch_dtype: str = "bfloat16", + **kwargs, + ) -> None: + self.vocab_size = vocab_size + self.tie_word_embeddings = tie_word_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.head_dim = head_dim + self.num_key_value_heads = num_key_value_heads + self.mlp_hidden_act = mlp_hidden_act + self.attention_bias = attention_bias + self.mlp_bias = mlp_bias + self.initializer_range = initializer_range + self.layer_norm_epsilon = layer_norm_epsilon + self.residual_in_fp32 = residual_in_fp32 + self.use_cache = use_cache + self.num_logits_to_keep = num_logits_to_keep + self.sliding_window = sliding_window + self.max_position_embeddings = max_position_embeddings + self.attention_dropout = attention_dropout + self.hidden_dropout = hidden_dropout + self.rope_scaling = rope_scaling + self.rope_theta = rope_theta + self.enable_rope = enable_rope + self.enable_mrope = enable_mrope + self.mrope_section = mrope_section if mrope_section is not None else [24, 20, 20] + self.torch_dtype = torch_dtype + self._attn_implementation = kwargs.pop("_attn_implementation", "eager") + super().__init__( + pad_token_id=pad_token_id, + bos_token_id=bos_token_id, + eos_token_id=eos_token_id, + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + @property + def rms_norm_eps(self) -> float: + """Alias for Qwen-style MoT code paths.""" + return self.layer_norm_epsilon diff --git a/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py new file mode 100644 index 00000000..e46f04e1 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/nemotron_3_dense_vl/nemotron_3_dense_vl.py @@ -0,0 +1,165 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Nemotron-H style text modules for Nemotron 3 Dense VL (ReLU^2 MLP, partial RoPE helper, mRoPE).""" + +from __future__ import annotations + +import functools + +import torch +import torch.nn.functional as F +from torch import nn +from transformers.activations import ACT2FN + +if "relu2" not in ACT2FN: + ACT2FN["relu2"] = lambda x: F.relu(x).square() +from transformers.modeling_rope_utils import dynamic_rope_update +from transformers.modeling_utils import PreTrainedModel + +from cosmos.model.vfm.vlm.nemotron_3_dense_vl.configuration_nemotron_3_dense_vl import ( + Nemotron3DenseVLTextConfig, +) + + +def rotate_half(x: torch.Tensor) -> torch.Tensor: + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_pos_emb_partial( + q: torch.Tensor, + k: torch.Tensor, + cos: torch.Tensor, + sin: torch.Tensor, + unsqueeze_dim: int = 1, +) -> tuple[torch.Tensor, torch.Tensor]: + """Apply RoPE to the first rot_dim channels; remainder passes through (rot_dim == head_dim for 2B Dense).""" + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + rot_dim = cos.shape[-1] + q_rot, q_pass = q[..., :rot_dim], q[..., rot_dim:] + k_rot, k_pass = k[..., :rot_dim], k[..., rot_dim:] + q_embed = (q_rot * cos) + (rotate_half(q_rot) * sin) + k_embed = (k_rot * cos) + (rotate_half(k_rot) * sin) + return torch.cat((q_embed, q_pass), dim=-1), torch.cat((k_embed, k_pass), dim=-1) + + +class Nemotron3DenseVLRMSNorm(nn.Module): + def __init__(self, hidden_size: int, eps: float = 1e-5) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return (self.weight.to(torch.float32) * hidden_states).to(input_dtype) + + def extra_repr(self) -> str: + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Nemotron3DenseVLMLP(nn.Module): + def __init__(self, config: Nemotron3DenseVLTextConfig, layer_idx: int | None = None) -> None: + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias) + if config.mlp_hidden_act in ACT2FN: + self.act_fn = ACT2FN[config.mlp_hidden_act] + else: + self.act_fn = lambda x: F.relu(x).square() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.down_proj(self.act_fn(self.up_proj(x))) + + +class MultiModalRotaryEmbedding(nn.Module): + inv_freq: torch.Tensor + + def __init__(self, config: Nemotron3DenseVLTextConfig, device: torch.device | None = None) -> None: + super().__init__() + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + self.config = config + self.mrope_section = getattr(config, "mrope_section", [24, 20, 20]) + inv_freq, self.attention_scaling = self.compute_default_rope_parameters(self.config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self.register_buffer("original_inv_freq", inv_freq.clone(), persistent=False) + + @staticmethod + def compute_default_rope_parameters( + config: Nemotron3DenseVLTextConfig | None = None, + device: torch.device | None = None, + seq_len: int | None = None, + ) -> tuple[torch.Tensor, float]: + rope_theta = config.rope_theta + dim = config.head_dim + attention_factor = 1.0 + inv_freq = 1.0 / ( + rope_theta ** (torch.arange(0, dim, 2, dtype=torch.int64, device=device).to(dtype=torch.float) / dim) + ) + return inv_freq, attention_factor + + def apply_interleaved_mrope(self, freqs: torch.Tensor, mrope_section: list[int]) -> torch.Tensor: + freqs_t = freqs[0] + for dim, offset in enumerate((1, 2), start=1): + length = mrope_section[dim] * 3 + idx = slice(offset, length, 3) + freqs_t[..., idx] = freqs[dim, ..., idx] + return freqs_t + + @torch.no_grad() + @dynamic_rope_update + def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + if position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) + inv_freq_expanded = self.inv_freq[None, None, :, None].float().expand(3, position_ids.shape[1], -1, 1) + position_ids_expanded = position_ids[:, :, None, :].float() + device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu" + with torch.autocast(device_type=device_type, enabled=False): + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3) + freqs = self.apply_interleaved_mrope(freqs, self.mrope_section) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() * self.attention_scaling + sin = emb.sin() * self.attention_scaling + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + inv_freq, self.attention_scaling = self.compute_default_rope_parameters(self.config, buffer_device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + +class Nemotron3DenseVLPreTrainedModel(PreTrainedModel): + config_class = Nemotron3DenseVLTextConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _supports_flash_attn = True + _supports_sdpa = True + + def _init_weights(self, module: nn.Module, buffer_device: torch.device | None) -> None: + super()._init_weights(module) + if isinstance(module, MultiModalRotaryEmbedding): + module.init_weights(buffer_device=buffer_device) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + self.apply(functools.partial(self._init_weights, buffer_device=buffer_device)) diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__init__.py b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..569163bdbbc7b7dec25fe8c4036c63463d58c623 GIT binary patch literal 248 zcmZ8bOA5k34DDD25j=<+vv4aO!7B{4rc#-o$+Xgw^bDTGV|W6otFFv|y6{2r^1{m_ z_vLbt3F2#ZPW(viAH!5Oeb!|wwZCOF5j7;w*n?>}5<yqm(mtSL-%yF)&F|t9YyA-ow3UU{Sh!r#ZFFluYsrmT auR}M^w_<7x?r0`|Dk)CGGgnGcF4G%}$V=M* literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/configuration_qwen3_vl.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/configuration_qwen3_vl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10426b2d1d71788179734bd794d1856f8c76d799 GIT binary patch literal 13493 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 R21Yl=@*90e{-8$?{4Y+gpzHtu literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/qwen3_vl.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl/__pycache__/qwen3_vl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f116fa9e001b2285093f6139bebbeecc82b334f GIT binary patch literal 84609 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

TDPL19HBZKau&|lXx+`Xhg2)YSvvE3L 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

{OtC}<@j4W zEpM;50Tb3pt|d3dHkNi;dsjRFNgUm5*}S}Ub*JmXcTCU+tGr)HoZLEDP7JMFtGb!C zqnqc;k>tudPoweFCs z<;~(d0OaBuZDm(?#e1aU?W}mC6>mqy8>vK&{4wO=+|_0#m?*irsHmhHFR1QZ+Q3}D z@~SPDVVhNh!3Tn6SA$V(qzqfaf=NdGIyr-zZ4@*Mr7_4pz*W>-iv29%Jm6Vk&(+wx z*@WrG;Haakam`V#-3=lIksCx+5Osa!s>fU&!((t62Z$3V{0?-1#)}^iDK9{9Y^6I^ zfN94QG_w3r&S*+WyBLtI2TKyKs*P#LzaE|Hh zd6amVD4lrYo0cc>OQp-7l;fY2TrsfmO>OtpRkhSTy#4V``0e5qh!dLI?=P(`K_+tg z;pv^G6I=0e)7kAC<)-sr|EwH-=bOcH_=DmV$VZ}`>l14eo9$b|leRP47s_oT#cM#Z zqrcKUTYnBadfq5otOke(&FOH4@es28A zs5-Ipw5z+yVFNCuHCpx1rk81s>{8tC&O6zjYOsTiY@Vtz@Y!0}#ZUD-r<&x@S-u;pjME!6472wP!j?IN}6$RPWAog}oM W0tyFTAP3Vt*utH?^g)ZB>;D1R?O%rg literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/qwen3_vl_moe.cpython-312.pyc b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/__pycache__/qwen3_vl_moe.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c15b067f45ef597a88edc65b99e2d40f46d92192 GIT binary patch literal 105495 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json new file mode 100644 index 00000000..f1933484 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-235B-A22B-Instruct.json @@ -0,0 +1,68 @@ +{ + "architectures": [ + "Qwen3VLMoeForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl_moe", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "decoder_sparse_step": 1, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 4096, + "initializer_range": 0.02, + "intermediate_size": 12288, + "max_position_embeddings": 262144, + "mlp_only_layers": [], + "model_type": "qwen3_vl_moe_text", + "moe_intermediate_size": 1536, + "norm_topk_prob": true, + "num_attention_heads": 64, + "num_experts": 128, + "num_experts_per_tok": 8, + "num_hidden_layers": 94, + "num_key_value_heads": 4, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl_moe", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 4096, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json new file mode 100644 index 00000000..23665bac --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configs/Qwen3-VL-30B-A3B-Instruct.json @@ -0,0 +1,68 @@ +{ + "architectures": [ + "Qwen3VLMoeForConditionalGeneration" + ], + "image_token_id": 151655, + "model_type": "qwen3_vl_moe", + "text_config": { + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "decoder_sparse_step": 1, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2048, + "initializer_range": 0.02, + "intermediate_size": 6144, + "max_position_embeddings": 262144, + "mlp_only_layers": [], + "model_type": "qwen3_vl_moe_text", + "moe_intermediate_size": 768, + "norm_topk_prob": true, + "num_attention_heads": 32, + "num_experts": 128, + "num_experts_per_tok": 8, + "num_hidden_layers": 48, + "num_key_value_heads": 4, + "rms_norm_eps": 1e-06, + "rope_scaling": { + "mrope_interleaved": true, + "mrope_section": [ + 24, + 20, + 20 + ], + "rope_type": "default" + }, + "rope_theta": 5000000, + "use_cache": true, + "vocab_size": 151936 + }, + "tie_word_embeddings": false, + "transformers_version": "4.57.0.dev0", + "video_token_id": 151656, + "vision_config": { + "deepstack_visual_indexes": [ + 8, + 16, + 24 + ], + "depth": 27, + "hidden_act": "gelu_pytorch_tanh", + "hidden_size": 1152, + "in_channels": 3, + "initializer_range": 0.02, + "intermediate_size": 4304, + "model_type": "qwen3_vl_moe", + "num_heads": 16, + "num_position_embeddings": 2304, + "out_hidden_size": 2048, + "patch_size": 16, + "spatial_merge_size": 2, + "temporal_patch_size": 2 + }, + "vision_end_token_id": 151653, + "vision_start_token_id": 151652 +} diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py new file mode 100644 index 00000000..e5d834a5 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/configuration_qwen3_vl_moe.py @@ -0,0 +1,330 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from transformers.configuration_utils import PretrainedConfig +from transformers.modeling_rope_utils import rope_config_validation + + +class Qwen3VLMoeTextConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLMoeTextModel`]. It is used to instantiate a + Qwen3-VL-MOE model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-30B-A3B-Instruct [Qwen/Qwen3-VL-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + Args: + vocab_size (`int`, *optional*, defaults to 151936): + Vocabulary size of the Qwen2MoE model. Defines the number of different tokens that can be represented by the + `inputs_ids` passed when calling [`Qwen2MoeModel`] + hidden_size (`int`, *optional*, defaults to 2048): + Dimension of the hidden representations. + intermediate_size (`int`, *optional*, defaults to 5632): + Dimension of the MLP representations. + num_hidden_layers (`int`, *optional*, defaults to 24): + Number of hidden layers in the Transformer encoder. + num_attention_heads (`int`, *optional*, defaults to 16): + Number of attention heads for each attention layer in the Transformer encoder. + num_key_value_heads (`int`, *optional*, defaults to 16): + This is the number of key_value heads that should be used to implement Grouped Query Attention. If + `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if + `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When + converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed + by meanpooling all the original heads within that group. For more details checkout [this + paper](https://arxiv.org/pdf/2305.13245.pdf). If it is not specified, will default to `32`. + hidden_act (`str` or `function`, *optional*, defaults to `"silu"`): + The non-linear activation function (function or string) in the decoder. + max_position_embeddings (`int`, *optional*, defaults to 128000): + The maximum sequence length that this model might ever be used with. + initializer_range (`float`, *optional*, defaults to 0.02): + The standard deviation of the truncated_normal_initializer for initializing all weight matrices. + rms_norm_eps (`float`, *optional*, defaults to 1e-06): + The epsilon used by the rms normalization layers. + use_cache (`bool`, *optional*, defaults to `True`): + Whether or not the model should return the last key/values attentions (not used by all models). Only + relevant if `config.is_decoder=True`. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether the model's input and output word embeddings should be tied. + rope_theta (`float`, *optional*, defaults to 5000000.0): + The base period of the RoPE embeddings. + attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`): + Whether to use a bias in the query, key, value and output projection layers during self-attention. + attention_dropout (`float`, *optional*, defaults to 0.0): + The dropout ratio for the attention probabilities. + decoder_sparse_step (`int`, *optional*, defaults to 1): + The frequency of the MoE layer. + moe_intermediate_size (`int`, *optional*, defaults to 1408): + Intermediate size of the routed expert. + num_experts_per_tok (`int`, *optional*, defaults to 4): + Number of selected experts. + num_experts (`int`, *optional*, defaults to 60): + Number of routed experts. + norm_topk_prob (`bool`, *optional*, defaults to `True`): + Whether to normalize the topk probabilities. + router_aux_loss_coef (`float`, *optional*, defaults to 0.001): + The aux loss factor for the total loss. + mlp_only_layers (`List[int]`, *optional*, defaults to `[]`): + Indicate which layers use Qwen3VLMoeMLP rather than Qwen3VLMoeSparseMoeBlock + The list contains layer index, from 0 to num_layers-1 if we have num_layers layers + If `mlp_only_layers` is empty, `decoder_sparse_step` is used to determine the sparsity. + rope_scaling (`Dict`, *optional*): + Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type + and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value + accordingly. + Expected contents: + `rope_type` (`str`): + The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope', + 'llama3'], with 'default' being the original RoPE implementation. + `factor` (`float`, *optional*): + Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In + most scaling types, a `factor` of x will enable the model to handle sequences of length x * + original maximum pre-trained length. + `original_max_position_embeddings` (`int`, *optional*): + Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during + pretraining. + `attention_factor` (`float`, *optional*): + Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention + computation. If unspecified, it defaults to value recommended by the implementation, using the + `factor` field to infer the suggested value. + `beta_fast` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear + ramp function. If unspecified, it defaults to 32. + `beta_slow` (`float`, *optional*): + Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear + ramp function. If unspecified, it defaults to 1. + `short_factor` (`List[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to short contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `long_factor` (`List[float]`, *optional*): + Only used with 'longrope'. The scaling factor to be applied to long contexts (< + `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden + size divided by the number of attention heads divided by 2 + `low_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE + `high_freq_factor` (`float`, *optional*): + Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE + head_dim (`int`, *optional*): + The dimension of the head. If not specified, will default to `hidden_size // num_attention_heads`. + + ```python + >>> from transformers import Qwen3VLMoeForConditionalGeneration, Qwen3VLMoeConfig + + >>> # Initializing a Qwen3VLMoe style configuration + >>> configuration = Qwen3VLMoeConfig() + + >>> # Initializing a model from the Qwen3-VL-30B-A3B style configuration + >>> model = Qwen3VLMoeForConditionalGeneration(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl_moe_text" + base_config_key = "text_config" + keys_to_ignore_at_inference = ["past_key_values"] + # Default tensor parallel plan for base model `Qwen3VLMoe` + base_model_tp_plan = { + "layers.*.self_attn.q_proj": "colwise", + "layers.*.self_attn.k_proj": "colwise", + "layers.*.self_attn.v_proj": "colwise", + "layers.*.self_attn.o_proj": "rowwise", + "layers.*.mlp.gate_proj": "colwise", + "layers.*.mlp.up_proj": "colwise", + "layers.*.mlp.down_proj": "rowwise", + } + base_model_pp_plan = { + "embed_tokens": (["input_ids"], ["inputs_embeds"]), + "layers": (["hidden_states", "attention_mask"], ["hidden_states"]), + "norm": (["hidden_states"], ["hidden_states"]), + } + + def __init__( + self, + vocab_size=151936, + hidden_size=2048, + intermediate_size=5632, + num_hidden_layers=24, + num_attention_heads=16, + num_key_value_heads=16, + hidden_act="silu", + max_position_embeddings=128000, + initializer_range=0.02, + rms_norm_eps=1e-6, + use_cache=True, + tie_word_embeddings=False, + rope_theta=5000000.0, + attention_bias=False, + attention_dropout=0.0, + decoder_sparse_step=1, + moe_intermediate_size=1408, + num_experts_per_tok=4, + num_experts=60, + norm_topk_prob=True, + router_aux_loss_coef=0.001, + mlp_only_layers=None, + rope_scaling=None, + head_dim=None, + **kwargs, + ): + self.vocab_size = vocab_size + self.max_position_embeddings = max_position_embeddings + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + + # for backward compatibility + if num_key_value_heads is None: + num_key_value_heads = num_attention_heads + + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.attention_bias = attention_bias + self.attention_dropout = attention_dropout + self.rope_scaling = rope_scaling + self.head_dim = head_dim or hidden_size // num_attention_heads + + rope_config_validation(self, ignore_keys={"mrope_section", "mrope_interleaved"}) + + # MoE arguments + self.decoder_sparse_step = decoder_sparse_step + self.moe_intermediate_size = moe_intermediate_size + self.num_experts_per_tok = num_experts_per_tok + self.num_experts = num_experts + self.norm_topk_prob = norm_topk_prob + self.router_aux_loss_coef = router_aux_loss_coef + self.mlp_only_layers = [] if mlp_only_layers is None else mlp_only_layers + + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + +class Qwen3VLMoeVisionConfig(PretrainedConfig): + model_type = "qwen3_vl_moe" + base_config_key = "vision_config" + + def __init__( + self, + depth=27, + hidden_size=1152, + hidden_act="gelu_pytorch_tanh", + intermediate_size=4304, + num_heads=16, + in_channels=3, + patch_size=16, + spatial_merge_size=2, + temporal_patch_size=2, + out_hidden_size=3584, + num_position_embeddings=2304, + deepstack_visual_indexes=[8, 16, 24], + initializer_range=0.02, + **kwargs, + ): + super().__init__(**kwargs) + + self.depth = depth + self.hidden_size = hidden_size + self.hidden_act = hidden_act + self.intermediate_size = intermediate_size + self.num_heads = num_heads + self.in_channels = in_channels + self.patch_size = patch_size + self.spatial_merge_size = spatial_merge_size + self.temporal_patch_size = temporal_patch_size + self.out_hidden_size = out_hidden_size + self.num_position_embeddings = num_position_embeddings + self.initializer_range = initializer_range + self.deepstack_visual_indexes = deepstack_visual_indexes + + +class Qwen3VLMoeConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`Qwen3VLMoeModel`]. It is used to instantiate a + Qwen3-VL-MOE model according to the specified arguments, defining the model architecture. Instantiating a configuration + with the defaults will yield a similar configuration to that of + Qwen3-VL-30B-A3B-Instruct [Qwen/Qwen3-VL-30B-A3B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct). + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + + Args: + text_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLMoeTextConfig`): + The config object or dictionary of the text backbone. + vision_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Qwen3VLMoeVisionConfig`): + The config object or dictionary of the vision backbone. + image_token_id (`int`, *optional*, defaults to 151655): + The image token index to encode the image prompt. + video_token_id (`int`, *optional*, defaults to 151656): + The video token index to encode the image prompt. + vision_start_token_id (`int`, *optional*, defaults to 151652): + The start token index to encode the image prompt. + vision_end_token_id (`int`, *optional*, defaults to 151653): + The end token index to encode the image prompt. + tie_word_embeddings (`bool`, *optional*, defaults to `False`): + Whether to tie the word embeddings. + + ```python + >>> from transformers import Qwen3VLMoeForConditionalGeneration, Qwen3VLMoeConfig + + >>> # Initializing a Qwen3-VL-MOE style configuration + >>> configuration = Qwen3VLMoeConfig() + + >>> # Initializing a model from the Qwen3-VL-30B-A3B style configuration + >>> model = Qwen3VLMoeForConditionalGeneration(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + + model_type = "qwen3_vl_moe" + sub_configs = {"vision_config": Qwen3VLMoeVisionConfig, "text_config": Qwen3VLMoeTextConfig} + keys_to_ignore_at_inference = ["past_key_values"] + + def __init__( + self, + text_config=None, + vision_config=None, + image_token_id=151655, + video_token_id=151656, + vision_start_token_id=151652, + vision_end_token_id=151653, + tie_word_embeddings=False, + **kwargs, + ): + if isinstance(vision_config, dict): + self.vision_config = self.sub_configs["vision_config"](**vision_config) + elif vision_config is None: + self.vision_config = self.sub_configs["vision_config"]() + + if isinstance(text_config, dict): + self.text_config = self.sub_configs["text_config"](**text_config) + elif text_config is None: + self.text_config = self.sub_configs["text_config"]() + + self.image_token_id = image_token_id + self.video_token_id = video_token_id + self.vision_start_token_id = vision_start_token_id + self.vision_end_token_id = vision_end_token_id + super().__init__(**kwargs, tie_word_embeddings=tie_word_embeddings) + + +__all__ = ["Qwen3VLMoeConfig", "Qwen3VLMoeTextConfig"] diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe.py b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe.py new file mode 100644 index 00000000..6618fd2a --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe.py @@ -0,0 +1,261 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Callable + +import torch +import torch.nn as nn +from transformers.activations import ACT2FN + +from cosmos.model.vfm.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeTextConfig, +) +from cosmos.model.vfm.vlm.qwen3_vl_moe.moe_kernels import ( + TOKEN_GROUP_ALIGN_SIZE_M, + _generate_permute_indices, +) + + +def _run_experts_grouped_mm( + gate_up_proj: torch.Tensor, # [num_experts,hidden_size,2*moe_intermediate_size] + down_proj: torch.Tensor, # [num_experts,moe_intermediate_size,hidden_size] + act_fn: Callable[[torch.Tensor], torch.Tensor], + x: torch.Tensor, # [num_tokens,hidden_size] (tokens sorted by expert) + num_tokens_per_expert: torch.Tensor, # [num_experts] + scores: torch.Tensor, # [padded_len] +) -> torch.Tensor: # [num_tokens,hidden_size] + """ + This function runs the gate/up/down projection in a grouped matrix multiplication fashion. + + Args: + gate_up_proj (torch.Tensor): (num_experts, hidden_size, 2 * moe_intermediate_size) + down_proj (torch.Tensor): (num_experts, moe_intermediate_size, hidden_size) + x (torch.Tensor): (batch_size * seq_len, hidden_size) + num_tokens_per_expert (torch.Tensor): (num_experts,) + scores (torch.Tensor): (num_tokens,) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + offsets = torch.cumsum(num_tokens_per_expert, dim=0, dtype=torch.int32) # [num_experts] + h = torch._grouped_mm(x, gate_up_proj, offs=offsets) # [num_tokens,2*moe_intermediate_size] + h = torch.chunk(h, chunks=2, dim=-1) # 2x [num_tokens,moe_intermediate_size] + h = act_fn(h[0]) * h[1] * scores.unsqueeze(-1) # [num_tokens,moe_intermediate_size] + return torch._grouped_mm(h, down_proj, offs=offsets) # [num_tokens,hidden_size] + + +class Qwen3VLMoeTextExpertsGroupedMm(nn.Module): + def __init__(self, config): + super().__init__() + self.gate_up_proj = nn.Parameter( + torch.empty(config.num_experts, config.hidden_size, 2 * config.moe_intermediate_size) + ) + self.down_proj = nn.Parameter(torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)) + self.act_fn = ACT2FN[config.hidden_act] + + self.num_experts = config.num_experts + self.moe_intermediate_size = config.moe_intermediate_size + self.hidden_size = config.hidden_size + self.top_k = config.num_experts_per_tok + + def forward( + self, + hidden_states: torch.Tensor, # [num_tokens,hidden_size] + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + num_tokens_per_expert: torch.Tensor, # [num_experts] + ) -> torch.Tensor: # [num_tokens,hidden_size] + """ + This module obtains the output of the experts by routing the tokens + to the experts and then performing a weighted sum of the output of the experts. + + Args: + hidden_states (torch.Tensor): (batch_size * seq_len, hidden_size) + topk_scores (torch.Tensor): (batch_size * seq_len, top_k) + expert_indices (torch.Tensor): (batch_size * seq_len, top_k) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + num_tokens, dim = hidden_states.shape + topk_scores_sorted, token_indices_sorted = self._reorder_tokens( + topk_scores, + expert_indices, + ) + # topk_scores_sorted: [num_tokens*top_k] + # token_indices_sorted: [num_tokens*top_k] + + # Build padded permutation indices + num_experts = num_tokens_per_expert.shape[0] + alignment = TOKEN_GROUP_ALIGN_SIZE_M + padded_size = num_tokens * self.top_k + num_experts * alignment + padded_size = ((padded_size + alignment - 1) // alignment) * alignment + + permuted_indices, padded_num_tokens_per_expert = _generate_permute_indices( + num_tokens_per_expert, + num_experts, + padded_size, + alignment, + ) + + # Compose: permuted_indices indexes into sorted order, + # token_indices_sorted maps sorted→original. Compose them: + sentinel = torch.tensor([num_tokens], device=hidden_states.device) # for padding slots + token_indices_ext = torch.cat([token_indices_sorted, sentinel]) + combined_indices = token_indices_ext[permuted_indices.long()] + combined_indices = combined_indices.unsqueeze(-1).expand(-1, dim) + + # Pad scores with a zero sentinel so padding slots contribute nothing + scores_ext = torch.cat([topk_scores_sorted, topk_scores_sorted.new_zeros(1)]) + combined_scores = scores_ext[permuted_indices.long()] # [padded_len] + + # Single gather (with a zero-padded sentinel row) + input_padded = torch.cat([hidden_states, hidden_states.new_zeros(1, dim)]) + routed_input = input_padded.gather(dim=0, index=combined_indices) + + # Run experts + routed_output = _run_experts_grouped_mm( + self.gate_up_proj, + self.down_proj, + self.act_fn, + routed_input, + padded_num_tokens_per_expert, + combined_scores, + ) + + output_padded = torch.zeros_like(input_padded) + output_padded.scatter_add_(dim=0, index=combined_indices, src=routed_output) + return output_padded[:-1] + + def _reorder_tokens( + self, + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + ) -> tuple[torch.Tensor, torch.Tensor]: + """Reorder tokens into expert-grouped order via argsort. + + Returns: + topk_scores_sorted: [num_tokens*top_k] scores in expert-grouped order. + token_indices_sorted: [num_tokens*top_k] original token indices in + expert-grouped order. + """ + token_indices_sorted = torch.argsort(expert_indices.view(-1), stable=True) # [num_tokens*top_k] + topk_scores_sorted = topk_scores.view(-1)[token_indices_sorted] # [num_tokens*top_k] + token_indices_sorted = token_indices_sorted // self.top_k # [num_tokens*top_k] + return topk_scores_sorted, token_indices_sorted + + def init_weights(self, buffer_device: torch.device): + nn.init.normal_(self.gate_up_proj, mean=0.0, std=0.02) + nn.init.normal_(self.down_proj, mean=0.0, std=0.02) + + +class Qwen3VLMoeTextExpertsNaive(nn.Module): + def __init__(self, config): + super().__init__() + self.gate_up_proj = nn.Parameter( + torch.empty(config.num_experts, config.hidden_size, 2 * config.moe_intermediate_size) + ) + self.down_proj = nn.Parameter(torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)) + self.act_fn = ACT2FN[config.hidden_act] + + self.num_experts = config.num_experts + self.moe_intermediate_size = config.moe_intermediate_size + self.hidden_size = config.hidden_size + + def forward( + self, + hidden_states: torch.Tensor, # [num_tokens,hidden_size] + topk_scores: torch.Tensor, # [num_tokens,top_k] + expert_indices: torch.Tensor, # [num_tokens,top_k] + num_tokens_per_expert: torch.Tensor, # [num_experts] + ) -> torch.Tensor: # [num_tokens,hidden_size] + """ + When training it is more efficient to just loop over the experts and compute the output for each expert + as otherwise the memory would explode. + + For inference we can sacrifice some memory and compute the output for all experts at once. By repeating the inputs. + + Args: + hidden_states (torch.Tensor): (batch_size * token_num, hidden_size) + routing_weights (torch.Tensor): (batch_size * token_num, top_k) + expert_indices (torch.Tensor): (batch_size * token_num, top_k) + num_tokens_per_expert (torch.Tensor): (num_experts,) + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + del num_tokens_per_expert + assert hidden_states.ndim == 2, "hidden_states must be of shape (batch_size * seq_len, hidden_size)" + assert hidden_states.shape[1] == self.hidden_size, ( + "hidden_states must be of shape (batch_size * seq_len, hidden_size)" + ) + routing_weights = torch.zeros( + hidden_states.shape[0], + self.num_experts, + dtype=hidden_states.dtype, + device=hidden_states.device, + ) # [num_tokens,num_experts] + routing_weights = routing_weights.scatter_(1, expert_indices, topk_scores) # [num_tokens,num_experts] + + if self.training: + next_states = torch.zeros_like(hidden_states) # [num_tokens,hidden_size] + with torch.no_grad(): + expert_mask = torch.nn.functional.one_hot( + expert_indices, num_classes=self.num_experts + ) # [num_tokens,top_k,num_experts] + expert_mask = expert_mask.permute(2, 1, 0) # [num_experts,top_k,num_tokens] + # we sum on the top_k and on the sequence length to get which experts + # are hit this time around + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + for expert_idx in expert_hit[:]: + with torch.no_grad(): + _, token_idx = torch.where(expert_mask[expert_idx[0]]) + current_state = hidden_states[token_idx] # [num_expert_tokens,hidden_size] + gate_up = current_state @ self.gate_up_proj[expert_idx] # [num_expert_tokens,2*moe_intermediate_size] + gate, up = gate_up.chunk(2, dim=-1) # 2x [num_expert_tokens,moe_intermediate_size] + gated_output = up * self.act_fn(gate) # [num_expert_tokens,moe_intermediate_size] + out = gated_output @ self.down_proj[expert_idx] # [num_expert_tokens,hidden_size] + weighted_output = out[0] * routing_weights[token_idx, expert_idx, None] + assert weighted_output.dtype == hidden_states.dtype + next_states.index_add_(0, token_idx, weighted_output) + else: + hidden_states = hidden_states.repeat(self.num_experts, 1) # [num_experts*num_tokens,hidden_size] + hidden_states = hidden_states.view( + self.num_experts, -1, self.hidden_size + ) # [num_experts,num_tokens,hidden_size] + gate_up = torch.bmm(hidden_states, self.gate_up_proj) # [num_experts,num_tokens,2*moe_intermediate_size] + gate, up = gate_up.chunk( + 2, dim=-1 + ) # not supported for DTensors # 2x [num_experts,num_tokens,moe_intermediate_size] + next_states = torch.bmm((up * self.act_fn(gate)), self.down_proj) # [num_experts,num_tokens,hidden_size] + next_states = next_states * routing_weights.transpose(0, 1).unsqueeze( + dim=-1 + ) # [num_experts,num_tokens,hidden_size] + next_states = next_states.sum(dim=0) # [num_tokens,hidden_size] + return next_states + + def init_weights(self, buffer_device: torch.device): + nn.init.normal_(self.gate_up_proj, mean=0.0, std=0.02) + nn.init.normal_(self.down_proj, mean=0.0, std=0.02) + + +def create_text_experts(config: Qwen3VLMoeTextConfig, implementation_type: str = "naive") -> nn.Module: + if implementation_type == "naive": + return Qwen3VLMoeTextExpertsNaive(config) + elif implementation_type == "grouped_mm": + return Qwen3VLMoeTextExpertsGroupedMm(config) + else: + raise ValueError(f"Invalid implementation: {implementation_type}") diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe_kernels.py b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe_kernels.py new file mode 100644 index 00000000..98841b35 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/moe_kernels.py @@ -0,0 +1,216 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Literal + +import torch +import triton +import triton.language as tl + +# Set the token group alignment size for experts in MoE. This is implemented by +# padding each expert size to the next multiple of TOKEN_GROUP_ALIGN_SIZE_M. + +# Valid values are: 8, 16, or 32. +# Different values are needed for different cases: + +# * For bf16, 8 is enough (16 byte alignment / 2 bytes per elem = 8 elements). +# * For fp8, 16 byte alignment / 1 byte per elem = 16 elements. +# * For mxfp8, we need 32 (or block_size) because scaling block size is (1 x 32), +# so when doing per-token-group quantization on each logically distinct subtensor, +# we need to ensure the contracting dim is divisible by block_size. +# In the backward pass, grad_weight = (grad_output_t @ input).t() has gemm dims +# of (N, M) @ (M, K) so M is the contracting dim, and group offsets are along M, +# so we need 32 element alignment. +TOKEN_GROUP_ALIGN_SIZE_M = 16 +ValidTokenGroupAlignmentSize = Literal[8, 16, 32] + + +def _permute( + x: torch.Tensor, + num_tokens_per_expert: int, + num_experts: int, + alignment: int = TOKEN_GROUP_ALIGN_SIZE_M, +): + x_padded_size = x.shape[0] + num_experts * alignment + padded_max_len = ((x_padded_size + alignment - 1) // alignment) * alignment + + with torch.no_grad(): + ( + permuted_indices, + padded_num_tokens_per_expert, + ) = _generate_permute_indices( + num_tokens_per_expert=num_tokens_per_expert, + num_experts=num_experts, + max_len=padded_max_len, + alignment=alignment, + ) + + x = torch.vstack((x, x.new_zeros(x.shape[-1]))) + input_shape = x.shape + x = x[permuted_indices, :] + + return input_shape, x, permuted_indices, padded_num_tokens_per_expert + + +def _unpermute(out, input_shape, permuted_indices): + out_unpermuted = out.new_empty(input_shape) + out_unpermuted[permuted_indices, :] = out + return out_unpermuted[:-1] + + +def indices_padding_wrapper(func: Callable) -> Callable: + """ + In order to use torch._grouped_mm, we need to make sure the number of + tokens each expert gets is a multiple of TOKEN_GROUP_ALIGN_SIZE_M. The + generate_permute_indices kernel also helps achieve this via padding, + without incurring synchronization between device and host. + """ + + def wrapper( + gate_up_proj: torch.Tensor, + down_proj: torch.Tensor, + act_fn: Callable[[torch.Tensor], torch.Tensor], + x: torch.Tensor, + num_tokens_per_expert: torch.Tensor, + ) -> torch.Tensor: + num_experts = num_tokens_per_expert.shape[0] + + input_shape, x, permuted_indices, padded_num_tokens_per_expert = _permute(x, num_tokens_per_expert, num_experts) + + out = func(gate_up_proj, down_proj, act_fn, x, padded_num_tokens_per_expert) + + out = _unpermute(out, input_shape, permuted_indices) + return out + + return wrapper + + +@triton.jit +def _fill_indices_kernel( + num_tokens_per_expert_ptr, + start_index_values_ptr, + write_offsets_ptr, + output_ptr, + num_experts: tl.constexpr, + BLOCK_SIZE: tl.constexpr, # Number of threads per block +): + pid = tl.program_id(axis=0) + num_programs = tl.num_programs(axis=0) + + # map programs (blocks) to the experts and loop (grid stride) if needed + for expert_id in range(pid, num_experts, num_programs): + # read this experts write offset + write_offset = tl.load(write_offsets_ptr + expert_id) + + # load number of tokens for this expert + start_index = tl.load(start_index_values_ptr + expert_id) + length = tl.load(num_tokens_per_expert_ptr + expert_id) + + # each thread in block processes tokens in parallel + offsets = tl.arange(0, BLOCK_SIZE) + + # tokens are processed in chunks of BLOCK_SIZE + for chunk_start in range(0, length, BLOCK_SIZE): + chunk_offsets = chunk_start + offsets + + # mask valid indices + mask = chunk_offsets < length + + values = start_index + chunk_offsets + + # destination + dest_indices = write_offset + chunk_offsets + + # store + tl.store(output_ptr + dest_indices, values, mask=mask) + + +def _fill_indices_wrapper( + num_tokens_per_expert: torch.Tensor, + start_index_values: torch.Tensor, + write_offsets: torch.Tensor, + num_experts: int, + max_len: int, + block_size: int = 128, + max_blocks: int = 1024, # cap on total number of blocks to launch +): + # preallocate output + permuted_indices = torch.full((max_len,), -1, dtype=torch.int32, device=num_tokens_per_expert.device) + + # write offsets is per local expert... + num_blocks = min(num_experts, max_blocks) + # grid = one block per expert unless capped and then we loop... + grid = (num_blocks,) + + # launch kernel + _fill_indices_kernel[grid]( + num_tokens_per_expert, + start_index_values, + write_offsets, + permuted_indices, + num_experts, + BLOCK_SIZE=block_size, + ) + return permuted_indices + + +def _generate_permute_indices( + num_tokens_per_expert: torch.Tensor, + num_experts: int, + max_len: int, + alignment: int, +): + """ + Prepare permutation indices and the number of tokens for each expert. + + Args: + num_tokens_per_expert: number of tokens for each expert. + num_experts: number of experts. + max_len: maximum length of the output index vector. + alignment: alignment for each returned element in `m_sizes` and padding min for zero token experts. + + Returns: + permuted_indices: Tensor of indices that map original token order to the expert-grouped order. + m_sizes: aligned number of tokens for each expert (padded to alignment boundary). + m_offsets: Cumulative sum of m_sizes. The exclusive ending position for each expert's tokens. + + Explanatory details: + `tokens_per_expert_group` is of shape (num_ranks * experts_per_rank,), for example: + From: | rank 0 | rank 1 | + To: | E0 | E1 | E2 | E3 | E0 | E1 | E2 | E3 | + | 4 | 2 | 1 | 3 | 1 | 2 | 3 | 4 | + """ + start_index_values = torch.cumsum(num_tokens_per_expert, dim=0) - num_tokens_per_expert + + # pad out empty experts to alignment requirement + m_sizes = torch.clamp_min(num_tokens_per_expert, alignment) + + # align the chunk sizes (cdiv) + m_sizes = (m_sizes.to(torch.int32) + alignment - 1) // alignment * alignment + + # additional prefix sum to get write offset of each expert in permuted_indices + # write offsets is per local expert, not global + write_offsets = torch.cumsum(m_sizes, dim=0) - m_sizes + + # Select the implementation to use + permuted_indices = _fill_indices_wrapper( + num_tokens_per_expert=num_tokens_per_expert, + start_index_values=start_index_values, + write_offsets=write_offsets, + num_experts=num_experts, + max_len=max_len, + ) + + return permuted_indices, m_sizes diff --git a/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/qwen3_vl_moe.py b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/qwen3_vl_moe.py new file mode 100644 index 00000000..eb891044 --- /dev/null +++ b/cosmos_training/cosmos/model/vfm/vlm/qwen3_vl_moe/qwen3_vl_moe.py @@ -0,0 +1,2041 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from dataclasses import dataclass +from typing import Any, Callable, NamedTuple, Optional, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache, DynamicCache +from transformers.generation import GenerationMixin +from transformers.integrations import use_kernel_forward_from_hub +from transformers.masking_utils import create_causal_mask +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_layers import GradientCheckpointingLayer +from transformers.modeling_outputs import BaseModelOutputWithPast, ModelOutput +from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.processing_utils import Unpack +from transformers.utils import TransformersKwargs, is_torchdynamo_compiling +from transformers.utils.deprecation import deprecate_kwarg + +from cosmos.model.vfm.vlm.qwen3_vl_moe.configuration_qwen3_vl_moe import ( + Qwen3VLMoeConfig, + Qwen3VLMoeTextConfig, + Qwen3VLMoeVisionConfig, +) +from cosmos.model.vfm.vlm.qwen3_vl_moe.moe import ( + create_text_experts, +) + +# Small additive constant to prevent log(0) in router entropy computation. +ENTROPY_EPSILON = 1e-9 + + +# Avoid torch.combinations here: during FSDP/lazy init this module can be built +# under a meta-device context, and torch.combinations internally calls +# masked_select, which does not have a meta kernel. +def _make_coactivation_pairs(top_k: int, device: torch.device | str | None = None) -> torch.Tensor: + target_device = torch.device(device) if device is not None else torch.device("cpu") + if target_device.type == "meta": + target_device = torch.device("cpu") + + pairs = [(i, j) for i in range(top_k) for j in range(i + 1, top_k)] + if not pairs: + return torch.empty((0, 2), dtype=torch.long, device=target_device) + return torch.tensor(pairs, dtype=torch.long, device=target_device) + + +# We need to use namedtuple instead of dataclass because it is picklable. +class LBLMetadata(NamedTuple): + """Metadata for load balancing loss computation.""" + + # The number of tokens routed to each expert for this rank. + num_tokens_per_expert: torch.Tensor + + # The total number of tokens in the batch. + num_tokens: torch.Tensor + + # The average probability of routing to each expert for this rank. + mean_router_prob_per_expert: torch.Tensor + + +@use_kernel_forward_from_hub("RMSNorm") +class Qwen3VLMoeTextRMSNorm(nn.Module): + def __init__(self, hidden_size, eps=1e-6): + """ + Qwen3VLMoeTextRMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states): + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + def extra_repr(self): + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Qwen3VLMoeTextSparseMoeBlock(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.num_experts = config.num_experts + self.top_k = config.num_experts_per_tok + self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False) + self.experts = create_text_experts(config, implementation_type="grouped_mm") + + # ── Heatmap tracking ────────────────────────────────────────────────────── + # Token counts read and reset by ExpertHeatmap on its own schedule. + # persistent=False so these are never saved to checkpoints. + self.register_buffer( + "total_tokens_per_expert", + torch.zeros(config.num_experts, dtype=torch.int64), + persistent=False, + ) + self.register_buffer( + "total_tokens", + torch.zeros(1, dtype=torch.int64), + persistent=False, + ) + + # ── Stability tracking ─────────────────────────────────────────────────── + # Separate token-count buffers owned and reset by MoEStabilityCallback, + # so it is fully independent of ExpertHeatmap's reset cycle. + self.register_buffer( + "stability_tokens_per_expert", + torch.zeros(config.num_experts, dtype=torch.int64), + persistent=False, + ) + self.register_buffer( + "stability_total_tokens", + torch.zeros(1, dtype=torch.int64), + persistent=False, + ) + # Sum of per-token router entropy H = -sum(p_i * log p_i) across all tokens + # seen since the last reset. Divided by stability_total_tokens in the + # callback to get the mean entropy, then normalized by log(N) for [0, 1]. + # float64 to avoid precision loss when accumulating over many steps. + self.register_buffer( + "sum_token_entropy", + torch.zeros(1, dtype=torch.float64), + persistent=False, + ) + + # ── Specialization tracking ─────────────────────────────────────────────── + # N×N symmetric matrix counting how often each expert pair (i, j) appears + # together in the top-K selection for the same token. Only the upper triangle + # (i < j) is written; read and reset by MoESpecializationCallback. + self.register_buffer( + "coactivation_counts", + torch.zeros(config.num_experts, config.num_experts, dtype=torch.int64), + persistent=False, + ) + # Precomputed C(top_k, 2) slot-index pairs used by the co-activation counting + # kernel in forward(). Registered as a buffer so it moves to the correct device + # with the module; persistent=False since it's derived from config constants. + self.register_buffer( + "_coact_pairs", + _make_coactivation_pairs(config.num_experts_per_tok), + persistent=False, + ) + + def _update_moe_callback_stats( + self, + num_tokens_per_expert: torch.Tensor, + num_tokens: torch.Tensor, + routing_weights: torch.Tensor, + expert_indices: torch.Tensor, + ) -> None: + # ── Heatmap + stability buffers ────────────────────────────────────── + # Accumulate into both buffer sets so each callback can reset independently. + self.total_tokens_per_expert.add_(num_tokens_per_expert) + self.total_tokens.add_(num_tokens) + self.stability_tokens_per_expert.add_(num_tokens_per_expert) + self.stability_total_tokens.add_(num_tokens) + + # Per-token router entropy H_t = -sum_i p_i * log(p_i). + # Summed (not meaned) so the callback can normalize by any window length. + # 1e-9 prevents log(0) for near-zero probabilities. + token_entropy = -torch.sum( + routing_weights * torch.log(routing_weights + ENTROPY_EPSILON), dim=-1 + ) # [num_tokens] + self.sum_token_entropy.add_(token_entropy.sum().to(torch.float64)) + + # ── Co-activation counting ──────────────────────────────────────────── + # For every ordered pair (k1, k2) of top-K slots with k1 < k2, find the + # expert assigned to each slot and increment coactivation_counts[i, j] + # where i = min(expert_k1, expert_k2), j = max(...) to keep counts in the + # upper triangle only (avoids double-counting the pair). + # Vectorized over all C(K,2) pairs in one scatter_add_ call to avoid + # C(K,2) separate kernel launches (28 for top_k=8). + # _coact_pairs: [C(K,2), 2] — precomputed slot index pairs (k1, k2) with k1 < k2 + e1 = expert_indices[:, self._coact_pairs[:, 0]] # [num_tokens, C(K,2)] + e2 = expert_indices[:, self._coact_pairs[:, 1]] # [num_tokens, C(K,2)] + lo = torch.minimum(e1, e2) + hi = torch.maximum(e1, e2) + flat_idx = (lo * self.num_experts + hi).to(torch.int64) # [num_tokens, C(K,2)] + flat_counts = torch.zeros( + self.num_experts * self.num_experts, + dtype=self.coactivation_counts.dtype, + device=self.coactivation_counts.device, + ) + flat_idx = flat_idx.reshape(-1) + flat_counts.scatter_add_(0, flat_idx, torch.ones_like(flat_idx, dtype=flat_counts.dtype)) + self.coactivation_counts.view(-1).add_(flat_counts) + + def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, LBLMetadata]: + """ + This function performs the MoE computation, including routing, dispatch, GEMMs and combine. + + Args: + hidden_states (torch.Tensor): (num_tokens, hidden_size) + + Returns: + torch.Tensor: (num_tokens, hidden_size) + - routed_out: Output of the MoE computation. + LBLMetadata: Load balancing loss metadata. + """ + assert hidden_states.ndim == 2, "hidden_states must be of shape (num_tokens, hidden_size)" + num_tokens = hidden_states.shape[0] + + router_logits = self.gate(hidden_states) # [num_tokens,num_experts] + routing_weights = torch.nn.functional.softmax( + router_logits, dim=-1, dtype=torch.float32 + ) # [num_tokens,num_experts] + expert_weights, expert_indices = torch.topk(routing_weights, self.top_k, dim=-1) + # expert_weights: [num_tokens,top_k], expert_indices: [num_tokens,top_k] + + expert_weights = expert_weights / expert_weights.sum(dim=-1, keepdim=True) # [num_tokens,top_k] + expert_weights = expert_weights.to(hidden_states.dtype) # [num_tokens,top_k] + + num_tokens_per_expert = torch.histc( + expert_indices.to(dtype=torch.int32).view(-1), + bins=self.num_experts, + min=0, + max=self.num_experts - 1, + ) # [num_experts] + + routed_out = self.experts( + hidden_states=hidden_states, + topk_scores=expert_weights, + expert_indices=expert_indices, + num_tokens_per_expert=num_tokens_per_expert, + ) # [num_tokens,hidden_size] + + num_tokens_per_expert = num_tokens_per_expert.to(dtype=torch.int64) # [num_experts] + num_tokens = torch.tensor( + [num_tokens], + dtype=torch.int64, + device=num_tokens_per_expert.device, + ) # [1] + + # Compute the average probability of routing to these experts. + # Summing over all experts should be equal to 1. + mean_router_prob_per_expert = torch.mean(routing_weights, dim=0) # [num_experts] + + lbl_metadata = LBLMetadata( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + mean_router_prob_per_expert=mean_router_prob_per_expert, + ) + + with torch.no_grad(): + self._update_moe_callback_stats( + num_tokens_per_expert=num_tokens_per_expert, + num_tokens=num_tokens, + routing_weights=routing_weights, + expert_indices=expert_indices, + ) + + return routed_out, lbl_metadata + + def get_total_tokens_per_expert(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + total_tokens = self.total_tokens_per_expert.detach().clone() + if reset: + self.total_tokens_per_expert.zero_() + return total_tokens + + def get_total_tokens(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + total_tokens = self.total_tokens.detach().clone() + if reset: + self.total_tokens.zero_() + return total_tokens + + def get_stability_tokens_per_expert(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.stability_tokens_per_expert.detach().clone() + if reset: + self.stability_tokens_per_expert.zero_() + return val + + def get_stability_total_tokens(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.stability_total_tokens.detach().clone() + if reset: + self.stability_total_tokens.zero_() + return val + + def get_sum_token_entropy(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.sum_token_entropy.detach().clone() + if reset: + self.sum_token_entropy.zero_() + return val + + def get_coactivation_counts(self, reset: bool = True) -> torch.Tensor: + with torch.no_grad(): + val = self.coactivation_counts.detach().clone() + if reset: + self.coactivation_counts.zero_() + return val + + def init_weights(self, buffer_device: torch.device | None = None): + self.register_buffer( + "total_tokens_per_expert", + torch.zeros(self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "total_tokens", + torch.zeros(1, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "stability_tokens_per_expert", + torch.zeros(self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "stability_total_tokens", + torch.zeros(1, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "sum_token_entropy", + torch.zeros(1, dtype=torch.float64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "coactivation_counts", + torch.zeros(self.num_experts, self.num_experts, dtype=torch.int64, device=buffer_device), + persistent=False, + ) + self.register_buffer( + "_coact_pairs", + _make_coactivation_pairs(self.top_k, device=buffer_device), + persistent=False, + ) + + if hasattr(self.config, "initializer_range"): + std = self.config.initializer_range + else: + std = getattr(self.config.get_text_config(), "initializer_range", 0.02) + + nn.init.normal_(self.gate.weight, mean=0.0, std=std) + nn.init.normal_(self.experts.gate_up_proj, mean=0.0, std=std) + nn.init.normal_(self.experts.down_proj, mean=0.0, std=std) + + +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) # [B,num_kv_heads,n_rep,N,head_dim] + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) # [B,num_heads,N,head_dim] + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, # [B,num_heads,N,head_dim] + key: torch.Tensor, # [B,num_kv_heads,N,head_dim] + value: torch.Tensor, # [B,num_kv_heads,N,head_dim] + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs: Unpack[TransformersKwargs], +): + key_states = repeat_kv(key, module.num_key_value_groups) # [B,num_heads,N,head_dim] + value_states = repeat_kv(value, module.num_key_value_groups) # [B,num_heads,N,head_dim] + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling # [B,num_heads,N,N] + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] + attn_weights = attn_weights + causal_mask # [B,num_heads,N,N] + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) # [B,num_heads,N,N] + attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training) + attn_output = torch.matmul(attn_weights, value_states) # [B,num_heads,N,head_dim] + attn_output = attn_output.transpose(1, 2).contiguous() # [B,N,num_heads,head_dim] + + return attn_output, attn_weights + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + position_ids (`torch.Tensor`, *optional*): + Deprecated and unused. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + sin = sin.unsqueeze(unsqueeze_dim) # [B,1,N,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [B,num_heads,N,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [B,num_kv_heads,N,head_dim] + return q_embed, k_embed + + +class Qwen3VLMoeTextAttention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, config: Qwen3VLMoeTextConfig, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = True + + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias + ) + self.q_norm = Qwen3VLMoeTextRMSNorm( + self.head_dim, eps=config.rms_norm_eps + ) # unlike olmo, only on the head dim! + self.k_norm = Qwen3VLMoeTextRMSNorm( + self.head_dim, eps=config.rms_norm_eps + ) # thus post q_norm does not need reshape + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_heads,N,head_dim] + key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose( + 1, 2 + ) # [B,num_kv_heads,N,head_dim] + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) # [B,num_kv_heads,N,head_dim] + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + # query_states: [B,num_heads,N,head_dim], key_states: [B,num_kv_heads,N,head_dim] + + if past_key_values is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs) + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + **kwargs, + ) + # attn_output: [B,N,num_heads,head_dim] + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() # [B,N,hidden_size] + attn_output = self.o_proj(attn_output) # [B,N,hidden_size] + return attn_output, attn_weights + + +class Qwen3VLMoeTextMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj + + +class Qwen3VLMoeTextDecoderLayer(GradientCheckpointingLayer): + def __init__(self, config: Qwen3VLMoeTextConfig, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + + self.self_attn = Qwen3VLMoeTextAttention(config, layer_idx) + + if (layer_idx not in config.mlp_only_layers) and ( + config.num_experts > 0 and (layer_idx + 1) % config.decoder_sparse_step == 0 + ): + self.mlp = Qwen3VLMoeTextSparseMoeBlock(config) + else: + self.mlp = Qwen3VLMoeTextMLP(config) + + self.input_layernorm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + + @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58") + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> torch.FloatTensor: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(batch * seq_len, embed_dim)` + attention_mask (`torch.FloatTensor`, *optional*): attention mask of size + `(batch, sequence_length)` where padding elements are indicated by 0. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + output_router_logits (`bool`, *optional*): + Whether or not to return the logits of all the routers. They are useful for computing the router loss, + and should not be returned during inference. + use_cache (`bool`, *optional*): + If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding + (see `past_key_values`). + past_key_values (`Cache`, *optional*): cached past key and value projection states + cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*): + Indices depicting the position of the input sequence tokens in the sequence. + position_embeddings (`tuple[torch.FloatTensor, torch.FloatTensor]`, *optional*): + Tuple containing the cosine and sine positional embeddings of shape `(batch_size, seq_len, head_dim)`, + with `head_dim` being the embedding dimension of each attention head. + kwargs (`dict`, *optional*): + Arbitrary kwargs to be ignored, used for FSDP and other methods that injects code + into the model + + Returns: + torch.Tensor: (batch_size * seq_len, hidden_size) + """ + residual = hidden_states + + hidden_states = self.input_layernorm(hidden_states) + + # Self Attention + hidden_states, _ = self.self_attn( + hidden_states=hidden_states, + position_embeddings=position_embeddings, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + **kwargs, + ) + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + return hidden_states + + +class Qwen3VLMoePreTrainedModel(PreTrainedModel): + config: Qwen3VLMoeConfig + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer", "Qwen3VLMoeVisionBlock"] + _skip_keys_device_placement = ["past_key_values"] + _supports_flash_attn = True + _supports_sdpa = True + _supports_flex_attn = True + _can_compile_fullgraph = False # MoE models don't work with torch.compile (`torch.where(condition)` not supported) + _supports_attention_backend = True + _can_record_outputs = { + "hidden_states": Qwen3VLMoeTextDecoderLayer, + "attentions": Qwen3VLMoeTextAttention, + } + + def _init_weights(self, module: nn.Module, buffer_device: torch.device | None): + """Initialize the weights.""" + super()._init_weights(module) + + if isinstance(module, Qwen3VLMoeTextSparseMoeBlock): + module.init_weights(buffer_device=buffer_device) + elif isinstance(module, Qwen3VLMoeTextRotaryEmbedding): + module.init_weights(buffer_device=buffer_device) + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + self.apply(functools.partial(self._init_weights, buffer_device=buffer_device)) + + +class Qwen3VLMoeVisionMLP(nn.Module): + def __init__(self, config): + super().__init__() + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.linear_fc1 = nn.Linear(self.hidden_size, self.intermediate_size, bias=True) + self.linear_fc2 = nn.Linear(self.intermediate_size, self.hidden_size, bias=True) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, hidden_state): + return self.linear_fc2(self.act_fn(self.linear_fc1(hidden_state))) + + +class Qwen3VLMoeVisionPatchEmbed(nn.Module): + def __init__(self, config) -> None: + super().__init__() + self.patch_size = config.patch_size + self.temporal_patch_size = config.temporal_patch_size + self.in_channels = config.in_channels + self.embed_dim = config.hidden_size + + kernel_size = [self.temporal_patch_size, self.patch_size, self.patch_size] + self.proj = nn.Conv3d(self.in_channels, self.embed_dim, kernel_size=kernel_size, stride=kernel_size, bias=True) + + def forward( + self, hidden_states: torch.Tensor + ) -> torch.Tensor: # hidden_states: [N_patches,in_channels*temporal_patch_size*patch_size*patch_size] + target_dtype = self.proj.weight.dtype + hidden_states = hidden_states.view( + -1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size + ) # [N_patches,in_channels,temporal_patch_size,patch_size,patch_size] + hidden_states = self.proj(hidden_states.to(dtype=target_dtype)).view( + -1, self.embed_dim + ) # [N_patches,embed_dim] + return hidden_states + + +class Qwen3VLMoeVisionRotaryEmbedding(nn.Module): + def __init__(self, dim: int, theta: float = 10000.0) -> None: + super().__init__() + inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim)) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def forward(self, seqlen: int) -> torch.Tensor: + seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype) # [seqlen] + freqs = torch.outer(seq, self.inv_freq) # [seqlen,dim//2] + return freqs # [seqlen,dim//2] + + +class Qwen3VLMoeVisionPatchMerger(nn.Module): + def __init__(self, config: Qwen3VLMoeVisionConfig, use_postshuffle_norm=False) -> None: + super().__init__() + self.hidden_size = config.hidden_size * (config.spatial_merge_size**2) + self.use_postshuffle_norm = use_postshuffle_norm + self.norm = nn.LayerNorm(self.hidden_size if use_postshuffle_norm else config.hidden_size, eps=1e-6) + self.linear_fc1 = nn.Linear(self.hidden_size, self.hidden_size) + self.act_fn = nn.GELU() + self.linear_fc2 = nn.Linear(self.hidden_size, config.out_hidden_size) + + def forward(self, x: torch.Tensor) -> torch.Tensor: # x: [N_patches,hidden_size] (before merge) + x = self.norm(x.view(-1, self.hidden_size) if self.use_postshuffle_norm else x).view( + -1, self.hidden_size + ) # [N_merged,merged_hidden_size] + x = self.linear_fc2(self.act_fn(self.linear_fc1(x))) # [N_merged,out_hidden_size] + return x + + +def apply_rotary_pos_emb_vision( + q: torch.Tensor, # [N,num_heads,head_dim] + k: torch.Tensor, # [N,num_heads,head_dim] + cos: torch.Tensor, # [N,head_dim] + sin: torch.Tensor, # [N,head_dim] +) -> tuple[torch.Tensor, torch.Tensor]: + orig_q_dtype = q.dtype + orig_k_dtype = k.dtype + q, k = q.float(), k.float() + cos, sin = cos.unsqueeze(-2).float(), sin.unsqueeze(-2).float() # [N,1,head_dim] + q_embed = (q * cos) + (rotate_half(q) * sin) # [N,num_heads,head_dim] + k_embed = (k * cos) + (rotate_half(k) * sin) # [N,num_heads,head_dim] + q_embed = q_embed.to(orig_q_dtype) + k_embed = k_embed.to(orig_k_dtype) + return q_embed, k_embed + + +class Qwen3VLMoeVisionAttention(nn.Module): + def __init__(self, config: Qwen3VLMoeVisionConfig) -> None: + super().__init__() + self.dim = config.hidden_size + self.num_heads = config.num_heads + self.head_dim = self.dim // self.num_heads + self.num_key_value_groups = 1 # needed for eager attention + self.qkv = nn.Linear(self.dim, self.dim * 3, bias=True) + self.proj = nn.Linear(self.dim, self.dim) + self.scaling = self.head_dim**-0.5 + self.config = config + self.attention_dropout = 0.0 + self.is_causal = False + + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: + seq_length = hidden_states.shape[0] + query_states, key_states, value_states = ( + self.qkv(hidden_states).reshape(seq_length, 3, self.num_heads, -1).permute(1, 0, 2, 3).unbind(0) + ) + # query_states, key_states, value_states: [N,num_heads,head_dim] + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb_vision(query_states, key_states, cos, sin) + # query_states, key_states: [N,num_heads,head_dim] + + query_states = query_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + key_states = key_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + value_states = value_states.transpose(0, 1).unsqueeze(0) # [1,num_heads,N,head_dim] + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + + if self.config._attn_implementation == "flash_attention_2": + # Flash Attention 2: Use cu_seqlens for variable length attention + max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max() + attn_output, _ = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + cu_seq_lens_q=cu_seqlens, + cu_seq_lens_k=cu_seqlens, + max_length_q=max_seqlen, + max_length_k=max_seqlen, + is_causal=False, + **kwargs, + ) + else: + # Other implementations: Process each chunk separately + lengths = cu_seqlens[1:] - cu_seqlens[:-1] + splits = [ + torch.split(tensor, lengths.tolist(), dim=2) for tensor in (query_states, key_states, value_states) + ] + + attn_outputs = [ + attention_interface( + self, + q, + k, + v, + attention_mask=None, + scaling=self.scaling, + dropout=0.0 if not self.training else self.attention_dropout, + is_causal=False, + **kwargs, + )[0] + for q, k, v in zip(*splits) + ] + attn_output = torch.cat(attn_outputs, dim=1) # [1,N,num_heads,head_dim] + + attn_output = attn_output.reshape(seq_length, -1).contiguous() # [N,hidden_size] + attn_output = self.proj(attn_output) # [N,hidden_size] + return attn_output + + +class Qwen3VLMoeVisionBlock(GradientCheckpointingLayer): + def __init__(self, config, attn_implementation: str = "sdpa") -> None: + super().__init__() + self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6) + self.attn = Qwen3VLMoeVisionAttention(config=config) + self.mlp = Qwen3VLMoeVisionMLP(config=config) + + def forward( + self, + hidden_states: torch.Tensor, + cu_seqlens: torch.Tensor, + rotary_pos_emb: Optional[torch.Tensor] = None, + position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, + **kwargs, + ) -> torch.Tensor: + hidden_states = hidden_states + self.attn( + self.norm1(hidden_states), + cu_seqlens=cu_seqlens, + rotary_pos_emb=rotary_pos_emb, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = hidden_states + self.mlp(self.norm2(hidden_states)) + return hidden_states + + +class Qwen3VLMoeVisionModel(Qwen3VLMoePreTrainedModel): + config: Qwen3VLMoeVisionConfig + _no_split_modules = ["Qwen3VLMoeVisionBlock"] + + def __init__(self, config, *inputs, **kwargs) -> None: + super().__init__(config, *inputs, **kwargs) + self.spatial_merge_size = config.spatial_merge_size + self.patch_size = config.patch_size + self.spatial_merge_unit = self.spatial_merge_size * self.spatial_merge_size + + self.patch_embed = Qwen3VLMoeVisionPatchEmbed( + config=config, + ) + + self.pos_embed = nn.Embedding(config.num_position_embeddings, config.hidden_size) + self.num_grid_per_side = int(config.num_position_embeddings**0.5) + + head_dim = config.hidden_size // config.num_heads + self.rotary_pos_emb = Qwen3VLMoeVisionRotaryEmbedding(head_dim // 2) + + self.blocks = nn.ModuleList([Qwen3VLMoeVisionBlock(config) for _ in range(config.depth)]) + self.merger = Qwen3VLMoeVisionPatchMerger( + config=config, + use_postshuffle_norm=False, + ) + + self.deepstack_visual_indexes = config.deepstack_visual_indexes + self.deepstack_merger_list = nn.ModuleList( + [ + Qwen3VLMoeVisionPatchMerger( + config=config, + use_postshuffle_norm=True, + ) + for _ in range(len(config.deepstack_visual_indexes)) + ] + ) + + self.gradient_checkpointing = False + + def rot_pos_emb(self, grid_thw: torch.Tensor) -> torch.Tensor: + merge_size = self.spatial_merge_size + + max_hw = int(grid_thw[:, 1:].max().item()) + freq_table = self.rotary_pos_emb(max_hw) # [max_hw,head_dim//4] + device = freq_table.device + + total_tokens = int(torch.prod(grid_thw, dim=1).sum().item()) + pos_ids = torch.empty((total_tokens, 2), dtype=torch.long, device=device) # [total_tokens,2] + + offset = 0 + for num_frames, height, width in grid_thw: + merged_h, merged_w = height // merge_size, width // merge_size + + block_rows = torch.arange(merged_h, device=device) # block row indices + block_cols = torch.arange(merged_w, device=device) # block col indices + intra_row = torch.arange(merge_size, device=device) # intra-block row offsets + intra_col = torch.arange(merge_size, device=device) # intra-block col offsets + + # Compute full-resolution positions + row_idx = block_rows[:, None, None, None] * merge_size + intra_row[None, None, :, None] + col_idx = block_cols[None, :, None, None] * merge_size + intra_col[None, None, None, :] + + row_idx = row_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + col_idx = col_idx.expand(merged_h, merged_w, merge_size, merge_size).reshape(-1) # [H*W] + + coords = torch.stack((row_idx, col_idx), dim=-1) # [H*W,2] + + if num_frames > 1: + coords = coords.repeat(num_frames, 1) # [T*H*W,2] + + num_tokens = coords.shape[0] + pos_ids[offset : offset + num_tokens] = coords + offset += num_tokens + + embeddings = freq_table[pos_ids] # [total_tokens,2,head_dim//4] + embeddings = embeddings.flatten(1) # [total_tokens,head_dim//2] + return embeddings + + def fast_pos_embed_interpolate(self, grid_thw): + grid_ts, grid_hs, grid_ws = grid_thw[:, 0], grid_thw[:, 1], grid_thw[:, 2] + + idx_list = [[] for _ in range(4)] + weight_list = [[] for _ in range(4)] + + for t, h, w in zip(grid_ts, grid_hs, grid_ws): + h_idxs = torch.linspace(0, self.num_grid_per_side - 1, h) + w_idxs = torch.linspace(0, self.num_grid_per_side - 1, w) + + h_idxs_floor = h_idxs.int() + w_idxs_floor = w_idxs.int() + h_idxs_ceil = (h_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + w_idxs_ceil = (w_idxs.int() + 1).clip(max=self.num_grid_per_side - 1) + + dh = h_idxs - h_idxs_floor + dw = w_idxs - w_idxs_floor + + base_h = h_idxs_floor * self.num_grid_per_side + base_h_ceil = h_idxs_ceil * self.num_grid_per_side + + indices = [ + (base_h[None].T + w_idxs_floor[None]).flatten(), + (base_h[None].T + w_idxs_ceil[None]).flatten(), + (base_h_ceil[None].T + w_idxs_floor[None]).flatten(), + (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(), + ] + + weights = [ + ((1 - dh)[None].T * (1 - dw)[None]).flatten(), + ((1 - dh)[None].T * dw[None]).flatten(), + (dh[None].T * (1 - dw)[None]).flatten(), + (dh[None].T * dw[None]).flatten(), + ] + + for i in range(4): + idx_list[i].extend(indices[i].tolist()) + weight_list[i].extend(weights[i].tolist()) + + idx_tensor = torch.tensor(idx_list, dtype=torch.long, device=self.pos_embed.weight.device) # [4,total_patches] + weight_tensor = torch.tensor( + weight_list, dtype=self.pos_embed.weight.dtype, device=self.pos_embed.weight.device + ) # [4,total_patches] + pos_embeds = self.pos_embed(idx_tensor) * weight_tensor[:, :, None] # [4,total_patches,hidden_size] + patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3] # [total_patches,hidden_size] + + patch_pos_embeds = patch_pos_embeds.split([h * w for h, w in zip(grid_hs, grid_ws)]) + + patch_pos_embeds_permute = [] + merge_size = self.config.spatial_merge_size + for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws): + pos_embed = pos_embed.repeat(t, 1) # [T*H*W,hidden_size] + pos_embed = ( + pos_embed.view(t, h // merge_size, merge_size, w // merge_size, merge_size, -1) + # [T,H//merge,merge,W//merge,merge,hidden_size] + .permute(0, 1, 3, 2, 4, 5) + # [T,H//merge,W//merge,merge,merge,hidden_size] + .flatten(0, 4) + # [T*H//merge*W//merge*merge*merge,hidden_size] = [T*H*W,hidden_size] + ) + patch_pos_embeds_permute.append(pos_embed) + patch_pos_embeds = torch.cat(patch_pos_embeds_permute) # [total_patches,hidden_size] + return patch_pos_embeds + + def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, **kwargs) -> torch.Tensor: + """ + Args: + hidden_states (`torch.Tensor` of shape `(seq_len, hidden_size)`): + The final hidden states of the model. + grid_thw (`torch.Tensor` of shape `(num_images_or_videos, 3)`): + The temporal, height and width of feature shape of each image in LLM. + + Returns: + `torch.Tensor`: hidden_states. + """ + hidden_states = self.patch_embed(hidden_states) # [total_patches,embed_dim] + + pos_embeds = self.fast_pos_embed_interpolate(grid_thw) # [total_patches,hidden_size] + hidden_states = hidden_states + pos_embeds # [total_patches,hidden_size] + + rotary_pos_emb = self.rot_pos_emb(grid_thw) # [total_patches,head_dim//2] + + seq_len, _ = hidden_states.size() + hidden_states = hidden_states.reshape(seq_len, -1) # [total_patches,hidden_size] + rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1) # [total_patches,head_dim//2] + emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1) # [total_patches,head_dim] + position_embeddings = (emb.cos(), emb.sin()) # 2x [total_patches,head_dim] + + cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum( + dim=0, + # Select dtype based on the following factors: + # - FA2 requires that cu_seqlens_q must have dtype int32 + # - torch.onnx.export requires that cu_seqlens_q must have same dtype as grid_thw + # See https://github.com/huggingface/transformers/pull/34852 for more information + dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32, + ) + cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0) + + deepstack_feature_lists = [] + for layer_num, blk in enumerate(self.blocks): + hidden_states = blk( + hidden_states, + cu_seqlens=cu_seqlens, + position_embeddings=position_embeddings, + **kwargs, + ) + if layer_num in self.deepstack_visual_indexes: + deepstack_feature = self.deepstack_merger_list[self.deepstack_visual_indexes.index(layer_num)]( + hidden_states + ) + deepstack_feature_lists.append(deepstack_feature) + + hidden_states = self.merger(hidden_states) + + return hidden_states, deepstack_feature_lists + + +class Qwen3VLMoeTextRotaryEmbedding(nn.Module): + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__() + if hasattr(config, "rope_scaling") and config.rope_scaling is not None: + self.rope_type = config.rope_scaling.get("rope_type", "default") + else: + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + self.mrope_section = config.rope_scaling.get("mrope_section", [24, 20, 20]) + + self.config = config + self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + + def init_weights(self, buffer_device: torch.device | None = None) -> None: + inv_freq, self.attention_scaling = self.rope_init_fn(self.config, buffer_device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def apply_interleaved_mrope(self, freqs, mrope_section): + """Apply interleaved MRoPE to 3D rotary embeddings. + Reorganizes frequency layout from chunked [TTT...HHH...WWW] to + interleaved [THTHWHTHW...TT], preserving frequency continuity. + args: + x: (3, bs, seq_len, head_dim // 2) + mrope_section: (3,) + returns: + x_t: (bs, seq_len, head_dim // 2) + """ + freqs_t = freqs[0] # just overwrite the first dimension T + for dim, offset in enumerate((1, 2), start=1): # H, W + length = mrope_section[dim] * 3 + idx = slice(offset, length, 3) + freqs_t[..., idx] = freqs[dim, ..., idx] + return freqs_t + + @torch.no_grad() + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward(self, x, position_ids): + assert self.inv_freq.dtype == torch.float32, f"inv_freq must be float32, but got {self.inv_freq.dtype}" + + # In contrast to other models, Qwen3VLMoe has different position ids for the grids + # So we expand the inv_freq to shape (3, ...) + if position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) # [3,B,N] + inv_freq_expanded = ( + self.inv_freq[None, None, :, None].float().expand(3, position_ids.shape[1], -1, 1) + ) # [3,B,head_dim//2,1] + position_ids_expanded = position_ids[:, :, None, :].float() # [3,B,1,N] + + freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3) # [3,B,N,head_dim//2] + freqs = self.apply_interleaved_mrope(freqs, self.mrope_section) # [B,N,head_dim//2] + emb = torch.cat((freqs, freqs), dim=-1) # [B,N,head_dim] + cos = emb.cos() * self.attention_scaling # [B,N,head_dim] + sin = emb.sin() * self.attention_scaling # [B,N,head_dim] + + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + +class Qwen3VLMoeTextModel(Qwen3VLMoePreTrainedModel): + config: Qwen3VLMoeTextConfig + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer"] + + def __init__(self, config: Qwen3VLMoeTextConfig): + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) + self.layers = nn.ModuleList( + [Qwen3VLMoeTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] + ) + self.norm = Qwen3VLMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3VLMoeTextRotaryEmbedding(config=config) + self.gradient_checkpointing = False + + # Initialize weights and apply final processing + self.post_init() + + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + use_cache: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + # args for deepstack + visual_pos_masks: Optional[torch.Tensor] = None, + deepstack_visual_embeds: Optional[list[torch.Tensor]] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Union[tuple, BaseModelOutputWithPast]: + r""" + visual_pos_masks (`torch.Tensor` of shape `(batch_size, seqlen)`, *optional*): + The mask of the visual positions. + deepstack_visual_embeds (`list[torch.Tensor]`, *optional*): + The deepstack visual embeddings. The shape is (num_layers, visual_seqlen, embed_dim). + The feature is extracted from the different visual encoder layers, and fed to the decoder + hidden states. It's from the paper DeepStack(https://arxiv.org/abs/2406.04334). + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + # torch.jit.trace() doesn't support cache objects in the output + if use_cache and past_key_values is None and not torch.jit.is_tracing(): + past_key_values = DynamicCache(config=self.config) + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) + + if cache_position is None: + past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0 + cache_position = torch.arange( + past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device + ) + + # the hard coded `3` is for temporal, height and width. + if position_ids is None: + position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1) + elif position_ids.ndim == 2: + position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) + + if position_ids.ndim == 3 and position_ids.shape[0] == 4: + text_position_ids = position_ids[0] + position_ids = position_ids[1:] + else: + text_position_ids = position_ids[0] + + attention_mask = create_causal_mask( + config=self.config, + input_embeds=inputs_embeds, + attention_mask=attention_mask, + cache_position=cache_position, + past_key_values=past_key_values, + position_ids=text_position_ids, + ) + + hidden_states = inputs_embeds + + # create position embeddings to be shared across the decoder layers + position_embeddings = self.rotary_emb(hidden_states, position_ids) + + # decoder layers + for layer_idx, decoder_layer in enumerate(self.layers): + layer_outputs = decoder_layer( + hidden_states, + attention_mask=attention_mask, + position_ids=text_position_ids, + past_key_values=past_key_values, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = layer_outputs + + # add visual features to the hidden states of first several layers + if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)): + hidden_states = self._deepstack_process( + hidden_states, + visual_pos_masks, + deepstack_visual_embeds[layer_idx], + ) + + hidden_states = self.norm(hidden_states) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values, + ) + + def _deepstack_process( + self, hidden_states: torch.Tensor, visual_pos_masks: torch.Tensor, visual_embeds: torch.Tensor + ): + visual_pos_masks = visual_pos_masks.to(hidden_states.device) + visual_embeds = visual_embeds.to(hidden_states.device, hidden_states.dtype) + local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds + hidden_states[visual_pos_masks, :] = local_this + return hidden_states + + +@dataclass +class Qwen3VLMoeCausalLMOutputWithPast(ModelOutput): + r""" + loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided): + Language modeling loss (for next-token prediction). + logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.vocab_size)`): + Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax). + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + loss: Optional[torch.FloatTensor] = None + logits: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + aux_loss: Optional[torch.FloatTensor] = None + + +@dataclass +class Qwen3VLMoeModelOutputWithPast(ModelOutput): + r""" + past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`): + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see + `past_key_values` input) to speed up sequential decoding. + rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*): + The rope index difference between sequence length and multimodal rope. + """ + + last_hidden_state: Optional[torch.FloatTensor] = None + past_key_values: Optional[Cache] = None + hidden_states: Optional[tuple[torch.FloatTensor]] = None + attentions: Optional[tuple[torch.FloatTensor]] = None + rope_deltas: Optional[torch.LongTensor] = None + + +class Qwen3VLMoeModel(Qwen3VLMoePreTrainedModel): + base_model_prefix = "" + _checkpoint_conversion_mapping = {} + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLMoeConfig + _no_split_modules = ["Qwen3VLMoeTextDecoderLayer", "Qwen3VLMoeVisionBlock"] + + def __init__(self, config): + super().__init__(config) + self.visual = Qwen3VLMoeVisionModel._from_config(config.vision_config) + self.language_model = Qwen3VLMoeTextModel._from_config(config.text_config) + self.rope_deltas = None # cache rope_deltas here + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.language_model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.language_model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.language_model = decoder + + def get_decoder(self): + return self.language_model + + def get_rope_index( + self, + input_ids: Optional[torch.LongTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Different from the original implementation, Qwen3VLMoe use timestamps rather than absolute time position ids.""" + + # Since we use timestamps to seperate videos, like , the video_grid_thw should also be split + if video_grid_thw is not None: + video_grid_thw = torch.repeat_interleave(video_grid_thw, video_grid_thw[:, 0], dim=0) + video_grid_thw[:, 0] = 1 + + spatial_merge_size = self.config.vision_config.spatial_merge_size + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + mrope_position_deltas = [] + if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None): + total_input_ids = input_ids + if attention_mask is None: + attention_mask = torch.ones_like(total_input_ids) + position_ids = torch.ones( + 3, + input_ids.shape[0], + input_ids.shape[1], + dtype=input_ids.dtype, + device=input_ids.device, + ) # [3,B,N] + image_index, video_index = 0, 0 + attention_mask = attention_mask.to(total_input_ids.device) + for i, input_ids in enumerate(total_input_ids): + input_ids = input_ids[attention_mask[i] == 1] + image_nums, video_nums = 0, 0 + vision_start_indices = torch.argwhere(input_ids == vision_start_token_id).squeeze(1) + vision_tokens = input_ids[vision_start_indices + 1] + image_nums = (vision_tokens == image_token_id).sum() + video_nums = (vision_tokens == video_token_id).sum() + input_tokens = input_ids.tolist() + llm_pos_ids_list: list = [] + st = 0 + remain_images, remain_videos = image_nums, video_nums + for _ in range(image_nums + video_nums): + if image_token_id in input_tokens and remain_images > 0: + ed_image = input_tokens.index(image_token_id, st) + else: + ed_image = len(input_tokens) + 1 + if video_token_id in input_tokens and remain_videos > 0: + ed_video = input_tokens.index(video_token_id, st) + else: + ed_video = len(input_tokens) + 1 + if ed_image < ed_video: + t, h, w = ( + image_grid_thw[image_index][0], + image_grid_thw[image_index][1], + image_grid_thw[image_index][2], + ) + image_index += 1 + remain_images -= 1 + ed = ed_image + + else: + t, h, w = ( + video_grid_thw[video_index][0], + video_grid_thw[video_index][1], + video_grid_thw[video_index][2], + ) + video_index += 1 + remain_videos -= 1 + ed = ed_video + llm_grid_t, llm_grid_h, llm_grid_w = ( + t.item(), + h.item() // spatial_merge_size, + w.item() // spatial_merge_size, + ) + text_len = ed - st + + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) # [3,text_len] + + # t_index is always 0 because llm_grid_t is always 1 (we use timestamps to encode the temporal information for videos) + t_index = ( + torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten() + ) # [T*H*W] + h_index = ( + torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten() + ) # [T*H*W] + w_index = ( + torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten() + ) # [T*H*W] + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx) # [3,T*H*W] + st = ed + llm_grid_t * llm_grid_h * llm_grid_w + + if st < len(input_tokens): + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + text_len = len(input_tokens) - st + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) # [3,N] + position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device) + mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i])) + mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1) # [B,1] + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + else: + if attention_mask is not None: + position_ids = attention_mask.long().cumsum(-1) - 1 # [B,N] + position_ids.masked_fill_(attention_mask == 0, 1) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device) # [3,B,N] + max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0] # [B,1] + mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1] # [B,1] + else: + position_ids = ( + torch.arange(input_ids.shape[1], device=input_ids.device) + .view(1, 1, -1) + .expand(3, input_ids.shape[0], -1) + ) # [3,B,N] + mrope_position_deltas = torch.zeros( + [input_ids.shape[0], 1], + device=input_ids.device, + dtype=input_ids.dtype, + ) # [B,1] + + return position_ids, mrope_position_deltas # [3,B,N], [B,1] + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + """ + Encodes videos into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values_videos (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input videos. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + # Same implementation as for images + return self.get_image_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + """ + Encodes images into continuous embeddings that can be forwarded to the language model. The deepstack visual features are also returned. + + Args: + pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`): + The tensors corresponding to the input images. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + """ + pixel_values = pixel_values.type(self.visual.dtype) + image_embeds, deepstack_image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw) + split_sizes = (image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist() + image_embeds = torch.split(image_embeds, split_sizes) + return image_embeds, deepstack_image_embeds + + def get_placeholder_mask( + self, + input_ids: torch.LongTensor, + inputs_embeds: torch.FloatTensor, + image_features: Optional[torch.FloatTensor] = None, + video_features: Optional[torch.FloatTensor] = None, + ): + """ + Obtains multimodal placeholder mask from `input_ids` or `inputs_embeds`, and checks that the placeholder token count is + equal to the length of multimodal features. If the lengths are different, an error is raised. + """ + if input_ids is None: + special_image_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_image_mask = special_image_mask.all(-1) + special_video_mask = inputs_embeds == self.get_input_embeddings()( + torch.tensor(self.config.video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + special_video_mask = special_video_mask.all(-1) + else: + special_image_mask = input_ids == self.config.image_token_id + special_video_mask = input_ids == self.config.video_token_id + + n_image_tokens = special_image_mask.sum() + special_image_mask = special_image_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + if image_features is not None and inputs_embeds[special_image_mask].numel() != image_features.numel(): + raise ValueError( + f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {image_features.shape[0]}" + ) + + n_video_tokens = special_video_mask.sum() + special_video_mask = special_video_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device) + if video_features is not None and inputs_embeds[special_video_mask].numel() != video_features.numel(): + raise ValueError( + f"Videos features and video tokens do not match: tokens: {n_video_tokens}, features {video_features.shape[0]}" + ) + + return special_image_mask, special_video_mask + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLMoeModelOutputWithPast]: + r""" + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError("You must specify exactly one of input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.get_input_embeddings()(input_ids) + + image_mask = None + video_mask = None + + if pixel_values is not None: + image_embeds, deepstack_image_embeds = self.get_image_features(pixel_values, image_grid_thw) + image_embeds = torch.cat(image_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype) + image_mask, _ = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) + + if pixel_values_videos is not None: + video_embeds, deepstack_video_embeds = self.get_video_features(pixel_values_videos, video_grid_thw) + video_embeds = torch.cat(video_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype) + _, video_mask = self.get_placeholder_mask( + input_ids, inputs_embeds=inputs_embeds, video_features=video_embeds + ) + inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) + + visual_pos_masks = None + deepstack_visual_embeds = None + if image_mask is not None and video_mask is not None: + # aggregate visual_pos_masks and deepstack_visual_embeds + image_mask = image_mask[..., 0] + video_mask = video_mask[..., 0] + visual_pos_masks = image_mask | video_mask + deepstack_visual_embeds = [] + image_mask_joint = image_mask[visual_pos_masks] + video_mask_joint = video_mask[visual_pos_masks] + for img_embed, vid_embed in zip(deepstack_image_embeds, deepstack_video_embeds): + embed_joint = img_embed.new_zeros(visual_pos_masks.sum(), img_embed.shape[-1]).to(img_embed.device) + embed_joint[image_mask_joint, :] = img_embed + embed_joint[video_mask_joint, :] = vid_embed + deepstack_visual_embeds.append(embed_joint) + elif image_mask is not None: + image_mask = image_mask[..., 0] + visual_pos_masks = image_mask + deepstack_visual_embeds = deepstack_image_embeds + elif video_mask is not None: + video_mask = video_mask[..., 0] + visual_pos_masks = video_mask + deepstack_visual_embeds = deepstack_video_embeds + + if position_ids is None: + attention_mask_tensor = ( + attention_mask if not isinstance(attention_mask, dict) else attention_mask["full_attention"] + ) + if attention_mask_tensor is not None and attention_mask_tensor.ndim == 4: + attention_mask_tensor = torch.diagonal(attention_mask_tensor[:, 0], dim1=1, dim2=2) + # Only apply conversion for floating point tensors (inverted masks) + if attention_mask_tensor.dtype.is_floating_point: + attention_mask_tensor = attention_mask_tensor / torch.finfo(attention_mask_tensor.dtype).min + attention_mask_tensor = (1.0 - attention_mask_tensor).int() + + # Calculate RoPE index once per generation in the pre-fill stage only. + # When compiling, we can't check tensor values thus we check only input length + # It is safe to assume that `length!=1` means we're in pre-fill because compiled + # models currently cannot do asssisted decoding + prefill_compiled_stage = is_torchdynamo_compiling() and ( + (input_ids is not None and input_ids.shape[1] != 1) + or (inputs_embeds is not None and inputs_embeds.shape[1] != 1) + ) + prefill_noncompiled_stage = not is_torchdynamo_compiling() and ( + (cache_position is not None and cache_position[0] == 0) + or (past_key_values is None or past_key_values.get_seq_length() == 0) + ) + if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None: + position_ids, rope_deltas = self.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + attention_mask=attention_mask_tensor, + ) + self.rope_deltas = rope_deltas + # then use the prev pre-calculated rope-deltas to get the correct position ids + else: + batch_size, seq_length, _ = inputs_embeds.shape + delta = ( + (cache_position[0] + self.rope_deltas).to(inputs_embeds.device) if cache_position is not None else 0 + ) + position_ids = torch.arange(seq_length, device=inputs_embeds.device) # [N] + position_ids = position_ids.view(1, -1).expand(batch_size, -1) # [B,N] + if cache_position is not None: # otherwise `deltas` is an int `0` + delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0) + position_ids = position_ids.add(delta) # [B,N] + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1) # [3,B,N] + + outputs = self.language_model( + input_ids=None, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + visual_pos_masks=visual_pos_masks, + deepstack_visual_embeds=deepstack_visual_embeds, + **kwargs, + ) + + return Qwen3VLMoeModelOutputWithPast( + last_hidden_state=outputs.last_hidden_state, + past_key_values=outputs.past_key_values, + rope_deltas=self.rope_deltas, + ) + + +def load_balancing_loss_func( + gate_logits: Union[torch.Tensor, tuple[torch.Tensor], None], + num_experts: Optional[int] = None, + top_k=2, + attention_mask: Optional[torch.Tensor] = None, +) -> Union[torch.Tensor, int]: + r""" + Computes auxiliary load balancing loss as in Switch Transformer - implemented in Pytorch. + + See Switch Transformer (https://huggingface.co/papers/2101.03961) for more details. This function implements the loss + function presented in equations (4) - (6) of the paper. It aims at penalizing cases where the routing between + experts is too unbalanced. + + Args: + gate_logits: + Logits from the `gate`, should be a tuple of model.config.num_hidden_layers tensors of + shape [batch_size X sequence_length, num_experts]. + num_experts: + Number of experts + top_k: + The number of experts to route per-token, can be also interpreted as the `top-k` routing + parameter. + attention_mask (`torch.Tensor`, *optional*): + The attention_mask used in forward function + shape [batch_size X sequence_length] if not None. + + Returns: + The auxiliary loss. + """ + if gate_logits is None or not isinstance(gate_logits, tuple): + return 0 + + if isinstance(gate_logits, tuple): + compute_device = gate_logits[0].device + concatenated_gate_logits = torch.cat([layer_gate.to(compute_device) for layer_gate in gate_logits], dim=0) + # concatenated_gate_logits: [num_layers*B*N,num_experts] + + routing_weights = torch.nn.functional.softmax(concatenated_gate_logits, dim=-1) # [num_layers*B*N,num_experts] + + _, selected_experts = torch.topk(routing_weights, top_k, dim=-1) # [num_layers*B*N,top_k] + + expert_mask = torch.nn.functional.one_hot(selected_experts, num_experts) # [num_layers*B*N,top_k,num_experts] + + if attention_mask is None: + # Compute the percentage of tokens routed to each experts + tokens_per_expert = torch.mean(expert_mask.float(), dim=0) # [top_k,num_experts] + + # Compute the average probability of routing to these experts + router_prob_per_expert = torch.mean(routing_weights, dim=0) # [num_experts] + else: + batch_size, sequence_length = attention_mask.shape + num_hidden_layers = concatenated_gate_logits.shape[0] // (batch_size * sequence_length) + + # Compute the mask that masks all padding tokens as 0 with the same shape of expert_mask + expert_attention_mask = ( + attention_mask[None, :, :, None, None] + .expand((num_hidden_layers, batch_size, sequence_length, top_k, num_experts)) + .reshape(-1, top_k, num_experts) + .to(compute_device) + ) # [num_layers*B*N,top_k,num_experts] + + # Compute the percentage of tokens routed to each experts + tokens_per_expert = torch.sum(expert_mask.float() * expert_attention_mask, dim=0) / torch.sum( + expert_attention_mask, dim=0 + ) # [top_k,num_experts] + + # Compute the mask that masks all padding tokens as 0 with the same shape of tokens_per_expert + router_per_expert_attention_mask = ( + attention_mask[None, :, :, None] + .expand((num_hidden_layers, batch_size, sequence_length, num_experts)) + .reshape(-1, num_experts) + .to(compute_device) + ) # [num_layers*B*N,num_experts] + + # Compute the average probability of routing to these experts + router_prob_per_expert = torch.sum(routing_weights * router_per_expert_attention_mask, dim=0) / torch.sum( + router_per_expert_attention_mask, dim=0 + ) # [num_experts] + + overall_loss = torch.sum(tokens_per_expert * router_prob_per_expert.unsqueeze(0)) + return overall_loss * num_experts + + +class Qwen3VLMoeForConditionalGeneration(Qwen3VLMoePreTrainedModel, GenerationMixin): + _checkpoint_conversion_mapping = {} + _tied_weights_keys = ["lm_head.weight"] + # Reference: fix gemma3 grad acc #37208 + accepts_loss_kwargs = False + config: Qwen3VLMoeConfig + + def __init__(self, config): + super().__init__(config) + self.model = Qwen3VLMoeModel(config) + self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False) + + self.post_init() + + def get_input_embeddings(self): + return self.model.get_input_embeddings() + + def set_input_embeddings(self, value): + self.model.set_input_embeddings(value) + + def set_decoder(self, decoder): + self.model.set_decoder(decoder) + + def get_decoder(self): + return self.model.get_decoder() + + def get_video_features( + self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None + ): + return self.model.get_video_features(pixel_values_videos, video_grid_thw) + + def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None): + return self.model.get_image_features(pixel_values, image_grid_thw) + + # Make modules available through conditional class for BC + @property + def language_model(self): + return self.model.language_model + + @property + def visual(self): + return self.model.visual + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + pixel_values: Optional[torch.Tensor] = None, + pixel_values_videos: Optional[torch.FloatTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + video_grid_thw: Optional[torch.LongTensor] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[TransformersKwargs], + ) -> Union[tuple, Qwen3VLMoeCausalLMOutputWithPast]: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*): + The temporal, height and width of feature shape of each image in LLM. + video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*): + The temporal, height and width of feature shape of each video in LLM. + + Example: + ```python + >>> from PIL import Image + >>> import requests + >>> from transformers import AutoProcessor, Qwen3VLMoeForConditionalGeneration + + >>> model = Qwen3VLMoeForConditionalGeneration.from_pretrained("Qwen/Qwen3-VL-30B-A3B-Instruct", dtype="auto", device_map="auto") + >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen3-VL-30B-A3B-Instruct") + + >>> messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "image": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg", + }, + {"type": "text", "text": "Describe this image in short."}, + ], + } + ] + + >>> # Preparation for inference + >>> inputs = processor.apply_chat_template( + messages, + tokenize=True, + add_generation_prompt=True, + return_dict=True, + return_tensors="pt" + ) + >>> inputs = inputs.to(model.device) + + >>> # Generate + >>> generated_ids = model.generate(**inputs, max_new_tokens=128) + >>> generated_ids_trimmed = [ + out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) + ] + >>> processor.batch_decode(generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "A woman in a plaid shirt sits on a sandy beach at sunset, smiling as she gives a high-five to a yellow Labrador Retriever wearing a harness. The ocean waves roll in the background." + ```""" + + outputs = self.model( + input_ids=input_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs[0] # [B,N,hidden_size] + + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep + logits = self.lm_head(hidden_states[:, slice_indices, :]) # [B,N_kept,vocab_size] + + loss = None + if labels is not None: + loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size) + + aux_loss = None + if kwargs.get("output_router_logits", False): + aux_loss = load_balancing_loss_func( + outputs.router_logits, + self.config.text_config.num_experts, + self.config.text_config.num_experts_per_tok, + attention_mask, + ) + if labels is not None: + loss += self.config.text_config.router_aux_loss_coef * aux_loss.to( + loss.device + ) # make sure to reside in the same device + + return Qwen3VLMoeCausalLMOutputWithPast( + loss=loss, + aux_loss=aux_loss, + logits=logits, + past_key_values=outputs.past_key_values, + rope_deltas=outputs.rope_deltas, + ) + + def prepare_inputs_for_generation( + self, + input_ids, + past_key_values=None, + attention_mask=None, + inputs_embeds=None, + cache_position=None, + position_ids=None, + use_cache=True, + pixel_values=None, + pixel_values_videos=None, + image_grid_thw=None, + video_grid_thw=None, + **kwargs, + ): + # Overwritten -- in specific circumstances we don't want to forward image inputs to the model + + model_inputs = super().prepare_inputs_for_generation( + input_ids, + past_key_values=past_key_values, + attention_mask=attention_mask, + inputs_embeds=inputs_embeds, + cache_position=cache_position, + position_ids=position_ids, + pixel_values=pixel_values, + pixel_values_videos=pixel_values_videos, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + use_cache=use_cache, + **kwargs, + ) + + # Qwen3VLMoe position_ids are prepareed with rope_deltas in forward + model_inputs["position_ids"] = None + + if cache_position[0] != 0: + model_inputs["pixel_values"] = None + model_inputs["pixel_values_videos"] = None + + return model_inputs + + def _get_image_nums_and_video_nums( + self, + input_ids: Optional[torch.LongTensor], + inputs_embeds: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Get the number of images and videos for each sample to calculate the separation length of the sample tensor. + These parameters are not passed through the processor to avoid unpredictable impacts from interface modifications. + + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. + + Returns: + image_nums (`torch.LongTensor` of shape `(batch_size, num_images_sample)`) + video_nums (`torch.LongTensor` of shape `(batch_size, num_videos_sample)`) + """ + image_token_id = self.config.image_token_id + video_token_id = self.config.video_token_id + vision_start_token_id = self.config.vision_start_token_id + + if inputs_embeds is not None: + vision_start_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(vision_start_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + image_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(image_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + video_mask = ( + inputs_embeds + == self.get_input_embeddings()( + torch.tensor(video_token_id, dtype=torch.long, device=inputs_embeds.device) + ) + )[..., 0] + else: + vision_start_mask = input_ids == vision_start_token_id + image_mask = input_ids == image_token_id + video_mask = input_ids == video_token_id + + vision_first_mask = torch.roll(vision_start_mask, shifts=1, dims=1) + image_nums = torch.sum(vision_first_mask & image_mask, dim=1) + video_nums = torch.sum(vision_first_mask & video_mask, dim=1) + + return image_nums, video_nums + + def _expand_inputs_for_generation( + self, + expand_size: int = 1, + is_encoder_decoder: bool = False, + input_ids: Optional[torch.LongTensor] = None, + **model_kwargs, + ) -> tuple[torch.LongTensor, dict[str, Any]]: + # Overwritten -- Support for expanding tensors without a batch size dimension + # e.g., pixel_values, image_grid_thw, pixel_values_videos, video_grid_thw, second_per_grid_t + # pixel_values.shape[0] is sum(seqlen_images for samples) + # image_grid_thw.shape[0] is sum(num_images for samples) + + if expand_size == 1: + return input_ids, model_kwargs + + visual_keys = ["pixel_values", "image_grid_thw", "pixel_values_videos", "video_grid_thw", "second_per_grid_ts"] + + def _expand_dict_for_generation_visual(dict_to_expand): + image_grid_thw = model_kwargs.get("image_grid_thw", None) + video_grid_thw = model_kwargs.get("video_grid_thw", None) + image_nums, video_nums = self._get_image_nums_and_video_nums( + input_ids, inputs_embeds=model_kwargs.get("inputs_embeds", None) + ) + + def _repeat_interleave_samples(x, lengths, repeat_times): + samples = torch.split(x, lengths) + repeat_args = [repeat_times] + [1] * (x.dim() - 1) + result = torch.cat([sample.repeat(*repeat_args) for sample in samples], dim=0) + return result + + for key in dict_to_expand: + if key == "pixel_values": + # split images into samples + samples = torch.split(image_grid_thw, list(image_nums)) + # compute the sequence length of images for each sample + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "image_grid_thw": + # get the num of images for each sample + lengths = list(image_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "pixel_values_videos": + samples = torch.split(video_grid_thw, list(video_nums)) + lengths = [torch.prod(sample, dim=1).sum() for sample in samples] + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "video_grid_thw": + lengths = list(video_nums) + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=lengths, repeat_times=expand_size + ) + elif key == "second_per_grid_ts": + dict_to_expand[key] = _repeat_interleave_samples( + dict_to_expand[key], lengths=list(video_nums), repeat_times=expand_size + ) + return dict_to_expand + + def _expand_dict_for_generation(dict_to_expand): + for key in dict_to_expand: + if ( + key != "cache_position" + and dict_to_expand[key] is not None + and isinstance(dict_to_expand[key], torch.Tensor) + and key not in visual_keys + ): + dict_to_expand[key] = dict_to_expand[key].repeat_interleave(expand_size, dim=0) + return dict_to_expand + + model_kwargs = _expand_dict_for_generation_visual(model_kwargs) + + if input_ids is not None: + input_ids = input_ids.repeat_interleave(expand_size, dim=0) + + model_kwargs = _expand_dict_for_generation(model_kwargs) + + if is_encoder_decoder: + if model_kwargs.get("encoder_outputs") is None: + raise ValueError("If `is_encoder_decoder` is True, make sure that `encoder_outputs` is defined.") + model_kwargs["encoder_outputs"] = _expand_dict_for_generation(model_kwargs["encoder_outputs"]) + + return input_ids, model_kwargs + + +__all__ = [ + "Qwen3VLMoeVisionModel", + "Qwen3VLMoeForConditionalGeneration", + "Qwen3VLMoeModel", + "Qwen3VLMoePreTrainedModel", + "Qwen3VLMoeTextModel", +] diff --git a/cosmos_training/cosmos/tools/__init__.py b/cosmos_training/cosmos/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/tools/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07e797dad73a59c67cf874c311450b8f9cb8d65d GIT binary patch literal 231 zcmZ8b!41MN3`{7M5K;%>Ll!`czzR_*Y1Jx?BRdi50<6Fc%)%H$0`m`xhIaRd=7vnW5u92)>EDXlpsXubEtv^n%}iPju3 Q=eHz-5_xA*O3avj0Y=P4mjD0& literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/tools/flops/__init__.py b/cosmos_training/cosmos/tools/flops/__init__.py new file mode 100644 index 00000000..b2d0b7ac --- /dev/null +++ b/cosmos_training/cosmos/tools/flops/__init__.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Reusable FLOPs estimation utilities for model architectures.""" + +from cosmos.tools.flops.omni_mot import ( + OmniMoTModelDescriptor, + compute_omni_mot_flops_per_batch, + get_omni_mot_model_descriptor, +) +from cosmos.tools.flops.wan_vae import compute_wan_vae_encoder_flops + +__all__ = [ + "OmniMoTModelDescriptor", + "compute_omni_mot_flops_per_batch", + "compute_wan_vae_encoder_flops", + "get_omni_mot_model_descriptor", +] diff --git a/cosmos_training/cosmos/tools/flops/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/tools/flops/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a9ac72636651e6524348cbb4c915226fbceba7d GIT binary patch literal 615 zcmZutJ8#rL5Z?9UE((N_0u;%jb7S)ZA{rhQAqY{q8Ew{cmyLEk);@@ijy^p-9lwQt z;FgMxkVHtSSUcxH!ALWj=l9Lbx4pex7A0Kery^tQ`xwlUY@N%e(fQ0;*07ehiRLD0 z5|cKm$(oFhxm4$7(JVMyvo`y}o8=}i7atGaVQ}JDWAW4Qh0SO-HMR+@ST zJz6*xUY-p5YxIAs69=?+03{#xXNWV=!3TkWos2B@7OBi^8dtFKE6grNlSlKW+!>Yt zElGgku_R~6Rh~$@YT~cvb^aM1=Y~iB>D}^9=p(Ao8sICtjQ#B9yw<^ai}lK#)je6i zBCW{Pw>Wig=`{Xoj^oV^!CRp^)vfE`(7Jjfy7j1jI|S5Y=O|c-bLMVh3p<@sPp8xX z5zGTr{pM!pAU-f?d}bfS<#<&0B$}To=lr5z{5k)fWjwiH(OmB`{*wR7@_WgrrM(~9 F{s8`2!+rn& literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/tools/flops/__pycache__/omni_mot.cpython-312.pyc b/cosmos_training/cosmos/tools/flops/__pycache__/omni_mot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b8bcd3e3beb11abb4f7ecb8233c5ab1fe052b51 GIT binary patch literal 18702 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 Hv*P~%yrjB3 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/tools/flops/__pycache__/wan_vae.cpython-312.pyc b/cosmos_training/cosmos/tools/flops/__pycache__/wan_vae.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c8f2bf14ea4730b63f1e7f4f8bc82eaf1c6607e GIT binary patch literal 5840 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/tools/flops/omni_mot.py b/cosmos_training/cosmos/tools/flops/omni_mot.py new file mode 100644 index 00000000..d4f4b7e4 --- /dev/null +++ b/cosmos_training/cosmos/tools/flops/omni_mot.py @@ -0,0 +1,498 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FLOPs estimation for the OmniMoT (Mixture-of-Tokens) dual-pathway transformer.""" + +from decimal import Decimal +from typing import NamedTuple + +from cosmos.utils import log + + +class OmniMoTModelDescriptor(NamedTuple): + """ + Holds information about the OmniMoT model architecture needed for the custom flops formula. + + This captures the dual-pathway (MoT) transformer with support for vision, action, and sound + modalities, and optional Mixture of Experts (MoE) layers. + """ + + # LLM / Transformer core + hidden_size: int # D: hidden dimension of the transformer (e.g. 2048, 3584) + num_hidden_layers: int # number of transformer decoder layers + num_attention_heads: int # number of Q heads + num_key_value_heads: int # number of K/V heads (GQA when < num_attention_heads) + head_dim: int # dimension per head + intermediate_size: int # dense MLP intermediate size (gate_proj / up_proj output dim) + vocab_size: int # vocabulary size for embed_tokens and lm_head + + # MoE parameters + use_moe: bool # whether MoE layers are used + num_experts: int # total number of experts per MoE layer + num_experts_per_tok: int # top-k experts activated per token + moe_intermediate_size: int # intermediate size inside each expert + decoder_sparse_step: int # every `decoder_sparse_step`-th layer is MoE + mlp_only_layers: list[int] # layers forced to use dense MLP even in MoE config + + # Vision modality + latent_patch_size: int # spatial patch size for latent patchification (default 2) + latent_channel_size: int # number of channels in the VAE latent (default 16) + + # Action modality + action_dim: int # action token dimension (default 32) + + # Sound modality + sound_dim: int # sound token dimension + + # TimestepEmbedder + frequency_embedding_size: int # sinusoidal frequency embedding dim (default 256) + + # Text prediction + predict_text_tokens: bool # whether lm_head is applied for text CE loss + + +def get_omni_mot_model_descriptor( + hidden_size: int = 2048, + num_hidden_layers: int = 24, + num_attention_heads: int = 16, + num_key_value_heads: int = 16, + head_dim: int | None = None, + intermediate_size: int = 5632, + vocab_size: int = 151936, + use_moe: bool = True, + num_experts: int = 60, + num_experts_per_tok: int = 4, + moe_intermediate_size: int = 1408, + decoder_sparse_step: int = 1, + mlp_only_layers: list[int] | None = None, + latent_patch_size: int = 2, + latent_channel_size: int = 16, + action_dim: int = 32, + sound_dim: int = 64, + frequency_embedding_size: int = 256, + predict_text_tokens: bool = False, +) -> OmniMoTModelDescriptor: + if head_dim is None: + head_dim = hidden_size // num_attention_heads + if mlp_only_layers is None: + mlp_only_layers = [] + return OmniMoTModelDescriptor( + hidden_size=hidden_size, + num_hidden_layers=num_hidden_layers, + num_attention_heads=num_attention_heads, + num_key_value_heads=num_key_value_heads, + head_dim=head_dim, + intermediate_size=intermediate_size, + vocab_size=vocab_size, + use_moe=use_moe, + num_experts=num_experts, + num_experts_per_tok=num_experts_per_tok, + moe_intermediate_size=moe_intermediate_size, + decoder_sparse_step=decoder_sparse_step, + mlp_only_layers=mlp_only_layers, + latent_patch_size=latent_patch_size, + latent_channel_size=latent_channel_size, + action_dim=action_dim, + sound_dim=sound_dim, + frequency_embedding_size=frequency_embedding_size, + predict_text_tokens=predict_text_tokens, + ) + + +def _pct(part: Decimal, whole: Decimal) -> str: + """Return percentage string, guarding against division by zero.""" + if whole == 0: + return "0" + return str(round(part / whole * 100, 1)) + + +def _extract_padding_tokens( + split_lens: list[int], + attn_modes: list[str], +) -> int: + """Return the total number of padding tokens in a packed sequence. + + Padding splits are lone ``"causal"`` entries that do not form a + ``(causal, full)`` pair with the next split. In practice, finalize() + appends at most one such split at the end. + """ + padding = 0 + i = 0 + while i < len(split_lens): + if i + 1 < len(split_lens) and attn_modes[i] == "causal" and attn_modes[i + 1] == "full": + i += 2 + else: + if attn_modes[i] == "causal": + padding += split_lens[i] + i += 1 + return padding + + +def _compute_per_sample_attn_flops( + n_heads: int, + d_head: int, + B: int | Decimal, + S_und: int | Decimal, + S_gen: int | Decimal, + split_lens: list[int] | None = None, + attn_modes: list[str] | None = None, + include_padding: bool = False, +) -> tuple[Decimal, Decimal]: + """Compute per-layer attention dot-product FLOPs (QK^T + Attn*V). + + The MoT attention pattern is: + - Und tokens (causal): each sample's text tokens self-attend causally. + - Gen tokens (full): each sample's gen tokens attend to ALL tokens in + that sample (und + gen) with full (non-causal) attention. + + When ``split_lens``/``attn_modes`` are provided (packed-sequence mode), + per-sample lengths are extracted from the alternating (causal, full) pairs. + Otherwise, ``B`` uniform samples each with ``S_und`` and ``S_gen`` tokens + are assumed. + + Args: + include_padding: If True, lone ``"causal"`` splits (padding tokens + appended by finalize()) are counted as additional causal + self-attention windows. + + Returns: + (und_attn_flops, gen_attn_flops) for a single layer (QK^T + Attn*V). + """ + if split_lens is not None and attn_modes is not None: + und_attn = Decimal(0) + gen_attn = Decimal(0) + i = 0 + while i < len(split_lens): + if i + 1 < len(split_lens) and attn_modes[i] == "causal" and attn_modes[i + 1] == "full": + s_und_i = split_lens[i] + s_gen_i = split_lens[i + 1] + und_attn += 4 * n_heads * d_head * s_und_i * s_und_i + gen_attn += 4 * n_heads * d_head * s_gen_i * (s_und_i + s_gen_i) + i += 2 + else: + if include_padding and attn_modes[i] == "causal": + s_pad = split_lens[i] + und_attn += 4 * n_heads * d_head * s_pad * s_pad + i += 1 + return und_attn, gen_attn + + und_attn = Decimal(4 * B * n_heads * d_head * S_und * S_und) + gen_attn = Decimal(4 * B * n_heads * d_head * S_gen * (S_und + S_gen)) + return und_attn, gen_attn + + +def compute_omni_mot_flops_per_batch( + cfg: OmniMoTModelDescriptor, + B: int | Decimal, + text_tokens: int = 512, + vision_tokens: int = 0, + action_tokens: int = 0, + sound_tokens: int = 0, + freeze_und: bool = False, + vision_gen: bool = True, + action_gen: bool = False, + sound_gen: bool = False, + backwardpass_ratio: float = 2.0, + split_lens: list[int] | None = None, + attn_modes: list[str] | None = None, + include_padding: bool = False, + use_activation_checkpointing: bool = False, +) -> Decimal: + """Compute training FLOPs for a single batch of the OmniMoT model. + + This is a standalone function that can be called from calculators or callbacks. + It accounts for all parts of the dual-pathway (MoT) transformer, including: + - Modality-specific embedding/projection layers (vae2llm, llm2vae, action2llm, + llm2action, sound2llm, llm2sound). + - TimestepEmbedder MLPs. + - lm_head for text prediction. + - Transformer blocks with dual-pathway attention (separate Q/K/V/O projections + for und and gen pathways). + - Per-sample attention: und tokens self-attend causally, gen tokens attend to + all tokens in their sample with full attention. + - Attention softmax FLOPs (~5 ops per element of the attention matrix). + - Dual-pathway MLPs (dense SwiGLU or MoE per layer). + - RMSNorm at all positions (4 per layer + Q/K norms + 2 final norms). + - Backward pass with special handling for freeze_und. + - Activation checkpointing forward recomputation during backward. + + Args: + cfg: Model architecture descriptor. + B: Batch size. For the packed-sequence path (``split_lens`` provided), + set ``B=1`` and let ``text_tokens``/``vision_tokens`` be the totals + across all packed samples. + text_tokens: Total number of text (understanding) tokens across all samples. + vision_tokens: Total number of vision generation tokens (after patchification) + across all samples. + action_tokens: Total number of action tokens across all samples. + sound_tokens: Total number of sound tokens across all samples. + freeze_und: If True, understanding pathway is frozen (no backward FLOPs for und). + vision_gen: Whether vision generation is active. + action_gen: Whether action generation is active. + sound_gen: Whether sound generation is active. + backwardpass_ratio: Multiplier for backward pass FLOPs relative to forward + (default 2.0). + split_lens: Per-split token lengths from the packed sequence. Alternating + ``[und_0, gen_0, und_1, gen_1, ...]`` with matching ``attn_modes``. + When provided, per-sample attention FLOPs are computed correctly + instead of assuming one big attention window. + attn_modes: Attention mode for each split (``"causal"`` or ``"full"``). + Must have the same length as ``split_lens``. + include_padding: If True, padding tokens (lone ``"causal"`` splits at + the end of ``split_lens``) are included in FLOPs for attention, + projections, MLP, and norms. Useful for measuring total GPU FLOPs + including wasted work on padding. + use_activation_checkpointing: If True, add FLOPs for the forward + recomputation of each transformer layer during the backward pass. + Activation checkpointing discards intermediate activations and + recomputes them on-the-fly, adding ~1x layer forward FLOPs. + + Returns: + Total training FLOPs (forward + backward) as a Decimal. + """ + bp_ratio = Decimal(backwardpass_ratio) + D = cfg.hidden_size + n_heads = cfg.num_attention_heads + n_kv_heads = cfg.num_key_value_heads + d_head = cfg.head_dim + n_layers = cfg.num_hidden_layers + + # =================================================================== + # Token counts + # =================================================================== + L_vision = vision_tokens if vision_gen else 0 + + S_und = text_tokens + S_gen = L_vision + (action_tokens if action_gen else 0) + (sound_tokens if sound_gen else 0) + + # Padding tokens follow the causal (und) path. When include_padding is + # set, add them to S_und so projections, MLP, and norms account for the + # extra work the GPU performs on padding. + S_pad = 0 + if include_padding and split_lens is not None and attn_modes is not None: + S_pad = _extract_padding_tokens(split_lens, attn_modes) + S_und = S_und + S_pad + + # =================================================================== + # 1. Embedding / Projection Layers (outside transformer blocks) + # =================================================================== + embedding_flops = Decimal(0) + + if vision_gen and L_vision > 0: + patch_latent_dim = cfg.latent_patch_size**2 * cfg.latent_channel_size + embedding_flops += 2 * B * L_vision * patch_latent_dim * D + + if vision_gen and L_vision > 0: + embedding_flops += 2 * B * L_vision * D * patch_latent_dim + + if action_gen and action_tokens > 0: + embedding_flops += 2 * B * action_tokens * cfg.action_dim * D + + if action_gen and action_tokens > 0: + embedding_flops += 2 * B * action_tokens * D * cfg.action_dim + + if sound_gen and sound_tokens > 0 and cfg.sound_dim is not None: + embedding_flops += 2 * B * sound_tokens * cfg.sound_dim * D + + if sound_gen and sound_tokens > 0 and cfg.sound_dim is not None: + embedding_flops += 2 * B * sound_tokens * D * cfg.sound_dim + + # TimestepEmbedder MLP: Linear(freq_dim, D) -> SiLU -> Linear(D, D) + freq_dim = cfg.frequency_embedding_size + timestep_mlp_flops_per_call = 2 * freq_dim * D + 2 * D * D + n_timestep_calls = 0 + if vision_gen and L_vision > 0: + n_timestep_calls += 1 + if action_gen and action_tokens > 0: + n_timestep_calls += 1 + if sound_gen and sound_tokens > 0: + n_timestep_calls += 1 + embedding_flops += n_timestep_calls * B * timestep_mlp_flops_per_call + + if cfg.predict_text_tokens: + embedding_flops += 2 * B * text_tokens * D * cfg.vocab_size + + log.debug(f"embedding_flops: {embedding_flops}") + + # =================================================================== + # Pre-compute per-sample attention dot-product FLOPs (shared by + # forward and backward). Und tokens self-attend causally, + # gen tokens attend to all tokens in their sample. + # =================================================================== + und_attn_dot, gen_attn_dot = _compute_per_sample_attn_flops( + n_heads, + d_head, + B, + S_und, + S_gen, + split_lens, + attn_modes, + include_padding=include_padding, + ) + + # Softmax FLOPs: ~5 ops per element of the S_q x S_k attention matrix + # (subtract max, exp, sum, divide, plus the mask/scale). + # Same sequence-length dependency as dot product but with coefficient + # 5 * n_heads instead of 4 * n_heads * d_head. + softmax_ratio = Decimal(5) / Decimal(4 * d_head) + und_softmax = und_attn_dot * softmax_ratio + gen_softmax = gen_attn_dot * softmax_ratio + + # =================================================================== + # 2. Transformer Blocks + # =================================================================== + total_block_flops = Decimal(0) + total_attn_dot_fwd = Decimal(0) + total_softmax_fwd = Decimal(0) + q_dim = n_heads * d_head + kv_dim = n_kv_heads * d_head + + def _dense_mlp_flops(seq_len: int | Decimal) -> Decimal: + return Decimal(6 * B * seq_len * D * cfg.intermediate_size) + + def _moe_mlp_flops(seq_len: int | Decimal) -> Decimal: + gate_flops = 2 * B * seq_len * D * cfg.num_experts + expert_flops = cfg.num_experts_per_tok * 6 * B * seq_len * D * cfg.moe_intermediate_size + return Decimal(gate_flops + expert_flops) + + for layer_idx in range(n_layers): + is_moe_layer = ( + cfg.use_moe + and cfg.num_experts > 0 + and layer_idx not in cfg.mlp_only_layers + and (layer_idx + 1) % cfg.decoder_sparse_step == 0 + ) + + # 2a. Attention (PackedAttentionMoT) + attn_und_proj = 2 * B * S_und * D * q_dim + 2 * B * S_und * D * kv_dim + 2 * B * S_und * D * kv_dim + attn_gen_proj = 2 * B * S_gen * D * q_dim + 2 * B * S_gen * D * kv_dim + 2 * B * S_gen * D * kv_dim + attn_dot = und_attn_dot + gen_attn_dot + attn_o_proj = 2 * B * S_und * q_dim * D + 2 * B * S_gen * q_dim * D + attn_qk_norm = ( + 5 * B * S_und * n_heads * d_head + + 5 * B * S_und * n_kv_heads * d_head + + 5 * B * S_gen * n_heads * d_head + + 5 * B * S_gen * n_kv_heads * d_head + ) + layer_attn_flops = attn_und_proj + attn_gen_proj + attn_qk_norm + attn_dot + attn_o_proj + + # 2b. MLP (separate for und and gen pathways) + mlp_und_flops = _moe_mlp_flops(S_und) if is_moe_layer else _dense_mlp_flops(S_und) + mlp_gen_flops = _moe_mlp_flops(S_gen) if is_moe_layer else _dense_mlp_flops(S_gen) + layer_mlp_flops = mlp_und_flops + mlp_gen_flops + + # 2c. RMSNorm (4 layer norms per decoder layer, dimension D) + layer_norm_flops = 5 * B * S_und * D + 5 * B * S_gen * D + 5 * B * S_und * D + 5 * B * S_gen * D + + # 2d. Attention softmax + layer_softmax_flops = und_softmax + gen_softmax + + layer_flops = layer_attn_flops + layer_mlp_flops + layer_norm_flops + layer_softmax_flops + total_block_flops += layer_flops + total_attn_dot_fwd += attn_dot + total_softmax_fwd += layer_softmax_flops + + if layer_idx == 0: + log.debug(f"Layer 0 breakdown (MoE={is_moe_layer}):") + log.debug(f" attn_und_proj: {attn_und_proj}") + log.debug(f" attn_gen_proj: {attn_gen_proj}") + log.debug(f" attn_qk_norm: {attn_qk_norm}") + log.debug(f" attn_dot: {attn_dot}") + log.debug(f" attn_softmax: {layer_softmax_flops}") + log.debug(f" attn_o_proj: {attn_o_proj}") + log.debug(f" mlp_und: {mlp_und_flops}") + log.debug(f" mlp_gen: {mlp_gen_flops}") + log.debug(f" layer_norms: {layer_norm_flops}") + log.debug(f" total layer: {layer_flops}") + + # =================================================================== + # 3. Final norms (applied to und and gen separately after all layers) + # =================================================================== + final_norm_flops = Decimal(5 * B * S_und * D + 5 * B * S_gen * D) + + log.debug(f"final_norm_flops: {final_norm_flops}") + + # =================================================================== + # 4. Forward pass total + # =================================================================== + fp = embedding_flops + total_block_flops + final_norm_flops + + log.debug(f"Forward pass FLOPs: {fp}") + log.debug(f" embedding_flops: {embedding_flops} ({_pct(embedding_flops, fp)}%)") + log.debug(f" transformer_blocks: {total_block_flops} ({_pct(total_block_flops, fp)}%)") + log.debug(f" final_norms: {final_norm_flops} ({_pct(final_norm_flops, fp)}%)") + + # =================================================================== + # 5. Backward pass + # =================================================================== + + if freeze_und: + # When freeze_und is True, the understanding pathway gradients are detached. + # Backward cost: gen-pathway projections/MLPs, gen-side attention (gen Q + # attends to the full sample), gen norms, and gen embedding layers. + # Causal (und) attention has zero backward cost. + gen_proj_mlp_flops = Decimal(0) + gen_norm_flops = Decimal(0) + for layer_idx in range(n_layers): + is_moe_layer = ( + cfg.use_moe + and cfg.num_experts > 0 + and layer_idx not in cfg.mlp_only_layers + and (layer_idx + 1) % cfg.decoder_sparse_step == 0 + ) + gen_proj_mlp_flops += ( + 2 * B * S_gen * D * q_dim + + 2 * B * S_gen * D * kv_dim + + 2 * B * S_gen * D * kv_dim + + 2 * B * S_gen * q_dim * D + ) + gen_proj_mlp_flops += _moe_mlp_flops(S_gen) if is_moe_layer else _dense_mlp_flops(S_gen) + + gen_norm_flops += 5 * B * S_gen * D * 2 + gen_norm_flops += 5 * B * S_gen * n_heads * d_head + 5 * B * S_gen * n_kv_heads * d_head + + gen_norm_flops += 5 * B * S_gen * D + + gen_embedding_flops = embedding_flops # conservative: count all embedding flops + + backward_attn_flops = gen_attn_dot * n_layers + backward_softmax_flops = gen_softmax * n_layers + + bp = ( + gen_proj_mlp_flops + backward_attn_flops + backward_softmax_flops + gen_norm_flops + gen_embedding_flops + ) * bp_ratio + + else: + bp = fp * bp_ratio + + # =================================================================== + # 6. Activation checkpointing recomputation + # =================================================================== + # When activation checkpointing is enabled, each transformer layer's + # forward pass is fully recomputed during the backward pass. This adds + # ~1x of the transformer-block forward FLOPs (projections, attention + # dot products, softmax, MLP, and norms — everything inside the layer). + ac_recomp = Decimal(0) + if use_activation_checkpointing: + ac_recomp = total_block_flops + + total = fp + bp + ac_recomp + + log.debug(f"Backward pass FLOPs: {bp}") + if use_activation_checkpointing: + log.debug(f"Activation checkpointing recomp FLOPs: {ac_recomp}") + log.debug(f"Total FLOPs: {total}") + + return total diff --git a/cosmos_training/cosmos/tools/flops/wan_vae.py b/cosmos_training/cosmos/tools/flops/wan_vae.py new file mode 100644 index 00000000..11aff78c --- /dev/null +++ b/cosmos_training/cosmos/tools/flops/wan_vae.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FLOPs estimation for the Wan 2.2 VAE encoder (Encoder3d).""" + +from decimal import Decimal + + +def compute_wan_vae_encoder_flops( + B: int | Decimal, + T: int, + H: int, + W: int, + *, + dim: int = 160, + z_dim: int = 48, + dim_mult: list[int] | None = None, + num_res_blocks: int = 2, + temperal_downsample: list[bool] | None = None, +) -> Decimal: + """Compute forward-pass FLOPs for the Wan 2.2 VAE encoder (Encoder3d). + + The encoder converts a pixel-space video [B, 3, T, H, W] into a latent + [B, z_dim, T//4, H//16, W//16]. It is frozen during training so only + forward-pass FLOPs are counted (no backward). + + The architecture: patchify(2) -> conv1 -> 4 downsample stages (each with + ``num_res_blocks`` residual blocks + optional spatial/temporal downsample) + -> middle block (ResBlock + single-head spatial attention + ResBlock) + -> head (RMSNorm + SiLU + conv) -> pointwise 1x1 conv. + + Args: + B: Batch size. + T: Number of pixel-space temporal frames. + H: Pixel-space height (must be divisible by 16). + W: Pixel-space width (must be divisible by 16). + dim: Base channel dimension of the encoder (default 160). + z_dim: Latent channel dimension (default 48, encoder outputs 2*z_dim). + dim_mult: Channel multiplier per stage (default [1, 2, 4, 4]). + num_res_blocks: Residual blocks per downsample stage (default 2). + temperal_downsample: Per-stage temporal downsampling flags (default + [False, True, True]). + + Returns: + Total forward-pass FLOPs as a Decimal. + """ + if dim_mult is None: + dim_mult = [1, 2, 4, 4] + if temperal_downsample is None: + temperal_downsample = [False, True, True] + + B = int(B) + flops = Decimal(0) + + def _causalconv3d_flops(c_in: int, c_out: int, kt: int, kh: int, kw: int, bt: int, bh: int, bw: int) -> int: + return 2 * c_out * c_in * kt * kh * kw * B * bt * bh * bw + + def _resblock_flops(in_dim: int, out_dim: int, bt: int, bh: int, bw: int) -> int: + vol = B * bt * bh * bw + f = 0 + f += 5 * in_dim * vol # RMS_norm(in_dim) + f += 2 * out_dim * in_dim * 27 * vol # CausalConv3d(in_dim, out_dim, 3) + f += 5 * out_dim * vol # RMS_norm(out_dim) + f += 2 * out_dim * out_dim * 27 * vol # CausalConv3d(out_dim, out_dim, 3) + if in_dim != out_dim: + f += 2 * out_dim * in_dim * vol # shortcut CausalConv3d(in_dim, out_dim, 1) + return f + + def _attnblock_flops(d: int, bt: int, bh: int, bw: int) -> int: + vol = B * bt * bh * bw + seq = bh * bw + f = 0 + f += 5 * d * vol # RMS_norm + f += 2 * (d * 3) * d * vol # to_qkv Conv2d(d, 3d, 1) + f += 4 * B * bt * seq * seq * d # QK^T + Attn*V + f += 2 * d * d * vol # proj Conv2d(d, d, 1) + return f + + # After patchify(patch_size=2): [B, 12, T, H/2, W/2] + t, h, w = T, H // 2, W // 2 + + # conv1: CausalConv3d(12, dims[0], 3) + dims = [dim * u for u in [1] + dim_mult] # [160, 160, 320, 640, 640] + flops += _causalconv3d_flops(12, dims[0], 3, 3, 3, t, h, w) + + # Downsample stages + for i, (in_d, out_d) in enumerate(zip(dims[:-1], dims[1:])): + t_down = temperal_downsample[i] if i < len(temperal_downsample) else False + down_flag = i != len(dim_mult) - 1 + + cur_in = in_d + for _ in range(num_res_blocks): + flops += _resblock_flops(cur_in, out_d, t, h, w) + cur_in = out_d + + if down_flag: + if t_down: + h_new, w_new = h // 2, w // 2 + flops += 2 * out_d * out_d * 9 * B * t * h_new * w_new # spatial conv2d + t_new = t // 2 + flops += 2 * out_d * out_d * 3 * B * t_new * h_new * w_new # temporal conv3d(3,1,1) + t, h, w = t_new, h_new, w_new + else: + h_new, w_new = h // 2, w // 2 + flops += 2 * out_d * out_d * 9 * B * t * h_new * w_new + h, w = h_new, w_new + + # Middle block: ResBlock + AttentionBlock + ResBlock + mid_dim = dims[-1] + flops += _resblock_flops(mid_dim, mid_dim, t, h, w) + flops += _attnblock_flops(mid_dim, t, h, w) + flops += _resblock_flops(mid_dim, mid_dim, t, h, w) + + # Head: RMS_norm + SiLU + CausalConv3d(mid_dim, z_dim*2, 3) + enc_out_dim = z_dim * 2 + flops += 5 * mid_dim * B * t * h * w # RMS_norm + flops += _causalconv3d_flops(mid_dim, enc_out_dim, 3, 3, 3, t, h, w) + + # WanVAE_.conv1: CausalConv3d(z_dim*2, z_dim*2, 1) — pointwise 1x1 + flops += _causalconv3d_flops(enc_out_dim, enc_out_dim, 1, 1, 1, t, h, w) + + return Decimal(flops) diff --git a/cosmos_training/cosmos/tools/visualize/__pycache__/video.cpython-312.pyc b/cosmos_training/cosmos/tools/visualize/__pycache__/video.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60e5be3999ee5a4f4d93900f5f5ade0b3c2dc3d6 GIT binary patch literal 3900 zcmcf^ZEO?Cb=K=$f31@^PDpYDGmty5xsZ?quE3?VkZ>s|N>1DzDM8dM-gUCZ-gRf! z3C?YLP@UAIYD>`lzz8b%aX+ZIAN*(~ezsMARO+AjoXXY`q)4a!$iL8_BJrnvv-T#y zrPr!eJJ!y7GxO%ndvD&n_xz_|(2t^j^5Z-}HtorH$G!HNJMGK(#{)LbrTL67F4#Dq4rc1c>i`c*o==(a zdLN=csZR3Bg5;A~xj|-RR~ww0w0p@fhopeyN_r*XQ*OL5?~2v0Qt@J}gfBiac6p{(i}fhmtH>iM{$C0NR%)_8z)ync+a)nz1< zMzgF$Av5bXLDd-!p%i?tH-qlATgvNToA3efqiq%$o+<`hY_mI~Duff*2d*kUi%fSa z`06?xnT+H%T@Pw+>5Y@g|oFe|_EL9l06wvmQ6S z9{cC)V^AJ-pdO?%(=G;`Lk#@#=n8udO*1iWgiu9R{|;Tuy-E$RYEMwX?Ip`S+567H znBQU(Q@xfqnapJ6iNut}i@K4|%9cybT3k+1jl-7DfUDHAnl4*`vx+2ZV?;4zVtEr& z#(!+zy}qQ|PXu$L_kv?(mAxu+TSux*UiU1CcxA?v-0tN`dIr!R0{kdjEWX!Cgp~ z0&ia(toTA@-;SbhM7WO@-o1fC|g$@A({jTZIi7@;7Fn6NeWAVC} z$);ti&K5qIjZbTF`HDf>fzaZKY~+ZVc=ck<-UJDQY+JlfiIaHe%vq!k z09BnM%O*57qQq7&b3;4Nrd+^hi#O`5r8fUsqPV)D|GVt2wP;Ekq zr{PxO>FMM*1|8Tf^@f9OLSu)61FxM9z6m$v`X-roZPGiKvrqEUNGk;#OQt#;icM&| z>EK)0(9vBIz6d&8X8CXS5#o~SR7R?w^%#3Mc!M=wZ|1+^!<)=)WO{C+U%FJrWRfna zK?<4dRhQHVV^#{^VNLFH<`)b^OrOcZ-N0CCqIdIuvTU9NjjHK9 z)^k~!^6qvN<_mdp_$5ZOE4Sr z5*kd&ZrV|4Z57ELO2A*97Qvq_uYGmkE*1-}q|*dMeVsN!Be)f^36#u4TqCtQV-V(P z{L}viPwOgL!w5=%cjwANrP}Lvue`tcw)%f?O2d{NoSSIFHZ2#-p}KCEQkdV^^X3#)Q?G@x*R!Bj2tLMddrc+#mM1O zPPyQdpVuF1POfFNAEZG=yIhZyl~5#hBFy z1?*}ofRT|{*y6#J!NCbFx(E#%gqCsSJG8UC^AKPbFDt4B1$Whp!M_PD@CZ$GgnFRW zR4seEa)y%DyK4W+Y|kc`67^9c7Tyq{zW6IdPSRQGuP?m)5Cn2*`4}02Avh?~W$4ye zhGCv##OyAjU0RM29xz*se*nQ5gS7wv literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/tools/visualize/video.py b/cosmos_training/cosmos/tools/visualize/video.py new file mode 100644 index 00000000..d4821ddf --- /dev/null +++ b/cosmos_training/cosmos/tools/visualize/video.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO, Any, Union + +import numpy as np +import torch +from einops import rearrange +from PIL import Image as PILImage +from torch import Tensor + +from cosmos.utils.easy_io import easy_io + + +def save_video(grid, video_name, fps=30): + import cv2 + import ffmpegcv + + grid = (grid * 255).astype(np.uint8) + grid = np.transpose(grid, (1, 2, 3, 0)) + with ffmpegcv.VideoWriter(video_name, "h264", fps) as writer: + for frame in grid: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + + writer.write(frame) + + +def save_img_or_video( + sample: Tensor, # [C,T,H,W] in [0,1] range + save_fp_wo_ext: Union[str, IO[Any]], + fps: int = 24, + quality=None, + ffmpeg_params=None, + **kwargs, +) -> None: + """ + Save a tensor as an image or video file based on shape + + Args: + sample (Tensor): Input tensor with shape (C, T, H, W) in [0, 1] range. + save_fp_wo_ext (Union[str, IO[Any]]): File path without extension or file-like object. + fps (int): Frames per second for video. Default is 24. + """ + assert sample.ndim == 4, "Only support 4D tensor" + assert isinstance(save_fp_wo_ext, str) or hasattr(save_fp_wo_ext, "write"), ( + "save_fp_wo_ext must be a string or file-like object" + ) + + if torch.is_floating_point(sample): + sample = sample.clamp(0, 1) + else: + assert sample.dtype == torch.uint8, "Only support uint8 tensor" + sample = sample.float().div(255) + + if ffmpeg_params is not None: + kwargs["ffmpeg_params"] = ffmpeg_params + + if sample.shape[1] == 1: + save_obj = PILImage.fromarray( + rearrange((sample.cpu().float().numpy() * 255), "c 1 h w -> h w c").astype(np.uint8), + mode="RGB", + ) + ext = ".jpg" if isinstance(save_fp_wo_ext, str) else "" + easy_io.dump( + save_obj, + f"{save_fp_wo_ext}{ext}" if isinstance(save_fp_wo_ext, str) else save_fp_wo_ext, + file_format="jpg", + format="JPEG", + quality=85 if quality is None else quality, + **kwargs, + ) + else: + if quality is not None: + kwargs["quality"] = quality + save_obj = rearrange((sample.cpu().float().numpy() * 255), "c t h w -> t h w c").astype(np.uint8) + ext = ".mp4" if isinstance(save_fp_wo_ext, str) else "" + easy_io.dump( + save_obj, + f"{save_fp_wo_ext}{ext}" if isinstance(save_fp_wo_ext, str) else save_fp_wo_ext, + file_format="mp4", + format="mp4", + fps=fps, + **kwargs, + ) diff --git a/cosmos_training/cosmos/trainer/__init__.py b/cosmos_training/cosmos/trainer/__init__.py new file mode 100644 index 00000000..b13a0170 --- /dev/null +++ b/cosmos_training/cosmos/trainer/__init__.py @@ -0,0 +1,460 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import inspect +import os +import signal + +import torch +import torch.distributed as dist +import torch.utils.data + +from cosmos.utils.flags import INTERNAL +from cosmos.utils.context_managers import distributed_init +from cosmos.utils.profiling import maybe_enable_memory_snapshot, maybe_enable_nsys_profiling, maybe_enable_profiling + +try: + from megatron.core import parallel_state + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + + +from cosmos.utils.lazy_config import LazyConfig, instantiate +from cosmos.model._base import ImaginaireModel +from cosmos.utils import callback, distributed, ema, log, misc +from cosmos.utils.checkpointer import Checkpointer +from cosmos.utils.misc import StragglerDetectorV2 + + + +class ImaginaireTrainer: + """The base trainer class of Imaginaire. + + All trainers in Imaginaire should inherit ImaginaireTrainer. It contains the basic functionality for model training + (particularly suited for large-scale training), including data parallel (DDP/FSDP), model weight average (EMA), + mixed-precision training (fp16/bf16). + + Attributes: + checkpointer (Checkpointer): checkpointer object to save/load model weights and optimizer states. + training_timer (misc.Timer): Timer object to time code blocks and functions. + """ + + def __init__(self, config): + """Constructor of the trainer. + + Args: + config (Config): The config object for the Imaginaire codebase. + """ + super().__init__() + self.config = config + # Set up the distributed computing environment. + with distributed_init(): + distributed.init() + # Set up parallel states. + if hasattr(config.model, "context_parallel_size"): + if config.model_parallel.context_parallel_size > 1: + raise ValueError( + "Both config.model.context_parallel_size and config.model_parallel.context_parallel_size are set. " + "config.model.context_parallel_size is deprecated. Please only set config.model_parallel.context_parallel_size." + ) + else: + log.critical( + "Using deprecated config.model.context_parallel_size. Please use config.model_parallel.context_parallel_size instead." + ) + config.model_parallel.context_parallel_size = config.model.context_parallel_size + if USE_MEGATRON: + if ( + "create_gloo_process_groups" + in inspect.signature(parallel_state.initialize_model_parallel).parameters + ): + parallel_state.initialize_model_parallel( + pipeline_model_parallel_size=config.model_parallel.pipeline_model_parallel_size, + tensor_model_parallel_size=config.model_parallel.tensor_model_parallel_size, + context_parallel_size=config.model_parallel.context_parallel_size, + create_gloo_process_groups=False, + ) + else: + parallel_state.initialize_model_parallel( + pipeline_model_parallel_size=config.model_parallel.pipeline_model_parallel_size, + tensor_model_parallel_size=config.model_parallel.tensor_model_parallel_size, + context_parallel_size=config.model_parallel.context_parallel_size, + ) + # `config.model_parallel.sequence_parallel` is a bool that indicates whether to use sequence parallelism. + # It is not part of the original `parallel_state` API, so we need to set it manually. + parallel_state.sequence_parallel = config.model_parallel.sequence_parallel + if parallel_state.sequence_parallel: + os.environ["CUDA_DEVICE_MAX_CONNECTIONS"] = "1" + + # Create the local job directory, save the config file, and pipe to a local log. + if distributed.is_rank0(): + os.makedirs(config.job.path_local, exist_ok=True) + # Save the config as .pkl for reproducibility. + LazyConfig.save_pkl(config, f"{config.job.path_local}/config.pkl") + # Save the config as .yaml for reading or parsing experiment hyperparameters. + LazyConfig.save_yaml(config, f"{config.job.path_local}/config.yaml") + dist.barrier() + if INTERNAL: + log.init_loguru_file(f"{config.job.path_local}/stdout.log") + if distributed.is_rank0(): + # Print important environment variables and the effective config. + log.info("Config:\n" + config.pretty_print(use_color=True)) + misc.print_environ_variables(["TORCH_HOME", "IMAGINAIRE_OUTPUT_ROOT", "ENABLE_ONELOGGER"]) + else: + misc.print_environ_variables(["HF_HOME", "IMAGINAIRE_OUTPUT_ROOT"]) + # Set the random seed. If multi-GPU, different ranks are set with different seeds. + misc.set_random_seed(seed=config.trainer.seed, by_rank=True) + # Initialize cuDNN. + torch.backends.cudnn.deterministic = config.trainer.cudnn.deterministic + torch.backends.cudnn.benchmark = config.trainer.cudnn.benchmark + if config.trainer.cudnn.deterministic: + # Use only deterministic op implementations. warn_only=True avoids + # crashes on ops that lack a deterministic kernel (e.g. some flash-attn + # backward passes); a warning is emitted instead. + torch.use_deterministic_algorithms(True, warn_only=True) + # CuBLAS requires this env var to select deterministic matrix-multiply + # algorithms (needed in addition to use_deterministic_algorithms). + os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8") + # Initialize the callback functions. + self.callbacks = callback.CallBackGroup(config=config, trainer=self) + # Initialize the model checkpointer. + if config.checkpoint.type is None: + self.checkpointer = Checkpointer(config.checkpoint, config.job, callbacks=self.callbacks) + else: + self.checkpointer: Checkpointer = instantiate( + config.checkpoint.type, config.checkpoint, config.job, callbacks=self.callbacks + ) + # Initialize the timer for speed benchmarking. + self.training_timer = misc.TrainingTimer() + # Initialize Straggler Detection + self.straggler_detector = StragglerDetectorV2( + enabled=self.config.trainer.straggler_detection.enabled, + report_freq=self.config.trainer.straggler_detection.report_freq, + profile_freq=self.config.trainer.straggler_detection.profile_freq, + max_diff=self.config.trainer.straggler_detection.max_diff, + raise_error=self.config.trainer.straggler_detection.raise_error, + save_s3=self.config.trainer.straggler_detection.save_s3, + ) + misc.set_torch_compile_options( + self.config.trainer.compile_config.recompile_limit, self.config.trainer.compile_config.use_duck_shape + ) + self.straggler_detector.initialize() + # Send a TimeoutError if a training step takes over timeout_period seconds. + signal.signal(signal.SIGALRM, functools.partial(misc.timeout_handler, config.trainer.timeout_period)) # type: ignore + + def _fetch_and_broadcast_data( + self, + model: ImaginaireModel, + dataloader_iter, + iteration: int, + ): + """ + Fetches data from the dataloader on the batch owner rank and broadcasts it to all other ranks in the Context Parallel group if CP is enabled. + When CP is disabled, data is fetched from the dataloader on the current rank and no broadcasting is needed. + + Args: + model (ImaginaireModel): The model containing parallel dimensions info. + dataloader_iter: Iterator for the dataloader. + iteration (int): Current iteration number to determine the batch owner. + + Returns: + tuple: (data_batch, stop_signal) + - data_batch: The fetched data batch (or None if stopped/not owner). + - stop_signal (bool): True if StopIteration was encountered. + """ + parallel_dims = getattr(model, "parallel_dims", None) + if parallel_dims is None or not parallel_dims.cp_enabled: + try: + return next(dataloader_iter), False + except StopIteration: + return None, True + + # To prevent redundant data loading among the Context Parallel ranks, + # one of the Context Parallel ranks (round-robin) broadcasts the data to all other cp ranks. + batch_owner_rank = iteration % parallel_dims.cp_mesh.size() + stop_signal = False + data_batch = None + + if parallel_dims.cp_rank == batch_owner_rank: + try: + data_batch = next(dataloader_iter) + except StopIteration: + stop_signal = True + data_batch = None + + objs = [data_batch, stop_signal] + + # Calculate the global rank of the batch owner within the CP group + global_src_rank = dist.get_global_rank(parallel_dims.cp_mesh.get_group(), batch_owner_rank) + + dist.broadcast_object_list( + objs, + src=global_src_rank, + group=parallel_dims.cp_mesh.get_group(), + ) + + return objs[0], objs[1] + + def train( + self, + model: ImaginaireModel, + dataloader_train: torch.utils.data.DataLoader, + dataloader_val: torch.utils.data.DataLoader, + ) -> None: + """The training function. + + Args: + model (ImaginaireModel): The PyTorch model. + dataloader_train (torch.utils.data.DataLoader): The training data loader. + dataloader_val (torch.utils.data.DataLoader): The validation data loader. + """ + # Leaving this for backward compability for now, but we can think about moving this to model.on_train_start for all models. + model = model.to("cuda", memory_format=self.config.trainer.memory_format) # type: ignore + model.on_train_start(self.config.trainer.memory_format) + + # Initialize the optimizer, scheduler, and grad_scaler. + self.callbacks.on_optimizer_init_start() + optimizer, scheduler = model.init_optimizer_scheduler(self.config.optimizer, self.config.scheduler) + grad_scaler = torch.amp.GradScaler("cuda", **self.config.trainer.grad_scaler_args) + self.callbacks.on_optimizer_init_end() + # Load the model checkpoint and get the starting iteration number. + iteration = self.checkpointer.load(model, optimizer, scheduler, grad_scaler) + if hasattr(dataloader_train, "set_start_iteration"): + dataloader_train.set_start_iteration(iteration * self.config.trainer.grad_accum_iter) + grad_accum_iter = 0 + log.critical(f"Distributed parallelism mode: {self.config.trainer.distributed_parallelism}") + if self.config.trainer.distributed_parallelism == "ddp": + # Create a DDP model wrapper. + model_ddp = distributed.parallel_model_wrapper(self.config.trainer.ddp, model) + elif self.config.trainer.distributed_parallelism == "fsdp": + model_ddp = model + else: + raise ValueError(f"Unknown distributed parallelism mode: {self.config.trainer.distributed_parallelism}") + + log.info("Starting training...") + sm_carveout = int(os.environ.get("GROUPED_MM_SM_CARVEOUT", "0")) + if sm_carveout: + torch._C._set_sm_carveout_experimental(sm_carveout) + log.info(f"Set SM carveout to {sm_carveout}") + self.callbacks.on_train_start(model, iteration=iteration) + # Initial validation. + if self.config.trainer.run_validation and iteration == 0 and self.config.trainer.run_validation_on_start: + self.validate(model, dataloader_val, iteration=iteration) + + if self.config.trainer.save_zero_checkpoint and iteration == 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=0) + + _end_training = False + if torch.are_deterministic_algorithms_enabled(): + # Re-seed all global RNGs after init (model load, checkpoint load, compile warmup, + # callbacks) so data-augmentation randomness starts from a deterministic state + # regardless of how much RNG state init consumed. + misc.set_random_seed(seed=self.config.trainer.seed, by_rank=True) + with ( + maybe_enable_profiling(self.config, global_step=iteration) as torch_profiler, + maybe_enable_memory_snapshot(self.config, global_step=iteration) as memory_profiler, + maybe_enable_nsys_profiling(self.config, global_step=iteration) as nsys_profiler, + ): + while True: + dataloader_train_iter = iter(dataloader_train) + while True: + self.callbacks.on_before_dataloading(iteration) + try: + with ( + self.training_timer("dataloader_train"), + self.straggler_detector.profile_section( + "dataloading", + self.config.trainer.straggler_detection.analyze_dataloading, + profile_cuda=False, + ), + ): + data_batch, stop_signal = self._fetch_and_broadcast_data( + model, + dataloader_train_iter, + iteration, + ) + if stop_signal: + raise StopIteration + except StopIteration: + break + finally: + self.callbacks.on_after_dataloading(iteration) + # If max_iter is reached, exit the training loop. + if iteration >= self.config.trainer.max_iter: + _end_training = True + break + # Move all tensors in the data batch to GPU device. + data_batch = misc.to(data_batch, device="cuda") + # The actual training step. + self.callbacks.on_training_step_start(model, data_batch, iteration=iteration) + self.callbacks.on_training_step_batch_start(model, data_batch, iteration=iteration) + if not model.training: + model_ddp.train() + assert model_ddp.training, "model_ddp is not in training mode." + assert model.training, "model is not in training mode." + output_batch, loss, grad_accum_iter = self.training_step( + model_ddp, + optimizer, + scheduler, + grad_scaler, + data_batch, + iteration=iteration, + grad_accum_iter=grad_accum_iter, + ) + self.callbacks.on_training_step_batch_end( + model, data_batch, output_batch, loss, iteration=iteration + ) + # If the gradients are still being accumulated, continue to load the next training batch. + if grad_accum_iter != 0: + continue + # Do the following when an actual optimizer (update) step has been made. + iteration += 1 + # Save checkpoint. + if iteration % self.config.checkpoint.save_iter == 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_training_step_end(model, data_batch, output_batch, loss, iteration=iteration) + # Validation. + if self.config.trainer.run_validation and iteration % self.config.trainer.validation_iter == 0: + self.validate(model, dataloader_val, iteration=iteration) + # This iteration is successful; reset the timeout signal. + signal.alarm(self.config.trainer.timeout_period) + self.straggler_detector.generate_report(iteration) + if torch_profiler: + torch_profiler.step() + if memory_profiler: + memory_profiler.step() + if nsys_profiler: + nsys_profiler.step() + if _end_training: + break + log.success("Done with training.") + if sm_carveout: + torch._C._set_sm_carveout_experimental(None) + if iteration % self.config.checkpoint.save_iter != 0: + self.checkpointer.save(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_train_end(model, iteration=iteration) + self.checkpointer.finalize() + distributed.barrier() + self.callbacks.on_app_end() + if dist.is_available() and dist.is_initialized(): + dist.destroy_process_group() + + def training_step( + self, + model_ddp: torch.nn.Module | distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + data: dict[str, torch.Tensor], + iteration: int = 0, + grad_accum_iter: int = 0, + ) -> tuple[dict[str, torch.Tensor], torch.Tensor, int]: + """The training step. + + Args: + model_ddp (torch.nn.Module | distributed.DistributedDataParallel): The model with a DDP wrapper or, the bare + module, depending on whether distributed training is enabled or not. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + iteration (int): Current iteration number. + grad_accum_iter (int): Number of gradient accumulation iterations. + + Returns: + output (dict[str, torch.Tensor]): The model output from the training data batch (dictionary of tensors). + loss (torch.Tensor): The total loss of the training data batch. + """ + # Only let DDP sync gradient at the last iteration of the gradient accumulation window + with distributed.ddp_sync_grad(model_ddp, grad_accum_iter == self.config.trainer.grad_accum_iter - 1): + self.callbacks.on_before_forward(iteration=iteration) + with self.training_timer("forward"): + with self.straggler_detector.profile_section( + "fwd", self.config.trainer.straggler_detection.analyze_forward + ): + output_batch, loss = model_ddp.training_step(data, iteration) + self.callbacks.on_after_forward(iteration=iteration) + self.callbacks.on_before_backward(model_ddp, loss, iteration=iteration) + with self.training_timer("backward"): + with self.straggler_detector.profile_section( + "bwd", self.config.trainer.straggler_detection.analyze_backward + ): + loss_scaled = grad_scaler.scale(loss / self.config.trainer.grad_accum_iter) + loss_scaled.backward() + if self.config.trainer.distributed_parallelism == "ddp": + model_ddp.module.on_after_backward() + else: + model_ddp.on_after_backward() + self.callbacks.on_after_backward(model_ddp, iteration=iteration) + grad_accum_iter += 1 + if grad_accum_iter == self.config.trainer.grad_accum_iter: + with self.training_timer("optimizer_step"): + with self.straggler_detector.profile_section( + "opt", self.config.trainer.straggler_detection.analyze_optimizer + ): + self.callbacks.on_before_optimizer_step( + model_ddp, optimizer, scheduler, grad_scaler, iteration=iteration + ) + model = model_ddp.module if self.config.trainer.distributed_parallelism == "ddp" else model_ddp + self._optimizer_step(model, optimizer, scheduler, grad_scaler, iteration=iteration) + self.callbacks.on_before_zero_grad(model_ddp, optimizer, scheduler, iteration=iteration) + if self.config.trainer.distributed_parallelism == "ddp": + model_ddp.module.on_before_zero_grad(optimizer, scheduler, iteration=iteration) + else: + model_ddp.on_before_zero_grad(optimizer, scheduler, iteration=iteration) + self._zero_grad(model, optimizer, iteration) + grad_accum_iter = 0 + return output_batch, loss, grad_accum_iter + + def _optimizer_step( + self, + model: torch.nn.Module, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Execute the optimizer step. Override to customise (e.g. PhaseOptimizer).""" + grad_scaler.step(optimizer) + grad_scaler.update() + scheduler.step() + + def _zero_grad(self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, iteration: int) -> None: + """Zero gradients. Override to customise (e.g. PhaseOptimizer).""" + optimizer.zero_grad(set_to_none=True) + + @torch.no_grad() + def validate(self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0) -> None: + """Validate on the full validation dataset. + + Args: + model (ImaginaireModel): The PyTorch model. + dataloader_val (torch.utils.data.DataLoader): The validation data loader. + iteration (int): Current iteration number. + """ + self.callbacks.on_validation_start(model, dataloader_val, iteration=iteration) + model.eval() + # Evaluate on the full validation set. + with ema.ema_scope(model, enabled=model.config.ema.enabled): + for val_iter, data_batch in enumerate(dataloader_val): + if self.config.trainer.max_val_iter is not None and val_iter >= self.config.trainer.max_val_iter: + break + data_batch = misc.to(data_batch, device="cuda") + self.callbacks.on_validation_step_start(model, data_batch, iteration=iteration) + output_batch, loss = model.validation_step(data_batch, iteration) + self.callbacks.on_validation_step_end(model, data_batch, output_batch, loss, iteration=iteration) + self.callbacks.on_validation_end(model, iteration=iteration) diff --git a/cosmos_training/cosmos/trainer/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/trainer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e69a4e64ef2c9dbac38143fa6df0011ae8c9ff6 GIT binary patch literal 27642 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

0j!IFIZ|7MVT^a#K2P!jKP1NJq& zU7FfX#dafrvq;Ro(x&}g2zDNVtTsr(;PrXN$*uPEkA%Kc-?`H$54FR8A7pb|Wl zSkxOdrC;h>9u+y?KJwg5X?A=`dH;dh`8l=oOKQ`X)W+wuFHh5_`58rmpD&hDbP;$@ jXiC#Xu6K@LHKkw2k>tOmdp1%ZZ#4FF>pt$%!0-PDm`RF% literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__init__.py b/cosmos_training/cosmos/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmos_training/cosmos/utils/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e6d550a32555e4ae1962aee7ec7c530cddf4a36 GIT binary patch literal 231 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 z%0D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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/checkpoint_db.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/checkpoint_db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dff586ccca2d5d28fbfb878aa3ca7a0c43e90009 GIT binary patch literal 16770 zcmb_@ZEPE7p5F{Pe3wW`)Qja8c`P}SXiJoB*|Fo;UOSRv*^cGJvNt*2-qMt2BvGMA z-3)14gq&42*<1M<)bj1NR_<;QrMPw#BtX;N72Qv53iLyY0tFTNAZAwEHE>%L`=#aD zDq0x*(BJNN+l(M_=T)f1rZCz@wkA}z>! zB0D6n+$#CxofCpo^S%(-W%;hf_wLenE9(9D-edU=;JeMno#&*w_c^Iv4osgZZ{JKm z#r`QoM5%$zL?Z>!bDvx*x7s-L8q`}@V|m}Fy#765Yo*>DY@SBU(xX@(Xfj)`hXUWWU@k?L38XrCoAJZlxJ;2DGx>SlI#Ae-HY%$)fE3 zRM1xvImpWQqFj^@8ngAV(tRjxx0LqEJ#sIbEzD-?pxIc<{b>2ZH=I>poQoWi56i7` zpT2*rx06Qja8ehQN!?iI5h)}c_-mfs$U%80+ksQ+dEXi7m-*=_ebv@j>NRd4{J!vM zsVZ`8o7R0*MnCkv)9Cl&@6qqDrQdP+#mI>{Tj)saCpg-{SUNTqQ)MwVAu4i0MphEX zr{wXsXH)THS`B->*QR7qDmR;n&8n7q5p8588B2)QuU=5abV{7fj3wgZ`e!UDi7{18 zjmKkYSrupGbc_ZPyXEj?*el(o?CVcfeTWKq@&ApX~OE2$@V=I^BLqK5_W?@$vEAL(-lO9aQg7@; z>}c=Ui6e)`j~_oWadcw5y@z#B`%jxA4aSwxetjS|m=ZtTuJ)ho>l@3Azb&WxW@G88 zzI3Y3JfPAjA$_o^iLHYTPnd%#@@z_tr&G$@>GqT|*+<`{-ZYmY-;AsARPuCtIcKR< zGhWXv@oD{^uiS3Nc-UcY`}utyy2r#tHr zQ;L{KjpIUTmauqbHjPEa5{bDUF#|BUF)62`=Jul*C4Pgh{bno?$KA>z-FY&l%-}mK zi)p|PhQ-yI7uQjm%e!?L@wE8t*xlcB{2?zu1{?TxgMBNwpdnu~j^Nu3Top2O$c@Uk z0m-cw#kt2MS?a+pB@$wnl)9Cq63c0KZ9#wuB{d_Cr_`C0 z8qTES2{k;Ch)t?ud}cPKq{Ry(*M_c+oO{jdjZff4%z~2)?w{>#E+#6Cl}D+v3NPz# zX$=^_#f+(GYtr0e@m2cBxKX<=Uy4m{IM}Lt;ELDpdM2xDa32S>wrvD zd(I`XEx??ld9KaP%I6aCn2K^?R8CB2?$k|LQR0$}k{v^Dy*_mH!sVfnYv*2z4h#&8f)gjFKEL@kK_w9f*m)KDmMD0k}ij zsGYWPr=^zVl;`I74leC1x7g1qcAonqZq9a#yKVm??iL@iXZKxYYY+*N zL|keKxd?P(3<<*5UJVJFKr~Pj2rD!@&Q9}wHzc9U(;ibcQkz+Xe`>1K2>YbDPJBL+KRA%{4d#TwC#dDBuRejA2i!b2Z9p5f)q}*BA?gmb zpxssk$yRQm)T4y~lV&r?UT#5S;0%Op>xeTk z_^jqt(=jEj-ioKEv>HX80ZOWwiHZ2_XU?bAU>U?w6cMY5;ip2dr$cJ$SZ_M8)^uRC zKi|~1Fj8g% z^PuXLZ+mFQu_~c$4$WvW4I#HONH!d5i6lsNTG<^=66U$T;(lf;9kL_4V>FhGr-^G; zoUyVS9h6;&eoI%Lt+9$EA%UHi(uSJyA3c} zqzolWDk~>H;kAGyPsB2bbaVpj338q0E=#y0As0-PcyMZ zsm2wJN~v)ayJzR5STY?S*Sz|=2IJ#tt%e~~bezezn&Z{DoB&~$P^E;iFD(#_#&jMC z8BL8wKj9eEGm@ipp@kkHqxK{E5%;yw_bAx8bYeZ&xfbkPed`y}!{ETeYrwCiw;p`| z{`Ws0eAx8T!tkP$7hWpb><+g+9u1}0ftXwH$zXqPq57K%k{U=G`V5a8KmZi zd^PsSh))~XS2fvEmsrCLJF;~bl1v0G$(AR@Y-?D&4DKc}8YB`861y1UbcO5;Ta+Ny z&5#FF`y}#Kf?XFaOSDn7pCJlC(mT|u4-sgR&wuaC-80Kw4}I+${vEkp2UiDHkL36J+1X++Bl8u3`9SptJ0=lF9+1=G4 zHINJ%yw2^r$HIYi&b91RF!KRq%w+rf}gRrL;BjN zT&pqX8|~NJ<1AWpXki)_> z$xIbto>y#=D;aoxE6FXnKl6NA5@qh#(=Fv;rb*hW2=n%761^%ynQus5!U@nFU-sW$ z%_PO)%;Y4bUGde}xGWC5cEM0{pn1*AFark^UR^n00=Y#Z)D@-cDoh_SF)81IHRGA# zprnx2Pbzd-fwHipu*i%rGGuBA`ud!gECuB*5c`px6H^ITS(w(H7M0W(XkM7L(}b(% zMlNamA&~Byx3l)CiL;u!JV~~;w->B!CYC-ut)`MA*Fa~SYFbKV(kzAUqzP$oJk7zl zqb88_8uDwll&ZPpe(wnHzRX&zunQY~d-x6WftH0HzgU!@l%HokvwqdQC;x8D+S?5 z!C(L0<$^HsXvgk_p~e2C#=9r;!tR35^vDq`2p!A)UkM#g?3}X^AEdwUT<+N9kmxLs z#;p$jM^W=!_;*Tn2Hk=Mx?OXQkh4<*Rv89Wf5%o= z9VY12NV21jV6Y;fluo-MOgiuIX=3s$%o_?M*l_v%imRp$O&e^u+?V4_=Bv6c(pRbK zhy1*aglOe$6g>6y_7eW~reQ79989@*>ZLN>^c-?2bSsKPQIet?6um*w>xeX`ZpwuG zOwSvPQnLz`PNz(k8BAq&ngR|)QjP5@F31PmU)tk_Q1d{S&r9?oVEU=xaBH+j&FnL>)Av?_rI1ggcJ3 zA@oj@AX5>h0~H1MtUmL+qbzpM+vf$krw@(_+y{dKwKoJ_b2OqHWDc6HA|g*Z?l|Xp ztP^&`JC1qhG}#j?g5;QYOd8@h&wbE>nP}h7>*IX3k106MJB`zIRo|m>;14*hZN@5} z)*aV3j7hS2MVNP$SIQ`#%R>2FpnNVy`8583GK-GNpHieC%6fWxOHfSN5*&wWH6el5 z8s#>%o1^F!BF!TwGvEw(YcQ#y@+MUq1e1tk8e+OmGgZ`q1~D>ej0G!CEKSd)s>a7P zSLwq-Mv3f$jIyAsa+TTNOD<DjT_+c8SxP^czdxUE*#A{S?`m+> zvFO@pXf_HLT?JoY-M9NI-|oVm{VS8X-QmZat@aYXH}M|GewtuAldO+_Rcq?>oEJclL{}eEY?v;pI2};?nQz)Us%&{t%q2lW_>sM8AuE zZpU5>?K!%5zEIOh1b_LDSG#h%kNka2KP>m2qmNqJo?HK$z^)SgH@2`5Yj&x==0ac5twq+a{s-@_^;-~%z zB4QjTAMMz=@Y+&GUf5X>+6#O4Eu3GxzSO^LUpjF&niuvlD;wCL*j}~53-(g9?a=uR zk`li`PUkl?pVgQ!Jh6rQl&RvA1AviAx$4`RTV?PhQ@cQ(8C5;-r+~S(3dNGWpBXqA z@yTBFaF_U-CsG6b#KSZex8yZSYb~X|5?}M9Uri|=K)$w=uao?;3;YiLFJ)Yh)R)TY zpx-peL3qQsP|~Q+1iz97OWP*1UiXDDMroGd;Sya;KY-%>OU$ae3Jj*;uSR|3^30Wn#Q)6I1#RFC~ zW{Miy&jb)Zdv@oFN2CuLd~vv*7Km~PexKjpih@*(IZe}4ybe17>Rt>2p^hlggV6oZ(yrxm%hE^j597dgJ{LHUb01(}yG2JdHv6yHW=%7f z(p-Ie)nw^W&XlC5iHueRc2ov;H!H~iR^}f(ap!Nz_scMYL$)yvlWi%hgJdmYCKKjB zBZ3R~FrRXlBDzmzBT=qX?jKY1=M-I{sH#?IA^xQ^FDr&b_to#9;{x~Je8B?FoXFwJ z_g~J{wEf2IyXU^^&IJ#w9{rbRJ~@*+a57i_@^r&WloQJPnnYYtPWlxsC(Q-aILNKs|qX<1~Poy$QctND_ zysg}d!{=lqm6XFH&^kn{1TQF-2Yh7|MR?)2k0(^!hEEGB8;JZfvAHohO0EuM!q4tC z8ENS(w>CINC%O*B9w&A@f$%Fwo*BI0*K}f}8sb$}{LEk!Xv) zsJV^%c^1~oRWH-coJChgUh0DDfqiR%eJdULK{Z$mFgNx>fsjpLzl)bZYMHZ;M`)yp5p<%y9}o^bEhx^lk*wClh=Y} z*DH(5P9;*4)lBznvx;>kXe?I^(Qb)a;XT&9YkBbROGp~pQB`E=M@xk)C(1F3OyPxI z&z0j85hqf9Owl4mWY1CFLj;jjfyaiDp3{jD(IGIY1ibcie;b8}seY9YDEF5XRjABA zqB4qVY2E)58IpX&sxOV++V?ILAPZ4imVKJ{Z}a*Hxu~su;o_2gNxJWauv8Gl0tzlI z1((wIcjh5a3A-P;8}kCpOr3>N<`7fSZH|Maq9b73 zAT{sL|7p*fuMK2wd0u~M9?I=`HQ#VPS989wyZ3{drO~h4t-o`jH3P?2L6B+~9F`mq zm5;Cz0}in?lV$xGzN%BcJKQ&Lc!NgX#`EF3&GWWumY8a=I7-qMNk$dH^k7l|+mP!F zYp3a%rCi6u;2S1s$N zwxm1uq=~JtP}+}m9CCu#f>pqt&TZTsOq#+Rubw)lCg5D6)s1fK5m)x+_Z(gv zDX@WmY$JB&@ci5t9$oZWb=8xnoMr-?z5RXllgui32#HJ1@-ro7X<4f z!Ku?q{Uv)f+?}^coaEYR(1bhoZQHpd&eBe>V7Xm#&kK0YCk}~F&FWe{1?Uvs5#?jF z%C?LE^ke1MO7>b0b>gI`yUo#9S+VRQR5Edw8?6*5d-u4!HO zb*%Y1R?a`92k19*ht7Xl`^67(;WzX6@bwh@yVm{PYyR%lmc0M)!udxvL3)H)?#S1) zEevhAyo<-)%PyV!;X812D)_^zcUDum)5%;-3P2z50_Z(nh}VIJdsBC(mNLs%mt#wF zc|S~D1(e;sdwbcj99te+_T&TY3&R_}U4`JzLVZi2zID?j)HwgQn{x%;oqBJ&=n)*c zr_nV7UkEdZK+z38`M-#)@=}SS(jBl+4c<-Oz&KPLIF#|`R_-g@4D8N%X)32`o!5vR!$ zFDavaj19;BYJ&b^*RyP)a_aJS(b|)XV&X%1P0nO6KvfR*W(}wfNw%{fbZPDuo3&3=m9PPCoEr&i&%ouHaw4^3nMZ&wn)X;mGQlhr3UfJTgae2Vc&;d@0xZ zTHb#-=e}IcHGB(&xrr|;g{fvN)h zV{Ba^>$LK>h(f+fbg00c7pP?g0@1N5fpdF* zeCyp?KYr)kcb1Y5T`yG1q-FH^yTlh~@-H$G);&r>^9=~i+vAWv%>P%s3caNJ8spg@ z39tAR?6QAMn}36%?^5&@MRkZYdu(i6AMk5GEn zRIe99sl{t6xlXFOiHexE)}Sl?*1yB)>O5T$)M3k?<#aRrH5!);W|}aDwd#M~VOo($ zHFTAl=N0_#=`#JbMR%tsTZiU9Hv#>qq^hM9h3xRmf13FuD}(eoM9~R~<|(>C(HKQc z@XJu{J&Jxr(S3@JQ^f2Cq#9|Sgp!Fe9>;__Mn4&BXu%4$NheofmV4?xrT^>TjPf5b z7?Bfo5YhwQTY3Jm80Lj1H+Vev{*v?klJoqMtNA5I`CoG#zv6cMn(O)pF7Ruv=f893 ze#Py6RNJ%w5zomtEsqvCBrDfoMdO=_4K{v=r!q>ZkkX>p!S7t@C~`RM~++T`$2+#&F7%jkz>1^tlV<=E4()RAimFWp{m39q$; zbA8{*x17y2oLzIDTX1YT1-@Zv1ioc_!=|6-dzS8Oa!5AaJb!p;hVqADVS#ggbZzIU zg_^~9o;y`^HuJs9M_240zEtFpr3E3`Jkr4LTpC#!*yNDeJP_m?X*498Jt5>V8j|8$ ze8q5^&XasQEibgz658bOQOxpOZy#opmilu$x^qFu)}cHXE^x<-PCp+8S9-!BEw*z* z{6+pT=5wy7MM{2gjU``>KcTPU0nX-I=Ns2BudR-+U+ph)NIr*U6A7%4wkB3y>@?aH zhAuqjT5-zuA|=0&Sn}mKD=hUudGUnVWB9elTqAm5a3o(evLyG$HC9^cf{J38YdMhP Lf(7mEI@b literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/checkpointer.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/checkpointer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b61e999db6ef4bcf2a03f0b936ebffbe478db553 GIT binary patch literal 24106 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/cluster_env.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/cluster_env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e68c91c9d4ec9918aa39b4df941700cc314312bb GIT binary patch literal 7572 zcmeI1U2NOd6@V|PpUAQ-Igab3>Cc$dtu4i|m!Gb!=ec!c!S&|9%1*Xv>oi)VY&sID zT~c=KIYZh7Sc4Xbiv_5IWoY&TtU(_3vWM+ubJ_wmuopRLYlCQC*1hayaN@kQPdn$5 z)L&#b3Hmq!;w5?i&N=t;eCJUAY-sQkIE*`Ln&b$1n@B|BG9-Z| zpW(86g6Htsm2qX=2{*fTXFOSN!pjk!3=qk4j!0gazvi;WN$g@zcfnI%*;C&Xk?MZW zCF-E`L+R&0r50=#jW#7!>j*6u+%}F8I2lIL5 zM!7M~KMYlAx72vflh}Pf5bKyPzL2*XHoav6K`7D%m8JbU~#MHEm z5Brsrkt#_{2rLUbwqWI8fn<{4B$D7IF5!~+gj;eYJoE0b$E=TL^130@iTRvtdNNu{ z%$P29DrI`~OrB;(!d}ytOsZm5P9{x%GMUw+dh)SV#coV42?$nhD_JUK-Bb% z51ot;UnN^8z?bK{Z~`iKrS-Y4oJNhbmQgglE2ZgKC~{PrkyD0qpPUqRIcZQ)Q5AKn zE3eB`@0u6YDf}6Br=*sXRb9)|lTG9jp5mrurxv)-cf;u% z*$C`88{6=Q&PL(t)fjDZ+B}dogSIte>g=GJ)=UqmqohoqBpafVktNe5W-?$?T94BN z@YDb&({rM4d@ySI`ePG)gF~@?)73XLL{UOZpgnl?kaKzzE-J(iJ$&{>(Bajn)fyXy zU8WxvBl+FY6Uo7TGl{&m1z#4g}Y~-n0VSOI-K}WH56i#mw!`a)!aRmYL3PCJ&;jL0sxs!DY^QKqPllV&bpk!}F)yl96 z)|DQD*^k3d-wUU+peH>BY*V$!M{9Ga7Wl8)mHE+=mp-CGoZAN8C1UN zm?fYTzhKGtRZ77^TMg-BFw9ln0uPM>K%#TGd%UfuS|jcm*ejwE+q=}Z3TB5 zQvjTuJS%5X#mxjAyzpZi?Z1p_i)80@INc7DJ!iO~7d!gN1un%aB*niBhCIV9at4mb zjv1eGRVrtR&b`_4Rjn2|PR#pvRR-}qJnSsWLE(!ezx)(hsr|sq5{UiS7~Zy9){L*S)ekECc@-U4FYvAsU;7<^Wf4|z%29TsCvy+ z>J$bo|8Hpm0ARCNCM?OR7>?a_%kViWOL7{hAPJ}C`Lb0vrbR;#DFQ{+6$w@VY>(8h zR=;{)hxHvo`D73u)D|4f3=`>wNDa1jC0XYbV_GdRo=yRC_(a3dQ}}fz|RWDLKa$6A6i~MmfI>o~)QZTxSJ0=;+KQY!Mqdm1GN(Jk=K;r=3tiyYRFhoA{X-bX1%^ z!XA_~yy~JDCYao5lbhAin}b*sQD)Xp$p(TP<3Wmq8-kzycQ^s%+KI2>VsO2&b*-`W z#;(>+BR!vl#;&}y!hI0`FmXNcUTBHCc=YY=x4M_bx4yI1DBK8*6?|hGb-|0B>rLUc zrtpor@TZZEKS!=cJ_!w9dFij*KLURXtepDz*`d374i|jGcF)$erq&yEt)Cuv;!pd3 zw|_mabNr^ zH2lTqp$;7b!pnQ3ZSKFc!0GzYmS~6jy*fOefl`a90MPJ)Er2F6#18{M zg8aJ!&_Q3`8JIgPb|<4h0u}-cKw`S4>qiyP#$`+fBg|JuMvM=oc*9ZKDGyspcMCT0 zg>A{JWbtzXhCvvGFy`8OrD*x>t$ny?R6wE?Vd(Ln;bdwb&#stSA=$-p-h`-2L;F%S;g;GNUxX?y`B1yWW+gP^ivNYrGIT zAG*xH_0>zzxi)&n(!0X1_0e1^uAWgReb^D-fzO|3=^}l>%lXr86mRfHU zD7IXLZ(t8@1&fuDB{x<^ij|SF%66+~d$F>8Gf2YEthe{9wfEd2k8sD9ezA$gO7lu; z_1Mbv^~bmHcC&{ZdTu?^yB6uaMIPmjEkC}A#mezj*XsDH?|SbR-fs4i!{1)-cww#M eg{g@z literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/config.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..204b255f408458a6d16a6100e9503d2da56727df GIT binary patch literal 23048 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 G!2b*U{Uka7 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/config_helper.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/config_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54e9f32f4e1d7e9ed00bbc9bf3c1423726fd3c81 GIT binary patch literal 10416 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/context_managers.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/context_managers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4118657328bcd18745ce88092c6fc0e2a6929410 GIT binary patch literal 3445 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/distributed.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/distributed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76256fc78af4a078da8a2cb948cbd4dd5c1aa7dc GIT binary patch literal 22332 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/flags.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/flags.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72c2a22eccaf559437952f95f5299249f65bb0ed GIT binary patch literal 3227 zcmahLTTdI=`OM9CFpvP|7EGG7v9=3k7q->r(hx9&gcuq_wzArtPCUoNtnHy^#_U3A zq)PkL@0%GLCvDXu&3C@rIp@2b z@62C1Isybn@kH4$0s9t-=J2>k;~RmHCuEK2Bt>*V7jr^NfLY3kc_}3c$dhw&UP&o7 zuH-yO=M0ZsO_0qrq5VTY8x&pHw_6MDAy{DxUZqqr_rgOGUr>jk;x2Xem_JN)K)PE$o z=XJyG-Bj4P;Lz=G<45h3gX_6p4F6#(li6hkuc67V`3^wfme}?F(&wXvS z4!7xywCVJ>>6~+PXqbt5IMqQVI|HNOS+r?e(~eXG{EM`xfo(B1w$nLJHLJ*IG?UZT z&GCPtp^2El{kmpp>6~VoK*;ezF%PpcpDk#)H0m@*&_xbxo z>JsEup&nhPvaV3C?tu@hL&iwhIsx6cC%5eKZ~J2b?oC<@Pu;nALrZ_OX)r5lWTGxJ znqfvhy0UFOFbdIk$0sMpxrb43qmXuEZCiyn=C&~i;0f6!8#eoOkR&gJ#0&6kOUo50 zI&0Eg#>BPJ@Bc87E1DLg6B+a42{a;OeQ_01i93TU8Z&7WJc25WT1Hfh-ZzXK)e7S=nadU{QZ7t#X`7l{ z*k*lrG+tw;=ADEE)lJ+)#f1!e-w#j$GYiQGc0InlfBDhX%J6h0I9-va9Y?SXxL2nc zt(ddYZW9L{Ay^FdMf?pQPlyH385`|dO`g(}p3b}Q;56IW@5Now6nBLru`qP@*cF>i z){elX1!|poZMQb3WmperloqzK%qZk(!5ZfTA|e_l_qtpzty9Z(I3_uDeF+X=m>v^7 zXjQZyD%x_wGY)*zGVu*4k$;3bAK%))^*FJgIGlRcQw?1x#lHx4)xv${&M$*~2gPb| zq#}>lG2`Lc<>Z~^q`G={W-+mtSXkx0CfbrMCRXEj6EjQm+}A{T$Xy4EZ^yY}PiZ*5 zHJUfLfwlW)Urx!olv1|kn8H1CbSs;tTuv8tjZ5jxBA4N6Xo;AIVS{GaI~jHwF3+yb z&2VXUWsS>s;;S=k3`KYwo$K-*VhFCoXJR$%ky^03G*=6ROS6D|I_GGk4)1V1n2Xc)Y+Ty+1o425St{A{4dFC z!NE-8Xxs;I8k{vkrIbwZu@dlErQQ_$!~iAQls^@)DaodSsSxeJXU(QMQ(;@C*i=`l z+on87ZF^%rcscQT(l))Ryea)s1O*(IUM7c+I65-vV?7m@A9-*IXQkKO!)pBb3 z0mWw!)|-h~&l{;>@4z`M4uH#eV|iFrwL-zbMrRaERb@Xxg?R*12o?}rLx9h=oiuhG zu|)(o5ZprWMuz8*hu{PF%%21JBl)U#q;#{aSLKn~Db~|pikGLVa(}INs5D=mugXKU zvqPnY@+x8%uU6#YL;Y{^Ri_?s$JihnhKaoefCpPYAJk-+EG|>{7hp1?9&39O8XLnl z!A20+9rQM0*r^!aN5|nM#O&DEUfTuwW8}S!pdW!9gzJdeUZW|HtfiTS zG1u5HKnd@cIS;Td34-ullnA}w68|eQ_KNhrB9Rl1PZ%zz52j8CAoV^`c<;ch6POOI z22S;WBAh#bR|2MI7wqZzR0FBwE;4Ywa{k?ytJTQ+C13gCfxQ1#mAqdUB|$z8xWct+ zbBc_&#}`*rowhZPbzy!cFh3Z{NoBa_D-B8T9wq;wOWNHO>?(IZ@|69Dl1 WiNQLO$34W~`N&&RYUFLZ5dIA(%JIAa literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/launch.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/launch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e45c0f4e10f0a8a9f17abe8e2e57996cbd3bb6fc GIT binary patch literal 10325 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
    mU%dbM`-PTn;Jitz+DSR*=UeX|(As*{;IGuJ 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

    cqNk9 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

    wH4utTzW427?`i1kbN(~m?x5-YmQC^_mOhn9 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 KbyUhW-~Rz@^%V60 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/optim_instantiate.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/optim_instantiate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ffee4563bd58f95cb66f20749cb025efa10f030 GIT binary patch literal 3804 zcmcImTWnOv8J;<}z5B9jd+~*2F}7i`YsUmijU5vMcB+_*Xw;%QiB^X_$Ln2o&)Lj5 zE7xXMaw4P%3DuBPwNY9@)kp==N_i_0571QVOJ8;|(yj&~QH4rPpB60-H4jz)IeYP9 z+!j^pNN47sng2Hb`RDuR&mNBxLHpCM#aI%kZ`h>TJQbq!Pasy1f)pG_GvyJ_V3>1p zoZx0S%+~lgpAcpQpaqqSSz4=#%Hu4ji06eF$$=b5X;P%ExfGjfQ|$0LRJ-C-9g0iE zv6kmGv01F`#kE8=UmU{>M#wg5ZxD1}(nilHTfevzuDQ5th zp(a%#n`*$EQv*qv$O+X{i4o9etrhtB^Ex>jh-#)D05T9EdMarg3@BQQ2yF|64c)3JSDI14~M1!y*-6l>INSublcXoCjkz_rTUVE>Pu6WX)op?JzL zi8?fE%nv1XV$SOEsBR2}bt3^olISrtY*yzX=5)v;a#V|Ik)f0UVHsMGwFvvnicm;T zs+ysvNLa0|)FdiuQ)V=74CzTTng~TT!<02M3IQKXE>M3&HA6&=q~bCuI}C@0_!1v@ z?T63!BXmos=tO&^wgx-Jk2- zzt(#o-#lGrxix&}(CtH2#%abl{iu5&-@P~2y?3p9U*0!)rDt`=>Zx1!j`*c`+je*O zw)>NbtZ#DZSk52a-`ZA?SilLrUg=F4WiQMW`(w(NXppE8t^FiJW5MPQT2jXIWzV;9u9bHTkts2 z_OxRWOlF*B&65}d%S}z;(@sVBO!y^N)sMky+QwuVY$@U|_z2Fp(!3&_g}9MU6M(ba zwkkO>cXfAT%(%5KkhfX#4#loG;}**ruom%hA$ss4yD~`*DQ{r zwD>vxDSih9T_>Ka04!jy!&e%EZW>{$3cL^XD}&ScKUIUD;}wk2#Wn#gpx^QH_y#_4 z15;5;CDiy0j&g&q5mt{4h7o}P@xt)TF&0rZ^(RR(zSzsIi*+llJcur;WN<95hvm32 zK3HQLWExoE{2Thm61vwhe!Y2h`09Zx2d<9fI>y&atYUoB(S`p%MzH<8Nk8@pz*GV^1WeTH-1;&^|f&=e0mB!~u5Q4hDiw3zaBu zq!NV1klMo}YDOXYR5W!Go};2ALq(?OH1(J9Z_vWOLCa>yGRm-kI)5a`Q|crkItexq z_V(FulrZQc+Zda4c3vhCgEm4TDuX~7CuuWVcn<5Kq@)t{JO=f$mFJi$n#vW#3LkZq zCl>Hh8yq1NjvItIX*56wg`=)nIZ3@QCL;MB@b)mFGGsq(LVG6g-PX0>TREX`>14LG z@3FrF67afyzhivOKb{p@9=3PE&&<9yalh;ETKnOw&{k;c%LYetZKF#k9}2dQk9~M7 z>)Uhv(5>O@o;R}13g=W zhhG0m|KZqjwZ8E}hs*;MUBox-Q*=QtO15V&%kjLhwzeBfzOz6uk z#&T>^i`4*fV*0i`RGrr-eoE{I~dKfeorS#YjpxRY}9~8+!#%Ya<}K*-8px6k>`BQqJ&(XPi(xe zvnV3>cS=ZdOGPA!5*Y!TL5}pnFcoYh<1m&Z!ZLum>tUn}JkoL9%0u?=s5l$fWfNe@ zBvd$~>+y0R9Vgj?iSGY7p-&NK$gXFb+M1ZTLW@>oSXt>>}6nl4qcJQ7?%PF zdmbS7*Qn*MsH-SB@$PHh-?aa#y@Y@)b|e2Qh1QNIt^nRsaCw$TE{qg;pdZ;iMG;0g zuy0$@#wa^#ZYeq#=h_LP!G*c|d4GW*Yi%9DD&g#-`_9La z_{07?Kcgx5whRWt7rj9bFK{D74;}mI)M7C;!LJT}YsET>x<*|yT{EAeXgIeR6-YJ9 zP6C?jFC`yPPduw?LbHMXxTu^kp^a*8Xh@}CR4W4VjjLZu8nCVZ#xIz+;PKLcd z{ZVQ+*Uf@cth1b8ml`OFlnHohatM^Fxl9@}`?Pl%b!?h?wz039rl={?lzBkw z)4gS!V1x>F#6YE*%cMSi%=|9(p5~*}UBWC+KB?+VUo{sqtG)RR?q~yd3)Gn#e1-}A zgf6O$>Z7^{R7T)tCk#;oobGEB#T!miKBIc#e@6?f+CQ*G&1KTS)=XZ_J*CGswOu9+ zz^&$n?=i~u+%YI>Q1g^=8XxZyHAHn${aw>XTBR<^Y~Z}zkB_&3-n)KK@7+JB_nyal z-!*?^P)hew6UHbVHF~PlR#D?GG_$nKuYqT0sBN4Qex$Yu98`0eRDD?!Mj!PH8b=rh z2G~sT^q7XG&I9kmk}KNt)END*+A-Q?>6R=)nDh^M1mf-I2=a{H4{6EBT><9n313R{ zQ=9~O8S;{~&Cu>0Eyy2QQ28wtrSkDll?ss-CTJneO#^xOVj#_Vpm8e1^wBgYo6tO^ zzeVkpNuy~l0(=0ZfD!^$HJ3@F8lUE_Hgnd3U9HQ@Uzz;-&EZ?m#Ca#QFKZa>K8|@A_%^I34ykXLvTg9@Qd$^BLb|e5UubnbF57 zm$_Rq`vs181%Ei?=_Po#k+SyD;Sqmmkd&w2$GB{%y>*F?ObOOXQQ8M*^}IOHCT zM5E$=gOU7aECC=w3Ji*r}G5*j%SYibKFejOY!=bPc4*B~f11!-W;)rA#A>jdk z5LPW&1uq%o1P`v2mz>;{kuVWF{X--?%qc5(8DL6iFFE^12~o;CFqbFfh2aa<7fQN| z+#oItHZb5Hlo%XWGRbFyop|IAEQHC)$w$|kf}^lMu4#ZDYr=U4!a;wSZ|V>8!;rwF z0j^(AzdaYdyu2hhztCXQD9;hTY1|tc#GjDr(AwE1x`Z={k-Zb_|4Ld6MgYw|Y z3AjhbC7ZIoA#cbRW#USTNpOyPiFx>AA@~!T5%~2xoUqbZVB$eFa z{%YWxV2`by5{yzZvEu;YqwsTbH(>|E>)kH>Rxe{%i1Yu-d0PW1aCb_nh$Y#D^1e;dn{wvh~PHartcbO!rc8 z{ZvQN?!0k%_Ug>lL|Kzq)|4naB*LHlP|{JHa5RXHhPm$ej^`KlE*3B97ejH^xn;+X zSJZKL|9a0St-owtYB{#Zi7ltTJQm;G6YqH?UUL4vwQmjfLPfUssBeVz_6z>YoSW@M ze#*0B{$P;3$g#ZlGQ47V?|(#w89A&R6z3Tr+?(t$+$=U4B%BvWh85s&*)U^Zh_Nqd|ChK!VCWzzjaP zLjpuYd>($tI|2<0)YS}g!(lQmnaBLX5a`t2{!5Zo&JTxi9Nx8=puR>>P)Uy5JD05L zIJhfPge{Q)62usiHW0ojnUNkn^3x!w0Z2xbo(L+NGDVUy%#~nmdSR)3>SS3Z8B_BR z$&kuPjvo02IC2!v-!F_y+G9QEB<9$;_M-$}on+14GeKp5G-HHm!IhNIgV6%BBlQ%oy6* z2isby79m2b{|!fj$%TbwCQIxCPc63!i>b4S8?Ky)6M zJpFq^*-CNg?D3i7v!`cH-zoi|^8Lz0?LM(~--2za_{AyHilsDRsSzzTOP1O@FWs|j zTd@{QpNgHjX_{-lZ*6{HuT0qMM0?$med`qSXU1%Lm3~-GnXJ>sm@)1=yx?DWHC}jP z*?97Sy>NOgHa2}VcJ-!z+1{{zoUbctZg0M|IZ@FpRx~Fn+Qf>s#o8PCDSgsjb@TF^ z{?^}!_NFNY$@9(Y<8kBpc;)#e{-=m%L^VFsnV+1H)h^Qlf$(Ub1tkV;%kb@n+Q6w3}K*YBfMtY}>=p$h8P zC`*BPm9m*?S4*jaP04M|Q{Btfx?~Z1&&qyVh2>cJa5Gg|pQvaOE1H%nnx_hq1x0V0 zzpYiP|H>nf_~{ALNhAH+0_%xG+TT`g!TcfJNuA-}X#?cbdKj_~)MM~vt^FJ5VSsD( z*9`wRxHhUGf8KybokxcwS|5Gg5W4sj#P-9mtwz<-u0Nxm;?q78wl(;eyE=7j(2>&G zR{(igP6!}}$_c9eeO<4ij#Gen^-~7Gy!y9{K7%^$gjPuF`GHM21Hh-x7&ENbA~h7? zNqPhKk_VTjXJ<@Qlu^*dIADtmO32zR`}nE?$N_UgJw-MF)@AbHR5(dfMjt#6Gfn8O z>TpI($LO(a6k_gsWBk#2GE7OO}kP zkWu6XgJP~8gCtD4%I5kb;Eij*!Wa(p@@pqyQKxk9q)9R0CgW zas>5T(~Z`#Veb{t%)p2SqT4_aI!a9csLw0uQalah+5jSm6pLk*y+tE218%jZ33 zyN~vq?L5=#={ov~r=z>~lZNhOtbR1ajxDbcf;r*IzxWW9RExC!k!MtN4&p`(n z$byP4TcQdj6ex;m5b(Gg%({TffYC`$9y0JYNJnM{5_7=c>> zH2Q zCCE=D25k*8vm~dm@H7^h(!$7PB4@DpCAnBe^k{OBjA@u(s1&Az7b1C5i#_lS$cR9) zsymlqKAJ{=XF>p=uW(KVhm8E_dvQ(d>W3luR?ro{1Dx$Bp0)jh?M|WS*z%6;tFoFq z?H_c#-!xKhp`wqjoD6 z&(k8?oUWVa;x&hEbgtBI`{4Nd$3Hmz{^|Km3s;uvJL8Vp92I*1y|VqEX}&OgYWSky z(}MV!3(IX@#3!d_PQ}^gh4%Z!M^_xqS>uc`UiJLKiG}Cm6~~tyoh#*=ZWr7tSSoM0 zq5Ildc5`30rrsu2wTaHQmEtOdI4kAVx6QZAiE@`%?n;!mh~+Iy<$IG=JL8p&bLZyw z%x{YyI1?}Ld8pHtlmbX7ELLVa@)?6mT59WwpM6DadnJDUwfK?OK8YX|TmfD8sJH6jhn6ApPyi($YK9SwZ zh^z`i((WiLp_x#k)Gd~}6QvzuX~*Q5uMDM0OW|~DtTkb&6D@TK%Z_`N z9RMR^ryo>R-`;;~|LsG!4$bL5Hh*YNG#nNi4#$u5ELFWUb$TTWKF<~2vu{@|R;TCp z+;<#!U@uPCS<%iW>@LynTC(q$V!manO+)nmP#8aKO(X4&#o*!_@xtC^Xmyt~fh(4AnS0AElF1HesdP(E$U z5M{7b)rhW}WSD7~GRn?X#>W8AOas^n-II;@;AS;}9$`SZ3EBj@T=f~<2|%X~TrWT( z7$)=p8}$lojOz1gU!*Y$K%$W_C**TZsP`2}>rR_a_wB5Xaa7bX!j)UAC=2Ki3eH@qlUKYWY3L4LX_dsYmD}+bf zW;61ub36=kkPUiAL;XYO5|NGdz@3F5xgOhu_;Gbij%{r0_l^joglhy(!60`yd-S6t z{@nUTF6285cy5Fp85f4aA=d47%L9Yy9^58^G9^dcill%wyHhL|XFV*CQEsOyMPx(u z;X~-rX-zHvWe(g5JQ%AvFj0dGu`wlZ*nyyTkY^iGpK{NgY_{tpGpb_Obt%j0g^^O7 z2<%IQsBe|g#V&TBM$-!iE=`4gHR-Er^q#U9598& zWvwJ~aKo!iWAJ~$0UZ#5Lb<7as{QY~lAG(M+JAmV);!YE02)wfbZvzN;3+;kx{hgL z%w<|e%6U0xOflqfz+Z85Ie|Z)<>F0SLo$tgU?mA;KGQkif7%!gG4r18qtqEc^`B1i zm@rkI6-YZ*(^F7t9XR!M**BSgMKKz%K6IANmi_s6OMaZO4CTe}Pg-GGs^ndvK6`~v zIa5Ra9jrHDj>C(EH)~V~FS6}bq=eU5nI$5xL^n-__Q6etB+f?cY=h$2lD`kKk7!r# zi;=DA-U@~1?+iE%H%TsGgoLg5m1K?d6nui)6__&-B!FufRFA9&ibTU~<7G*ge zT%H2u%rci!3J^o`h#?WkDM(IMYa{zMPIrjnKE&~U3lX9?t7E$Bde_aDMC<0CcY(*` zCO9ykOFBv+J3SGbxMK!AbAHb!M}BoAjirt*)_>9XY2#u|yvGy2a4GHy#)HH0iqNtn zyv~lc-hG%z59p-?KLXT0HZsGmrN|&&#r7UvkTH*egAA z+hXw-)t^=`+T#^H_Z=^-8mPVPbll#Q0#thP8tjU^j?o(!A)k|3lmY`7@+KCbN=XPt z^2y>QrVm!Ku4ASMc>fuGJlfV}*rYMl-#NQRLGrE9VtN^H62=Nn;-OJwXX+U#3>jac zBK>@B_9y7IP03p2DN1x>#LR*Wm3{xL%aMhYELl=B3jpDo6;I&-$Vpl-#%8@k`B|nP zBMD*S5s20~8%MFU6C)Hz1eYxXpuERpfm&16Sh5~ZO`P3Ul*0$ESzbWJhf6-eB-5?mOBGt`Oc-n#>tc4G7jC#^q(n=AJ$Vk zUpTpNXz|T2tG=|w&%vV(#y!JvTWFaHe+4eZQ%m-S$xeXGx|U=?>GguI zEzX2x^F7Pv6>H)2$=Jz+wOX`RFIj66)?K1?*L~~mRV`($`EKgKvourubU!<`wWERl z%}%C$JN27g2OH`LMO*?0z|R$1%5~=Z+a^fJTe|qEEBCH3ZE#-_-Tj$=h5^U zOVLHYqfEb}EPtS^|3$g}lWJJCQZ-xSR2A5GG}Z)NDbkh6A07OLYNnm@EpoD^r8HHm w8YoyDqBD(-w0%SErkx8;IY~8wg4IA)qt+weP`l`s1+AROjsB#80(rsz3m4{U>i_@% literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/progress_bar.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/progress_bar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5982a086c68a1c17f28a6e90e10c8ce533c029e4 GIT binary patch literal 2399 zcmZuyO=ufO6rPn<(pr+E#IhYbhjge-Y#CQdXxf5FLS2&7fz)-dOR;)bRy!kk?bWV2 zGqS8A7dNEfme3L?xW^QFNO2F%p^!r_Irn1YLd_=8ai&`y`FS zI+;&M%UMK1`NT(w+pjj~npQ(g;aODEGD&MxLnuDQ{1T&_xi#39iji4?7;V@QhOi14c`Z?~P)gVauyyU(CU#?R$bQi8v@T zifeQT>?}`U;kO;(%Ax3d6g=8)l{j9h8DQqd<@d0|vF8iWq?~#}g;3F}971r&F@E{l z^!Pj1ub5K(;<+sTkYd~NFkJ-ws><6U1XATrx#Ci6aocf@M20O~1jT}7FW@4aq3m!D zS;PsgP>)d0b|}xr|B`i_%xFOZa1hKXR0SolOgWw@Bn)N~hMB8&tKw>kfK0)&Hw9u1 z3AHH&>xGgdS6dE+P$M)SI{0w#(m)PsXy0pJY12qZKWdQBI#f=Mn_4D3$)w>YZyg59 z@wkBVP?E({F10uXWYrFK1Qdv1!Rpv5S&o;@gacz@d|H7Q=;C8i4ieYO6`zSKjQI>c z>lQ2VAi7YKGmAkm9tMFi-v2V^Rsji`EAm=S+7x})@p;bnc^M|C?mV?cdu!f};zb#cKf%U0?^rzw*+TUVGRA;mK? z*AWma=t(T~$Vfj;^{tAp>tEEr9o$G|?w;99Wj2h=vv|)&@B3TvsSSN<542Ix375d8 zCX~72T6MeK!19}{f;|4--yjZG^Z)8Me^4_}ZVlP`>#bUo@WL=c%es)Lsl?(b_=sieTES=G@jM5#=Y^2y>$mWqKpRLk*pU8VK%nKgQN&{xGJ| z01^@>HGnkY%P}B~=yL4S*zGgT`4C|k)~+NBs5?GP8o&`43SmKOLPG#`(E1RxyX;xn zO3;O@UhqkM6x);us6Ft=k&lWVvdZra7~?WrDO{kqRJ9lZu2J`ti!r4kqmMv`%Mb-< z%(=3~79?Q+Z9%xQ<=GSu*Bmj|WTwm^t7}CpuL#by_~{7wV2e?22)bj@a$HGrqnIx^ z71cP@BG@kiMK~^mYNpyu1YDM@u5exhU$$h?9DaA|10X4RO5cN2s=P@-%<>lS*-Z0v zt&0WSIEv@xab0JD0wp<~0G^Sqt=YO(6s0vKCzGudb0JiQEeS`Io$jY%?1YSNq>+L>4Q9|K0Fr_U(t)w^JupJDw%_zU{g@c#r;;IKQEvfBB*lC3~JD z4XlmiKRaP~+2F&KOFb+K8X}EcS9|RzpS_z|eZOQ{6WwwqwWTq8kwYd_rQQb?@_&5a*ldFuE5T R*TVO85I=`5gnx;^%fDmNpg{lt literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/serialization.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/serialization.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a16bffec1b792f48fe06979e14596c3701530932 GIT binary patch literal 17426 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 I--PP^C$TuNga7~l literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/timer.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/timer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f74a9359758e3f8f53aecd33ca091e233f79a796 GIT binary patch literal 11884 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

    T$@4kaF?T z?Bg-zv0kM&t^^W_C!sM;=_gddEPcdJDU7@as3Mv^;$ue+SYeTs6*`5|1)o#sWBLfX zYP^sVhVeI@LE(#-fTq|WWvycALU9vBkwQBW3)mbT&9mB%$c#3C7*q|$$}oIz9GFg z1Y~`aQos9q^5_HK8~V4-zYv((LRT0sU-$i1F6*ZZWM?YsK0=0QL0bqQ6?s?M_iZju z(MC=C!4AblWqMDz&g^Ii+YW9ksv{-{%xJO|;KM4OTb38W6VyK2WiE`G$Ph;Pv^Z+2 zh^dD{osg5*ae|F_8GAdyX;}v`_{aLop3~|*WU$&NsfWITLeN9BaQMxA3l3-A1s*#r zU(bIzf2@C|Vhc7^dN_aKsOxqA%l=d@DSx#I{^XiluAlqg^(zmbQmP-DDSBM-Kh6}t zpDyKbC2N2Ok<;z6ps^2=mN?hEw?;YIuB<&bQ+8e{I1em{ICo^?*55AN@8jO}t>545 z%vjeW;%7q#yAD?Dczw+g8{F8(XCt`7NIF=dQRi}PK!jR>C>^a3D1K?^`jFzO)Hp>8 zLNjV(lHu=FU#!>FhRtKAF57{rk6?@CJ4m1mwbdGc4b<#bV0_#hqZ%Xcrf+Z5zI`Qw zrhQXw@0@%4+*IrIS|xB&@to8~Cv}YBh|{o68pO?Sr$9*0rZ7rulm>6=<>9T*C^mi7 zR0`4rLcqJ;!6mIbP+c)BmuL;MvIfDYkq6Dc=->#MT92&Lt%gar%T|x>e0|T$duGZu zegW+Ps}6aOQN~+wqiN5RQ%T(#Nx(S-`OL!~nSPc>l(j$2+08M*Q) z2qqu;JCyVL5r>0l;fRyYPOuS+fERxtv8eC7l>Qy{BT3w}^-$2DRGLI)h(y+(_9D^R zgN9tZMm;#pdhqk);>gaq;<^-mxpiY-{va183z!=yTW%%ijy#b%@`TdTu2i3!DLSwC z&;M=`GM{@lfBpU)nG%vl31hwoaAO^ZcJmxIk1@v;66nsDYhae)(9f7~N>RL&E|JF? za0~9N7P5!&JXmcR0n26Ui`0^|Rz#DzikH56{i~y29$Pn8yFFFAeI~F&@$C4+3R+9; z?WApyzTqs*0c|8MYnwc3rR)Y`64y##hTi@qYL;!RS>_9yjZ>18s;$b_gOls#wjW4s zKcH+obgLCRYA?Ne>DJTA={6;BM)92a4B6X*{C+#xi@|KNQ)Hq$dpWF0S`Y~|jEuE1 zUdq~lu~9+#1=6RbuSPm6%iVy5en|L!hnl7lS&;C}(%e_p%vEkqRc^MIy(^rM@Gw<} zRLR;2K}56`9UEy=vuBs=1IQy{mv`ZY$?K`Lw~|WRQ%bn|W6mekOdedM%O@4O+}=rY z`>Zav&sya+Wo0k7&*IHWRzX+?cm05eFZ_Uy;BbU)S{e7?-utQxvYceLJA%UqAG&$zR2reA zq`k*7hJce&qyaTIoQNmI-o7Lhe5yARmAkPXAx6}kE-@rgn9vjI!?Acdaj_q#=fyCU z4MkM1+%MsX5?PF3aw~-7XeiF~9ds2sW1;XRRlwHYq&S%0ip6 zS3We6eW7?Xth!)gDyr@-tcAoxs@DMa3U}kI4;;)kSQ5gCR7oV3VgteCWvEl4qC8lv zg(4@+%8bCws6A0>c~m2?P)o1Z3`@w7!Q4bF0wp4QG2v2k#NGtW)m0Y`Vnpq_Md)&* zJ6>9D?v@h$eHpV8g3U}4e1MykP=U5~&vvvUR)EgH9C5O@G%2=w>tb(R z>4|R|@!2vMpgGgmzsm4^CJD>r1W))t0^{AM{FDZ+wtFl|4y|4%IJ=8qPH_jnqTNfmcczH z>}s>Adbz~=GvXQ4IygoxuW&A}HkDU9_FyV+%dlhKe7--Gx5@gvE0x!1eSRpF*F5aF zU0L&nSF^L4texzfs-5bb3GSb%JfL_gu`a3(zbdxU@xc$(5= zyEpX!TJwtV(0)XQ99NwY$06>*1&{z8az#6~t@-LHhFC`<-bTc84>@g(MchWm$^46@ zIET@M=tJh4h>PZn5pFeB#UjF0*e%3)7dHfDTR(X3i-*!=5Ydm*vae631_e2gp@xSJ zGIhlhaSR)r#uQ2PN`HgWTG+g%O|#MX_Xxm}+J0Q5zk{dxpktAe^5tlmz|WTExf88_JFkpHILmzaP%V`$EEt%V=UAu#+GlD*npe|m$6I( zj{Z53)W1R$C@3m}uRrwiL$B-w3C!nKDs}gza_@sBP~~Rq{;F#Eg!ATw`4P{2aQ#T$ zeC>w0+6Pj#4@`MdwfjHeT!m{!j?b6Z&XsRTm2Vlp_{;JK=QnKoN!{edKWo0VKehcZ zEcuiTM@Mt#H#Uyu{bfbLJb8kI28NY?Kz81yP@<)Qs zG(CtQ&7#s4Bt{gj>3Fk&Dxsjkl5FNiaO?y6_(t~eAC=V2l{BVG8m)#Dn^R?94304* z#%jWl7*YP`z%lj}l9dN=zF;_n~na;`^e8Wd-^Rvb(p|(EAcB|;InzWZ#yM$RI))lc% zDS{t~(r%V=OR$qpVb`VMNWbIFmQQ131z8 z{+&TFu91c&*L0)C!EL6!x@|Np)Dvws(iSegQ(^sRnl`F#IxhSEovOF<{+$}_Nz@^# zhj5o1jz(2MJ4=$pE&x1b##W9q%vthuvhC2`|0&{TO+lDE%^)j7mi{gAkef(B$~ofz z$_0lz?g)+BPubP)TKLNU7?aDGDq8@TW@wuC$sqx4v;)DQ>1!k)yOKqSPI@ zwe41&vbJTW?1)lugzRGQ=8jyBx_zf+e-rm^)2aijoj=RTKTzuYS*d{g46JXG{=E>T zYxK@CxsoPJO$(WqQ7O$-otA3s^&A<^ZOG0~skx~$Np@}>+xf#i-`_LVJXte&=%**% zIx)HL77lVesCW)87o5a{8C3WZZtkU80}2cr*J5kc_sQj~e`1L0hTFZRF7oXp8xen;uz(?m{a5Z;DLbuc#h0#K;;* zz!G>qTLPbIW0v^~zI-9VkA@dGx{h@(vg-m4bUdVs!b1>+<_0Fpu%S|NAWz9f9#a7_ zCCT^jN_B=h!xD81J9AleL=)Noc4HA4hB^{`g6h_UlJ;f9?${BL?3^98Et_q&()JWE z!^$5~6k3$m7H)n)cTDKBZ`?$W#2ne7i8xVAI~{tTl#lx1$H^&p7(P6|807f;UvYt7 zaRtBPa(~5n|1($jS6mr>AA2}o!9T0G=3dHQ!{?1SmpELO4)C?SZ}`C_4wt3ld;=aD zmS8gBTY8c`95Ws$+EYff68;oViRil2w3hdctXkr5Svty>@DB_R(3=OAn#%Y-7LBe; n+k?mf+2OL(!mroT;Ii}>zZVbDB@UORM;!E~;Wr#E?34cw*H!WA literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/__pycache__/wandb_util.cpython-312.pyc b/cosmos_training/cosmos/utils/__pycache__/wandb_util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd854137ec97cd64844d9add07c4a59ddd5970f8 GIT binary patch literal 7010 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 IfllPV0m-ynDF6Tf literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/callback.py b/cosmos_training/cosmos/utils/callback.py new file mode 100644 index 00000000..7b8a62f2 --- /dev/null +++ b/cosmos_training/cosmos/utils/callback.py @@ -0,0 +1,618 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import sys +import time +import warnings +from typing import TYPE_CHECKING, Any, Callable, Optional + +import omegaconf +import torch +import torch.distributed as dist +import torch.utils.data +import tqdm +import wandb + +from cosmos.utils.lazy_config import instantiate +from cosmos.utils import distributed, log, misc, wandb_util +from cosmos.utils.misc import get_local_tensor_if_DTensor + + +try: + from megatron.core import parallel_state +except ImportError: + parallel_state = None + + +if TYPE_CHECKING: + from cosmos.utils.config import Config + from cosmos.model._base import ImaginaireModel + from cosmos.trainer import ImaginaireTrainer + + +class CallBackGroup: + """A class for hosting a collection of callback objects. + + It is used to execute callback functions of multiple callback objects with the same method name. + When callbackgroup.func(args) is executed, internally it loops through the objects in self._callbacks and runs + self._callbacks[0].func(args), self._callbacks[1].func(args), etc. The method name and arguments should match. + + Attributes: + _callbacks (list[Callback]): List of callback objects. + """ + + def __init__(self, config: Config, trainer: ImaginaireTrainer) -> None: + """Initializes the list of callback objects. + + Args: + config (Config): The config object for the Imaginaire codebase. + trainer (ImaginaireTrainer): The main trainer. + """ + self._callbacks = [] + callback_configs = config.trainer.callbacks + if callback_configs: + if isinstance(callback_configs, list) or isinstance(callback_configs, omegaconf.listconfig.ListConfig): + warnings.warn( + "The 'config.trainer.callbacks' parameter should be a dict instead of a list. " + "Please update your code", + DeprecationWarning, + stacklevel=2, + ) + callback_configs = {f"callback_{i}": v for i, v in enumerate(callback_configs)} + for callback_name, current_callback_cfg in callback_configs.items(): + if "_target_" not in current_callback_cfg: + log.critical( + f"Callback {callback_name} is missing the '_target_' field. \n SKip {current_callback_cfg}" + ) + continue + log.critical(f"Instantiating callback {callback_name}: {current_callback_cfg}") + _callback = instantiate(current_callback_cfg) + assert isinstance(_callback, Callback), f"{current_callback_cfg} is not a valid callback." + _callback.config = config + _callback.trainer = trainer + self._callbacks.append(_callback) + + def __getattr__(self, method_name: str) -> Callable: + """Loops through the callback objects to call the corresponding callback function. + + Args: + method_name (str): Callback method name. + """ + + def multi_callback_wrapper(*args, **kwargs) -> None: + for callback in self._callbacks: + assert hasattr(callback, method_name) + method = getattr(callback, method_name) + assert callable(method) + _ = method(*args, **kwargs) + + return multi_callback_wrapper + + +class Callback: + """The base class for all callbacks. + + All callbacks should inherit from this class and adhere to the established method names and signatures. + """ + + def __init__(self, config: Optional["Config"] = None, trainer: Optional["ImaginaireTrainer"] = None): + """Initializes a Callback object. + + Args: + config (Optional[Config]): The configuration object for the Imaginaire codebase, if available. + trainer (Optional[ImaginaireTrainer]): The main trainer handling the training loop, if available. + + Notes: + The config and trainer parameters are optional to maintain backward compatibility. + In future releases, these parameters will be removed. Upon using these parameters, a deprecation + warning will be issued. + + """ + if config is not None or trainer is not None: + warnings.warn( + "The 'config' and 'trainer' parameters are deprecated and will be removed in a future release. " + "Please update your code to create Callback instances without these parameters.", + DeprecationWarning, + stacklevel=2, + ) + del config, trainer + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + """ + Called before the training step, for each batch. This is paired with on_training_step_end() but note that + when using gradient accumulation, while on_training_step_end() is only called when the optimizer is updated, + this function is called for every batch. + Use on_training_step_batch_start and on_training_step_batch_end if you need callbacks that are called + for every batch, albeit with the same iteration number. + FIXME - should this either be deprecated, or called only when a new training step is started after having updated + the optimizer? + """ + pass + + def on_training_step_batch_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + """ + Called before the training step, for each batch, similarly to on_training_step_start(). This function is paired with + on_training_step_batch_end(), and both functions are called for every batch even when using gradient accumulation. + Note that the iteration is only updated when the optimizer is updated, and therefore it may be the same for multiple invocations. + """ + pass + + def on_before_forward(self, iteration: int = 0) -> None: + pass + + def on_after_forward(self, iteration: int = 0) -> None: + pass + + def on_before_backward( + self, model_ddp: distributed.DistributedDataParallel, loss: torch.Tensor, iteration: int = 0 + ) -> None: + pass + + def on_after_backward(self, model_ddp: distributed.DistributedDataParallel, iteration: int = 0) -> None: + pass + + def on_before_dataloading(self, iteration: int = 0) -> None: + pass + + def on_after_dataloading(self, iteration: int = 0) -> None: + pass + + def on_optimizer_init_start(self) -> None: + pass + + def on_optimizer_init_end(self) -> None: + pass + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + pass + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + pass + + def on_training_step_batch_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + """ + Called at the end of a training step for every batch even when using gradient accumulation. + This is paired with on_training_step_batch_start(). Note that the iteration is only updated when the optimizer is updated, + and therefore it may be the same for multiple batches. + """ + pass + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + """ + Called at the end of a training step, but note that when using gradient accumulation, this is only called + when the optimizer is updated, and the iteration incremented, whereas on_training_step_start is called every time. + Use on_training_step_batch_start and on_training_step_batch_end if you need callbacks that are called + for every batch. + """ + pass + + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + pass + + def on_validation_step_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + pass + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + pass + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_load_checkpoint_start(self, model: ImaginaireModel) -> None: + pass + + def on_load_checkpoint_end( + self, model: ImaginaireModel, iteration: int = 0, checkpoint_path: Optional[str] = None + ) -> None: + pass + + def on_load_checkpoint(self, model: ImaginaireModel, state_dict: dict[Any]) -> None: + """ + Called when checkpoint loading is about to start, but after on_save_checkpoint_start(). + FIXME - why do we need this callback, can't we just use on_save_checkpoint_start()? + """ + pass + + def on_save_checkpoint_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + """ + Called when checkpoint saving is about to start. + """ + pass + + def on_save_checkpoint_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + """ + Called when the synchronous part of checkpointing is finished, this function can be used + along with on_save_checkpoint_start() to measure the exposed (synchronous) checkpoint time. + Note that for asynchronous checkpoint, the checkpoint may still be ongoing, so this function + does not mean the checkpoint is finished for the asynchronous case, use on_save_checkpoint_success() + for that. + """ + pass + + def on_save_checkpoint_success(self, iteration: int = 0, elapsed_time: float = 0) -> None: + """ + Called when checkpoint saving is fully finished, and succeeded. Not called if checkpoint failed. + For synchronous checkpoint, it is called at the same time as on_save_checkpoint_end(), but for asynchronous + checkpoint, it is called after the asynchronous part has also finished. For checkpointers with out-of-process + checkpointing, this function is called as soon as the notification is received from the checkpointer process, + which may not be immediately after the checkpoint has completed but later on. Therefore, if you need to measure + the full checkpoint duration for the asynchronous part, use the elapsed_time parameter, do not measure it directly + as this would be a significant overestimate. + """ + pass + + def on_save_checkpoint(self, model: ImaginaireModel, state_dict: dict[Any]) -> None: + pass + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + pass + + def on_app_end(self) -> None: + pass + + +class EMAModelCallback(Callback): + """The callback class for tracking EMA model weights.""" + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + # Set up the EMA model weight tracker. + if model.config.ema.enabled: + assert hasattr(model, "ema"), "EMA should be initialized from ImaginaireModel" + # EMA model must be kept in FP32 precision. + model.ema = model.ema.to(dtype=torch.float32) + else: + assert not hasattr(model, "ema"), "There should be no EMA initialized." + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + # Update the EMA model with the new regular weights. + if model.config.ema.enabled: + model.ema.update_average(model, iteration) + + +class ProgressBarCallback(Callback): + """The callback class for visualizing the training/validation progress bar in the console.""" + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.train_pbar = tqdm.trange(self.config.trainer.max_iter, initial=iteration, desc="Training") + + @distributed.rank0_only + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self.train_pbar.update() + + @distributed.rank0_only + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + if self.config.trainer.max_val_iter is not None: + num_iter = self.config.trainer.max_val_iter + else: + num_iter = len(dataloader_val) + assert num_iter is not None and num_iter > 0, f"Invalid number of validation iterations: {num_iter}" + self.val_pbar = tqdm.trange(num_iter, desc="Validating", position=1, leave=False) + + @distributed.rank0_only + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + self.val_pbar.update() + + @distributed.rank0_only + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.val_pbar.close() + + @distributed.rank0_only + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + self.trainer.checkpointer.finalize() + self.train_pbar.close() + + +class IterationLoggerCallback(Callback): + """The callback class for visualizing the training/validation progress bar in the console.""" + + @distributed.rank0_only + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + # self.train_pbar = tqdm.trange(self.config.trainer.max_iter, initial=iteration, desc="Training") + self.start_iteration_time = time.time() + self.elapsed_iteration_time = 0 + + @distributed.rank0_only + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + self.start_iteration_time = time.time() + + @distributed.rank0_only + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: + + # but this is only called when the optimizer is updated, so it's only the time for the last batch. + self.elapsed_iteration_time += time.time() - self.start_iteration_time + + if iteration % self.config.trainer.logging_iter == 0: + avg_time = self.elapsed_iteration_time / self.config.trainer.logging_iter + log.info(f"Iteration: {iteration}, average iter time: {avg_time:2f}, total loss {loss.item():4f}") + + self.elapsed_iteration_time = 0 + + +class WandBCallback(Callback): + """The callback class for logging to Weights and Biases (W&B). + + By default, WandBCallback logs the following training stats to W&B every config.trainer.logging_iter: + - iteration: The current iteration number (useful for visualizing the training progress over time). + - train/loss: The computed overall loss in the training batch. + - optim/lr: The current learning rate. + - timer/*: The averaged timing results of each code block recorded by trainer.training_timer. + For validation, WandBCallback logs: + - val/loss: The computed overall loss in the validation dataset. + """ + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + wandb_util.init_wandb(self.config, model=model) + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: # Log the curent learning rate. + if iteration % self.config.trainer.logging_iter == 0 and distributed.is_rank0(): + wandb.log({"optim/lr": scheduler.get_last_lr()[0]}, step=iteration) + wandb.log({"optim/grad_scale": grad_scaler.get_scale()}, step=iteration) + + def on_training_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: # Log the timing results (over a number of iterations) and the training loss. + if iteration % self.config.trainer.logging_iter == 0: + timer_results = self.trainer.training_timer.compute_average_results() + + # reduce loss + sample_size = torch.tensor(misc.get_data_batch_size(data_batch), device="cuda") + loss_sum = loss * sample_size + dist.all_reduce(loss_sum, op=dist.ReduceOp.SUM) + dist.all_reduce(sample_size, op=dist.ReduceOp.SUM) + avg_loss = loss_sum.item() / sample_size.item() + + if distributed.is_rank0(): + wandb.log({f"timer/{key}": value for key, value in timer_results.items()}, step=iteration) + wandb.log({"train/loss": avg_loss}, step=iteration) + wandb.log({"iteration": iteration}, step=iteration) + self.trainer.training_timer.reset() + + def on_validation_start( + self, model: ImaginaireModel, dataloader_val: torch.utils.data.DataLoader, iteration: int = 0 + ) -> None: + # Cache for collecting data/output batches. + self._val_cache: dict[str, Any] = dict( + data_batches=[], + output_batches=[], + loss=torch.tensor(0.0, device="cuda"), + sample_size=torch.tensor(0, device="cuda"), + ) + + def on_validation_step_end( + self, + model: ImaginaireModel, + data_batch: dict[str, torch.Tensor], + output_batch: dict[str, torch.Tensor], + loss: torch.Tensor, + iteration: int = 0, + ) -> None: # Collect the validation batch and aggregate the overall loss. + # Collect the validation batch and aggregate the overall loss. + batch_size = misc.get_data_batch_size(data_batch) + self._val_cache["loss"] += loss * batch_size + self._val_cache["sample_size"] += batch_size + + def on_validation_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + # Compute the average validation loss across all devices. + dist.all_reduce(self._val_cache["loss"], op=dist.ReduceOp.SUM) + dist.all_reduce(self._val_cache["sample_size"], op=dist.ReduceOp.SUM) + loss = self._val_cache["loss"].item() / self._val_cache["sample_size"] + # Log data/stats of validation set to W&B. + if distributed.is_rank0(): + log.info(f"Validation loss (iteration {iteration}): {loss:4f}") + wandb.log({"val/loss": loss}, step=iteration) + + def on_train_end(self, model: ImaginaireModel, iteration: int = 0) -> None: + wandb.finish() + + +class LowPrecisionCallback(Callback): + """The callback class handling low precision training""" + + def __init__(self, config: Config, trainer: ImaginaireTrainer, update_iter: int): + self.update_iter = update_iter + + def on_train_start(self, model: ImaginaireModel, iteration: int = 0) -> None: + if model.precision == torch.float32: + log.critical("Using fp32. We should disable master weights update.") + self.update_iter = sys.maxsize + else: + assert model.precision in [ + torch.bfloat16, + torch.float16, + torch.half, + ], "LowPrecisionCallback must use a low precision dtype." + self.precision_type = model.precision + + def on_training_step_start(self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0) -> None: + for k, v in data.items(): + if isinstance(v, torch.Tensor) and torch.is_floating_point(data[k]): + data[k] = v.to(dtype=self.precision_type) + + def on_validation_step_start( + self, model: ImaginaireModel, data: dict[str, torch.Tensor], iteration: int = 0 + ) -> None: + for k, v in data.items(): + if isinstance(v, torch.Tensor) and torch.is_floating_point(data[k]): + data[k] = v.to(dtype=self.precision_type) + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + if iteration % self.update_iter == 0: + if getattr(optimizer, "master_weights", False): + params, master_params = [], [] + for group, group_master in zip(optimizer.param_groups, optimizer.param_groups_master): + for p, p_master in zip(group["params"], group_master["params"]): + params.append(get_local_tensor_if_DTensor(p).data) + master_params.append(get_local_tensor_if_DTensor(p_master).data) + torch._foreach_copy_(params, master_params) + + +class NVTXCallback(Callback): + """The callback for creating NVTX ranges""" + + def __init__( + self, + synchronize: bool = False, + config: Optional["Config"] = None, + trainer: Optional["ImaginaireTrainer"] = None, + ): + super().__init__(config, trainer) + self.synchronize = synchronize + + def on_before_forward(self, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("forward") + + def on_after_forward(self, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_backward( + self, model_ddp: distributed.DistributedDataParallel, loss: torch.Tensor, iteration: int = 0 + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("backward") + + def on_after_backward(self, model_ddp: distributed.DistributedDataParallel, iteration: int = 0) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_optimizer_step( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int = 0, + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push("optimizer_step") + + def on_before_zero_grad( + self, + model_ddp: distributed.DistributedDataParallel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + iteration: int = 0, + ) -> None: + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + def on_before_dataloading(self, iteration: int = 0) -> None: + torch.cuda.nvtx.range_push("dataloading") + + def on_after_dataloading(self, iteration: int = 0) -> None: + torch.cuda.nvtx.range_pop() + + diff --git a/cosmos_training/cosmos/utils/checkpoint_db.py b/cosmos_training/cosmos/utils/checkpoint_db.py new file mode 100644 index 00000000..b9420c12 --- /dev/null +++ b/cosmos_training/cosmos/utils/checkpoint_db.py @@ -0,0 +1,446 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Database of released checkpoints. + +The database maps checkpoint internal URIs to public URIs and associates metadata (e.g. +experiment name). + +## Usage + +Register a checkpoint: + +```python +CheckpointConfig( + uuid="0e8177cc-0db5-4cfd-a8a4-b820c772f4fc", + s3=CheckpointDirS3( + uri="s3://bucket/path/to/checkpoint", + ), + hf=CheckpointDirHf( + repository="org/repo", + revision="revision", + subdirectory="path/to/checkpoint", + ), +).register() +``` + +Checkpoints can be referenced by UUID, S3 URI, or local path. Optionally, use `get_checkpoint_uri` to validate and normalize the URI. + +```python +# S3 URI +checkpoint_uri = get_checkpoint_uri("s3://bucket/path/to/checkpoint") +# UUID +checkpoint_uri = get_checkpoint_uri("0e8177cc-0db5-4cfd-a8a4-b820c772f4fc") +# Local path +checkpoint_uri = get_checkpoint_uri("/path/to/checkpoint", check_exists=True) +``` + +When the checkpoint is loaded, call 'download_checkpoint': + +```python +from cosmos.utils.flags import INTERNAL + +if not INTERNAL: + from cosmos.utils.checkpoint_db import download_checkpoint + + checkpoint_uri = download_checkpoint(checkpoint_uri) + +load_checkpoint(checkpoint_uri) +``` +""" + +import functools +import json +import os +import shlex +import subprocess +import uuid +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Annotated, TypeAlias + +import pydantic +from typing_extensions import Self, override + +from cosmos.utils.flags import EXPERIMENTAL_CHECKPOINTS, INTERNAL +from cosmos.utils import log + +HF_VERSION = "1.13.0" + + +def _is_uuid(checkpoint_uri: str) -> bool: + """Return True if the URI is a UUID.""" + try: + uuid.UUID(str(checkpoint_uri)) + return True + except ValueError: + return False + + +def _is_path(checkpoint_uri: str) -> bool: + """Return True if the URI is a local path.""" + return not ("://" in checkpoint_uri or _is_uuid(checkpoint_uri)) + + +def normalize_uri(checkpoint_uri: str) -> str: + """Normalize checkpoint URI.""" + checkpoint_uri = checkpoint_uri.rstrip("/") + if checkpoint_uri.startswith("s3://"): + checkpoint_uri = checkpoint_uri.removesuffix("/model") + return checkpoint_uri + + +def sanitize_uri(checkpoint_uri: str) -> str: + """Sanitize checkpoint URI.""" + checkpoint_uri = normalize_uri(checkpoint_uri) + if checkpoint_uri.startswith("s3://"): + checkpoint_uri = checkpoint_uri.removeprefix("s3://").split("/", 1)[1] + checkpoint_uri = f"s3://bucket/{checkpoint_uri}" + return checkpoint_uri + + +class _CheckpointUri(pydantic.BaseModel): + """Config for checkpoint file/directory.""" + + model_config = pydantic.ConfigDict(extra="forbid", frozen=True) + + metadata: dict = pydantic.Field(default_factory=dict) + """File metadata. + + Only used for debugging. + """ + + +def _validate_s3_uri(uri: str) -> str: + """Validate and normalize S3 URI.""" + if not uri.startswith("s3://"): + raise ValueError(f"Invalid S3 URI: {uri}. Must start with 's3://'") + return normalize_uri(uri) + + +S3Uri = Annotated[str, pydantic.AfterValidator(_validate_s3_uri)] + + +class _CheckpointS3(_CheckpointUri): + """Config for checkpoint on S3.""" + + uri: S3Uri + """S3 URI.""" + + +class CheckpointFileS3(_CheckpointS3): + """Config for checkpoint file on S3.""" + + +class CheckpointDirS3(_CheckpointS3): + """Config for checkpoint directory on S3.""" + + +CheckpointS3: TypeAlias = CheckpointFileS3 | CheckpointDirS3 + + +def _hf_download(cmd_args: list[str]) -> str: + """Run Hugging Face CLI download command and return the local path. + + Uses a newer Hugging Face CLI version to download checkpoint. The dependency + version is very old and not robust. + """ + is_rank0 = os.environ.get("RANK", "0") == "0" + cmd = [ + "uvx", + f"hf@{HF_VERSION}", + "download", + "--format=json", + *cmd_args, + ] + log.info(f"{shlex.join(cmd)}") + output = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=None if is_rank0 else subprocess.PIPE, + text=True, + check=True, + ) + return json.loads(output.stdout)["path"] + + +class _CheckpointHf(_CheckpointUri, ABC): + """Config for checkpoint on Hugging Face.""" + + repository: str + """Repository id (organization/repository).""" + revision: str + """Git revision id which can be a branch name, a tag, or a commit hash.""" + + _path: str | None = None + """Local path.""" + + @abstractmethod + def _download(self) -> str: ... + + def download(self) -> str: + """Download checkpoint and return the local path.""" + if self._path is None: + self._path = self._download() + return self._path + + +class CheckpointFileHf(_CheckpointHf): + """Config for checkpoint file on Hugging Face.""" + + filename: str + """File name.""" + + @override + def _download(self) -> str: + """Download checkpoint and return the local path.""" + cmd_args = [ + self.repository, + "--repo-type", + "model", + "--revision", + self.revision, + self.filename, + ] + path = _hf_download(cmd_args) + assert os.path.exists(path), path + return path + + +class CheckpointDirHf(_CheckpointHf): + """Config for checkpoint directory on Hugging Face.""" + + subdirectory: str = "" + """Repository subdirectory.""" + include: tuple[str, ...] = () + """Include patterns. + + See https://huggingface.co/docs/huggingface_hub/en/guides/download#filter-files-to-download + """ + exclude: tuple[str, ...] = () + """Exclude patterns. + + See https://huggingface.co/docs/huggingface_hub/en/guides/download#filter-files-to-download + """ + + @override + def _download(self) -> str: + """Download checkpoint and return the local path.""" + include = list(self.include) or ["*"] + exclude = list(self.exclude) + if self.subdirectory: + for patterns in [include, exclude]: + for i, pattern in enumerate(patterns): + patterns[i] = os.path.join(self.subdirectory, pattern) + + cmd_args = [ + self.repository, + "--repo-type", + "model", + "--revision", + self.revision, + ] + for pattern in include: + cmd_args.extend(["--include", pattern]) + for pattern in exclude: + cmd_args.extend(["--exclude", pattern]) + path = _hf_download(cmd_args) + if self.subdirectory: + path = os.path.join(path, self.subdirectory) + assert os.path.exists(path), path + return path + + +CheckpointHf: TypeAlias = CheckpointFileHf | CheckpointDirHf + + +class CheckpointConfig(pydantic.BaseModel): + """Config for checkpoint.""" + + model_config = pydantic.ConfigDict(extra="forbid", frozen=True) + + uuid: str + """Checkpoint UUID.""" + name: str + """Checkpoint name. + + Only used for debugging. + """ + metadata: dict = pydantic.Field(default_factory=dict) + """Checkpoint metadata. + + Only used for debugging. + """ + experiment: str | None = None + """Hydra experiment name.""" + config_file: str | None = None + """Hydra config file.""" + + s3: CheckpointS3 + """Config for checkpoint on S3.""" + hf: CheckpointHf + """Config for checkpoint on Hugging Face.""" + + @property + def full_name(self) -> str: + """Return full name for debugging.""" + return f"{self.name}({self.uuid})" + + def download(self) -> str: + """Download checkpoint and return the local path.""" + if INTERNAL: + return self.s3.uri + + log.info(f"Downloading checkpoint {self.full_name}") + return self.hf.download() + + @classmethod + def maybe_from_uri(cls, uri: str) -> Self | None: + """Return checkpoint config for URI if found, otherwise None.""" + uri = normalize_uri(uri) + return _CHECKPOINTS.get(uri, None) + + @classmethod + def from_uri(cls, uri: str) -> Self: + """Return checkpoint config for URI if found, otherwise raise an error.""" + self = cls.maybe_from_uri(uri) + if self is None: + raise ValueError( + f"Checkpoint '{uri}' not found. Set 'export COSMOS_EXPERIMENTAL_CHECKPOINTS=1' to include experimental checkpoints." + ) + return self + + def register(self): + """Register checkpoint config.""" + register_checkpoint(self) + + +_CHECKPOINTS: dict[str, CheckpointConfig] = {} +"""Mapping from checkpoint URI to checkpoint config.""" + + +def register_checkpoint(checkpoint_config: CheckpointConfig): + """Register checkpoint config. + + DEPRECATED: Use 'CheckpointConfig.register' instead. + """ + if not EXPERIMENTAL_CHECKPOINTS: + if checkpoint_config.hf.repository in ["nvidia/Cosmos-Experimental", "nvidia-cosmos-ea/Cosmos-Experimental"]: + # Don't register experimental checkpoints. An exception will be + # raised in CI if the checkpoint is used without + # EXPERIMENTAL_CHECKPOINTS. + return + for uri in [checkpoint_config.uuid, checkpoint_config.s3.uri]: + if uri in _CHECKPOINTS: + raise ValueError(f"Checkpoint '{uri}' already registered.") + _CHECKPOINTS[uri] = checkpoint_config + + +def get_checkpoint_uri(checkpoint_uri: str, *, check_exists: bool = False) -> str: + """Validate and normalize checkpoint URI.""" + checkpoint_uri = normalize_uri(checkpoint_uri) + if (checkpoint := CheckpointConfig.maybe_from_uri(checkpoint_uri)) is not None: + return checkpoint.s3.uri + if checkpoint_uri.startswith("hf://"): + return checkpoint_uri + if _is_path(checkpoint_uri): + if check_exists: + checkpoint_path = Path(checkpoint_uri).expanduser().absolute() + if not checkpoint_path.exists(): + raise ValueError(f"Checkpoint '{checkpoint_path}' does not exist.") + checkpoint_uri = str(checkpoint_path) + return checkpoint_uri + if INTERNAL: + return checkpoint_uri + raise ValueError( + f"Checkpoint '{checkpoint_uri}' not found. Set 'export COSMOS_EXPERIMENTAL_CHECKPOINTS=1' to include experimental checkpoints." + ) + + +@functools.lru_cache +def _download_hf_checkpoint(checkpoint_hf: str) -> str: + # Parse hf://org/repo/path/to/file.pth + assert checkpoint_hf.startswith("hf://"), f"Not a HuggingFace URI: {checkpoint_hf}" + hf_path = checkpoint_hf.removeprefix("hf://") + # Split into repo_id (org/repo) and filename (path/to/file.pth) + parts = hf_path.split("/") + if len(parts) < 3: + raise ValueError( + f"Invalid HuggingFace URI format: {checkpoint_hf}. Expected format: hf://org/repo/path/to/file.pth" + ) + repo_id = "/".join(parts[:2]) # org/repo + filename = "/".join(parts[2:]) # path/to/file.pth + return CheckpointFileHf( + repository=repo_id, + revision="main", + filename=filename, + ).download() + + +@functools.lru_cache +def download_checkpoint(checkpoint_uri: str, *, check_exists: bool = True) -> str: + """Download a checkpoint by URI and return the local path. + + DEPRECATED: Use 'download_checkpoint_v2' instead. + + This should only be used when the checkpoint is loaded. If you just need a + URI, use 'get_checkpoint_uri' instead. + + Downloaded checkpoints are cached, so calling this multiple times will + return the same path. + + Supports: + - Checkpoint UUID: 0e8177cc-0db5-4cfd-a8a4-b820c772f4fc + - S3 URI: s3://bucket/path/to/checkpoint + - HuggingFace URI: hf://org/repo/path/to/file.pth + - Local path: /path/to/checkpoint + """ + if INTERNAL: + return checkpoint_uri + if (checkpoint := CheckpointConfig.maybe_from_uri(checkpoint_uri)) is not None: + return checkpoint.download() + if checkpoint_uri.startswith("hf://"): + return _download_hf_checkpoint(checkpoint_uri) + if check_exists and not os.path.exists(checkpoint_uri): + raise ValueError(f"Checkpoint path {checkpoint_uri} does not exist.") + return checkpoint_uri + + +@functools.lru_cache +def download_checkpoint_v2(checkpoint_uri: str, *, check_exists: bool = True) -> str: + """Maybe download a checkpoint by URI and return the local path. + + Similar to 'download_checkpoint', but unknown S3 URIs are passed through. + """ + # Local-path short-circuit: if the URI exists on disk, return it as-is + # without consulting the registry. Prevents the registry from rewriting + # a known basename (e.g. Wan2.2_VAE.pth) into an s3:// URI we can't open. + if os.path.exists(checkpoint_uri): + return checkpoint_uri + if INTERNAL: + return checkpoint_uri + if (checkpoint := CheckpointConfig.maybe_from_uri(sanitize_uri(checkpoint_uri))) is not None: + return checkpoint.download() + if checkpoint_uri.startswith("s3://"): + return checkpoint_uri + if checkpoint_uri.startswith("hf://"): + return _download_hf_checkpoint(checkpoint_uri) + if check_exists and not os.path.exists(checkpoint_uri): + raise ValueError(f"Checkpoint path {checkpoint_uri} does not exist.") + return checkpoint_uri + + +get_checkpoint_path = download_checkpoint +"""DEPRECATED: Use 'download_checkpoint' instead.""" diff --git a/cosmos_training/cosmos/utils/checkpointer.py b/cosmos_training/cosmos/utils/checkpointer.py new file mode 100644 index 00000000..471cf3ef --- /dev/null +++ b/cosmos_training/cosmos/utils/checkpointer.py @@ -0,0 +1,504 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +import threading +from typing import TYPE_CHECKING, List, NamedTuple, Tuple + +import torch + +from cosmos.model._base import ImaginaireModel +from cosmos.utils import callback, distributed, log, misc, object_store + +if TYPE_CHECKING: + from cosmos.utils.config import CheckpointConfig, JobConfig + +TORCH_VERSION: Tuple[int, ...] = tuple(int(x) for x in torch.__version__.split(".")[:2]) +if TORCH_VERSION >= (1, 11): + from torch.ao import quantization + from torch.ao.quantization import FakeQuantizeBase, ObserverBase +elif ( + TORCH_VERSION >= (1, 8) + and hasattr(torch.quantization, "FakeQuantizeBase") + and hasattr(torch.quantization, "ObserverBase") +): + from torch import quantization + from torch.quantization import FakeQuantizeBase, ObserverBase + + +class Checkpointer: + """The checkpointer class. Supports checkpoint saving/loading to both local disk or object store.""" + + def __init__(self, config_checkpoint: CheckpointConfig, config_job: JobConfig, callbacks: callback.CallBackGroup): + """Constructor of the checkpointer. + + Args: + config_checkpoint (CheckpointConfig): The config object for the checkpointer. + """ + # Set the callback functions. + self.callbacks = callbacks + + + + self.checkpoint_dir_local = f"{config_job.path_local}/checkpoints" + self.checkpoint_dir_object_store = f"{config_job.path}/checkpoints" + self.save_to_object_store = config_checkpoint.save_to_object_store.enabled + self.load_from_object_store = config_checkpoint.load_from_object_store.enabled + self.strict_resume = config_checkpoint.strict_resume + self.load_path = config_checkpoint.load_path or None + self.load_training_state = config_checkpoint.load_training_state + self.only_load_scheduler_state = config_checkpoint.only_load_scheduler_state + self.save_thread = None + # Create the object store client interface. + if self.save_to_object_store: + self.object_store_saver = object_store.ObjectStore(config_checkpoint.save_to_object_store) + if self.load_from_object_store: + self.object_store_loader = object_store.ObjectStore(config_checkpoint.load_from_object_store) + + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + self.callbacks.on_save_checkpoint_start(model, iteration) + + checkpoint_file = f"iter_{iteration:09}.pt" + + if distributed.get_rank() == 0: + state_dict = dict( + model=model.state_dict(), + optimizer=optimizer.state_dict(), + scheduler=scheduler.state_dict(), + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + state_dict = misc.to(state_dict, device="cpu") + self.callbacks.on_save_checkpoint(model, state_dict=state_dict) + # Wait for previous saver thread to end. + if self.save_thread: + self.save_thread.join() + # Run the checkpoint saver in a separate thread. + self.save_thread = threading.Thread( + target=self._save_worker_object_store if self.save_to_object_store else self._save_worker_local, + daemon=False, + args=(state_dict, checkpoint_file, distributed.get_rank()), + ) + self.save_thread.start() + + # Note: Checkpoints are saved on a separate thread and this callback is not accurate. + # Please check logs from on_save_checkpoint_success() for better accuracy + self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration) + + @misc.timer("checkpoint saving (local)") + def _save_worker_local(self, state_dict: dict[str, torch.Tensor], checkpoint_file: str, rank: int = 0) -> None: + """Worker to save checkpoint to local disk, spawned with a child thread (runs in parallel with the training). + + Args: + state_dict (dict[str, torch.Tensor]): The state dict of the model/optimizer/scheduler. + checkpoint_file (str): The file name of the model checkpoint. + rank (int): GPU device (default: 0). + """ + checkpoint_path = os.path.join(self.checkpoint_dir_local, checkpoint_file) + os.makedirs(self.checkpoint_dir_local, exist_ok=True) + try: + torch.save(state_dict, checkpoint_path) + if rank == 0: + self._write_latest_checkpoint_file(checkpoint_file) + log.success(f"Saved checkpoint (local): {checkpoint_path}") + iteration = int(checkpoint_file.replace("iter_", "").replace(".pt", "")) + self.callbacks.on_save_checkpoint_success(iteration=iteration) + except Exception as e: # noqa: BLE001 + log.exception(f"Checkpoint failed to save (local): {e}") + + @misc.timer("checkpoint saving (object store)") + def _save_worker_object_store( + self, state_dict: dict[str, torch.Tensor], checkpoint_file: str, rank: int = 0 + ) -> None: + """Worker to upload checkpoint to object store, spawned with a child thread (in parallel with the training). + + Args: + state_dict (dict[str, torch.Tensor]): The state dict of the model/optimizer/scheduler. + checkpoint_file (str): The file name of the model checkpoint. + rank (int): GPU device (default: 0). + """ + checkpoint_path = os.path.join(self.checkpoint_dir_object_store, checkpoint_file) + try: + self.object_store_saver.save_object(state_dict, key=checkpoint_path, type="torch") + if rank == 0: + self._write_latest_checkpoint_file(checkpoint_file) + log.success(f"Saved checkpoint (object store): {checkpoint_path}") + iteration = int(checkpoint_file.replace("iter_", "").replace(".pt", "")) + self.callbacks.on_save_checkpoint_success(iteration=iteration) + except Exception as e: # noqa: BLE001 + log.exception(f"Checkpoint failed to upload (object store): {e}") + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + """Load network weights and optimizer states from a checkpoint in a single process. + + The priority of the checkpoint loading logic is: + 1. Attempt to resume training if possible by looking for latest_checkpoint.txt under the same name. + 2. If no latest checkpoint were found, it loads the model weights specified by config_checkpoint.path. + - This is typically used for inference mode. + - If config_checkpoint.load_optimizer_state is True, then also load the optimizer and scheduler states. + 3. If none of the above, randomly initialize the model parameters and train from scratch. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer | None): The model optimizer (default: None). + scheduler (torch.optim.lr_scheduler.LRScheduler | None): The optimization scheduler (default: None). + grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training). + + Returns: + iteration (int): the iteration number to start/resume from. + """ + self.callbacks.on_load_checkpoint_start(model) + + latest_checkpoint_file = self._read_latest_checkpoint_file() + if latest_checkpoint_file is not None: + # 1. Resume training from latest_checkpoint.txt under the same name. + checkpoint_dir = ( + self.checkpoint_dir_object_store if self.load_from_object_store else self.checkpoint_dir_local + ) + checkpoint_path = os.path.join(checkpoint_dir, latest_checkpoint_file) + resume = True + only_resume_scheduler = True + else: + if self.load_path: + # 2. Load the module weights specified by config_checkpoint.path. + checkpoint_path = self.load_path + resume = self.load_training_state + only_resume_scheduler = self.only_load_scheduler_state + else: + # 3. Randomly initialize the model parameters and train from scratch. + checkpoint_path = None + resume = False + only_resume_scheduler = False + # Load checkpoint. + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + if self.load_from_object_store: + log.info(f"Loading checkpoint (object store): {checkpoint_path}") + state_dict = self.object_store_loader.load_object(key=checkpoint_path, type="torch") + log.success(f"Complete loading checkpoint (object store): {checkpoint_path}") + else: + log.info(f"Loading checkpoint (local): {checkpoint_path}") + state_dict = torch.load(checkpoint_path, map_location=lambda storage, loc: storage, weights_only=False) + log.success(f"Complete loading checkpoint (local): {checkpoint_path}") + self.callbacks.on_load_checkpoint(model, state_dict=state_dict) + # Load the state dicts. + log.info("- Loading the model...") + model.load_state_dict(state_dict["model"], strict=self.strict_resume) + if resume or only_resume_scheduler: + iteration = state_dict["iteration"] + assert scheduler + log.info("- Loading the scheduler...") + scheduler.load_state_dict(state_dict["scheduler"]) + scheduler.last_epoch = iteration + else: + iteration = 0 + if resume: + assert optimizer + log.info("- Loading the optimizer...") + optimizer.load_state_dict(state_dict["optimizer"]) + log.info("- Loading the gradient scaler...") + grad_scaler.load_state_dict(state_dict["grad_scaler"]) + log.success(f"Done with loading the checkpoint (iteration {iteration}).") + else: + log.success("Done with loading the checkpoint.") + else: + # Checkpoint not found and not specified. We will train everything from scratch. + iteration = 0 + log.info("Training from scratch.") + torch.cuda.empty_cache() + + self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path) + + return iteration + + def _read_latest_checkpoint_file(self) -> str | None: + """Get the file name of the latest saved checkpoint. If it doesn't exist, return None. + + Returns: + checkpoint_file (str | None): file name of the latest saved checkpoint. + """ + checkpoint_file = None + if self.load_from_object_store: + latest_path = os.path.join(self.checkpoint_dir_object_store, "latest_checkpoint.txt") + if self.object_store_loader.object_exists(key=latest_path): + checkpoint_file = self.object_store_loader.load_object(key=latest_path, type="text").strip() + else: + latest_path = os.path.join(self.checkpoint_dir_local, "latest_checkpoint.txt") + if os.path.isfile(latest_path): + checkpoint_file = open(latest_path).read().strip() + return checkpoint_file + + def _write_latest_checkpoint_file(self, checkpoint_file: str) -> None: + """Track the file name of the latest saved checkpoint. + + Args: + checkpoint_file (str): file name of the latest saved checkpoint. + """ + content = f"{checkpoint_file}\n" + if self.save_to_object_store: + latest_path = os.path.join(self.checkpoint_dir_object_store, "latest_checkpoint.txt") + self.object_store_saver.save_object(content, key=latest_path, type="text") + else: + latest_path = os.path.join(self.checkpoint_dir_local, "latest_checkpoint.txt") + with open(latest_path, "w") as file: + file.write(content) + + def _check_checkpoint_exists(self, checkpoint_path: str) -> None: + """If the file checkpoint_path does not exist, raise an error. + + Args: + checkpoint_path (str): full path to the checkpoint. + """ + if self.load_from_object_store: + if not self.object_store_loader.object_exists(key=checkpoint_path): + raise FileNotFoundError(f"File not found (object store): {checkpoint_path}") + else: + if not os.path.exists(checkpoint_path): + raise FileNotFoundError(f"File not found (local): {checkpoint_path}") + + def finalize(self) -> None: + """Finalize the checkpointer.""" + if self.save_thread: + self.save_thread.join() + + +class _IncompatibleKeys( + NamedTuple( + "IncompatibleKeys", + [ + ("missing_keys", List[str]), + ("unexpected_keys", List[str]), + ("incorrect_shapes", List[Tuple[str, Tuple[int], Tuple[int]]]), + ], + ) +): + pass + + +class MultiRankCheckpointer(Checkpointer): + def save( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler.LRScheduler, + grad_scaler: torch.amp.GradScaler, + iteration: int, + ) -> None: + """Save network weights, optimizer parameters, scheduler parameters to a checkpoint. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer): The model optimizer. + scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler. + grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training). + iteration (int): Current iteration number. + """ + # checkpoint_file = f"iter_{iteration:09}.pt" + postfix, _, total_ema_num = model.get_ckpt_postfix() + checkpoint_file = f"iter_{iteration:09}{postfix}.pt" + save_ranks = list(range(total_ema_num)) + for _rank in save_ranks: + if distributed.get_rank() == _rank: + state_dict = dict( + model=model.state_dict(), + optimizer=optimizer.state_dict(), + scheduler=scheduler.state_dict(), + grad_scaler=grad_scaler.state_dict(), + iteration=iteration, + ) + state_dict = misc.to(state_dict, device="cpu") + self.callbacks.on_save_checkpoint(model, state_dict=state_dict) + # Wait for previous saver thread to end. + if self.save_thread: + self.save_thread.join() + # Run the checkpoint saver in a separate thread. + self.save_thread = threading.Thread( + target=self._save_worker_object_store if self.save_to_object_store else self._save_worker_local, + daemon=False, + args=(state_dict, checkpoint_file, distributed.get_rank()), + ) + self.save_thread.start() + + @misc.timer("checkpoint loading") + def load( + self, + model: ImaginaireModel, + optimizer: torch.optim.Optimizer | None = None, + scheduler: torch.optim.lr_scheduler.LRScheduler | None = None, + grad_scaler: torch.amp.GradScaler | None = None, + ) -> int: + """Load network weights and optimizer states from a checkpoint in a single process. + + The priority of the checkpoint loading logic is: + 1. Attempt to resume training if possible by looking for latest_checkpoint.txt under the same name. + 2. If no latest checkpoint were found, it loads the model weights specified by config_checkpoint.path. + - This is typically used for inference mode. + - If config_checkpoint.load_optimizer_state is True, then also load the optimizer and scheduler states. + 3. If none of the above, randomly initialize the model parameters and train from scratch. + + Args: + model (ImaginaireModel): The PyTorch model. + optimizer (torch.optim.Optimizer | None): The model optimizer (default: None). + scheduler (torch.optim.lr_scheduler.LRScheduler | None): The optimization scheduler (default: None). + grad_scaler (torch.amp.GradScaler | None): The gradient scaler (for mixed precision training). + + Returns: + iteration (int): the iteration number to start/resume from. + """ + latest_checkpoint_file = self._read_latest_checkpoint_file() + if latest_checkpoint_file is not None: + # different from base checkpointer, this support multi-EMA + postfix, _, total_ema_num = model.get_ckpt_postfix() + latest_checkpoint_file = latest_checkpoint_file.replace(".pt", f"{postfix}.pt") + # 1. Resume training from latest_checkpoint.txt under the same name. + checkpoint_dir = ( + self.checkpoint_dir_object_store if self.load_from_object_store else self.checkpoint_dir_local + ) + checkpoint_path = os.path.join(checkpoint_dir, latest_checkpoint_file) + resume = True + else: + if self.load_path: + # 2. Load the module weights specified by config_checkpoint.path. + checkpoint_path = self.load_path + # different from base checkpointer, this support multi-EMA + postfix, _, total_ema_num = model.get_ckpt_postfix() + checkpoint_path = checkpoint_path.replace(".pt", f"{postfix}.pt") + resume = self.load_training_state + else: + # 3. Randomly initialize the model parameters and train from scratch. + checkpoint_path = None + resume = False + # Load checkpoint. + if checkpoint_path is not None: + self._check_checkpoint_exists(checkpoint_path) + if self.load_from_object_store: + log.info(f"Loading checkpoint (object store): {checkpoint_path}") + state_dict = self.object_store_loader.load_object(key=checkpoint_path, type="torch") + log.success(f"Complete loading checkpoint (object store): {checkpoint_path}") + else: + log.info(f"Loading checkpoint (local): {checkpoint_path}") + state_dict = torch.load(checkpoint_path, map_location=lambda storage, loc: storage) + log.success(f"Complete loading checkpoint (local): {checkpoint_path}") + self.callbacks.on_load_checkpoint(model, state_dict=state_dict) + # Load the state dicts. + log.info("- Loading the model...") + log.critical(model.load_state_dict(state_dict["model"], strict=self.strict_resume)) + if resume: + iteration = state_dict["iteration"] + assert optimizer and scheduler + log.info("- Loading the optimizer...") + optimizer.load_state_dict(state_dict["optimizer"]) + log.info("- Loading the scheduler...") + scheduler.load_state_dict(state_dict["scheduler"]) + scheduler.last_epoch = iteration + log.info("- Loading the gradient scaler...") + grad_scaler.load_state_dict(state_dict["grad_scaler"]) + log.success(f"Done with loading the checkpoint (iteration {iteration}).") + else: + iteration = 0 + log.success("Done with loading the checkpoint.") + else: + # Checkpoint not found and not specified. We will train everything from scratch. + iteration = 0 + log.info("Training from scratch.") + torch.cuda.empty_cache() + return iteration + + +# https://github.com/facebookresearch/fvcore/blob/9d683aae73fb899dd35d6cf6720e5ef567761c57/fvcore/common/checkpoint.py +def non_strict_load_model(model: torch.nn.Module, checkpoint_state_dict: dict) -> _IncompatibleKeys: + # workaround https://github.com/pytorch/pytorch/issues/24139 + model_state_dict = model.state_dict() + incorrect_shapes = [] + for k in list(checkpoint_state_dict.keys()): + if k in model_state_dict: + if "_extra_state" in k: # Key introduced by TransformerEngine for FP8 + log.warning(f"Skipping key {k} introduced by TransformerEngine for FP8 in the checkpoint.") + continue + model_param = model_state_dict[k] + # Allow mismatch for uninitialized parameters + if TORCH_VERSION >= (1, 8) and isinstance(model_param, torch.nn.parameter.UninitializedParameter): + continue + if not isinstance(model_param, torch.Tensor): + raise ValueError( + f"Find non-tensor parameter {k} in the model. type: {type(model_param)} {type(checkpoint_state_dict[k])}, please check if this key is safe to skip or not." + ) + + shape_model = tuple(model_param.shape) + shape_checkpoint = tuple(checkpoint_state_dict[k].shape) + if shape_model != shape_checkpoint: + has_observer_base_classes = ( + TORCH_VERSION >= (1, 8) + and hasattr(quantization, "ObserverBase") + and hasattr(quantization, "FakeQuantizeBase") + ) + if has_observer_base_classes: + # Handle the special case of quantization per channel observers, + # where buffer shape mismatches are expected. + def _get_module_for_key(model: torch.nn.Module, key: str) -> torch.nn.Module: + # foo.bar.param_or_buffer_name -> [foo, bar] + key_parts = key.split(".")[:-1] + cur_module = model + for key_part in key_parts: + cur_module = getattr(cur_module, key_part) + return cur_module + + cls_to_skip = ( + ObserverBase, + FakeQuantizeBase, + ) + target_module = _get_module_for_key(model, k) + if isinstance(target_module, cls_to_skip): + # Do not remove modules with expected shape mismatches + # them from the state_dict loading. They have special logic + # in _load_from_state_dict to handle the mismatches. + continue + + incorrect_shapes.append((k, shape_checkpoint, shape_model)) + checkpoint_state_dict.pop(k) + incompatible = model.load_state_dict(checkpoint_state_dict, strict=False) + # Remove keys with "_extra_state" suffix, which are non-parameter items introduced by TransformerEngine for FP8 handling + missing_keys = [k for k in incompatible.missing_keys if "_extra_state" not in k] + unexpected_keys = [k for k in incompatible.unexpected_keys if "_extra_state" not in k] + return _IncompatibleKeys( + missing_keys=missing_keys, + unexpected_keys=unexpected_keys, + incorrect_shapes=incorrect_shapes, + ) diff --git a/cosmos_training/cosmos/utils/cluster_env.py b/cosmos_training/cosmos/utils/cluster_env.py new file mode 100644 index 00000000..55787cf5 --- /dev/null +++ b/cosmos_training/cosmos/utils/cluster_env.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from enum import Enum +from functools import lru_cache +from typing import Dict + + +class ClusterType(Enum): + LOCAL = "local" + NGC = "ngc" + SLURM = "slurm" + + +class ClusterEnvInfo(Enum): + BASIC = "basic" + DETAILED = "detailed" + ALL = "all" + + +NGC_ENV_BASIC_VARS = [ + "NGC_JOB_ID", + "NGC_ARRAY_SIZE", + "NGC_GPUS_PER_NODE", +] + +SLURM_ENV_BASIC_VARS = [ + "SLURM_JOB_USER", + "SLURM_JOB_PARTITION", + "SLURM_LOG_DIR", + "SLURM_JOBID", + "SLURM_NNODES", + "SLURM_JOB_NAME", + "SLURM_JOB_NODELIST", + "SLURMD_NODENAME", +] + + +@lru_cache() +def is_local() -> bool: + """ + Check if the code is running on a local machine. + """ + return not is_ngc() and not is_slurm() + + +@lru_cache() +def is_ngc() -> bool: + """ + Check if the code is running on NGC. + """ + return "NGC_ARRAY_SIZE" in os.environ + + +@lru_cache() +def is_slurm() -> bool: + """ + Check if the code is running on SLURM. + """ + return "SLURM_JOB_ID" in os.environ + + +def get_ngc_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves NVIDIA GPU Cloud (NGC) environment variables based on the specified detail level. + The function filters environment variables to include only those relevant to NGC, + differentiated by the detail level specified. + + Parameters: + level (ClusterInfoLevel): The level of detail for the information returned. + Defaults to ClusterInfoLevel.BASIC. + + Returns: + dict: A dictionary containing the environment variables. If the level is BASIC, + it includes only predefined key variables that are considered basic. + If the level is DETAILED, it includes all environment variables that start + with "NGC_". + + Raises: + ValueError: If an unknown level is specified, an exception is raised indicating that the + level is not recognized. + """ + if level == ClusterEnvInfo.BASIC: + return {k: os.environ[k] for k in NGC_ENV_BASIC_VARS if k in os.environ} + elif level == ClusterEnvInfo.DETAILED: + return {k: os.environ[k] for k in os.environ if k.startswith("NGC_")} + elif level == ClusterEnvInfo.ALL: + return {k: v for k, v in os.environ} + else: + raise ValueError(f"Unknown level {level}") + + +def get_slurm_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves SLURM environment variables based on the specified detail level. + This function filters the environment variables related to the SLURM job scheduler + environment based on the provided detail level of the cluster information. + + Parameters: + level (ClusterEnvInfo): The detail level of the environment variables to retrieve. + This can be BASIC, DETAILED, or ALL. Defaults to BASIC. + + Returns: + Dict[str, str]: A dictionary containing the SLURM environment variables. The contents of + the dictionary vary based on the level: + - BASIC: Returns predefined key variables important for basic SLURM variables. + - DETAILED: Includes all variables that start with "SLURM_". + - ALL: Returns all environment variables available in the current session. + + Raises: + ValueError: If an unknown level is specified, it raises an exception indicating + that the level is not recognized. + """ + if level == ClusterEnvInfo.BASIC: + return {k: os.environ[k] for k in SLURM_ENV_BASIC_VARS if k in os.environ} + elif level == ClusterEnvInfo.DETAILED: + return {k: os.environ[k] for k in os.environ if k.startswith("SLURM_")} + elif level == ClusterEnvInfo.ALL: + return {k: v for k, v in os.environ.items()} + else: + raise ValueError(f"Unknown level {level}") + + +def get_cluster_env(level: ClusterEnvInfo = ClusterEnvInfo.BASIC) -> Dict[str, str]: + """ + Retrieves a combination of environment variables from the cluster, merging information from + both NVIDIA GPU Cloud (NGC) and SLURM environments based on the specified detail level. + This function provides a unified dictionary of environment settings that are crucial for + applications running in clustered computing environments. + + Parameters: + level (ClusterEnvInfo): The level of detail for the environment variables to be retrieved. + The level can be BASIC, DETAILED, or ALL. Defaults to BASIC. + - BASIC: Gathers basic environment variables from both NGC and SLURM. + - DETAILED: Includes more detailed information from both NGC and SLURM. + - ALL: Combines all available environment variables from the system + with NGC and SLURM specific ones. + + Returns: + Dict[str, str]: A dictionary containing key-value pairs of environment variables. + Initially includes the current working directory under the key 'PWD'. + """ + env_info = { + "PWD": os.getcwd(), # Always include the present working directory. + } + if level == ClusterEnvInfo.ALL: + env_info.update(os.environ) # Adds all system environment variables. + return env_info + + # For BASIC and DETAILED levels, merge environment variables from NGC and SLURM: + env_info.update(get_ngc_env(level)) + env_info.update(get_slurm_env(level)) + return env_info diff --git a/cosmos_training/cosmos/utils/config.py b/cosmos_training/cosmos/utils/config.py new file mode 100644 index 00000000..52991707 --- /dev/null +++ b/cosmos_training/cosmos/utils/config.py @@ -0,0 +1,600 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Training config system for Imaginare4""" + +from __future__ import annotations + +import importlib +import os +import time +from typing import Any, Dict, Optional, Type, TypeVar, Union + +import attrs +import torch +from loguru import logger as logging + +from cosmos.utils.flags import TRAINING + +try: + from megatron.core import ModelParallelConfig + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.lazy_config import LazyDict +from cosmos.utils import distributed +from cosmos.utils.misc import Color + +T = TypeVar("T") + + +def _is_attrs_instance(obj: object) -> bool: + """ + Helper function to check if an object is an instance of an attrs-defined class. + + Args: + obj: The object to check. + + Returns: + bool: True if the object is an instance of an attrs-defined class, False otherwise. + """ + return hasattr(obj, "__attrs_attrs__") + + +def make_freezable(cls: T) -> T: + """ + A decorator that adds the capability to freeze instances of an attrs-defined class. + + NOTE: This requires the wrapped attrs to be defined with attrs.define(slots=False) because we need + to hack on a "_is_frozen" attribute. + + This decorator enhances an attrs-defined class with the ability to be "frozen" at runtime. + Once an instance is frozen, its attributes cannot be changed. It also recursively freezes + any attrs-defined objects that are attributes of the class. + + Usage: + @make_freezable + @attrs.define(slots=False) + class MyClass: + attribute1: int + attribute2: str + + obj = MyClass(1, 'a') + obj.freeze() # Freeze the instance + obj.attribute1 = 2 # Raises AttributeError + + Args: + cls: The class to be decorated. + + Returns: + The decorated class with added freezing capability. + """ + + if not hasattr(cls, "__dict__"): + raise TypeError( + "make_freezable cannot be used with classes that do not define __dict__. Make sure that the wrapped " + "class was defined with `@attrs.define(slots=False)`" + ) + + original_setattr = cls.__setattr__ + + def setattr_override(self, key, value) -> None: # noqa: ANN001 + """ + Override __setattr__ to allow modifications during initialization + and prevent modifications once the instance is frozen. + """ + if hasattr(self, "_is_frozen") and self._is_frozen and key != "_is_frozen": + raise AttributeError("Cannot modify frozen instance") + original_setattr(self, key, value) # type: ignore + + cls.__setattr__ = setattr_override # type: ignore + + def freeze(self: object) -> None: + """ + Freeze the instance and all its attrs-defined attributes. + """ + for _, value in attrs.asdict(self, recurse=False).items(): + if _is_attrs_instance(value) and hasattr(value, "freeze"): + value.freeze() + self._is_frozen = True # type: ignore + + cls.freeze = freeze # type: ignore + + return cls + + +def _pretty_print_attrs_instance(obj: object, indent: int = 0, use_color: bool = False) -> str: + """ + Recursively pretty prints attrs objects with color. + """ + + assert attrs.has(obj.__class__) + + lines: list[str] = [] + for attribute in attrs.fields(obj.__class__): + value = getattr(obj, attribute.name) + if attrs.has(value.__class__): + if use_color: + lines.append(" " * indent + Color.cyan("* ") + Color.green(attribute.name) + ":") + else: + lines.append(" " * indent + "* " + attribute.name + ":") + lines.append(_pretty_print_attrs_instance(value, indent + 1, use_color)) + else: + if use_color: + lines.append( + " " * indent + Color.cyan("* ") + Color.green(attribute.name) + ": " + Color.yellow(value) + ) + else: + lines.append(" " * indent + "* " + attribute.name + ": " + str(value)) + return "\n".join(lines) + + +def pretty_print_overrides(overrides: Optional[list[str]] = None, use_color: bool = False) -> str: + """ + Pretty prints overrides. + """ + + lines: list[str] = [] + lines.append(Color.cyan("* ") + Color.green("overrides") + ": ") + for override in overrides: + if override == "--": + continue + if override.startswith("~"): + attribute_name = override[1:] + attribute_value = None + else: + attribute_name, attribute_value = override.split("=") + if use_color: + lines.append(" " + Color.cyan("* ") + Color.green(attribute_name) + ": " + Color.yellow(attribute_value)) + else: + lines.append(" " + "* " + attribute_name + ": " + str(attribute_value)) + + return "\n".join(lines) + + +@make_freezable +@attrs.define(slots=False) # slots=False is required for make_freezable. See the make_freezable notes for more info. +class ObjectStoreConfig: + # Whether the file I/O is from object store instead of local disk. + enabled: bool = False + # Path to the object store credentials file. + credentials: str = "" + # Object store bucket to read from / write to the objects. + bucket: str = "" + + +@make_freezable +@attrs.define(slots=False) +class HFExportConfig: + # Whether to enable HuggingFace safetensors export after each DCP checkpoint. + enabled: bool = False + # HuggingFace Hub repo ID to push exported weights to (e.g. "nvidia/cosmos3-qwen3-8b"). + # None means local/S3 only. + hf_repo_id: Optional[str] = None + # Object store for uploading exports. If not enabled, upload is skipped. + # To reuse the DCP checkpoint bucket, copy checkpoint.save_to_object_store here. + upload_to_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + # Export every N DCP checkpoints. Must be >= 1 and ideally a multiple of checkpoint.save_iter. + export_every_n: int = attrs.field(default=1, validator=attrs.validators.ge(1)) + + +@make_freezable +@attrs.define(slots=False) +class JobConfig: + # Project name. + project: str = "" + # Experiment name. + group: str = "" + # Run/job name. + name: str = "" + # W&B mode, can be "online", or "disabled". + wandb_mode: str = "online" + # Cluster configuration (optional, for cluster-specific settings). + cluster: Optional[Any] = None + + @property + def path(self) -> str: + return f"{self.project}/{self.group}/{self.name}" + + @property + def path_local(self) -> str: + local_root = os.environ.get("IMAGINAIRE_OUTPUT_ROOT", "/tmp/imaginaire4-output") + return f"{local_root}/{self.path}" + + +@make_freezable +@attrs.define(slots=False) +class EMAConfig: + # Enable tracking a set of exponential moving average (EMA) weights. + enabled: bool = False + # EMA decay rate. + beta: float = 0.9999 + # Enable removing "_orig_mod-" from buffer names that is added by torch.compile + torch_compile_buffer_renaming: bool = False + + +@make_freezable +@attrs.define(slots=False) +class PowerEMAConfig: + # Enable tracking a set of exponential moving average (EMA) weights. + enabled: bool = False + # EDM2 paper EMA decay rate. + s: float = 0.1 + # Enable removing "_orig_mod-" from buffer names that is added by torch.compile + torch_compile_buffer_renaming: bool = False + + +@make_freezable +@attrs.define(slots=False) +class DDPConfig: + # Traverse the computation graph to find parameters that don't receive gradients. + find_unused_parameters: bool = False + # Set to True if the computation graph does not change during the whole training loop. + static_graph: bool = True + # Set to True if we want to synchronize buffers. Set to False if the sync is going to be handled elsewhere. + broadcast_buffers: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CuDNNConfig: + # Set to True for better reproducibility of the results (only using deterministic cudnn functions). + deterministic: bool = False + # If set to True, cudnn will benchmark several algorithms and pick the fastest one. + benchmark: bool = True + + +@make_freezable +@attrs.define(slots=False) +class JITConfig: + # Enable exporting a JIT compiled model. + enabled: bool = False + # Input tensor shape, for example input. + input_shape: Union[list[int], None] = None + # Device to compile onto. + device: str = "cuda" + # # Data type to compile onto. + dtype: str = "bfloat16" + # Strict mode for PyTorch JIT. + strict: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CheckpointConfig: + # possible checkpoint class + type: Optional[Dict] = None + + # for dcp, whether to use async mode + dcp_async_mode_enabled: bool = False + + # Configs for saving the checkpoints to object store. + save_to_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + + # Save the checkpoint every N iterations. + save_iter: int = 999999999 + + # Load state_dict to the models in strict mode. If True, `allow_partial_load` in dcp + # planner will be set to False. DCP will raise an error if there are missing keys. + # If False, `allow_partial_load` in dcp planner will be set to True. DCP will not + # raise an error if there are missing keys. + strict_resume: bool = True + + # Configs for loading the checkpoints from object store. + load_from_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig) + + # Path of model weights to resume the checkpoint from. + load_path: str = "" + + # The following 3 flags (load_training_state, only_load_scheduler_state, keys_to_skip_loading) + # only take effect when the checkpoints are loaded from `load_path`. If loading happens from + # the previous checkpoint of the same model, these flags are ignored. + + # Whether to load the training states (optimizer/scheduler/grad-scaler) from the checkpoint path. + load_training_state: bool = False + + # Whether to load the scheduler state only from the checkpoint path. If + # load_training_state is True, this will be ignored. + only_load_scheduler_state: bool = False + + # When loading checkpoints from `load_path`, this list serves as a filter + # to bypass the loading for specific model parameters. A key is considered + # a match—and thus its loading is skipped—if it contains any element of this + # list as a substring. This mechanism allows for broad suppression of entire + # modules or parameter groups without requiring the specification of fully + # qualified names (FQNs). Skipping loading of keys is useful when the new model + # has a different component architecture, e.g. different RoPE embeddings than + # the current model. + keys_to_skip_loading: list[str] = [] + + # Configs for JIT compiling EMA model. + jit: JITConfig = attrs.field(factory=JITConfig) + + # Print detailed information during checkpoint saving/loading. + verbose: bool = True + + # Keys not to resume from the checkpoint, choices: ["model", "optim", "scheduler", "trainer", "dataloader"] + keys_not_to_resume: list[str] = [] + + # Whether to use the local filesystem for broadcasting checkpoint data (used for Tensor Parallel Checkpointer). + broadcast_via_filesystem: bool = False + load_ema_to_reg: bool = False + + # Enable GCS patch in boto3 for loading/saving checkpoints from/to GCS + enable_gcs_patch_in_boto3: bool = False + + # Config for exporting HuggingFace-compatible safetensors after each DCP checkpoint. + hf_export: HFExportConfig = attrs.field(factory=HFExportConfig) + + +@make_freezable +@attrs.define(slots=False) +class NVTXConfig: + """Config for NVTX ranges used in the main training loop. + + See tutorials/nanogpt for more details on how to integrate profiling into your model.""" + + # Enable the NVTX ranges. + enabled: bool = False + # Synchronize everything in each NVTX range. + cuda_synchronize: bool = False + + +@make_freezable +@attrs.define(slots=False) +class StragglerDetectionConfig: + """Config for Straggler detection tool: https://gitlab-master.nvidia.com/dl/gwe/fault_tolerance_related/straggler/-/tree/cupti?ref_type=heads""" + + # Enable the Straggler Detection. + enabled: bool = False + # How frequently should the Straggler reports be generated. + report_freq: int = 100 + # How frequently iterations should be profiled + profile_freq: int = 1 + # What is the maximum relative difference between GPUs after they are considered stragglers + max_diff: float = 2.0 + # Should the error be raised when straggler is detected + raise_error: bool = True + # Analyze kernels in the forward pass. + analyze_forward: bool = True + # Analyze kernels in the backward pass. + analyze_backward: bool = True + # Analyze kernels in the optimizer. + analyze_optimizer: bool = True + # Analyze dataloading time. + analyze_dataloading: bool = True + # Whether to save logs to S3 + save_s3: bool = False + + +@make_freezable +@attrs.define(slots=False) +class Profiling: + # Torch profiler: set this True to dump chrome traces. + enable_profiling: bool = False + # Nsight Systems: set this True AND launch under `nsys profile --capture-range=cudaProfilerApi`. + enable_nsys: bool = False + # CUDA memory snapshot: set this True to dump allocator snapshots. + enable_memory_snapshot: bool = False + save_s3: bool = False + profile_freq: int = 1 + # Number of warmup iterations before the active profile iteration. + profile_warmup: int = 3 + # Target ranks for profiling, each entry must be >=0 and < world_size. + target_ranks: list[int] = list(range(8)) + # The options below apply only to the torch profiler (enable_profiling). + # Set `record_shape` and `profile_memory` to False to reduce profile size. + record_shape: bool = False + profile_memory: bool = False + with_stack: bool = True + with_modules: bool = True + + +@make_freezable +@attrs.define(slots=False) +class CompileConfig: + """ + torch.compile config options passed to set_torch_compile_options function. + """ + + recompile_limit: int = 8 + use_duck_shape: bool = True + + +@make_freezable +@attrs.define(slots=False) +class TrainerConfig: + if TRAINING: + from cosmos.trainer import ImaginaireTrainer + from cosmos.utils import callback + + type: Type[ImaginaireTrainer] = ImaginaireTrainer + # Set the callback class. + # Defaults to the callbacks below. + callbacks: LazyDict = LazyDict( + dict( + ema=L(callback.EMAModelCallback)(), + progress_bar=L(callback.ProgressBarCallback)(), + wandb=L(callback.WandBCallback)(), + ) + ) + + # distributed parallelism strategy + distributed_parallelism: str = "ddp" + # Distributed data parallel configs. + ddp: DDPConfig = attrs.field(factory=DDPConfig) + # cuDNN configs. + cudnn: CuDNNConfig = attrs.field(factory=CuDNNConfig) + # Set the random seed. + seed: int = 0 + # Gradient scaler arguments (for torch.amp.GradScaler). + grad_scaler_args: dict = attrs.field(factory=lambda: dict(enabled=False)) + # Maximum number of iterations to train the model. + max_iter: int = 999999999 + # Maximum number of iterations to validate the model. If None, validate on the entire dataset. + max_val_iter: int | None = None + # How often we log the training stats. + logging_iter: int = 100 + # Whether we want to run the validation routines. + run_validation: bool = True + # How often we evaluate on the validation set. + validation_iter: int = 999999999 + # Whether to run the validation on the start of the training. + run_validation_on_start: bool = False + # Kill the process after N seconds since the last iteration (usually means dead job). + timeout_period: int = 999999999 + # Tensor memory organization format. + memory_format: torch.memory_format = torch.preserve_format + # Gradient accumulation (update step every N iteration). + grad_accum_iter: int = 1 + # Straggler Detection config + straggler_detection: StragglerDetectionConfig = attrs.field(factory=StragglerDetectionConfig) + # Profiling config + profiling: Profiling = attrs.field(factory=Profiling) + compile_config: CompileConfig = attrs.field(factory=CompileConfig) + + # Whether to save the checkpoint at iteration 0. + save_zero_checkpoint: bool = False + + +@make_freezable +@attrs.define(slots=False) +class Config: + """Config for an imaginaire4 job. + + See /README.md/Configuration System for more info. + """ + + # Model configs. + model: LazyDict + # Optimizer configs. + optimizer: LazyDict + # Scheduler configs. + scheduler: LazyDict + # Training data configs. + dataloader_train: LazyDict | None + # Validation data configs. + dataloader_val: LazyDict | None + + # Training job configs. + job: JobConfig = attrs.field(factory=JobConfig) + + # Trainer configs. + trainer: TrainerConfig = attrs.field(factory=TrainerConfig) + + if USE_MEGATRON: + # Megatron-Core configs + model_parallel: ModelParallelConfig = attrs.field(factory=ModelParallelConfig) + else: + model_parallel: None = None + + # Checkpointer configs. + checkpoint: CheckpointConfig = attrs.field(factory=CheckpointConfig) + + # enable upload reproducible setup to s3 + upload_reproducible_setup: bool = False + + def pretty_print(self, use_color: bool = False) -> str: + return _pretty_print_attrs_instance(self, 0, use_color) + + def to_dict(self) -> dict[str, Any]: + return attrs.asdict(self) + + def model_init_kwargs(self) -> dict[str, Any]: + """Live root-level sub-configs to pass into instantiate(self.model). + + Override in subclasses whose model __init__ expects fully-composed + top-level configs (e.g. policy/checkpoint/train) rather than the stale + LazyCall snapshot stored under config.model.* at make_config() time. + """ + return {} + + + def validate(self) -> None: + """Validate that the config has all required fields.""" + + # broadcast job.name across all ranks to make sure it is consistent + # otherwise, unaligned job names leads unaligned path to save checkpoints + job_name_tensor = torch.ByteTensor(bytearray(self.job.name, "utf-8")).cuda() + distributed.broadcast(job_name_tensor, 0) + self.job.name = job_name_tensor.cpu().numpy().tobytes().decode("utf-8") + + + assert self.job.project != "" + assert self.job.group != "" + assert self.job.name != "" + + +def load_config(config_path: str, opts: list[str], enable_one_logger: bool = False) -> Config: + from cosmos.utils.serialization import from_yaml, load_callable + + t1 = time.monotonic_ns() + if config_path.endswith(".yaml"): + config = from_yaml(config_path) + # for registration of dataloaders, etc. + _ = load_callable(config.__module__).make_config() + + from cosmos.utils.config_helper import override + + config = override(config, opts, remove_defaults=True) + else: + config = _load_py_config(config_path, opts, validate=False) + + if enable_one_logger: + try: + # pyrefly: ignore # missing-import + from cosmos.utils.one_logger.one_logger_override_utils import override_one_logger_callback + + ol_t1 = time.monotonic_ns() + config = override_one_logger_callback(config) + ol_t2 = time.monotonic_ns() + logging.debug(f"override_one_logger_callback: took {(ol_t2 - ol_t1) / 1e6:.2f}ms") + except ImportError: + pass + + t2 = time.monotonic_ns() + logging.debug(f"total time to load config: {(t2 - t1) / 1e6:.2f}ms") + return config + + +def _load_py_config(config_path: str, opts: list[str], validate: bool = True) -> Config: + + from cosmos.utils.config_helper import get_config_module, override + + t1 = time.monotonic_ns() + config_module = get_config_module(config_path) + t2 = time.monotonic_ns() + logging.debug(f"get_config_module: took {(t2 - t1) / 1e6:.2f}ms") + + t1 = time.monotonic_ns() + config = importlib.import_module(config_module).make_config() + t2 = time.monotonic_ns() + logging.debug(f"importlib.import_module: took {(t2 - t1) / 1e6:.2f}ms") + + t1 = time.monotonic_ns() + config = override(config, opts) + t2 = time.monotonic_ns() + logging.debug(f"override: took {(t2 - t1) / 1e6:.2f}ms") + + if validate: + t1 = time.monotonic_ns() + config.validate() + t2 = time.monotonic_ns() + logging.debug(f"config.validate: took {(t2 - t1) / 1e6:.2f}ms") + + return config diff --git a/cosmos_training/cosmos/utils/config_helper.py b/cosmos_training/cosmos/utils/config_helper.py new file mode 100644 index 00000000..6ef177aa --- /dev/null +++ b/cosmos_training/cosmos/utils/config_helper.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import importlib.util +import os +import pkgutil +import sys +from dataclasses import fields as dataclass_fields +from dataclasses import is_dataclass +from typing import Any, Dict, Optional + +import attr +import attrs +from hydra import compose, initialize +from hydra.core.config_store import ConfigStore +from hydra.core.global_hydra import GlobalHydra +from omegaconf import DictConfig, OmegaConf + +from cosmos.utils.config import Config +from cosmos.utils import log + + +def is_attrs_or_dataclass(obj) -> bool: + """ + Check if the object is an instance of an attrs class or a dataclass. + + Args: + obj: The object to check. + + Returns: + bool: True if the object is an instance of an attrs class or a dataclass, False otherwise. + """ + return is_dataclass(obj) or attr.has(type(obj)) + + +def get_fields(obj): + """ + Get the fields of an attrs class or a dataclass. + + Args: + obj: The object to get fields from. Must be an instance of an attrs class or a dataclass. + + Returns: + list: A list of field names. + + Raises: + ValueError: If the object is neither an attrs class nor a dataclass. + """ + if is_dataclass(obj): + return [field.name for field in dataclass_fields(obj)] + elif attr.has(type(obj)): + return [field.name for field in attr.fields(type(obj))] + else: + raise ValueError("The object is neither an attrs class nor a dataclass.") + + +def override(config: Config, overrides: Optional[list[str]] = None, remove_defaults: bool = False) -> Config: + """ + :param config: the instance of class `Config` (usually from `make_config`) + :param overrides: list of overrides for config + :return: the composed instance of class `Config` + """ + # Store the class of the config for reconstruction after overriding. + # config_class = type(config) + + def remove_defaults_filter(f, _): + return f.name != "defaults" + + # Convert Config object to a DictConfig object + config_dict = attrs.asdict(config, filter=remove_defaults_filter if remove_defaults else None) + config_omegaconf = DictConfig(content=config_dict, flags={"allow_objects": True}) + # Enforce "--" separator between the script arguments and overriding configs. + if overrides: + if overrides[0] != "--": + raise ValueError( + f'Hydra config overrides must be separated with a "--" token. but got overrides={overrides}, and overrides[0]={overrides[0]}' + ) + overrides = overrides[1:] + # Use Hydra to handle overrides + cs = ConfigStore.instance() + cs.store(name="config", node=config_omegaconf) + if not GlobalHydra().is_initialized(): + with initialize(version_base=None): + config_omegaconf = compose(config_name="config", overrides=overrides) + OmegaConf.resolve(config_omegaconf) + else: + config_omegaconf = compose(config_name="config", overrides=overrides) + OmegaConf.resolve(config_omegaconf) + + def config_from_dict(ref_instance: Any, kwargs: Any) -> Any: + """ + Construct an instance of the same type as ref_instance using the provided dictionary or data or unstructured data + + Args: + ref_instance: The reference instance to determine the type and fields when needed + kwargs: A dictionary of keyword arguments to use for constructing the new instance or primitive data or unstructured data + + Returns: + Any: A new instance of the same type as ref_instance constructed using the provided kwargs or the primitive data or unstructured data + + Raises: + AssertionError: If the fields do not match or if extra keys are found. + Exception: If there is an error constructing the new instance. + """ + is_type = is_attrs_or_dataclass(ref_instance) + if not is_type: + return kwargs + else: + ref_fields = set(get_fields(ref_instance)) + assert isinstance(kwargs, dict) or isinstance(kwargs, DictConfig), ( + "kwargs must be a dictionary or a DictConfig" + ) + keys = set(kwargs.keys()) + + # ref_fields must equal to or include all keys + extra_keys = keys - ref_fields + assert ref_fields == keys or keys.issubset(ref_fields), ( + f"Fields mismatch: {ref_fields} != {keys}. Extra keys found: {extra_keys} \n \t when constructing {type(ref_instance)} with {keys}" + ) + + resolved_kwargs: Dict[str, Any] = {} + for f in keys: + resolved_kwargs[f] = config_from_dict(getattr(ref_instance, f), kwargs[f]) + try: + new_instance = type(ref_instance)(**resolved_kwargs) + except Exception as e: + log.error(f"Error when constructing {type(ref_instance)} with {resolved_kwargs}") + log.error(e) + raise e + return new_instance + + config = config_from_dict(config, config_omegaconf) + + return config + + +def get_config_module(config_file: str) -> str: + if not config_file.endswith(".py"): + log.error("Config file cannot be specified as module.") + log.error("Please provide the path to the Python config file (relative to the Imaginaire4 root).") + # Convert to importable module format. + config_module = config_file.replace("/", ".").replace(".py", "") + if importlib.util.find_spec(config_module) is None: + raise ValueError(f"Imaginaire4 config module ({config_module}) not found.") + return config_module + + +def import_module(full_module_name: str, reload: bool = False): + """ + Import a module by name. + + Args: + full_module_name: The fully qualified name of the module to import. + reload: If True, reload the module if it's already imported. + """ + if full_module_name in sys.modules and reload: + importlib.reload(sys.modules[full_module_name]) + else: + importlib.import_module(full_module_name) + + +def import_all_modules_from_package(package_path: str, reload: bool = False, skip_underscore: bool = True) -> None: + """ + Import all modules from the specified package path recursively. + + This function is typically used in conjunction with Hydra to ensure that all modules + within a specified package are imported, which is necessary for registering configurations. + + Example usage: + ```python + import_all_modules_from_package("projects.cosmos.diffusion.v1.config.experiment", reload=True, skip_underscore=False) + ``` + + Args: + package_path (str): The dotted path to the package from which to import all modules. + reload (bool): Flag to determine whether to reload modules if they're already imported. + skip_underscore (bool): If True, skips importing modules that start with an underscore. + """ + log.critical(f"{'Reloading' if reload else 'Importing'} all modules from package {package_path}") + package = importlib.import_module(package_path) + package_directory = package.__path__ + + def import_modules_recursively(directory: str, prefix: str) -> None: + """ + Recursively imports or reloads all modules in the given directory. + + Args: + directory (str): The file system path to the current package directory. + prefix (str): The module prefix (e.g., 'projects.cosmos.diffusion.v1.config'). + """ + for _, module_name, is_pkg in pkgutil.iter_modules([directory]): + if skip_underscore and module_name.startswith("_"): + log.debug(f"Skipping module {module_name} as it starts with an underscore") + continue + + full_module_name = f"{prefix}.{module_name}" + log.debug(f"{'Reloading' if reload else 'Importing'} module {full_module_name}") + + import_module(full_module_name, reload=reload) + + if is_pkg: + sub_package_directory = os.path.join(directory, module_name) + import_modules_recursively(sub_package_directory, full_module_name) + + for directory in package_directory: + import_modules_recursively(directory, package_path) diff --git a/cosmos_training/cosmos/utils/configs/__pycache__/lr_scheduler.cpython-312.pyc b/cosmos_training/cosmos/utils/configs/__pycache__/lr_scheduler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83ff3849ce67a5275fd4b1540fbd380ca6085cc6 GIT binary patch literal 676 zcmZuuL2J}N6n>N4?5+)~=pqXBP^C(@;0AgwZJ~-D7B3>mUWREhNmC{>VP-;2e?c#T zc+sQasnmlP|AHR8EO=Q01wjNo2!$4fo}9^UM8OH)I8MLQ( z(>Zjf*-?wSv>l#XHDNK=_r6=XkIKtKl5T}$m~cwu!(f|+Iiu1fIzzHoY!b$5ekTcZ z@~r{D3;-6sUL%X~(;x3%{d)g)Ew&QC@p|7aySqfDIM1-sG*e|aC;~VZ~^=+-JGclKPR_^)n%LBv>MpKehHV zDRyX}>lkklMX{D7;R%oZTv4g~g7DZv&x)|fC|4qv0j-rk6*JpvIFqzPEC27w23c{| zSxtp~oi*7rE!tOCcg=R(KnVR@0CW!>!O9Wb{=BqtfX2(~pO!Wzy(`adO?ubImu^hD ez5V+W@BIGFYCH8>eP;~wFCU*m3pw(larzCc6Vl`W literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/configs/lr_scheduler.py b/cosmos_training/cosmos/utils/configs/lr_scheduler.py new file mode 100644 index 00000000..d6cd8d14 --- /dev/null +++ b/cosmos_training/cosmos/utils/configs/lr_scheduler.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.utils.functional.lr_scheduler import LambdaLinearScheduler +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.lazy_config import LazyDict + +LambdaLinearSchedulerConfig: LazyDict = L(LambdaLinearScheduler)( + warm_up_steps=[1000], + cycle_lengths=[10000000000000], + f_start=[1.0e-6], + f_max=[1.0], + f_min=[1.0], +) diff --git a/cosmos_training/cosmos/utils/context_managers.py b/cosmos_training/cosmos/utils/context_managers.py new file mode 100644 index 00000000..66ece380 --- /dev/null +++ b/cosmos_training/cosmos/utils/context_managers.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import ExitStack, contextmanager +from typing import Generator + +import torch + +from cosmos.utils.misc import timer + + + +@contextmanager +def disable_tf32() -> Generator[None, None, None]: + """Context manager to temporarily disable TF32 for CUDA matrix multiplications. + + This is useful for ensuring full FP32 precision in numerical computations, + particularly when debugging or comparing results between different implementations. + + Example: + with disable_tf32(): + result = torch.matmul(a, b) # Uses full FP32 precision + """ + old_allow_tf32_matmul = torch.backends.cuda.matmul.allow_tf32 + try: + torch.backends.cuda.matmul.allow_tf32 = False + with torch.backends.cudnn.flags(enabled=None, benchmark=None, deterministic=None, allow_tf32=False): + yield + finally: + torch.backends.cuda.matmul.allow_tf32 = old_allow_tf32_matmul + + +@contextmanager +def data_loader_init() -> Generator[None, None, None]: + """ + Wrap the data loader initialization with multiple context managers used for telemetry and one logger. + """ + contexts = [ + timer("init_data_loader"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] + + +@contextmanager +def model_init(set_barrier: bool = False) -> Generator[None, None, None]: + """ + Wrap the instantiation of the model with multiple context managers used for telemetry and one logger. + """ + contexts = [ + timer("init_model"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] + + +@contextmanager +def distributed_init() -> Generator[None, None, None]: + """ + Wrap the distributed initialization, used for telemetry and timers + """ + contexts = [ + timer("init_distributed"), + ] + with ExitStack() as stack: + yield [stack.enter_context(cm) for cm in contexts] diff --git a/cosmos_training/cosmos/utils/count_params.py b/cosmos_training/cosmos/utils/count_params.py new file mode 100644 index 00000000..79c03199 --- /dev/null +++ b/cosmos_training/cosmos/utils/count_params.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from torch import nn + + +def count_params(model: nn.Module, verbose=False) -> int: + total_params = sum(p.numel() for p in model.parameters() if p.requires_grad) + if verbose: + print(f"{model.__class__.__name__} has {total_params * 1.0e-6:.2f} M params.") + return total_params diff --git a/cosmos_training/cosmos/utils/device.py b/cosmos_training/cosmos/utils/device.py new file mode 100644 index 00000000..3f186f39 --- /dev/null +++ b/cosmos_training/cosmos/utils/device.py @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc +import math +import os +from functools import wraps + +import pynvml +from loguru import logger as logging + + +def get_gpu_architecture(): + """ + Retrieves the GPU architecture of the available GPUs. + + Returns: + str: The GPU architecture, which can be "H100", "A100", or "Other". + """ + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + for i in range(device_count): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + model_name = pynvml.nvmlDeviceGetName(handle) + if isinstance(model_name, bytes): + model_name = model_name.decode("utf-8") + print(f"GPU {i}: Model: {model_name}") + + # Check for specific models like H100 or A100 + if "H100" in model_name or "H200" in model_name: + return "H100" + elif "A100" in model_name: + return "A100" + elif "L40S" in model_name: + return "L40S" + elif "B200" in model_name: + return "B200" + except pynvml.NVMLError as error: + print(f"Failed to get GPU info: {error}") + finally: + pynvml.nvmlShutdown() + + # return "Other" incase of non hopper/ampere or error + return "Other" + + +class GPUArchitectureNotSupported(Exception): + """ + Custom exception raised when the expected GPU architecture is not supported. + """ + + pass + + +def print_gpu_mem(str=None): + try: + pynvml.nvmlInit() + meminfo = pynvml.nvmlDeviceGetMemoryInfo(pynvml.nvmlDeviceGetHandleByIndex(0)) + logging.info( + f"{str}: {meminfo.used / 1024 / 1024}/{meminfo.total / 1024 / 1024}MiB used ({meminfo.free / 1024 / 1024}MiB free)" + ) + except pynvml.NVMLError as error: + print(f"Failed to get GPU memory info: {error}") + + +def force_gc(): + print_gpu_mem() + print("gc()") + gc.collect() + print_gpu_mem() + print("empty cuda cache") + # print(torch.cuda.memory_summary()) + print_gpu_mem() + + +def gpu0_has_80gb_or_less(): + try: + pynvml.nvmlInit() + meminfo = pynvml.nvmlDeviceGetMemoryInfo(pynvml.nvmlDeviceGetHandleByIndex(0)) + return meminfo.total / 1024 / 1024 / 1024 <= 80 + except pynvml.NVMLError as error: + print(f"Failed to get GPU memory info: {error}") + + +class Device: + + + _nvml_affinity_elements = math.ceil(os.cpu_count() / 64) # type: ignore + + def __init__(self, device_idx: int): + + super().__init__() + self.handle = pynvml.nvmlDeviceGetHandleByIndex(device_idx) + + def get_name(self) -> str: + + return pynvml.nvmlDeviceGetName(self.handle) + + def get_cpu_affinity(self) -> list[int]: + + affinity_string = "" + for j in pynvml.nvmlDeviceGetCpuAffinity(self.handle, Device._nvml_affinity_elements): + # assume nvml returns list of 64 bit ints + affinity_string = "{:064b}".format(j) + affinity_string + affinity_list = [int(x) for x in affinity_string] + affinity_list.reverse() # so core 0 is in 0th element of list + return [i for i, e in enumerate(affinity_list) if e != 0] + + +def with_torch_device(device): + """ + Decorator factory that wraps a function to execute within a specific torch device context. + + This decorator ensures that all tensor allocations and operations within the decorated + function use the specified device by default. + + Args: + device: The torch device to use (e.g., 'cuda', 'cuda:0', 'cpu', or torch.device object). + + Returns: + A decorator function that wraps the target function with the specified device context. + + Example: + @with_torch_device('cuda:0') + def create_tensors(): + x = torch.randn(10, 10) # Will be created on cuda:0 + return x + """ + import torch + + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + with torch.device(device): + return fn(*args, **kwargs) + + return wrapper + + return decorator diff --git a/cosmos_training/cosmos/utils/distributed.py b/cosmos_training/cosmos/utils/distributed.py new file mode 100644 index 00000000..bbf24699 --- /dev/null +++ b/cosmos_training/cosmos/utils/distributed.py @@ -0,0 +1,491 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +import collections.abc +import ctypes +import functools +import os +from contextlib import contextmanager +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Callable, Container, Optional + +import pynvml +import torch +import torch.distributed as dist +from torch.distributed import get_process_group_ranks + +from cosmos.utils.flags import INTERNAL +from cosmos.utils.device import Device + +if dist.is_available(): + from torch.distributed.distributed_c10d import _get_default_group + from torch.distributed.utils import _sync_module_states, _verify_param_shape_across_processes + +from cosmos.utils import log + +if TYPE_CHECKING: + from cosmos.utils.config import DDPConfig + + +def init() -> int | None: + """Initialize distributed training.""" + if dist.is_initialized(): + return torch.cuda.current_device() + + # Set GPU affinity. + pynvml.nvmlInit() + local_rank = int(os.getenv("LOCAL_RANK", 0)) + try: + device = Device(local_rank) + os.sched_setaffinity(0, device.get_cpu_affinity()) + except pynvml.NVMLError as e: + log.warning(f"Failed to set device affinity: {e}") + # Set up NCCL communication. + os.environ["TORCH_NCCL_BLOCKING_WAIT"] = "0" + os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "1" + if dist.is_available(): + torch.cuda.set_device(local_rank) + # Get the timeout value from environment variable + timeout_seconds = os.getenv("TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC", 1800) + # Convert the timeout to an integer (if it isn't already) and then to a timedelta + timeout_timedelta = timedelta(seconds=int(timeout_seconds)) + dist.init_process_group(backend="nccl", init_method="env://", timeout=timeout_timedelta) + log.critical( + f"Initialized distributed training with local rank {local_rank} with timeout {timeout_seconds}", + rank0_only=False, + ) + # Increase the L2 fetch granularity for faster speed. + # For oss, we need to search for the library in site-packages. + if INTERNAL: + _libcudart = ctypes.CDLL("libcudart.so") + # Set device limit on the current device. + p_value = ctypes.cast((ctypes.c_int * 1)(), ctypes.POINTER(ctypes.c_int)) + _libcudart.cudaDeviceSetLimit(ctypes.c_int(0x05), ctypes.c_int(128)) + _libcudart.cudaDeviceGetLimit(p_value, ctypes.c_int(0x05)) + log.info(f"Training with {get_world_size()} GPUs.") + + +def get_rank(group: Optional[dist.ProcessGroup] = None) -> int: + """Get the rank (GPU device) of the worker. + + Returns: + rank (int): The rank of the worker. + """ + rank = 0 + if dist.is_available() and dist.is_initialized(): + rank = dist.get_rank(group) + return rank + + +def get_world_size(group: Optional[dist.ProcessGroup] = None) -> int: + """Get world size. How many GPUs are available in this job. + + Returns: + world_size (int): The total number of GPUs available in this job. + """ + world_size = 1 + if dist.is_available() and dist.is_initialized(): + world_size = dist.get_world_size(group) + return world_size + + +def is_rank0() -> bool: + """Check if current process is the master GPU. + + Returns: + (bool): True if this function is called from the master GPU, else False. + """ + return get_rank() == 0 + + +def is_local_rank0() -> bool: + """Check if current process is the local master GPU in the current node. + + Returns: + (bool): True if this function is called from the local master GPU, else False. + """ + return torch.cuda.current_device() == 0 + + +def rank0_only(func: Callable) -> Callable: + """Apply this function only to the master GPU. + + Example usage: + @rank0_only + def func(x): + return x + 3 + + Args: + func (Callable): a function. + + Returns: + (Callable): A function wrapper executing the function only on the master GPU. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + if is_rank0(): + return func(*args, **kwargs) + else: + return None + + return wrapper + + +def barrier() -> None: + """Barrier for all GPUs.""" + if dist.is_available() and dist.is_initialized(): + dist.barrier() + + +def rank0_first(func: Callable) -> Callable: + """run the function on rank 0 first, then on other ranks.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + if is_rank0(): + result = func(*args, **kwargs) + barrier() + if not is_rank0(): + result = func(*args, **kwargs) + return result + + return wrapper + + +def parallel_model_wrapper(config_ddp: DDPConfig, model: torch.nn.Module) -> torch.nn.Module | DistributedDataParallel: + """Wraps the model to enable data parallalism for training across multiple GPU devices. + + Args: + config_ddp (DDPConfig): The data parallel config. + model (torch.nn.Module): The PyTorch module. + + Returns: + model (torch.nn.Module | DistributedDataParallel): The data parallel model wrapper + if distributed environment is available, otherwise return the original model. + """ + if dist.is_available() and dist.is_initialized(): + local_rank = int(os.getenv("LOCAL_RANK", 0)) + try: + from megatron.core import parallel_state + + ddp_group = parallel_state.get_data_parallel_group(with_context_parallel=True) + except Exception as e: + log.info(e) + log.info("parallel_state not initialized, treating all GPUs equally for DDP") + ddp_group = None + + model = DistributedDataParallel( + model, + device_ids=[local_rank], + output_device=local_rank, + find_unused_parameters=config_ddp.find_unused_parameters, + static_graph=config_ddp.static_graph, + broadcast_buffers=config_ddp.broadcast_buffers, + process_group=ddp_group, + ) + return model + + +class DistributedDataParallel(torch.nn.parallel.DistributedDataParallel): + """This extends torch.nn.parallel.DistributedDataParallel with .training_step(). + + This borrows the concept of `forward-redirection` from Pytorch lightning. It wraps an ImaginaireModel such that + model.training_step() would be executed when calling self.training_step(), while preserving the behavior of calling + model() for Pytorch modules. Internally, this is a double rerouting mechanism (training_step -> forward -> + training_step), allowing us to preserve the function names and signatures. + """ + + def __init__(self, model: torch.nn.Module, *args, **kwargs): + super().__init__(model, *args, **kwargs) + self.show_sync_grad_static_graph_warning = True + + def training_step(self, *args, **kwargs) -> Any: + # Cache the original model.forward() method. + original_forward = self.module.forward + + def wrapped_training_step(*_args, **_kwargs): # noqa: ANN202 + # Unpatch immediately before calling training_step() because itself may want to call the real forward. + self.module.forward = original_forward + # The actual .training_step(). + return self.module.training_step(*_args, **_kwargs) + + # Patch the original_module's forward so we can redirect the arguments back to the real method. + self.module.forward = wrapped_training_step + # Call self, which implicitly calls self.forward() --> model.forward(), which is now model.training_step(). + # Without calling self.forward() or model.forward() explciitly, implicit hooks are also executed. + return self(*args, **kwargs) + + +@contextmanager +def ddp_sync_grad(model, enabled): + r""" + Context manager to enable/disable gradient synchronizations across DDP processes for DDP model. + Modified from: + https://pytorch.org/docs/stable/_modules/torch/nn/parallel/distributed.html#DistributedDataParallel.no_sync + Note that this is incompatible with static_graph=True and will be an no-op if static_graph=True. + + Within this context, gradients will be accumulated on module + variables, which will later be synchronized in the first + forward-backward pass exiting the context. + + .. warning:: + The forward pass should be included inside the context manager, or + else gradients will still be synchronized. + """ + assert isinstance(model, torch.nn.Module) + if isinstance(model, DistributedDataParallel): + old_require_backward_grad_sync = model.require_backward_grad_sync + if model.static_graph and model.require_backward_grad_sync != enabled: + if model.show_sync_grad_static_graph_warning: + log.warning("DDP static_graph=True is incompatible with sync_grad(). Performance will be reduced.") + model.show_sync_grad_static_graph_warning = False + else: + model.require_backward_grad_sync = enabled + try: + yield + finally: + if isinstance(model, DistributedDataParallel): + model.require_backward_grad_sync = old_require_backward_grad_sync + + +def collate_batches(data_batches: list[dict[str, torch.Tensor]]) -> torch.Tensor | dict[str, torch.Tensor]: + """Aggregate the list of data batches from all devices and process the results. + + This is used for gathering validation data batches with cosmos.utils.dataloader.DistributedEvalSampler. + It will return the data/output of the entire validation set in its original index order. The sizes of data_batches + in different ranks may differ by 1 (if dataset size is not evenly divisible), in which case a dummy sample will be + created before calling dis.all_gather(). + + Args: + data_batches (list[dict[str, torch.Tensor]]): List of tensors or (hierarchical) dictionary where + leaf entries are tensors. + + Returns: + data_gather (torch.Tensor | dict[str, torch.Tensor]): tensors or (hierarchical) dictionary where + leaf entries are concatenated tensors. + """ + if isinstance(data_batches[0], torch.Tensor): + # Concatenate the local data batches. + data_concat = torch.cat(data_batches, dim=0) # type: ignore + # Get the largest number of local samples from all ranks to determine whether to dummy-pad on this rank. + max_num_local_samples = torch.tensor(len(data_concat), device="cuda") + dist.all_reduce(max_num_local_samples, op=dist.ReduceOp.MAX) + if len(data_concat) < max_num_local_samples: + assert len(data_concat) + 1 == max_num_local_samples + dummy = torch.empty_like(data_concat[:1]) + data_concat = torch.cat([data_concat, dummy], dim=0) + dummy_count = torch.tensor(1, device="cuda") + else: + dummy_count = torch.tensor(0, device="cuda") + # Get all concatenated batches from all ranks and concatenate again. + dist.all_reduce(dummy_count, op=dist.ReduceOp.SUM) + data_concat = all_gather_tensor(data_concat.contiguous()) + data_collate = torch.stack(data_concat, dim=1).flatten(start_dim=0, end_dim=1) + # Remove the dummy samples. + if dummy_count > 0: + data_collate = data_collate[:-dummy_count] + elif isinstance(data_batches[0], collections.abc.Mapping): + data_collate = dict() + for key in data_batches[0].keys(): + data_collate[key] = collate_batches([data[key] for data in data_batches]) # type: ignore + else: + raise TypeError + return data_collate + + +@torch.no_grad() +def all_gather_tensor(tensor: torch.Tensor) -> list[torch.Tensor]: + """Gather the corresponding tensor from all GPU devices to a list. + + Args: + tensor (torch.Tensor): Pytorch tensor. + + Returns: + tensor_list (list[torch.Tensor]): A list of Pytorch tensors gathered from all GPU devices. + """ + tensor_list = [torch.zeros_like(tensor) for _ in range(get_world_size())] + dist.all_gather(tensor_list, tensor) + return tensor_list + + +def gather_object(payload: Any) -> list[Any] | None: + """Gather the corresponding object from all GPU devices to a rank 0 hosted list. + + Args: + payload: Any pickle-able object. + + Returns: + payload_list (list[Any]) | None: + Rank 0: A list of Pytorch tensors gathered from all RANK process. + Rest : None + """ + rank, world_size = get_rank(), get_world_size() + payload_gathered = [None] * world_size if rank == 0 else None + dist.gather_object(payload, object_gather_list=payload_gathered, dst=0) + return payload_gathered + + +def broadcast(tensor, src, group=None, async_op=False): + world_size = get_world_size() + if world_size < 2: + return tensor + dist.broadcast(tensor, src=src, group=group, async_op=async_op) + + +def dist_reduce_tensor(tensor, rank=0, reduce="mean"): + r"""Reduce to rank 0""" + world_size = get_world_size() + if world_size < 2: + return tensor + with torch.no_grad(): + dist.reduce(tensor, dst=rank) + if get_rank() == rank: + if reduce == "mean": + tensor /= world_size + elif reduce == "sum": + pass + else: + raise NotImplementedError + return tensor + + +def sync_model_states( + model: torch.nn.Module, + process_group: Optional[dist.ProcessGroup] = None, + src: int = 0, + params_and_buffers_to_ignore: Optional[Container[str]] = None, + broadcast_buffers: bool = True, +): + """ + Modify based on DDP source code + Synchronizes the parameters and buffers of a model across different processes in a distributed setting. + + This function ensures that all processes in the specified process group have the same initial parameters and + buffers from the source rank, typically rank 0. It is useful when different processes start with different model + states and a synchronization is required to ensure consistency across all ranks. + + Args: + model (nn.Module): The model whose parameters and buffers are to be synchronized. + process_group (dist.ProcessGroup, optional): The process group for communication. If None, + the default group is used. Defaults to None. + src (int, optional): The source rank from which parameters and buffers will be broadcasted. + Defaults to 0. + params_and_buffers_to_ignore (Optional[Container[str]], optional): A container of parameter and buffer + names to exclude from synchronization. Defaults to None, which means all parameters and buffers are + included. + broadcast_buffers (bool, optional): Whether to broadcast buffers or not. Defaults to True. + + Side Effects: + This function modifies the state of the model in-place to synchronize it with the source rank's model state. + + Raises: + RuntimeError: If the shapes of parameters across processes do not match, a runtime error will be raised. + + Examples: + >>> # downloading duplicated model weights from s3 in each rank and save network bandwidth + >>> # useful and save our time when model weights are huge + >>> if dist.get_rank == 0: + >>> model.load_state_dict(network_bound_weights_download_fn(s3_weights_path)) + >>> dist.barrir() + >>> sync_model_states(model) # sync rank0 weights to other ranks + """ + if not dist.is_available() or not dist.is_initialized(): + return + if process_group is None: + process_group = _get_default_group() + if not params_and_buffers_to_ignore: + params_and_buffers_to_ignore = set() + + log.info( + f"Synchronizing model states from rank {src} to all ranks in process group {get_process_group_ranks(process_group)}." + ) + + # Build tuple of (module, parameter) for all parameters that require grads. + modules_and_parameters = [ + (module, parameter) + for module_name, module in model.named_modules() + for parameter in [ + param + # Note that we access module.named_parameters instead of + # parameters(module). parameters(module) is only needed in the + # single-process multi device case, where it accesses replicated + # parameters through _former_parameters. + for param_name, param in module.named_parameters(recurse=False) + if f"{module_name}.{param_name}" not in params_and_buffers_to_ignore + # if param.requires_grad + # and f"{module_name}.{param_name}" not in params_and_buffers_to_ignore + ] + ] + + # Deduplicate any parameters that might be shared across child modules. + memo = set() + modules_and_parameters = [ + # "p not in memo" is the deduplication check. + # "not memo.add(p)" is always True, and it's only there to cause "add(p)" if needed. + (m, p) + for m, p in modules_and_parameters + if p not in memo and not memo.add(p) # type: ignore[func-returns-value] + ] + + # Build list of parameters. + parameters = [parameter for _, parameter in modules_and_parameters] + if len(parameters) == 0: + return + + _verify_param_shape_across_processes(process_group, parameters) + + _sync_module_states( + module=model, + process_group=process_group, + broadcast_bucket_size=int(250 * 1024 * 1024), + src=src, + params_and_buffers_to_ignore=params_and_buffers_to_ignore, + broadcast_buffers=broadcast_buffers, + ) + + +def all_gather_object(payload: Any) -> list[Any]: + """Gather the corresponding object from all GPU devices to all ranks.""" + world_size = get_world_size() + payload_gathered = [None] * world_size + dist.all_gather_object(payload_gathered, payload) + return payload_gathered # type: ignore[return-value] + + +def broadcast_object(object, *args, **kwargs): + """Broadcast a object to all GPU.""" + if not dist.is_available() or not dist.is_initialized(): + return object + object_list = [object] + dist.broadcast_object_list(object_list, *args, **kwargs) + return object_list[0] + + +def broadcast_object_list(object_list, *args, **kwargs): + """Broadcast a object list to all GPU. (the list is inplace edited)""" + if not dist.is_available() or not dist.is_initialized(): + return None + else: + dist.broadcast_object_list(object_list, *args, **kwargs) + + +def destroy_process_group(): + if not dist.is_available() or not dist.is_initialized(): + return + dist.destroy_process_group() diff --git a/cosmos_training/cosmos/utils/easy_io/README.md b/cosmos_training/cosmos/utils/easy_io/README.md new file mode 100644 index 00000000..38a658a3 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/README.md @@ -0,0 +1,3 @@ +# easy_io + +A flexible and easy-to-use io package built on top of [mmengine](https://github.com/open-mmlab/mmengine) (Apache-2.0 License) and inspired by [jammy](https://gitlab.com/qsh.zh/jam/) (MIT License). diff --git a/cosmos_training/cosmos/utils/easy_io/__init__.py b/cosmos_training/cosmos/utils/easy_io/__init__.py new file mode 100644 index 00000000..3159bfe6 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cosmos_training/cosmos/utils/easy_io/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1b027bdc1075a300da4040618a6597df29624f9 GIT binary patch literal 239 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 So0=&HKgmDB55eHU;Qv3ACsJGh literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__init__.py b/cosmos_training/cosmos/utils/easy_io/backends/__init__.py new file mode 100644 index 00000000..8e92bd7f --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/__init__.py @@ -0,0 +1,23 @@ +from cosmos.utils.flags import TRAINING +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend +from cosmos.utils.easy_io.backends.http_backend import HTTPBackend +from cosmos.utils.easy_io.backends.local_backend import LocalBackend +from cosmos.utils.easy_io.backends.registry_utils import backends, prefix_to_backends, register_backend + +__all__ = [ + "BaseStorageBackend", + "LocalBackend", + "HTTPBackend", + "register_backend", + "backends", + "prefix_to_backends", +] + +if TRAINING: + from cosmos.utils.easy_io.backends.boto3_backend import Boto3Backend + from cosmos.utils.easy_io.backends.msc_backend import MSCBackend + + __all__ += [ + "Boto3Backend", + "MSCBackend", + ] diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff155949435d5cfbbf607767a5402c04ed34fa3d GIT binary patch literal 960 zcmah`&u`N(6tnj%-&D!~y<_-MR6% zaN>fdND)Y!xOI%vPS|OaiOXimpT76(_rB-PU;F(YP)q)`PQG^l_@#qp8C!DkL6K9C zfCLgiVF;n-9nzsjXw=*wCbdGV<|eVJ6FN1wh)cU+m-fOQ^+FGV6eRX%82T9UUDd-O zeYz9w)OTHSiw?p81lUiyNiXfZ?*kC}g;#hZ|F<4x1WR}R>e+NQogFAXoJ0a2NyeiT zPonq@UM4E*9nR-3uR{MhizCv+M(G}JZi;f4bG*pjA<0mi8*rRvLSo*eMm9HZeTrL>NmJE`lOjrh3b55n?%B3bx`gZZd7fwr&0RVEbV~^I-u(s;@+7oEMkw z6Uvem!B6=;H78VC>?v3q5W))FufVhdlM3wr0O>c7{xKb|^U*mU0@JU|!^+(MX?kbw Z-7|N0Z5adm92l1U>0sSe=lqW9^AEwYDeM3M literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/auto_auth.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/auto_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83dd595b946532e846d6605d5bebd7f827378206 GIT binary patch literal 2944 zcmai0Uu@gP8NVYbiLxx&lAX-1os6yB!LnN`P1dyy>ST`WrAZSzsGGLYR%kSNQs_{m zdPmujAp?d{VEqRq8@9$n7tGz0i(r5sy@vrEvWH@Oq2OR~IS=i+3|L{Dg-itLNF67gfgLmoRNLGWQGeZnHHZ8n2|zjrnL~wL@~-EJ@_k}i4in& zu(9QCuZKv81iL|?%cNZo6OqU|CUMeEf;oZ9NgyZkp4VJZZ+TU^0TeHpgdXwd?9t9i zdh4rFrbCbZzja`b`}Jd>-&w}Vwomz@6G`laPZEnz)v;+(@`O{9o;YfiJ$bU|7`COF zUf{e11SpE8olgp0IQ`uCSay8kg()xAB(r0u($6PFPbyMd_e7n{dLnc3UVClzd+ctJ{>616mepJS&4q3?sbeFr1-EW+TR3StKx;Ob>&ul9~i#})Jr zxb3=lV@Hp>Jm5g|I`s0>*)cmvmjIi7IQ+4RrDa=_j*%n z3G1PXFcWT!@AeHBa_t4t@q?3(&NPB?R~0$21~ZH3p^E6V@8JiWjs{n0-e5uc8vDo< zaql@Xsz?p%e9g@8{WrwO+1K13Rq%}+Z#<3`0+qnq0=GCM4SbCe?twn^7-D!%z~};a zluIs)7tkF3wK#{9Efe#-6SlG&B1+CMi2_kHYv@E*?%`n%r#}5R6v}s-KbW>lrmk4F zqZC!jh+$}OkOxUKJmPt4V3%U zOXP{;M?A~ss9ngy$vnv}5-S;`+_kjL-^o%7#EbHiaE+|b?z5+7h~{J&q@tQ9SaL|Y}2q=O0!u3IxrkxqrqpVRYtN7RSgTwkt#7l zS*omBd0w5JNm+h8W|yc&8j|LWon=doVX_oaSvhOiscBWaL@b@9)RJRop}9C*EPG+T zjL%V?!@W@1KcV>t`eG6NHF97}j$Dynm)(w(d+3ClO27Y@8y~+Ve{VCEc%x-gj(r?C za68ghjr6UgHzEV8iE8AL+mXZ7$lR ztFLS(I^H<>xd`;i1Hpa-%_6D^V&D|sQifL-s>%pNOf858kN+vIt@ODkUUXmje)Yr; z+#gPFoS44acP;tjw9#@hPx# literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/base_backend.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/base_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99c0841a568b5e24036d78e8479fbefda60f693f GIT binary patch literal 7159 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
    NbcW6x* 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 BJ&^za literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/http_backend.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/http_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da5432b955beda3a1fd836b409cccfb3cf45380f GIT binary patch literal 11489 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/msc_backend.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/backends/__pycache__/msc_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74849c9735c7b67b9225cbd2ef4bf164d53ebee2 GIT binary patch literal 43848 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/backends/auto_auth.py b/cosmos_training/cosmos/utils/easy_io/backends/auto_auth.py new file mode 100644 index 00000000..a2eb35c4 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/auto_auth.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import json +from collections.abc import Generator +from typing import IO, Any, Optional, Union + +from cosmos.utils import log +from cosmos.utils.env_parsers.cred_env_parser import CRED_ENVS, CRED_ENVS_DICT + +DEPLOYMENT_ENVS = ["prod", "dev", "stg"] + + +# context manger to open a file or read from env variable +@contextlib.contextmanager +def open_auth(s3_credential_path: Optional[Any], mode: str) -> Generator[Union[None, dict[str, Any], IO]]: + if not s3_credential_path: + log.info(f"No credential file provided {s3_credential_path}.") + yield None + return + + name = s3_credential_path.split("/")[-1].split(".")[0] + if not name: + raise ValueError(f"Could not parse into env var: {s3_credential_path}") + cred_env_name = f"PROD_{name.upper()}" + + if CRED_ENVS.APP_ENV in DEPLOYMENT_ENVS and cred_env_name in CRED_ENVS_DICT: + object_storage_config = get_creds_from_env(cred_env_name) + log.info(f"using ENV vars for {cred_env_name}") + yield object_storage_config + else: + log.info(f"using credential file: {s3_credential_path}") + with open(s3_credential_path, mode) as f: + yield f + + +def get_creds_from_env(cred_env_name: str) -> dict[str, Any]: + try: + object_storage_config = CRED_ENVS_DICT[cred_env_name] + except KeyError: + raise ValueError(f"Could not find {cred_env_name} in CRED_ENVS") + empty_args = {key.upper() for key in object_storage_config if object_storage_config[key] == ""} + if empty_args: + raise ValueError(f"Some required environment variable(s) were not provided for {cred_env_name}", empty_args) + return object_storage_config + + +def json_load_auth(f: Union[None, dict[str, Any], IO]) -> dict[str, Any]: + # None. + if f is None: + return {} + # dict[str, Any]. + elif isinstance(f, dict): + return f + # IO. + else: + return json.load(f) diff --git a/cosmos_training/cosmos/utils/easy_io/backends/base_backend.py b/cosmos_training/cosmos/utils/easy_io/backends/base_backend.py new file mode 100644 index 00000000..1c3853b4 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/base_backend.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import os.path as osp +from abc import ABCMeta, abstractmethod +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union + + +def mkdir_or_exist(dir_name, mode=0o777): + if dir_name == "": + return + dir_name = osp.expanduser(dir_name) + os.makedirs(dir_name, mode=mode, exist_ok=True) + + +def has_method(obj, method): + return hasattr(obj, method) and callable(getattr(obj, method)) + + +class BaseStorageBackend(metaclass=ABCMeta): + """Abstract class of storage backends.""" + + # a flag to indicate whether the backend can create a symlink for a file + # This attribute will be deprecated in future. + _allow_symlink: bool = False + + @property + def allow_symlink(self) -> bool: + return self._allow_symlink + + @property + def name(self) -> str: + return self.__class__.__name__ + + @abstractmethod + def size(self, filepath: Union[str, Path]) -> int: + pass + + @abstractmethod + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + pass + + @abstractmethod + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + pass + + @abstractmethod + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + pass + + @abstractmethod + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + pass + + @abstractmethod + def exists(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def isdir(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def isfile(self, filepath: Union[str, Path]) -> bool: + pass + + @abstractmethod + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + pass + + @abstractmethod + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + pass + + @abstractmethod + def copyfile(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copytree(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copyfile_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copytree_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + pass + + @abstractmethod + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + pass + + @abstractmethod + def copytree_to_local(self, src: Union[str, Path], dst: Union[str, Path]) -> Union[str, Path]: + pass + + @abstractmethod + def remove(self, filepath: Union[str, Path]) -> None: + pass + + @abstractmethod + def rmtree(self, dir_path: Union[str, Path]) -> None: + pass + + @abstractmethod + def copy_if_symlink_fails(self, src: Union[str, Path], dst: Union[str, Path]) -> bool: + pass + + @abstractmethod + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + pass + + @abstractmethod + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + pass diff --git a/cosmos_training/cosmos/utils/easy_io/backends/boto3_backend.py b/cosmos_training/cosmos/utils/easy_io/backends/boto3_backend.py new file mode 100644 index 00000000..a6f11773 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/boto3_backend.py @@ -0,0 +1,862 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import re +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from shutil import SameFileError +from typing import Optional, Union + +from cosmos.utils import log +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend, has_method, mkdir_or_exist +from cosmos.utils.easy_io.backends.boto3_client import Boto3Client + + +class Boto3Backend(BaseStorageBackend): + """boto3 storage backend (for internal usage). + + **Deprecated**. Use the MSC backend instead. + + Boto3Backend supports reading and writing data to multiple clusters. + If the file path contains the cluster name, Boto3Backend will read data + from specified cluster or write data to it. Otherwise, Boto3Backend will + access the default cluster. + + Args: + path_mapping (dict, optional): Path mapping dict from local path to + Boto3 path. When ``path_mapping={'src': 'dst'}``, ``src`` in + ``filepath`` will be replaced by ``dst``. Defaults to None. + s3_credential_path (str, optional): Config path of Boto3 client. Default: None. + `New in version 0.3.3`. + + Examples: + >>> backend = Boto3Backend() + >>> filepath1 = 's3://path/of/file' + >>> filepath2 = 'cluster-name:s3://path/of/file' + >>> backend.get(filepath1) # get data from default cluster + >>> client.get(filepath2) # get data from 'cluster-name' cluster + """ + + def __init__( + self, + s3_credential_path: str = "", + path_mapping: Optional[dict] = None, + ): + self._client = Boto3Client(s3_credential_path=s3_credential_path) + assert isinstance(path_mapping, dict) or path_mapping is None + self.path_mapping = path_mapping + if path_mapping: + for k, v in path_mapping.items(): + log.critical(f"Path mapping: {k} -> {v}", rank0_only=False) + + def _map_path(self, filepath: Union[str, Path]) -> str: + """Map ``filepath`` to a string path whose prefix will be replaced by + :attr:`self.path_mapping`. + + Args: + filepath (str or Path): Path to be mapped. + """ + filepath = str(filepath) + if self.path_mapping is not None: + for k, v in self.path_mapping.items(): + filepath = filepath.replace(k, v, 1) + return filepath + + def _format_path(self, filepath: str) -> str: + """Convert a ``filepath`` to standard format of s3 oss. + + If the ``filepath`` is concatenated by ``os.path.join``, in a Windows + environment, the ``filepath`` will be the format of + 's3://bucket_name\\image.jpg'. By invoking :meth:`_format_path`, the + above ``filepath`` will be converted to 's3://bucket_name/image.jpg'. + + Args: + filepath (str): Path to be formatted. + """ + return re.sub(r"\\+", "/", filepath) + + def _replace_prefix(self, filepath: Union[str, Path]) -> str: + filepath = str(filepath) + return filepath + # return filepath.replace('s3://', 's3://') + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.size(filepath) + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Return bytes read from filepath. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.get(filepath) + b'hello world' + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + value = self._client.get(filepath=filepath, offset=offset, size=size) + return value + + def get_text( + self, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.get_text(filepath) + 'hello world' + """ + return str(self.get(filepath), encoding=encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath``. + + Args: + obj (bytes): Data to be saved. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.put(b'hello world', filepath) + """ + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.put(obj, filepath) + + def fast_put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path], num_processes: int = 32) -> None: + """Write bytes to a given ``filepath`` with multiple processes and async""" + assert num_processes > 1 + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.fast_put(obj, filepath, num_processes=num_processes) + + def put_text( + self, + obj: str, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> None: + """Write text to a given ``filepath``. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to encode the ``obj``. + Defaults to 'utf-8'. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.put_text('hello world', filepath) + """ + self.put(bytes(obj, encoding=encoding), filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.exists(filepath) + True + """ + if not (has_method(self._client, "contains") and has_method(self._client, "isdir")): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `contains` and `isdir` methods, please use a higher" + "version or dev branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.contains(filepath) or self._client.isdir(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/dir' + >>> backend.isdir(filepath) + True + """ + if not has_method(self._client, "isdir"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `isdir` method, please use a higher version or dev" + " branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.isfile(filepath) + True + """ + if not has_method(self._client, "contains"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `contains` method, please use a higher version or " + "dev branch instead." + ) + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + return self._client.contains(filepath) + + def join_path( + self, + filepath: Union[str, Path], + *filepaths: Union[str, Path], + ) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result after concatenation. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.join_path(filepath, 'another/path') + 's3://path/of/file/another/path' + >>> backend.join_path(filepath, '/another/path') + 's3://path/of/file/another/path' + """ + filepath = self._format_path(self._map_path(filepath)) + if filepath.endswith("/"): + filepath = filepath[:-1] + formatted_paths = [filepath] + for path in filepaths: + formatted_path = self._format_path(self._map_path(path)) + formatted_paths.append(formatted_path.lstrip("/")) + + return "/".join(formatted_paths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str or Path): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = Boto3Backend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> filepath = 's3://path/of/file' + >>> with backend.get_local_path(filepath) as path: + ... # do something here + """ + assert self.isfile(filepath) + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 's3://path/of/file' + >>> dst = 's3://path/of/file1' + >>> backend.copyfile(src, dst) + 's3://path/of/file1' + + >>> # dst is a directory + >>> dst = 's3://path/of/dir' + >>> backend.copyfile(src, dst) + 's3://path/of/dir/file' + """ + src = self._format_path(self._map_path(src)) + dst = self._format_path(self._map_path(dst)) + if self.isdir(dst): + dst = self.join_path(dst, src.split("/")[-1]) + + if src == dst: + raise SameFileError("src and dst should not be same") + + self.put(self.get(src), dst) + return dst + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + src and dst should have the same prefix. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/dir' + >>> dst = 's3://path/of/dir1' + >>> backend.copytree(src, dst) + 's3://path/of/dir1' + """ + src = self._format_path(self._map_path(src)) + dst = self._format_path(self._map_path(dst)) + + if self.exists(dst): + raise FileExistsError("dst should not exist") + + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + src_path = self.join_path(src, path) + dst_path = self.join_path(dst, path) + self.put(self.get(src_path), dst_path) + + return dst + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Upload a local file src to dst and return the destination file. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 'path/of/your/file' + >>> dst = 's3://path/of/file1' + >>> backend.copyfile_from_local(src, dst) + 's3://path/of/file1' + + >>> # dst is a directory + >>> dst = 's3://path/of/dir' + >>> backend.copyfile_from_local(src, dst) + 's3://path/of/dir/file' + """ + dst = self._format_path(self._map_path(dst)) + if self.isdir(dst): + dst = self.join_path(dst, os.path.basename(src)) + + with open(src, "rb") as f: + self.put(f.read(), dst) + + return dst + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = Boto3Backend() + >>> src = 'path/of/your/dir' + >>> dst = 's3://path/of/dir1' + >>> backend.copytree_from_local(src, dst) + 's3://path/of/dir1' + """ + dst = self._format_path(self._map_path(dst)) + if self.exists(dst): + raise FileExistsError("dst should not exist") + + src = str(src) + + for cur_dir, _, files in os.walk(src): + for f in files: + src_path = os.path.join(cur_dir, f) + dst_path = self.join_path(dst, src_path.replace(src, "")) + self.copyfile_from_local(src_path, dst_path) + + return dst + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = Boto3Backend() + >>> # dst is a file + >>> src = 's3://path/of/file' + >>> dst = 'path/of/your/file' + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/file' + + >>> # dst is a directory + >>> dst = 'path/of/your/dir' + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/dir/file' + """ + assert dst_type in ["file", "dir"] + # There is no good way to detect whether dst is a directory or a file, so we make dst_type required + if dst_type == "dir": + basename = os.path.basename(src) + if isinstance(dst, str): + dst = os.path.join(dst, basename) + else: + assert isinstance(dst, Path) + dst = dst / basename + + # Create parent directory if it doesn't exist + parent_dir = os.path.dirname(dst) + os.makedirs(parent_dir, exist_ok=True) + + try: + with open(dst, "wb") as f: + data = self.get(src) + f.write(data) + except Exception as e: + log.error(f"Failed to write file: {e}") + raise + + return dst + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/dir' + >>> dst = 'path/of/your/dir' + >>> backend.copytree_to_local(src, dst) + 'path/of/your/dir' + """ + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + dst_path = os.path.join(dst, path) + mkdir_or_exist(os.path.dirname(dst_path)) + with open(dst_path, "wb") as f: + f.write(self.get(self.join_path(src, path))) + + return dst + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> backend = Boto3Backend() + >>> filepath = 's3://path/of/file' + >>> backend.remove(filepath) + """ + if not has_method(self._client, "delete"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `delete` method, please use a higher version or dev " + "branch instead." + ) + + if not self.exists(filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath): + raise IsADirectoryError("filepath should be a file") + + filepath = self._map_path(filepath) + filepath = self._format_path(filepath) + filepath = self._replace_prefix(filepath) + self._client.delete(filepath) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> backend.rmtree(dir_path) + """ + for path in self.list_dir_or_file(dir_path, list_dir=False, recursive=True): + filepath = self.join_path(dir_path, path) + self.remove(filepath) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + Directly copy src to dst because PetrelBacekend does not support create + a symbolic link. + + Args: + src (str or Path): A file or directory to be copied. + dst (str or Path): Copy a file or directory to dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + bool: Return False because Boto3Backend does not support create + a symbolic link. + + Examples: + >>> backend = Boto3Backend() + >>> src = 's3://path/of/file' + >>> dst = 's3://path/of/your/file' + >>> backend.copy_if_symlink_fails(src, dst) + False + >>> src = 's3://path/of/dir' + >>> dst = 's3://path/of/your/dir' + >>> backend.copy_if_symlink_fails(src, dst) + False + """ + if self.isfile(src): + self.copyfile(src, dst) + else: + self.copytree(src, dst) + return False + + def list_dir(self, dir_path: Union[str, Path]): + """List all folders in an S3 bucket with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> backend.list_dir(dir_path) + """ + dir_path = self._map_path(dir_path) + dir_path = self._format_path(dir_path) + dir_path = self._replace_prefix(dir_path) + return self._client.ls_dir(dir_path) + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + Boto3 has no concept of directories but it simulates the directory + hierarchy in the filesystem through public prefixes. In addition, + if the returned path ends with '/', it means the path is a public + prefix which is a logical directory. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + In addition, the returned path of directory will not contains the + suffix '/' which is consistent with other backends. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = Boto3Backend() + >>> dir_path = 's3://path/of/dir' + >>> # list those files and directories in current directory + >>> for file_path in backend.list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in backend.list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in backend.list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in backend.list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in backend.list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ # noqa: E501 + if not has_method(self._client, "list"): + raise NotImplementedError( + "Current version of Boto3 Python SDK has not supported " + "the `list` method, please use a higher version or dev" + " branch instead." + ) + + dir_path = self._map_path(dir_path) + dir_path = self._format_path(dir_path) + dir_path = self._replace_prefix(dir_path) + if list_dir and suffix is not None: + raise TypeError("`list_dir` should be False when `suffix` is not None") + + if list_dir and not list_file and not recursive: + raise TypeError( + "Please use `list_dir` instead of `list_dir_or_file` when you only want to list the first level directories." + ) + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + # Boto3's simulated directory hierarchy assumes that directory paths + # should end with `/` + if not dir_path.endswith("/"): + dir_path += "/" + + root = dir_path + + def _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive): + # Keep track of directories we've already yielded to avoid duplicates + yielded_dirs = set() if list_dir else None + + for path in self._client.list(dir_path): + # All paths returned by S3 list are file paths, never directory paths + absolute_path = self.join_path(dir_path, path) + rel_path = absolute_path[len(root) :] + + # If we want directories, extract directory prefixes from file paths + # boto3 client actually never return dir, it only return file paths + if list_dir and "/" in rel_path: + if not recursive: + # Non-recursive: only yield immediate child directory (first level) + first_slash_pos = rel_path.find("/") + immediate_child_dir = rel_path[:first_slash_pos] + + if immediate_child_dir not in yielded_dirs: + yielded_dirs.add(immediate_child_dir) + yield immediate_child_dir + else: + # Recursive: yield all directory levels + path_parts = rel_path.split("/")[:-1] # Exclude filename + current_dir = "" + for part in path_parts: + if current_dir: + current_dir += "/" + part + else: + current_dir = part + + if current_dir not in yielded_dirs: + yielded_dirs.add(current_dir) + yield current_dir + + # Handle file listing + if (suffix is None or rel_path.endswith(suffix)) and list_file: + yield rel_path + + return _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) + + def generate_presigned_url(self, url: str, client_method: str = "get_object", expires_in: int = 3600) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on Boto3 backend. + + Note: + Now only work on Boto3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Default: 'get_object'. + expires_in (int): expires, in seconds. Default: 3600. + + Returns: + str: Generated presigned url. + """ + raise NotImplementedError("generate_presigned_url is not supported in Boto3Backend") + return self._client.generate_presigned_url(url, client_method, expires_in) diff --git a/cosmos_training/cosmos/utils/easy_io/backends/boto3_client.py b/cosmos_training/cosmos/utils/easy_io/backends/boto3_client.py new file mode 100644 index 00000000..e7df8858 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/boto3_client.py @@ -0,0 +1,640 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import concurrent.futures +import io +import os +import time +from collections.abc import Generator +from math import ceil +from multiprocessing import shared_memory +from typing import Any, Optional + +import boto3 +import numpy as np +from botocore.config import Config as S3Config +from botocore.exceptions import ClientError + +import cosmos.utils.easy_io.backends.auto_auth as auto +from cosmos.utils import log +from cosmos.utils.env_parsers.cred_env_parser import CRED_ENVS + +try: + # pyrefly: ignore # import-error + import aioboto3 + + # pyrefly: ignore # import-error + import aioboto3.session + + # pyrefly: ignore # import-error + from aiobotocore.config import AioConfig + + # pyrefly: ignore # import-error + from aiobotocore.session import AioSession +except ImportError: + aioboto3 = None + AioSession = None + +MAX_RETRIES = 5 +RETRY_DELAY = 1 # seconds + + +async def upload_single_part_async( + s3: AioSession, bucket: str, key: str, part_number: int, data: bytes, upload_id: str +) -> dict[str, Any]: + """ + Uploads a single part of a file asynchronously to S3. + + Args: + s3 (S3): The S3 client. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + part_number (int): The part number of the upload. + data (bytes): The data to upload. + upload_id (str): The upload ID for the multipart upload. + + Returns: + dict[str, Any]: A dictionary containing the part number and ETag. + """ + for attempt in range(MAX_RETRIES): + try: + response = await s3.upload_part( + Bucket=bucket, Key=key, PartNumber=part_number, UploadId=upload_id, Body=data + ) + return {"PartNumber": part_number, "ETag": response["ETag"]} + except (ClientError, asyncio.TimeoutError, Exception) as e: + log.warning(f"Attempt {attempt + 1} failed for part {part_number}: {str(e)}", rank0_only=False) + if attempt < MAX_RETRIES - 1: + await asyncio.sleep(RETRY_DELAY * (2**attempt)) # Exponential backoff + else: + log.error(f"Failed to upload part {part_number} after {MAX_RETRIES} attempts", rank0_only=False) + raise + + +async def upload_parts_async( + part_size: int, + part_numbers: range, + upload_id: str, + data: bytes, + bucket: str, + key: str, + client_config: dict[str, Any], +) -> list[dict[str, Any]]: + """ + Uploads multiple parts of a file asynchronously to S3. + + Args: + part_size (int): The size of each part in bytes. + part_numbers (range): The range of part numbers to upload. + upload_id (str): The upload ID for the multipart upload. + data (bytes): The data to upload. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + list[dict[str, Any]]: A list of dictionaries containing part numbers and ETags. + """ + session = aioboto3.Session() + config = AioConfig(retries={"max_attempts": 3, "mode": "adaptive"}, connect_timeout=5, read_timeout=10) + start_idx = part_numbers[0] + async with session.client("s3", config=config, **client_config) as s3: + tasks = [] + for part_number in part_numbers: + start = (part_number - start_idx) * part_size + end = min(start + part_size, len(data)) + part_data = data[start:end] + tasks.append(upload_single_part_async(s3, bucket, key, part_number + 1, part_data, upload_id)) + + results = await asyncio.gather(*tasks, return_exceptions=True) + + successful_parts = [] + failed_parts = [] + for part_number, result in enumerate(results, start=start_idx + 1): + if isinstance(result, Exception): + failed_parts.append(part_number) + else: + successful_parts.append(result) + + if failed_parts: + log.error(f"Failed to upload parts: {failed_parts}", rank0_only=False) + raise Exception(f"Failed to upload {len(failed_parts)} parts") + + successful_parts.sort(key=lambda part: part["PartNumber"]) + return successful_parts + + +def upload_parts_to_s3(args: tuple[range, str, int, bytes, str, str, dict[str, Any]]) -> list[dict[str, Any]]: + """ + Uploads parts of a file to S3 using a new event loop. + + Args: + args (tuple[range, str, int, bytes, str, str, dict[str, Any]]): The arguments for uploading parts, including: + part_numbers (range): The range of part numbers to upload. + upload_id (str): The upload ID for the multipart upload. + part_size (int): The size of each part in bytes. + data (bytes): The data to upload. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + list[dict[str, Any]]: A list of dictionaries containing part numbers and ETags. + """ + part_numbers, upload_id, part_size, data, bucket, key, client_config = args + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + parts = loop.run_until_complete( + upload_parts_async(part_size, part_numbers, upload_id, data, bucket, key, client_config) + ) + loop.close() + return parts + + +async def download_single_part_async( + s3, bucket: str, key: str, part_number: int, start: int, end: int, shm_name: str, part_size: int +) -> None: + """ + Downloads a single part of a file asynchronously and writes it to shared memory. + + Args: + s3 (S3): The S3 client. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + part_number (int): The part number. + start (int): The start byte of the part. + end (int): The end byte of the part. + shm_name (str): The name of the shared memory block. + part_size (int): The size of each part in bytes. + """ + for attempt in range(MAX_RETRIES): + try: + range_header = f"bytes={start}-{end}" + response = await s3.get_object(Bucket=bucket, Key=key, Range=range_header) + data = await response["Body"].read() + + shm = shared_memory.SharedMemory(name=shm_name) + offset = part_number * part_size + shm.buf[offset : offset + len(data)] = data + shm.close() + return + except (ClientError, asyncio.TimeoutError, Exception) as e: + log.warning(f"Attempt {attempt + 1} failed for part {part_number}: {str(e)}", rank0_only=False) + if attempt < MAX_RETRIES - 1: + await asyncio.sleep(RETRY_DELAY * (2**attempt)) # Exponential backoff + else: + log.error(f"Failed to download part {part_number} after {MAX_RETRIES} attempts", rank0_only=False) + raise + + +async def download_parts_async( + part_size: int, part_numbers: range, bucket: str, key: str, client_config: dict[str, Any], shm_name: str +) -> None: + """ + Downloads multiple parts of a file asynchronously and writes them to shared memory. + + Args: + part_size (int): The size of each part in bytes. + part_numbers (range): The range of part numbers to download. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + shm_name (str): The name of the shared memory block. + """ + session = aioboto3.Session() + config = AioConfig(retries={"max_attempts": 5, "mode": "adaptive"}, connect_timeout=10, read_timeout=30) + async with session.client("s3", config=config, **client_config) as s3: + tasks = [ + download_single_part_async( + s3, + bucket, + key, + part_number, + part_number * part_size, + (part_number + 1) * part_size - 1, + shm_name, + part_size, + ) + for part_number in part_numbers + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + failed_parts = [part for part, result in zip(part_numbers, results) if isinstance(result, Exception)] + + if failed_parts: + log.error(f"Failed to download parts: {failed_parts}", rank0_only=False) + raise Exception(f"Failed to download {len(failed_parts)} parts") + + +def download_parts_to_s3(args: tuple[range, int, str, str, dict[str, Any], str]) -> bytes: + """ + Downloads parts of a file using a new event loop. + + Args: + args (tuple[range, int, str, str, dict[str, Any]]): The arguments for downloading parts, including: + part_numbers (range): The range of part numbers to download. + part_size (int): The size of each part in bytes. + bucket (str): The S3 bucket name. + key (str): The S3 key (file path). + client_config (dict[str, Any]): The S3 client configuration. + + Returns: + bytes: The combined file data from all downloaded parts. + """ + part_numbers, part_size, bucket, key, client_config, shm_name = args + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(download_parts_async(part_size, part_numbers, bucket, key, client_config, shm_name)) + loop.close() + + +class Boto3Client: + """ + This class: + + - Provides higher-level S3 operations. + - Serves as a wrapper around boto3.client in order to make boto3.client serializable. + - It's required to use spawn method of creating DataLoader workers, + which is in turn required to avoid segfaults when using Triton, + e.g. for torch.compile or custom kernels. + """ + + def __init__( + self, + s3_credential_path: str, + max_attempt: int = 3, + ): + self.max_attempt: int = max_attempt + assert s3_credential_path, "s3_credential_path is required" + assert os.path.exists(s3_credential_path) or CRED_ENVS.APP_ENV in [ + "prod", + "dev", + "stg", + ], f"Credential file not found: {s3_credential_path}" + + # Keep track of S3 client constructor parameters so it can be recreated when pickling. + with auto.open_auth(s3_credential_path, "r") as f: + self._s3_cred_info = auto.json_load_auth(f) + self._s3_config = S3Config( + signature_version="s3v4", + s3={"addressing_style": "virtual"}, + response_checksum_validation="when_required", + request_checksum_calculation="when_required", + ) + self._init_client() + self._mc_kv_store = None + + def _init_client(self): + """Initialize the S3 client.""" + self._client = boto3.client("s3", **self._s3_cred_info, config=self._s3_config) + + def __getstate__(self): + state = self.__dict__.copy() + # S3 client isn't pickleable. + del state["_client"] + return state + + def __setstate__(self, state: dict[str, Any]): + self.__dict__.update(state) + self._init_client() + + def size(self, filepath: str) -> int: + filepath = self._check_path(filepath) + + if self._mc_kv_store and self._mc_kv_store.available: + if self._mc_kv_store.has(filepath): + return len(self._mc_kv_store.get(filepath)) + + attempt: int = 0 + while attempt < self.max_attempt: + try: + return self._client.head_object( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + )["ContentLength"] + except ClientError as e: + if e.response["Error"]["Code"] == "404": + raise # Object does not exist. + else: + attempt += 1 + log.error(f"Attempt {attempt} failed for {filepath}: {e}", rank0_only=False) + if attempt >= self.max_attempt: + raise # Re-raise the exception after max attempt + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + attempt += 1 + log.error(f"Attempt {attempt} failed for {filepath}: due to an unexpected error: {e}", rank0_only=False) + if attempt >= self.max_attempt: + raise # Re-raise the exception after max attempt + time.sleep(2) # Wait for 2 seconds before retrying + + raise ConnectionError("Unable to head {} from. {} attempts tried.".format(filepath, attempt)) + + def get(self, filepath: str, offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + raw_filepath = filepath + filepath = self._check_path(filepath) + + read_offset: Optional[int] = None + read_size: Optional[int] = None + byte_range: Optional[str] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=raw_filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + byte_range = f"bytes={read_offset}-{read_offset + read_size - 1}" + + if self._mc_kv_store and self._mc_kv_store.available: + if self._mc_kv_store.has(filepath): + chunk: bytes = self._mc_kv_store.get(filepath) + if read_offset is not None and read_size is not None: + return chunk[read_offset : read_offset + read_size] + else: + return chunk + + attempt = 0 + while attempt < self.max_attempt: + try: + buffer = io.BytesIO() + if byte_range is None: + self._client.download_fileobj( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + Fileobj=buffer, + ) + else: + # The boto S3 Transfer Manager doesn't support ranged reads yet. + # + # https://github.com/boto/boto3/issues/1215 + # https://github.com/boto/s3transfer/issues/248 + resp = self._client.get_object( + Bucket=filepath.split("/")[0], + Key="/".join(filepath.split("/")[1:]), + Range=byte_range, + ) + buffer.write(resp["Body"].read()) + buffer.seek(0) + # Only cache full reads. + if byte_range is None: + if self._mc_kv_store and self._mc_kv_store.available: + self._mc_kv_store.put(filepath, buffer.read()) + buffer.seek(0) + + return buffer.read() + except Exception as e: + attempt += 1 + log.error(f"Got an exception: attempt={attempt} - {e} - {filepath}", rank0_only=False) + + raise ConnectionError("Unable to read {} from. {} attempts tried.".format(filepath, attempt)) + + def put(self, obj, filepath): + filepath = self._check_path(filepath) + bucket_name = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + attempt = 0 + while attempt < self.max_attempt: + try: + # If obj is a string path to a local file, use upload_file instead + if isinstance(obj, str) and os.path.isfile(obj): + self._client.upload_file(Filename=obj, Bucket=bucket_name, Key=key) + return + if isinstance(obj, io.BytesIO): + obj.seek(0) + self._client.upload_fileobj(obj, Bucket=bucket_name, Key=key) + return + if isinstance(obj, bytes): + self._client.put_object(Body=obj, Bucket=bucket_name, Key=key) + return + else: + raise ValueError("Unsupported object type for upload") + except ClientError as e: + attempt += 1 + log.error(f"Got an exception: attempt={attempt} - {e} - {filepath}", rank0_only=False) + + raise ConnectionError("Unable to write {} to. {} attempts tried.".format(filepath, attempt)) + + def fast_put(self, obj, filepath, num_processes: int = 32): + assert aioboto3 is not None, "aioboto3 is required for fast_put" + original_filepath = filepath + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + part_size = 16 * 1024 * 1024 # 16 MB part size + + if isinstance(obj, bytes): + data = obj + elif isinstance(obj, str) and os.path.isfile(obj): + with open(obj, "rb") as f: + data = f.read() + elif isinstance(obj, io.BytesIO): + obj.seek(0) + data = obj.read() + else: + raise ValueError("Unsupported object type for upload") + + file_size = len(data) + if file_size <= part_size * num_processes: + return self.put(data, original_filepath) + num_parts = ceil(file_size / part_size) + upload_id = self._client.create_multipart_upload(Bucket=bucket, Key=key)["UploadId"] + + part_numbers = np.array_split(np.arange(num_parts), num_processes) + + with concurrent.futures.ProcessPoolExecutor(max_workers=num_processes) as executor: + args = [] + for i in range(num_processes): + cur_parts = part_numbers[i].tolist() + cur_data = data[cur_parts[0] * part_size : min(cur_parts[-1] * part_size + part_size, file_size)] + args.append((cur_parts, upload_id, part_size, cur_data, bucket, key, self._s3_cred_info)) + results = executor.map(upload_parts_to_s3, args) + parts = [] + for result in results: + parts.extend(result) + + parts = sorted(parts, key=lambda part: part["PartNumber"]) + self._client.complete_multipart_upload( + Bucket=bucket, Key=key, UploadId=upload_id, MultipartUpload={"Parts": parts} + ) + + def contains(self, filepath: str, max_retries=10) -> bool: + """ + Checks if the specified object exists in the S3 bucket with retry logic for errors. + + Args: + filepath (str): The s3 path of the file to check, must start with "s3://". + + Returns: + bool: True if the object exists in the S3 bucket, False otherwise. + + Raises: + ClientError: If an error response other than "404 Not Found" is returned from the S3 service. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + key = "/".join(filepath.split("/")[1:]) + + retries = 0 + while retries < max_retries: + try: + # Try to check if the object exists + self._client.head_object(Bucket=bucket, Key=key) + return True # Object exists + except ClientError as e: + if e.response["Error"]["Code"] == "404": + return False # Object does not exist + else: + retries += 1 + print(f"Attempt {retries} failed with error: {e}") + if retries >= max_retries: + raise # Re-raise the exception if max retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + retries += 1 + print(f"Attempt {retries} failed due to an unexpected error: {e}") + if retries >= max_retries: + raise # Re-raise the exception if max retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + + def isdir(self, filepath: str, max_retries=10) -> bool: + """ + Determines if the specified path corresponds to a directory in S3 with retry logic. + + A directory in S3 is implied if there are any objects stored with the given prefix, + which means this function checks for the existence of any objects at or under the specified path. + + Args: + filepath (str): The s3 path to check, must start with "s3://". + + Returns: + bool: True if the specified path corresponds to a directory in S3, False otherwise. + Directories in S3 are not physical entities but are implied by object keys. + + Raises: + ClientError: An error from the S3 API that isn't related to the absence of the directory + (logged but not raised further). + """ + filepath = self._check_path(filepath) + if not filepath.endswith("/"): + filepath += "/" + + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + + retries = 0 + while retries < max_retries: + try: + # Try to check if any objects exist with the given prefix (i.e., directory in S3) + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter="/", MaxKeys=1) + # Check if any content or prefixes exist under the given path + return "CommonPrefixes" in resp or "Contents" in resp + except ClientError as e: + retries += 1 + log.error(f"Attempt {retries} failed: {e}", rank0_only=False) + if retries >= max_retries: + return False # Return False if maximum retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + except Exception as e: + retries += 1 + log.error(f"Attempt {retries} failed due to an unexpected error: {e}", rank0_only=False) + if retries >= max_retries: + return False # Return False if maximum retries are reached + time.sleep(2) # Wait for 2 seconds before retrying + + def delete(self, filepath): + filepath = self._check_path(filepath) + self._client.delete_object(Bucket=filepath.split("/")[0], Key="/".join(filepath.split("/")[1:])) + + def ls_dir(self, filepath: str) -> Generator[str, None, None]: + """ + List all folders in an S3 bucket with a given prefix. + + Args: + filepath (str): The S3 path of the folder to list. + + Yields: + str: The keys of the folders in the S3 bucket. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + continuation_token = None + if prefix and not prefix.endswith("/"): + prefix += "/" + + while True: + if continuation_token: + resp = self._client.list_objects_v2( + Bucket=bucket, Prefix=prefix, Delimiter="/", ContinuationToken=continuation_token + ) + else: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter="/") + + if "CommonPrefixes" in resp: + for item in resp["CommonPrefixes"]: + yield item["Prefix"][len(prefix) :] + + # Check if there are more keys to retrieve + if resp.get("IsTruncated"): # If IsTruncated is True, there are more keys + continuation_token = resp.get("NextContinuationToken") + else: + break + + def list(self, filepath: str, exclude_prefix: Optional[str] = None) -> Generator[str, None, None]: + """ + List all keys in an S3 bucket with a given prefix, excluding files that start with + specified prefix. + + Args: + filepath (str): The S3 path of the file to list. + exclude_prefix (str): Files starting with this prefix will be excluded from results. + Defaults to "real". + + Yields: + str: The keys of the files in the S3 bucket that don't start with exclude_prefix. + """ + filepath = self._check_path(filepath) + bucket = filepath.split("/")[0] + prefix = "/".join(filepath.split("/")[1:]) + + continuation_token = None + + while True: + if continuation_token: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=continuation_token) + else: + resp = self._client.list_objects_v2(Bucket=bucket, Prefix=prefix) + + if "Contents" in resp: + for item in resp["Contents"]: + key = item["Key"][len(prefix) :] + # Skip files that start with the excluded prefix + if exclude_prefix is None or not key.startswith(exclude_prefix): + yield key + + # Check if there are more keys to retrieve + if resp.get("IsTruncated"): # If IsTruncated is True, there are more keys + continuation_token = resp.get("NextContinuationToken") + else: + break + + def _check_path(self, filepath: str): + assert filepath.startswith("s3://") + filepath = filepath[5:] + return filepath diff --git a/cosmos_training/cosmos/utils/easy_io/backends/http_backend.py b/cosmos_training/cosmos/utils/easy_io/backends/http_backend.py new file mode 100644 index 00000000..ce46c039 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/http_backend.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union +from urllib.request import Request, urlopen + +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend + + +class HTTPBackend(BaseStorageBackend): + """HTTP and HTTPS storage bachend.""" + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = HTTPBackend() + >>> filepath = 'http://path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + request = Request(url=str(filepath), method="HEAD") + with urlopen(request) as response: + if response.status == 200: + return int(response.headers["Content-Length"]) + else: + raise RuntimeError(f"Unexpected response: {response}") + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> backend = HTTPBackend() + >>> backend.get('http://path/of/file') + b'hello world' + """ + request = Request(url=str(filepath), method="GET") + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + request.add_header("Range", f"bytes={read_offset}-{read_offset + read_size - 1}") + with urlopen(request) as response: + if response.status in {200, 206}: + return response.read() + else: + raise RuntimeError(f"Unexpected response: {response}") + + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + """Read text from a given ``filepath``. + + Args: + filepath (str): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = HTTPBackend() + >>> backend.get_text('http://path/of/file') + 'hello world' + """ + return self.get(filepath=filepath).decode(encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + raise NotImplementedError(f"put not supported in {self.name}") + + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + raise NotImplementedError(f"put_text not supported in {self.name}") + + def exists(self, filepath: Union[str, Path]) -> bool: + request = Request(url=str(filepath), method="HEAD") + with urlopen(request) as response: + if response.status == 404: + return False + elif response.status == 200: + return True + else: + raise RuntimeError(f"Unexpected response: {response}") + + def isdir(self, filepath: Union[str, Path]) -> bool: + raise NotImplementedError(f"isdir not supported in {self.name}") + + def isfile(self, filepath: Union[str, Path]) -> bool: + raise NotImplementedError(f"isfile not supported in {self.name}") + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + raise NotImplementedError(f"join_path not supported in {self.name}") + + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = HTTPBackend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> with backend.get_local_path('http://path/of/file') as path: + ... # do something here + """ + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copyfile not supported in {self.name}") + + def copytree(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copytree not supported in {self.name}") + + def copyfile_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copyfile_from_local not supported in {self.name}") + + def copytree_from_local(self, src: Union[str, Path], dst: Union[str, Path]) -> str: + raise NotImplementedError(f"copytree_from_local not supported in {self.name}") + + def copyfile_to_local(self, src: Union[str, Path], dst: Union[str, Path], dst_type: str) -> Union[str, Path]: + raise NotImplementedError(f"copyfile_to_local not supported in {self.name}") + + def copytree_to_local(self, src: Union[str, Path], dst: Union[str, Path]) -> Union[str, Path]: + raise NotImplementedError(f"copytree_to_local not supported in {self.name}") + + def remove(self, filepath: Union[str, Path]) -> None: + raise NotImplementedError(f"remove not supported in {self.name}") + + def rmtree(self, dir_path: Union[str, Path]) -> None: + raise NotImplementedError(f"rmtree not supported in {self.name}") + + def copy_if_symlink_fails(self, src: Union[str, Path], dst: Union[str, Path]) -> bool: + raise NotImplementedError(f"copy_if_symlink_fails not supported in {self.name}") + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + raise NotImplementedError(f"list_dir not supported in {self.name}") + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + raise NotImplementedError(f"list_dir_or_file not supported in {self.name}") diff --git a/cosmos_training/cosmos/utils/easy_io/backends/local_backend.py b/cosmos_training/cosmos/utils/easy_io/backends/local_backend.py new file mode 100644 index 00000000..aec52eee --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/local_backend.py @@ -0,0 +1,599 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os +import os.path as osp +import shutil +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Union + +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend, mkdir_or_exist + + +class LocalBackend(BaseStorageBackend): + """Raw local storage backend.""" + + _allow_symlink = True + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.size(filepath) # file containing 'hello world' + 11 + """ + return osp.getsize(filepath) + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode. + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.get(filepath) + b'hello world' + """ + read_offset: Optional[int] = None + read_size: Optional[int] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + with open(filepath, "rb") as f: + if read_offset is not None: + f.seek(read_offset) + value = f.read(read_size) + return value + + def get_text(self, filepath: Union[str, Path], encoding: str = "utf-8") -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.get_text(filepath) + 'hello world' + """ + with open(filepath, encoding=encoding) as f: + text = f.read() + return text + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` will create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.put(b'hello world', filepath) + """ + mkdir_or_exist(osp.dirname(filepath)) + if isinstance(obj, io.BytesIO): + obj.seek(0) + obj = obj.getvalue() + with open(filepath, "wb") as f: + f.write(obj) + + def put_text(self, obj: str, filepath: Union[str, Path], encoding: str = "utf-8") -> None: + """Write text to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` will create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.put_text('hello world', filepath) + """ + mkdir_or_exist(osp.dirname(filepath)) + with open(filepath, "w", encoding=encoding) as f: + f.write(obj) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.exists(filepath) + True + """ + return osp.exists(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/dir' + >>> backend.isdir(filepath) + True + """ + return osp.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.isfile(filepath) + True + """ + return osp.isfile(filepath) + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result of concatenation. + + Examples: + >>> backend = LocalBackend() + >>> filepath1 = '/path/of/dir1' + >>> filepath2 = 'dir2' + >>> filepath3 = 'path/of/file' + >>> backend.join_path(filepath1, filepath2, filepath3) + '/path/of/dir/dir2/path/of/file' + """ + # TODO, if filepath or filepaths are Path, should return Path + return osp.join(filepath, *filepaths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Only for unified API and do nothing. + + Args: + filepath (str or Path): Path to be read data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Examples: + >>> backend = LocalBackend() + >>> with backend.get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + """ + yield filepath + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to '/path1/of/dir/file' + >>> backend.copyfile(src, dst) + '/path1/of/dir/file' + """ + return shutil.copy(src, dst) + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + src and dst should have the same prefix and dst must not already exist. + + TODO: Whether to support dirs_exist_ok parameter. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree(src, dst) + '/path/of/dir2' + """ + return shutil.copytree(src, dst) + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a local file src to dst and return the destination file. Same + as :meth:`copyfile`. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile_from_local(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to + >>> backend.copyfile_from_local(src, dst) + '/path1/of/dir/file' + """ + return self.copyfile(src, dst) + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. Same as + :meth:`copytree`. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree_from_local(src, dst) + '/path/of/dir2' + """ + return self.copytree(src, dst) + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: Optional[str] = None, + ) -> str: + """Copy the file src to local dst and return the destination file. Same + as :meth:`copyfile`. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = LocalBackend() + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> backend.copyfile_to_local(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to + >>> backend.copyfile_to_local(src, dst) + '/path1/of/dir/file' + """ + return self.copyfile(src, dst) + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> backend.copytree_from_local(src, dst) + '/path/of/dir2' + """ + return self.copytree(src, dst) + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + + Examples: + >>> backend = LocalBackend() + >>> filepath = '/path/of/file' + >>> backend.remove(filepath) + """ + if not self.exists(filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath): + raise IsADirectoryError("filepath should be a file") + + os.remove(filepath) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> dir_path = '/path/of/dir' + >>> backend.rmtree(dir_path) + """ + shutil.rmtree(dir_path) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + If failed to create a symbolic link pointing to src, directly copy src + to dst instead. + + Args: + src (str or Path): Create a symbolic link pointing to src. + dst (str or Path): Create a symbolic link named dst. + + Returns: + bool: Return True if successfully create a symbolic link pointing + to src. Otherwise, return False. + + Examples: + >>> backend = LocalBackend() + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> backend.copy_if_symlink_fails(src, dst) + True + >>> src = '/path/of/dir' + >>> dst = '/path1/of/dir1' + >>> backend.copy_if_symlink_fails(src, dst) + True + """ + try: + os.symlink(src, dst) + return True + except Exception: + if self.isfile(src): + self.copyfile(src, dst) + else: + self.copytree(src, dst) + return False + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + """List all folders in a storage location with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = LocalBackend() + >>> dir_path = 'path/of/dir' + >>> list(backend.list_dir(dir_path)) + ['subdir1/', 'subdir2/'] + """ + for entry in os.scandir(dir_path): + if entry.is_dir(): + yield f"{entry.name}/" + + def list_dir_or_file( + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str or Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix that we are + interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the directory. + Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = LocalBackend() + >>> dir_path = '/path/of/dir' + >>> # list those files and directories in current directory + >>> for file_path in backend.list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in backend.list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in backend.list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in backend.list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in backend.list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ # noqa: E501 + if list_dir and suffix is not None: + raise TypeError("`suffix` should be None when `list_dir` is True") + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + root = dir_path + + def _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive): + for entry in os.scandir(dir_path): + if not entry.name.startswith(".") and entry.is_file(): + rel_path = osp.relpath(entry.path, root) + if (suffix is None or rel_path.endswith(suffix)) and list_file: + yield rel_path + elif osp.isdir(entry.path): + if list_dir: + rel_dir = osp.relpath(entry.path, root) + yield rel_dir + if recursive: + yield from _list_dir_or_file(entry.path, list_dir, list_file, suffix, recursive) + + return _list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) diff --git a/cosmos_training/cosmos/utils/easy_io/backends/msc_backend.py b/cosmos_training/cosmos/utils/easy_io/backends/msc_backend.py new file mode 100644 index 00000000..f943b4f7 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/msc_backend.py @@ -0,0 +1,1075 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import io +import os +import re +import tempfile +from collections.abc import Generator, Iterator +from contextlib import contextmanager +from pathlib import Path +from shutil import SameFileError +from typing import Any, Optional, Union +from urllib.parse import urlparse + +import yaml +from multistorageclient import StorageClient, StorageClientConfig +from multistorageclient.types import Range + +import cosmos.utils.easy_io.backends.auto_auth as auto +from cosmos.utils import log +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend, mkdir_or_exist + +# {scheme}:// +_URL_PREFIX_REGEX = r"[a-zA-Z0-9+.-]*:\/\/" + + +def _get_telemetry_config_from_msc_secret() -> Optional[dict[str, Any]]: + """Generate MSC telemetry configuration from credentials/msc.secret file if available. + + Reads OpenTelemetry configuration from the ``credentials/msc.secret`` YAML file. + The file should contain an ``msc.opentelemetry`` section with the required fields + (vault_endpoint, vault_namespace, approle_id, approle_secret, mount_point, secret_path). + The endpoint field is optional and defaults to the production OTLP endpoint. + + Returns: + Optional[dict]: OpenTelemetry configuration dictionary if file exists and contains + required fields, None otherwise. + """ + msc_secret_path = Path("credentials/msc.secret") + + if not msc_secret_path.exists(): + log.debug(f"MSC secret file not found at {msc_secret_path}", rank0_only=True) + return None + + try: + with open(msc_secret_path, "r") as f: + msc_config = yaml.safe_load(f) + + if not msc_config or not isinstance(msc_config, dict): + log.warning(f"Invalid MSC secret file format at {msc_secret_path}", rank0_only=True) + return None + + msc_section = msc_config.get("msc", {}) + opentelemetry_section = msc_section.get("opentelemetry", {}) + + required_fields = ( + "vault_endpoint", + "vault_namespace", + "approle_id", + "approle_secret", + "mount_point", + "secret_path", + ) + missing = [f for f in required_fields if not opentelemetry_section.get(f)] + if missing: + log.warning( + f"MSC secret file at {msc_secret_path} missing required fields: {', '.join(missing)}", + rank0_only=True, + ) + return None + + vault_endpoint = opentelemetry_section["vault_endpoint"] + vault_namespace = opentelemetry_section["vault_namespace"] + approle_id = opentelemetry_section["approle_id"] + approle_secret = opentelemetry_section["approle_secret"] + mount_point = opentelemetry_section["mount_point"] + secret_path = opentelemetry_section["secret_path"] + cert_key = opentelemetry_section["cert_key"] + key_key = opentelemetry_section["key_key"] + ca_key = opentelemetry_section["ca_key"] + endpoint = opentelemetry_section["endpoint"] + except Exception as e: + log.warning(f"Failed to load MSC secret file at {msc_secret_path}: {e}", rank0_only=True) + return None + + # Construct OpenTelemetry configuration dictionary. + opentelemetry_config = { + "opentelemetry": { + "metrics": { + "attributes": [ + # All environments. + {"type": "static", "options": {"attributes": {"msc.ppp": "COSMOS", "msc.job": "unknown"}}}, + {"type": "host", "options": {"attributes": {"msc.cluster": "name", "msc.node": "name"}}}, + {"type": "process", "options": {"attributes": {"msc.process": "pid"}}}, + { + "type": "msc_config", + "options": { + "attributes": { + "msc.approle_id": { + "expression": "opentelemetry.metrics.exporter.options.auth.approle_id" + }, + "msc.approle_secret": { + "expression": ( + "hash('sha3-224', opentelemetry.metrics.exporter.options.auth.approle_secret)" + ) + }, + } + }, + }, + # Progressive enhancement for Lepton environments. + # + # https://docs.nvidia.com/dgx-cloud/lepton/features/batch-jobs/predefined-env-vars + { + "type": "environment_variables", + "options": { + "attributes": { + "msc.job": "LEPTON_JOB_NAME", + "msc.job_user": "LEPTON_USERID", + "msc.job_nodes": "LEPTON_JOB_TOTAL_WORKERS", + "msc.cluster": "LEPTON_WORKER_CLUSTER_NAME", + "msc.node": "LEPTON_WORKER_ID", + "msc.node_gpus": "LEPTON_RESOURCE_ACCELERATOR_NUM", + } + }, + }, + # Progressive enhancement for Slurm environments. + # + # https://slurm.schedmd.com/prolog_epilog.html#environment_variables + { + "type": "environment_variables", + "options": { + "attributes": { + "msc.ppp": "SLURM_JOB_ACCOUNT", + "msc.job": "SLURM_JOB_ID", + "msc.job_user": "SLURM_JOB_USER", + "msc.job_nodes": "SLURM_JOB_NUM_NODES", + "msc.job_gpus": "SLURM_GPUS", + "msc.cluster": "SLURM_CLUSTER_NAME", + "msc.node": "SLURMD_NODENAME", + "msc.node_gpus": "SLURM_GPUS_ON_NODE", + "msc.slurm_job_partition": "SLURM_JOB_PARTITION", + } + }, + }, + ], + "reader": { + "options": { + # ≤ 100 Hz collect frequency. + "collect_interval_millis": 10, + "collect_timeout_millis": 100, + # ≤ 1 Hz export frequency. + "export_interval_millis": 1000, + "export_timeout_millis": 500, + } + }, + "exporter": { + "type": "_otlp_mtls_vault", + "options": { + "exporter": {"endpoint": endpoint}, + "auth": { + "vault_endpoint": vault_endpoint, + "vault_namespace": vault_namespace, + "approle_id": approle_id, + "approle_secret": approle_secret, + "mount_point": mount_point, + "secret_path": secret_path, + "cert_key": cert_key, + "key_key": key_key, + "ca_key": ca_key, + }, + }, + }, + }, + } + } + + return opentelemetry_config + + +class MSCBackend(BaseStorageBackend): + """Multi-Storage Client (MSC) backend. + + Uses MSC storage clients instead of MSC shortcuts. + + URL file paths (e.g. 's3://path/of/file') are handled transparently. Using URL file paths + as input will return URL file path outputs when appropriate to match Boto3Backend behavior. + + **If using URL file paths, the storage provider's base path option must be empty!** + + Get/put concurrency can be set for certain providers in the MSC configuration file. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get(filepath) + """ + + _storage_client: StorageClient + _path_mapping: dict[str, str] + + def __init__( + self, + config_path: Optional[str] = "credentials/msc_config.yaml", + profile: Optional[str] = None, + s3_credential_path: Optional[str] = None, + path_mapping: Optional[dict[str, str]] = None, + ): + """Initialize a backend. + + Args: + config_path (str, optional): MSC config path (e.g. ``credentials/msc_config.yaml``). + profile (str, optional): MSC profile from the MSC config to use. + Mutually exclusive with ``s3_credential_path``. + s3_credential_path (str, optional): Legacy Boto3 config path (e.g. ``credentials/s3_training.secret``). + Translated into an MSC profile that's merged with the MSC config at ``config_path`` with: + + - The profile name set to ``s3_credential_path`` verbatim. + - The storage and credentials provider types determined by the file contents. + + Mutually exclusive with ``profile``. + path_mapping (dict, optional): Path mapping dict from src path to dst path. + When ``path_mapping={'src': 'dst'}``, ``src`` in ``filepath`` will be replaced by ``dst``. + Doesn't apply to the local path in ``copy{file,tree}_{from,to}_local`` methods. + """ + if all(_ is None for _ in (profile, s3_credential_path)) or all( + _ is not None for _ in (profile, s3_credential_path) + ): + raise ValueError("Must specify exactly one of profile or s3_credential_path") + + msc_config_dict: dict[str, Any] = {} + + # Use an existing MSC config file as the base MSC config. + if config_path is not None: + config_dict, _ = StorageClientConfig.read_msc_config(config_file_paths=[config_path]) + if config_dict is None: + log.info(f"No MSC config at {config_path}, using empty base MSC config", rank0_only=True) + else: + msc_config_dict = config_dict + + # Create an MSC profile from the legacy Boto3 config. + if s3_credential_path is not None: + with auto.open_auth(s3_credential_path, "r") as unloaded_legacy_boto3_config: + legacy_boto3_config = auto.json_load_auth(unloaded_legacy_boto3_config) + if len(legacy_boto3_config) > 0: + profile = s3_credential_path + + # Merge with any existing profiles. + msc_config_dict["profiles"] = msc_config_dict.get("profiles", {}) + # Merge with the existing profile, replacing `storage_provider` and `credentials_provider` completely. + msc_config_dict["profiles"][profile] = msc_config_dict["profiles"].get(profile, {}) + + storage_provider_type: str = "s3" + parsed_endpoint_url = urlparse(legacy_boto3_config["endpoint_url"]) + # Handle regional SwiftStack endpoints. + if parsed_endpoint_url.hostname.endswith(".s8k.io"): + storage_provider_type = "s8k" + # Handle global and regional GCS endpoints. + elif parsed_endpoint_url.hostname.startswith("storage.") and parsed_endpoint_url.hostname.endswith( + ".googleapis.com" + ): + storage_provider_type = "gcs_s3" + + msc_config_dict["profiles"][profile]["storage_provider"] = { + "type": storage_provider_type, + "options": { + "base_path": "", + "endpoint_url": legacy_boto3_config["endpoint_url"], + "region_name": legacy_boto3_config["region_name"], + }, + } + + if all(_ in legacy_boto3_config for _ in ("aws_access_key_id", "aws_secret_access_key")): + msc_config_dict["profiles"][profile]["credentials_provider"] = { + "type": "S3Credentials", + "options": { + "access_key": legacy_boto3_config["aws_access_key_id"], + "secret_key": legacy_boto3_config["aws_secret_access_key"], + }, + } + else: + raise ValueError("Cannot create profile from empty legacy Boto3 config") + + assert profile is not None, "Failed to resolve MSC profile" + + # Add OpenTelemetry configuration if credentials/msc.secret file is provided. + otel_config = _get_telemetry_config_from_msc_secret() + if otel_config: + msc_config_dict.update(otel_config) + log.debug("MSC Observability is configured from credentials/msc.secret", rank0_only=True) + else: + log.debug( + "MSC Observability is not configured (credentials/msc.secret not found or invalid)", rank0_only=True + ) + + # easy_io needs backend args to be JSON-serializable for backend instance cache keys. + # + # StorageClientConfig isn't, so we need to construct it here instead of receiving one. + self._storage_client = StorageClient( + config=StorageClientConfig.from_dict(config_dict=msc_config_dict, profile=profile) + ) + + assert isinstance(path_mapping, dict) or path_mapping is None + # Make a deep copy of the path mapping to prevent external mutation. + self._path_mapping = {} if path_mapping is None else copy.deepcopy(path_mapping) + for src, dst in self._path_mapping.items(): + log.info(f"Path mapping: {src} -> {dst}", rank0_only=False) + + def _translate_filepath(self, filepath: Union[str, Path], translate_url: bool = True) -> str: + """Translate a `filepath` to a string. + + Paths are of the form 'path/to/file' (path form) or '{protocol}://path/to/file' (URL form). + + Args: + filepath (str): File path to be translated. + translate_url (bool): Strip '{scheme}://' prefixes. Needed for paths passed directly to MSC storage clients. + """ + assert isinstance(filepath, (str, Path)) + + # Change to a POSIX path string. + if isinstance(filepath, str): + # If the ``filepath`` is concatenated by ``os.path.join`` in a Windows + # environment, the ``filepath`` will be the format of 'prefix\file.txt'. + filepath = re.sub(r"\\+", "/", filepath) + elif isinstance(filepath, Path): + # These should only be filesystem paths (e.g. '/path/of/file'). + # URL paths (e.g. ``Path('s3://profile/path/of/file')``) collapse '://' to ':/'. + filepath = filepath.as_posix() + else: + raise ValueError(f"Unhandled filepath type: {type(filepath)}") + + # Remap path. + # + # If there's multiple matching srcs, use the longest src (i.e. the most specific). + longest_src: str = "" + for src in self._path_mapping.keys(): + if filepath.startswith(src) and len(src) > len(longest_src): + longest_src = src + if len(longest_src) > 0: + filepath = filepath.replace(longest_src, self._path_mapping[longest_src], 1) + + # Optionally strip URL prefix then return. + # + # Don't use urlparse in case filepath is an invalid URL. + return re.sub(rf"^{_URL_PREFIX_REGEX}", "", filepath) if translate_url else filepath + + def size(self, filepath: Union[str, Path]) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.size(filepath) # file containing "hello world" + 11 + """ + path = self._translate_filepath(filepath=filepath) + return self._storage_client.info(path=path, strict=False).content_length + + def get(self, filepath: Union[str, Path], offset: Optional[int] = None, size: Optional[int] = None) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + + Returns: + bytes: Return bytes read from filepath. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get(filepath) + b'hello world' + """ + path = self._translate_filepath(filepath=filepath) + byte_range: Optional[Range] = None + if offset is not None or size is not None: + read_offset = offset or 0 + assert read_offset >= 0, "Read offset must be ≥ 0" + + # Try not to incur a remote call to get the file size. This can heavily slow down ranged reads. + # + # This means we won't always validate the read offset or read size against the file size. + read_size = size or (self.size(filepath=filepath) - read_offset) + assert read_size >= 1, "Read size must be ≥ 1 or read offset must be < file size" + + byte_range = Range(offset=read_offset, size=read_size) + + if byte_range is None: + buffer = io.BytesIO() + # `StorageClient.read()` defers to `StorageProvider.get_object()` while + # `StorageClient.download_file()` defers to `StorageProvider.download_file()`. + # + # Currently, only `StorageProvider.download_file()` supports parallel downloads + # in some storage providers (e.g. boto S3 transfer manager for S3 storage providers) + # so it's often much faster. + self._storage_client.download_file(remote_path=path, local_path=buffer) + buffer.seek(0) + return buffer.read() + else: + return self._storage_client.read(path=path, byte_range=byte_range) + + def get_text( + self, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.get_text(filepath) + 'hello world' + """ + return str(self.get(filepath=filepath), encoding=encoding) + + def put(self, obj: Union[bytes, io.BytesIO], filepath: Union[str, Path]) -> None: + """Write bytes to a given ``filepath``. + + Args: + obj (bytes): Data to be saved. + filepath (str or Path): Path to write data. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.put(b"hello world", filepath) + """ + path = self._translate_filepath(filepath=filepath) + buffer = io.BytesIO() + if isinstance(obj, bytes): + buffer.write(obj) + buffer.seek(0) + elif isinstance(obj, io.BytesIO): + buffer = obj + else: + raise ValueError(f"Unhandled obj type: {type(obj)}") + # `StorageClient.write()` defers to `StorageProvider.put_object()` while + # `StorageClient.upload_file()` defers to `StorageProvider.upload_file()`. + # + # Currently, only `StorageProvider.upload_file()` supports parallel uploads + # in some storage providers (e.g. boto S3 transfer manager for S3 storage providers) + # so it's often much faster. + self._storage_client.upload_file(remote_path=path, local_path=buffer) + + def put_text( + self, + obj: str, + filepath: Union[str, Path], + encoding: str = "utf-8", + ) -> None: + """Write text to a given ``filepath``. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str): The encoding format used to encode the ``obj``. + Defaults to 'utf-8'. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.put_text("hello world", filepath) + """ + self.put(obj=bytes(obj, encoding=encoding), filepath=filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.exists(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + # Include directories and files. + self._storage_client.info(path=path, strict=True) + return True + except FileNotFoundError: + return False + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/dir" # or "s3://path/of/file" + >>> backend.isdir(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + # Include directories and files. + metadata = self._storage_client.info(path=path, strict=True) + return metadata.type == "directory" + except FileNotFoundError: + return False + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.isfile(filepath) + True + """ + path = self._translate_filepath(filepath=filepath) + try: + return self._storage_client.is_file(path=path) + except FileNotFoundError: + return False + + def join_path( + self, + filepath: Union[str, Path], + *filepaths: Union[str, Path], + ) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result after concatenation. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.join_path(filepath, "another/path") + 'path/of/file/another/path' # or "s3://path/of/file/another/path" + >>> backend.join_path(filepath, "/another/path") + 'path/of/file/another/path' # or "s3://path/of/file/another/path" + """ + filepath = self._translate_filepath(filepath=filepath, translate_url=False) + if filepath.endswith("/") and not filepath.endswith("://"): + filepath = filepath[:-1] + formatted_paths = [filepath] + for path in filepaths: + formatted_path = self._translate_filepath(filepath=path) + formatted_paths.append(formatted_path.lstrip("/")) + + return "/".join(formatted_paths) + + @contextmanager + def get_local_path( + self, + filepath: Union[str, Path], + ) -> Generator[Union[str, Path], None, None]: + """Download a file from ``filepath`` to a local temporary directory, + and return the temporary path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Args: + filepath (str or Path): Download a file from ``filepath``. + + Yields: + Iterable[str]: Only yield one temporary path. + + Examples: + >>> backend = MSCBackend() + >>> # After existing from the ``with`` clause, + >>> # the path will be removed + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> with backend.get_local_path(filepath) as path: + ... # do something here + """ + assert self.isfile(filepath=filepath) + try: + f = tempfile.NamedTemporaryFile(delete=False) + f.write(self.get(filepath=filepath)) + f.close() + yield f.name + finally: + os.remove(f.name) + + def copyfile( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Copy a file src to dst and return the destination file. + + If dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError + will be raised. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/file1" # or "s3://path/of/file1" + >>> backend.copyfile(src, dst) + 'path/of/file1' # or "s3://path/of/file1" + + >>> # dst is a directory + >>> dst = "path/of/dir" # or "s3://path/of/dir" + >>> backend.copyfile(src, dst) + 'path/of/dir/file' # or "s3://path/of/dir/file" + """ + if not self.isfile(filepath=src): + raise FileNotFoundError("src does not exist or is not a file") + if self.isdir(filepath=dst): + dst = self.join_path(dst, self._translate_filepath(filepath=src).split("/")[-1]) + if self._translate_filepath(filepath=src) == self._translate_filepath(filepath=dst): + raise SameFileError("src and dst should not be same") + + self.put(obj=self.get(filepath=src), filepath=dst) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copytree( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/dir1" # or "s3://path/of/dir1" + >>> backend.copytree(src, dst) + 'path/of/dir1' # or "s3://path/of/dir1" + """ + if not self.isdir(filepath=src): + raise FileNotFoundError("src does not exist or is not a directory") + if self.exists(filepath=dst): + raise FileExistsError("dst should not exist") + + for path in self.list_dir_or_file(src, list_dir=False, recursive=True): + src_path = self.join_path(src, path) + dst_path = self.join_path(dst, path) + self.put(obj=self.get(filepath=src_path), filepath=dst_path) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copyfile_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Upload a local file src to dst and return the destination file. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/your/file" + >>> dst = "path/of/file1" # or "s3://path/of/file1" + >>> backend.copyfile_from_local(src, dst) + 'path/of/file1' # or "s3://path/of/file1" + + >>> # dst is a directory + >>> dst = "path/of/dir" + >>> backend.copyfile_from_local(src, dst) + 'path/of/dir/file' # or "s3://path/of/dir/file" + """ + if self.isdir(filepath=dst): + dst = self.join_path(dst, os.path.basename(src)) + + with open(src, "rb") as f: + self.put(obj=f.read(), filepath=dst) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copytree_from_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> str: + """Recursively copy an entire directory tree rooted at src to a + directory named dst and return the destination directory. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will + be raised. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/your/dir" + >>> dst = "path/of/dir1" # or "s3://path/of/dir1" + >>> backend.copytree_from_local(src, dst) + 'path/of/dir1' # or "s3://path/of/dir1" + """ + if self.exists(filepath=dst): + raise FileExistsError("dst should not exist") + + src = str(src) + + for cur_dir, _, files in os.walk(src): + for f in files: + src_path = os.path.join(cur_dir, f) + dst_path = self.join_path(dst, src_path.replace(src, "")) + self.copyfile_from_local(src=src_path, dst=dst_path) + + return self._translate_filepath(filepath=dst, translate_url=False) + + def copyfile_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + ) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> backend = MSCBackend() + >>> # dst is a file + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/your/file" + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/file' + + >>> # dst is a directory + >>> dst = "path/of/your/dir" + >>> backend.copyfile_to_local(src, dst) + 'path/of/your/dir/file' + """ + assert dst_type in ["file", "dir"] + # There is no good way to detect whether dst is a directory or a file, so we make dst_type required + if dst_type == "dir": + basename = os.path.basename(self._translate_filepath(filepath=src)) + if isinstance(dst, str): + dst = os.path.join(dst, basename) + else: + assert isinstance(dst, Path) + dst = dst / basename + + # Create parent directory if it doesn't exist + parent_dir = os.path.dirname(dst) + os.makedirs(parent_dir, exist_ok=True) + + try: + with open(dst, "wb") as f: + data = self.get(filepath=src) + f.write(data) + except Exception as e: + log.error(f"Failed to write file: {e}") + raise + + return dst + + def copytree_to_local( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + + Returns: + str: The destination directory. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/your/dir" + >>> backend.copytree_to_local(src, dst) + 'path/of/your/dir' + """ + for path in self.list_dir_or_file(dir_path=src, list_dir=False, recursive=True): + dst_path = os.path.join(dst, path) + mkdir_or_exist(os.path.dirname(dst_path)) + with open(dst_path, "wb") as f: + f.write(self.get(filepath=self.join_path(src, path))) + + return dst + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str or Path): Path to be removed. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> backend = MSCBackend() + >>> filepath = "path/of/file" # or "s3://path/of/file" + >>> backend.remove(filepath) + """ + if not self.exists(filepath=filepath): + raise FileNotFoundError(f"filepath {filepath} does not exist") + + if self.isdir(filepath=filepath): + raise IsADirectoryError("filepath should be a file") + + self._storage_client.delete(path=self._translate_filepath(filepath=filepath), recursive=False) + + def rmtree(self, dir_path: Union[str, Path]) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> backend.rmtree(dir_path) + """ + self._storage_client.delete(path=self._translate_filepath(filepath=dir_path), recursive=True) + + def copy_if_symlink_fails( + self, + src: Union[str, Path], + dst: Union[str, Path], + ) -> bool: + """Create a symbolic link pointing to src named dst. + + Directly copy src to dst because MSCBackend does not support creating + a symbolic link. + + Args: + src (str or Path): A file or directory to be copied. + dst (str or Path): Copy a file or directory to dst. + + Returns: + bool: Return False because MSCBackend does not support create + a symbolic link. + + Examples: + >>> backend = MSCBackend() + >>> src = "path/of/file" # or "s3://path/of/file" + >>> dst = "path/of/your/file" # or "s3://path/of/your/file" + >>> backend.copy_if_symlink_fails(src, dst) + False + >>> src = "path/of/dir" # or "s3://path/of/dir" + >>> dst = "path/of/your/dir" # or "s3://path/of/your/dir" + >>> backend.copy_if_symlink_fails(src, dst) + False + """ + if self.isfile(filepath=src): + self.copyfile(src=src, dst=dst) + else: + self.copytree(src=src, dst=dst) + return False + + def list_dir(self, dir_path: Union[str, Path]) -> Generator[str, None, None]: + """List all folders in a storage location with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> list(backend.list_dir(dir_path)) + ["subdir1/", "subdir2/"] + """ + path = self._translate_filepath(filepath=dir_path).removesuffix("/") + "/" + for metadata in self._storage_client.list(path=path, include_directories=True, include_url_prefix=False): + if metadata.type == "directory": + yield metadata.key.removeprefix(path).removesuffix("/") + "/" + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + Most object stores have no concept of directories but it simulates + the directory hierarchy in the filesystem through public prefixes. + In addition, if the returned path ends with '/', it means the path + is a public prefix which is a logical directory. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + In addition, the returned path of directory will not contains the + suffix '/' which is consistent with other backends. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> backend = MSCBackend() + >>> dir_path = "path/of/dir" # or "s3://path/of/dir" + >>> # list those files and directories in current directory + >>> list(backend.list_dir_or_file(dir_path)) + ["file.txt", "subdir", "subdir/cat.png", "subdir/subsubdir/dog.jpg"] + >>> # only list files + >>> list(backend.list_dir_or_file(dir_path, list_dir=False)) + ["file.txt", "subdir/cat.png", "subdir/subsubdir/dog.jpg"] + >>> # only list directories + >>> list(backend.list_dir_or_file(dir_path, list_file=False)) + ["subdir"] + >>> # only list files ending with specified suffixes + >>> list(backend.list_dir_or_file(dir_path, suffix=".txt")) + ["file.txt"] + >>> # list all files and directory recursively + >>> list(backend.list_dir_or_file(dir_path, recursive=True)) + ["file.txt", "subdir", "subdir/cat.png", "subdir/subsubdir", "subdir/subsubdir/dog.png"] + """ + dir_path = self._translate_filepath(filepath=dir_path).removesuffix("/") + "/" + + if list_dir and suffix is not None: + raise TypeError("`list_dir` should be False when `suffix` is not None") + + if list_dir and not list_file and not recursive: + raise TypeError( + "Please use `list_dir` instead of `list_dir_or_file` " + "when you only want to list the first level directories." + ) + + if (suffix is not None) and not isinstance(suffix, (str, tuple)): + raise TypeError("`suffix` must be a string or tuple of strings") + + yielded_subdir_paths: set[str] = set() + # In the MSC, the `include_directories` option switches between flat and hierarchical for both files and "directories". + # + # In the Boto3Backend, however, the `recursive` option only applies to "directories" (seems like a bug). + # + # Construct directories from file paths to match the Boto3Backend behavior. + # + # If this behavior needs to be fixed, switch to `include_directories=(not recursive)` and adjust metadata processing. + for metadata in self._storage_client.list(path=dir_path, include_directories=False, include_url_prefix=False): + # Only files should be returned with `include_directories=False`, but just in case. + if metadata.type == "file": + rel_path: str = metadata.key.removeprefix(dir_path) + if list_dir: + rel_path_fragments = rel_path.split("/") + if len(rel_path_fragments) > 1: + for i in range(len(rel_path_fragments) - 1 if recursive else 1): + subdir_path = "/".join(rel_path_fragments[: i + 1]) + if subdir_path not in yielded_subdir_paths: + yielded_subdir_paths.add(subdir_path) + yield subdir_path + if list_file: + if suffix is None or rel_path.endswith(suffix): + yield rel_path + + def generate_presigned_url(self, url: str, client_method: str = "get_object", expires_in: int = 3600) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on Boto3 backend. + + Note: + Now only work on Boto3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Default: 'get_object'. + expires_in (int): expires, in seconds. Default: 3600. + + Returns: + str: Generated presigned url. + """ + raise NotImplementedError("generate_presigned_url is not supported in MSCBackend") diff --git a/cosmos_training/cosmos/utils/easy_io/backends/registry_utils.py b/cosmos_training/cosmos/utils/easy_io/backends/registry_utils.py new file mode 100644 index 00000000..35dc340a --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/backends/registry_utils.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Optional, Type, Union + +from cosmos.utils.flags import TRAINING +from cosmos.utils.easy_io.backends.base_backend import BaseStorageBackend +from cosmos.utils.easy_io.backends.http_backend import HTTPBackend +from cosmos.utils.easy_io.backends.local_backend import LocalBackend + +backends: dict = {} +prefix_to_backends: dict = {} + + +def _register_backend( + name: str, + backend: Type[BaseStorageBackend], + force: bool = False, + prefixes: Union[str, list, tuple, None] = None, +): + """Register a backend. + + Args: + name (str): The name of the registered backend. + backend (BaseStorageBackend): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + force (bool): Whether to override the backend if the name has already + been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefix + of the registered storage backend. Defaults to None. + """ + global backends, prefix_to_backends + + if not isinstance(name, str): + raise TypeError(f"the backend name should be a string, but got {type(name)}") + + if not inspect.isclass(backend): + raise TypeError(f"backend should be a class, but got {type(backend)}") + if not issubclass(backend, BaseStorageBackend): + raise TypeError(f"backend {backend} is not a subclass of BaseStorageBackend") + + if name in backends and not force: + raise ValueError( + f'{name} is already registered as a storage backend, add "force=True" if you want to override it' + ) + backends[name] = backend + + if prefixes is not None: + if isinstance(prefixes, str): + prefixes = [prefixes] + else: + assert isinstance(prefixes, (list, tuple)) + + for prefix in prefixes: + if prefix in prefix_to_backends and not force: + raise ValueError( + f'{prefix} is already registered as a storage backend, add "force=True" if you want to override it' + ) + + prefix_to_backends[prefix] = backend + + +def register_backend( + name: str, + backend: Optional[Type[BaseStorageBackend]] = None, + force: bool = False, + prefixes: Union[str, list, tuple, None] = None, +): + """Register a backend. + + Args: + name (str): The name of the registered backend. + backend (class, optional): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + When this method is used as a decorator, backend is None. + Defaults to None. + force (bool): Whether to override the backend if the name has already + been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefix + of the registered storage backend. Defaults to None. + + This method can be used as a normal method or a decorator. + + Examples: + + >>> class NewBackend(BaseStorageBackend): + ... def get(self, filepath): + ... return filepath + ... + ... def get_text(self, filepath): + ... return filepath + >>> register_backend('new', NewBackend) + + >>> @register_backend('new') + ... class NewBackend(BaseStorageBackend): + ... def get(self, filepath): + ... return filepath + ... + ... def get_text(self, filepath): + ... return filepath + """ + if backend is not None: + _register_backend(name, backend, force=force, prefixes=prefixes) + return + + def _register(backend_cls): + _register_backend(name, backend_cls, force=force, prefixes=prefixes) + return backend_cls + + return _register + + +register_backend("local", LocalBackend, prefixes="") +register_backend("http", HTTPBackend, prefixes=["http", "https"]) + +if TRAINING: + from cosmos.utils.easy_io.backends.msc_backend import MSCBackend + + # To avoid breaking backward Compatibility, 's3' is also used as a + # prefix for MSCBackend + register_backend("s3", MSCBackend, prefixes=["s3"]) diff --git a/cosmos_training/cosmos/utils/easy_io/easy_io.py b/cosmos_training/cosmos/utils/easy_io/easy_io.py new file mode 100644 index 00000000..b68c3c0d --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/easy_io.py @@ -0,0 +1,1117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import warnings +from contextlib import contextmanager +from io import BytesIO, StringIO +from pathlib import Path +from typing import IO, Any, Generator, Iterator, Optional, Tuple, Union + +from cosmos.utils.easy_io.backends import backends, prefix_to_backends +from cosmos.utils.easy_io.file_client import FileClient +from cosmos.utils.easy_io.handlers import file_handlers + +backend_instances: dict = {} + + +def is_filepath(filepath): + return isinstance(filepath, (str, Path)) + + +def _parse_uri_prefix(uri: Union[str, Path]) -> str: + """Parse the prefix of uri. + + Args: + uri (str or Path): Uri to be parsed that contains the file prefix. + + Examples: + >>> _parse_uri_prefix('/home/path/of/your/file') + '' + >>> _parse_uri_prefix('s3://path/of/your/file') + 's3' + >>> _parse_uri_prefix('clusterName:s3://path/of/your/file') + 's3' + + Returns: + str: Return the prefix of uri if the uri contains '://'. Otherwise, + return ''. + """ + assert is_filepath(uri) + uri = str(uri) + # if uri does not contains '://', the uri will be handled by + # LocalBackend by default + if "://" not in uri: + return "" + else: + prefix, _ = uri.split("://") + # In the case of Boto3Backend, the prefix may contain the cluster + # name like clusterName:s3://path/of/your/file + if ":" in prefix: + _, prefix = prefix.split(":") + return prefix + + +def _get_file_backend(prefix: str, backend_args: dict): + """Return a file backend based on the prefix or backend_args. + + Args: + prefix (str): Prefix of uri. + backend_args (dict): Arguments to instantiate the corresponding + backend. + """ + # backend name has a higher priority + if "backend" in backend_args: + # backend_args should not be modified + backend_args_bak = backend_args.copy() + backend_name = backend_args_bak.pop("backend") + backend = backends[backend_name](**backend_args_bak) + else: + backend = prefix_to_backends[prefix](**backend_args) + return backend + + +def set_s3_backend( + key: str = "s3:{}", + backend_args: Optional[dict] = None, +): + """register s3 backend. + + Args: + key str: The key to register the s3 backend. Defaults to s3. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + """ + global backend_instances + if backend_args is None: + backend_args = {} + backend = _get_file_backend(key, backend_args) + backend_instances[key] = backend + return backend + + +def get_file_backend( + uri: Union[str, Path, None] = None, + *, + backend_args: Optional[dict] = None, + enable_singleton: bool = False, + backend_key: Optional[str] = None, +): + """Return a file backend based on the prefix of uri or backend_args. + + Args: + uri (str or Path): Uri to be parsed that contains the file prefix. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + enable_singleton (bool): Whether to enable the singleton pattern. + If it is True, the backend created will be reused if the + signature is same with the previous one. Defaults to False. + backend_key: str: The key to register the backend. Defaults to None. + + Returns: + BaseStorageBackend: Instantiated Backend object. + + Examples: + >>> # get file backend based on the prefix of uri + >>> uri = 's3://path/of/your/file' + >>> backend = get_file_backend(uri) + >>> # get file backend based on the backend_args + >>> backend = get_file_backend(backend_args={'backend': 's3'}) + >>> # backend name has a higher priority if 'backend' in backend_args + >>> backend = get_file_backend(uri, backend_args={'backend': 's3'}) + """ + global backend_instances + if backend_key is not None: + if backend_key in backend_instances: + return backend_instances[backend_key] + + if backend_args is None: + backend_args = {} + + if uri is None and "backend" not in backend_args and backend_key is None: + raise ValueError('uri should not be None when "backend" does not exist in backend_args and backend_key is None') + + if uri is not None: + prefix = _parse_uri_prefix(uri) + else: + prefix = "" + + if enable_singleton: + + unique_key = f"{prefix}:{json.dumps(backend_args)}" + if unique_key in backend_instances: + return backend_instances[unique_key] + + backend = _get_file_backend(prefix, backend_args) + backend_instances[unique_key] = backend + if backend_key is not None: + backend_instances[backend_key] = backend + return backend + else: + backend = _get_file_backend(prefix, backend_args) + return backend + + +def size( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> int: + """Get the file size in bytes for a given ``filepath``. + + Args: + filepath (str or Path): Path to get file size in bytes. + + Returns: + int: File size in bytes for filepath. + + Examples: + >>> filepath = 'path/of/file' + >>> size(filepath) # file containing 'hello world' + 11 + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.size(filepath) + + +def get( + filepath: Union[str, Path], + offset: Optional[int] = None, + size: Optional[int] = None, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bytes: + """Read bytes from a given ``filepath`` with 'rb' mode in range [offset, offset + size). + + Args: + filepath (str or Path): Path to read data. + offset (int, optional): Read offset in bytes (0-index). Defaults to 0. + size (int, optional): Read size in bytes. Defaults to the file size. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bytes: Expected bytes object. + + Examples: + >>> filepath = '/path/of/file' + >>> get(filepath) + b'hello world' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.get(filepath, offset=offset, size=size) + + +def get_text( + filepath: Union[str, Path], + encoding="utf-8", + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """Read text from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: Expected text reading from ``filepath``. + + Examples: + >>> filepath = '/path/of/file' + >>> get_text(filepath) + 'hello world' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.get_text(filepath, encoding) + + +def put( + obj: bytes, + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Write bytes to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Examples: + >>> filepath = '/path/of/file' + >>> put(b'hello world', filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.put(obj, filepath) + + +def put_text( + obj: str, + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Write text to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str, optional): The encoding format used to open the + ``filepath``. Defaults to 'utf-8'. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Examples: + >>> filepath = '/path/of/file' + >>> put_text('hello world', filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.put_text(obj, filepath) + + +def exists( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + + Examples: + >>> filepath = '/path/of/file' + >>> exists(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.exists(filepath) + + +def isdir( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + + Examples: + >>> filepath = '/path/of/dir' + >>> isdir(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.isdir(filepath) + + +def isfile( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + + Examples: + >>> filepath = '/path/of/file' + >>> isfile(filepath) + True + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.isfile(filepath) + + +def join_path( + filepath: Union[str, Path], + *filepaths: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + *filepaths (str or Path): Other paths to be concatenated. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: The result of concatenation. + + Examples: + >>> filepath1 = '/path/of/dir1' + >>> filepath2 = 'dir2' + >>> filepath3 = 'path/of/file' + >>> join_path(filepath1, filepath2, filepath3) + '/path/of/dir/dir2/path/of/file' + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + return backend.join_path(filepath, *filepaths) + + +@contextmanager +def get_local_path( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Generator[Union[str, Path], None, None]: + """Download data from ``filepath`` and write the data to local path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Note: + If the ``filepath`` is a local path, just return itself and it will + not be released (removed). + + Args: + filepath (str or Path): Path to be read data. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Yields: + Iterable[str]: Only yield one path. + + Examples: + >>> with get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + with backend.get_local_path(str(filepath)) as local_path: + yield local_path + + +def copyfile( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy a file src to dst and return the destination file. + + src and dst should have the same prefix. If dst specifies a directory, + the file will be copied into dst using the base filename from src. If + dst specifies a file that already exists, it will be replaced. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination file. + + Raises: + SameFileError: If src and dst are the same file, a SameFileError will + be raised. + + Examples: + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> # src will be copied to '/path1/of/file1' + >>> copyfile(src, dst) + '/path1/of/file1' + + >>> # dst is a directory + >>> dst = '/path1/of/dir' + >>> # src will be copied to '/path1/of/dir/file' + >>> copyfile(src, dst) + '/path1/of/dir/file' + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile(src, dst) + + +def copytree( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a directory + named dst and return the destination directory. + + src and dst should have the same prefix and dst must not already exist. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + backend_key (str, optional): The key to get the backend from register. + + Returns: + str: The destination directory. + + Raises: + FileExistsError: If dst had already existed, a FileExistsError will be + raised. + + Examples: + >>> src = '/path/of/dir1' + >>> dst = '/path/of/dir2' + >>> copytree(src, dst) + '/path/of/dir2' + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree(src, dst) + + +def copyfile_from_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy a local file src to dst and return the destination file. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copyfile`. + + Args: + src (str or Path): A local file to be copied. + dst (str or Path): Copy file to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> # dst is a file + >>> src = '/path/of/file' + >>> dst = 's3://openmmlab/mmengine/file1' + >>> # src will be copied to 's3://openmmlab/mmengine/file1' + >>> copyfile_from_local(src, dst) + s3://openmmlab/mmengine/file1 + + >>> # dst is a directory + >>> dst = 's3://openmmlab/mmengine' + >>> # src will be copied to 's3://openmmlab/mmengine/file'' + >>> copyfile_from_local(src, dst) + 's3://openmmlab/mmengine/file' + """ + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile_from_local(src, dst) + + +def copytree_from_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a directory + named dst and return the destination directory. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copytree`. + + Args: + src (str or Path): A local directory to be copied. + dst (str or Path): Copy directory to dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> src = '/path/of/dir' + >>> dst = 's3://openmmlab/mmengine/dir' + >>> copyfile_from_local(src, dst) + 's3://openmmlab/mmengine/dir' + """ + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree_from_local(src, dst) + + +def copyfile_to_local( + src: Union[str, Path], + dst: Union[str, Path], + dst_type: str, # Choose from ["file", "dir"] + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Copy the file src to local dst and return the destination file. + + If dst specifies a directory, the file will be copied into dst using + the base filename from src. If dst specifies a file that already + exists, it will be replaced. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copyfile`. + + Args: + src (str or Path): A file to be copied. + dst (str or Path): Copy file to to local dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: If dst specifies a directory, the file will be copied into dst + using the base filename from src. + + Examples: + >>> # dst is a file + >>> src = 's3://openmmlab/mmengine/file' + >>> dst = '/path/of/file' + >>> # src will be copied to '/path/of/file' + >>> copyfile_to_local(src, dst) + '/path/of/file' + + >>> # dst is a directory + >>> dst = '/path/of/dir' + >>> # src will be copied to '/path/of/dir/file' + >>> copyfile_to_local(src, dst) + '/path/of/dir/file' + """ + assert dst_type in ["file", "dir"] + Path(dst).parent.mkdir(parents=True, exist_ok=True) + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copyfile_to_local(src, dst, dst_type=dst_type) + + +def copytree_to_local( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Union[str, Path]: + """Recursively copy an entire directory tree rooted at src to a local + directory named dst and return the destination directory. + + Note: + If the backend is the instance of LocalBackend, it does the same + thing with :func:`copytree`. + + Args: + src (str or Path): A directory to be copied. + dst (str or Path): Copy directory to local dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: The destination directory. + + Examples: + >>> src = 's3://openmmlab/mmengine/dir' + >>> dst = '/path/of/dir' + >>> copytree_to_local(src, dst) + '/path/of/dir' + """ + Path(dst).parent.mkdir(parents=True, exist_ok=True) + backend = get_file_backend(dst, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copytree_to_local(src, dst) + + +def remove( + filepath: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Remove a file. + + Args: + filepath (str, Path): Path to be removed. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Raises: + FileNotFoundError: If filepath does not exist, an FileNotFoundError + will be raised. + IsADirectoryError: If filepath is a directory, an IsADirectoryError + will be raised. + + Examples: + >>> filepath = '/path/of/file' + >>> remove(filepath) + """ + backend = get_file_backend( + filepath, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.remove(filepath) + + +def rmtree( + dir_path: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> None: + """Recursively delete a directory tree. + + Args: + dir_path (str or Path): A directory to be removed. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Examples: + >>> dir_path = '/path/of/dir' + >>> rmtree(dir_path) + """ + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + backend.rmtree(dir_path) + + +def copy_if_symlink_fails( + src: Union[str, Path], + dst: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> bool: + """Create a symbolic link pointing to src named dst. + + If failed to create a symbolic link pointing to src, directory copy src to + dst instead. + + Args: + src (str or Path): Create a symbolic link pointing to src. + dst (str or Path): Create a symbolic link named dst. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + bool: Return True if successfully create a symbolic link pointing to + src. Otherwise, return False. + + Examples: + >>> src = '/path/of/file' + >>> dst = '/path1/of/file1' + >>> copy_if_symlink_fails(src, dst) + True + >>> src = '/path/of/dir' + >>> dst = '/path1/of/dir1' + >>> copy_if_symlink_fails(src, dst) + True + """ + backend = get_file_backend(src, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.copy_if_symlink_fails(src, dst) + + +def list_dir( + dir_path: Union[str, Path], + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +): + """List all folders in an S3 bucket with a given prefix. + + Args: + dir_path (str | Path): Path of the directory. + + Examples: + >>> dir_path = '/path/of/dir' + >>> for file_path in list_dir(dir_path): + ... print(file_path) + """ + if not dir_path.endswith("/"): + dir_path += "/" + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + + return backend.list_dir(dir_path) + + +def list_dir_or_file( + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, Tuple[str]]] = None, + recursive: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str or Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix that we are + interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the directory. + Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + + Examples: + >>> dir_path = '/path/of/dir' + >>> for file_path in list_dir_or_file(dir_path): + ... print(file_path) + >>> # list those files and directories in current directory + >>> for file_path in list_dir_or_file(dir_path): + ... print(file_path) + >>> # only list files + >>> for file_path in list_dir_or_file(dir_path, list_dir=False): + ... print(file_path) + >>> # only list directories + >>> for file_path in list_dir_or_file(dir_path, list_file=False): + ... print(file_path) + >>> # only list files ending with specified suffixes + >>> for file_path in list_dir_or_file(dir_path, suffix='.txt'): + ... print(file_path) + >>> # list all files and directory recursively + >>> for file_path in list_dir_or_file(dir_path, recursive=True): + ... print(file_path) + """ + backend = get_file_backend( + dir_path, + backend_args=backend_args, + enable_singleton=True, + backend_key=backend_key, + ) + yield from backend.list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) + + +def generate_presigned_url( + url: str, + client_method: str = "get_object", + expires_in: int = 3600, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """Generate the presigned url of video stream which can be passed to + mmcv.VideoReader. Now only work on s3 backend. + + Note: + Now only work on s3 backend. + + Args: + url (str): Url of video stream. + client_method (str): Method of client, 'get_object' or + 'put_object'. Defaults to 'get_object'. + expires_in (int): expires, in seconds. Defaults to 3600. + backend_args (dict, optional): Arguments to instantiate the + corresponding backend. Defaults to None. + + Returns: + str: Generated presigned url. + """ + backend = get_file_backend(url, backend_args=backend_args, enable_singleton=True, backend_key=backend_key) + return backend.generate_presigned_url(url, client_method, expires_in) + + +def load( + file: Union[str, Path, IO[Any]], + file_format: Optional[str] = None, + file_client_args: Optional[dict] = None, + fast_backend: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, + **kwargs, +): + """Load data from json/yaml/pickle files. + + This method provides a unified api for loading data from serialized files. + + ``load`` supports loading data from serialized files those can be storaged + in different backends. + + Args: + file (str or :obj:`Path` or file-like object): Filename or a file-like + object. + file_format (str, optional): If not specified, the file format will be + inferred from the file extension, otherwise use the specified one. + Currently supported formats include "json", "yaml/yml" and + "pickle/pkl". + file_client_args (dict, optional): Arguments to instantiate a + FileClient. See :class:`mmengine.fileio.FileClient` for details. + Defaults to None. It will be deprecated in future. Please use + ``backend_args`` instead. + fast_backend: bool: Whether to use multiprocess. Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + New in v0.2.0. + + Examples: + >>> load('/path/of/your/file') # file is storaged in disk + >>> load('https://path/of/your/file') # file is storaged in Internet + >>> load('s3://path/of/your/file') # file is storaged in s3 + + Returns: + The content from the file. + """ + if isinstance(file, Path): + file = str(file) + if file_format is None and isinstance(file, str): + file_format = file.split(".")[-1] + # convert file_format to lower case + file_format = file_format.lower() + if file_format not in file_handlers: + raise TypeError(f"Unsupported format: {file_format}") + + if file_client_args is not None: + warnings.warn( + '"file_client_args" will be deprecated in future. Please use "backend_args" instead', + DeprecationWarning, + ) + if backend_args is not None: + raise ValueError('"file_client_args and "backend_args" cannot be set at the same time.') + + handler = file_handlers[file_format] + if isinstance(file, str): + if file_client_args is not None: + file_client = FileClient.infer_client(file_client_args, file) + file_backend = file_client + else: + file_backend = get_file_backend( + file, + backend_args=backend_args, + backend_key=backend_key, + enable_singleton=True, + ) + + if handler.str_like: + with StringIO(file_backend.get_text(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + if fast_backend: + if hasattr(file_backend, "fast_get"): + with BytesIO(file_backend.fast_get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + warnings.warn( + f"fast_backend is not supported by the backend, type {type(file_backend)} fallback to normal get" + ) + with BytesIO(file_backend.get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + else: + with BytesIO(file_backend.get(file)) as f: + obj = handler.load_from_fileobj(f, **kwargs) + elif hasattr(file, "read"): + obj = handler.load_from_fileobj(file, **kwargs) + else: + raise TypeError('"file" must be a filepath str or a file-object') + return obj + + +def dump( + obj: Any, + file: Union[str, Path, IO[Any], None] = None, + file_format: Optional[str] = None, + file_client_args: Optional[dict] = None, + fast_backend: bool = False, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, + **kwargs, +): + """Dump data to json/yaml/pickle strings or files. + + This method provides a unified api for dumping data as strings or to files, + and also supports custom arguments for each file format. + + ``dump`` supports dumping data as strings or to files which is saved to + different backends. + + Args: + obj (any): The python object to be dumped. + file (str or :obj:`Path` or file-like object, optional): If not + specified, then the object is dumped to a str, otherwise to a file + specified by the filename or file-like object. + file_format (str, optional): Same as :func:`load`. + file_client_args (dict, optional): Arguments to instantiate a + FileClient. See :class:`mmengine.fileio.FileClient` for details. + Defaults to None. It will be deprecated in future. Please use + ``backend_args`` instead. + fast_backend: bool: Whether to use multiprocess. Defaults to False. + backend_args (dict, optional): Arguments to instantiate the + prefix of uri corresponding backend. Defaults to None. + New in v0.2.0. + backend_key: str: The key to register the backend. Defaults to None. + + Examples: + >>> dump('hello world', '/path/of/your/file') # disk + >>> dump('hello world', 's3://path/of/your/file') # ceph or s3 + + Returns: + bool: True for success, False otherwise. + """ + if isinstance(file, Path): + file = str(file) + if file_format is None: + if isinstance(file, str): + file_format = file.split(".")[-1] + elif file is None: + raise ValueError("file_format must be specified since file is None") + # convert file_format to lower case + file_format = file_format.lower() + if file_format not in file_handlers: + raise TypeError(f"Unsupported format: {file_format}") + + if file_client_args is not None: + warnings.warn( + '"file_client_args" will be deprecated in future. Please use "backend_args" instead', + DeprecationWarning, + ) + if backend_args is not None: + raise ValueError('"file_client_args" and "backend_args" cannot be set at the same time.') + + handler = file_handlers[file_format] + if file is None: + return handler.dump_to_str(obj, **kwargs) + elif isinstance(file, str): + if file_client_args is not None: + file_client = FileClient.infer_client(file_client_args, file) + file_backend = file_client + else: + file_backend = get_file_backend( + file, + backend_args=backend_args, + backend_key=backend_key, + enable_singleton=True, + ) + + if handler.str_like: + with StringIO() as f: + handler.dump_to_fileobj(obj, f, **kwargs) + file_backend.put_text(f.getvalue(), file) + else: + with BytesIO() as f: + handler.dump_to_fileobj(obj, f, **kwargs) + if fast_backend: + if hasattr(file_backend, "fast_put"): + file_backend.fast_put(f, file) + else: + warnings.warn("fast_backend is not supported by the backend, fallback to normal put") + file_backend.put(f, file) + else: + file_backend.put(f, file) + elif hasattr(file, "write"): + handler.dump_to_fileobj(obj, file, **kwargs) + else: + raise TypeError('"file" must be a filename str or a file-object') diff --git a/cosmos_training/cosmos/utils/easy_io/easy_io_test.py b/cosmos_training/cosmos/utils/easy_io/easy_io_test.py new file mode 100644 index 00000000..8eca3114 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/easy_io_test.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from cosmos.utils.easy_io import easy_io +from cosmos.utils.helper_test import RunIf + + +def setup_s3(): + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": None, + "s3_credential_path": "credentials/pbss_getty.secret", + } + ) + + +@pytest.mark.L1("Requires data loading from S3.") +@RunIf(requires_file="credentials/pbss_getty.secret") +def test_s3_backend(): + setup_s3() + for ith, _ in enumerate(easy_io.list_dir_or_file("s3://checkpoints/")): + if ith > 5: + break + + easy_io.copyfile_from_local("pyproject.toml", "s3://checkpoints/pyproject.toml") + easy_io.remove("s3://checkpoints/pyproject.toml") + + +@pytest.mark.L1("Requires data uploading to S3.") +@RunIf(requires_file="credentials/pbss_getty.secret") +def test_s3_dump(): + if easy_io.exists("s3://debug/test_00.mp4"): + easy_io.remove("s3://debug/test_00.mp4") + np_frames, metadata = easy_io.load("s3://debug/00.mp4") + easy_io.dump( + np_frames, + "s3://debug/test_00.mp4", + format="mp4", + fps=metadata.get("fps", 30), + codec=metadata.get("codec", "h264"), + ) + if easy_io.exists("s3://debug/dummy_dict.pkl"): + easy_io.remove("s3://debug/dummy_dict.pkl") + + dummy_dict = {"a": 1, "b": 2} + easy_io.dump(dummy_dict, "s3://debug/dummy_dict.pkl") + dummy_np = np.array([1, 2, 3]) + easy_io.dump(dummy_np, "s3://debug/dummy_np.npy") + + +@pytest.mark.L0 +def test_local_backend(): + num_files = len(list(easy_io.list_dir_or_file("."))) + assert num_files > 0 + + if easy_io.exists("dummy_dict.pkl"): + easy_io.remove("dummy_dict.pkl") + + dummy_dict = {"a": 1, "b": 2} + easy_io.dump(dummy_dict, "dummy_dict.pkl") + load_dict = easy_io.load("dummy_dict.pkl") + for key in dummy_dict: + assert dummy_dict[key] == load_dict[key] + + if easy_io.exists("dummy_dict.pkl"): + easy_io.remove("dummy_dict.pkl") diff --git a/cosmos_training/cosmos/utils/easy_io/file_client.py b/cosmos_training/cosmos/utils/easy_io/file_client.py new file mode 100644 index 00000000..10047b78 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/file_client.py @@ -0,0 +1,459 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Generator, Iterator, Optional, Tuple, Union + +from cosmos.utils.flags import TRAINING +from cosmos.utils.easy_io.backends import BaseStorageBackend, HTTPBackend, LocalBackend + + +def is_filepath(filepath): + return isinstance(filepath, (str, Path)) + + +class HardDiskBackend(LocalBackend): + """Raw hard disks storage backend.""" + + @property + def name(self): + return self.__class__.__name__ + + +class FileClient: + """A general file client to access files in different backends. + + The client loads a file or text in a specified backend from its path + and returns it as a binary or text file. There are two ways to choose a + backend, the name of backend and the prefix of path. Although both of them + can be used to choose a storage backend, ``backend`` has a higher priority + that is if they are all set, the storage backend will be chosen by the + backend argument. If they are all `None`, the disk backend will be chosen. + Note that It can also register other backend accessor with a given name, + prefixes, and backend class. In addition, We use the singleton pattern to + avoid repeated object creation. If the arguments are the same, the same + object will be returned. + + Warning: + `FileClient` will be deprecated in future. Please use io functions + in https://mmengine.readthedocs.io/en/latest/api/fileio.html#file-io + + Args: + backend (str, optional): The storage backend type. Options are "disk", + "memcached", "lmdb", "http" and "s3". Defaults to None. + prefix (str, optional): The prefix of the registered storage backend. + Options are "s3", "http", "https". Defaults to None. + + Examples: + >>> # only set backend + >>> file_client = FileClient(backend='s3') + >>> # only set prefix + >>> file_client = FileClient(prefix='s3') + >>> # set both backend and prefix but use backend to choose client + >>> file_client = FileClient(backend='s3', prefix='s3') + >>> # if the arguments are the same, the same object is returned + >>> file_client1 = FileClient(backend='s3') + >>> file_client1 is file_client + True + + Attributes: + client (:obj:`BaseStorageBackend`): The backend object. + """ + + _backends = { + "disk": HardDiskBackend, + "http": HTTPBackend, + } + + _prefix_to_backends: dict = { + "http": HTTPBackend, + "https": HTTPBackend, + } + + if TRAINING: + from cosmos.utils.easy_io.backends.msc_backend import MSCBackend + + _backends["s3"] = MSCBackend + _backends["msc"] = MSCBackend + _prefix_to_backends["s3"] = MSCBackend + + _instances: dict = {} + + client: Any + + def __new__(cls, backend=None, prefix=None, **kwargs): + if backend is None and prefix is None: + backend = "disk" + if backend is not None and backend not in cls._backends: + raise ValueError( + f"Backend {backend} is not supported. Currently supported ones are {list(cls._backends.keys())}" + ) + if prefix is not None and prefix not in cls._prefix_to_backends: + raise ValueError( + f"prefix {prefix} is not supported. Currently supported ones are {list(cls._prefix_to_backends.keys())}" + ) + + # concatenate the arguments to a unique key for determining whether + # objects with the same arguments were created + arg_key = f"{backend}:{prefix}" + for key, value in kwargs.items(): + arg_key += f":{key}:{value}" + + # if a backend was overridden, it will create a new object + if arg_key in cls._instances: + _instance = cls._instances[arg_key] + else: + # create a new object and put it to _instance + _instance = super().__new__(cls) + if backend is not None: + _instance.client = cls._backends[backend](**kwargs) + else: + _instance.client = cls._prefix_to_backends[prefix](**kwargs) + + cls._instances[arg_key] = _instance + + return _instance + + @property + def name(self): + return self.client.name + + @property + def allow_symlink(self): + return self.client.allow_symlink + + @staticmethod + def parse_uri_prefix(uri: Union[str, Path]) -> Optional[str]: + """Parse the prefix of a uri. + + Args: + uri (str | Path): Uri to be parsed that contains the file prefix. + + Examples: + >>> FileClient.parse_uri_prefix('s3://path/of/your/file') + 's3' + + Returns: + str | None: Return the prefix of uri if the uri contains '://' else + ``None``. + """ + assert is_filepath(uri) + uri = str(uri) + if "://" not in uri: + return None + else: + prefix, _ = uri.split("://") + # In the case of MSCBackend, the prefix may contains the cluster + # name like clusterName:s3 + if ":" in prefix: + _, prefix = prefix.split(":") + return prefix + + @classmethod + def infer_client( + cls, + file_client_args: Optional[dict] = None, + uri: Optional[Union[str, Path]] = None, + ) -> "FileClient": + """Infer a suitable file client based on the URI and arguments. + + Args: + file_client_args (dict, optional): Arguments to instantiate a + FileClient. Defaults to None. + uri (str | Path, optional): Uri to be parsed that contains the file + prefix. Defaults to None. + + Examples: + >>> uri = 's3://path/of/your/file' + >>> file_client = FileClient.infer_client(uri=uri) + >>> file_client_args = {'backend': 's3'} + >>> file_client = FileClient.infer_client(file_client_args) + + Returns: + FileClient: Instantiated FileClient object. + """ + assert file_client_args is not None or uri is not None + if file_client_args is None: + file_prefix = cls.parse_uri_prefix(uri) # type: ignore + return cls(prefix=file_prefix) + else: + return cls(**file_client_args) + + @classmethod + def _register_backend(cls, name, backend, force=False, prefixes=None): + if not isinstance(name, str): + raise TypeError(f"the backend name should be a string, but got {type(name)}") + if not inspect.isclass(backend): + raise TypeError(f"backend should be a class but got {type(backend)}") + if not issubclass(backend, BaseStorageBackend): + raise TypeError(f"backend {backend} is not a subclass of BaseStorageBackend") + if not force and name in cls._backends: + raise KeyError( + f'{name} is already registered as a storage backend, add "force=True" if you want to override it' + ) + + if name in cls._backends and force: + for arg_key, instance in list(cls._instances.items()): + if isinstance(instance.client, cls._backends[name]): + cls._instances.pop(arg_key) + cls._backends[name] = backend + + if prefixes is not None: + if isinstance(prefixes, str): + prefixes = [prefixes] + else: + assert isinstance(prefixes, (list, tuple)) + for prefix in prefixes: + if prefix not in cls._prefix_to_backends: + cls._prefix_to_backends[prefix] = backend + elif (prefix in cls._prefix_to_backends) and force: + overridden_backend = cls._prefix_to_backends[prefix] + for arg_key, instance in list(cls._instances.items()): + if isinstance(instance.client, overridden_backend): + cls._instances.pop(arg_key) + else: + raise KeyError( + f"{prefix} is already registered as a storage backend," + ' add "force=True" if you want to override it' + ) + + @classmethod + def register_backend(cls, name, backend=None, force=False, prefixes=None): + """Register a backend to FileClient. + + This method can be used as a normal class method or a decorator. + + .. code-block:: python + + class NewBackend(BaseStorageBackend): + + def get(self, filepath): + return filepath + + def get_text(self, filepath): + return filepath + + FileClient.register_backend('new', NewBackend) + + or + + .. code-block:: python + + @FileClient.register_backend('new') + class NewBackend(BaseStorageBackend): + + def get(self, filepath): + return filepath + + def get_text(self, filepath): + return filepath + + Args: + name (str): The name of the registered backend. + backend (class, optional): The backend class to be registered, + which must be a subclass of :class:`BaseStorageBackend`. + When this method is used as a decorator, backend is None. + Defaults to None. + force (bool, optional): Whether to override the backend if the name + has already been registered. Defaults to False. + prefixes (str or list[str] or tuple[str], optional): The prefixes + of the registered storage backend. Defaults to None. + `New in version 1.3.15.` + """ + if backend is not None: + cls._register_backend(name, backend, force=force, prefixes=prefixes) + return + + def _register(backend_cls): + cls._register_backend(name, backend_cls, force=force, prefixes=prefixes) + return backend_cls + + return _register + + def get(self, filepath: Union[str, Path]) -> Union[bytes, memoryview]: + """Read data from a given ``filepath`` with 'rb' mode. + + Note: + There are two types of return values for ``get``, one is ``bytes`` + and the other is ``memoryview``. The advantage of using memoryview + is that you can avoid copying, and if you want to convert it to + ``bytes``, you can use ``.tobytes()``. + + Args: + filepath (str or Path): Path to read data. + + Returns: + bytes | memoryview: Expected bytes object or a memory view of the + bytes object. + """ + return self.client.get(filepath) + + def get_text(self, filepath: Union[str, Path], encoding="utf-8") -> str: + """Read data from a given ``filepath`` with 'r' mode. + + Args: + filepath (str or Path): Path to read data. + encoding (str): The encoding format used to open the ``filepath``. + Defaults to 'utf-8'. + + Returns: + str: Expected text reading from ``filepath``. + """ + return self.client.get_text(filepath, encoding) + + def put(self, obj: bytes, filepath: Union[str, Path]) -> None: + """Write data to a given ``filepath`` with 'wb' mode. + + Note: + ``put`` should create a directory if the directory of ``filepath`` + does not exist. + + Args: + obj (bytes): Data to be written. + filepath (str or Path): Path to write data. + """ + self.client.put(obj, filepath) + + def put_text(self, obj: str, filepath: Union[str, Path]) -> None: + """Write data to a given ``filepath`` with 'w' mode. + + Note: + ``put_text`` should create a directory if the directory of + ``filepath`` does not exist. + + Args: + obj (str): Data to be written. + filepath (str or Path): Path to write data. + encoding (str, optional): The encoding format used to open the + `filepath`. Defaults to 'utf-8'. + """ + self.client.put_text(obj, filepath) + + def remove(self, filepath: Union[str, Path]) -> None: + """Remove a file. + + Args: + filepath (str, Path): Path to be removed. + """ + self.client.remove(filepath) + + def exists(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path exists. + + Args: + filepath (str or Path): Path to be checked whether exists. + + Returns: + bool: Return ``True`` if ``filepath`` exists, ``False`` otherwise. + """ + return self.client.exists(filepath) + + def isdir(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a directory. + + Args: + filepath (str or Path): Path to be checked whether it is a + directory. + + Returns: + bool: Return ``True`` if ``filepath`` points to a directory, + ``False`` otherwise. + """ + return self.client.isdir(filepath) + + def isfile(self, filepath: Union[str, Path]) -> bool: + """Check whether a file path is a file. + + Args: + filepath (str or Path): Path to be checked whether it is a file. + + Returns: + bool: Return ``True`` if ``filepath`` points to a file, ``False`` + otherwise. + """ + return self.client.isfile(filepath) + + def join_path(self, filepath: Union[str, Path], *filepaths: Union[str, Path]) -> str: + r"""Concatenate all file paths. + + Join one or more filepath components intelligently. The return value + is the concatenation of filepath and any members of \*filepaths. + + Args: + filepath (str or Path): Path to be concatenated. + + Returns: + str: The result of concatenation. + """ + return self.client.join_path(filepath, *filepaths) + + @contextmanager + def get_local_path(self, filepath: Union[str, Path]) -> Generator[Union[str, Path], None, None]: + """Download data from ``filepath`` and write the data to local path. + + ``get_local_path`` is decorated by :meth:`contxtlib.contextmanager`. It + can be called with ``with`` statement, and when exists from the + ``with`` statement, the temporary path will be released. + + Note: + If the ``filepath`` is a local path, just return itself. + + .. warning:: + ``get_local_path`` is an experimental interface that may change in + the future. + + Args: + filepath (str or Path): Path to be read data. + + Examples: + >>> file_client = FileClient(prefix='s3') + >>> with file_client.get_local_path('s3://bucket/abc.jpg') as path: + ... # do something here + + Yields: + Iterable[str]: Only yield one path. + """ + with self.client.get_local_path(str(filepath)) as local_path: + yield local_path + + def list_dir_or_file( # pylint: disable=too-many-arguments + self, + dir_path: Union[str, Path], + list_dir: bool = True, + list_file: bool = True, + suffix: Optional[Union[str, Tuple[str]]] = None, + recursive: bool = False, + ) -> Iterator[str]: + """Scan a directory to find the interested directories or files in + arbitrary order. + + Note: + :meth:`list_dir_or_file` returns the path relative to ``dir_path``. + + Args: + dir_path (str | Path): Path of the directory. + list_dir (bool): List the directories. Defaults to True. + list_file (bool): List the path of files. Defaults to True. + suffix (str or tuple[str], optional): File suffix + that we are interested in. Defaults to None. + recursive (bool): If set to True, recursively scan the + directory. Defaults to False. + + Yields: + Iterable[str]: A relative path to ``dir_path``. + """ + yield from self.client.list_dir_or_file(dir_path, list_dir, list_file, suffix, recursive) diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__init__.py b/cosmos_training/cosmos/utils/easy_io/handlers/__init__.py new file mode 100644 index 00000000..350bfe1a --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/__init__.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler +from cosmos.utils.easy_io.handlers.json_handler import JsonHandler +from cosmos.utils.easy_io.handlers.pickle_handler import PickleHandler +from cosmos.utils.easy_io.handlers.registry_utils import file_handlers, register_handler +from cosmos.utils.easy_io.handlers.yaml_handler import YamlHandler + +__all__ = [ + "BaseFileHandler", + "JsonHandler", + "PickleHandler", + "YamlHandler", + "register_handler", + "file_handlers", +] diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66361fd8582ad81e160c559fcab7058182a785b2 GIT binary patch literal 734 zcmah_Jx{|h5Ve!02`v?f9SN}&q4*CVkQfj`2r)tyD>QCNvE#^ggfj9g7}(hOE&PK> zh=GX>@v$&*cAK(+C7<5eckkZ2$HAb7;F!-~de=ed#U}0X=HTNPz%9y9hB*>A!PsJt zd%{nAhkf1=K@vFJ;aw3Xp@@>mB(7`UFE|p1?G?nY=~?K zZRkQc87?=QBdLYdab7fwNWE5i+Jw*PJ@owr4cmG@uE z%&w}b6YDHEGyiF;VI_+}b_GpHY05dJ59k9@^nFWsrHx`_tglHal^IK(OHF1LNU7wM zjZB+UyA^6wniqL7Ar-7hlR7OX_L_MUl_e{*tkj6LFN=;jt+PSqJ16bV38lbmD2>bd uJK7O4t2o8Y52n3h-vfAv96Q^shJF~_( zGExpXZ~;}~5RN%;ND2Q5r@*0T#i0@>Zf+elQck?LAHg;%bS(eo&CHv5@BQ9;_Aiyn z9N>r#^z~%};7?i8VUmiK7l~lfWO`w~fDO zVJk`L!>xE1o-UMC2-_`)wN=I_D@6vkR=eE<*G z(gU#7M3X#hCR!6OCdS>qCsw7TAZxYS(f3Z+!+5yUlzpcgiMSbstw=h4B-=Q3MYPWS zxLCU@=zxpiw>quPs?(EPNN3=6R_Pn(m)xk!J2L7CpBFpFH}YqDaVwM#_vFBBMb26l zjdbYsE8RhIDvZ3qZHlPvlGD-3`o`i+=vV%w@(P7cncjp)rZq+Z{5+K;r(=p9Vd9=B zBiNdt;z?ArBuGbC2$Fb2j%uPphYLZk-F4&W*rnHJU7AriPY*2+oJ9EVR89DzroM<5 z=wj+do~q1?6NM2>+QzwOd4Pv+Ok1C10^1`iln{k&h#5}l7_yZ*#zJl$9pW+^a&*4pY8zk#vYb^uA&j7wsm!Z$nD6yF|st_@cFWGv= zyh(ZPdhuGKwWENifhVjfDW8;rJO}gtWSx{Z&d(AseSQIN%Xbkr;g8Cx`xk$>^;>0e z1gv~x=d<6d_Wf%QtM-1?-d)_UUf=x^;ZVi1LsED`a3rZjzO7zTf70tEpX#9)#~gNW zXNp5*s1{rxhx#))WJA5FAd4EY$Wjg`SseTyi{d4;EUE~$F;Oxv)9zUUDjq>yA<0=H zv7pf^iYO)V)maiJPcPB^_Yh_&tRsb1?VDQ%Ur8iNrdQSbt2A zlX&JWbx*%mTC!Qf1VJBv&`j6ucx~>w$(-wA;(H;n<0>e_AzZ z)Zz>+&k?*vK#iPsTG}XrVore=l6MfcU^E9OPjB9Sq*os+Os|YpFiww18W|`($KKdb z^cx4JqSrG^WCUX=<9~tYhn|#6ww9>giZ7_&GKW(~rky)7-_L^mKwLmQ>Oc7+!boL| U9oB$dJTT6(`cCO7;FOyB8^8UJNdN!< literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/byte_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/byte_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6aa5c8e40a3bb3cbedbfaf908de696e97e837655 GIT binary patch literal 1278 zcmZuvy>HV%6uSM1AFuo!~h-PJ)DXrs*TSQFBUhFpQZ1eCB{o+ zNL3~Z7*eEgO7|m$QOh=DVYy9~fl#FFA6RUcbCIZAF5i4K-3|{JHJkjvZ2K%|`mW=1 zv*B|OjyCi6Xd{@|qz>I90kfQz)7mvVoHA~PR%=)6AbCXmHf?dg!y0trnZ8KUb^^!c zCbf7-9N&BukAj_m)QWst5`IBrf z+h2R1zbww5guU?Hn{)mC2!be*Ayp&Y%jqm0Kp18r-8PG()GR`DimSjU^?*46H54W$ z$?gat`pohmIB;1sxy%ZnL`A#fwMpPl|FV+)%Rj+Y@xBP6i-xl(TR6$}a{WgiicI+a zqLHA-m`1ap3{U_vUT-lgfd(MwtA4QKwO#7bRzU4%jQPxvQ;^4K6H3HjXC`=n9EVG{ z0fm5Q7S};^(f8T4bA4@$u4-f>osgF0Q9_~|A)aq{T%qR)dEK$xB%(tG;yU}3iOP{G zrq7x91zQke3B)NHryOHqn(zv-2%A3!af~jC3*GIZBC9K(>R%Q=J0k?@Fqcu6FAJri z3g=Km#ku}s&lzRF8kMlR9_JL9s)ibALD&X1$0*ighE~Q5tt1SsOc+`b{AXgUF&HtW pKLBs^C-GM|SQ!FhFk>(Z-cl!s~tW8o@Ds`WyK0GvWXM literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/csv_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/csv_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63edd8d11f1ff89ddfd01f9b82ccb71c439cc03b GIT binary patch literal 2102 zcmcJQ-D@0G6u|GDkIiJ0bj>Cv?KWvA?Sl^DuAtaZLmq4z6(M~H_^=?uWacKFb#`aH zcQ$durY$88iUli*2|oK^2!els|9~&4Nog44lL-1I-3po~&zXc2ev82hQeE|kr zgpo3JNLg~|CMnB~1X6KikZMyg)G&KZtfpS19yfT10eISTk(XOikoqN+UcJPmMR$|^ z4LL|JPg-)ix=42Df+2^pEmp$}XO_cwsCcdj3?)rN-dnTyir^Spp@7h# z9e-K!8Y1AVv@F(2b)N^zzUTU)Wc#8DU7hhjEhp!s;%G? zBu1uRXEotBxXsd)jz!|O5xAZxF-tT}*Du|SV+}76t7bAO)|=t5=Ua}s%>Al~H}ID# z>l59e7XPt3jyIbGafciZ=8uL)w+f#Yw&(VTr#{Ra?-er~hhW?C%yP=<)b<%f6|@i}fl1 zsV_0z(j$shj&TYzYupV0ZQ1g?F#C?>HQ05|eIEA58*pMn<%6gy11Wq6M*Iv4Btl^5 zfcNA=VeZ7y*9%X-)&$zW2I5q#@^FoBuN~y44|3B-`H8*BSNHQ*_6DyUYG)p4XCG*1 zw?`golYAKL?M_}`(t++fm;iJ=WgtZeKKKr%71Te`x_YZ*5Iew~01;DvqVXE;{S{(2fhg(1JR2lKL-oz#wl5YYIX{U&!Lk7it{Kir$!&g@J4t=kZ`Q) z9FgTZh)`Q$!7755fW@b93z-Ttej}(if>YTVNZEputdE_3!pq=9+yIdFi#)|bH&^pj24|C6ca{Hs(`#F72(<9Cnj7+GRX3eTH(+snwS@oTUhvPxhe7|9N z@eY3muX70nMrVga#?c4QGq{Q(iaWx)NghkGI`KHGsu$X4`qZgd8+D9K{E14gA4N30 z6qos8ROX9GnJ=bgzKA~&jy%V&c>zcGin!KjGx2d}c@fsI{0Yp~?=q#dtrI%%6B+-7 S%s$DC(y99w{~*vs)_()Wgw;;~ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/gzip_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/gzip_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0cbf58e9216a30746d36a53df96e7fe94e28153 GIT binary patch literal 1466 zcmah}&u`pB6nEwHI7<-~iow}gP`c;|cX&3N9-`{wih z*lZpGZ27L$`;G(rO2M>QwKG{lX9Hr0SqedAS->zhQloDMCZjPk<$Wu#N}s28-w7NB z23&>MT8G#c&bC?Q3mOIUoZl%}@|bo$EOMbf`WRhvVNjrHev-6TQgJOB#Ho<&YE^}J z#IjA!qS*iebd11=SzyM7;2k42*Lh&YT-asIadRFxh3z@|@YUPNuui(Dc_&Fl+omS@=xXGWZPM zg7-kNvBBU5FnkKQW!``>^UNh}$mNHIcIdd-a!>Pq77LGS-og(F|qQ6YYE& zhs%)?VJ@R&kPN#1NC~O@A{un5=k=m68;XI-MzSrYBQI9eX-9dIDqlpZ2$RhJyt-iJ zQz>B`wT6X0LN$aPnf1fE?AvebyS&!FrcYy8>V1Se@U(g4-p#u=w?23fKWU!Z0X8%L zX!Yr_*EcSGeQ9&`n=8*v9C=Q|e-iAe8CV+l0O@UtD`8bXBw!~Filp3q??f^2}sG3?)@Q5 z5TPtZI^+?&hF!gbuns@X9lvvJha1-XR(Aqub{dA|ZlN~NJm~I{k#9~HdNUQBC6h*y zm0M~%FNUc6a#ww}Qgf?RbE{T!YpUi}^&w96EQ##uTrPR#MnwBw<8oE*75O%X^zWeN i5q3<**u(`k`!h6ufs^?A4HkFp8Rp)9_ZgrmbNmI>XItz5 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/imageio_video_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/imageio_video_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db0a31da0e3bfe7b481dece210737fbd135805e6 GIT binary patch literal 6055 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 C{euPo literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/np_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/np_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fd87f7d4e986957ab98a48f3bbb23cf4998c6d8 GIT binary patch literal 3822 zcmd5<-D@0G6u);q_A8rgO>ONLb*fF8Roul&Q9`MvMB9p_YNgN>87Dh;vy;xwtoP1# zyCF&+3O0x~BGUB96a}C>E8?=IV$({Rg z?z!il^Si%u^L=k`hCuOdsgu9;5ONbcouWpKrI(>`iI~KsHmT7PrP!8isV0}?no?4N zzHF&m8Uh<`5$9;Lk zX^p{f?yw=)OP0-!8jfi*UJloWb=RnGq5nRZ#Yl;oq$HWNB%2abn7SQiOqnGrim9Ae zN=Z{?sjz>6RH&IapDm@$B+E>0jVlh@AeVx5DOi`ir%ZPjkW7PQ?>!}9XOPK&Oy31^ zmX>lSNWRC0bX@y#3 zTKb%RL}43hJOknxEh?|LhN&4E?EGp=GdMR|T7|nc7{cA3vaQohbH^uH*(+qai(WOj zQDYwCVz@ho`f!OhbnN(>;~2oV^26F2RrVJ}TG@3x!*VQVLi1ogzPD+3Mq!oCC!RRn zH28!V)<#Uz@+{XeZ0$5_HC+yd^NB`{IiAq!h7iouJXd?$sTXWG)!Q+HUk`k^###rx zU2x3c)S;=k4z$E9S}_LE=+u1$W%9DG2xeD!8l3DWLJI|g8N6`!a?x%GkF#P$G>dhY zdllEVTv05$q6VKjchR4nzCI4H);%72q}UMP)?&+WCh*UT7U^!CIl^u5GV6?VS43(X zo@I+7Gek?b++sDnBckZk^{8E_xBQ3DdL1LIW3V8yQ@i1`UDO4hTQ$EE4kflhb&lNH zN;dUhY|b=iy~W(%+!?5@rv{f`j`%55S2w)s`am55nNMjPsx$I5`A{JQ;b~G+vS=5g zveCbBh(hGOYAn=$hfNgKh9EQ%GJ`%q22mJb7ux8cm7pW4|2GKqm6~fZU*h9{MO8HF zjQf2n0makg72XG~=9{s~VPMj_Ubf+K=(+%JBReL6qX9|HLL(metI)@CfV?YvcxQq# ziPo(t(=wn#>W~*D)AD3;jRWNSD`)jSwREL>mG=iby7cU5s9ku;9Xn zdp`i!3F8dAFY&>7Lb~hjyo&NPLv#S zXr%);Er6TfV*<_So~y&|Q`=U-E;e%j6o`XR1(568^1-P#k$`9Fr8=D`#H2m@KCkK~R5LrgKKd`zhkd;5!cToOL9Z{2Iw7OHenR7pk#6h`k z=9D7%$(TD4JOCRl4x3Rp6ZmRPK>tzh+z?dIQ!62XyS&kq3ovU|t#UP>x-iXKLPK@z zL9kr}J2VK(SY8TX!gqm}S78Gq698llI&vKhbh6T}brBNHj)E5P2vmTQevzG zHg6O;8%=5d0y1keVUYcT!Df|l7K4eXx((aZ#+ime{tv+7OU>~yd{E&NCk7ie%wz$j zbkiB%jiJX|8~`n(mTU(+cz~(x>mmoIj^oeKwijpf<7H;sx}K*T|C&e1fE z73OvE9#nVElN-5*&mCE=yG#BJ0{Iqwc)q(l_UkqOvZWCx;dwoIu=F}24+gmg0;Joxj=1Qhbpn0vl zjGy2R1_tFbzGS&U^6(R0s}8v|@I~MRUv+?lTH)az3!k!u;3->(p0b6`Q?`H)WsWyF zQ2P}Lx4_5D-PFO*{fGE7&_%Zh^j%w~l-|||edK4d<0k34<_W9cczk!NB E0UsODLjV8( literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pandas_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pandas_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6259fe552dae7d9462c003b88d5608afb17a394d GIT binary patch literal 1244 zcmZWoy>HV%6u+~b)Ji|1e()s(m@v7BWT`r#4oH-;1ZcM`CpW$nr%uk?IaEnARHy@8 zXt#7hVqmHIr?9l@Qdw+>fekff>BM`s(>7qq&-e1~ef)m!mv7_aIRa03o~d^XLVln# zcrxjsUxY)CxWuJCsZjW8zShtyIwcx;L0o;CxCYk`_2hoVXj41$3!Sn}qx>^R@kP() zOHR}ExvVC)5=&F3Uw}i8@VIc2j||2AT4dB<`Y5}; z;m9?m5T#{V7oSUhONEk`YHFht2pQId?+I0^3e|v5AVr;5!-3CMfdv*y$7_1cwNguQ zsY-39xrT3;CNUB4rV=e#<%3@?Gt#oH(DPM^JF3k*QF@zD3f*E-Sn3ypb~Nb=fKij8 z!Ek)BTJJmvZVisf`B16hSRIzf9gC4n3j!O?y6@8$# zR_;6*YHPWIE2}9&OSJ;jfQ56wf~`JQF2~@?_DEzh)^r-2u_(t_L%1y; z&tr_eYdL-zF`)(Xy>%|Jkeo(?3OFEfnRp(FYmN;Ps&0YVCa3wCt)(-AW*(nqY35NM z%nD6%@VcFevO%m#X@VOS68RQm`4&_876y*;IMd-{=vT&Zw(G|M)>JPzhoctGf);(YV literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pickle_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/pickle_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..802c87426b0eab898301428b9ea8c70643daa87c GIT binary patch literal 2098 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 Ggz#UZH{$^S literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/registry_utils.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/registry_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f72f1706b94bc3c8747b9e7741b25a147d4cc1a8 GIT binary patch literal 3857 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torch_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torch_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0d262681b465656f1d4bd559c40b613b2472f83 GIT binary patch literal 1257 zcmah{J#Q015S_gb=WuL{37|-z$T5g;6g~+k5lBc0AB7ue;%0UD*1^u%XLipRTPaYK z1_?pBq@V~A4dSQJfM}wFj*2ddgM?Bsb9W92AhFWU&fe_Iy?OIKgg&#)@ARx{v#4;(Rbrv(i#u+|^M$M@`H5xeWmHCI0|eBV2Q>}L>7KR*HG_Uz zGd%-sW?Ad&%y@J65%| z$PB4=WcY#W*?MFs;Wr|qf!L$Wa^ICpN>TTvQfdFSuoV%l5*iuN>j3 z0dEG*ql8f9#ISjKt#k)bkviZF8MHZ}4<5FLGlzMr{1rcoeW^2Op263`saM12!_5ny z3ij4TG#@Q{M6A`*(UT7cWM9F{2K=Z|}?pGGXK!2Ra3 zkOW;$k)dh6gSbo#MAFD&GmTC>>^0afoO*U=*I?%5-3&7?jnI^n6nbr&$$%!YWz=}w zZB$OgSc%10NyS(hi?KpdmqbAFKN8k&4Exsgq=OrhhC_s_YiM?K#@J3tXT>kZi60iQ tn|qloE3e=D0eJk}$%8eu3yc-Fb0=2k>2+~mkv>+9Y1o`7>u*?D{{^hBHR}KX literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torchjit_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/torchjit_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..994f2da28e3c566f04de66399e8ba02691552633 GIT binary patch literal 1330 zcmbVLJ8u&~5T3nS@dkp z_ASI*&#vU+!ab?#5AOs~7H+<790^ErM)B)kPvS*`_|QChD|2XKGLDZ`)?fjw8t>Rk zb{BToV#SQ@h@OniW)Q_zC_NwdEG@!DY&B31BgEfDE`QefBf_Bhea=Th6?e@I(5`9tT#AQdcwd|_880;K+ zWMR8e5NcO=y2k_QK1`FX-GnpFwyI zUkg*4y^Y@HvyErl{>Q@Hn+C$JGuNkV*0Tm78xHF+kBcq%pL(ns4FfA;Qbic_Kn-E2 zim4o&)Gz|p!h0+#mZHMPCaO_mbe2cZRAQ`2NsZHC!SA*@Jd%g9&mWh4?7}&UaT)0CloEw zj)Z?xDXCBrd8tW)gnQOcDfQ6-GwiIV)BmE4hcoD}DLF`ad}h~~Nf<6xhGK{Fw$~Dz z$0MA#q~8t69^?E`*9)@~2P5z>SQe^4Jf;XvBJ4mCCgGx}stKGzL!UrchtGwn=L`E5 zv#a|#W?$+flv5YFU9ppT#iOVie~3hITuwW;nsjb8>)h&~bF1`UD9Sn+a literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/trimesh_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/trimesh_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd03c81dc0c235fbe1be3c896ba26a7dbbbcb22c GIT binary patch literal 1469 zcmZ`(zi%8x6rR~%x4E-@Bz6J;1Q&yZwz+K)ilTs$;K-6GVw#P_Xni~9>}7Z7n3+>@ z(M5{DNKO?xQ=!n=3KCEO6@LH?MG9+_E~22bbV-vc@9pk6ae$HT&3kX&oBiH=@7tnjE^5>Vq;*Am6{$8d0?A{u0R?m6LBINkPlu*@vJX>FpPKoMl3g#!bk6P zI?uijvysx0hkbP;915xXB1?q|dqU-~4W$_Ho}Srk1L$B{#%Yljo8d@tslstwY~oMX zNlXlRp~Oh`_{?*dQC~aKX{JIRt1(MOc&*}Ag*A9qJG#TMIgWL(z7#pbI-%fOoufo_ z{}u5T)TU6}A&=Up?oIAa?yvv6{_E9Wu0CvkyzT6_7q?qeI3~Ti^-w2=WSs|NhfL_; zkS;u~*2Dsm{)4<1BvCT4t`Lc|15F;kiH}+l;mFs(YiuZ@BtsqtQ{i`kiH^j^H0@-R z51AH+)xPsewXdANi&s>&?)$BK&AZL-F6>_TX?1UPyZO-nu&h-sLC}%L#LCLXTmS_c z6J)i%q5;LZTU-&I`?xi&6?1*d>1XI)qlx>n5s@w`Rzooo>DPKd9mCJxEvB zi@?spnZNuqCA_s@-uR3aan2btO~!JOjG)7rnPcqBQJmE$Hr!y^jKxJEbgWZRD2VaF z%d+%3mk7zY8)MCt#Nte&Mj{u%l5e2E=h`FXXG`ePLoW&}P2vB>H)MaV{q^d8d+E-~ zfn&K#_cwM={*WFJP=DL}L;NnLs2=zrc2DeH+&f*`Q=hua)x~&S;294F6Go-PRRLPA zW}sWnK)0TO?ra9SIAIcdqC)jPYRF%S^qGIQdY5zZ1Mne@>TM7QHl_4whtN}x$->{{ Q+!Jq}E`Q6O64=V@=Pp=o4FCWD literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/txt_handler.cpython-312.pyc b/cosmos_training/cosmos/utils/easy_io/handlers/__pycache__/txt_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73a2fbea7b76a139ac6fbd1e34bfec94e3ff90be GIT binary patch literal 1238 zcmbVLJ8u&~5Z=8l7iKlH4Pv>-?r*RFfXVlCeXw+zIJ#r*};s^Yh6M6v` zZW@$Ini<2JpzIP(8q^~V&7&Knp<~^K;prevtQZ%cud2L?cCHNSr{TNVehC5mAe~D!17ZU}Z`;e&~nGc2{yC?Vb}ZYZLZ8AOi9nj&g5>^)j)t<9Y=RR81l3Sg3xAl{LK($s-f z-tKMn-oN=;m>nWBZZ-L$b#=%6cyIIZL7}u=+$!$e|6qwr5a*8I zf-xdy9tQP& zHMLnxU@g?f;sRr#)8>rDMaJ5Z*9}lFG4{Ia1nG^Kz;1AN#59O~k|ZY`37l;ru;XO4 zHpr2tnb(gBhB-4V8D=?+#=r%b@$GcM>&b%Gvjwl`3tq?HMHt_6n}R-QIsV|O`W7M(hoIOZ4m{;C1ctGNt?Dn;iZ!~lwd^4l}d`r9aV>c zh73l407c`?2C`)EVr1-}(Ir9)v>;k^=+w=y5}@$Z_ee?*k^os!Lj3Xf-o1Bs?@K=~ zFE0T;@zYZORtey55@rv|*)tvBXCFN9SO7Lui17eIW^+bdL$28(Vw4g69QWHqxE=*ev!3i959AKu@A+u9cFFw^*lTsBRXmal(03WvVT z=vR^JA?2x_D^tDDu(W>7RsM}$;D6x`yug>8{LCC#Axz&zvJXDk%mbTyY!__J4TIS>d|@BTNEP3yoozGb{n^F~MjngjK938=Af9Ct`uc)#LC37idNAgP z>__%Bn0!-`ktxz8<;R}ibw@#*sZx>P-BJ-?B1|n6$`87!=wjHZ{@so%x0Rw~HyVx8 zhgL9BvGlF3+OdX_jJr|LiRhwwylx!rLq!tuuX4V zI62YKAE;;~JN`^F_b9d<#l1jTzN-?a7g^utwV<8Cz$v8mFiFo+08Up%p+lFCZualI zvyk=n3(Id~RB9dJF8pqs`OR3J7^?@Z!yCUEAO2Bad63*s4$e*Ltw$FSo*1nuT0*Dj zJK`{}-8U%g!&rNLwHxy;$LeWRJq8IJVg2a1BO$3{nX)!&|EjGyi1B90($EZE1+^_7d?Zp#g{owmwjCHw!mYsQz zqA<_R|J&E{Jf0;lAedT~0(p@XS@cEM=D}BI@nQeodFy3?<3baCU(pdOllsOZ9pQZ##412^;?CtY%!F!DwL@tWh@8o5EfBHyHYtrA>9ID*VMyPK?#mVj0i)oK?` zI4_we1}|+)5n8!5*_f5A?vkR*+kmQ^PjdCkkf!o8DI=L)&c~;njZeE6pZ08g+BBT9 zOs?7S?-8RaW>-2juI6EUEH9&u{*u&32*(;@Y}x=;I~Ia9{}k)^du9Op Tuple[np.ndarray, Dict[str, Any]]: + """ + Load video from a file-like object using imageio.v3 with specified format and color mode. + + Parameters: + file (IO[bytes]): A file-like object containing video data. + format (str): Format of the video file (default 'mp4'). + mode (str): Color mode of the video, 'rgb' or 'gray' (default 'rgb'). + + Returns: + tuple: A tuple containing an array of video frames and metadata about the video. + """ + file.seek(0) + + # The plugin argument in v3 replaces the format argument in v2 + plugin = kwargs.pop("plugin", "pyav") + + # Load all frames at once using v3 API + video_frames = iio_v3.imread(file, plugin=plugin, **kwargs) + + # Handle grayscale conversion if needed + if mode == "gray": + import cv2 + + if len(video_frames.shape) == 4: # (frames, height, width, channels) + gray_frames = [] + for frame in video_frames: + gray_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + gray_frame = np.expand_dims(gray_frame, axis=2) # Keep dimensions consistent + gray_frames.append(gray_frame) + video_frames = np.array(gray_frames) + + # Extract metadata + # Note: iio_v3.imread doesn't return metadata directly like v2 did + # We need to extract it separately + file.seek(0) + metadata = self._extract_metadata(file, plugin=plugin) + + return video_frames, metadata + + def _extract_metadata(self, file: IO[bytes], plugin: str = "pyav") -> Dict[str, Any]: + """ + Extract metadata from a video file. + + Parameters: + file (IO[bytes]): File-like object containing video data. + plugin (str): Plugin to use for reading. + + Returns: + dict: Video metadata. + """ + try: + # Create a generator to read frames and metadata + metadata = iio_v3.immeta(file, plugin=plugin) + + # Add some standard fields similar to v2 metadata format + if "fps" not in metadata and "duration" in metadata: + # Read the first frame to get shape information + file.seek(0) + first_frame = iio_v3.imread(file, plugin=plugin, index=0) + metadata["size"] = first_frame.shape[1::-1] # (width, height) + metadata["source_size"] = metadata["size"] + + # Create a consistent metadata structure with v2 + metadata["plugin"] = plugin + if "codec" not in metadata: + metadata["codec"] = "unknown" + if "pix_fmt" not in metadata: + metadata["pix_fmt"] = "unknown" + + # Calculate nframes if possible + if "fps" in metadata and "duration" in metadata: + metadata["nframes"] = int(metadata["fps"] * metadata["duration"]) + else: + metadata["nframes"] = float("inf") + + return metadata + + except Exception as e: + # Fallback to basic metadata + return { + "plugin": plugin, + "nframes": float("inf"), + "codec": "unknown", + "fps": 30.0, # Default values + "duration": 0, + "size": (0, 0), + } + + def dump_to_fileobj( + self, + obj: np.ndarray | torch.Tensor, + file: IO[bytes], + format: str = "mp4", # pylint: disable=redefined-builtin + fps: int = 17, + quality: int = 5, + ffmpeg_params=None, + **kwargs, + ): + """ + Save an array of video frames to a file-like object using imageio. + + Parameters: + obj (Union[np.ndarray, torch.Tensor]): An array of frames to be saved as video. + file (IO[bytes]): A file-like object to which the video data will be written. + format (str): Format of the video file (default 'mp4'). + fps (int): Frames per second of the output video (default 17). + quality (int): Quality of the video (0-10, default 5). + ffmpeg_params (list): Additional parameters to pass to ffmpeg. + + """ + if isinstance(obj, torch.Tensor): + assert obj.dtype == torch.uint8, "Tensor must be of type uint8" + obj = obj.cpu().numpy() + h, w = obj.shape[1:-1] + + # Default ffmpeg params that ensure width and height are set + default_ffmpeg_params = ["-s", f"{w}x{h}"] + + # Use provided ffmpeg_params if any, otherwise use defaults + final_ffmpeg_params = ffmpeg_params if ffmpeg_params is not None else default_ffmpeg_params + + mimsave_kwargs = { + "fps": fps, + "quality": quality, + "macro_block_size": 1, + "ffmpeg_params": final_ffmpeg_params, + "output_params": ["-f", "mp4"], + } + # Update with any other kwargs + mimsave_kwargs.update(kwargs) + log.debug(f"mimsave_kwargs: {mimsave_kwargs}") + + imageio.mimsave(file, obj, format, **mimsave_kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/json_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/json_handler.py new file mode 100644 index 00000000..e493ca58 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/json_handler.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import numpy as np + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +def set_default(obj): + """Set default json values for non-serializable values. + + It helps convert ``set``, ``range`` and ``np.ndarray`` data types to list. + It also converts ``np.generic`` (including ``np.int32``, ``np.float32``, + etc.) into plain numbers of plain python built-in types. + """ + if isinstance(obj, (set, range)): + return list(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, np.generic): + return obj.item() + raise TypeError(f"{type(obj)} is unsupported for json dump") + + +class JsonHandler(BaseFileHandler): + def load_from_fileobj(self, file): + return json.load(file) + + def dump_to_fileobj(self, obj, file, **kwargs): + kwargs.setdefault("default", set_default) + json.dump(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("default", set_default) + return json.dumps(obj, **kwargs) diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/jsonl_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/jsonl_handler.py new file mode 100644 index 00000000..ae717cc7 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/jsonl_handler.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from typing import IO + +import numpy as np + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +def set_default(obj): + """Set default json values for non-serializable values. + + It helps convert ``set``, ``range`` and ``np.ndarray`` data types to list. + It also converts ``np.generic`` (including ``np.int32``, ``np.float32``, + etc.) into plain numbers of plain python built-in types. + """ + if isinstance(obj, (set, range)): + return list(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, np.generic): + return obj.item() + raise TypeError(f"{type(obj)} is unsupported for json dump") + + +class JsonlHandler(BaseFileHandler): + """Handler for JSON lines (JSONL) files.""" + + def load_from_fileobj(self, file: IO[bytes]): + """Load JSON objects from a newline-delimited JSON (JSONL) file object. + + Returns: + A list of Python objects loaded from each JSON line. + """ + data = [] + for line in file: + line = line.strip() + if not line: + continue # skip empty lines if any + data.append(json.loads(line)) + return data + + def dump_to_fileobj(self, obj: IO[bytes], file, **kwargs): + """Dump a list of objects to a newline-delimited JSON (JSONL) file object. + + Args: + obj: A list (or iterable) of objects to dump line by line. + """ + kwargs.setdefault("default", set_default) + for item in obj: + file.write(json.dumps(item, **kwargs) + "\n") + + def dump_to_str(self, obj, **kwargs): + """Dump a list of objects to a newline-delimited JSON (JSONL) string.""" + kwargs.setdefault("default", set_default) + lines = [json.dumps(item, **kwargs) for item in obj] + return "\n".join(lines) + + +if __name__ == "__main__": + from cosmos.utils.easy_io import easy_io + + easy_io.dump([1, 2, 3], "test.jsonl", file_format="jsonl") + print(easy_io.load("test.jsonl")) + easy_io.dump([{"key1": 1, "key2": 2}, {"key1": 3, "key2": 4}], "test.jsonl", file_format="jsonl") + print(easy_io.load("test.jsonl")) diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/np_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/np_handler.py new file mode 100644 index 00000000..48adc58d --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/np_handler.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io import BytesIO +from typing import IO, Any + +import numpy as np + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class NumpyHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file: IO[bytes], **kwargs) -> Any: + """ + Load a NumPy array from a file-like object. + + Parameters: + file (IO[bytes]): The file-like object containing the NumPy array data. + **kwargs: Additional keyword arguments passed to `np.load`. + + Returns: + numpy.ndarray: The loaded NumPy array. + """ + return np.load(file, **kwargs) + + def load_from_path(self, filepath: str, **kwargs) -> Any: + """ + Load a NumPy array from a file path. + + Parameters: + filepath (str): The path to the file to load. + **kwargs: Additional keyword arguments passed to `np.load`. + + Returns: + numpy.ndarray: The loaded NumPy array. + """ + return super().load_from_path(filepath, mode="rb", **kwargs) + + def dump_to_str(self, obj: np.ndarray, **kwargs) -> str: + """ + Serialize a NumPy array to a string in binary format. + + Parameters: + obj (np.ndarray): The NumPy array to serialize. + **kwargs: Additional keyword arguments passed to `np.save`. + + Returns: + str: The serialized NumPy array as a string. + """ + with BytesIO() as f: + np.save(f, obj, **kwargs) + return f.getvalue() + + def dump_to_fileobj(self, obj: np.ndarray, file: IO[bytes], **kwargs): + """ + Dump a NumPy array to a file-like object. + + Parameters: + obj (np.ndarray): The NumPy array to dump. + file (IO[bytes]): The file-like object to which the array is dumped. + **kwargs: Additional keyword arguments passed to `np.save`. + """ + np.save(file, obj, **kwargs) + + def dump_to_path(self, obj: np.ndarray, filepath: str, **kwargs): + """ + Dump a NumPy array to a file path. + + Parameters: + obj (np.ndarray): The NumPy array to dump. + filepath (str): The file path where the array should be saved. + **kwargs: Additional keyword arguments passed to `np.save`. + """ + with open(filepath, "wb") as f: + np.save(f, obj, **kwargs) diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/pandas_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/pandas_handler.py new file mode 100644 index 00000000..7c3d4fde --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/pandas_handler.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler # isort:skip + + +class PandasHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return pd.read_csv(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + obj.to_csv(file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError("PandasHandler does not support dumping to str") diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/pickle_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/pickle_handler.py new file mode 100644 index 00000000..e7f9490c --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/pickle_handler.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +from io import BytesIO +from typing import Any + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class PickleHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file: BytesIO, **kwargs): + return pickle.load(file, **kwargs) + + def load_from_path(self, filepath, **kwargs): + return super().load_from_path(filepath, mode="rb", **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("protocol", 2) + return pickle.dumps(obj, **kwargs) + + def dump_to_fileobj(self, obj: Any, file: BytesIO, **kwargs): + kwargs.setdefault("protocol", 2) + pickle.dump(obj, file, **kwargs) + + def dump_to_path(self, obj, filepath, **kwargs): + with open(filepath, "wb") as f: + pickle.dump(obj, f, **kwargs) diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/pil_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/pil_handler.py new file mode 100644 index 00000000..9d096e4c --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/pil_handler.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO, Optional, Tuple, Union + +import numpy as np + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + +try: + from PIL import Image +except ImportError: + Image = None + + +class PILHandler(BaseFileHandler): + format: str + str_like = False + + def load_from_fileobj( + self, + file: IO[bytes], + fmt: str = "pil", + size: Optional[Union[int, Tuple[int, int]]] = None, + **kwargs, + ): + """ + Load an image from a file-like object and return it in a specified format. + + Args: + file (IO[bytes]): A file-like object containing the image data. + fmt (str): The format to convert the image into. Options are \ + 'numpy', 'np', 'npy', 'type' (all return numpy arrays), \ + 'pil' (returns PIL Image), 'th', 'torch' (returns a torch tensor). + size (Optional[Union[int, Tuple[int, int]]]): The new size of the image as a single integer \ + or a tuple of (width, height). If specified, the image is resized accordingly. + **kwargs: Additional keyword arguments that can be passed to conversion functions. + + Returns: + Image data in the format specified by `fmt`. + + Raises: + IOError: If the image cannot be loaded or processed. + ValueError: If the specified format is unsupported. + """ + try: + img = Image.open(file) + img.load() # Explicitly load the image data + if size is not None: + if isinstance(size, int): + size = ( + size, + size, + ) # create a tuple if only one integer is provided + img = img.resize(size, Image.ANTIALIAS) + + # Return the image in the requested format + if fmt in ["numpy", "np", "npy"]: + return np.array(img, **kwargs) + if fmt == "pil": + return img + if fmt in ["th", "torch"]: + import torch + + # Convert to tensor + img_tensor = torch.from_numpy(np.array(img, **kwargs)) + # Convert image from HxWxC to CxHxW + if img_tensor.ndim == 3: + img_tensor = img_tensor.permute(2, 0, 1) + return img_tensor + raise ValueError( + "Unsupported format. Supported formats are 'numpy', 'np', 'npy', 'pil', 'th', and 'torch'." + ) + except Exception as e: + raise IOError(f"Unable to load image: {e}") from e + + def dump_to_fileobj(self, obj, file: IO[bytes], **kwargs): + if "format" not in kwargs: + kwargs["format"] = self.format + kwargs["format"] = "JPEG" if self.format.lower() == "jpg" else self.format.upper() + obj.save(file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/registry_utils.py b/cosmos_training/cosmos/utils/easy_io/handlers/registry_utils.py new file mode 100644 index 00000000..03a16944 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/registry_utils.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from cosmos.utils.flags import TRAINING +from cosmos.utils.easy_io.handlers.base import BaseFileHandler +from cosmos.utils.easy_io.handlers.byte_handler import ByteHandler +from cosmos.utils.easy_io.handlers.csv_handler import CsvHandler +from cosmos.utils.easy_io.handlers.gzip_handler import GzipHandler +from cosmos.utils.easy_io.handlers.imageio_video_handler import ImageioVideoHandler +from cosmos.utils.easy_io.handlers.json_handler import JsonHandler +from cosmos.utils.easy_io.handlers.jsonl_handler import JsonlHandler +from cosmos.utils.easy_io.handlers.np_handler import NumpyHandler +from cosmos.utils.easy_io.handlers.pickle_handler import PickleHandler +from cosmos.utils.easy_io.handlers.pil_handler import PILHandler +from cosmos.utils.easy_io.handlers.tarfile_handler import TarHandler +from cosmos.utils.easy_io.handlers.torch_handler import TorchHandler +from cosmos.utils.easy_io.handlers.torchjit_handler import TorchJitHandler +from cosmos.utils.easy_io.handlers.txt_handler import TxtHandler +from cosmos.utils.easy_io.handlers.yaml_handler import YamlHandler + +file_handlers = { + "json": JsonHandler(), + "yaml": YamlHandler(), + "yml": YamlHandler(), + "pickle": PickleHandler(), + "pkl": PickleHandler(), + "tar": TarHandler(), + "jit": TorchJitHandler(), + "npy": NumpyHandler(), + "txt": TxtHandler(), + "csv": CsvHandler(), + "gz": GzipHandler(), + "jsonl": JsonlHandler(), + "byte": ByteHandler(), +} + +if TRAINING: + from cosmos.utils.easy_io.handlers.pandas_handler import PandasHandler + + file_handlers["pandas"] = PandasHandler() + +for torch_type in ["pt", "pth", "ckpt"]: + file_handlers[torch_type] = TorchHandler() +for img_type in ["jpg", "jpeg", "png", "bmp", "gif"]: + file_handlers[img_type] = PILHandler() + file_handlers[img_type].format = img_type +try: + from cosmos.utils.easy_io.handlers.trimesh_handler import TrimeshHandler + + for mesh_type in ["ply", "stl", "obj", "glb"]: + file_handlers[mesh_type] = TrimeshHandler() + file_handlers[mesh_type].format = mesh_type +except ImportError: + pass +for video_type in ["mp4", "avi", "mov", "webm", "flv", "wmv"]: + file_handlers[video_type] = ImageioVideoHandler() + + +def _register_handler(handler, file_formats): + """Register a handler for some file extensions. + + Args: + handler (:obj:`BaseFileHandler`): Handler to be registered. + file_formats (str or list[str]): File formats to be handled by this + handler. + """ + if not isinstance(handler, BaseFileHandler): + raise TypeError(f"handler must be a child of BaseFileHandler, not {type(handler)}") + if isinstance(file_formats, str): + file_formats = [file_formats] + if not all([isinstance(item, str) for item in file_formats]): + raise TypeError("file_formats must be a str or a list of str") + for ext in file_formats: + file_handlers[ext] = handler + + +def register_handler(file_formats, **kwargs): + def wrap(cls): + _register_handler(cls(**kwargs), file_formats) + return cls + + return wrap diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/tarfile_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/tarfile_handler.py new file mode 100644 index 00000000..0720594a --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/tarfile_handler.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tarfile + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class TarHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, mode="r|*", **kwargs): + return tarfile.open(fileobj=file, mode=mode, **kwargs) + + def load_from_path(self, filepath, mode="r|*", **kwargs): + return tarfile.open(filepath, mode=mode, **kwargs) + + def dump_to_fileobj(self, obj, file, mode="w", **kwargs): + with tarfile.open(fileobj=file, mode=mode) as tar: + tar.add(obj, **kwargs) + + def dump_to_path(self, obj, filepath, mode="w", **kwargs): + with tarfile.open(filepath, mode=mode) as tar: + tar.add(obj, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/torch_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/torch_handler.py new file mode 100644 index 00000000..119fee2f --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/torch_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import torch +except ImportError: + torch = None + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class TorchHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return torch.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + torch.save(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/torchjit_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/torchjit_handler.py new file mode 100644 index 00000000..e2e99fad --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/torchjit_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import torch +except ImportError: + torch = None + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class TorchJitHandler(BaseFileHandler): + str_like = False + + def load_from_fileobj(self, file, **kwargs): + return torch.jit.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + torch.jit.save(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/trimesh_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/trimesh_handler.py new file mode 100644 index 00000000..c1e8e4a1 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/trimesh_handler.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import IO + +import trimesh + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class TrimeshHandler(BaseFileHandler): + format: str + str_like = False + + def load_from_fileobj(self, file: IO[bytes], **kwargs) -> trimesh.Trimesh: + file = trimesh.load(file_obj=file, file_type=self.format) + return file + + def dump_to_fileobj(self, obj, file: IO[bytes], **kwargs): + obj.export(file_obj=file, file_type=self.format) + return file + + def dump_to_str(self, obj, **kwargs): + raise NotImplementedError diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/txt_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/txt_handler.py new file mode 100644 index 00000000..9ab3f4ca --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/txt_handler.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler + + +class TxtHandler(BaseFileHandler): + def load_from_fileobj(self, file, **kwargs): + del kwargs + return file.read() + + def dump_to_fileobj(self, obj, file, **kwargs): + del kwargs + if not isinstance(obj, str): + obj = str(obj) + file.write(obj) + + def dump_to_str(self, obj, **kwargs): + del kwargs + if not isinstance(obj, str): + obj = str(obj) + return obj diff --git a/cosmos_training/cosmos/utils/easy_io/handlers/yaml_handler.py b/cosmos_training/cosmos/utils/easy_io/handlers/yaml_handler.py new file mode 100644 index 00000000..1e7c8bb0 --- /dev/null +++ b/cosmos_training/cosmos/utils/easy_io/handlers/yaml_handler.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml + +try: + from yaml import CDumper as Dumper # type: ignore + from yaml import CLoader as Loader # type: ignore +except ImportError: + from yaml import Dumper, Loader # type: ignore + +from cosmos.utils.easy_io.handlers.base import BaseFileHandler # isort:skip + + +class YamlHandler(BaseFileHandler): + def load_from_fileobj(self, file, **kwargs): + kwargs.setdefault("Loader", Loader) + return yaml.load(file, **kwargs) + + def dump_to_fileobj(self, obj, file, **kwargs): + kwargs.setdefault("Dumper", Dumper) + yaml.dump(obj, file, **kwargs) + + def dump_to_str(self, obj, **kwargs): + kwargs.setdefault("Dumper", Dumper) + return yaml.dump(obj, **kwargs) diff --git a/cosmos_training/cosmos/utils/ema.py b/cosmos_training/cosmos/utils/ema.py new file mode 100644 index 00000000..a0533800 --- /dev/null +++ b/cosmos_training/cosmos/utils/ema.py @@ -0,0 +1,366 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from contextlib import contextmanager, nullcontext +from typing import TYPE_CHECKING, Any, Generator, List, Optional, Union + +import numpy as np +import torch + +try: + from megatron.core import parallel_state + + USE_MEGATRON = True +except ImportError: + USE_MEGATRON = False + +from cosmos.utils import distributed, log + +if TYPE_CHECKING: + from cosmos.model._base import ImaginaireModel + + +class FastEmaModelUpdater: + """ + This class is used to update target model~(EMA) given source model~(regular model) and beta. + The method interaface mimic :class:`EMAModelTracker` and :class:`PowerEMATracker`. + Different from two classes, this class does not maintain the EMA model weights as buffers. It expects the user to have two module with same architecture and weights shape. + The class is proposed to work with FSDP model where above two classes are not working as expected. Besides, it is strange to claim model weights as buffers and do unnecessary name changing in :class:`EMAModelTracker` and :class:`PowerEMATracker`. Moeving forward, we should use this class instead of above two classes. + """ + + def __init__(self): + # Flag to indicate whether the cache is taken or not. Useful to avoid cache overwrite + self.is_cached = False + + def update_average(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module, beta: float = 0.9999) -> None: + target_list = [] + source_list = [] + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + assert tgt_params.dtype == torch.float32, ( + f"EMA model only works in FP32 dtype, got {tgt_params.dtype} instead." + ) + target_list.append(tgt_params) + source_list.append(src_params.data) + torch._foreach_mul_(target_list, beta) + torch._foreach_add_(target_list, source_list, alpha=1.0 - beta) + + def copy_to(self, src_model: torch.nn.Module, tgt_model: torch.nn.Module) -> None: + for tgt_params, src_params in zip(tgt_model.parameters(), src_model.parameters()): + tgt_params.data.copy_(src_params.data) + + def cache(self, parameters: Any, is_cpu: bool = False) -> None: + """Save the current parameters for restoring later. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be temporarily stored. + """ + assert self.is_cached is False, "EMA cache is already taken. Did you forget to restore it?" + device = "cpu" if is_cpu else "cuda" + self.collected_params = [param.clone().to(device) for param in parameters] + self.is_cached = True + + def restore(self, parameters: Any) -> None: + """Restore the parameters in self.collected_params. + + Useful to validate the model with EMA parameters without affecting the + original optimization process. Store the parameters before copy_to(). + After validation (or model saving), use this to restore the former parameters. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be updated with the stored parameters. + """ + assert self.is_cached, "EMA cache is not taken yet." + for c_param, param in zip(self.collected_params, parameters, strict=False): + param.data.copy_(c_param.data.type_as(param.data)) + self.collected_params = [] + # Release the cache after we call restore + self.is_cached = False + + +def get_buffer_name(param_name: str, torch_compile_buffer_renaming: bool = False) -> str: + """ + This function creates buffer name used by EMA from parameter's name + + Args: + param_name (str): Model's parameter name + Returns: + buffer_name (str): buffer name to be used for given parameter name + """ + + buffer_name = param_name.replace(".", "-") + + if torch_compile_buffer_renaming: + # torch.compile() adds _orig_mod to state dict names, this way we get original name + buffer_name = buffer_name.replace("_orig_mod-", "") + + return buffer_name + + +class EMAModelTracker(torch.nn.Module): + """This is a class to track the EMA model weights. + + The EMA weights are registered as buffers, which are extractable as state dicts. The names follow those of the + regular weights, except all "." are replaced with "-" (limitation of register_buffer()). This is similar to SDXL's + implementation of EMA. There are no optimizable parameters. + TODO(snah): multi-EMA weights. + + Attributes: + collected_params (list): temporarily stores the regular weights while in EMA mode. + beta (float): EMA decay rate. (default: 0.9999). + torch_compile_buffer_renaming (bool): whether to remove '_orig_mod-' from buffer names when torch.compile is used + """ + + def __init__(self, model: ImaginaireModel, beta: float = 0.9999, torch_compile_buffer_renaming: bool = False): + """Constructor of the EMA model weight tracker. + + Args: + model (ImaginaireModel): The PyTorch model. + beta (float): EMA decay rate. (default: 0.9999). + """ + super().__init__() + self.torch_compile_buffer_renaming: bool = torch_compile_buffer_renaming + if not 0.0 <= beta <= 1.0: + raise ValueError("Decay must be between 0 and 1") + self.beta = beta + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + self.register_buffer(buffer_name, param.clone().detach().data) + self.collected_params = [] + # Flag to indicate whether the cache is taken or not. Useful to avoid cache overwrite + self.is_cached = False + + @torch.no_grad() + def update_average(self, model: ImaginaireModel, iteration: Optional[int] = None) -> None: + del iteration + target_list = [] + source_list = [] + ema_buffers = self.state_dict() + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + buffer = ema_buffers[buffer_name] + assert buffer.dtype == torch.float32, f"EMA model only works in FP32 dtype, got {buffer.dtype} instead." + target_list.append(buffer) + source_list.append(param.data) + torch._foreach_mul_(target_list, self.beta) + torch._foreach_add_(target_list, source_list, alpha=1.0 - self.beta) + + def copy_to(self, model: ImaginaireModel) -> None: + ema_buffers = self.state_dict() + for name, param in model.named_parameters(): + if param.requires_grad: + buffer_name = get_buffer_name(name, self.torch_compile_buffer_renaming) + buffer = ema_buffers[buffer_name] + param.data.copy_(buffer.data) + + def cache(self, parameters: Any, is_cpu: bool = False) -> None: + """Save the current parameters for restoring later. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be temporarily stored. + """ + assert self.is_cached is False, "EMA cache is already taken. Did you forget to restore it?" + device = "cpu" if is_cpu else "cuda" + self.collected_params = [param.clone().to(device) for param in parameters] + self.is_cached = True + + def restore(self, parameters: Any) -> None: + """Restore the parameters in self.collected_params. + + Useful to validate the model with EMA parameters without affecting the + original optimization process. Store the parameters before copy_to(). + After validation (or model saving), use this to restore the former parameters. + + Args: + parameters (iterable): Iterable of torch.nn.Parameter to be updated with the stored parameters. + """ + assert self.is_cached, "EMA cache is not taken yet." + for c_param, param in zip(self.collected_params, parameters, strict=False): + param.data.copy_(c_param.data.type_as(param.data)) + self.collected_params = [] + # Release the cache after we call restore + self.is_cached = False + + @classmethod + def initialize_multi_rank_ema( + cls, model: torch.nn.Module, rate: Union[float, List[float]], num: int = 1, enabled: bool = True + ) -> Optional[EMAModelTracker]: + """ + Class method to initialize per rank EMA Model Tracker with different rate. + Each rank will have a different rate based on the given configuration, resulting in different EMA weights. + + Args: + model (torch.nn.Module): The neural network model to be tracked. + rate (Union[float, List[float]]): The decay rate(s) for the EMA. If a list is provided, + it corresponds to rates for different ranks. + num (int, optional): The number of leading ranks to consider for different rates. + Defaults to 1. + enabled (bool, optional): Flag to enable or disable the creation of the tracker. + If False, returns None. Defaults to True. + + Returns: + Optional[EMAModelTracker]: An instance of EMAModelTracker if enabled, otherwise None. + + Example: + >>> model = torch.nn.Linear(10, 2) + >>> tracker = EMAModelTracker.initialize_ema_from_settings(model, rate=[0.1, 0.2], num=2) + >>> print(tracker) + + Notes: + If `rate` is a list and the current rank is less than `num`, the rate for the current rank + is used. If the current rank exceeds `num`, the first rate in the list is used by default. + """ + if not enabled: + return None + if USE_MEGATRON and parallel_state.is_initialized(): + cur_dp_rank = parallel_state.get_data_parallel_rank(with_context_parallel=True) + log.critical(f"using MCore parallel_state for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + log.warning("It should not used together with FSDP!") + else: + cur_dp_rank = distributed.get_rank() + log.critical(f"using torch.distributed for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + rate = rate if isinstance(rate, list) else [rate] + num = min(num, len(rate)) + rate = rate[cur_dp_rank] if cur_dp_rank < num else rate[0] + if cur_dp_rank < num: + print(f"EMAModelTracker: rank {cur_dp_rank}, rate {rate}") + return cls(model, rate) + + +class PowerEMATracker(EMAModelTracker): + def __init__(self, model: ImaginaireModel, s: float = 0.1, torch_compile_buffer_renaming: bool = False): + """Constructor of the EMA model weight tracker. + + Args: + model (ImaginaireModel): The PyTorch model. + s (float): EMA decay rate. See EDM2 paper + torch_compile_buffer_renaming (bool): whether to remove '_orig_mod-' from buffer names when torch.compile is used + """ + super().__init__(model=model, beta=0.0, torch_compile_buffer_renaming=torch_compile_buffer_renaming) + self.exp = np.roots([1, 7, 16 - s**-2, 12 - s**-2]).real.max() + + @torch.no_grad() + def update_average(self, model: ImaginaireModel, iteration: Optional[int] = None) -> None: + if iteration == 0: + beta = 0.0 + else: + i = iteration + 1 + beta = (1 - 1 / i) ** (self.exp + 1) + self.beta = beta + + super().update_average(model, iteration) + + @classmethod + def initialize_multi_rank_ema( + cls, model: torch.nn.Module, rate: float, num: int, enabled: bool = True + ) -> Optional[PowerEMATracker]: + """ + Class method to initialize per rank EMA Model Tracker with different rate. + Each rank will have a different rate based on the given configuration, resulting in different EMA weights. + + Args: + model (torch.nn.Module): The neural network model for which the EMA tracker is being set up. + num (int): The number of ranks for which the rate adjustment is applied. Beyond this, the rate remains unchanged. + rate (float): The base decay rate for the EMA calculation. + enabled (bool, optional): Flag to enable or disable the initialization of the tracker. If False, returns None. + Defaults to True. + + Returns: + Optional[PowerEMATracker]: An instance of PowerEMATracker with adjusted rate if enabled, otherwise None. + + Raises: + None + + Example: + >>> model = torch.nn.Linear(10, 2) + >>> tracker = PowerEMATracker.initialize_multi_rank_ema(model, num=3, rate=0.99) + >>> print(tracker) + + Notes: + The decay rate is modified by dividing it by 2 raised to the power of the rank for each rank less than `num`. + If the rank is greater than or equal to `num`, the base rate is used without modification. This approach + allows higher ranked processes to have a less aggressive decay, potentially reflecting their delayed synchronization + in a distributed training scenario. + """ + if not enabled: + return None + if USE_MEGATRON and parallel_state.is_initialized(): + cur_dp_rank = parallel_state.get_data_parallel_rank(with_context_parallel=True) + log.critical(f"using MCore parallel_state for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + log.warning("It should not used together with FSDP!") + else: + cur_dp_rank = distributed.get_rank() + log.critical(f"using torch.distributed for EMA initialization. DP RANK: {cur_dp_rank}", rank0_only=False) + + divider = 2**cur_dp_rank if cur_dp_rank < num else 1 + if cur_dp_rank < num: + print(f"PowerEMATracker: rank {cur_dp_rank}, rate {rate / divider}") + return cls(model, rate / divider) + + +@contextmanager +def ema_scope(model: ImaginaireModel, enabled: bool = False, context: str | None = None) -> Generator[None, None, None]: + """Context manager for switching between regular and EMA model weights. + + This function is a dispatcher that handles two main cases: + 1. If the model has its own `ema_scope` method, it will be used. + This allows models to define custom EMA logic (e.g., for FSDP). + 2. If not, it falls back to a generic mechanism that expects the model + to have a `.ema` attribute containing an EMA tracker object. + + Args: + model (ImaginaireModel): The PyTorch model. + enabled (bool): Whether switching to EMA weights is enabled (default: False). + context (str | None): A logging context string, passed to the model's ema_scope if used. + """ + + def scope_function(): + if enabled: + has_custom_scope = hasattr(model, "ema_scope") and callable(model.ema_scope) + has_generic_ema = hasattr(model, "ema") and isinstance( + model.ema, (FastEmaModelUpdater, EMAModelTracker, PowerEMATracker) + ) + assert has_custom_scope or has_generic_ema + + if has_custom_scope: + return model.ema_scope(context=context) + else: + return ema_scope_generic(model) + else: + return nullcontext() + + with scope_function(): + yield + + +@contextmanager +def ema_scope_generic(model: ImaginaireModel) -> Generator[None, None, None]: + """Generic context manager for switching between regular and EMA model weights. + + Args: + model (ImaginaireModel): The PyTorch model, which must have a `.ema` attribute. + """ + model.ema.cache(model.parameters()) + model.ema.copy_to(model) + + log.info("EMA: switched to EMA weights.") + try: + yield + finally: + model.ema.restore(model.parameters()) + log.info("EMA: restored regular weights.") diff --git a/cosmos_training/cosmos/utils/env_parsers/__pycache__/cred_env_parser.cpython-312.pyc b/cosmos_training/cosmos/utils/env_parsers/__pycache__/cred_env_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e932b86db04774202960568a1f1123b2c180c892 GIT binary patch literal 3059 zcmb7`PgC1Q7{*t&Y;XcjfCM{$!3GQ%h@C=a+ENM_TLF?_du-AszU(NLk~+05cO@qS zr*tOYq3KLMK-=`-Bj2GHYpNMy&Gf+Z)LSx1FFAF0{RfG457CU*((irt*;lL8tNmke z@B+Yx_QCb!)C<7h$XAOCA*#sbZpQ5F~wt^KQO&~o&kXAFiR!ZD;`I#un|m0ds2T-I>yqr z_XHck-jh9PuqT~nY1@02jbQJ&o^+unU1Vw7dzp=3?`ThYqbFTqDe;!vuUz@D!@fxW zKSU7WOpj1`nh}vIBYfe`X{cCvT(EX-uL|(~H;$|%8RR($U-0X+2B9>(c%?l8g6{%Jt=K~P_ zP!mh72;%YjF#F<}3=~q|m{XZ-Itgzb!1%X0n8*sr9E>LtLM{jI2oK<1(wt!hIROiC zkFL&~WJN+MnMv=Z4&c4)T{FPOv%=0^It5eleZdUUaBdYQc7?>9R<|>ElErKf1)E0e zX6!z;JX(3$1=d7DbeNq)=KnJZ_mZ%wv~KB~lP#B4vgZ`mWLg%PrIKAZqA-)fjd)ae2wA{NvcK9)SZ`;V%>@n-0Gnu_a zJeNzu_`QQ&xSvi6cOi-A4zdqmJhMmEr|IvCFt9rR3f_}C`0?jhsita3jy+PJ#i|O@ z9x0`wqQ(k}TE>4>q&$%eTIU)*6jd2&NGw)x;f>XB9adw9V&yUUY3)u>smc{qsiA`0 zA>AHr+nQD^sWG|o6jo_5$Nu=`hi4#KJw$GtA8p7tR|U9ozW`0|m6ut=8+^XgoLjDE zjkzxx{7iH4#__PRxZdFBo8hI#&HG09L4%)e&Mwx!GG;d$eCTxkdi|j>|7C-pZ7zLw zJZ&s}-QX9j7U8A(wh{gucRO8(){(KW*5Jd<<(1%_gRYW>Y|xO8G-OkTu91dp#Lzft$i@o=NJBPTXo@sJ z+?d|(aQMYe9H1Ezp${R9&p!6Aw>}{oXcb?Qt%Ztrso<@Xy0mX;@i-D8}B@> zG51?9mG0<)9V$)hp){4ob$^^nH+A18mX3F%yLw=kN;7&WL#5lgf166Tbl(=1PIRQ7 n;JU2`wyEoFJ#?G8uIv7F>bj=;*0AfS``zl)u;WKBouvN&Tf_c| literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/env_parsers/__pycache__/env_parser.cpython-312.pyc b/cosmos_training/cosmos/utils/env_parsers/__pycache__/env_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc17182536ca03c29ad5e8999d5a879d832e7105 GIT binary patch literal 6381 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 aWZPd!*Baxbm str: + return self.value + + @staticmethod + def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str: + return name.lower() + + +def _parse_bool(value: str) -> bool: + """Parse string to a boolean.""" + return value.lower() in ["true", "1", "yes", "y"] + + +def _get_bool(name: str, default: bool) -> bool: + """Get a boolean flag from the environment.""" + value = os.environ.get(name, "") + if not value: + return default + return _parse_bool(value) + + +TRAINING: Final[bool] = _get_bool("COSMOS_TRAINING", True) +"""Whether to enable training features. + +This is used to make training dependencies optional. +""" + +INTERNAL: Final[bool] = _get_bool("COSMOS_INTERNAL", False) +"""Whether to use internal (nvidia-only) resources (e.g. S3).""" + +SMOKE: Final[bool] = _get_bool("COSMOS_SMOKE", False) +"""Whether to enable smoke test. + +Sets parameters to minimum values (e.g. num_steps=1, num_layers=2). +""" + + +class Device(StrEnum): + CUDA = "cuda" + CPU = "cpu" + META = "meta" + + +DEVICE: Final[Device] = Device(os.environ.get("COSMOS_DEVICE", "cuda").lower()) +"""Torch device to use. + +Used for checkpoint conversion and smoke tests. +""" + +VERBOSE: Final[bool] = _get_bool("COSMOS_VERBOSE", INTERNAL) +"""Whether to enable verbose console output.""" + +EXPERIMENTAL_CHECKPOINTS: Final[bool] = _get_bool("COSMOS_EXPERIMENTAL_CHECKPOINTS", INTERNAL) +"""Whether to enable experimental checkpoints.""" + + +if INTERNAL: + TRAINING = True + + +@dataclass +class Flags: + internal: bool = INTERNAL + training: bool = TRAINING + smoke: bool = SMOKE + device: Device = DEVICE + verbose: bool = VERBOSE + experimental_checkpoints: bool = EXPERIMENTAL_CHECKPOINTS + + +FLAGS = Flags() +"""Convenience object for accessing flags.""" diff --git a/cosmos_training/cosmos/utils/functional/__pycache__/batch_ops.cpython-312.pyc b/cosmos_training/cosmos/utils/functional/__pycache__/batch_ops.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1689260fd4716e7c704ba432a5d641ff2105e78e GIT binary patch literal 1923 zcmb_c&u<$=6rQnn*G_CVMxlXFP_`;mHKJX$f>H!Rs)!;Lst75lMO8zz*&QdF?hj{X zHfe$lrs7~Dq!=U=AJY@Da>;) z)vuLX$tS!RY#P{)g!yI1CfPHqS?0*s;H1d zWbKo(DvAPm{?&Kq96}U-d*(wZ?b1*mTwv8+rk$?y9#)Xxbq9FSbC5SP05y<}4XAQr z9ooGPU$PFcUhE3gs0=NfLE;S=`Bh*&DDuE>fyfYhU}W5jTu4r@PfR)skeGBU@h>G+PnV1$fNW~>1>v3M6W7nHKzWGk+qieuitx!AO2%wZ7Wm^~x6hF?Bz+t=!MGN6zo1XZMVce>HCI8#nhp zz1=ou+h7?vA14Ja)=>IHL%a9oK{mH>t$wXxZZ3Scu%&EkyJM}9ef`S**p+tnof?kQ zD4VO9wKuCD#VItLZ=C&R;=eGzWwM8e4tx)K>f(w{%8UCtY&H?^UNX1_FrK}OcpC=4 zf+}*b#1PM*7M^ZlS*KRwpJ`y?;_5mb1r-e)7TL6I`VySqfYmt#k1BeQIn}({TyAGh ze>GXXT3tQ>ddu~^B^*<#s0Su9Cff?J36 zlC6H6e$NZ+&?WEF*8nO;i2VQ$m~4#yx`gm}tmSaNsl^D?)@7--$?nuoQ@hjq7e43+ fbF3%ubB(bWfoe`kwYAa_r+E07JdZ>)+2+3i%T1cI literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/functional/__pycache__/lr_scheduler.cpython-312.pyc b/cosmos_training/cosmos/utils/functional/__pycache__/lr_scheduler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d681eab86a3cf8888286574de6bc27cee00edd26 GIT binary patch literal 9469 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/functional/__pycache__/runge_kutta.cpython-312.pyc b/cosmos_training/cosmos/utils/functional/__pycache__/runge_kutta.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b786d181fcb33014320cc01b5cd5e2cedcaad85 GIT binary patch literal 12761 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 tuple[Tensor, Tensor]: + ndims1 = x.ndim + ndims2 = y.ndim + + common_ndims = min(ndims1, ndims2) + for axis in range(common_ndims): + assert x.shape[axis] == y.shape[axis], "Dimensions not equal at axis {}".format(axis) + + if ndims1 < ndims2: + x = x.reshape(x.shape + (1,) * (ndims2 - ndims1)) # x broadcast-padded to ndims2: [*x.shape,1,...] + elif ndims2 < ndims1: + y = y.reshape(y.shape + (1,) * (ndims1 - ndims2)) # y broadcast-padded to ndims1: [*y.shape,1,...] + + return x, y + + +def batch_add(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x + y # broadcast result shape + + +def batch_mul(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x * y # broadcast result shape + + +def batch_sub(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x - y # broadcast result shape + + +def batch_div(x: Tensor, y: Tensor) -> Tensor: + x, y = common_broadcast(x, y) + return x / y # broadcast result shape diff --git a/cosmos_training/cosmos/utils/functional/lr_scheduler.py b/cosmos_training/cosmos/utils/functional/lr_scheduler.py new file mode 100644 index 00000000..37ad73f8 --- /dev/null +++ b/cosmos_training/cosmos/utils/functional/lr_scheduler.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import numpy as np + +from cosmos.utils import distributed, log + + +class TeroPolyScheduler: + def __init__( + self, + total_Mimg: int, + batch_size: int, + ref_Mimg: Optional[int] = None, + ref_batches: float = 70e3 / 1024, + max_lr_ratio: Optional[float] = 1.0, + min_lr_ratio: Optional[float] = None, + rampup_Mimg: float = 0, + rampdown_Mimg: int = 0, + verbosity_interval: int = 0, + formula: str = "poly", + poly_exp: float = 0.5, + ): + self.total_Mimg = total_Mimg + self.batch_size = batch_size * distributed.get_world_size() + self.ref_Mimg = ref_Mimg or ref_batches * batch_size / 1e6 + self.ref_batches = ref_batches + self.max_lr_ratio = max_lr_ratio + self.min_lr_ratio = min_lr_ratio + self.rampup_Mimg = rampup_Mimg + self.rampdown_Mimg = rampdown_Mimg + self.verbosity_interval = verbosity_interval + self.formula = formula + self.poly_exp = poly_exp + + self._model = None + + @property + def model(self): + return self._model + + @model.setter + def model(self, model): + self._model = model + + def schedule(self, n, **kwargs): + cur_Mimg = getattr(self.model, "sample_counter", 0) / 1e6 + + if self.formula == "constant": + lr = 1.0 + elif self.formula == "poly": + lr = max(cur_Mimg / self.ref_Mimg, 1e-8) ** -self.poly_exp + else: + raise ValueError(f'Invalid learning rate formula "{self.formula}"') + + if self.max_lr_ratio is not None: + lr = min(lr, self.max_lr_ratio) + if self.min_lr_ratio is not None: + lr = max(lr, self.min_lr_ratio) + + if self.rampup_Mimg > 0 and cur_Mimg < self.rampup_Mimg: + lr *= cur_Mimg / self.rampup_Mimg + if self.rampdown_Mimg > 0 and cur_Mimg > self.total_Mimg - self.rampdown_Mimg: + lr *= (self.total_Mimg - cur_Mimg) / self.rampdown_Mimg + + return lr + + def __call__(self, n, **kwargs): + return self.schedule(n, **kwargs) + + +class LambdaWarmUpCosineScheduler: + """ + A learning rate scheduler that combines warm-up with a cosine decay schedule for multiple cycles. + It supports different configurations for each cycle, including the number of warm-up steps, minimum + and maximum scaling factors for the learning rate. + + The scheduler is intended to be used with a base learning rate of 1.0, where the actual learning + rate at any step is the base learning rate multiplied by the scaling factor computed by the scheduler. + + Parameters: + warm_up_steps (list[int]): List of integers where each element represents the number of warm-up + steps for the corresponding cycle. + f_min (list[float]): List of the minimum scaling factors for each cycle after warm-up. + f_max (list[float]): List of the maximum scaling factors at the start and end of each cosine cycle. + f_start (list[float]): List of starting scaling factors for each warm-up phase. + cycle_lengths (list[int]): List of the total lengths of each cycle, including warm-up steps. + verbosity_interval (int, optional): Interval of training steps at which to print current step and + scaling factor information. Set to 0 by default to disable verbosity. + + Examples: + >>> scheduler = LambdaWarmUpCosineScheduler2( + warm_up_steps=[10, 10], + f_min=[0.1, 0.1], + f_max=[1.0, 1.0], + f_start=[0.01, 0.01], + cycle_lengths=[50, 50], + verbosity_interval=10) + >>> for step in range(100): + >>> lr_multiplier = scheduler(step) + >>> print(f"Step {step}: LR Multiplier = {lr_multiplier}") + """ + + def __init__(self, warm_up_steps, f_min, f_max, f_start, cycle_lengths, verbosity_interval=0): + assert len(warm_up_steps) == len(f_min) == len(f_max) == len(f_start) == len(cycle_lengths) + self.lr_warm_up_steps = warm_up_steps + self.f_start = f_start + self.f_min = f_min + self.f_max = f_max + self.cycle_lengths = cycle_lengths + self.cum_cycles = np.cumsum([0] + list(self.cycle_lengths)) + self.last_f = 0.0 + self.verbosity_interval = verbosity_interval + + def find_in_interval(self, n): + interval = 0 + for cl in self.cum_cycles[1:]: + if n <= cl: + return interval + interval += 1 + + def schedule(self, n, **kwargs): + cycle = self.find_in_interval(n) + n = n - self.cum_cycles[cycle] + if self.verbosity_interval > 0: + if n % self.verbosity_interval == 0: + log.info(f"current step: {n}, recent lr-multiplier: {self.last_f}, current cycle {cycle}") + if n < self.lr_warm_up_steps[cycle]: + f = (self.f_max[cycle] - self.f_start[cycle]) / self.lr_warm_up_steps[cycle] * n + self.f_start[cycle] + self.last_f = f + return f + else: + t = (n - self.lr_warm_up_steps[cycle]) / (self.cycle_lengths[cycle] - self.lr_warm_up_steps[cycle]) + t = min(t, 1.0) + f = self.f_min[cycle] + 0.5 * (self.f_max[cycle] - self.f_min[cycle]) * (1 + np.cos(t * np.pi)) + self.last_f = f + return f + + def __call__(self, n, **kwargs): + return self.schedule(n, **kwargs) + + +class LambdaLinearScheduler(LambdaWarmUpCosineScheduler): + """ + Linear instead of cosine decay for the main part of the cycle. + """ + + def schedule(self, n, **kwargs): + cycle = self.find_in_interval(n) + n = n - self.cum_cycles[cycle] + if self.verbosity_interval > 0: + if n % self.verbosity_interval == 0: + log.info(f"current step: {n}, recent lr-multiplier: {self.last_f}, current cycle {cycle}") + + if n < self.lr_warm_up_steps[cycle]: + f = (self.f_max[cycle] - self.f_start[cycle]) / self.lr_warm_up_steps[cycle] * n + self.f_start[cycle] + self.last_f = f + return f + else: + f = self.f_min[cycle] + (self.f_max[cycle] - self.f_min[cycle]) * (self.cycle_lengths[cycle] - n) / ( + self.cycle_lengths[cycle] - self.lr_warm_up_steps[cycle] + ) + self.last_f = f + return f diff --git a/cosmos_training/cosmos/utils/functional/multi_step.py b/cosmos_training/cosmos/utils/functional/multi_step.py new file mode 100644 index 00000000..2da93ed8 --- /dev/null +++ b/cosmos_training/cosmos/utils/functional/multi_step.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Impl of multistep methods to solve the ODE in the diffusion model. +""" + +from typing import Callable, List, Tuple + +import torch + +from cosmos.utils.functional.runge_kutta import reg_x0_euler_step, res_x0_rk2_step + + +def order2_fn( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_s: torch.Tensor, x0_preds: torch.Tensor +) -> Tuple[torch.Tensor, List[torch.Tensor]]: + """ + impl the second order multistep method in https://arxiv.org/pdf/2308.02157 + Adams Bashforth approach! + """ + if x0_preds: + x0_s1, s1 = x0_preds[0] + x_t = res_x0_rk2_step(x_s, t, s, x0_s, s1, x0_s1) + else: + x_t = reg_x0_euler_step(x_s, s, t, x0_s)[0] + return x_t, [(x0_s, s)] + + +# key: method name, value: method function +# key: order + algorithm name +MULTISTEP_FNs = { + "2ab": order2_fn, +} + + +def get_multi_step_fn(name: str) -> Callable: + if name in MULTISTEP_FNs: + return MULTISTEP_FNs[name] + methods = "\n\t".join(MULTISTEP_FNs.keys()) + raise RuntimeError("Only support multistep method\n" + methods) + + +def is_multi_step_fn_supported(name: str) -> bool: + """ + Check if the multistep method is supported. + """ + return name in MULTISTEP_FNs diff --git a/cosmos_training/cosmos/utils/functional/runge_kutta.py b/cosmos_training/cosmos/utils/functional/runge_kutta.py new file mode 100644 index 00000000..5b1719b2 --- /dev/null +++ b/cosmos_training/cosmos/utils/functional/runge_kutta.py @@ -0,0 +1,333 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Tuple + +import torch + +from cosmos.utils.functional.batch_ops import batch_mul + + +def phi1(t: torch.Tensor) -> torch.Tensor: + """ + Compute the first order phi function: (exp(t) - 1) / t. + + Args: + t: Input tensor. + + Returns: + Tensor: Result of phi1 function. + """ + input_dtype = t.dtype + t = t.to(dtype=torch.float64) + return (torch.expm1(t) / t).to(dtype=input_dtype) + + +def phi2(t: torch.Tensor) -> torch.Tensor: + """ + Compute the second order phi function: (phi1(t) - 1) / t. + + Args: + t: Input tensor. + + Returns: + Tensor: Result of phi2 function. + """ + input_dtype = t.dtype + t = t.to(dtype=torch.float64) + return ((phi1(t) - 1.0) / t).to(dtype=input_dtype) + + +def res_x0_rk2_step( + x_s: torch.Tensor, + t: torch.Tensor, + s: torch.Tensor, + x0_s: torch.Tensor, + s1: torch.Tensor, + x0_s1: torch.Tensor, +) -> torch.Tensor: + """ + Perform a residual-based 2nd order Runge-Kutta step. + + Args: + x_s: Current state tensor. + t: Target time tensor. + s: Current time tensor. + x0_s: Prediction at current time. + s1: Intermediate time tensor. + x0_s1: Prediction at intermediate time. + + Returns: + Tensor: Updated state tensor. + + Raises: + AssertionError: If step size is too small. + """ + s = -torch.log(s) # scalar or [B] + t = -torch.log(t) # scalar or [B] + m = -torch.log(s1) # scalar or [B] + + dt = t - s # scalar or [B] + assert not torch.any(torch.isclose(dt, torch.zeros_like(dt), atol=1e-6)), "Step size is too small" + assert not torch.any(torch.isclose(m - s, torch.zeros_like(dt), atol=1e-6)), "Step size is too small" + + c2 = (m - s) / dt # scalar or [B] + phi1_val, phi2_val = phi1(-dt), phi2(-dt) # scalar or [B] each + + # Handle edge case where t = s = m + b1 = torch.nan_to_num(phi1_val - 1.0 / c2 * phi2_val, nan=0.0) # scalar or [B] + b2 = torch.nan_to_num(1.0 / c2 * phi2_val, nan=0.0) # scalar or [B] + + return batch_mul(torch.exp(-dt), x_s) + batch_mul(dt, batch_mul(b1, x0_s) + batch_mul(b2, x0_s1)) # [B,...] + + +def reg_x0_euler_step( + x_s: torch.Tensor, + s: torch.Tensor, + t: torch.Tensor, + x0_s: torch.Tensor, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a regularized Euler step based on x0 prediction. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_s: Prediction at current time. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current prediction. + """ + coef_x0 = (s - t) / s # scalar or [B] + coef_xs = t / s # scalar or [B] + return batch_mul(coef_x0, x0_s) + batch_mul(coef_xs, x_s), x0_s # [B,...], [B,...] + + +def reg_eps_euler_step( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, eps_s: torch.Tensor +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a regularized Euler step based on epsilon prediction. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + eps_s: Epsilon prediction at current time. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current x0 prediction. + """ + return x_s + batch_mul(eps_s, t - s), x_s + batch_mul(eps_s, 0 - s) # [B,...], [B,...] + + +def rk1_euler( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a first-order Runge-Kutta (Euler) step. + + Recommended for diffusion models with guidance or model undertrained + Usually more stable at the cost of a bit slower convergence. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + x0_s = x0_fn(x_s, s) # [B,...] + return reg_x0_euler_step(x_s, s, t, x0_s) # [B,...], [B,...] + + +def rk2_mid_stable( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a stable second-order Runge-Kutta (midpoint) step. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + s1 = torch.sqrt(s * t) # scalar or [B] + x_s1, _ = rk1_euler(x_s, s, s1, x0_fn) # [B,...] + + x0_s1 = x0_fn(x_s1, s1) # [B,...] + return reg_x0_euler_step(x_s, s, t, x0_s1) # [B,...], [B,...] + + +def rk2_mid(x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a second-order Runge-Kutta (midpoint) step. + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and x0 prediction. + """ + s1 = torch.sqrt(s * t) # scalar or [B] + x_s1, x0_s = rk1_euler(x_s, s, s1, x0_fn) # [B,...], [B,...] + + x0_s1 = x0_fn(x_s1, s1) # [B,...] + + return res_x0_rk2_step(x_s, t, s, x0_s, s1, x0_s1), x0_s1 # [B,...], [B,...] + + +def rk_2heun_naive( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive second-order Runge-Kutta (Heun's method) step. + Impl based on rho-rk-deis solvers, https://github.com/qsh-zh/deis + Recommended for diffusion models without guidance and relative large NFE + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + x_t, x0_s = rk1_euler(x_s, s, t, x0_fn) # [B,...], [B,...] + eps_s = batch_mul(1.0 / s, x_t - x0_s) # [B,...] + x0_t = x0_fn(x_t, t) # [B,...] + eps_t = batch_mul(1.0 / t, x_t - x0_t) # [B,...] + + avg_eps = (eps_s + eps_t) / 2 # [B,...] + + return reg_eps_euler_step(x_s, s, t, avg_eps) # [B,...], [B,...] + + +def rk_2heun_edm( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive second-order Runge-Kutta (Heun's method) step. + Impl based no EDM second order Heun method + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + x_t, x0_s = rk1_euler(x_s, s, t, x0_fn) # [B,...], [B,...] + x0_t = x0_fn(x_t, t) # [B,...] + + avg_x0 = (x0_s + x0_t) / 2 # [B,...] + + return reg_x0_euler_step(x_s, s, t, avg_x0) # [B,...], [B,...] + + +def rk_3kutta_naive( + x_s: torch.Tensor, s: torch.Tensor, t: torch.Tensor, x0_fn: Callable +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Perform a naive third-order Runge-Kutta step. + Impl based on rho-rk-deis solvers, https://github.com/qsh-zh/deis + Recommended for diffusion models without guidance and relative large NFE + + Args: + x_s: Current state tensor. + s: Current time tensor. + t: Target time tensor. + x0_fn: Function to compute x0 prediction. + + Returns: + Tuple[Tensor, Tensor]: Updated state tensor and current state. + """ + c2, c3 = 0.5, 1.0 # stage time fractions + a31, a32 = -1.0, 2.0 # Butcher tableau coefficients + b1, b2, b3 = 1.0 / 6, 4.0 / 6, 1.0 / 6 # quadrature weights + + delta = t - s # scalar or [B] + + s1 = c2 * delta + s # scalar or [B] + s2 = c3 * delta + s # scalar or [B] + x_s1, x0_s = rk1_euler(x_s, s, s1, x0_fn) # [B,...], [B,...] + eps_s = batch_mul(1.0 / s, x_s - x0_s) # [B,...] + x0_s1 = x0_fn(x_s1, s1) # [B,...] + eps_s1 = batch_mul(1.0 / s1, x_s1 - x0_s1) # [B,...] + + _eps = a31 * eps_s + a32 * eps_s1 # [B,...] + x_s2, _ = reg_eps_euler_step(x_s, s, s2, _eps) # [B,...] + + x0_s2 = x0_fn(x_s2, s2) # [B,...] + eps_s2 = batch_mul(1.0 / s2, x_s2 - x0_s2) # [B,...] + + avg_eps = b1 * eps_s + b2 * eps_s1 + b3 * eps_s2 # [B,...] + return reg_eps_euler_step(x_s, s, t, avg_eps) # [B,...], [B,...] + + +# key : order + name +RK_FNs = { + "1euler": rk1_euler, + "2mid": rk2_mid, + "2mid_stable": rk2_mid_stable, + "2heun_edm": rk_2heun_edm, + "2heun_naive": rk_2heun_naive, + "3kutta_naive": rk_3kutta_naive, +} + + +def get_runge_kutta_fn(name: str) -> Callable: + """ + Get the specified Runge-Kutta function. + + Args: + name: Name of the Runge-Kutta method. + + Returns: + Callable: The specified Runge-Kutta function. + + Raises: + RuntimeError: If the specified method is not supported. + """ + if name in RK_FNs: + return RK_FNs[name] + methods = "\n\t".join(RK_FNs.keys()) + raise RuntimeError(f"Only support the following Runge-Kutta methods:\n\t{methods}") + + +def is_runge_kutta_fn_supported(name: str) -> bool: + """ + Check if the specified Runge-Kutta function is supported. + + Args: + name: Name of the Runge-Kutta method. + + Returns: + bool: True if the method is supported, False otherwise. + """ + return name in RK_FNs diff --git a/cosmos_training/cosmos/utils/fused_adam.py b/cosmos_training/cosmos/utils/fused_adam.py new file mode 100644 index 00000000..1cc3c31c --- /dev/null +++ b/cosmos_training/cosmos/utils/fused_adam.py @@ -0,0 +1,383 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import transformer_engine as te +import transformer_engine_torch as tex + +from cosmos.utils import distributed, log + + +class FusedAdam(torch.optim.Optimizer): + """Implements Adam algorithm. + + Currently GPU-only. Requires Apex to be installed via + ``pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./``. + + This version of fused Adam implements 2 fusions. + + * Fusion of the Adam update's elementwise operations + * A multi-tensor apply launch that batches the elementwise updates applied to all the model's parameters + into one or a few kernel launches. + + :class:`FusedAdam` may be used as a drop-in replacement for ``torch.optim.AdamW``, + or ``torch.optim.Adam`` with ``adam_w_mode=False``:: + + opt = FusedAdam(model.parameters(), lr = ....) + ... + opt.step() + + .. warning:: + A previous version of :class:`FusedAdam` allowed a number of additional arguments to ``step``. + These additional arguments are now deprecated and unnecessary. + + Adam was been proposed in `Adam: A Method for Stochastic Optimization`_. + + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups. + lr (float, optional): learning rate. (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square. (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability. (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + (default: False) NOT SUPPORTED in FusedAdam! + adam_w_mode (boolean, optional): Apply L2 regularization or weight decay + True for decoupled weight decay(also known as AdamW) (default: True) + capturable (bool, optional): whether to use the version of the optimizer + that can be used with CUDA Graphs. (default: False) + master_weights (bool, optional): whether to maintain FP32 master weights + in the optimizer with FP16 mixed precision training, currently can + only be used with capturable set to True. (default: False) + + .. _Adam - A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__( + self, + params, + lr=1e-3, + bias_correction=True, + betas=(0.9, 0.999), + eps=1e-8, + adam_w_mode=True, + weight_decay=0.0, + amsgrad=False, + capturable=False, + master_weights=False, + ): + if amsgrad: + raise RuntimeError("FusedAdam does not support the AMSGrad variant.") + if master_weights and not capturable: + raise RuntimeError("Master weights is currently only supported with the capturable version.") + # If the optimizer is capturable then LR should be a tensor (on GPU) + log.warning(f"FusedAdam master_weights: {master_weights} capturable: {capturable}") + lr = torch.tensor(lr, dtype=torch.float32) if capturable else lr + defaults = dict(lr=lr, bias_correction=bias_correction, betas=betas, eps=eps, weight_decay=weight_decay) + super(FusedAdam, self).__init__(params, defaults) + self.adam_w_mode = 1 if adam_w_mode else 0 + + self.capturable = capturable + self.master_weights = master_weights + + self.param_groups_master = None + + if capturable: + for idx, group in enumerate(self.param_groups): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + for item in ["lr"]: + if isinstance(group[item], float): + group[item] = torch.tensor(group[item], dtype=torch.float32) + self.param_groups[idx][item] = group[item].to(device=device) + + self._step_supports_amp_scaling = True + + # Skip buffer + self._dummy_overflow_buf = torch.tensor([0], dtype=torch.int, device="cuda") + self.multi_tensor_adam = tex.multi_tensor_adam + self.multi_tensor_adam_capturable = tex.multi_tensor_adam_capturable + self.multi_tensor_adam_capturable_master = tex.multi_tensor_adam_capturable_master + + def step(self, closure=None, grads=None, output_params=None, scale=None, grad_norms=None, grad_scaler=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + + The remaining arguments are deprecated, and are only retained (for the moment) for error-checking purposes. + """ + if any(p is not None for p in [grads, output_params, scale, grad_norms]): + raise RuntimeError( + "FusedAdam has been updated. " + "Simply initialize it identically to torch.optim.Adam, and call step() with no arguments." + ) + loss = None + if closure is not None: + loss = closure() + + if self.param_groups_master is None: + # Create full precision master weights + self.param_groups_master = [] + for i, pg in enumerate(self.param_groups): + param_list = pg["params"] + self.param_groups_master.append( + { + "params": [p.clone().detach().float() if self.master_weights else None for p in param_list], + } + ) + + for group, group_master in zip(self.param_groups, self.param_groups_master): + if len(group["params"]) == 0: + continue + device = group["params"][0].device + bias_correction = 1 if "bias_correction" in group and group["bias_correction"] else 0 + beta1, beta2 = group["betas"] + + # assume same step across group now to simplify things + # per parameter step can be easily support by making it tensor, or pass list into kernel + if "step" in group: + if self.capturable: + group["step"] = ( + group["step"].to(device=device) + if isinstance(group["step"], torch.Tensor) + else torch.tensor(group["step"], dtype=torch.int32, device=device) + ) + group["step"] += (self._dummy_overflow_buf != 1).to(torch.int) + else: + group["step"] += 1 + else: + group["step"] = 1 if not self.capturable else torch.tensor([1], dtype=torch.int, device=device) + + if self.capturable: + group["lr"] = ( + group["lr"].to(device=device) + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32, device=device) + ) + + # create lists for multi-tensor apply + g_16, p_16, m_16, v_16 = [], [], [], [] + g_bf, p_bf, m_bf, v_bf = [], [], [], [] + g_32, p_32, m_32, v_32 = [], [], [], [] + p_16_master = [] + p_32_master = [] + bf16_master = [] + + for p, p_master in zip(group["params"], group_master["params"]): + if p.grad is None: + continue + if p.grad.data.is_sparse: + raise RuntimeError( + "FusedAdam does not support sparse gradients, please consider SparseAdam instead" + ) + + state = self.state[p] + # State initialization + if len(state) == 0: + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like(p.data).float() + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like(p.data).float() + + if p.dtype == torch.float16: + if self.master_weights: + p_16_master.append(p_master.data) + g_16.append(p.grad.data) + p_16.append(p.data) + m_16.append(state["exp_avg"]) + v_16.append(state["exp_avg_sq"]) + elif p.dtype == torch.bfloat16: + if self.master_weights: + bf16_master.append(p_master.data) + g_bf.append(p.grad) + p_bf.append(p) + m_bf.append(state["exp_avg"]) + v_bf.append(state["exp_avg_sq"]) + elif p.dtype == torch.float32: + if self.master_weights: + p_32_master.append(p_master.data) + g_32.append(p.grad.data) + p_32.append(p.data) + m_32.append(state["exp_avg"]) + v_32.append(state["exp_avg_sq"]) + else: + raise RuntimeError("FusedAdam only support fp16 and fp32.") + + # If the optimizer is capturable, then if there's a grad scaler it works + # on the GPU + a different multi_tensor_applier should be called + if self.capturable: + # overflow check of gradients + found_inf = ( + grad_scaler._check_inf_per_device(self)[device] + if grad_scaler is not None + else torch.zeros((1,), device=device) + ) + self._dummy_overflow_buf.copy_(found_inf) + + # get unscale scale factor + scale, inv_scale = None, None + if grad_scaler: + scale = grad_scaler._get_scale_async() + inv_scale = scale.double().reciprocal().float() + else: + scale = torch.ones((1,), device=device, dtype=torch.float32) + inv_scale = torch.ones((1,), device=device, dtype=torch.float32) + + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16, p_16_master] if self.master_weights else [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf, bf16_master] if self.master_weights else [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + ( + self.multi_tensor_adam_capturable_master + if self.master_weights + else self.multi_tensor_adam_capturable + ), + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32, p_32_master] if self.master_weights else [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + inv_scale, + ) + else: + if len(g_16) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_16, p_16, m_16, v_16], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_bf) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_bf, p_bf, m_bf, v_bf], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + if len(g_32) > 0: + te.pytorch.optimizers.multi_tensor_applier( + self.multi_tensor_adam, + self._dummy_overflow_buf, + [g_32, p_32, m_32, v_32], + group["lr"], + beta1, + beta2, + group["eps"], + group["step"], + self.adam_w_mode, + bias_correction, + group["weight_decay"], + ) + + return loss + + def load_state_dict(self, state_dict): + super().load_state_dict(state_dict) + for group in self.param_groups: + if self.capturable: + group["lr"] = ( + group["lr"].cuda() + if isinstance(group["lr"], torch.Tensor) + else torch.tensor(group["lr"], dtype=torch.float32).cuda() + ) + + if "step" in group: + if self.capturable: + if distributed.get_rank() == 0: + step = ( + group["step"].cuda() + if isinstance(group["step"], torch.Tensor) + else torch.tensor([group["step"]], dtype=torch.int32).cuda() + ) + else: + step = torch.zeros(1, dtype=torch.int32).cuda() + # make it compatible with FSDP optimizer + distributed.broadcast(step, 0) + group["step"] = step + elif isinstance(group["step"], torch.Tensor): + group["step"] = group["step"].item() + for p in group["params"]: + state = self.state[p] + if "exp_avg" in state: + state["exp_avg"] = state["exp_avg"].float() + state["exp_avg_sq"] = state["exp_avg_sq"].float() diff --git a/cosmos_training/cosmos/utils/launch.py b/cosmos_training/cosmos/utils/launch.py new file mode 100644 index 00000000..b4115525 --- /dev/null +++ b/cosmos_training/cosmos/utils/launch.py @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import sys +import time + +import torch +from omegaconf import OmegaConf + +from cosmos.utils.config import Config +from cosmos.utils import distributed, log +from cosmos.utils.cluster_env import get_cluster_env +from cosmos.utils.easy_io import easy_io +from cosmos.utils.env_parsers.cred_env_parser import CRED_ENVS +from cosmos.utils.wandb_util import set_wandb_job_info + +# Global variable to track S3 readiness +S3_READY = False + + +def log_reproducible_setup(config: Config, args: argparse.Namespace) -> None: + """ + Configures the environment for reproducibility of experiments by setting up + S3 backends for storage, logging important job details, and saving configuration and + environment details both locally and on S3. + This function is crucial for ensuring that all aspects of the computational environment are captured and can be + replicated for future runs or analysis. + + Parameters: + config (Config): A configuration object containing all the settings necessary + for the job, including paths and credentials. + args (argparse.Namespace): An argparse namespace containing the command line + arguments passed to the script. This includes configurations + and any overrides specified at runtime. + + Actions: + - Sets up S3 backend for storing user data and other outputs. + - Logs job paths and critical information regarding job execution. + - Saves the job configuration locally only for the main node in a distributed setting. + - Captures and logs command-line execution details. + - Optionally reads git commit and branch information if available and logs them. + - Saves both job environment information and launch details locally and syncs these to S3. + - Supports conditional integration with Weights & Biases (wandb) for experiment tracking. + + Notes: + - The function is designed to run within a distributed environment where certain actions + (like saving configurations) are restricted to the main node (rank 0). + - It uses the 'easy_io' module for interacting with S3, ensuring files are written and + read correctly from the object store. + - It leverages OmegaConf for saving YAML configurations + - git information is read from 'git_commit.txt' and 'git_branch.txt' files if they exist. + - snapshot codebase is saved as 'codebase.zip' if it exists in the current directory. + + Raises: + FileNotFoundError: If specific files like 'git_commit.txt' or 'codebase.zip' are expected + but not found. + IOError: If there are issues in file handling operations, particularly with file + reading/writing. + """ + + run_timestamp = f"{time.strftime('%Y-%m-%d_%H-%M-%S')}" + time_tensor = torch.ByteTensor(bytearray(run_timestamp, "utf-8")).cuda() + distributed.broadcast(time_tensor, 0) + run_timestamp = time_tensor.cpu().numpy().tobytes().decode("utf-8") + + global S3_READY + if os.path.exists(config.checkpoint.save_to_object_store.credentials) or CRED_ENVS.APP_ENV in [ + "prod", + "dev", + "stg", + ]: + easy_io.set_s3_backend( + backend_args={ + "backend": "s3", + "path_mapping": { + "s3://timestamps_rundir/": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/", + "s3://rundir/": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/", + }, + "s3_credential_path": config.checkpoint.save_to_object_store.credentials, + } + ) + S3_READY = True + else: + log.warning("S3 credentials not found. Skipping easy_io S3 setup.") + + log.warning(f"Job path: {config.job.path}") + job_info = get_cluster_env() + # save cfg to local + if distributed.get_rank() == 0: + job_local_path = config.job.path_local + log.critical(f"Job local path: {job_local_path}") + os.makedirs(config.job.path_local, exist_ok=True) + launch_info = { + "cmd": " ".join(sys.argv), + "args_cfg_path": args.config, + "args_override": args.opts, + } + + job_info["job_local_path"] = str(job_local_path) + job_info["s3"] = f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/" + # optional read git_commit.txt and save git commit id + if os.path.exists("git_commit.txt"): + with open("git_commit.txt", "r") as f: + job_info["commit_id"] = f.read().strip() + log.critical(f"Commit id: {job_info['commit_id']}") + if os.path.exists("git_branch.txt"): + with open("git_branch.txt", "r") as f: + job_info["git_branch"] = f.read().strip() + log.critical(f"git branch: {job_info['git_branch']}") + if os.path.exists("git_diff.txt"): + with open("git_diff.txt", "r") as f: + job_info["git_diff"] = f.read().strip() + log.critical(f"git diff: {job_info['git_diff']}") + + with open(f"{job_local_path}/job_env.yaml", "w") as f: + OmegaConf.save(job_info, f) + with open(f"{job_local_path}/launch_info.yaml", "w") as f: + OmegaConf.save(launch_info, f) + set_wandb_job_info(job_info) + + # by default, we upload run in ngc and slurm + if config.upload_reproducible_setup: + # sync to s3 + if S3_READY: + log.critical( + f"Uploading reproducible setup to s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/" + ) + + config_pkl_save_fp = f"{config.job.path_local}/config.pkl" + easy_io.copyfile_from_local( + config_pkl_save_fp, f"s3://timestamps_rundir/{config_pkl_save_fp.split('/')[-1]}" + ) + config_yaml_save_fp = config_pkl_save_fp.replace(".pkl", ".yaml") + easy_io.copyfile_from_local( + config_yaml_save_fp, f"s3://timestamps_rundir/{config_yaml_save_fp.split('/')[-1]}" + ) + easy_io.copyfile_from_local(f"{job_local_path}/job_env.yaml", "s3://timestamps_rundir/job_env.yaml") + easy_io.copyfile_from_local( + f"{job_local_path}/launch_info.yaml", + "s3://timestamps_rundir/launch_info.yaml", + ) + if os.path.exists("codebase.zip"): + easy_io.copyfile_from_local("codebase.zip", "s3://timestamps_rundir/codebase.zip") + if os.path.exists("code.tar.gz"): + easy_io.copyfile_from_local("code.tar.gz", "s3://timestamps_rundir/code.tar.gz") + if os.path.exists("git_diff.txt"): + easy_io.copyfile_from_local("git_diff.txt", "s3://timestamps_rundir/git_diff.txt") + if easy_io.exists("s3://rundir/job_history.yaml"): + job_history = easy_io.load("s3://rundir/job_history.yaml") + else: + job_history = {} + job_history[len(job_history)] = { + "timestamp": run_timestamp, + "reproduce_dir": f"s3://{config.checkpoint.save_to_object_store.bucket}/{config.job.path}/job_runs/{run_timestamp}/", + **launch_info, + } + print(job_history) + easy_io.dump(job_history, "s3://rundir/job_history.yaml") + else: + log.warning("S3 credentials not found. Skipping upload of reproducible setup.") + + # save per rank cluster information to s3 + if config.upload_reproducible_setup: + if S3_READY: + easy_io.dump(job_info, f"s3://timestamps_rundir/cluster_env/RANK_{distributed.get_rank():06d}.yaml") diff --git a/cosmos_training/cosmos/utils/lazy_config/__init__.py b/cosmos_training/cosmos/utils/lazy_config/__init__.py new file mode 100644 index 00000000..25e22fd3 --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/__init__.py @@ -0,0 +1,70 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +import os + +from omegaconf import DictConfig, OmegaConf + +from cosmos.utils.flags import TRAINING +from cosmos.utils.lazy_config.instantiate import instantiate +from cosmos.utils.lazy_config.lazy_call import LazyCall +from cosmos.utils.lazy_config.omegaconf_patch import to_object + +OmegaConf.to_object = to_object + +PLACEHOLDER = None + + +class LazyDict(DictConfig): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +__all__ = ["instantiate", "LazyCall", "PLACEHOLDER", "LazyDict"] +if TRAINING: + from cosmos.utils.lazy_config.lazy import LazyConfig + + __all__ += ["LazyConfig"] + + +DOC_BUILDING = os.getenv("_DOC_BUILDING", False) # set in docs/conf.py + + +def fixup_module_metadata(module_name, namespace, keys=None): + """ + Fix the __qualname__ of module members to be their exported api name, so + when they are referenced in docs, sphinx can find them. Reference: + https://github.com/python-trio/trio/blob/6754c74eacfad9cc5c92d5c24727a2f3b620624e/trio/_util.py#L216-L241 + """ + if not DOC_BUILDING: + return + seen_ids = set() + + def fix_one(qualname, name, obj): + # avoid infinite recursion (relevant when using + # typing.Generic, for example) + if id(obj) in seen_ids: + return + seen_ids.add(id(obj)) + + mod = getattr(obj, "__module__", None) + if mod is not None and (mod.startswith(module_name) or mod.startswith("fvcore.")): + obj.__module__ = module_name + # Modules, unlike everything else in Python, put fully-qualitied + # names into their __name__ attribute. We check for "." to avoid + # rewriting these. + if hasattr(obj, "__name__") and "." not in obj.__name__: + obj.__name__ = name + obj.__qualname__ = qualname + if isinstance(obj, type): + for attr_name, attr_value in obj.__dict__.items(): + fix_one(objname + "." + attr_name, attr_name, attr_value) + + if keys is None: + keys = namespace.keys() + for objname in keys: + if not objname.startswith("_"): + obj = namespace[objname] + fix_one(objname, objname, obj) + + +fixup_module_metadata(__name__, globals(), __all__) +del fixup_module_metadata diff --git a/cosmos_training/cosmos/utils/lazy_config/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/lazy_config/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10bbb9e750729cae0805a108d4c69afa7255991e GIT binary patch literal 3013 zcmZ`*U2Ggz6~1?V_TRgX|B^scPirU5CiQOO*fA|pez05D$aPQv6_&s-p1Es}JG-;o zJL_c4BwD0M*-ZnnpruCQ!3887<$=Nr@C1(tUa;b*n^7V~DnH`QiH%x?7tWpCu}w(s zYR|oQ&d)t_?m6F?zqGZ*5RCGsJl7pX=wrSKhTK|g`~w#6BLf-OLODJQ7A^=mp&;f& z%=e-t737@kuO%x~2ijTs!C|jra^+XhSHM6c9mkF9x=@XCq;>-4a@sI*gQ& zyn=I`Al*s2fOg+%iIQY+9!B5jtl)I}zc^~DVLpWu;hGHsUCZB~G(L~Z_}!Csx#7*QRn!8yg5Y?%?tvK6g3!~af8 zNwrI)$m}xJN$_M#qh+@2m=?=g5K~q6FCrVbo+(vm5}aGu#UsuGlnGe7;4$k4@@W;_ zi?r3ltC2lR{eO+5mk->Iq&I+p(lQT1Q7aHt^9RC2XtrZQO zSQdl~(++N*$NMzx1WI}R_@tdPAS=o5>6{-wz?qp!Vj zc5>{sbG|lYOw7+7|3&F%xKNuqc6L<#!dE9I#~|Ha*IT6lEQGizzTfx_kgMpbbXC3@ zf=qy2b1+=N&Xz`y@Yg~$sfKEDEtHoG;Y#G1&p(IZz zDe53;78S$R83>kU&EiEx*NRHsEE-(6kWtPBlCSz^W*w)*Ud?7_OlP({ozd+=wp4Lu z?c#t#O*`w8Y0I9@9v>MV(np4frsp-|6Mp-@V3Mxkre>huc59f&i zF4N}>NWQs5AhkWuW14g^Sl~(S4;R2p-AX)%*Ld@};P86%$Clk=V3k+h6Ju`o6Axe; zq&TEEK*o6h=S$Kh8PYaXj~LPdNVqNP%0ZMcy1pAyxb;iRQ?v)<7Q*>?-KHc%Ircop znRLPvOv4j3!|=j0#L*mwdNJr0)L|DOt9&glJgYIz@wF)T(2JR@*;RDnNlv9iI9M3~ zaR5-V>5u|T$Gk{@Q=Y`r-xHzLcp_Ac7v<)BaPwmRa$d8_#0%%mi>h5DUc7~NzFHX6 z{6&NjQdCWYF^*@vDgpLkG%+s2U+@4}D3=1TEWjHXnxkb-ShlWN>|~~OKpJG(pMe1W zZiY}IS<~McS)5&sKi^1nG$QRc67|Hp=a$4Dy|otE_qSyCqP}!urR#PwQxhBUWbM?l z_{-!6$(8S}DWCrx`@`kmU;dN1cIs=5eFtl!*QYiksD1Y$Tl(T!>ZO&@57}?sU%RU> zk2ZSy7R7fHjlRQ+;##|MFSU26@2AdMs=qOC7rhha|-al&Irk8pAn2;<58Z3yz|)6G8Obw4${ zbOhvjxx%-A{HKckoj{2_jY#rds^gt<^^H%Y4jJ4%h$4xq+eq!Xak+l^cB;1~H9B`c zLZW=Q7H*_^K1#i?ntEZm?@sDKqXS-gwWGf_dNV;Jmj6lWgd!_|`RI|^9pgc)&o*v`sJm7vcLe&Dm zLu%hd&B$;}ejGW`te=x~6t*0U*##i$BF6Y*6#XYU{CD)yeRSeJ8o7^7KIrMKM%Pg1 zgJefFz8Mnmq1wVGg6UBV#n0es_^uegFMaNT6kkl#F0V@as^jY+)V{YyZ&d1)rQVhO jw^K)|iH%SnKKyVJBiyq%`$72U(Up-kbnLFQ*LUo{jGO^H literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/lazy_config/__pycache__/file_io.cpython-312.pyc b/cosmos_training/cosmos/utils/lazy_config/__pycache__/file_io.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b59201e3fb316e8462eb1e52f2c74c1badedc65 GIT binary patch literal 617 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ÐYfF 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 literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/lazy_config/__pycache__/omegaconf_patch.cpython-312.pyc b/cosmos_training/cosmos/utils/lazy_config/__pycache__/omegaconf_patch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9718d695e5246de79127d68fa4e4230106135ea GIT binary patch literal 2479 zcma)8PiP!f7@yhye^Oh@q6U$?q-e5r%~DGhgGORorC3vGB9tvY?Jndh=ND(xA{VC?3>mrGfv(uvlEEmtK^njM5!)<&55LChrY1*gJDSWSd$ik0kJl`HN88K~ z=hOCf(j90#v!ktPQpt_VW@*?<+Z_ZC|FSZoLCtSYrK`iJEX(Of53tVB!eJzTVX&y0K#zz2Zd;y-bj6`2q_X;13dkgOd$zCm0- zZ!KbT=B6MuBI$h&1MnpEx+J|K31Z?&j>S?spiUURWG(`lpb%4m9w5>&z>x}tEYe_b z3SyB&?tmwJj&8`FAt%ugXsqEyWOaZz#x_NKVPH6ZgfN1Hd?`;UM}#GK7FdxMA-Qkj zl?J>C$OQC^0cAYF@C>zW;NY34a}tGlk_cC^0O500Hb_2#Mgs;%&@h%Hc|g=vB;VAiQG zA!(3aoJqpQq6-zOSRDDJ63m#qC|_Mxw=AlXB-J3O;!1<4oOU_xt}+iA1PP7TV)cn| z#Pff#OYMj)W%C9ay`ix7m_*)&(Kv#Bf};ju$S2>TNqnZ1MPd>^&R#RzJ|=?{nC0c= z2%NXaLT2Im+*JtXmdKpCt1#Cfb8TtxF37f;RB70m`=mZHRLK}c=mJ`uE~Rn!v_dDO zD%u{iOyxAR;_`_Gd96WCUU@n@uFtD;^W*0tMPX^^+4_6+Jg;Jk4vQGg%o#vQN4>=T_I&S60{0t*oD|>yZ&cJy+z>S53!9Ezq>ZRg?Xs;k_fZz3CVCYSX{Xy!2q^#O}K6-FzC0?u zg=G2;EOUZGMTdAFtNpC5>;Do>KmLnW|67~+LtFe^du6|HTrb=>xvydK<*8e>TUYO# ky#Mm+yR&cHt=%<$oc`&|y<_*p{n?AVFRUG?O_8Gi0E=MkrT_o{ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/lazy_config/__pycache__/registry.cpython-312.pyc b/cosmos_training/cosmos/utils/lazy_config/__pycache__/registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c5ec7f37b18265743a7dfe672c1ccf7db85fb88 GIT binary patch literal 2418 zcmZuyO>7%Q6rS;((9}J;K2yJ(Q(Ll{j%gT1b$paN*6`n^4F|JMYc= zo0&K7d;5J?R~*48@9@R>1VX>EML4()v;8J8>qtWyHc_rkaSp>eVn&uoj$kArjcD9L zB*y`VG~VQwqPZwWI;TY!@M;%AIZlh|L=$wOz-i*!Tu#)*TwIIm0>}u+5+F-d@KL14 zCJ=>~=g>PN%V>q1L(6zF`5QaT=}GL7S*zkjOvf&Fva8Sq9f&PEE;XzL zFci+}3kIwzGzzXBcD*Ro-7>XQ%wly0(-HW#e*oz^a`$vqxbBrm+ltP+%qAGcFfE|~ zXcZM%od1j~omljVgY`9B#YN#iPOz*+Kps5Hy}FMf;SlLS%_UMJ)-Y%fj)#iLdtFSGmw4%+Rywl`KT$wMcOwwA!atPUEWl2nxMw(P}if z4fz#d7C-eDCJkZY41?Q=NW47*7l44aP`+QMXU1`~H7r{rCmu#(U>av{A zgIWtj7s`rhDNBJcaQ6gRc675K%ey%98+-cGlU-hHQE?R4rC!XioDu{D#!*ehaVUF7 zFTNj~1!za~lCr!X-{YNCBFh^bp1SF{a_vP$$4CzeV!b)Tcurjr7VF&R-! z$BVX4?FnHhFWI?ANujRe;n%!qiRuMo#S>MymZ1Tn99BOp6Kw3hmo>|PAU#`fma}Y^ zf^8bMlT~eJ2}p_BEKlKDKCd{s>{7+B0D;-E18L7z;Lz-IJ5I7)(k;g>Q&kTodmf#z zW!EsBtf{P4WPo?USjf^2k~5_W<>7$AbQBopcbMvEryB{W#)an8&DiLt4}F&XB>TnK zS5se3-5h`VPIv0v;>BWv+)DSaA6Yxnh~64Jv~I4M%~v;%ZJzlo_epN^RIBgUMd41m zcm43%VKC_HUq8Nfydn5W)IYL5vo^Cyw)!UOr*3!kHjjOvZcJ|tKD5>K@SiBgryH0B z9$p*XnBJt9acg91EBy$>7#eLZZYZ0=#%im7sv-IjPE6lU4K$6dl(ac}BlWqFw)QU3ZkhS}p-=^WhIHo)WGovqmEY9QVl)qQM2SE{F>jYl-X+ItPq7XjqmYcY?i zI6&ZDzX1de)QBsFAp!9S(8^yTS2}&LO5TcSxJKS2%Xkl^(baDP_?!wFdH}C>mj*V( zVLrbC$SXTgRZMDFrLrrv`#fOrjg`zkh-#T&8J4^(*Yf%4U<0SDE=r0cjkiINamwdA zTmZ)adgGm@a8@y(VLZ2@>Lu5(VaI}GxDtTGvR&!CE*0#urDcM!rzy)O{BB}$ySJ>4 z&9&{c3OtFSDyCVH+N$tAyLfDp_xMspv(*lW7b_Z7GC}3jaR^9RKb+*K1jHlIj{>QO zb%%kTfs@`3OyC_%&%?@5VG7WY=y`Yg;&jv9iXX1e`2rFW^*8)TOqBkLqOR#b1k^XO zG4cMRjk!CC?)u5btOU>E zU8f)R`6&7bx+2bsN518FU?-Ebo1JO`CNJt%O7LQo{W&2!G9*Hly}`oks!jEb zYA-F>RwhJt`yVatrQ9R%z5&d=L#zuN_>&JV{yEx)4N#DdU16sErpM`HAY*mstilB6 z#&}yoIQ271{(=U5j$qD@qhx<;Alr&RR*(4{kHufO>>V!Aj5n$`xclla`0-IZzDaxp zbFml&_5hEcDaK++JoWS-JcOCZM{V@3iy2{Ux Iukj4|2UrzkW&i*H literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/lazy_config/file_io.py b/cosmos_training/cosmos/utils/lazy_config/file_io.py new file mode 100644 index 00000000..0c6693f4 --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/file_io.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from iopath.common.file_io import HTTPURLHandler, OneDrivePathHandler, PathHandler +from iopath.common.file_io import PathManager as PathManagerBase + +__all__ = ["PathManager", "PathHandler"] + + +PathManager = PathManagerBase() +PathManager.register_handler(HTTPURLHandler()) +PathManager.register_handler(OneDrivePathHandler()) diff --git a/cosmos_training/cosmos/utils/lazy_config/instantiate.py b/cosmos_training/cosmos/utils/lazy_config/instantiate.py new file mode 100644 index 00000000..243b26ac --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/instantiate.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections.abc as abc +import dataclasses +import logging +from typing import Any + +import attrs + +from cosmos.utils.lazy_config.registry import _convert_target_to_string, locate + +__all__ = ["dump_dataclass", "instantiate"] + + +def is_dataclass_or_attrs(target): + return dataclasses.is_dataclass(target) or attrs.has(target) + + +def dump_dataclass(obj: Any): + """ + Dump a dataclass recursively into a dict that can be later instantiated. + + Args: + obj: a dataclass object + + Returns: + dict + """ + assert dataclasses.is_dataclass(obj) and not isinstance(obj, type), ( + "dump_dataclass() requires an instance of a dataclass." + ) + ret = {"_target_": _convert_target_to_string(type(obj))} + for f in dataclasses.fields(obj): + v = getattr(obj, f.name) + if dataclasses.is_dataclass(v): + v = dump_dataclass(v) + if isinstance(v, (list, tuple)): + v = [dump_dataclass(x) if dataclasses.is_dataclass(x) else x for x in v] + ret[f.name] = v + return ret + + +def instantiate(cfg, *args, **kwargs): + """ + Recursively instantiate objects defined in dictionaries by + "_target_" and arguments. + + Args: + cfg: a dict-like object with "_target_" that defines the caller, and + other keys that define the arguments + args: Optional positional parameters pass-through. + kwargs: Optional named parameters pass-through. + + Returns: + object instantiated by cfg + """ + from omegaconf import DictConfig, ListConfig, OmegaConf + + if isinstance(cfg, ListConfig): + lst = [instantiate(x) for x in cfg] + return ListConfig(lst, flags={"allow_objects": True}) + if isinstance(cfg, list): + # Specialize for list, because many classes take + # list[objects] as arguments, such as ResNet, DatasetMapper + return [instantiate(x) for x in cfg] + + # If input is a DictConfig backed by dataclasses (i.e. omegaconf's structured config), + # instantiate it to the actual dataclass. + if isinstance(cfg, DictConfig) and is_dataclass_or_attrs(cfg._metadata.object_type): + return OmegaConf.to_object(cfg) + + if isinstance(cfg, abc.Mapping) and "_target_" in cfg: + # conceptually equivalent to hydra.utils.instantiate(cfg) with _convert_=all, + # but faster: https://github.com/facebookresearch/hydra/issues/1200 + is_recursive = getattr(cfg, "_recursive_", True) + if is_recursive: + cfg = {k: instantiate(v) for k, v in cfg.items()} + else: + cfg = {k: v for k, v in cfg.items()} + # pop the _recursive_ key to avoid passing it as a parameter + if "_recursive_" in cfg: + cfg.pop("_recursive_") + cls = cfg.pop("_target_") + cls = instantiate(cls) + + if isinstance(cls, str): + cls_name = cls + cls = locate(cls_name) + assert cls is not None, cls_name + else: + try: + cls_name = cls.__module__ + "." + cls.__qualname__ + except Exception: + # target could be anything, so the above could fail + cls_name = str(cls) + assert callable(cls), f"_target_ {cls} does not define a callable object" + try: + # override config with kwargs + instantiate_kwargs = {} + instantiate_kwargs.update(cfg) + instantiate_kwargs.update(kwargs) + return cls(*args, **instantiate_kwargs) + except TypeError: + logger = logging.getLogger(__name__) + logger.error(f"Error when instantiating {cls_name}!") + raise + return cfg # return as-is if don't know what to do diff --git a/cosmos_training/cosmos/utils/lazy_config/lazy.py b/cosmos_training/cosmos/utils/lazy_config/lazy.py new file mode 100644 index 00000000..6a9fc473 --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/lazy.py @@ -0,0 +1,377 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ast +import builtins +import importlib +import inspect +import logging +import os +import pickle +import uuid +from collections import OrderedDict +from contextlib import contextmanager +from copy import deepcopy +from typing import Any, Dict, List, Tuple, Union + +import attrs +import yaml +from omegaconf import DictConfig, ListConfig, OmegaConf + +try: + import dill as dill_pickle +except ImportError: + dill_pickle = None + +try: + import cloudpickle +except ImportError: + cloudpickle = None + +from cosmos.utils.lazy_config.file_io import PathManager +from cosmos.utils.lazy_config.lazy_call import LazyCall, get_default_params + +__all__ = ["LazyCall", "LazyConfig"] + + +def sort_dict(d: Dict[str, Any]) -> OrderedDict[str, Any]: + return OrderedDict(sorted(d.items(), key=lambda x: x[0])) + + +def dict_representer(dumper: yaml.Dumper, data: OrderedDict[str, Any]) -> yaml.nodes.MappingNode: + return dumper.represent_mapping("tag:yaml.org,2002:map", data.items()) + + +def sort_recursive(obj: Union[Dict[str, Any], List[Any], Any]) -> Union[OrderedDict[str, Any], List[Any], Any]: + if isinstance(obj, dict): + return sort_dict({k: sort_recursive(v) for k, v in obj.items()}) + elif isinstance(obj, list): + return [sort_recursive(item) for item in obj] + return obj + + +yaml.add_representer(OrderedDict, dict_representer) + +OmegaConf.register_new_resolver("add", lambda *vals: sum(vals)) +OmegaConf.register_new_resolver("subtract", lambda *vals: vals[0] - sum(vals[1:])) + + +def _visit_dict_config(cfg, func): + """ + Apply func recursively to all DictConfig in cfg. + """ + if isinstance(cfg, DictConfig): + func(cfg) + for v in cfg.values(): + _visit_dict_config(v, func) + elif isinstance(cfg, ListConfig): + for v in cfg: + _visit_dict_config(v, func) + + +def _validate_py_syntax(filename): + # see also https://github.com/open-mmlab/mmcv/blob/master/mmcv/utils/config.py + with PathManager.open(filename, "r") as f: + content = f.read() + try: + ast.parse(content) + except SyntaxError as e: + raise SyntaxError(f"Config file {filename} has syntax error!") from e + + +def _cast_to_config(obj): + # if given a dict, return DictConfig instead + if isinstance(obj, dict): + return DictConfig(obj, flags={"allow_objects": True}) + return obj + + +_CFG_PACKAGE_NAME = "detectron2._cfg_loader" +""" +A namespace to put all imported config into. +""" + + +def _random_package_name(filename): + # generate a random package name when loading config files + return _CFG_PACKAGE_NAME + str(uuid.uuid4())[:4] + "." + os.path.basename(filename) + + +@contextmanager +def _patch_import(): + """ + Enhance relative import statements in config files, so that they: + 1. locate files purely based on relative location, regardless of packages. + e.g. you can import file without having __init__ + 2. do not cache modules globally; modifications of module states has no side effect + 3. support other storage system through PathManager, so config files can be in the cloud + 4. imported dict are turned into omegaconf.DictConfig automatically + """ + old_import = builtins.__import__ + + def find_relative_file(original_file, relative_import_path, level): + + # if such import should produce `x` as a python module or DictConfig. + # This can be discussed further if needed. + relative_import_err = """ +Relative import of directories is not allowed within config files. +Within a config file, relative import can only import other config files. +""".replace("\n", " ") + if not len(relative_import_path): + raise ImportError(relative_import_err) + + cur_file = os.path.dirname(original_file) + for _ in range(level - 1): + cur_file = os.path.dirname(cur_file) + cur_name = relative_import_path.lstrip(".") + for part in cur_name.split("."): + cur_file = os.path.join(cur_file, part) + if not cur_file.endswith(".py"): + cur_file += ".py" + if not PathManager.isfile(cur_file): + cur_file_no_suffix = cur_file[: -len(".py")] + if PathManager.isdir(cur_file_no_suffix): + raise ImportError(f"Cannot import from {cur_file_no_suffix}." + relative_import_err) + else: + raise ImportError( + f"Cannot import name {relative_import_path} from {original_file}: {cur_file} does not exist." + ) + return cur_file + + def new_import(name, globals=None, locals=None, fromlist=(), level=0): + if ( + # Only deal with relative imports inside config files + level != 0 and globals is not None and (globals.get("__package__", "") or "").startswith(_CFG_PACKAGE_NAME) + ): + cur_file = find_relative_file(globals["__file__"], name, level) + _validate_py_syntax(cur_file) + spec = importlib.machinery.ModuleSpec(_random_package_name(cur_file), None, origin=cur_file) + module = importlib.util.module_from_spec(spec) + module.__file__ = cur_file + with PathManager.open(cur_file) as f: + content = f.read() + exec(compile(content, cur_file, "exec"), module.__dict__) + for name in fromlist: # turn imported dict into DictConfig automatically + val = _cast_to_config(module.__dict__[name]) + module.__dict__[name] = val + return module + return old_import(name, globals, locals, fromlist=fromlist, level=level) + + builtins.__import__ = new_import + yield new_import + builtins.__import__ = old_import + + +class LazyConfig: + """ + Provide methods to save, load, and overrides an omegaconf config object + which may contain definition of lazily-constructed objects. + """ + + @staticmethod + def load_rel(filename: str, keys: Union[None, str, Tuple[str, ...]] = None): + """ + Similar to :meth:`load()`, but load path relative to the caller's + source file. + + This has the same functionality as a relative import, except that this method + accepts filename as a string, so more characters are allowed in the filename. + """ + caller_frame = inspect.stack()[1] + caller_fname = caller_frame[0].f_code.co_filename + assert caller_fname != "", "load_rel Unable to find caller" + caller_dir = os.path.dirname(caller_fname) + filename = os.path.join(caller_dir, filename) + return LazyConfig.load(filename, keys) + + @staticmethod + def load(filename: str, keys: Union[None, str, Tuple[str, ...]] = None): + """ + Load a config file. + + Args: + filename: absolute path or relative path w.r.t. the current working directory + keys: keys to load and return. If not given, return all keys + (whose values are config objects) in a dict. + """ + has_keys = keys is not None + filename = filename.replace("/./", "/") # redundant + if os.path.splitext(filename)[1] not in [".py", ".yaml", ".yml"]: + raise ValueError(f"Config file {filename} has to be a python or yaml file.") + if filename.endswith(".py"): + _validate_py_syntax(filename) + + with _patch_import(): + # Record the filename + module_namespace = { + "__file__": filename, + "__package__": _random_package_name(filename), + } + with PathManager.open(filename) as f: + content = f.read() + # Compile first with filename to: + # 1. make filename appears in stacktrace + # 2. make load_rel able to find its parent's (possibly remote) location + exec(compile(content, filename, "exec"), module_namespace) + + ret = module_namespace + else: + with PathManager.open(filename) as f: + obj = yaml.unsafe_load(f) + ret = OmegaConf.create(obj, flags={"allow_objects": True}) + + if has_keys: + if isinstance(keys, str): + return _cast_to_config(ret[keys]) + else: + return tuple(_cast_to_config(ret[a]) for a in keys) + else: + if filename.endswith(".py"): + # when not specified, only load those that are config objects + ret = DictConfig( + { + name: _cast_to_config(value) + for name, value in ret.items() + if isinstance(value, (DictConfig, ListConfig, dict)) and not name.startswith("_") + }, + flags={"allow_objects": True}, + ) + return ret + + @staticmethod + def save_pkl(cfg, filename: str) -> str: + """ + Saves a Config object to a file using pickle serialization. This method is typically used + when the configuration object contains complex objects, such as lambdas, that are not supported by + simpler serialization methods like YAML. The function attempts to create a deep copy of the configuration + object before serialization to ensure that the original object remains unmodified. + + Args: + cfg: A Config object to be serialized and saved. + filename: The path and name of the file where the configuration should be saved. The function + assumes the file extension indicates a pickle format (e.g., .pkl). + + Returns: + str: The filename to which the configuration was saved. This can be used to verify the file location + or log the outcome. + + Notes: + - The function logs a warning if the configuration is successfully saved using pickle. + - If saving fails, an error is logged with the exception details. + """ + logger = logging.getLogger(__name__) + try: + cfg = deepcopy(cfg) + except Exception: + pass + + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(cfg, f) + logger.warning(f"Config is saved using pickle at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}. Trying dill or cloudpickle instead") + if dill_pickle: + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(dill_pickle.dumps(cfg, recurse=True), f) + logger.warning(f"Config is saved using dill at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}.") + if cloudpickle: + try: + with PathManager.open(filename, "wb") as f: + pickle.dump(cloudpickle.dumps(cfg), f) + logger.warning(f"Config is saved using cloudpickle at {filename}.") + except Exception as e: + logger.error(f"Failed to save config to {filename}: {e}.") + else: + logger.error("cloudpickle is not available. Cannot save the config.") + raise e + + return filename + + @staticmethod + def save_yaml(cfg, filename: str) -> str: + """ + Saves a Config object to a file using YAML serialization. This method is beneficial when the configuration object's content needs to be human-readable and easily editable. YAML is suitable for configurations that do not contain complex types like lambdas, which must be handled differently. The function converts unserializable items to strings before saving to ensure compatibility with YAML serialization. + + Args: + cfg: A Config object to be serialized and saved. It handles both DictConfig and ListConfig types. + filename: The path and name of the file where the configuration should be saved. The function does not require a specific file extension but typically uses '.yaml'. + + Returns: + str: The filename to which the configuration was saved. This can be used to verify the file location or log the outcome. + + Notes: + - The function logs a warning if the configuration is successfully saved using YAML. + - If saving fails, an error is logged with the exception details. + """ + logger = logging.getLogger(__name__) + try: + cfg = deepcopy(cfg) + except Exception: + pass + + # Define a function to check if an item is serializable to YAML + def is_serializable(item): + try: + OmegaConf.to_yaml(item) + return True + except Exception as e: + return False + + # Function to convert unserializable items to strings + def serialize_config(config): + if isinstance(config, DictConfig): + for key, value in config.items(): + if isinstance(value, (DictConfig, ListConfig)): + try: + if "_target_" in value: + default_params = get_default_params(value["_target_"]) + for default_key, default_v in default_params.items(): + if default_key not in value: + value[default_key] = default_v + except Exception as e: + logger.error(f"Failed to add default argument values: {e}") + + serialize_config(value) + else: + if not is_serializable(value) and value is not None: + config[key] = str(value) + elif isinstance(config, ListConfig): + for i, item in enumerate(config): + if isinstance(item, (DictConfig, ListConfig)): + serialize_config(item) + else: + if not is_serializable(item) and item is not None: + config[i] = str(item) + else: + raise NotImplementedError("Input config must be a DictConfig or ListConfig.") + return config + + # Convert Config object to a DictConfig object. + config_dict = attrs.asdict(cfg) + config_omegaconf = DictConfig(content=config_dict, flags={"allow_objects": True}) + + # Serialize the DictConfig object by converting non-serializable objects to strings. + config_omegaconf = serialize_config(config_omegaconf) + + config_dict: Dict[str, Any] = OmegaConf.to_container(config_omegaconf, resolve=True) + sorted_config: OrderedDict[str, Any] = sort_recursive(config_dict) + with open(filename, "w") as f: + yaml.dump(sorted_config, f, default_flow_style=False) + logger.warning(f"Config is saved using omegaconf at {filename}.") + return filename diff --git a/cosmos_training/cosmos/utils/lazy_config/lazy_call.py b/cosmos_training/cosmos/utils/lazy_config/lazy_call.py new file mode 100644 index 00000000..c3a15f35 --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/lazy_call.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections.abc as abc +import inspect +from dataclasses import is_dataclass +from typing import ClassVar + +import attrs +from omegaconf import DictConfig + +from cosmos.utils.lazy_config.registry import convert_target_to_string + +__all__ = ["LazyCall"] + + +def get_default_params(cls_or_func): + if callable(cls_or_func): + # inspect signature for function + signature = inspect.signature(cls_or_func) + else: + # inspect signature for class + signature = inspect.signature(cls_or_func.__init__) + params = signature.parameters + default_params = { + name: param.default for name, param in params.items() if param.default is not inspect.Parameter.empty + } + return default_params + + +_CONVERT_TARGET_TO_STRING: ClassVar[bool] = False +"""Used by tests to enforce conversion of target to string.""" + + +class LazyCall: + """ + Wrap a callable so that when it's called, the call will not be executed, + but returns a dict that describes the call. + + LazyCall object has to be called with only keyword arguments. Positional + arguments are not yet supported. + + Examples: + :: + from detectron2.config import instantiate, LazyCall + + layer_cfg = LazyCall(nn.Conv2d)(in_channels=32, out_channels=32) + layer_cfg.out_channels = 64 # can edit it afterwards + layer = instantiate(layer_cfg) + """ + + def __init__(self, target): + if not (callable(target) or isinstance(target, (str, abc.Mapping))): + raise TypeError(f"target of LazyCall must be a callable or defines a callable! Got {target}") + self._target = target + + def __call__(self, **kwargs): + if _CONVERT_TARGET_TO_STRING or is_dataclass(self._target) or attrs.has(self._target): + # omegaconf object cannot hold dataclass type + # https://github.com/omry/omegaconf/issues/784 + target = convert_target_to_string(self._target) + else: + target = self._target + kwargs["_target_"] = target + + _final_params = get_default_params(self._target) + _final_params.update(kwargs) + + return DictConfig(content=_final_params, flags={"allow_objects": True}) diff --git a/cosmos_training/cosmos/utils/lazy_config/omegaconf_patch.py b/cosmos_training/cosmos/utils/lazy_config/omegaconf_patch.py new file mode 100644 index 00000000..39dca42a --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/omegaconf_patch.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Dict, List, Union + +from omegaconf import OmegaConf +from omegaconf.base import DictKeyType, SCMode +from omegaconf.dictconfig import DictConfig # pragma: no cover + + +def to_object(cfg: Any) -> Union[Dict[DictKeyType, Any], List[Any], None, str, Any]: + """ + Converts an OmegaConf configuration object to a native Python container (dict or list), unless + the configuration is specifically created by LazyCall, in which case the original configuration + is returned directly. + + This function serves as a modification of the original `to_object` method from OmegaConf, + preventing DictConfig objects created by LazyCall from being automatically converted to Python + dictionaries. This ensures that configurations meant to be lazily evaluated retain their intended + structure and behavior. + + Differences from OmegaConf's original `to_object`: + - Adds a check at the beginning to return the configuration unchanged if it is created by LazyCall. + + Reference: + - Original OmegaConf `to_object` method: https://github.com/omry/omegaconf/blob/master/omegaconf/omegaconf.py#L595 + + Args: + cfg (Any): The OmegaConf configuration object to convert. + + Returns: + Union[Dict[DictKeyType, Any], List[Any], None, str, Any]: The converted Python container if + `cfg` is not a LazyCall created configuration, otherwise the unchanged `cfg`. + + Examples: + >>> cfg = DictConfig({"key": "value", "_target_": "Model"}) + >>> to_object(cfg) + DictConfig({"key": "value", "_target_": "Model"}) + + >>> cfg = DictConfig({"list": [1, 2, 3]}) + >>> to_object(cfg) + {'list': [1, 2, 3]} + """ + if isinstance(cfg, DictConfig) and "_target_" in cfg.keys(): + return cfg + + return OmegaConf.to_container( + cfg=cfg, + resolve=True, + throw_on_missing=True, + enum_to_str=False, + structured_config_mode=SCMode.INSTANTIATE, + ) diff --git a/cosmos_training/cosmos/utils/lazy_config/registry.py b/cosmos_training/cosmos/utils/lazy_config/registry.py new file mode 100644 index 00000000..4621fa82 --- /dev/null +++ b/cosmos_training/cosmos/utils/lazy_config/registry.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import pydoc +from typing import Any + +""" +``Registry`` and `locate` provide ways to map a string (typically found +in config files) to callable objects. +""" + +__all__ = ["locate", "convert_target_to_string"] + +try: + from fvcore.common.registry import Registry # for backward compatibility. + + __all__ += ["Registry"] +except Exception: + pass + + +def convert_target_to_string(t: Any) -> str: + """ + Inverse of ``locate()``. + + Args: + t: any object with ``__module__`` and ``__qualname__`` + """ + if hasattr(t, "__self__") and inspect.isclass(t.__self__): + # classmethod + cls = t.__self__ + module = cls.__module__ + qualname = f"{cls.__name__}.{t.__name__}" + else: + module = t.__module__ + qualname = t.__qualname__ + + # Compress the path to this object, e.g. ``module.submodule._impl.class`` + # may become ``module.submodule.class``, if the later also resolves to the same + # object. This simplifies the string, and also is less affected by moving the + # class implementation. + module_parts = module.split(".") + for k in range(1, len(module_parts)): + prefix = ".".join(module_parts[:k]) + candidate = f"{prefix}.{qualname}" + try: + if locate(candidate) is t: + return candidate + except ImportError: + pass + return f"{module}.{qualname}" + + +_convert_target_to_string = convert_target_to_string # for backward compatibility. + + +def locate(name: str) -> Any: + """ + Locate and return an object ``x`` using an input string ``{x.__module__}.{x.__qualname__}``, + such as "module.submodule.class_name". + + Raise Exception if it cannot be found. + """ + obj = pydoc.locate(name) + + # Some cases (e.g. torch.optim.sgd.SGD) not handled correctly + # by pydoc.locate. Try a private function from hydra. + if obj is None: + try: + # from hydra.utils import get_method - will print many errors + + from hydra.utils import _locate + except ImportError as e: + raise ImportError(f"Cannot dynamically locate object {name}!") from e + else: + obj = _locate(name) # it raises if fails + + return obj diff --git a/cosmos_training/cosmos/utils/log.py b/cosmos_training/cosmos/utils/log.py new file mode 100644 index 00000000..45545d38 --- /dev/null +++ b/cosmos_training/cosmos/utils/log.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +import os +import sys +from typing import Any + +import torch.distributed as dist +from loguru._logger import Core, Logger + +RANK0_ONLY = True +LEVEL = os.environ.get("LOGURU_LEVEL", "INFO") +RANK = int(os.environ.get("RANK", "0")) + + +def make_new_logger(depth: int = 1) -> Logger: + return Logger( + core=Core(), + exception=None, + depth=depth, + record=False, + lazy=False, + colors=False, + raw=False, + capture=True, + patchers=[], + extra={}, + ) + + +logger = make_new_logger(depth=1) +atexit.register(logger.remove) + + +def _add_relative_path(record: dict[str, Any]) -> None: + try: + start = os.getcwd() + record["extra"]["relative_path"] = os.path.relpath(record["file"].path, start) + except OSError: + # CWD may have been removed (e.g. on some ranks in distributed jobs). + # Fall back to the absolute path so logging still works. + record["extra"]["relative_path"] = f":{record['file'].path}" + + +*options, _, extra = logger._options # type: ignore +logger._options = tuple([*options, [_add_relative_path], extra]) # type: ignore + + +def init_loguru_stdout() -> None: + logger.remove() + datetime_format = get_datetime_format() + machine_format = get_machine_format() + message_format = get_message_format() + logger.add( + sys.stdout, + level=LEVEL, + format=f"{datetime_format}{machine_format}{message_format}", + filter=_rank0_only_filter, + ) + + +def init_loguru_file(path: str) -> None: + datetime_format = get_datetime_format() + machine_format = get_machine_format() + message_format = get_message_format() + logger.add( + path, + encoding="utf8", + level=LEVEL, + format=f"{datetime_format}{machine_format}{message_format}", + rotation="100 MB", + filter=lambda result: _rank0_only_filter(result) or not RANK0_ONLY, + enqueue=True, + ) + + +def get_datetime_format() -> str: + return "[{time:MM-DD HH:mm:ss}|" + + +def get_machine_format() -> str: + node_id = os.environ.get("NGC_ARRAY_INDEX", "0") + num_nodes = int(os.environ.get("NGC_ARRAY_SIZE", "1")) + machine_format = "" + rank = 0 + if dist.is_available(): + if not RANK0_ONLY and dist.is_initialized(): + rank = dist.get_rank() + world_size = dist.get_world_size() + machine_format = ( + f"[Node{node_id:<3}/{num_nodes:<3}][RANK{rank:<5}/{world_size:<5}]" + "[{process.name:<8}]| " + ) + return machine_format + + +def get_message_format() -> str: + message_format = "{level}|{extra[relative_path]}:{line}:{function}] {message}" + return message_format + + +def _rank0_only_filter(record: Any) -> bool: + is_rank0 = record["extra"].get("rank0_only", True) + if RANK == 0 and is_rank0: + return True + if not is_rank0: + record["message"] = f"[RANK {RANK}] " + record["message"] + return not is_rank0 + + +def trace(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).trace(message) + + +def debug(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).debug(message) + + +def info(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).info(message) + + +def success(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).success(message) + + +def warning(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).warning(message) + + +def error(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).error(message) + + +def critical(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).critical(message) + + +def exception(message: str, rank0_only: bool = True) -> None: + logger.opt(depth=1).bind(rank0_only=rank0_only).exception(message) + + +# Execute at import time. +init_loguru_stdout() diff --git a/cosmos_training/cosmos/utils/misc.py b/cosmos_training/cosmos/utils/misc.py new file mode 100644 index 00000000..43429e3c --- /dev/null +++ b/cosmos_training/cosmos/utils/misc.py @@ -0,0 +1,689 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +import collections.abc +import functools +import json +import os +import random +from contextlib import ContextDecorator, nullcontext +from dataclasses import fields +from typing import Any, Callable, List, Tuple, TypeVar, Union + +import numpy as np +from loguru import logger as logging + +try: + # pyrefly: ignore # import-error + import straggler +except ImportError: + straggler = None +import termcolor +import torch +from torch.distributed._functional_collectives import AsyncCollectiveTensor +from torch.distributed._tensor.api import DTensor + +from cosmos.utils import distributed, log +from cosmos.utils.distributed import all_gather_tensor +from cosmos.utils.easy_io import easy_io +from cosmos.utils.timer import Timer + + +def requires_grad(model: torch.nn.Module, value: bool = True) -> None: + """Set a model to require gradients or not. + + Args: + model (torch.nn.Module): Neural network model. + value (bool): Whether the network requires gradients or not. + """ + for p in model.parameters(): + p.requires_grad = value + + +def to( + data: Any, + device: str | torch.device | None = None, + dtype: torch.dtype | None = None, + memory_format: torch.memory_format = torch.preserve_format, +) -> Any: + """Recursively cast data into the specified device, dtype, and/or memory_format. + + The input data can be a tensor, a list of tensors, a dict of tensors. + See the documentation for torch.Tensor.to() for details. + + Args: + data (Any): Input data. + device (str | torch.device): GPU device (default: None). + dtype (torch.dtype): data type (default: None). + memory_format (torch.memory_format): memory organization format (default: torch.preserve_format). + + Returns: + data (Any): Data cast to the specified device, dtype, and/or memory_format. + """ + assert device is not None or dtype is not None or memory_format is not None, ( + "at least one of device, dtype, memory_format should be specified" + ) + + if isinstance(data, torch.Tensor): + if ( + memory_format == torch.channels_last + and data.dim() != 4 + or memory_format == torch.channels_last_3d + and data.dim() != 5 + ): + memory_format = torch.preserve_format # do not change the memory format + is_cpu = (isinstance(device, str) and device == "cpu") or ( + isinstance(device, torch.device) and device.type == "cpu" + ) + data = data.to( + device=device, + dtype=dtype, + memory_format=memory_format, + non_blocking=(not is_cpu), + ) + return data + elif isinstance(data, collections.abc.Mapping): + return type(data)({key: to(data[key], device=device, dtype=dtype, memory_format=memory_format) for key in data}) + elif isinstance(data, collections.abc.Sequence) and not isinstance(data, (str, bytes)): + return type(data)([to(elem, device=device, dtype=dtype, memory_format=memory_format) for elem in data]) + else: + return data + + +def serialize(data: Any) -> Any: + """Serialize data by hierarchically traversing through iterables. + + Args: + data (Any): Input data. + + Returns: + data (Any): Serialized data. + """ + if isinstance(data, collections.abc.Mapping): + return type(data)({key: serialize(data[key]) for key in data}) + elif isinstance(data, collections.abc.Sequence) and not isinstance(data, (str, bytes)): + return type(data)([serialize(elem) for elem in data]) + else: + try: + json.dumps(data) + except TypeError: + data = str(data) + return data + + +def print_environ_variables(env_vars: list[str]) -> None: + """Print a specific list of environment variables. + + Args: + env_vars (list[str]): List of specified environment variables. + """ + for env_var in env_vars: + if env_var in os.environ: + log.info(f"Environment variable {Color.green(env_var)}: {Color.yellow(os.environ[env_var])}") + else: + log.warning(f"Environment variable {Color.green(env_var)} not set!") + + +def set_random_seed(seed: int, by_rank: bool = False) -> None: + """Set random seed. This includes random, numpy, Pytorch. + + Args: + seed (int): Random seed. + by_rank (bool): if true, each GPU will use a different random seed. + """ + if by_rank: + seed += distributed.get_rank() + log.info(f"Using random seed {seed}.") + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) # sets seed on the current CPU & all GPUs + + +def arch_invariant_rand( + shape: List[int] | Tuple[int], dtype: torch.dtype, device: str | torch.device, seed: int | None = None +): + """Produce a GPU-architecture-invariant randomized Torch tensor. + + Args: + shape (list or tuple of ints): Output tensor shape. + dtype (torch.dtype): Output tensor type. + device (torch.device): Device holding the output. + seed (int): Optional randomization seed. + + Returns: + tensor (torch.tensor): Randomly-generated tensor. + """ + # Create a random number generator, optionally seeded + rng = np.random.RandomState(seed) + + # Generate random numbers using the generator + random_array = rng.standard_normal(shape).astype(np.float32) # Use standard_normal for normal distribution + + # Convert to torch tensor and return + return torch.from_numpy(random_array).to(dtype=dtype, device=device) + + +def get_data_batch_size(data: dict[str, torch.Tensor] | torch.Tensor) -> int: + """Get the batch size from a data batch, a (possibly hierarchical) dictionary of tensors. + + Args: + data (dict[str, torch.Tensor]): Data batch (dictionary of tensors). + + Returns: + batch_size (int): Data batch size. + """ + + def _get_batch_size(input_data: Any) -> Union[int, None]: + """ + Helper function that recursively finds a tensor in the input data + (could be a nested dictionary or list of tensors) and returns its batch size. + """ + if isinstance(input_data, torch.Tensor): + return len(input_data) + elif isinstance(input_data, collections.abc.Mapping): + for key, value in input_data.items(): + batch_size = _get_batch_size(value) + if batch_size is not None: + return batch_size + elif isinstance(input_data, (list, tuple)) and len(input_data) > 0: + # Handle list/tuple of tensors (variable-length batches) + # The batch size is the length of the list + # We are verifying if input_data[0] is indeed a tensor. If so, return the length of the list. + if isinstance(input_data[0], torch.Tensor): + return len(input_data) + # Recurse into first element if it's a nested structure + return _get_batch_size(input_data[0]) + return None + + batch_size = _get_batch_size(data) + if not isinstance(batch_size, int): + raise ValueError(f"Batch size ({batch_size}) obtained from invalid data: {data}") + return batch_size + + +def parameters_to_buffer(module: torch.nn.Module, persistent: bool = True): + """Convert parameters in a module to buffers. + Buffers do not have its own gradients and thus not updated by backpropagation. + + Args: + module (torch.nn.Module): a module to convert parameters + persistent (bool): If True, buffers are included in state_dict. + """ + named_params = dict() + + for name, param in module.named_parameters(): + named_params[name] = param + + for name, param in named_params.items(): + module_hierarchy = name.split(".") + submodule_name = ".".join(module_hierarchy[:-1]) + submodule = module.get_submodule(submodule_name) + subname = module_hierarchy[-1] + delattr(submodule, subname) + submodule.register_buffer(subname, param, persistent=persistent) + + return + + +T = TypeVar("T", bound=Callable[..., Any]) + + +class timer(Timer): + """Simple CPU timer for timing the execution of code. + + It can be used as either a context manager or a function decorator. The timing result will be logged upon exit. + + Example: + def func_a(): + time.sleep(1) + with timer("func_a"): + func_a() + + @timer("func_b) + def func_b(): + time.sleep(1) + func_b() + """ + + def __init__(self, context: str, debug: bool = False): + super().__init__( + tag=context, + measure_cpu=True, + measure_cuda=False, + unit="s", + debug=debug, + ) + + +class memory_checker(ContextDecorator): # noqa: N801 + """Simple memory checker for a given block of code. + + It can be used as either a context manager or a function decorator. The memory usage will be logged upon exit. + Example: + def func_a(): + torch.rand([int(1024**2)]).float().cuda() + with memory_checker("func_a"): + func_a() + >>> 0.004GB memory used + + @memory_checker("func_b") + def func_b(): + random_var = torch.rand([int(1024**2)]).cuda() + func_b() + """ + + def __init__(self, context: str, debug: bool = False): + self.context = context + self.debug = debug + + def __enter__(self) -> None: + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + self.initial_memory = torch.cuda.max_memory_allocated() + + def __exit__(self, exc_type, exc_value, traceback) -> None: # noqa: ANN001 + torch.cuda.synchronize() + final_memory = torch.cuda.max_memory_allocated() + message = f"Memory used within {self.context}: {(final_memory - self.initial_memory) / 1024**3:.4f} GB" + if self.debug: + log.debug(message) + else: + log.info(message) + + def __call__(self, func: T) -> T: + @functools.wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + initial_memory = torch.cuda.max_memory_allocated() + result = func(*args, **kwargs) + torch.cuda.synchronize() + final_memory = torch.cuda.max_memory_allocated() + message = f"Memory used within {self.context}: {(final_memory - initial_memory) / 1024**3:.4f} GB" + if self.debug: + log.debug(message) + else: + log.info(message) + return result + + return wrapper # type: ignore + + +class TrainingTimer: + """Timer for timing the execution of code, aggregating over multiple training iterations. + + It is used as a context manager to measure the execution time of code and store the timing results + for each function. The context managers can be nested. + + Attributes: + results (dict): A dictionary to store timing results for various code. + + Example: + timer = Timer() + for i in range(100): + with timer("func_a"): + func_a() + avg_time = sum(timer.results["func_a"]) / len(timer.results["func_a"]) + print(f"func_a() took {avg_time} seconds.") + """ + + def __init__(self) -> None: + self.results = dict() + self.average_results = dict() + self.timers = [] + self.func_stack = [] + self.reset() + + def reset(self) -> None: + self.results = {key: [] for key in self.results} + + def __enter__(self) -> TrainingTimer: + timer = Timer(measure_cpu=True, measure_cuda=False, debug=True, unit="s") + self.timers.append(timer) + timer.start() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: # noqa: ANN001 + timer = self.timers.pop() + timer.end() + result = timer.get_cpu_time() + key = self.func_stack.pop() + self.results.setdefault(key, []) + self.results[key].append(result) + + def __call__(self, func_name: str) -> TrainingTimer: + self.func_stack.append(func_name) + return self + + def __getattr__(self, func_name: str) -> TrainingTimer: + return self.__call__(func_name) + + def nested(self, func_name: str) -> TrainingTimer: + return self.__call__(func_name) + + def compute_average_results(self) -> dict[str, float]: + results = dict() + for key, value_list in self.results.items(): + results[key] = sum(value_list) / len(value_list) + return results + + +def timeout_handler(timeout_period: float, signum: int, frame: int) -> None: + # What to do when the process gets stuck. For now, we simply end the process. + error_message = f"Timeout error: more than {timeout_period} seconds passed since the last iteration." + if distributed.is_rank0(): + import wandb + + wandb.alert(title="Timeout error!", text=error_message, level=wandb.AlertLevel.ERROR) + raise TimeoutError(error_message) + + +class Color: + """A convenience class to colorize strings in the console. + + Example: + import + print("This is {Color.red('important')}.") + """ + + @staticmethod + def red(x: str) -> str: + return termcolor.colored(str(x), color="red") + + @staticmethod + def green(x: str) -> str: + return termcolor.colored(str(x), color="green") + + @staticmethod + def blue(x: str) -> str: + return termcolor.colored(str(x), color="blue") + + @staticmethod + def cyan(x: str) -> str: + return termcolor.colored(str(x), color="cyan") + + @staticmethod + def yellow(x: str) -> str: + return termcolor.colored(str(x), color="yellow") + + @staticmethod + def magenta(x: str) -> str: + return termcolor.colored(str(x), color="magenta") + + @staticmethod + def grey(x: str) -> str: + return termcolor.colored(str(x), color="grey") + + +class BufferCnt: + """ + Buffer counter which keeps track of the condition when called and returns True when the condition in met "thres" + amount of times, otherwise returns False. + + Example usage: + buf = BufferCnt(thres=3) + for _ in range(5): + if buf(random.random() > 0.5): + print("We got lucky 3 times out of 5.") + + Args: + thres (int): The amount of times the expression needs to be True before returning True. + reset_over_thres (bool): Whether to reset the buffer after returning True. + """ + + def __init__(self, thres=10, reset_over_thres=False): + self._cnt = 0 + self.thres = thres + self.reset_over_thres = reset_over_thres + + def __call__(self, expre, thres=None): + if expre is True: + self._cnt += 1 + else: + self._cnt = 0 + + if thres is None: + thres = self.thres + + if self._cnt >= thres: + if self.reset_over_thres: + self.reset() + return True + + return False + + @property + def cnt(self): + return self._cnt + + def reset(self): + self._cnt = 0 + + +def dataclass_instance_to_dict(dataclass: Any) -> dict: + """Convert a dataclass to a dictionary. + + Args: + dataclass (Any): Dataclass object. + + Returns: + dict: Dictionary representation of the dataclass. + """ + return {f.name: getattr(dataclass, f.name) for f in fields(dataclass)} + + +def get_local_tensor_if_DTensor(tensor: torch.Tensor | DTensor) -> torch.tensor: + if isinstance(tensor, DTensor): + local = tensor.to_local() + # As per PyTorch documentation, if the communication is not finished yet, we need to wait for it to finish + # https://pytorch.org/docs/stable/distributed.tensor.html#torch.distributed.tensor.DTensor.to_local + if isinstance(local, AsyncCollectiveTensor): + return local.wait() + else: + return local + return tensor + + +def set_torch_compile_options(recompile_limit: int = 8, use_duck_shape: bool = True): + """ + Set some of the torch compile config options. The default values of arguments are default config values in PyTorch as of 2.10 version. + The value recompile_limit=32 is useful for Wan Tokenizer encoding compilation, as the standard value of 8 can easily overflow. + The value of use_duck_shape=False is useful for Cosmos3 MoT training to reduce recompilations. + + Args: + recompile_limit (int): Controls the maximum number of cache entries with a guard on same ID_MATCH'd object. + use_duck_shape (bool): This flag changes whether we should use the same symbolic variable to represent input sizes that are the same + """ + try: + # PyTorch >= 2.7 + torch._dynamo.config.recompile_limit = recompile_limit + torch.fx.experimental._config.use_duck_shape = use_duck_shape + except AttributeError: + try: + torch._dynamo.config.cache_size_limit = recompile_limit + torch.fx.experimental._config.use_duck_shape = use_duck_shape + except AttributeError as e: + log.warning("torch.compile is not available due to missing config options.") + raise e + + +class NVTXRangeContext: + """ + Context manager which inserts NVTX range around the current context and optionally calls torch.cuda.synchronize + at the start and the end of the context. + + Args: + name (str): Name of the NVTX range. + enabled (bool): Whether the context manager is enabled. When disabled, it does nothing. Default: True. + synchronize (bool): Whether to call torch.cuda.synchronize() at the start and the end of the context. Default: True. + """ + + def __init__(self, name: str, enabled: bool = True, synchronize: bool = True): + self.name = name + self.enabled = enabled + self.synchronize = synchronize + + def __enter__(self): + if not self.enabled: + return + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_push(self.name) + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.enabled: + return + if self.synchronize: + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + + +class StragglerDetectorV2: + """StragglerDetectorV2 is a class that allows you to easily integrate "straggler" tool: + https://gitlab-master.nvidia.com/dl/gwe/fault_tolerance_related/straggler/-/tree/cupti?ref_type=heads. + + This tool detects stragglers using low-level CUPTI tool, which can gather kernel execution time with very low overhead. + The execution times are compared across different ranks, as well as to the execution time of the exact same kernels in the past. + This tool can be easily integrated, as it's resilient to any synchronizations, since it captures kernels execution time. + It means that we can wrap the entire forward or backward passes and the stragglers will be identified regardless + of synchronizations happening during the iteration. + + Args: + enabled (bool): Whether the straggler detection is enabled. When disabled, it does nothing. Default: True. + report_freq (int): Generate a report each report_freq iterations that analyzes the GPUs performance. Defaults to 100. + profile_freq (int): Enable the CUPTI profiling each profile_freq iterations. Since the overhead is very low, + the default value is 1. + max_diff (float): Defines the maximum relative difference between the fastest and the slowest rank to determine the slowdown. Defaults to 2.0 + raise_error (bool): Whether to raise error when stragglers are detected enough times. Defaults to True.""" + + def __init__( + self, + enabled: bool = True, + report_freq: int = 100, + profile_freq: int = 1, + max_diff: float = 2.0, + raise_error: bool = True, + save_s3: bool = False, + ): + self.enabled = enabled + self.report_freq = report_freq + self.profile_freq = profile_freq + self.name = self.__class__.__name__ + self.slowdown_count = BufferCnt(thres=10, reset_over_thres=True) + self.max_diff = max_diff + self.raise_error = raise_error + self.save_s3 = save_s3 + + def initialize(self): + if self.enabled: + if not straggler: + + raise RuntimeError( + "Please install straggler package before using StragglerDetectionV2." + "Package can be installed from here: https://gitlab-master.nvidia.com/dl/osiris/straggler" + ) + + straggler.Detector.initialize( + scores_to_compute=["relative_perf_scores", "individual_perf_scores"], + gather_on_rank0=False, # all ranks results will be available on rank 0 + profiling_interval=self.profile_freq, + ) + + def profile_section(self, name: str, section_enabled: bool, profile_cuda: bool = True): + if section_enabled and self.enabled: + return straggler.Detector.detection_section(name, profile_cuda=profile_cuda) + else: + return nullcontext() + + def _aggregate_section_results(self, local_section_summaries): + data = [] + for key in local_section_summaries: + # straggler reports time in ms + data.append(local_section_summaries[key][straggler.Statistic.MAX] / 1000) + return distributed.all_gather_tensor(torch.tensor(data).cuda()) + + def generate_report(self, iteration): + if self.enabled and iteration % self.report_freq == 0: + report = straggler.Detector.generate_report() + gpu_relative_perf_score = report.gpu_relative_perf_scores[distributed.get_rank()] + gpu_relative_perf_score_gather_list = distributed.all_gather_tensor( + torch.tensor([gpu_relative_perf_score]).cuda() + ) + local_section_data = self._aggregate_section_results(report.local_section_summaries) + if distributed.get_rank() == 0: + stragglers = report.identify_stragglers(gpu_rel_threshold=1 / self.max_diff) + wandb_info = { + f"{self.name}/relative_gpu_perf_{rank}": perf[0].item() + for rank, perf in enumerate(gpu_relative_perf_score_gather_list) + } + for key_id, key in enumerate(report.local_section_summaries): + wandb_info.update( + {f"{self.name}/{key}_{rank:03d}": v[key_id].item() for rank, v in enumerate(local_section_data)} + ) + + data_tensor = torch.tensor(gpu_relative_perf_score_gather_list) + slowest_rank_id = torch.argmin(data_tensor) + wandb_info.update( + { + f"slowest_rank/{self.name}_rank": slowest_rank_id.item(), + f"slowest_rank/{self.name}_relative_perf": torch.min(data_tensor).item(), + } + ) + + for key_id, key in enumerate(report.local_section_summaries): + data_tensor = torch.tensor([v[key_id] for v in local_section_data]) + wandb_info.update( + { + f"slowest_rank/slowest_{key}_rank": torch.argmax(data_tensor).item(), + f"slowest_rank/slowest_{key}_time": torch.max(data_tensor).item(), + } + ) + + import wandb + + if wandb.run: + wandb.log(wandb_info, step=iteration) + + import cosmos.utils.launch + + if cosmos.utils.launch.S3_READY and (iteration % (5 * self.report_freq) == 0) and self.save_s3: + easy_io.dump( + wandb_info, + f"s3://rundir/{self.__class__.__name__}/iter_{iteration:09d}.yaml", + ) + easy_io.dump( + report, + f"s3://rundir/{self.__class__.__name__}/report_iter_{iteration:09d}.pkl", + ) + + # Which GPUs are slower than other GPUs, based on the execution time of kernels + relative_stragglers = stragglers["straggler_gpus_relative"] + # Which GPUs are slower than itself in the past, based on the past execution time of kernels. + individual_stragglers = stragglers["straggler_gpus_individual"] + is_slowdown = relative_stragglers or individual_stragglers + if is_slowdown: + hostname = torch.ByteTensor(bytearray(os.uname().nodename, "utf-8")).cuda() + whole_hostname = all_gather_tensor(hostname) + slowest_hostname = whole_hostname[slowest_rank_id].cpu().numpy().tobytes().decode("utf-8") + logging.critical(f"Slowest rank hostname: {slowest_hostname}") + + if self.slowdown_count(is_slowdown) and self.raise_error: + raise RuntimeError( + f"Detected GPU {slowest_rank_id} to be too slow compared to other GPUs." + f" The relative performance of {slowest_rank_id} rank was {report.gpu_relative_perf_scores[slowest_rank_id]}. Terminating the training." + ) diff --git a/cosmos_training/cosmos/utils/object_store.py b/cosmos_training/cosmos/utils/object_store.py new file mode 100644 index 00000000..c579d8b4 --- /dev/null +++ b/cosmos_training/cosmos/utils/object_store.py @@ -0,0 +1,417 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import io +import json +import os +import pickle +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Optional +from urllib.parse import urlparse + +import boto3 +import numpy as np +import torch +import yaml +from botocore.config import Config +from PIL import Image + +import cosmos.utils.easy_io.backends.auto_auth as auto +from cosmos.utils import distributed, log +from cosmos.utils.easy_io import easy_io + +GLOBAL_S3_CONFIG = Config( + retries={"max_attempts": 20, "mode": "adaptive"}, + connect_timeout=10, + read_timeout=60, + request_checksum_calculation="when_required", + response_checksum_validation="when_required", +) +Image.MAX_IMAGE_PIXELS = None + +if TYPE_CHECKING: + from cosmos.utils.config import ObjectStoreConfig + + +class ObjectStore: + """This is the interface class for object store, used for interacting with PBSS/AWS (S3). + + **Deprecated**. Use `easy_io` directly instead. + + Attributes: + client (botocore.client.S3): Object store client object. + easy_io_backend: easy_io backend. + bucket (str): Object store bucket name. + """ + + def __init__(self, config_object_storage: ObjectStoreConfig): + + # extracts the easy_io backend instead of the boto3 S3 client. + with auto.open_auth(config_object_storage.credentials, "r") as file: + object_storage_config = auto.json_load_auth(file) + self.client = Boto3Wrapper( + "s3", + **object_storage_config, + ) + self.easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": config_object_storage.credentials, + "path_mapping": None, + } + ) + self.bucket = config_object_storage.bucket + + def _translate_key(self, key: str) -> str: + """Translate an object key to an S3 URL for easy_io. + + Args: + key (str): The key of the object. + + Returns: + str: The object's S3 URL. + """ + return f"s3://{self.bucket}/{key}" + + def load_object( + self, + key: str, + type: str | None = None, + load_func: Callable | None = None, + encoding: str = "UTF-8", + ) -> Any: + """Helper function for loading object from storage. + + Args: + key (str): The key of the object. + type (str): Specified for some common data types. If not provided, `load_func` should be specified. + The predefined types currently supported are: + - "torch": PyTorch model checkpoints, opened with torch.load(). + - "torch.jit": A JIT-compiled TorchScript model, loaded with torch.jit.load(). + - "image": Image objects, opened with PIL.Image.open(). + - "json": JSON files, opened with json.load(). + - "pickle": Picklable objects, opened with pickle.load(). + - "yaml": YAML files, opened with yaml.safe_load(). + - "text": Pure text files. + - "numpy": Numpy arrays, opened with np.load(). + - "bytes": Raw bytes. + load_func (Callable): a custom function for reading the buffer if `type` were not provided. + encoding (str): Text encoding standard (default: "UTF-8"). + + Returns: + object (Any): The downloaded object. + """ + assert type is not None or load_func is not None, "Either type or load_func should be specified." + + buffer = io.BytesIO(self.easy_io_backend.get(filepath=self._translate_key(key=key))) + buffer.seek(0) + + # Read from buffer for common data types. + if type == "torch": + return torch.load(buffer, map_location=lambda storage, loc: storage, weights_only=False) + elif type == "torch.jit": + return torch.jit.load(buffer) + elif type == "image": + image = Image.open(buffer) + image.load() + return image + elif type == "json": + return json.load(buffer) + elif type == "pickle": + return pickle.load(buffer) + elif type == "yaml": + return yaml.safe_load(buffer) + elif type == "text": + return buffer.read().decode(encoding) + elif type == "numpy": + return np.load(buffer, allow_pickle=True) + # Read from buffer as raw bytes. + elif type == "bytes": + return buffer.read() + # Customized load_func should be provided. + else: + return load_func(buffer) + + def save_object( + self, object: Any, key: str, type: str | None = None, save_func: Callable | None = None, encoding: str = "UTF-8" + ) -> None: + """Helper function for saving object to storage. + + Args: + object (Any): The object to upload. + key (str): The key of the object. + type (str): Specified for some common data types. If not provided, `save_func` should be specified. + The predefined types currently supported are: + - "torch": PyTorch model checkpoints, saved with torch.save(). + - "torch.jit": A JIT-compiled TorchScript model, exported with torch.jit.save(). + - "image": Image objects, saved with PIL.Image.save(). + - "json": JSON files, saved with json.dumps(). + - "pickle": Picklable objects, saved with pickle.dump(). + - "yaml": YAML files, saved with yaml.safe_dump(). + - "text": Pure text files. + - "numpy": Numpy arrays, saved with np.save(). + - "bytes": Raw bytes. + save_func (Callable): a custom function for writing the buffer if `type` were not provided. + encoding (str): Text encoding standard (default: "UTF-8"). + """ + assert type is not None or save_func is not None + with io.BytesIO() as buffer: + + # Write to buffer for common data types. + if type == "torch": + torch.save(object, buffer) + elif type == "torch.jit": + torch.jit.save(object, buffer) + elif type == "image": + type = os.path.basename(key).split(".")[-1] + object.save(buffer, format=type) + elif type == "json": + buffer.write(json.dumps(object).encode(encoding)) + elif type == "pickle": + pickle.dump(object, buffer) + elif type == "yaml": + buffer.write(yaml.safe_dump(object).encode(encoding)) + elif type == "text": + buffer.write(object.encode(encoding)) + elif type == "numpy": + np.save(buffer, object) + # Write to buffer as raw bytes. + elif type == "bytes": + buffer.write(bytes(object)) + # Customized save_func should be provided. + else: + save_func(object, buffer) + buffer.seek(0) + self.easy_io_backend.put(obj=buffer, filepath=self._translate_key(key=key)) + + def object_exists(self, key: str) -> bool: + """ + Check whether an object exists in the storage, with retry logic for transient errors. + + Args: + key (str): The key of the object. + + Returns: + bool: True if the object exists, False if not. + """ + return self.easy_io_backend.exists(filepath=self._translate_key(key=key)) + + +class Boto3Wrapper: + """ + This class serves as a wrapper around boto3.client in order to make boto3.client serializable. It's required to use + spawn method of creating DataLoader workers, which is in turn required to avoid segfaults when using Triton, e.g. + for torch.compile or custom kernels. + """ + + def __init__(self, *args, **kwargs): + self._args = args + self._kwargs = kwargs + self.client = None + + def __setstate__(self, state): + self.__dict__ = state + + def __getattr__(self, item): + is_worker = torch.utils.data.get_worker_info() is not None + client = ( + boto3.client(*self._args, **self._kwargs, config=GLOBAL_S3_CONFIG) if self.client is None else self.client + ) + if is_worker: + self.client = client + return getattr(client, item) + + +def sync_s3_dir_to_local( + s3_dir: str, + s3_credential_path: str, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + local_rank_sync: bool = False, +) -> str: + """ + Download an entire directory from S3 to the local cache directory. + + Args: + s3_dir (str): The AWS S3 directory to download. + s3_credential_path (str): The path to the AWS S3 credentials file. + rank_sync (bool, optional): Whether to synchronize download across + ALL distributed workers using `distributed.barrier()`. Defaults to True. + cache_dir (str, optional): The cache folder to sync the S3 directory to. + If None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "~/.cache/imaginaire") will be used. + local_rank_sync (bool, optional): Whether to synchronize download across + workers within the same node using a node-level barrier. This is useful + when the cache directory is not shared across nodes. Defaults to False. + Note: rank_sync and local_rank_sync cannot both be True. + + Returns: + local_dir (str): The path to the local directory. + """ + if local_rank_sync and rank_sync: + raise ValueError("rank_sync and local_rank_sync cannot be True at the same time.") + + if not s3_dir.startswith("s3://"): + # If the directory exists locally, return the local path + assert os.path.exists(s3_dir), f"{s3_dir} is not a S3 path or a local path." + return s3_dir + + # Get local rank for node-level synchronization + local_rank = int(os.getenv("LOCAL_RANK", 0)) if local_rank_sync else None + + easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": s3_credential_path, + "path_mapping": None, + } + ) + + # Parse the S3 URL + parsed_url = urlparse(s3_dir) + obj_prefix = parsed_url.path.lstrip("/") + + # If the local directory is not specified, use the default cache directory + cache_dir = ( + os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire")) + if cache_dir is None + else cache_dir + ) + cache_dir = os.path.expanduser(cache_dir) + Path(cache_dir).mkdir(parents=True, exist_ok=True) + + for obj_suffix in easy_io_backend.list_dir_or_file(dir_path=s3_dir, list_dir=False, list_file=True): + # Create the full path for the destination file, preserving the directory structure + dest_path = os.path.join(cache_dir, obj_prefix, obj_suffix) + + # Ensure the directory exists + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + + # Check if the file already exists + if os.path.exists(dest_path): + continue + else: + s3_obj = f"{s3_dir.removesuffix('/')}/{obj_suffix}" + log.info(f"Downloading {s3_obj} to {dest_path}") + # Download the file + if rank_sync: + # Only rank 0 downloads when using global rank sync + if distributed.get_rank() == 0: + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + elif local_rank_sync: + # Only local rank 0 (first rank on each node) downloads when using local rank sync + if local_rank == 0: + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + else: + # No synchronization - every rank downloads + easy_io_backend.copyfile_to_local(src=s3_obj, dst=dest_path, dst_type="file") + # Synchronize after downloads complete + if rank_sync or local_rank_sync: + distributed.barrier() + + local_dir = os.path.join(cache_dir, obj_prefix) + return local_dir + + +def download_from_s3_with_cache( + s3_path: str, + s3_credential_path: str, + cache_fp: Optional[str] = None, + cache_dir: Optional[str] = None, + rank_sync: bool = True, + backend_args: Optional[dict] = None, + backend_key: Optional[str] = None, +) -> str: + """download data from S3 with optional caching. + + This function first attempts to load the data from a local cache file. If + the cache file doesn't exist, it downloads the data from S3 to the cache + location. Caching is performed in a rank-aware manner + using `distributed.barrier()` to ensure only one download occurs across + distributed workers (if `rank_sync` is True). + + Args: + s3_path (str): The S3 path of the data to load. + cache_fp (str, optional): The path to the local cache file. If None, + a filename will be generated based on `s3_path` within `cache_dir`. + cache_dir (str, optional): The directory to store the cache file. If + None, the environment variable `IMAGINAIRE_CACHE_DIR` (defaulting + to "/tmp") will be used. + rank_sync (bool, optional): Whether to synchronize download across + distributed workers using `distributed.barrier()`. Defaults to True. + backend_args (dict, optional): The backend arguments passed to easy_io to construct the backend. + backend_key (str, optional): The backend key passed to easy_io to registry the backend or retrieve the backend if it is already registered. + + Returns: + cache_fp (str): The path to the local cache file. + + Raises: + FileNotFoundError: If the data cannot be found in S3 or the cache. + """ + if not s3_path.startswith("s3://"): + # If the file exists locally, return the local path + assert os.path.exists(s3_path), f"{s3_path} is not a S3 path nor a local path." + return s3_path + + easy_io_backend = easy_io.get_file_backend( + backend_args={ + "backend": "s3", + "s3_credential_path": s3_credential_path, + "path_mapping": None, + } + ) + cache_dir = ( + os.environ.get("IMAGINAIRE_CACHE_DIR", os.path.expanduser("~/.cache/imaginaire")) + if cache_dir is None + else cache_dir + ) + cache_dir = os.path.expanduser(cache_dir) + if cache_fp is None: + cache_fp = os.path.join(cache_dir, s3_path.replace("s3://", "")) + if not cache_fp.startswith("/"): + cache_fp = os.path.join(cache_dir, cache_fp) + + if rank_sync: + if distributed.get_rank() == 0: + if os.path.exists(cache_fp): + # check the size of cache_fp + if os.path.getsize(cache_fp) < 1: + os.remove(cache_fp) + log.warning(f"Removed empty cache file {cache_fp}.") + + if not os.path.exists(cache_fp): + easy_io_backend.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + log.info(f"Downloaded {s3_path} to {cache_fp}.") + else: + log.info(f"The cache file {cache_fp} already exists.") + distributed.barrier() + else: + if os.path.exists(cache_fp): + # check the size of cache_fp + if os.path.getsize(cache_fp) < 1: + os.remove(cache_fp) + log.warning(f"Removed empty cache file {cache_fp}.") + if not os.path.exists(cache_fp): + easy_io_backend.copyfile_to_local( + s3_path, cache_fp, dst_type="file", backend_args=backend_args, backend_key=backend_key + ) + log.info(f"Downloaded {s3_path} to {cache_fp}.") + else: + log.info(f"The cache file {cache_fp} already exists") + return cache_fp diff --git a/cosmos_training/cosmos/utils/one_logger/README.md b/cosmos_training/cosmos/utils/one_logger/README.md new file mode 100644 index 00000000..bc8b334c --- /dev/null +++ b/cosmos_training/cosmos/utils/one_logger/README.md @@ -0,0 +1,97 @@ +# OneLogger + +`You must use OneLogger or we will not let you run any jobs greater than one node.` OneLogger is a tool that measures our end-to-end job efficiency and the measures will be used for future resources governance. For all the training jobs we run in NVIDIA clusters, we need to enable OneLogger to report our utilization. Currently, slurm clusters (`aws-iad-cs-001`, `cw-pdx-cs-001`) are supported. + +* Related docs + * [Example integration](https://gitlab-master.nvidia.com/hwinf-dcm/one-logger-utils) + +## Usage + +* OneLogger logs the resource utilization by using wandb. Please set `WANDB_API_KEY` environment variable. +```bash +WANDB_API_KEY=xxx torchrun --nproc_per_node 1 -m scripts.train --config projects/dv_diffusion/config/cifar10.py +``` + +* By default, OneLogger is automatically enabled by detecting the job environment - Slurm, NGC, Run:AI, and Local. Environment variable `ENABLE_ONELOGGER` is there to provide explicit control by the value between `TRUE` and `FALSE`. We assume `ENABLE_ONELOGGER` is set from [launcher](packages/launcher/README.md) Executor and users will always submit imaginaire4 jobs with [launcher](packages/launcher/README.md). + * NGC: `launcher.NGCExecutor` always sets `ENABLE_ONELOGGER=FALSE`. + * Slurm: `launcher.SlurmExecutor` sets `ENABLE_ONELOGGER=TRUE` by default. It will follow user input if provided. + * Run:AI: `launcher.RunAIExecutor` always sets `ENABLE_ONELOGGER=FALSE`. + * Local: `launcher.Executor` does not set `ENABLE_ONELOGGER` and will follow user input if provided. + +* OneLogger supports 2 modes - `production` and `test`. + * By default, `launcher.SlurmExecutor` sets `ONE_LOGGER_JOB_CATEGORY=production`. + * If necessary, users are allowed to set `ONE_LOGGER_JOB_CATEGORY=test` through launcher. `test` mode is for jobs with abnormal behaviors such as interactive debugging jobs. + +* OneLogger is implemented in imaginaire4 in the form of Callback. `OneLoggerCallback` is defined in [imaginaire/utils/callback.py](imaginaire/utils/callback.py). `OneLoggerCallback` is added to the config callbacks before initializing `ImaginaireTrainer`. + +`scripts/train.py` +```python +config = override_one_logger_callback(config) +``` + +`imaginaire/trainer.py` +```python +# OneLogger - initialize one_logger before instantiating CallBackGroup +enable_one_logger = os.environ.get("ENABLE_ONELOGGER", "FALSE").lower() == "true" +if enable_one_logger: + initialize_one_logger_from_imaginaire_config(config) +# Initialize the callback functions. +self.callbacks = callback.CallBackGroup(config=config, trainer=self) +``` + +## Requirements + +* W&B + +As OneLogger relies on wandb to log efficiency graphs, everyone who is using OneLogger / training models should belong to the corresponding wandb team, [hwinf_dcm](https://wandb.ai/hwinf_dcm). Please make sure you can find your name from the users list. If you are not there, please ask in slack channel [#hwinf-mlwfo-e2e-support](https://nvidia.enterprise.slack.com/archives/C0730DFM6UC) to onboard yourself. + +* python dependency (already included in imaginaire4 dockers images) +```bash +pip install --index-url=https://sc-hw-artf.nvidia.com/artifactory/api/pypi/hwinf-mlwfo-pypi/simple --upgrade one-logger +``` + +## Implementation details + +* New files are added for OneLogger utility. + * cosmos/utils/one_logger/one_logger_global_vars.py + * cosmos/utils/one_logger/one_logger_utils.py +* global variable `one_logger` is initialized from `ImaginaireTrainer` and most logging functions are implemented in the form of callbacks. +* `OneLoggerCallback` is implemented with trainer callback functions. + * `__init__()` + * `on_train_start()` + * `on_training_step_start()` + * `on_optimizer_init_start()` (**new**) + * `on_optimizer_init_end()` (**new**) + * `on_training_step_end()` + * `on_validation_start()` + * `on_validation_step_start()` + * `on_validation_step_end()` + * `on_validation_end()` + * `on_load_checkpoint_start()` (**new**) + * `on_load_checkpoint_end()` (**new**) + * `on_save_checkpoint_start()` (**new**) + * `on_save_checkpoint_end()` (**new**) + * `on_save_checkpoint_success()` (**new**) + * `on_train_end()` + * `on_app_end()` (**new**) +* Above callbacks marked (**new**) are new callback functions that did not exist before. +* Exceptions are + * Dataloader callbacks are called from the context manager data_loader_init: + * `one_logger.on_dataloader_init_start()` + * `one_logger.on_dataloader_init_end()` + * model callbacks are called from the context manager model_init: + * `one_logger.on_model_init_start()` + * `one_logger.on_model_init_end()` +* **Important** + * `app_tag` and `app_tag_run_name` are the variables necessary for resource efficiency tracking. + * Current formats are +```python +app_tag_run_name = f"{config.job.project}/{config.job.group}/{config.job.name}" +app_tag_run_version = "0.0.0" # hard-coded app_tag_run version. +app_tag = f"{app_tag_run_name}/{job_size}/ENV_{job_environment}_GPU_{gpu_name}" +``` + +OneLogger implementation is tested with the following command on Slurm, NGC, and Local environments. +```bash +WANDB_API_KEY=xxx torchrun --nproc_per_node 1 -m scripts.train --config projects/dv_diffusion/config/cifar10.py +``` diff --git a/cosmos_training/cosmos/utils/one_logger/__pycache__/one_logger_override_utils.cpython-312.pyc b/cosmos_training/cosmos/utils/one_logger/__pycache__/one_logger_override_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a95d1dc8cc07ccc6790dcf2938bce73c564f1d4d GIT binary patch literal 2643 zcmbtW&2JM&6rZ)%-<#$GoP^M>HgSk+XyVWoQmU%0NencYm_oQ%D%*{BZFX7jMziar zPMv_N9za5Ba*8TdYN;xr3Jz34Du@07m3r|KOE#+@A+=T2TSRi=)EV1jCxk;PMjFp= z-prf%z4zw5^&f3*et=qj%cXpW(4Tlhnxg?Wrx92MA`mGRr10oashlI_$WbYZ!gHtU z%+V>jhG~RdDR&LKR8P*E^473h_2v92Kf+$o^EI6c6sU;rUwp+>#9{lAIVmfQ>srS4 z=@_OFj8Jyqd?Gd;Ka)&M#4n(!Hz6z(#syVHxHG9q2|b&YAQsFB>8nWSR%?c? zH|kE*H@!5r!^E;$Q;3B`fJVi&ZPI`uB|7dl_9~v;>vzdM=hZg>SfoUH(NP47clS=E z=Ix#G3GmrHq~$c*h>x<#BB(WHn*8B_8620kL zv`A|oiSE5~EIJP$)j0}MxAOm2>*e8$peGBb-PFzo=G5jNF=_ZMDL=50vbhw zBDG9GchRv-8BPZ<-yoW;Gl<0|+gxj+dp;8RMc1oEh@1n%d-sCUPZ+hv;`RuEi@*$# zwW8~upV+eKHV+ZGGDJ{o+&c%6Cd?_Y?M(F2Uu=6nv*;#gzFdcIX5qFQU(E_Hxe>i< zpah4YsTS<7;|Dsrl9m|RAOIiWUi=|*)jM!55;Bp4ExM;%I8v=0`B(|T@JFy$Fl z;zT)3G)HVNZwfFgnf$`|ZjQzkJ&&%^HMPLa)D9H6`74sfy-1Uj4Q~3vrT8?evF?T~ z9uo?(CP7p#{kkUc^^zG0+78{YJ(4yjLtV3-=x*Dk>hltW?I_Z=uW5+wmyxxGDQIcQ zrm?*cRaRj86A0oOE@vSwezs>`fEw}(#H9`5%4t&xh+}KJh1pq26KxtB4RLw4{RXlY zot00ci*ph(KH`D6z}X#=CLjm#h;5y$-Pa<{6JEUZ1-jp`eNBno-dw9Y)9&4-r=o93 zYin0CwETE1kvJI}KWkvdT)jR2F{qa!K8#y%0LS2#4CYkdy zDA0V((a8QNia0c)1wqT=ZZ?#79l5~J^Dr$DNmHY7Y~GYrBibUp4f6UN3XLpEd~MUn zY{7oLDQamnErd9KaNRVPQH8$_w!EOXZ*^g1VRdO`sWkN7!|+K0f8t@p>YuRM zC#oI2Rd(=u?OUzF9=F)z_X`#qtFV(6J9#7dG|*ROqmKg7$NNGhMkyUq*3j?1P&G1K zVul_wof`)brDrVg_CDg?uZG5cI$Az-d@VHg+n42Oz8w0zCsz%5+n6!*B@^^*Kk5X{4WRM@Y=y08%YM#)30O48cL>GqSl_q&j5z;8ThmD z-1|XJ7xSw0Asj|i{ExuEpTrF(MNyj^p#1-U!+(R(C*bWT;GHKRwB;V4x@!9W0SlR2 A_y7O^ literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/one_logger/one_logger_context_managers.py b/cosmos_training/cosmos/utils/one_logger/one_logger_context_managers.py new file mode 100644 index 00000000..110132db --- /dev/null +++ b/cosmos_training/cosmos/utils/one_logger/one_logger_context_managers.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import contextmanager +from typing import Generator + +from cosmos.utils.log import logger +from cosmos.utils.one_logger.one_logger_utils import get_one_logger, one_logger_is_initialized + + +@contextmanager +def data_loader_init() -> Generator[None, None, None]: + """ + Wrap the execution data loader initialization by invoking the one logger callbacks. + """ + try: + one_logger = get_one_logger() + if one_logger_is_initialized(): + one_logger.on_dataloader_init_start() + + yield + + finally: + try: + if one_logger_is_initialized(): + one_logger.on_dataloader_init_end() + except Exception as exc: # noqa: BLE001 + logger.warning("one_logger.on_dataloader_init_end() failed (non-fatal): %s", exc) + + +@contextmanager +def model_init(set_barrier: bool = False) -> Generator[None, None, None]: + """ + Wrap the instantiation of the model by invoking the one logger callbacks. + """ + try: + one_logger = get_one_logger() + if one_logger_is_initialized(): + one_logger.on_model_init_start(set_barrier=set_barrier) + + yield + + finally: + try: + if one_logger_is_initialized(): + one_logger.on_model_init_end(set_barrier=set_barrier) + except Exception as exc: # noqa: BLE001 + logger.warning("one_logger.on_model_init_end() failed (non-fatal): %s", exc) diff --git a/cosmos_training/cosmos/utils/one_logger/one_logger_global_vars.py b/cosmos_training/cosmos/utils/one_logger/one_logger_global_vars.py new file mode 100644 index 00000000..3edd0d50 --- /dev/null +++ b/cosmos_training/cosmos/utils/one_logger/one_logger_global_vars.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +VERSION = "1.0.2" +MINIMAL_SCHEMA_CALLBACK_ARGS = { + "initialize": { + "required": { + "enable_for_current_rank", + "one_logger_project", + "one_logger_run_name", + "one_logger_async", + "log_every_n_train_iterations", + "app_tag_run_name", + "app_tag_run_version", + "world_size", + "global_batch_size", + "batch_size", + "app_tag", + "train_iterations_target", + "train_samples_target", + "is_baseline_run", + "is_train_iterations_enabled", + "is_validation_iterations_enabled", + "is_test_iterations_enabled", + "is_save_checkpoint_enabled", + "is_log_throughput_enabled", + }, + "optional": { + "micro_batch_size", + "summary_data_schema_version", + "app_run_type", + "app_start_time", + "app_metrics_feature_tags", + "seq_length", + "metadata", + "barrier", + }, + }, + "on_train_start": { + "required": {"train_iterations_start"}, + "optional": {"train_samples_start"}, + }, + "on_app_start": {"optional": {"app_start_time"}}, + "on_app_end": {"optional": {"app_finish_time"}}, + "on_model_init_start": {"optional": {"app_model_init_start_time"}}, + "on_model_init_end": {"optional": {"app_model_init_finish_time"}}, + "on_dataloader_init_start": {"optional": {"app_build_dataiters_start_time"}}, + "on_dataloader_init_end": {"optional": {"app_build_dataiters_finish_time"}}, + "on_load_checkpoint_start": {"optional": {"load_checkpoint_start_time"}}, + "on_load_checkpoint_end": {"optional": {"load_checkpoint_finish_time"}}, + "on_optimizer_init_start": {"optional": {"app_build_optimizer_start_time"}}, + "on_optimizer_init_end": {"optional": {"app_build_optimizer_finish_time"}}, +} + +THROUGHPUT_CALLBACK_ARGS = { + "initialize": { + "optional": {"flops_per_sample"}, + }, +} + +CHECKPOINT_CALLBACK_ARGS = { + "initialize": { + "required": {"save_checkpoint_strategy"}, + }, + "on_save_checkpoint_start": { + "required": {"global_step"}, + }, + "on_save_checkpoint_end": { + "required": {"global_step"}, + }, + "on_save_checkpoint_success": { + "required": {"global_step"}, + }, +} diff --git a/cosmos_training/cosmos/utils/one_logger/one_logger_override_utils.py b/cosmos_training/cosmos/utils/one_logger/one_logger_override_utils.py new file mode 100644 index 00000000..a067beec --- /dev/null +++ b/cosmos_training/cosmos/utils/one_logger/one_logger_override_utils.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from omegaconf import OmegaConf, omegaconf + +from cosmos.utils.lazy_config import PLACEHOLDER +from cosmos.utils.lazy_config import LazyCall as L +from cosmos.utils.callback import OneLoggerCallback +from cosmos.utils.log import logger + + +def override_one_logger_callback(config) -> None: + """Add OneLoggerCallback to imaginaire config""" + + # Enable OneLogger by environment variable. + enable_onelogger = os.environ.get("ENABLE_ONELOGGER", "FALSE").lower() == "true" + + # Check if OneLoggerCallback already exists (by an explicit input argument) + one_logger_callback_exists = False + for _callback in config.trainer.callbacks: + if isinstance(config.trainer.callbacks, (list, omegaconf.ListConfig)): # old format + logger.warning("Using old list format for callbacks. Please use registry-compatible dict format.") + callback_target = _callback._target_ + else: # omegaconf.dictconfig.DictConfig, registry-compatible format + if "_target_" not in config.trainer.callbacks[_callback]: + continue + callback_target = config.trainer.callbacks[_callback]._target_ + + if callback_target is OneLoggerCallback: + assert enable_onelogger, "OneLoggerCallback should only be used when ENABLE_ONELOGGER is TRUE" + one_logger_callback_exists = True + break + + # Add OneLoggerCallback + if enable_onelogger and not one_logger_callback_exists: + one_logger_lazy_callback = L(OneLoggerCallback)(config=PLACEHOLDER, trainer=PLACEHOLDER) + if isinstance(config.trainer.callbacks, list): # old format + config.trainer.callbacks.append(one_logger_lazy_callback) + else: + ONELOGGER_CALLBACK = dict(one_logger=one_logger_lazy_callback) + + OmegaConf.set_struct(config.trainer.callbacks, False) + config.trainer.callbacks = OmegaConf.merge(config.trainer.callbacks, ONELOGGER_CALLBACK) + OmegaConf.set_struct(config.trainer.callbacks, True) + + return config diff --git a/cosmos_training/cosmos/utils/one_logger/one_logger_utils.py b/cosmos_training/cosmos/utils/one_logger/one_logger_utils.py new file mode 100644 index 00000000..0bd6f72a --- /dev/null +++ b/cosmos_training/cosmos/utils/one_logger/one_logger_utils.py @@ -0,0 +1,1445 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import uuid +from functools import wraps +from typing import Any, Dict, Optional + +import torch + +from cosmos.utils.distributed import get_rank, get_world_size +from cosmos.utils.log import logger +from cosmos.utils.one_logger.one_logger_global_vars import ( + CHECKPOINT_CALLBACK_ARGS, + MINIMAL_SCHEMA_CALLBACK_ARGS, + THROUGHPUT_CALLBACK_ARGS, + VERSION, +) + +try: + from megatron.core import parallel_state +except ImportError: + parallel_state = None + + +one_logger = None # globally unique OneLoggerUtils object + +rankpulse = None +try: + if os.environ.get("RANKPULSE_ENABLE", "0").lower() in ["1", "true", "yes", "y"]: + # pyrefly: ignore # import-error + import one_logger.rankpulse as rankpulse +except ImportError: + print( + "WARNING: RANKPULSE_ENABLE is set but rankpulse module is not available. Please install one-logger package with rankpulse support." + ) + + +####################################################### +# Timer implementation +####################################################### + + +class _NamedTimer: + """ + A timer class that supports multiple named timers. + dt will be returned. + A named timer cannot be started if it is already currently running. + Use case: measuring execution of multiple code blocks. + Note that this is only an internal class to log time info. + """ + + def __init__(self, barrier: Optional[callable] = None): + """ + Initializes a new instance of the _NamedTimer class. + + :param barrier: A function to call for synchronization. Default to None. + :type barrier: callable + """ + self.barrier = barrier + self.reset() + + def __getitem__(self, name): + """ + Gets the timer data for a specified timer name. + + Args: + name (str): The name of the timer. + + Returns: + dict: Timer data for the specified timer name. + """ + return self.get(name) + + def reset(self, name=None): + """ + Resets all / specific timer + + Args: + name (str): timer name to reset (if None all timers are reset) + """ + if name is None: + self.timers = {} + else: + self.timers[name] = {} + + def start(self, name: str, set_barrier: bool = False): + """ + Starts measuring a named timer. + + :param name: timer name to start + :type name: str + :param set_barrier: Synchronize ranks before starting. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + """ + timer_data = self.timers.get(name, {}) + + if "is_active" in timer_data: + raise RuntimeError(f"Cannot start timer = '{name}' since it is already active") + + if set_barrier: + if not callable(self.barrier): + raise RuntimeError("No barrier function to call in the _NamedTimer") + self.barrier() + + timer_data["start"] = time.time() + timer_data["is_active"] = True + timer_data["count"] = timer_data.get("count", 0) + 1 + + self.timers[name] = timer_data + + def stop(self, name: str, set_barrier: bool = False): + """ + Stops measuring a named timer. + + :param name: timer name to stop. + :type name: str + :param set_barrier: Synchronize ranks before starting. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + """ + timer_data = self.timers.get(name) + if (timer_data is None) or ("is_active" not in timer_data): + raise RuntimeError(f"Cannot end timer = '{name}' since it is not active") + + if set_barrier: + if not callable(self.barrier): + raise RuntimeError("No barrier function to call in the _NamedTimer") + self.barrier() + + # compute dt and make timer inactive + timer_data["finish"] = time.time() + timer_data.pop("is_active") + + # update latest,avg,min,max,total + timer_data["latest"] = timer_data["finish"] - timer_data["start"] + if timer_data.get("min", float("inf")) > timer_data["latest"]: + timer_data["min"] = timer_data["latest"] + + if timer_data.get("max", 0) < timer_data["latest"]: + timer_data["max"] = timer_data["latest"] + + timer_data["total"] = timer_data.get("total", 0) + timer_data["latest"] + timer_data["avg"] = timer_data["total"] / timer_data["count"] + + self.timers[name] = timer_data + + def is_active(self, name=""): + """ + Checks if a named timer is currently active. + + Args: + name (str): The name of the timer to check. + + Returns: + bool: True if the timer is active, False otherwise. + """ + timer_data = self.timers.get(name, {}) + if "is_active" in timer_data: + return True + return False + + def active_timers(self): + """ + Return list of all active named timers + + Returns: + list: A list of names of active timers. + """ + return [k for k, v in self.timers.items() if ("is_active" in v)] + + def get(self, name): + """ + Retrieves the data for a specified timer name. + + Args: + name (str): The name of the timer. + + Returns: + dict: The data associated with the specified timer name. + """ + if name not in self.timers: + self.timers[name] = {} + return self.timers[name] + + +####################################################### +# Callback implementation +####################################################### + + +class OneLoggerUtils: + O_MINIMAL = 0x00000001 + """The mode must include this bit to enable the minimal schema.""" + + O_THROUGHPUT = 0x00000002 + """The mode must include this bit to enable throughput metrics.""" + + O_CKPT = 0x00000004 + """The mode must include this bit to enable saving checkpoint-related metrics.""" + + def __init__(self, config: Dict[str, Any]) -> None: + """ + Construct TrainingCallbacks to log metrics of training jobs. + + :param config: Configuration of OneLogger callback for E2E training metrics logging. This dictionary contains: 1. application-specific values for logging various metrics. such as minimal schema, throughput metrics, checkpoint metrics etc. 2. control settings for callbacks behavior. E.g., barrier function, log interval etc. + :type config: dict + + **** + + **config** (for callback control) must contain: + - **enable_for_current_rank** (bool): Whether to enable logging for the current rank in distributed training. + - **one_logger_project** (str): The project name for the OneLogger system. + - **one_logger_run_name** (str): The name for the current run, used for identifying the log entries. + - **one_logger_async** (bool): Whether to enable asynchronous logging. + - **log_every_n_train_iterations** (int): Frequency of logging, specified as the number of steps between logs. NOTE: this value will only affect the on_train_batch_end callback + - **barrier** (callable, optional): Function to synchronize all ranks, optional. Default to None. NOTE: If no barrier function provided, OneLogger won't set any barrier for any timestamp calculation across ranks. So only the timestamp calculated in last rank will be used. + + **config** (for Minimal Schema) must contain: + - **app_tag_run_name** (str or callable): Tag (or callable to generate the tag) for the run name. Jobs belonging to same training run, suppose to have the same app_tag_run_name. NOTE: Please review this value with HWInf-MLWFO-E2E-Dev before enabling instrumentation in production. + - **app_tag_run_version** (str or callable): Tag version (or callable to generate version) for the run. NOTE: It will be used to track the changes in the application side which might change the performance baseline, suggesting we should do separate baseline calculation even if the runs belong to the same training that continues. + - **app_start_time** (int or callable): Timestamp (or callable to get timestamp) when the application started. + - **world_size** (int or callable): Number (or callable to get number) of processes participating in the training. + - **global_batch_size** (int or callable): Global batch size or function to compute it. + - **batch_size** (int or callable): Batch size in each batch iteration or function to calculate the batch size. + - **app_tag** (str or list or callable): App_tag or function to compute the app tag. The app_tag is array of strings/single string of unique tag values, calculated from user provided input to be used to identify app use cases expected to have similar perf. NOTE: Please review this value with HWInf-MLWFO-E2E-Dev before enabling instrumentation in production. + - **train_iterations_target** (int or callable): Target number of training iterations or callable to generate it. + - **train_samples_target** (int or callable): Target number of training samples or function to generate the number, optional. + - **is_baseline_run** (bool or callable): Flag (or callable to return flag) that indicates if this is a baseline run for comparison purposes, optional. Default to False. + - **is_train_iterations_enabled** (bool or callable): Flag (or callable to return flag) that whether to log training iterations. Default to True. + - **is_test_iterations_enabled** (bool or callable): Flag (or callable to return flag) that whether to log test iterations. Default to True. + - **is_save_checkpoint_enabled** (bool or callable): Flag (or callable to return flag) that whether to log metrics related to saving checkpoints. Default to True. + - **is_log_throughput_enabled** (bool or callable): Flag (or callable to return flag) that whether to log throughput-related metrics. Default to True. + - **micro_batch_size** (int or callable, optional): Size (or callable to generate the size) of each micro-batch in training. + - **summary_data_schema_version** (str or callable, optional): Version (or callable to return version) of the data schema used for summarizing metrics. Default to "1.0.0" + - **app_run_type** (str or callable, optional): Type (or callable to return type) of the application run (e.g., training, validation), optional. Default to "training". + - **app_metrics_feature_tags** (str or callable, optional): Feature tags (or callable to return tags) used to categorize metrics, optional. Default to "full" + - **seq_length** (int or callable, optional): Sequence length of a training sample or function to calculate the length. Default to None. + - **metadata** (dict or callable, optional): Other static metadata to track or callable to generate the metadata dict. Default to None. NOTE: the metadata name should be different with key names in `config`. + + **config** (for Throughput Metrics) must contain: + - **flops_per_sample** (int or callable, optional): FLOPs per sample or function to compute FLOPs per sample. NOTE: this must be set if `is_log_throughput_enabled` is set to `True` or exception would be raised from OneLogger. + + **config** (for Saving Checkpoint-related Metrics) must contain: + - **save_checkpoint_strategy** (str): Strategy used for saving checkpoints. + + """ + self.enable_for_current_rank = config.get("enable_for_current_rank", False) + self.one_logger_project = config.get("one_logger_project", "e2e-tracking") + self.one_logger_run_name = config.get("one_logger_run_name", str(uuid.uuid4())) + self.quiet = config.get("quiet", False) + self.one_logger_async = config.get("one_logger_async", True) + self.log_every_n_train_iterations = config.get("log_every_n_train_iterations", 50) + self.barrier = config.get("barrier", None) + + try: + self._set_one_logger() + self.timer = _NamedTimer(barrier=self.barrier) + except Exception as e: + logger.info(e) + self.one_logger = None + + self.mode = self.O_MINIMAL + if self._evaluate_value(config.get("is_log_throughput_enabled", True)): + self.mode |= self.O_THROUGHPUT + if self._evaluate_value(config.get("is_save_checkpoint_enabled", True)): + self.mode |= self.O_CKPT + + self._initialize(**config) + + def _no_exception_raise(func): + """ + A decorator function that wraps a given function to handle exceptions gracefully. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + if not hasattr(self, "quiet") or not self.quiet: + return func(self, *args, **kwargs) + try: + return func(self, *args, **kwargs) + except Exception as e: + logger.info(e) + # Skipping execution if error happens + self.one_logger = None + return None + + return wrapper + + @_no_exception_raise + def __getattr__(self, name): + """The __getattr__ method is called only when an attribute is not found in the class""" + message = ( + f"Attribute '{name}' is not found in the current version of the library. " + f"Please ensure you are using the latest version." + ) + raise AttributeError(message) + + @_no_exception_raise + def _set_one_logger(self): + """ + Initializes the `one_logger` instance if the `enable_for_current_rank` attribute is set to True. + """ + # Mark: Get OneLogger + if self.enable_for_current_rank: + try: + # pyrefly: ignore # import-error + from one_logger.core import OneLogger + + config = { + "project": self.one_logger_project, + "name": self.one_logger_run_name, + "quiet": self.quiet, + "async": self.one_logger_async, + "no_exception": False, + } + self.one_logger = OneLogger(config=config) + except BaseException: + logger.info( + "WARNING: one_logger package is required to enable e2e metrics " + "tracking. please go to " + "https://confluence.nvidia.com/display/MLWFO/Package+Repositories" + " for details to install it" + ) + else: + self.one_logger = None + + @_no_exception_raise + def _get_one_logger(self): + """ + Returns the `one_logger` instance. + """ + return self.one_logger + + @_no_exception_raise + def _check_enabled(func): + """ + A decorator to ensure `one_logger` is enabled before executing the function. + + The wrapped function only executes if `one_logger` is initialized; otherwise, it returns None. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + if hasattr(self, "one_logger") and self.one_logger: + return func(self, *args, **kwargs) + else: + # Skipping execution because OneLogger is not enabled. + return None + + return wrapper + + @_check_enabled + def _evaluate_value(self, input_val): + """ + Resolves the input variable to a value. If the input is callable, it calls it; + otherwise, it returns the input directly. + """ + if callable(input_val): + return input_val() + return input_val + + @_check_enabled + def _store_has_key(self, key): + """ + A wrapper function to reuse one_logger.store_has_key. + """ + return self.one_logger.store_has_key(key) + + @_check_enabled + def _store_set(self, key, value): + """ + A wrapper function to reuse one_logger.store_set. + """ + self.one_logger.store_set(key, value) + + @_check_enabled + def _store_get(self, key, default_value=None): + """ + A wrapper function to reuse one_logger.store_get and evaluate the value at the same time. + """ + if not self._store_has_key(key): + if default_value is None: + raise ValueError(f"Missing required key '{key}' from store") + else: + return default_value + else: + return self._evaluate_value(self.one_logger.store_get(key)) + + @_check_enabled + def _log_metrics(self, metrics_to_log): + """ + Logs metrics using the `one_logger`. + + :param metrics_to_log: A dictionary of metrics to log. + :type metrics_to_log: dict + """ + self.one_logger.log_metrics(metrics_to_log) + + @_check_enabled + def _log_app_tag(self, app_tag): + """ + Logs an application tag using the `one_logger`. + + :param app_tag: The application tag to log. + :type app_tag: str + """ + self.one_logger.log_app_tag(app_tag) + + @_check_enabled + def _on_start(self, name: str, set_barrier: bool = False): + """ + Starts a timer with the given name. Resets the timer if already active. + + :param name: The name of the timer. + :type name: str + + :param set_barrier: Synchronize ranks before starting the timer. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :return: None + """ + if self.timer.is_active(name): + logger.info( + f"Timer `{name}` was not correctly stopped, suggesting a " + "possible issue. The timer will be reset for now." + ) + self.timer.reset(name) + + self.timer.start(name, set_barrier) + + # Method to do timer related implemention when we want to stop a timer + @_check_enabled + def _on_end(self, name, set_barrier=False): + """ + Stops a timer with the given name. + + :param name: The name of the timer. + :type name: str + + :param set_barrier: Synchronize ranks before stopping the timer. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :return: None + """ + self.timer.stop(name, set_barrier) + + @_check_enabled + def _validate_and_store_args(self, callback_name, kwargs): + """ + Validates the arguments passed to a callback against expected schema. + + Checks for required, optional, and default arguments for the specified callback. Raises + an error if required arguments are missing or additional unexpected arguments are provided. + + :param callback_name: The name of the callback. + :type callback_name: str + :param kwargs: The arguments provided to the callback. + :type kwargs: dict + + :raises ValueError: If required arguments are missing or unexpected arguments are provided. + """ + args_mappings = [] + if self.mode & self.O_MINIMAL: + args_mappings.append(MINIMAL_SCHEMA_CALLBACK_ARGS) + if self.mode & self.O_THROUGHPUT: + args_mappings.append(THROUGHPUT_CALLBACK_ARGS) + if self.mode & self.O_CKPT: + args_mappings.append(CHECKPOINT_CALLBACK_ARGS) + + required_args = set() + optional_args = set() + for args_mapping in args_mappings: + required_args |= args_mapping.get(callback_name, {}).get("required", set()) + optional_args |= args_mapping.get(callback_name, {}).get("optional", set()) + missing_args = required_args - kwargs.keys() + if len(missing_args) != 0: + raise ValueError( + f"Missing required arguments for callback '{self.__class__.__name__}.{callback_name}': {missing_args}" + ) + additional_args = kwargs.keys() - (required_args | optional_args) + if len(additional_args) != 0: + raise ValueError( + f"Additional arguments provided for callback '{self.__class__.__name__}.{callback_name}': {additional_args}" + ) + for k, v in kwargs.items(): + self._store_set(k, v) + + @_check_enabled + def _initialize(self, **config): + """ + Initializes the logger with initial values and stores metric settings. + """ + self._validate_and_store_args("initialize", config) + + self._store_set("train_iterations", 0) + self._store_set("validation_iterations", 0) + self._store_set("train_samples", 0) + self._store_set("train_epochs", 0) + self._store_set("train_iterations_time_total", 0) + self._store_set("validation_iterations_time_total", 0) + if self.mode & self.O_CKPT: + self._store_set("save_checkpoint_count", 0) + self._store_set("save_checkpoint_sync_count", 0) + if self._store_get("save_checkpoint_strategy") == "async": + self._store_set("save_checkpoint_async_count", 0) + + metrics_to_log = {"one_logger_utils_version": VERSION} + if self.mode & self.O_MINIMAL: + self._log_app_tag(self._store_get("app_tag")) + + # Metrics with default values provided + metrics_to_log["summary_data_schema_version"] = self._store_get( + "summary_data_schema_version", "1.0.0" + ) # Associate the code with the data schema version + metrics_to_log["app_run_type"] = self._store_get("app_run_type", "training") # Hard coded this to traininig + metrics_to_log["app_metrics_feature_tags"] = self._store_get( + "app_metrics_feature_tags", "full" + ) # Hard coded this to full + metrics_to_log["is_baseline_run"] = self._store_get("is_baseline_run") + metrics_to_log["is_train_iterations_enabled"] = self._store_get("is_train_iterations_enabled") + metrics_to_log["is_validation_iterations_enabled"] = self._store_get("is_validation_iterations_enabled") + metrics_to_log["is_test_iterations_enabled"] = self._store_get("is_test_iterations_enabled") + metrics_to_log["is_save_checkpoint_enabled"] = self._store_get("is_save_checkpoint_enabled") + metrics_to_log["is_log_throughput_enabled"] = self._store_get("is_log_throughput_enabled") + # Required metrics that must be specified + metrics_to_log["app_tag_run_version"] = self._store_get("app_tag_run_version") + metrics_to_log["world_size"] = self._store_get("world_size") + metrics_to_log["micro_batch_size"] = self._store_get("micro_batch_size", None) + metrics_to_log["global_batch_size"] = self._store_get("global_batch_size") + metrics_to_log["app_tag_run_name"] = self._store_get("app_tag_run_name") + metrics_to_log["train_iterations_target"] = self._store_get("train_iterations_target") + train_samples_target = self._store_get("train_samples_target") + metrics_to_log["train_samples_target"] = train_samples_target + + # Log seq length if available + if self._store_has_key("seq_length"): + seq_length = self._store_get("seq_length") + train_tokens_target = seq_length * train_samples_target + metrics_to_log["model_seq_length"] = seq_length + metrics_to_log["train_tokens_target"] = train_tokens_target + + if self.mode & self.O_CKPT: + metrics_to_log["save_checkpoint_strategy"] = self._store_get("save_checkpoint_strategy") + + if self._store_has_key("metadata"): + self._validate_metadata_and_update(metrics_to_log, self._store_get("metadata")) + + self._log_metrics(metrics_to_log) + + def _validate_metadata_and_update(self, target_to_update: dict, metadata: dict) -> dict: + """Validate metadata and update target dict. + + :param target_to_update: target dictionary to be updated with metadata. + :type target_update: dict + :param metadata: metadata of the current run + :type metadata: dict + :return: updated dict with metadata + :rtype: dict + """ + overlap_keys = set(target_to_update.keys()).intersection(metadata.keys()) + + if overlap_keys: + raise ValueError(f"Metadata overlap found with keys: {overlap_keys}") + + target_to_update.update(metadata) + return target_to_update + + @_check_enabled + def on_model_init_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the start of the model initialization. + + :param set_barrier: synchronize ranks before executing the callback. default to false. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_model_init_start_time**: The timestamp of starting model initialization. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + """ + self._on_start("model_init", set_barrier) + self._validate_and_store_args("on_model_init_start", metrics_input_kwargs) + if self._store_has_key("app_model_init_start_time"): + self._log_metrics({"app_model_init_start_time": self._store_get("app_model_init_start_time")}) + else: + self._log_metrics({"app_model_init_start_time": self.timer.get("model_init").get("start") * 1000}) + + @_check_enabled + def on_model_init_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of the model initialization. + + :param set_barrier: synchronize ranks before executing the callback. default to false. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_model_init_finish_time**: The timestamp of finishing model initialization. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + """ + self._on_end("model_init", set_barrier) + self._validate_and_store_args("on_model_init_end", metrics_input_kwargs) + + if self._store_has_key("app_model_init_finish_time"): + self._log_metrics({"app_model_init_finish_time": self._store_get("app_model_init_finish_time")}) + else: + self._log_metrics({"app_model_init_finish_time": self.timer.get("model_init").get("finish") * 1000}) + + @_check_enabled + def on_dataloader_init_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the start of the dataloader initialization. + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_build_dataiter_start_time**: The timestamp of starting dataloader initialization. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_start("dataloader_init", set_barrier) + self._validate_and_store_args("on_dataloader_init_start", metrics_input_kwargs) + + if self._store_has_key("app_build_dataiter_start_time"): + self._log_metrics({"app_build_dataiter_start_time": self._store_get("app_build_dataiter_start_time")}) + else: + self._log_metrics({"app_build_dataiters_start_time": self.timer.get("dataloader_init").get("start") * 1000}) + + @_check_enabled + def on_dataloader_init_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of the dataloader initialization. + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_build_dataiter_finish_time**: The timestamp of finishing dataloader initialization. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_end("dataloader_init", set_barrier) + self._validate_and_store_args("on_dataloader_init_end", metrics_input_kwargs) + if self._store_has_key("app_build_dataiters_finish_time"): + self._log_metrics({"app_build_dataiters_finish_time": self._store_get("app_build_dataiters_finish_time")}) + else: + self._log_metrics( + {"app_build_dataiters_finish_time": self.timer.get("dataloader_init").get("finish") * 1000} + ) + + @_check_enabled + def on_load_checkpoint_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics when start loading checkpoint. + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **load_checkpoint_start_time**: The timestamp of starting checkpoint loading. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_start("load_checkpoint", set_barrier) + self._validate_and_store_args("on_load_checkpoint_start", metrics_input_kwargs) + if self._store_has_key("load_checkpoint_start_time"): + self._log_metrics({"load_checkpoint_start_time": self._store_get("load_checkpoint_start_time")}) + else: + self._log_metrics({"load_checkpoint_start_time": self.timer.get("load_checkpoint").get("start") * 1000}) + + @_check_enabled + def on_load_checkpoint_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics when finish loading checkpoint. + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **load_checkpoint_finish_time**: The timestamp of finishing checkpoint loading. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_end("load_checkpoint", set_barrier) + self._validate_and_store_args("on_load_checkpoint_end", metrics_input_kwargs) + + if self._store_has_key("load_checkpoint_finish_time"): + load_checkpoint_finish_time = self._store_get("load_checkpoint_finish_time") + else: + load_checkpoint_finish_time = self.timer.get("load_checkpoint").get("finish") * 1000 + + if self._store_has_key("load_checkpoint_start_time"): + load_checkpoint_time = ( + load_checkpoint_finish_time - self._store_get("load_checkpoint_start_time") + ) / 1000 # in sec + else: + load_checkpoint_time = self.timer.get("load_checkpoint").get("total") # in sec + + self._log_metrics( + { + "load_checkpoint_finish_time": load_checkpoint_finish_time, + "load_checkpoint_time": load_checkpoint_time, + } + ) + + @_check_enabled + def on_optimizer_init_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the start of building optimizer + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_build_optimizer_start_time**: The timestamp of starting optimizer build. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_start("optimizer_init", set_barrier) + self._validate_and_store_args("on_optimizer_init_start", metrics_input_kwargs) + + if self._store_has_key("app_build_optimizer_start_time"): + self._log_metrics({"app_build_optimizer_start_time": self._store_get("app_build_optimizer_start_time")}) + else: + self._log_metrics({"app_build_optimizer_start_time": self.timer.get("optimizer_init").get("start") * 1000}) + + @_check_enabled + def on_optimizer_init_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of building optimizer + + :param set_barrier: synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_build_optimizer_finish_time**: The timestamp of finishing optimizer build. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_end("optimizer_init", set_barrier) + self._validate_and_store_args("on_optimizer_init_end", metrics_input_kwargs) + + if self._store_has_key("app_build_optimizer_finish_time"): + self._log_metrics({"app_build_optimizer_finish_time": self._store_get("app_build_optimizer_finish_time")}) + else: + self._log_metrics( + {"app_build_optimizer_finish_time": self.timer.get("optimizer_init").get("finish") * 1000} + ) + + @_check_enabled + def on_train_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, int]) -> None: + """ + Log metrics at the beginning of the train loop. + + :param set_barrier: synchronize ranks before executing the callback. default to false. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** (for minimal schema) must contain: + - **train_iterations_start**: the start iteration number. + - **train_samples_start**: the start sample number. + + """ + if rankpulse: + try: + rankpulse.start( + interval_seconds=int(os.getenv("RANKPULSE_INTERVAL_SECONDS", "15")), + twindow_seconds=int(os.getenv("RANKPULSE_TWINDOW_SECONDS", "300")), + enable_gpu_debug_info=True + if os.environ.get("RANKPULSE_GPU_DEBUG_INFO", "0").lower() in ["1", "true", "yes", "y"] + else False, + ) + except Exception as e: + print(f"WARNING: Failed to start rankpulse: {e}") + self._on_start("app_train_loop", set_barrier) + self._validate_and_store_args("on_train_start", metrics_input_kwargs) + + metrics_to_log = {} + if self.mode & self.O_MINIMAL: + batch_size = self._store_get("batch_size") + train_iterations_start = self._store_get("train_iterations_start") + self._store_set("train_iterations_start", train_iterations_start) + self._store_set("train_iterations_end", train_iterations_start) + if self._store_has_key("train_samples_start"): + self._store_set("train_samples_end", self._store_get("train_samples_start")) + else: + self._store_set( + "train_samples_start", + train_iterations_start * batch_size, + ) + self._store_set( + "train_samples_end", + train_iterations_start * batch_size, + ) + metrics_to_log["train_iterations_start"] = train_iterations_start + metrics_to_log["train_iterations_end"] = train_iterations_start + metrics_to_log["train_samples_start"] = self._store_get("train_samples_start") + metrics_to_log["train_samples_end"] = self._store_get("train_samples_end") + metrics_to_log["app_train_loop_start_time"] = self.timer.get("app_train_loop").get("start") * 1000 + if self._store_has_key("app_start_time"): + metrics_to_log["app_start_time"] = self._store_get("app_start_time") + if self.mode & self.O_THROUGHPUT: + batch_size = self._store_get("batch_size") + flops_per_sample = self._store_get("flops_per_sample") + # The initial value of num_floating_point_operations_so_far is nonzero if loading ckpt, while total_flops is always zero. + self._store_set( + "num_floating_point_operations_so_far", + train_iterations_start * batch_size * flops_per_sample, + ) + self._store_set("total_flops", 0) + metrics_to_log["train_tflop_start"] = float(self._store_get("num_floating_point_operations_so_far")) / ( + 10**12 + ) + self._log_metrics(metrics_to_log) + + @_check_enabled + def on_train_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of the train loop + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_end("app_train_loop", set_barrier) + self._validate_and_store_args("on_train_end", metrics_input_kwargs) + metrics_to_log = { + "app_train_loop_finish_time": self.timer.get("app_train_loop").get("finish") * 1000, + } + self._log_metrics(metrics_to_log) + + @_check_enabled + def on_train_batch_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the beginning of each train iteraion + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_start("train_iterations", set_barrier) + self._validate_and_store_args("on_train_batch_start", metrics_input_kwargs) + + @_check_enabled + def on_train_batch_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of each train iteraion + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_end("train_iterations", set_barrier) + self._validate_and_store_args("on_train_batch_end", metrics_input_kwargs) + + if self.mode & self.O_MINIMAL: + global_batch_size = self._store_get("global_batch_size") + self._store_set("train_iterations", self._store_get("train_iterations") + 1) + self._store_set( + "train_iterations_end", + self._store_get("train_iterations_start") + self._store_get("train_iterations"), + ) + self._store_set( + "train_samples", + self._store_get("train_samples") + global_batch_size, + ) + self._store_set( + "train_samples_end", + self._store_get("train_samples_start") + self._store_get("train_samples"), + ) + self._store_set( + "train_iterations_time_total", + self.timer.get("train_iterations").get("total"), + ) + if not self._store_has_key("first_logged_train_iterations_finish_time"): + self._store_set( + "first_logged_train_iterations_finish_time", + self.timer.get("train_iterations").get("finish") * 1000, + ) + if self._store_has_key("seq_length"): + self._store_set( + "train_tokens", + self._store_get("seq_length") * self._store_get("train_samples"), + ) + if self.mode & self.O_THROUGHPUT: + global_batch_size = self._store_get("global_batch_size") + flops_per_sample = self._store_get("flops_per_sample") + flops = global_batch_size * flops_per_sample + self._store_set( + "num_floating_point_operations_so_far", + self._store_get("num_floating_point_operations_so_far") + flops, + ) + self._store_set("total_flops", self._store_get("total_flops") + flops) + + metrics_to_log = self._get_metrics_on_train_batch_end() + + if self._store_get("train_iterations_end") % self.log_every_n_train_iterations == 0: + self._log_metrics(metrics_to_log) + + def _get_metrics_on_train_batch_end(self): + """Helper function to get all metrics needed to be tracked on_train_batch_end""" + metrics_to_log = {} + if self.mode & self.O_MINIMAL: + metrics_to_log["train_iterations"] = self._store_get("train_iterations") + metrics_to_log["train_iterations_end"] = self._store_get("train_iterations_start") + self._store_get( + "train_iterations" + ) + metrics_to_log["train_samples"] = self._store_get("train_samples") + metrics_to_log["train_samples_end"] = self._store_get("train_samples_start") + self._store_get( + "train_samples" + ) + metrics_to_log["train_iterations_time_total"] = self._store_get("train_iterations_time_total") + timer_data = self.timer.get("train_iterations") + timer_avg = timer_data.get("avg") + timer_min = timer_data.get("min") + timer_finish = timer_data.get("finish") + metrics_to_log["train_iterations_time_msecs_avg"] = timer_avg * 1000 if timer_avg is not None else 0 + metrics_to_log["train_iterations_time_msecs_min"] = timer_min * 1000 if timer_min is not None else 0 + metrics_to_log["first_logged_train_iterations_finish_time"] = self._store_get( + "first_logged_train_iterations_finish_time", default_value=0 + ) + metrics_to_log["last_logged_train_iterations_finish_time"] = ( + timer_finish * 1000 if timer_finish is not None else 0 + ) + if self._store_has_key("train_tokens"): + metrics_to_log["train_tokens"] = self._store_get("train_tokens") + self._log_app_tag(self._store_get("app_tag")) + if self.mode & self.O_THROUGHPUT: + train_iterations_time_total = self._store_get("train_iterations_time_total") + if train_iterations_time_total: + train_throughput_per_gpu = self._store_get("total_flops") / ( + train_iterations_time_total * 10**12 * self._store_get("world_size") + ) + else: + train_throughput_per_gpu = 0.0 + if not self._store_has_key("train_throughput_per_gpu_max"): + self._store_set("train_throughput_per_gpu_max", train_throughput_per_gpu) + else: + self._store_set( + "train_throughput_per_gpu_max", + max( + train_throughput_per_gpu, + self._store_get("train_throughput_per_gpu_max"), + ), + ) + metrics_to_log["train_tflop_end"] = float(self._store_get("num_floating_point_operations_so_far")) / ( + 10**12 + ) + metrics_to_log["train_tflop"] = float(self._store_get("total_flops")) / (10**12) + metrics_to_log["train_throughput_per_gpu"] = train_throughput_per_gpu + metrics_to_log["train_throughput_per_gpu_max"] = self._store_get("train_throughput_per_gpu_max") + + return metrics_to_log + + @_check_enabled + def on_validation_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the begininig of the validation loop + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_start("validation_loop", set_barrier) + self._validate_and_store_args("on_validation_start", metrics_input_kwargs) + + @_check_enabled + def on_validation_batch_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the beginning of each validation iteraion + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_start("validation_iterations", set_barrier) + self._validate_and_store_args("on_validation_batch_start", metrics_input_kwargs) + + @_check_enabled + def on_validation_batch_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]): + """Log metrics at the end of each validation iteraion + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_end("validation_iterations", set_barrier) + self._validate_and_store_args("on_validation_batch_end", metrics_input_kwargs) + self._store_set( + "validation_iterations", + self._store_get("validation_iterations") + 1, + ) + + @_check_enabled + def on_validation_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics at the end of the validation loop + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + """ + self._on_end("validation_loop", set_barrier) + self._validate_and_store_args("on_validation_end", metrics_input_kwargs) + self._store_set( + "validation_iterations_time_total", + self.timer.get("validation_iterations").get("total"), + ) + + metrics_to_log = { + "validation_iterations_time_total": self.timer.get("validation_iterations").get("total"), + "validation_iterations_time_msecs_avg": self.timer.get("validation_iterations").get("avg") * 1000, + "validation_iterations_time_msecs_min": self.timer.get("validation_iterations").get("min") * 1000, + } + self._log_metrics(metrics_to_log) + + @_check_enabled + def on_save_checkpoint_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """Log metrics before saving the chekpoint + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. Currently no input metrics needed. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** must contain: + - **global_step**: The saving iteration number. + + """ + assert self.mode & self.O_CKPT, ( + "Checkpoint saving is not enabled. Please ensure that `is_save_checkpoint_enabled = True` " + "in the config to enable this feature." + ) + + self._on_start("save_checkpoint", set_barrier) + self._validate_and_store_args("on_save_checkpoint_start", metrics_input_kwargs) + + # Make sure iteration related metrics is updated in DB for each checkpointing + metrics_to_update = self._get_metrics_on_train_batch_end() + self._log_metrics(metrics_to_update) + + global_step = self._store_get("global_step") + + self._store_set( + "save_checkpoint_count", + self._store_get("save_checkpoint_count") + 1, + ) + + productive_metrics = { + "train_iterations_productive_end": self._store_get("train_iterations_end"), + "train_samples_productive_end": self._store_get("train_samples_end"), + "train_iterations_time_total_productive": self._store_get("train_iterations_time_total"), + "validation_iterations_time_total_productive": self._store_get("validation_iterations_time_total"), + } + if self.mode & self.O_THROUGHPUT: + productive_metrics.update( + { + "train_tflop_productive_end": float(self._store_get("num_floating_point_operations_so_far")) + / (10**12) + } + ) + self._store_set(f"productive_metrics:{global_step}", productive_metrics) + metrics_to_log = { + "train_iterations_save_checkpoint_end": self._store_get("train_iterations_end"), + "save_checkpoint_count": self._store_get("save_checkpoint_count"), + } + self._log_metrics(metrics_to_log) + + @_check_enabled + def on_save_checkpoint_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, int]) -> None: + """ + Log metrics after saving the chekpoint + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** (for Saving Checkpoint-related Metrics) must contain: + - **global_step**: The saving iteration number. + + """ + assert self.mode & self.O_CKPT, ( + "Checkpoint saving is not enabled. Please ensure that `is_save_checkpoint_enabled = True` " + "in the config to enable this feature." + ) + + self._on_end("save_checkpoint", set_barrier) + self._validate_and_store_args("on_save_checkpoint_end", metrics_input_kwargs) + + global_step = self._store_get("global_step") + + metrics_to_log = {} + + self._store_set( + "save_checkpoint_sync_count", + self._store_get("save_checkpoint_sync_count") + 1, + ) + + productive_time = { + "save_checkpoint_sync_time_total_productive": self.timer.get("save_checkpoint").get("total"), + "successful_save_checkpoint_sync_finish_time": self.timer.get("save_checkpoint").get("finish") * 1000, + } + + self._store_set(f"productive_time:{global_step}", productive_time) + + + if self._store_has_key(f"on_save_checkpoint_success:{global_step}"): + successful_save_checkpoint_sync_finish_time = productive_time.pop( + "successful_save_checkpoint_sync_finish_time" + ) + metrics_to_log.update(productive_time) + + # Check and track first/last_successful_save_checkpoint_sync_finish_time + if not self._store_has_key("first_save_checkpoint_success"): + self._store_set("first_save_checkpoint_success", True) + metrics_to_log.update( + { + "first_saved_train_iterations_start_time": self.timer.get("app_train_loop").get("start") * 1000, + "first_successful_save_checkpoint_sync_finish_time": successful_save_checkpoint_sync_finish_time, + } + ) + metrics_to_log.update( + { + "last_successful_save_checkpoint_sync_finish_time": successful_save_checkpoint_sync_finish_time, + } + ) + self.one_logger.store_pop(f"on_save_checkpoint_success:{global_step}") + + metrics_to_log.update( + { + "save_checkpoint_sync_time_total": self.timer.get("save_checkpoint").get("total"), + "save_checkpoint_sync_time_min": self.timer.get("save_checkpoint").get("min"), + "save_checkpoint_sync_time_max": self.timer.get("save_checkpoint").get("max"), + "save_checkpoint_sync_count": self._store_get("save_checkpoint_sync_count"), + } + ) + + self._log_metrics(metrics_to_log) + + # Set flag for on_save_checkpoint_end call done + self._store_set(f"on_save_checkpoint_end:{global_step}", True) + + @_check_enabled + def on_save_checkpoint_success(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, int]) -> None: + """ + Log metrics after saving the chekpoint successfully + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** (for Saving Checkpoint-related Metrics) must contain: + - **global_step**: The saving iteration number. + + """ + assert self.mode & self.O_CKPT, ( + "Checkpoint saving is not enabled. Please ensure that `is_save_checkpoint_enabled = True` " + "in the config to enable this feature." + ) + + self._validate_and_store_args("on_save_checkpoint_success", metrics_input_kwargs) + + metrics_to_log = {} + + global_step = self._store_get("global_step") + + # Fetch productivity metrics cached on_save_checkpoint_start + productive_metrics = self.one_logger.store_pop(f"productive_metrics:{global_step}") + + + # Check if on_save_checkpoint_end is called. + if self._store_has_key(f"on_save_checkpoint_end:{global_step}"): + productive_time = self._store_get(f"productive_time:{global_step}") + successful_save_checkpoint_sync_finish_time = productive_time.pop( + "successful_save_checkpoint_sync_finish_time" + ) + productive_metrics.update(productive_time) + + # Check and track first/last_successful_save_checkpoint_sync_finish_time + if not self._store_has_key("first_save_checkpoint_success"): + self._store_set("first_save_checkpoint_success", True) + metrics_to_log.update( + { + "first_saved_train_iterations_start_time": self.timer.get("app_train_loop").get("start") * 1000, + "first_successful_save_checkpoint_sync_finish_time": successful_save_checkpoint_sync_finish_time, + } + ) + metrics_to_log.update( + { + "last_successful_save_checkpoint_sync_finish_time": successful_save_checkpoint_sync_finish_time, + } + ) + self.one_logger.store_pop(f"on_save_checkpoint_end:{global_step}") + + # Check need to update productive metrics for current step + need_update = True + if self._store_has_key("global_step_max"): + need_update = global_step > self._store_get("global_step_max") + + if need_update: + self._store_set("global_step_max", global_step) + metrics_to_log.update(productive_metrics) + + if self.mode & self.O_CKPT: + if self._store_get("save_checkpoint_strategy") == "async": + self._store_set( + "save_checkpoint_async_count", + self._store_get("save_checkpoint_async_count") + 1, + ) + metrics_to_log.update({"save_checkpoint_async_count": self._store_get("save_checkpoint_async_count")}) + + self._log_metrics(metrics_to_log) + + # Set flag for on_save_checkpoint_success call done + self._store_set(f"on_save_checkpoint_success:{global_step}", True) + + @_check_enabled + def on_app_start(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """ + Log metrics at the begining of application run. + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** must contain: + - **app_start_time**: The application start timestamp in ms. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + + """ + self._on_start("app_run", set_barrier) + self._validate_and_store_args("on_app_start", metrics_input_kwargs) + + if self._store_has_key("app_start_time"): + self._log_metrics({"app_start_time": self._store_get("app_start_time")}) + else: + self._log_metrics({"app_start_time": self.timer.get("app_run").get("start") * 1000.0}) + + @_check_enabled + def on_app_end(self, set_barrier: bool = False, **metrics_input_kwargs: Dict[str, Any]) -> None: + """ + Log metrics at the end of application run. + + :param set_barrier: Synchronize ranks before executing the callback. Default to False. NOTE: if this is set to True, `barrier` in `OneLoggerUtils` constructor must be set with correct callable object. + :type set_barrier: bool + + :param metrics_input_kwargs: Metrics needed for callback function invocation. + :type metrics_input_kwargs: dict + + **** + + **metrics_input_kwargs** could contain (optional): + - **app_finish_time**: The timestamp of finishing application run. If provide, OneLogger will use this value or it will use internal timer to get the timestamp. + """ + self._on_end("app_run", set_barrier) + self._validate_and_store_args("on_app_end", metrics_input_kwargs) + if self._store_has_key("app_finish_time"): + self._log_metrics({"app_finish_time": self._store_get("app_finish_time")}) + else: + self._log_metrics({"app_finish_time": self.timer.get("app_run").get("finish") * 1000.0}) + + @_check_enabled + def finish(self) -> None: + """ + Mark a OneLogger tracking as finished, and finish uploading all data with clean up. NOTE: Please remember to call this function explicitly to avoid potential data loss. + """ + if rankpulse: + try: + rankpulse.stop(timeout_seconds=3.0) + except Exception as e: + print(f"WARNING: Failed to stop rankpulse: {e}") + self.one_logger.finish() + + +def initialize_one_logger(config: dict) -> None: + """Initialize OneLoggerUtils object as a global variable""" + global one_logger + assert one_logger is None, "one_logger is already initialized" + + one_logger = OneLoggerUtils(config) + + return + + +def initialize_one_logger_from_imaginaire_config(config: Any) -> None: + """Initialize OneLoggerUtils object from imaginaire4 config""" + + try: + batch_size = config.dataloader_train.batch_size + except Exception as e: + logger.warning(e) + logger.warning("config.dataloader_train.batch_size does not exist. We substitute it with 1.") + # For edify video, image & video joint trianing configs do not have a fixed batch_size. + # We use a substitute batch_size = 1. + batch_size = 1 + + if parallel_state is None or not parallel_state.is_initialized(): + # Support environment without mcore installed or not initialized. + cp_size = 1 + tp_size = 1 + data_parallel_size = 1 + pp_size = 1 + else: + cp_size = parallel_state.get_context_parallel_world_size() + tp_size = parallel_state.get_tensor_model_parallel_world_size() + data_parallel_size = parallel_state.get_data_parallel_world_size() + pp_size = parallel_state.get_pipeline_model_parallel_world_size() + + micro_batch_size = batch_size / cp_size / tp_size + global_batch_size = batch_size * data_parallel_size + + job_size = ( + f"BS_{batch_size}" + + f"_microBS_{micro_batch_size}" + + f"_nGPUs_{get_world_size()}" + + f"_TP_{tp_size}" + + f"_CP_{cp_size}" + + f"_PP_{pp_size}" + ) + + # check if the environment is local / slurm / NGC. + slurm_job_id = os.environ.get("SLURM_JOB_ID") + ngc_job_id = os.environ.get("NGC_JOB_ID") + if slurm_job_id is not None: + job_environment = "slurm" + elif ngc_job_id is not None: + job_environment = "ngc" + else: # usually a local machine. + job_environment = os.uname().nodename + + # Check GPU information. + if torch.cuda.is_available(): + gpu_name = torch.cuda.get_device_name().replace(" ", "-") # ex) NVIDIA-A100-SXM4-80GB + else: + gpu_name = "none" + + app_tag_run_name = f"{config.job.project}/{config.job.group}/{config.job.name}" + app_tag_run_version = "0.0.0" # hard-coded app_tag_run version. + app_tag = f"{app_tag_run_name}/{job_size}/ENV_{job_environment}_GPU_{gpu_name}" + # Allow disabling async mode via environment variable to avoid BrokenPipeError issues + # Set ONE_LOGGER_ASYNC=false to use synchronous mode (more stable with wandb online) + one_logger_async = os.environ.get("ONE_LOGGER_ASYNC", "true").lower() == "true" + one_logger_config = dict( + enable_for_current_rank=(get_rank() == 0), # global master rank + one_logger_project="imaginaire4", # wandb project name for OneLogger + one_logger_run_name=f"{config.job.project}/{config.job.group}/{config.job.name}", + one_logger_async=one_logger_async, # Set ONE_LOGGER_ASYNC=false env var to avoid pipe issues + log_every_n_train_iterations=config.trainer.logging_iter, + barrier=None, + app_tag=app_tag, # jobs with same app_tag are expected to have similar perf. + app_tag_run_name=app_tag_run_name, # variations within an app_tag + app_tag_run_version=app_tag_run_version, + app_start_time=round(time.time() * 1000), # timestamp in ms + world_size=get_world_size(), + micro_batch_size=micro_batch_size, + batch_size=batch_size, + global_batch_size=global_batch_size, + train_iterations_target=config.trainer.max_iter, + train_samples_target=config.trainer.max_iter * global_batch_size, + is_baseline_run=False, + is_train_iterations_enabled=True, + is_validation_iterations_enabled=True, + is_test_iterations_enabled=False, + is_save_checkpoint_enabled=True, + save_checkpoint_strategy="async", + is_log_throughput_enabled=False, + ) + initialize_one_logger(one_logger_config) + + return + + +def one_logger_is_initialized() -> bool: + return one_logger is not None + + +def destroy_one_logger() -> None: + global one_logger + one_logger = None + + return + + +def get_one_logger() -> OneLoggerUtils: + return one_logger diff --git a/cosmos_training/cosmos/utils/optim_instantiate.py b/cosmos_training/cosmos/utils/optim_instantiate.py new file mode 100644 index 00000000..4305a990 --- /dev/null +++ b/cosmos_training/cosmos/utils/optim_instantiate.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hydra +import torch +from torch import nn + +from cosmos.utils import log + + +def get_regular_param_group(net: nn.Module): + """ + seperate the parameters of the network into two groups: decay and no_decay. + based on nano_gpt codebase. + """ + param_dict = {pn: p for pn, p in net.named_parameters()} + param_dict = {pn: p for pn, p in param_dict.items() if p.requires_grad} + + decay_params = [p for n, p in param_dict.items() if p.dim() >= 2] + nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2] + return decay_params, nodecay_params + + +def get_base_optimizer( + model: nn.Module, + lr: float, + weight_decay: float, + optim_type: str = "adamw", + sharding: bool = False, + **kwargs, +) -> torch.optim.Optimizer: + net_decay_param, net_nodecay_param = get_regular_param_group(model) + + num_decay_params = sum(p.numel() for p in net_decay_param) + num_nodecay_params = sum(p.numel() for p in net_nodecay_param) + net_param_total = num_decay_params + num_nodecay_params + log.critical(f"total num parameters : {net_param_total:,}") + + param_group = [ + { + "params": net_decay_param + net_nodecay_param, + "lr": lr, + "weight_decay": weight_decay, + }, + ] + + if optim_type == "adamw": + opt_cls = torch.optim.AdamW + elif optim_type == "fusedadam": + from cosmos.utils.fused_adam import FusedAdam + + opt_cls = FusedAdam + else: + raise ValueError(f"Unknown optimizer type: {optim_type}") + + return opt_cls(param_group, **kwargs) + + +def get_base_scheduler( + optimizer: torch.optim.Optimizer, + model: nn.Module, + scheduler_config: dict, +): + net_scheduler = hydra.utils.instantiate(scheduler_config) + net_scheduler.model = model + + num_param_groups = len(optimizer.param_groups) + + return torch.optim.lr_scheduler.LambdaLR( + optimizer, + lr_lambda=[ + net_scheduler.schedule, + ] + * num_param_groups, + ) diff --git a/cosmos_training/cosmos/utils/profiling.py b/cosmos_training/cosmos/utils/profiling.py new file mode 100644 index 00000000..6bff99fe --- /dev/null +++ b/cosmos_training/cosmos/utils/profiling.py @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import os +import time + +import torch + +from cosmos.utils import distributed, log +from cosmos.utils.easy_io import easy_io + +# (qsh 2024-11-23) credits +# https://github.com/pytorch/torchtitan/blob/main/torchtitan/profiling.py + +# how much memory allocation/free ops to record in memory snapshots +MEMORY_SNAPSHOT_MAX_ENTRIES = 100000 + + +@contextlib.contextmanager +def maybe_enable_profiling(config, *, global_step: int = 0): + # get user defined profiler settings + enable_profiling = config.trainer.profiling.enable_profiling + profile_freq = config.trainer.profiling.profile_freq + + if enable_profiling: + trace_dir = os.path.join(config.job.path_local, "torch_trace") + if distributed.get_rank() == 0: + os.makedirs(trace_dir, exist_ok=True) + + rank = distributed.get_rank() + + def trace_handler(prof): + curr_trace_dir_name = "iteration_" + str(prof.step_num) + curr_trace_dir = os.path.join(trace_dir, curr_trace_dir_name) + if not os.path.exists(curr_trace_dir): + os.makedirs(curr_trace_dir, exist_ok=True) + + log.info(f"Dumping traces at step {prof.step_num}") + begin = time.monotonic() + if rank in config.trainer.profiling.target_ranks: + prof.export_chrome_trace(f"{curr_trace_dir}/rank{rank}_trace.json.gz") + log.info(f"Finished dumping traces in {time.monotonic() - begin:.2f} seconds") + + log.info(f"Profiling active. Traces will be saved at {trace_dir}") + + if not os.path.exists(trace_dir): + os.makedirs(trace_dir, exist_ok=True) + + warmup, active = config.trainer.profiling.profile_warmup, 1 + wait = profile_freq - (active + warmup) + assert wait >= 0, "profile_freq must be greater than or equal to warmup + active" + + with torch.profiler.profile( + activities=[ + torch.profiler.ProfilerActivity.CPU, + torch.profiler.ProfilerActivity.CUDA, + ], + schedule=torch.profiler.schedule(wait=wait, warmup=warmup, active=active), + on_trace_ready=trace_handler, + record_shapes=config.trainer.profiling.record_shape, + profile_memory=config.trainer.profiling.profile_memory, + with_stack=config.trainer.profiling.with_stack, + with_modules=config.trainer.profiling.with_modules, + ) as torch_profiler: + torch_profiler.step_num = global_step + yield torch_profiler + else: + torch_profiler = contextlib.nullcontext() + yield None + + +@contextlib.contextmanager +def maybe_enable_memory_snapshot(config, *, global_step: int = 0): + enable_snapshot = config.trainer.profiling.enable_memory_snapshot + if enable_snapshot: + if config.trainer.profiling.save_s3: + snapshot_dir = "s3://rundir" + else: + snapshot_dir = os.path.join(config.job.path_local, "memory_snapshot") + if distributed.get_rank() == 0: + os.makedirs(snapshot_dir, exist_ok=True) + + rank = torch.distributed.get_rank() + + class MemoryProfiler: + def __init__(self, step_num: int, freq: int): + torch.cuda.memory._record_memory_history(max_entries=MEMORY_SNAPSHOT_MAX_ENTRIES) + # when resume training, we start from the last step + self.step_num = step_num + self.freq = freq + + def step(self, exit_ctx: bool = False): + self.step_num += 1 + if not exit_ctx and self.step_num % self.freq != 0: + return + if not exit_ctx: + curr_step = self.step_num + dir_name = f"iteration_{curr_step}" + else: + # dump as iteration_0_exit if OOM at iter 1 + curr_step = self.step_num - 1 + dir_name = f"iteration_{curr_step}_exit" + curr_snapshot_dir = os.path.join(snapshot_dir, dir_name) + if not config.trainer.profiling.save_s3 and not os.path.exists(curr_snapshot_dir): + os.makedirs(curr_snapshot_dir, exist_ok=True) + log.info(f"Dumping memory snapshot at step {curr_step}") + begin = time.monotonic() + + if rank in config.trainer.profiling.target_ranks: + easy_io.dump( + torch.cuda.memory._snapshot(), + f"{curr_snapshot_dir}/rank{rank}_memory_snapshot.pickle", + ) + log.info(f"Finished dumping memory snapshot in {time.monotonic() - begin:.2f} seconds") + + log.info(f"Memory profiler active. Snapshot will be saved at {snapshot_dir}") + profiler = MemoryProfiler(global_step, config.trainer.profiling.profile_freq) + try: + yield profiler + except torch.cuda.OutOfMemoryError as e: + profiler.step(exit_ctx=True) + else: + yield None + + +@contextlib.contextmanager +def maybe_enable_nsys_profiling(config, *, global_step: int = 0): + """Context manager for Nsight Systems profiling via cudaProfilerStart/Stop. + + Usage: launch training with + nsys profile --capture-range=cudaProfilerApi --capture-range-end=stop python ... + and set trainer.profiling.enable_nsys=true, profile_freq=. + + Reuses the torch-profile flags (profile_freq, target_ranks, profile_warmup). + The profiler is started `profile_warmup` iterations before the target and + stopped right after it. + """ + enable_nsys = config.trainer.profiling.enable_nsys + if not enable_nsys: + yield None + return + + rank = distributed.get_rank() + target_ranks = config.trainer.profiling.target_ranks + freq = config.trainer.profiling.profile_freq + warmup = config.trainer.profiling.profile_warmup + + active_iter = freq - 1 # profile_freq=5001 profiles iter 5000 + start_iter = max(0, active_iter - warmup) + + class NsysProfiler: + def __init__(self, step_num: int): + self.step_num = step_num + self._profiling = False + + def step(self): + self.step_num += 1 + if rank not in target_ranks: + return + if self.step_num == start_iter and not self._profiling: + log.info(f"[Nsys] Starting CUDA profiler at iter {self.step_num} (active iter: {active_iter})") + torch.cuda.cudart().cudaProfilerStart() + self._profiling = True + if self.step_num == active_iter + 1 and self._profiling: + torch.cuda.cudart().cudaProfilerStop() + self._profiling = False + log.info(f"[Nsys] Stopped CUDA profiler at iter {self.step_num}") + + log.info(f"[Nsys] Profiling enabled. Will capture iter {start_iter}-{active_iter} on ranks {target_ranks}") + profiler = NsysProfiler(global_step) + try: + yield profiler + finally: + if profiler._profiling: + torch.cuda.cudart().cudaProfilerStop() diff --git a/cosmos_training/cosmos/utils/progress_bar.py b/cosmos_training/cosmos/utils/progress_bar.py new file mode 100644 index 00000000..a10683ee --- /dev/null +++ b/cosmos_training/cosmos/utils/progress_bar.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Progress bar wrapper that gets automatically disabled when in a Timer region, or any other context +where we'd want to disable progress bars, including when TQDM is not present, or when user sets +DISABLE_TQDM=1. We can eventually add a simple ascii progress bar as fallback for missing +dependencies. +""" + +import os + +from cosmos.utils import distributed +from cosmos.utils.timer import in_timer_region + +try: + import tqdm as _tqdm # noqa: F401 + + HAS_TQDM = True +except ImportError: + HAS_TQDM = False +except Exception as e: + HAS_TQDM = True + + +def _tqdm_wrapper(*args, **kwargs): + if HAS_TQDM: + import tqdm + + return tqdm.tqdm(*args, **kwargs) + + raise ImportError("TQDM is not installed. Please install it and try again.") + + +def progress_bar(fn, desc=None, total=None, force_display: bool = False): + """ + Progress bars a great, but they're not for everybody, certainly not for everywhere. + They must be guarded against: + * We're benchmarking performance (with Timer) + * If tqdm / other progress bars aren't available, skip instead of failing. + * If multi-process / GPU, only one (usually rank 0) must display it, just like prints. + * If the user just doesn't want progress bars (toggle via environment variables. + + This function consideres all of those cases + """ + + disable_tqdm = os.environ.get("DISABLE_TQDM", "0") == "1" + is_in_timer_region = in_timer_region() + is_rank0 = True + + # Wide-scope try/except on determining rank, in case distributed context is uninitialized in a + # single-process program. If exception occurs, it's better to just assume single-process. + try: + is_rank0 = distributed.get_rank() == 0 + except Exception as e: + pass + + if not force_display and (not is_rank0 or is_in_timer_region or disable_tqdm): + return fn + + return _tqdm_wrapper(fn, desc=desc, total=total) + + +__all__ = ["progress_bar"] diff --git a/cosmos_training/cosmos/utils/serialization.py b/cosmos_training/cosmos/utils/serialization.py new file mode 100644 index 00000000..7a4ebaeb --- /dev/null +++ b/cosmos_training/cosmos/utils/serialization.py @@ -0,0 +1,447 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import functools +import importlib +import json +import os +from collections.abc import Callable as Callable2 +from collections.abc import Mapping, Sequence +from dataclasses import fields, is_dataclass +from types import UnionType +from typing import Any, List, Optional, TypeVar, Union, get_args, get_origin + +import attrs +import torch +import yaml +from omegaconf import DictConfig, ListConfig, OmegaConf + +from cosmos.utils.lazy_config import LazyCall, LazyDict, instantiate +from cosmos.utils.lazy_config.lazy import get_default_params + +T = TypeVar("T") + + +def from_dict( + x: dict, clazz: str | type | None = None, force_construct_target: bool | None = None, field_name: str = "" +) -> T: ... +def to_dict(x: T, field_name: str = "", hydra_compat: bool = True) -> dict: ... +def from_yaml(path: str | None = None, clazz: type | None = None, file_like_or_str=None) -> T: + if path: + assert os.path.exists(path), f"{path} does not exist" + with open(path) as in_f: + return from_dict(yaml.safe_load(in_f), clazz=clazz) + elif file_like_or_str: + return from_dict(yaml.safe_load(file_like_or_str), clazz=clazz) + else: + raise ValueError("expected file_like_or_str or path to not be None") + + +def _yaml_safe(obj: Any) -> Any: + # primitives + if obj is None or isinstance(obj, (bool, int, float, str)): + return obj + + # dict-like + if isinstance(obj, Mapping): + return {str(k): _yaml_safe(v) for k, v in obj.items()} + + # list/tuple-like (but not strings/bytes) + if isinstance(obj, Sequence) and not isinstance(obj, (str, bytes, bytearray)): + return [_yaml_safe(v) for v in obj] + + # classes / functions / bound methods -> import path + if hasattr(obj, "__module__") and hasattr(obj, "__qualname__"): + return f"{obj.__module__}.{obj.__qualname__}" + + # torch dtype, Path, enums, dataclasses, etc. + return str(obj) + + +def to_yaml(config: T, out_path: str | None = None) -> str | None: + config_dict = to_dict(config) + safe_dict = _yaml_safe(config_dict) + + if out_path is not None: + with open(out_path, "w") as f: + yaml.safe_dump( + safe_dict, + f, + sort_keys=False, + default_flow_style=False, + allow_unicode=True, + ) + return None + + return yaml.safe_dump( + safe_dict, + sort_keys=False, + default_flow_style=False, + allow_unicode=True, + ) + + +def load_callable(name: str) -> Callable2 | None: + if not name: + return None + + idx = name.rfind(".") + assert idx != -1, "expected ." + module_name = name[0:idx] + fn_name = name[idx + 1 :] + mod = importlib.import_module(module_name) + return getattr(mod, fn_name) + + +def maybe_load_callable(name: str | Callable2 | None) -> Callable2 | None: + if isinstance(name, str): + return load_callable(name) + + return name + + +def maybe_idx(x: Any, idx: int) -> Any: + if idx < 0 or idx >= len(x): + return None + return x[idx] + + +def is_attrs(x: Any) -> bool: + return hasattr(x, "__attrs_attrs__") + + +def to_qualitified_name(x) -> str: + # Handle functools.partial explicitly + if isinstance(x, functools.partial): + fn = x.func + fn_name = to_qualitified_name(fn) + + # args/keywords may contain non-serializable stuff; stringify safely + args = [] + if x.args: + args = [repr(a) for a in x.args] + + kwargs = {} + if x.keywords: + kwargs = {str(k): repr(v) for k, v in x.keywords.items()} + + if args or kwargs: + return f"functools.partial({fn_name}, args={args}, kwargs={kwargs})" + return f"functools.partial({fn_name})" + + # Normal callable/class/module qualified name + mod = getattr(x, "__module__", None) + qn = getattr(x, "__qualname__", None) + + if mod and qn: + return f"{mod}.{qn}" + + # Some callables only have __name__ + name = getattr(x, "__name__", None) + if mod and name: + return f"{mod}.{name}" + + # Fallback: repr + return repr(x) + + +def is_optional(x: type) -> bool: + origin = get_origin(x) + args = get_args(x) + return origin is Optional or (origin in (Union, UnionType) and len(args) == 2 and type(None) in args) + + +def _to_dict_value(x: T, field_type: type, metadata: dict, field_name: str = ""): + + t = type(x) + + # attrs specific + if x is attrs.NOTHING or x is None: + return None + # torch specifics + elif field_type in (torch.memory_format, torch.dtype): + return str(x) + # i4 specific types + elif field_type == LazyCall: + result = _to_dict_value(x, field_type._target, metadata, field_name) + return result + elif field_type in (DictConfig, LazyDict): + if "_target_" in x: + default_params = get_default_params(x["_target_"]) + for default_key, default_v in default_params.items(): + if default_key not in x: + x[default_key] = default_v + result = _to_dict_value(x, dict, metadata, field_name) + object_type = getattr(x._metadata, "object_type", None) + if object_type and (is_dataclass(object_type) or is_attrs(object_type)): + result.setdefault("_target_", to_qualitified_name(object_type)) + return result + elif field_type == ListConfig: + return _to_dict_value(x, list, metadata, field_name) + # general python types + dataclasses + attrs + # * meta types + elif field_type == type or field_type == abc.ABCMeta: + + return to_qualitified_name(x) + elif get_origin(field_type) is type: + return to_qualitified_name(x) + elif callable(x) or get_origin(field_type) is Callable2: + if callable(x): + return to_qualitified_name(x) + else: + assert isinstance(x, str), f"{x.__class__=}" + return x + elif is_dataclass(t) or is_attrs(t): + return to_dict(x, field_name=field_name) + # * built-in composites types + elif is_optional(field_type): + return _to_dict_value(x, get_args(field_type)[0], metadata) + elif get_origin(field_type) in (Union, UnionType): + raise AssertionError("unions are not implemented yet!") + # * primitives + elif t in (dict,) or field_type in (dict,) or get_origin(field_type) in (dict,): + return { + _to_dict_value( + k, + maybe_idx(get_args(field_type), 0) or type(k), + metadata, + field_name=f"{field_name}.{k}.key", + ): _to_dict_value( + v, + maybe_idx(get_args(field_type), 1) or type(v), + metadata, + field_name=f"{field_name}.{k}", + ) + for k, v in x.items() + } + elif ( + t + in ( + tuple, + list, + ) + or field_type + in ( + tuple, + list, + ) + or get_origin(field_type) in (tuple, list) + ): + if field_type is None or field_type not in ( + tuple, + list, + ): + field_type = list + + return field_type( + [ + _to_dict_value(xx, maybe_idx(get_args(field_type), 0) or type(xx), metadata, field_name + f"[{i}]") + for i, xx in enumerate(x) + ] + ) + elif field_type in (int, str, float, bool): + result = field_type(x) + return result + else: # catch all for everything else + return x + + +def to_dict(x: T, field_name: str = "", hydra_compat: bool = True) -> dict: + if is_dataclass(x): + result = {} + if hydra_compat: + result["_target_"] = to_qualitified_name(x.__class__) + for f in fields(x): + + if hydra_compat and f.name == "defaults": + continue + result[f.name] = _to_dict_value( + x.__dict__[f.name], + f.type, + f.metadata, + field_name=field_name + f".{f.name}" if field_name else f.name, + ) + return result + elif is_attrs(x): + # references: + # - https://github.com/python-attrs/attrs/blob/main/src/attr/_funcs.py + attrs.resolve_types(x.__class__) + + result = {} + if hydra_compat: + result["_target_"] = to_qualitified_name(x.__class__) + for f in attrs.fields(x.__class__): + + if hydra_compat and f.name == "defaults": + continue + result[f.name] = _to_dict_value( + getattr(x, f.name), + f.type, + f.metadata, + field_name=field_name + f".{f.name}" if field_name else f.name, + ) + return result + + +def _from_dict_value( + x: T, + field_type: type, + concrete_type: type, + field_name: str, + force_construct_target: bool | None = None, +): + + + is_dc_type = is_dataclass(field_type) + is_attrs_type = is_attrs(field_type) + origin = get_origin(field_type) or field_type + args = get_args(field_type) + + if x is None: + return None + elif field_type in (torch.memory_format, torch.dtype): + return maybe_load_callable(x) + elif field_type == LazyCall: + return _from_dict_value(x, field_type._target, concrete_type, field_name=field_name) + elif is_dc_type or is_attrs_type: + if concrete_type == str: + assert isinstance(x, str) + if x.endswith(".json"): + json_value = json.loads(x) + return from_dict( + json_value, field_type, force_construct_target=force_construct_target, field_name=field_name + ) + elif x.endswith(".yaml"): + yaml_value = yaml.safe_load(x) + return from_dict( + yaml_value, field_type, force_construct_target=force_construct_target, field_name=field_name + ) + else: + raise AssertionError(f"unexpected string: {x}") + else: + assert not isinstance(x, str) + return from_dict(x, field_type, field_name=field_name) + elif field_type in (DictConfig, LazyDict) or origin in (dict,): + + construct_target = x.get("_recursive_", field_type == DictConfig) + if force_construct_target is not None: + construct_target = force_construct_target + + target_value = x.get("_target_") + target_cls = maybe_load_callable(target_value) + + if target_value and construct_target and (is_dataclass(target_cls) or is_attrs(target_cls)): + result = from_dict(x, target_cls, force_construct_target=force_construct_target, field_name=field_name) + else: + result = { + _from_dict_value( + k, + maybe_idx(get_args(field_type), 0) or type(k), + type(k), + field_name=f"{field_name}.{k}.key", + force_construct_target=construct_target, + ): _from_dict_value( + v, + maybe_idx(get_args(field_type), 1) or type(v), + type(v), + field_name=f"{field_name}.{k}", + force_construct_target=construct_target, + ) + for k, v in x.items() + } + if field_type in (DictConfig, LazyDict): + result = OmegaConf.structured(result, flags={"allow_objects": True}) + if construct_target: + result = instantiate(result) + if "_target_" in result: + result["_target_"] = maybe_load_callable(result["_target_"]) + elif construct_target and target_cls: # instantiate a regular class from a dict + special_keys = { + "_target_", + "_recursive_", + "_convert_", + "_args_", + "_kwargs_", + } + constructable_items = { + k: v for k, v in result.items() if not (isinstance(k, str) and k in special_keys) + } + result = target_cls(**constructable_items) + return result + elif field_type is ListConfig or origin in ( + list, + List, + ): + return [ + _from_dict_value( + xx, maybe_idx(get_args(field_type), 0) or type(xx), type(xx), field_name=f"{field_type}[{i}]" + ) + for i, xx in enumerate(x) + ] + elif is_optional(field_type): + return _from_dict_value(x, args[0], type(x), field_name=field_name) + elif origin in (Union, UnionType): + raise AssertionError("unions are not implemented yet!") + elif origin is Callable2 or origin is type: + return maybe_load_callable(x) + elif field_type in (int, float, str, bool): + return x + elif field_type is type(None) or field_type == Any: # no typing + return x + else: + raise TypeError( + f"unexpected type: {field_type} (origin={origin}, concrete_type={concrete_type}, args={args}, x={x})" + ) + + +def from_dict( + x: dict, clazz: type | None = None, force_construct_target: bool | None = None, field_name: str = "" +) -> T: + if clazz is None: + assert "_target_" in x + clazz = maybe_load_callable(x["_target_"]) + + assert is_dataclass(clazz) or is_attrs(clazz), f"{clazz} is not a dataclass or attrs" + if is_dataclass(clazz): + construct_args = {} + for f in fields(clazz): + if f.name in x: + construct_args[f.name] = _from_dict_value( + x[f.name], + f.type, + type(x[f.name]), + field_name=field_name + "." + f.name if field_name else f.name, + force_construct_target=force_construct_target, + ) + elif is_optional(f.type): + construct_args[f.name] = None + return clazz(**construct_args) + elif is_attrs(clazz): + attrs.resolve_types(clazz) + + construct_args = {} + for f in attrs.fields(clazz): + if f.name in x: + construct_args[f.name] = _from_dict_value( + x[f.name], + f.type, + type(x[f.name]), + field_name=field_name + "." + f.name if field_name else f.name, + force_construct_target=force_construct_target, + ) + elif is_optional(f.type): + construct_args[f.name] = None + return clazz(**construct_args) diff --git a/cosmos_training/cosmos/utils/timer.py b/cosmos_training/cosmos/utils/timer.py new file mode 100644 index 00000000..2430d2ac --- /dev/null +++ b/cosmos_training/cosmos/utils/timer.py @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Timer: helps measure CPU and CUDA times easily and reliably. +""" + +import time +from contextlib import ContextDecorator +from contextvars import ContextVar +from functools import wraps +from typing import Callable + +import torch + +from cosmos.utils import log + +_timer_active = ContextVar("_timer_active", default=False) + + +def in_timer_region() -> bool: + return _timer_active.get() + + +def _autoformat_time_us(time_us: float) -> str: + """ + Automatically format time in nanoseconds. + """ + if time_us >= 1e6: + time_s = time_us * 1e-6 + return f"{time_s:.2f} s" + + if time_us >= 1e3: + time_ms = time_us * 1e-3 + return f"{time_ms:.2f} ms" + + return f"{time_us:.2f} us" + + +def format_time_str(time_us: float, unit: str | None = None) -> str: + """ + Automatically format time in nanoseconds either automatically or based on + desired unit. + """ + if unit is None: + return _autoformat_time_us(time_us) + + if unit == "us": + return f"{time_us:.2f} us" + + if unit == "ms": + return f"{time_us * 1e-3:.2f} ms" + + if unit == "s": + return f"{time_us * 1e-6:.2f} s" + + raise NotImplementedError(f"Time unit {unit} is not supported.") + + +def format_time(time_us: float, unit: str) -> float: + """ + Format time in nanoseconds based on desired unit. + """ + + if unit == "us": + return time_us + + if unit == "ms": + return time_us * 1e-3 + + if unit == "s": + return time_us * 1e-6 + + raise NotImplementedError(f"Time unit {unit} is not supported.") + + +class Timer(ContextDecorator): + """ + Reliable CPU and CUDA Timer. + + Args: + tag (str | None): Optional tag used in logs/prints. + + measure_cpu (bool): Whether to measure CPU time (using `time`). Default: `True`. + + measure_cuda (bool): Whether to measure CUDA time (using CUDA events). Default: `True`. + + unit (str | None): Optional time unit. Must be either "s" (seconds), "ms" (microseconds), + "us" (nanoseconds), or None (format automatically based on value). + + debug (bool): Whether to log results in debug mode instead of info. Default is False. + + Examples: + ```python + with Timer(measure_cpu=True, measure_cuda=True, unit="ms"): + model(x) + ``` + + ```python + @Timer(measure_cpu=True, measure_cuda=True, unit="ms") + def func(x): + return model(x) + ``` + """ + + def __init__( + self, + tag: str | None = None, + measure_cpu: bool = True, + measure_cuda: bool = True, + unit: str | None = None, + debug: bool = False, + ): + self.measure_cpu = measure_cpu + self.measure_cuda = measure_cuda + + self.measured = False + self.cpu_time_us = 0 + self.cuda_time_us = 0 + + self.busy = False + self.cpu_time_start = None + self.cuda_start_event = None + self.cuda_end_event = None + self.cuda_stream = None + + self.tag = "unknown" if tag is None else tag + self.unit = unit + if self.unit is not None and self.unit not in ["s", "ms", "us"]: + raise NotImplementedError(f"Time unit {self.unit} is not supported.") + + self.debug = debug + + def _log(self, msg: str): + if self.debug: + log.debug(msg) + else: + log.info(msg) + + def __enter__(self): + self.token = _timer_active.set(True) + self.start() + + def __exit__(self, exc_type, exc_value, traceback): + self.end() + self.report() + _timer_active.reset(self.token) + + def __call__(self, func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN202 + self.start() + result = func(*args, **kwargs) + self.end() + self.report() + return result + + return wrapper # type: ignore + + def report(self): + """ + Reports measurements. + """ + if self.measure_cpu and self.measure_cuda: + self._log(f"Time spent on {self.tag}: CPU: {self.get_cpu_time_str()}, CUDA: {self.get_cuda_time_str()}") + elif self.measure_cpu: + self._log(f"Time spent on {self.tag}: {self.get_cpu_time_str()}") + elif self.measure_cuda: + self._log(f"CUDA time spent on {self.tag}: {self.get_cuda_time_str()}") + else: + raise NotImplementedError() + + def get_cpu_time(self) -> float: + """ + Returns CPU time measurement. + """ + if not self.measure_cpu: + raise RuntimeError(f"CPU timer is disabled ({self.measure_cpu=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + if self.unit is None: + raise RuntimeError("No unit was specified. Please use get_cpu_time_str() instead.") + + assert self.unit is not None + return format_time(self.cpu_time_us, unit=self.unit) + + def get_cuda_time(self) -> float: + """ + Returns CUDA time measurement. + """ + if not self.measure_cuda: + raise RuntimeError(f"CUDA timer is disabled ({self.measure_cuda=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + if self.unit is None: + raise RuntimeError("No unit was specified. Please use get_cuda_time_str() instead.") + + assert self.unit is not None + return format_time(self.cuda_time_us, unit=self.unit) + + def get_cpu_time_str(self) -> str: + """ + Returns CPU time measurement in string format. + """ + if not self.measure_cpu: + raise RuntimeError(f"CPU timer is disabled ({self.measure_cpu=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + return format_time_str(self.cpu_time_us, unit=self.unit) + + def get_cuda_time_str(self) -> str: + """ + Returns CUDA time measurement in string format. + """ + if not self.measure_cuda: + raise RuntimeError(f"CUDA timer is disabled ({self.measure_cuda=}).") + + if not self.measured: + raise RuntimeError("No measurements were made yet!") + + return format_time_str(self.cuda_time_us, unit=self.unit) + + def reset(self): + """ + Resets recorded measurements + """ + self.measured = False + self.cpu_time_us = 0 + self.cuda_time_us = 0 + + def start(self, cuda_device: torch.device | None = None, cuda_stream: torch.cuda.Stream | None = None): + """ + Start time measurements. + + Args: + cuda_device (torch.device | None): CUDA device. Will use default CUDA device if not indicated. + + cuda_stream (torch.cuda.Stream | None): CUDA stream to use for CUDA time measurement. + Will use default stream for current CUDA device if not indicated. + """ + if self.busy: + raise RuntimeError("Already called Timer.start() once!") + + self.busy = True + + if self.measure_cuda: + self.cuda_stream = cuda_stream if cuda_stream is not None else torch.cuda.current_stream(cuda_device) + self.cuda_stream.synchronize() + + if self.measure_cpu: + self.cpu_time_start = time.time() + + if self.measure_cuda: + self.cuda_start_event = torch.cuda.Event(enable_timing=True) + self.cuda_end_event = torch.cuda.Event(enable_timing=True) + self.cuda_stream.record_event(self.cuda_start_event) + + def end(self): + """ + Ends time measurements. + + NOTE: must be done on the same CUDA device and stream as start(). + """ + if not self.busy: + raise RuntimeError("Timer.start() must be called exactly once before end()!") + + if self.measure_cuda: + self.cuda_stream.record_event(self.cuda_end_event) + self.cuda_end_event.synchronize() + + if self.measure_cpu: + self.cpu_time_end = time.time() + self.cpu_time_us = (self.cpu_time_end - self.cpu_time_start) * 1e6 + + if self.measure_cuda: + self.cuda_time_us = self.cuda_start_event.elapsed_time(self.cuda_end_event) * 1e3 + + self.busy = False + self.measured = True diff --git a/cosmos_training/cosmos/utils/training_telemetry/README.md b/cosmos_training/cosmos/utils/training_telemetry/README.md new file mode 100644 index 00000000..a8496440 --- /dev/null +++ b/cosmos_training/cosmos/utils/training_telemetry/README.md @@ -0,0 +1,122 @@ +# Training Telemetry + +Training Telemetry is a utility [library](https://gitlab-master.nvidia.com/ai-efficiency/training_telemetry) for tracking and recording training metrics, events, and performance data during model training. The code in this folder is a wrapper for this library, primarily to adapt it to existing imaginaire4 code through a function decorator, a callback implementation, and context managers. + +The code in this folder also gracefully handles cases where the library is not available or training telemetry is disabled. + +The library provides a standardized way to monitor and analyze the training process that backend infrastructure components such as Heimdall can use to manage applications and report training KPIs. + +This is done by either logging to stdout on rank 0, creating a JSON file for each rank, or both. + +The library also inserts [NVTX marks](https://github.com/NVIDIA/NVTX), which by default are a no-op, but when running with the NVIDIA Nsight profiler are useful to know where the code is spending time without relying on the slower torch profiler. + +## Enabling the library + +The library must be installed in the container or manually if not already available: + +```bash +pip install training-telemetry --index-url https://__token__:{gitlab_token}@gitlab-master.nvidia.com/api/v4/projects/166461/packages/pypi/simple +``` + +where `{gitlab_token}` is a GitLab token with read access to the [package registry](https://gitlab-master.nvidia.com/ai-efficiency/training_telemetry/-/packages). Anyone in NVIDIA with a GitLab account should have access to this package registry. Therefore, if you have an existing GitLab token, it should work. If you do not have one, follow these [instructions](https://docs.gitlab.com/user/profile/personal_access_tokens/) to create one. The token needs to have at least `read_repository` access, or higher `read` access. + +To enable the library, set this environment variable: + +```bash +export ENABLE_TELEMETRY=true +``` + +If this variable is set to true and the library is not installed, then an error will be logged, and telemetry will be disabled. + +To specify which telemetry backends to use, set the following environment variable with a list of comma separated backends: + +```bash +export TELEMETRY_BACKENDS=logger,nvtx,file +``` + +This will enable all 3 backends but by default, only the logger backend is enabled. + +It is recommended to enable training telemetry with the logger backend when running the training process with Heimdall, formerly known as APS. + +You should also consider enabling the nvtx backend when running the training process with NVIDIA Nsight Systems profiler. + +The file backend will make every rank generate a json file with telemetry events, which could be processed to extract KPIs for each rank. + +## Features + +The library uses the following mechanisms to intercept training events: + +* Top-level function decorator +* Context managers +* Callback implementation + +### Top-level function decorator + +This is defined in [telemetry.py](telemetry.py) and performs the library initialization. It also overrides the config to inject the callback. + +Most train.py files have already been annotated, but if any are missing, simply add `@telemetry.monitor` to the main launch functions. + +Training telemetry can be imported as: + +``` +from cosmos.utils.training_telemetry import telemetry +``` + +### Context managers + +They are defined in [context_managers.py](./context_managers.py) and are called by higher-level context managers that also wrap other telemetry code (one-logger and timers), defined in [../context_managers.py](../context_managers.py). They are as follows: + +* `data_loader_init`: can be used to wrap code that initializes the data loader +* `model_init`: can be used to wrap code that initializes a model +* `distributed_init`: can be used to wrap code that initializes distributed communication + +The context managers capture events that are not currently available in the callback. + +### Callback + +This is defined in [callback.py](./callback.py) and implements most of the functions defined by the Imaginaire4 callback to capture the remaining training events. + +## Training events + +The following training events are captured: + +* Application running time and other information: + * Timezone + * Node name + * World size + * Rank + * Total iterations + * Checkpoint strategy and other information + * More data can be added in [telemetry.py](telemetry.py) +* Data loader init +* Model init +* Distributed init +* Optimizer init +* Entire training loop duration +* Training step duration (NVTX only) +* Model forward (NVTX only) +* Model backward (NVTX only) +* Data loading (NVTX only) +* Training iterations - every `trainer.logging_iter` the following is logged: + * Avg iteration time + * Avg forward time + * Avg backward time + * Avg data loading time + * Current iteration + * Number of iterations since previous logging + * Loss + * Batch size + * FLOPS - TODO (?) +* Validation + * Total - logged + * Single step (NVTX only) +* Checkpoint load +* Checkpoint save + +## Output + +The library outputs to stdout along with the main logger on rank zero, but also to a separate `stdout.log` file in the `telemetry` folder in the main output location. + +The library also outputs one JSON file per rank in the `telemetry` folder in the main output location. In future, these json files could be ingested to be processed instead of logs, or analyzed to generate rank-level latency and other performance KPIs. + +As already mentioned, the library also inserts NVTX code marks. diff --git a/cosmos_training/cosmos/utils/training_telemetry/__init__.py b/cosmos_training/cosmos/utils/training_telemetry/__init__.py new file mode 100644 index 00000000..1600ad59 --- /dev/null +++ b/cosmos_training/cosmos/utils/training_telemetry/__init__.py @@ -0,0 +1,13 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. +# +# This codebase constitutes NVIDIA proprietary technology and is strictly +# confidential. Any unauthorized reproduction, distribution, or disclosure +# of this code, in whole or in part, outside NVIDIA is strictly prohibited +# without prior written consent. +# +# For inquiries regarding the use of this code in other NVIDIA proprietary +# projects, please contact the Deep Imagination Research Team at +# dir@exchange.nvidia.com. +# ----------------------------------------------------------------------------- diff --git a/cosmos_training/cosmos/utils/training_telemetry/__pycache__/__init__.cpython-312.pyc b/cosmos_training/cosmos/utils/training_telemetry/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e04b04cce4c1bc06b5c5e71094f2cd7d2c83e144 GIT binary patch literal 250 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 ArvLx| literal 0 HcmV?d00001 diff --git a/cosmos_training/cosmos/utils/training_telemetry/__pycache__/utils.cpython-312.pyc b/cosmos_training/cosmos/utils/training_telemetry/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8518317b2a630fe617827e40da83a7a701b9f981 GIT binary patch literal 7358 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

- New GitHub page for NVIDIA Cosmos:
- https://github.com/nvidia-cosmos -

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